Skip to content

Commit 6ebcd23

Browse files
committed
spec(fn-154): chart - reopened charts regain a capture door
Dogfood finding from MergeFoundry's chart consumer (fn-147, built against 3.13.0): after reopen stales every briefing, re-running 'chart briefing' with an unchanged proposal fingerprint idempotently returns the STALE briefing - a briefable chart with no capture-ready door and no hint that a changed proposal is needed. Spec: briefing on a briefable chart must always end final (restore or mint B(n+1), plan-time call), with an explicit outcome discriminator; normal idempotence and --force drafts byte-unchanged.
1 parent 42df78c commit 6ebcd23

2 files changed

Lines changed: 62 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"branch_name": "fn-154-chart-reopened-charts-regain-a-capture",
3+
"created_at": "2026-08-01T14:54:24.147964Z",
4+
"depends_on_epics": [],
5+
"id": "fn-154-chart-reopened-charts-regain-a-capture",
6+
"next_task": 1,
7+
"plan_review_status": "unknown",
8+
"plan_reviewed_at": null,
9+
"spec_path": ".flow/specs/fn-154-chart-reopened-charts-regain-a-capture.md",
10+
"status": "open",
11+
"title": "chart: reopened charts regain a capture door (briefing re-finalize after reopen)",
12+
"tracker": {
13+
"baseHashFlow": null,
14+
"baseHashTracker": null,
15+
"depRelations": [],
16+
"id": null,
17+
"identifier": null,
18+
"lastSyncedAt": null,
19+
"mergeBaseFlow": null,
20+
"mergeBaseTracker": null,
21+
"url": null
22+
},
23+
"updated_at": "2026-08-01T14:54:54.355933Z"
24+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# chart: reopened charts regain a capture door (briefing re-finalize after reopen)
2+
3+
## Goal & Context
4+
<!-- scope: business -->
5+
6+
Found while dogfooding chart (3.13.0) as a downstream consumer: MergeFoundry's cockpit built a chart decision-map surface whose capture handoff renders ONLY from a `final` briefing (draft/stale never — their reviewer treated anything else as a stale-masquerade class). Driving the real binary through lifecycle edges surfaced one state where the CLI strands the operator:
7+
8+
**Repro (verified against the released 3.13.0 binary):**
9+
1. `chart create``add-decision``resolve``briefing` — briefing B1 goes `final`, chart → `done`.
10+
2. `chart reopen --reason "more work needed"` — chart → `open`, B1 correctly staled.
11+
3. Settle the remaining decisions so the chart is `briefable: true` again.
12+
4. Run `chart briefing` with an **unchanged proposal fingerprint** → the command is idempotent on the fingerprint and returns the existing **stale** B1 (`status: "stale"`, `chart_status: "open"`). It does not re-finalize, does not mint B2, and prints no hint that a changed proposal is required.
13+
14+
Result: a `briefable: true` chart with **no capture-ready briefing and no path to one** short of the operator guessing that the proposal content must differ. Downstream surfaces that (correctly) gate capture on `final` render this honestly as inactive-with-no-door — but the door itself is upstream's to restore. The fingerprint idempotence is right for the normal case (double-invocation must not mint duplicate briefings); it is wrong when every briefing the fingerprint matches is stale.
15+
16+
## Scope
17+
<!-- scope: technical -->
18+
19+
1. **Re-finalize on stale-only match.** When `chart briefing` finds an existing briefing by fingerprint AND that briefing's status is `stale` AND the chart is currently `briefable`, the match must produce a capture-ready outcome instead of a silent stale echo. Decide the mechanism at plan time — the two candidates:
20+
- (a) restore the matched briefing to `final` (and chart → `done`), on the argument that an unchanged proposal over a re-settled ledger is the same conclusion re-reached; or
21+
- (b) mint the next briefing id (`B(n+1)`) as `final` with the same content, preserving the staled one as history.
22+
Either way the invariant becomes: **`briefing` on a briefable chart always ends with a final briefing** (or a hard error naming why not).
23+
2. **Honest message on the idempotent path.** Whatever mechanism lands, the non-JSON output and the `--json` envelope must say what happened (`refinalized` / `superseded_stale` / equivalent), never silently return a stale object as if it were the requested outcome.
24+
3. **Downstream-visible signal.** `chart show --json` already carries per-briefing `status`; ensure whatever new state transitions exist keep that field authoritative so read-only consumers need no new logic.
25+
4. **Docs**: the chart skill's reopen section documents the re-finalize behavior; the `--force` draft path is untouched (drafts remain "never capture-ready").
26+
27+
## Boundaries / non-goals
28+
29+
- No change to fingerprint idempotence for the normal case (matching a `final` or `draft` briefing keeps today's behavior).
30+
- No change to `reopen` staling semantics — staling on reopen is correct and is what made the downstream honesty rendering possible.
31+
- No new flags required; this is the existing command doing the right thing on an edge.
32+
33+
## Acceptance Criteria
34+
35+
- **R1:** The repro above ends with a capture-ready (`final`) briefing: reopen → re-settle → `briefing` with an unchanged proposal yields a final briefing (restored or minted per the plan-time decision), chart status consistent with it (tested against the real CLI).
36+
- **R2:** The stale-echo path is gone: no invocation of `briefing` on a `briefable` chart can return only stale briefings without either producing a final one or erroring with a message that names the fingerprint idempotence and the remedy.
37+
- **R3:** `--json` envelopes for the new path carry an explicit outcome discriminator; per-briefing `status` in `chart show --json` remains the single source of truth for capture-readiness (no consumer-side inference needed).
38+
- **R4:** Draft (`--force`) semantics and normal idempotence are byte-unchanged (regression-tested); reopen staling untouched; docs updated in the same change.

0 commit comments

Comments
 (0)