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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-08-15 06:09:09 -07:00
parent 8d8ea26cb2
commit b26894e0dd
4 changed files with 115 additions and 44 deletions

View File

@@ -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.

View File

@@ -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. 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 ## 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 ### Promote release-gate enrichment

View File

@@ -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. */} {/* 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 && (
<section className="detail-section spec-lock-report" data-testid="spec-lock-report" aria-label="Spec lock alignment">
<div className="detail-source-header">
<div className="detail-source-summary">
<span className="detail-source-label">Spec alignment</span>
<span className="badge">{specLock.report?.alignment ?? "unavailable"}</span>
</div>
</div>
<dl className="detail-source-grid">
<div><dt>Latest lock</dt><dd>v{specLock.latestLock?.version ?? "—"}</dd></div>
<div><dt>Current plan</dt><dd>v{specLock.currentPlan?.version ?? "—"}</dd></div>
<div><dt>Lock state</dt><dd>{specLock.activeLock ? "active" : "inactive"}</dd></div>
<div><dt>Findings</dt><dd>{specLock.report?.findings.length ?? 0}</dd></div>
</dl>
{specLock.latestLock && (
<p className="spec-lock-provenance">
Accepted {specLock.latestLock.acceptedAt} · plan hash {specLock.latestLock.currentPlanHash} · approval {specLock.latestLock.approvalFingerprint}
</p>
)}
{specLock.currentPlan && (
<p className="spec-lock-provenance">
Captured {specLock.currentPlan.capturedAt} · source revision {specLock.currentPlan.sourceRevision} · source hash {specLock.currentPlan.sourceHash}
</p>
)}
{specLock.latestLock?.diff?.changedSections.length ? (
<p className="spec-lock-provenance">Re-lock changed: {specLock.latestLock.diff.changedSections.join(", ")}</p>
) : null}
{(specLock.history?.locks.length ?? 0) > 1 || (specLock.history?.currentPlans.length ?? 0) > 1 || (specLock.history?.reports.length ?? 0) > 1 ? (
<p className="spec-lock-provenance">
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
</p>
) : null}
{specLock.report?.findings.length ? (
<ul className="spec-lock-findings">
{specLock.report.findings.map((finding, index) => (
<li key={`${finding.kind}:${finding.category}:${finding.path ?? index}`}>
{finding.kind}: {finding.category}{finding.path ? ` (${finding.path})` : ""}
</li>
))}
</ul>
) : null}
</section>
)}
{(retrySummary?.total ?? 0) > 0 && ( {(retrySummary?.total ?? 0) > 0 && (
<div className="detail-section detail-retries-section"> <div className="detail-section detail-retries-section">
<div className="detail-source-header"> <div className="detail-source-header">
@@ -7001,6 +6958,50 @@ export function TaskDetailContent({
</div> </div>
); );
})()} })()}
{/* 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 && (
<section className="detail-section spec-lock-report" data-testid="spec-lock-report" aria-label="Spec lock alignment">
<div className="detail-source-header">
<div className="detail-source-summary">
<span className="detail-source-label">Spec alignment</span>
<span className="badge">{specLock.report?.alignment ?? "unavailable"}</span>
</div>
</div>
<dl className="detail-source-grid">
<div><dt>Latest lock</dt><dd>v{specLock.latestLock?.version ?? "—"}</dd></div>
<div><dt>Current plan</dt><dd>v{specLock.currentPlan?.version ?? "—"}</dd></div>
<div><dt>Lock state</dt><dd>{specLock.activeLock ? "active" : "inactive"}</dd></div>
<div><dt>Findings</dt><dd>{specLock.report?.findings.length ?? 0}</dd></div>
</dl>
{specLock.latestLock && (
<p className="spec-lock-provenance">
Accepted {specLock.latestLock.acceptedAt} · plan hash {specLock.latestLock.currentPlanHash} · approval {specLock.latestLock.approvalFingerprint}
</p>
)}
{specLock.currentPlan && (
<p className="spec-lock-provenance">
Captured {specLock.currentPlan.capturedAt} · source revision {specLock.currentPlan.sourceRevision} · source hash {specLock.currentPlan.sourceHash}
</p>
)}
{specLock.latestLock?.diff?.changedSections.length ? (
<p className="spec-lock-provenance">Re-lock changed: {specLock.latestLock.diff.changedSections.join(", ")}</p>
) : null}
{(specLock.history?.locks.length ?? 0) > 1 || (specLock.history?.currentPlans.length ?? 0) > 1 || (specLock.history?.reports.length ?? 0) > 1 ? (
<p className="spec-lock-provenance">
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
</p>
) : null}
{specLock.report?.findings.length ? (
<ul className="spec-lock-findings">
{specLock.report.findings.map((finding, index) => (
<li key={`${finding.kind}:${finding.category}:${finding.path ?? index}`}>
{finding.kind}: {finding.category}{finding.path ? ` (${finding.path})` : ""}
</li>
))}
</ul>
) : null}
</section>
)}
</> </>
)} )}
</> </>

View File

@@ -75,6 +75,69 @@ describe("TaskDetailModal spec-lock report", () => {
expect(report).toHaveTextContent("source revision 42"); expect(report).toHaveTextContent("source revision 42");
expect(report).toHaveTextContent("source hash source-hash"); expect(report).toHaveTextContent("source hash source-hash");
expect(report).toHaveTextContent("Retained history: lock v1; plan v1, plan v2; 2 reports"); 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)); 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(
<TaskDetailContent
initialTab="definition"
active
task={makeTask()}
onRequestClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={vi.fn()}
onTaskUpdated={vi.fn()}
/>,
);
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(
<TaskDetailContent
initialTab="definition"
active
task={makeTask()}
onRequestClose={noop}
onMoveTask={noopMove}
onDeleteTask={noopDelete}
onMergeTask={noopMerge}
onOpenDetail={noopOpenDetail}
addToast={vi.fn()}
onTaskUpdated={vi.fn()}
/>,
);
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();
});
}); });