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.
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
<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 && (
|
||||
<div className="detail-section detail-retries-section">
|
||||
<div className="detail-source-header">
|
||||
@@ -7001,6 +6958,50 @@ export function TaskDetailContent({
|
||||
</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 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(
|
||||
<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