From b26894e0dd1116d3875f77876084fe50ac8be31a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 15 Aug 2026 06:09:09 -0700 Subject: [PATCH] FN-9095: move spec alignment details to Definition tab bottom Place low-frequency spec alignment provenance after plan and relationship content.\n\n- Move the Definition-tab spec-lock report to the final section.\n- Cover populated, unavailable, and absent spec-lock report states.\n- Document the placement and add a patch changeset.\n\nFiles changed:\n .changeset/fn-9095-spec-alignment-position.md | 7 ++\n docs/dashboard-guide.md | 2 +-\n .../dashboard/app/components/TaskDetailModal.tsx | 87 +++++++++++-----------\n .../__tests__/TaskDetailModal.spec-lock.test.tsx | 63 ++++++++++++++++\n 4 files changed, 115 insertions(+), 44 deletions(-) Fusion-Task-Id: FN-9095 Fusion-Task-Lineage: c017325c-b7c1-4f0e-abab-49462903a778 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-9095-spec-alignment-position.md | 7 ++ docs/dashboard-guide.md | 2 +- .../app/components/TaskDetailModal.tsx | 87 ++++++++++--------- .../TaskDetailModal.spec-lock.test.tsx | 63 ++++++++++++++ 4 files changed, 115 insertions(+), 44 deletions(-) create mode 100644 .changeset/fn-9095-spec-alignment-position.md diff --git a/.changeset/fn-9095-spec-alignment-position.md b/.changeset/fn-9095-spec-alignment-position.md new file mode 100644 index 0000000000..67fe15da1b --- /dev/null +++ b/.changeset/fn-9095-spec-alignment-position.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Show Task Detail plan content before low-frequency spec alignment provenance. +category: fix +dev: Moves the shared Definition-tab spec-lock report after task relationship sections. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index a2c872740a..4f8203e9db 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -2439,7 +2439,7 @@ In **Settings → Merge**, pull-request mode offers **Required pull-request chec Activity includes durable and ephemeral agent sessions from heartbeat, executor, workflow-step, triage, reviewer, and merger lanes. **Sessions** is the sum of CLI session rows and `usage_events` session-start rows in the `agent-session` category; each session class has one writer. Human chat and mailbox turns supply `user_message` events. Active nodes remains zero on a single-node installation when no mesh routing node id exists. ## Plan alignment in Task Detail -The shared Task Detail Definition view shows the persisted spec alignment, latest lock/current-plan versions, and deterministic finding categories. `activeLock` is derived from the live approval fingerprint and current-plan hash; an unavailable or inactive lock is not presented as on-plan. A historical report from a prior lock or plan revision stays in retained history and displays as unavailable until a matching current report exists. Findings are structural; `mission-statement` identifies a changed Mission narrative hash without displaying or judging its prose. The same shared content is used by modal and right-dock task detail hosts. +The shared Task Detail Definition view shows the persisted spec alignment, latest lock/current-plan versions, and deterministic finding categories at the bottom, below plan content and task-relationship sections. `activeLock` is derived from the live approval fingerprint and current-plan hash; an unavailable or inactive lock is not presented as on-plan. A historical report from a prior lock or plan revision stays in retained history and displays as unavailable until a matching current report exists. Findings are structural; `mission-statement` identifies a changed Mission narrative hash without displaying or judging its prose. The same shared content is used by modal and right-dock task detail hosts. ### Promote release-gate enrichment diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index 13eca4d8b7..b16fdd1074 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -6183,49 +6183,6 @@ export function TaskDetailContent({ ) : ( <> {/* FNXC:TaskDetailSummaryTab 2026-07-29-00:00: FN-8197 keeps Definition focused on plan, retry, and source metadata; completed merge metadata renders exclusively in the done-only Summary tab. */} - {specLock && ( -
-
-
- Spec alignment - {specLock.report?.alignment ?? "unavailable"} -
-
-
-
Latest lock
v{specLock.latestLock?.version ?? "—"}
-
Current plan
v{specLock.currentPlan?.version ?? "—"}
-
Lock state
{specLock.activeLock ? "active" : "inactive"}
-
Findings
{specLock.report?.findings.length ?? 0}
-
- {specLock.latestLock && ( -

- Accepted {specLock.latestLock.acceptedAt} · plan hash {specLock.latestLock.currentPlanHash} · approval {specLock.latestLock.approvalFingerprint} -

- )} - {specLock.currentPlan && ( -

- Captured {specLock.currentPlan.capturedAt} · source revision {specLock.currentPlan.sourceRevision} · source hash {specLock.currentPlan.sourceHash} -

- )} - {specLock.latestLock?.diff?.changedSections.length ? ( -

Re-lock changed: {specLock.latestLock.diff.changedSections.join(", ")}

- ) : null} - {(specLock.history?.locks.length ?? 0) > 1 || (specLock.history?.currentPlans.length ?? 0) > 1 || (specLock.history?.reports.length ?? 0) > 1 ? ( -

- Retained history: {specLock.history.locks.map((lock) => `lock v${lock.version}`).join(", ") || "no locks"}; {specLock.history.currentPlans.map((plan) => `plan v${plan.version}`).join(", ") || "no plan evidence"}; {specLock.history.reports.length} reports -

- ) : null} - {specLock.report?.findings.length ? ( -
    - {specLock.report.findings.map((finding, index) => ( -
  • - {finding.kind}: {finding.category}{finding.path ? ` (${finding.path})` : ""} -
  • - ))} -
- ) : null} -
- )} {(retrySummary?.total ?? 0) > 0 && (
@@ -7001,6 +6958,50 @@ export function TaskDetailContent({
); })()} + {/* FNXC:SpecLockTaskDetail 2026-08-15-12:54: Spec alignment is low-frequency lock/hash provenance, so it renders LAST in the Definition (Plan) tab — operators opening Plan must see plan content first, not the alignment report. Keep this block at the tail of the Definition fragment. */} + {specLock && ( +
+
+
+ Spec alignment + {specLock.report?.alignment ?? "unavailable"} +
+
+
+
Latest lock
v{specLock.latestLock?.version ?? "—"}
+
Current plan
v{specLock.currentPlan?.version ?? "—"}
+
Lock state
{specLock.activeLock ? "active" : "inactive"}
+
Findings
{specLock.report?.findings.length ?? 0}
+
+ {specLock.latestLock && ( +

+ Accepted {specLock.latestLock.acceptedAt} · plan hash {specLock.latestLock.currentPlanHash} · approval {specLock.latestLock.approvalFingerprint} +

+ )} + {specLock.currentPlan && ( +

+ Captured {specLock.currentPlan.capturedAt} · source revision {specLock.currentPlan.sourceRevision} · source hash {specLock.currentPlan.sourceHash} +

+ )} + {specLock.latestLock?.diff?.changedSections.length ? ( +

Re-lock changed: {specLock.latestLock.diff.changedSections.join(", ")}

+ ) : null} + {(specLock.history?.locks.length ?? 0) > 1 || (specLock.history?.currentPlans.length ?? 0) > 1 || (specLock.history?.reports.length ?? 0) > 1 ? ( +

+ Retained history: {specLock.history.locks.map((lock) => `lock v${lock.version}`).join(", ") || "no locks"}; {specLock.history.currentPlans.map((plan) => `plan v${plan.version}`).join(", ") || "no plan evidence"}; {specLock.history.reports.length} reports +

+ ) : null} + {specLock.report?.findings.length ? ( +
    + {specLock.report.findings.map((finding, index) => ( +
  • + {finding.kind}: {finding.category}{finding.path ? ` (${finding.path})` : ""} +
  • + ))} +
+ ) : null} +
+ )} )} diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.spec-lock.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.spec-lock.test.tsx index 6b2d4132d2..49e0b8f69f 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.spec-lock.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.spec-lock.test.tsx @@ -75,6 +75,69 @@ describe("TaskDetailModal spec-lock report", () => { expect(report).toHaveTextContent("source revision 42"); expect(report).toHaveTextContent("source hash source-hash"); expect(report).toHaveTextContent("Retained history: lock v1; plan v1, plan v2; 2 reports"); + for (const heading of ["Progress", "Original prompt", "Dependencies", "Blocking"]) { + const anchor = screen.getByText(heading, { exact: true }); + expect(anchor.compareDocumentPosition(report) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + } await waitFor(() => expect(fetchSpecLock).toHaveBeenCalledWith("FN-099", undefined)); }); + + it("keeps an unavailable all-null report at the end without rendering findings", async () => { + const { fetchSpecLock } = await import("../../api"); + vi.mocked(fetchSpecLock).mockResolvedValue({ + latestLock: null, + activeLock: null, + currentPlan: null, + report: null, + latestReport: null, + history: { locks: [], currentPlans: [], reports: [] }, + }); + + render( + , + ); + + const report = await screen.findByTestId("spec-lock-report"); + expect(report).toHaveTextContent("unavailable"); + expect(report).toHaveTextContent("v—"); + expect(report.querySelector(".spec-lock-findings")).toBeNull(); + const blocking = screen.getByText("Blocking", { exact: true }); + expect(blocking.compareDocumentPosition(report) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + }); + + it("does not leave a spec-lock shell when no spec-lock data exists", async () => { + const { fetchSpecLock } = await import("../../api"); + vi.mocked(fetchSpecLock).mockResolvedValue(null as never); + + render( + , + ); + + await waitFor(() => expect(fetchSpecLock).toHaveBeenCalledWith("FN-099", undefined)); + expect(screen.queryByTestId("spec-lock-report")).toBeNull(); + expect(screen.getByText("Dependencies", { exact: true })).toBeInTheDocument(); + expect(screen.getByText("Blocking", { exact: true })).toBeInTheDocument(); + }); });