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:
7
.changeset/fn-9095-spec-alignment-position.md
Normal file
7
.changeset/fn-9095-spec-alignment-position.md
Normal 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.
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user