feat(schemas): draw post-apply schema steps after implementation - #48
feat(schemas): draw post-apply schema steps after implementation#48nthansen wants to merge 16 commits into
Conversation
…ntation OpenSpec cannot express an artifact produced after implementation: an artifact's `requires` may name only other artifacts, and the CLI's graph pass dereferences each entry as a declared artifact. Authors point such an artifact at the last planning artifact and state the real ordering in prose, so spek levels it onto apply's own row and draws it as apply's peer. Surveying every schema discoverable on GitHub — 88 declaring an apply block, against the five in the official catalog — 18 declare post-implementation artifacts, four of which fail OpenSpec's own validation and never enumerate. This reverses a decision the spec recorded. The prior requirement forbade inferred edges because they show a constraint absent from `openspec status`. That reasoning holds for an edge indistinguishable from a declared one, but it treated the alternative as neutral, and it is not: drawing these steps beside apply asserts an interchangeability wrong in every real case. The ordering is stated as a resolved property carrying its source, not as an inference, so a later format that declares it satisfies the same requirement without changing it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
OpenSpec cannot express an artifact produced after implementation, so authors point one at the last planning artifact and state the ordering in prose. `postApplyArtifacts` recovers it from the graph alone: an artifact outside the transitive closure of `apply.requires` whose own closure covers everything apply requires cannot become available before apply does, and apply does not need it. Two guards, each for an input the schema survey produced. With no resolvable requirement the superset test is vacuously true and would report every artifact. Under a cycle the levels are declaration order rather than a reading of the graph, so there is no ordering to derive against — which needs the fallback reported, hence `levelArtifacts`. `resolveImplementationOrdering` is the seam: it answers, per step, the ordering and the source it came from. Only `derived` occurs today. A format that declares this adds a branch returning `declared`, per step, leaving the graph, the edge model and the view untouched. The apply step is a parameter rather than the literal id "apply", because a schema may declare an artifact of that name — `superspec` does. `computeArtifactLevels` keeps its signature and delegates: it is a published export, so widening it would break registry consumers for one in-repo caller. Verified against the 17 schemas resolvable on this machine: flags superpowers-bridge (verify, retrospective), anvil (verify), superspec (apply, verify, finalize) and spec-super (blackbox-test, a known false positive), and is a no-op on the other 13. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
…om id Apply was placed once the artifacts were levelled, so nothing could depend on it and a post-implementation step could only ever land beside it. It is now a node of the same graph, with an edge from it to each step `resolveImplementationOrdering` places after it. superpowers-bridge's `verify` moves from apply's own level to the one after. Every edge carries its origin. The view branches on that field rather than on which steps an edge connects, so an ordering that later arrives declared renders as an ordinary edge — proved by a test that feeds a schema naming the apply phase in its `requires` and needs no production code to pass. Steps now carry a key distinct from their declared id, because a schema may declare an artifact called `apply`. Keyed by id alone, it and the phase collapsed into one entry in every map in the layout, and connections resolved to whichever won. Keys are the declared id unless already claimed, so for every schema without a collision the key is the id and the drawn graph still reads in the schema's own vocabulary. Found while implementing: the declared branch must stand down when an artifact claims the phase's id. In superspec, `verify.requires: [apply]` names the declared artifact — a dependency the CLI resolves — so reading it as the phase would invent an edge the author never wrote. Archive's leaves are found through the drawn connections, so a derived ordering counts as a declared one does. superpowers-bridge's archive now waits on `retrospective` alone: apply feeds `verify`, so it is no longer a step nothing else requires. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
…rests on The apply-to-post-implementation connection is an ordering spek worked out, not one openspec blocks on, so it must not render as a declared edge. Dashed rather than a second hue: colour in this diagram already means selection, and a dash survives greyscale and a colour-blind reader. It is also the vocabulary the archive node already uses for "not declared by this schema", one level down from a node to a connection. Three places state what it rests on, at the depth each reader is at — the edge's own title, a legend entry shown only when the diagram contains one, and a callout in the selected step's detail giving the actual reasoning and saying plainly that openspec does not enforce it. Selection moves from declared id to step key throughout the page, since a schema with two steps sharing an id could otherwise select only one. Verified by screenshot in both themes across superpowers-bridge, anvil, superspec, spec-super and spec-driven. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
… note Reported as an extra space after "artifact's". There was none — the DOM carries exactly one space either side of the chip — but a chip's `px-1.5` padding stacks on the word space, and after a possessive, which ends tight, the pair reads as a double space. Fixed in the copy rather than the styling: each chip now sits after an em dash or a plain word, where the extra air looks deliberate. The chip utilities are the app's shared vocabulary, and a blanket rule for inline `code` would flatten every other deliberate chip with it. Tightened the sentence while there, since the callout sits in a narrow panel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
…w it `resolveImplementationOrdering` reports every step that follows implementation, so in superpowers-bridge both `verify` and `retrospective` carried a derived edge from apply. Retrospective reaches apply only through verify, so that edge states nothing the graph does not already say — and the detail panel repeated the whole "spek placed this here" explanation on a step whose position is a consequence of verify's rather than an inference of its own. Derived edges are now dropped where a longer path already entails them, after levelling from the full graph so the levels stay a guarantee rather than a coincidence. Declared edges are left whole: a schema's own claim is carried faithfully and reduced only for drawing, whereas a derived edge is spek's claim and spek should not claim what is already entailed. Two steps that independently follow apply each keep their edge, which is the other half of the rule and is pinned by its own test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
…emas Verified against the 17 schemas resolvable on this machine rather than fixtures. superpowers-bridge and anvil place apply before verify with exactly one dashed edge each; superspec renders both its declared `apply` artifact and the apply phase; spec-super's blackbox-test — a known false positive — reads as spek's inference rather than a declared dependency; spec-driven, nanopm and e2e-runbooks are untouched, as are 13 of the 17. CLI cost measured with a logging shim ahead of the real binary on PATH, not inferred: one `openspec schemas --json` for the whole list and one `schema which` per detail page, both unchanged. The branch adds no fetch, adapter call, effect or EventSource, so no page gained a request either. 4.7 stays open. The extension is built from this branch and installed, but this session is the remote half of an SSH session, so the VS Code UI is on the user's machine and cannot be driven from here. The half that can be checked mechanically is: both inline `code` elements added carry all four chip utilities and no bare `code` was added, which is the rule the webview's injected stylesheet breaks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
The one check with no browser equivalent — the host injects its own stylesheet, so a bare element is styled from its theme rather than ours. Confirmed against the real webview: the two chips in the derived-order callout match the pre-existing chips on the same panel. All 28 tasks complete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
…entory The change artifacts had recorded which schemas happened to resolve here — a count of 17, an installed extension id, the shape of this session. None of that is true of the repo, and a future reader cannot check any of it. Verification is now stated by the role each schema plays: a genuine post-implementation step, an artifact whose id collides with the apply phase, a known false positive, and unaffected controls. Which schemas resolve depends on what the CLI is configured with, so the count is gone and the CLI-cost claim is phrased per request rather than per schema total. Also removes the same slip from a shipped comment, where the legend's conditional cited "13 of the 17 schemas resolvable here" as its reason. Named schemas are kept where they are public artifacts anyone can install — superpowers-bridge, anvil, superspec — which is how CLAUDE.md and the existing spec already cite them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
Folds the delta into openspec/specs/schema-browsing/spec.md — one MODIFIED requirement, 10 scenarios added, none dropped — and moves the change under archive/. The main spec had carried the prohibition this change reversed, so until now it contradicted the shipped code. Records the two rules a future session would otherwise rediscover: that a post-implementation step is derived rather than declared, with the guards, the ~82% precision and the resolver seam that makes a declared source a branch; and that a workflow step is identified by its key, never by its declared id, because a schema may declare an artifact named `apply`. docs/prd.md gained a Workflow schemas entry. That view had shipped with no description at all — a pre-existing gap rather than one this change made, filled here so the docs describe master. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
Three defects from review. **A derived edge could suppress a declared one.** The transitive reduction was fed derived and declared edges alike, so an inference could imply away something the schema states — and it bit on exactly the schemas where the inference is wrong. In spec-super, `blackbox-test` declares `requires: [tasks]` and apply also requires `tasks`, so `tasks → apply ⇢ blackbox-test` implied the declared edge away, leaving a node whose only incoming line was dashed and captioned "openspec does not block on this" while the dependency openspec really does block on was drawn nowhere. A declared edge is now suppressed only by a path made entirely of declared edges. The asymmetry is the point: the reduction preserves information over facts, and a derived edge is not a fact. Derived edges are still suppressed by any implying path, since spek must not claim what the drawn graph entails. **The declared branch could build a cycle through apply.** A schema naming the apply phase in the `requires` of an artifact apply already waits on is rejected by the CLI but reaches spek, which parses schema.yaml directly. Levelling then fell back to positional for the whole schema and drew an edge running backwards up the diagram. Excluded the way `postApplyArtifacts` already excludes it. **A blank line before a requirement heading** was lost when the delta was synced — the only such heading in the file without one. superpowers-bridge now draws `plan → verify` alongside `apply ⇢ verify`, which is the visible cost and the honest reading: verify declares a dependency the CLI enforces, and spek separately places it after apply. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
From /simplify. Four review passes converged on the transitive reduction being one rule written as two halves in two packages: buildFlowSteps reduced derived edges against the full graph, layoutGraph reduced declared edges against a synthetic declared-only list, and neither file stated the whole rule. It is one rule with a parameter — an edge is redundant iff some path implies it whose weakest edge is at least as authoritative, ordering declared > derived. `drawableEdges` in core states that once; both callers now ask it, and layoutGraph goes back to being geometry. Also from the passes: `applyStepLevel` had lost its only caller while its rule lived on as an unnamed branch in the view, so buildFlowSteps delegates to it again; `requiresOf` and apply's closure were built twice per call, now shared via `applyContext`; the step-graph projection and the declared-edge mapping were each written twice; leaf detection was a nested scan; the reduction ran on every schema including the majority with nothing to reduce, and ran twice inside the two-pass buildEdges. The legend swatch was drawing a 7-unit arrowhead against the real 9 and its own copy of the dash — both now come from `ARROW_LEN` and `DERIVED_DASH`. The derived-edge wording existed in three hand-written variants across two components; `DERIVED_EDGE_MEANING` is the one home, the same reason `sourceTitle` gives for centralising. `withArchiveStep` rebuilt `requires` from leaf ids — the one namespace this change abandoned as ambiguous — so on superspec it could name a different step than the edge it drew. `postApplyArtifacts` and `ArtifactLevelling` left the published root index: an internal step of one rule and a type only reachable through its own function. Comments: 259 → 226 lines. What went was rationale repeated across source, test, spec and design doc — the superspec collision alone appeared in nine places. What stayed is the traps: why each guard exists, why levelling uses the full graph, why a derived edge may not imply a declared one away. Behaviour verified unchanged: edge sets identical on superpowers-bridge, spec-super, superspec, spec-driven and the no-resolvable-requires fallback, plus the rendered diagram across seven schemas. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
Two edges arriving at one node were slotted by the other end's x position alone. Two parents in the same column tie on that, so the order fell to whatever the edge list happened to hold — which put the long way round on the inside slot and crossed it over the short one. It was not only a crossing. With the detour forced to the inner slot the bow search had to swing wide enough that it exhausted LANE_MAX and gave up, so the curve was drawn through `apply` — the exact node it exists to go around. Ties now break on how far away the other end is, nearest first: a distant end is the one that has to travel around whatever sits between, so it takes the outer slot and stays outside the direct edge. In superpowers-bridge `plan → verify` now leaves and arrives on the right while `apply ⇢ verify` runs straight down the left. Measured: no edge in superpowers-bridge, anvil, spec-super, spec-driven or superspec now passes through an unrelated node, and the diagram is no wider. Found by looking at the rendered diagram, not by a test — so there is now a test asserting both halves. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XxjCm66xf19fcsD4jnntu4
…proposal The proposal for draw-post-apply-schema-steps specified that a step derived to follow apply draws a single incoming connection, from apply — the derived edge reduced like a declared one. 9490bb6 later reversed that (a declared edge suppressed only by an all-declared path), so anvil / superpowers-bridge `verify` drew two lines: a solid tasks/plan → verify and the dashed apply ⇢ verify. That extra solid edge is the regression this restores. - drawableEdges is a plain transitive reduction again: any path may imply an edge away, declared and derived counted alike. `verify` keeps one edge, from apply. Reverses 9490bb6's asymmetry only — its cycle guard and blank-line fix stay. - de8f6fa's converging-edge tiebreak stays (correct for any same-column tie); only its test is dropped, because the derived verify-pair it demonstrated no longer occurs once the redundant declared edge is reduced away. - Specs (main + archived delta), CLAUDE.md, and tests updated to match; two direct drawableEdges unit tests added (the function had none). Also reshapes the derived-step note (panel + tooltip) to state it plainly and mark it as spek's guess rather than a fact: "Assumed to run after apply. spek's guess, since it relies on the same artifacts apply does — not something openspec enforces." And fixes a webview-only rendering gap where a curly apostrophe (U+2019) fell back to a font that drew it with a wide bearing — now plain ASCII. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AkL1XCKmM9xejTshGfkEoZ
|
Thanks for this — the derived-ordering rule is carefully thought through, and I appreciate that the guards in 1.7 each name the input that motivated them. The Fourteen things below. Note first that the branch is behind 1.
Simplest fix is to reuse the existing 2.
This ships as a minor on the core line, so it's worth getting the surface right in one go. 3. The post-hoc apply-level override can contradict the graph
On the duplicate-id input the In every other case the override just recomputes the level the graph already produced. I think this block can go away entirely now that apply is levelled as a graph node; if it needs to stay, it should key by 4. Duplicate parallel edges survive the reduction and collide downstream
5. The archive step's displayed
6.
7. "why a derived edge may not imply a declared one away" is left over from 9490bb6, which d900865 reversed. 8. It says the diagram draws 9.
10.
11.
12. The same small graph is walked four times per render
13. The derived edge's
14. Three dash patterns for one meaning
Thanks again — happy to talk through any of these. |
What this does
Draws a schema step that runs after implementation (like
verify) after the apply node — as a dashed, inferred edge — instead of beside apply as a peer.Why
OpenSpec's
requires:can only name other artifacts. There is no way to say "runs after apply." So authors work around it: point the post-implementation step at the last planning artifact, and state the real order in prose.spek drew the declared graph literally, so the step landed on apply's level and read as its peer — inviting the exact mistake the schema's runtime precheck exists to block.
Simple example — superpowers-bridge
verifyin superpowers-bridge:requires: [plan]applyalso requiresplanBefore —
verifysits besideapply, looking interchangeable with implementation:After — spek infers
verifyruns after apply and draws it there, dashed:One edge into
verify, from apply. The declaredplan -> verifyis dropped: the pathplan -> apply -> verifyalready carries it, so drawing both says the same thing twice.(anvil's
verifyis the same shape — its instruction opens "Produced AFTER apply completes.")How spek decides
A step runs after implementation when both:
applyneeds, andapplyneeds nothing from it.~82% precise across 88 community schemas. It can misread a planning artifact that happens to depend on everything apply does — so the edge is dashed and marked a guess, never asserted as a rule
openspecblocks on.The note a reader sees
Click a derived step:
Notes for review
applyno longer collapses against the synthesized apply step (superspecdoes this).requiresgraph spek already has.resolveImplementationOrdering— a declared ordering renders as an ordinary solid edge, pinned by a test.type-check,lint, and the core / ui / web suites pass (340 / 24 / 182).🤖 Generated with Claude Code