Skip to content

feat(schemas): draw post-apply schema steps after implementation - #48

Open
nthansen wants to merge 16 commits into
spekhq:masterfrom
nthansen:feat/post-apply-schema-steps
Open

feat(schemas): draw post-apply schema steps after implementation#48
nthansen wants to merge 16 commits into
spekhq:masterfrom
nthansen:feat/post-apply-schema-steps

Conversation

@nthansen

Copy link
Copy Markdown
Contributor

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

verify in superpowers-bridge:

  • declares requires: [plan]
  • its instruction says it "MUST run on a completed implementation"
  • apply also requires plan

Beforeverify sits beside apply, looking interchangeable with implementation:

      plan
      /  \
   apply  verify      <- peers. wrong.

After — spek infers verify runs after apply and draws it there, dashed:

      plan
       |
     apply
       :               <- dashed = spek's guess, not enforced
     verify

One edge into verify, from apply. The declared plan -> verify is dropped: the path plan -> apply -> verify already carries it, so drawing both says the same thing twice.

(anvil's verify is the same shape — its instruction opens "Produced AFTER apply completes.")

How spek decides

A step runs after implementation when both:

  1. it needs everything apply needs, and
  2. apply needs 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 openspec blocks on.

The note a reader sees

Click a derived step:

Assumed to run after apply. spek's guess, since it relies on the same artifacts apply does — not something openspec enforces.

Notes for review

  • Breaking (view only): a schema declaring an artifact literally named apply no longer collapses against the synthesized apply step (superspec does this).
  • No new CLI call, no filesystem read — the rule reads the requires graph spek already has.
  • Forward path: when OpenSpec adds a way to declare this ordering (#1456), it is one branch in resolveImplementationOrdering — a declared ordering renders as an ordinary solid edge, pinned by a test.
  • Gates: type-check, lint, and the core / ui / web suites pass (340 / 24 / 182).

🤖 Generated with Claude Code

nthansen and others added 16 commits August 11, 2026 05:40
…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
@kewang

kewang commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

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 key-vs-id split in schemaView.ts is the right call, and the drawableEdges origin-aware reduction reads well.

Fourteen things below. Note first that the branch is behind master, and master requires branches to be up to date before merging — so a merge from master is needed either way, and it will surface the first item.

1. border-accent/30 will fail the contrast guard once master is merged in

packages/web/src/components/SchemaFlow.tsx:105 (and three sibling spots) adds border-accent/30. master's packages/web/src/styles/contrast.test.ts scans every packages/web/src/**/*.tsx for border-<token>/<alpha> and fails on any pair that is neither measured nor listed in DECORATIVE_BORDERS — which today holds only accent/40 and status-warning/40. So Node gates goes red the moment the branch is up to date, and it can't reproduce on the branch as it stands.

Simplest fix is to reuse the existing accent/40; otherwise add "accent/30" to DECORATIVE_BORDERS with the reason (the callout's own text names the state, which is the exemption the table is for).

2. postApplyArtifacts and the new types are missing from the package index

packages/core/src/index.ts:95-102 exports levelArtifacts and resolveImplementationOrdering (plus type OrderingSource), but:

  • postApplyArtifacts isn't there at all, so task 1.5's "and the package index" is only half done.
  • ArtifactLevelling, RequiresNode, OriginEdge and OriginNode aren't exported either. A consumer who does import { levelArtifacts } from "@spekjs/core" can't name the return type or resolveImplementationOrdering's second parameter without reaching into the subpath.

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

packages/web/src/utils/schemaView.ts:202-208 recomputes apply's level from new Map(steps.filter(s => !s.isApply).map(s => [s.id, s.level])) — keyed by declared id, after keyOf/incoming have already resolved edges by key.

On the duplicate-id input the key indirection exists for, the two disagree. With artifacts = [{id:"p"}, {id:"x", requires:["p"]}, {id:"x", requires:[]}] and apply.requires = ["x"], the graph levels apply at 3 (its edge came from the first x), then the override recomputes from {p:1, x:1} — the second x won the map — and drops apply to 2. Apply then sits in the same row as its own parent, and layoutGraph draws an arrow with dy <= 0.

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

4. Duplicate parallel edges survive the reduction and collide downstream

packages/core/src/schema-flow.ts:287 — a schema listing the same id twice in one step's requires produces two FlowEdges with the same from, and impliedBy strips both direct hops from its start stack so neither is judged implied. layoutGraph's anchor() keys its slot map by "${parent.key}->${child.key}", so both get the same p0/p3 and the same path string, and SchemaGraph renders two <path> siblings with the same React key (console warning, stacked arrowheads). Inherited from drawableRequires, but it comes along into the new path.

5. The archive step's displayed requires can list the same name twice

packages/web/src/utils/schemaView.ts:246 uses leaves.map((s) => s.id) while incoming correctly uses s.key. A schema with a declared artifact named apply that nothing requires, alongside an apply phase that nothing requires, has both as leaves — the detail panel then reads requires apply, apply, and so does the <title>. The comment just above already acknowledges that a leaf's id doesn't always name the step the edge points at; the list could dedupe or disambiguate.

6. closureOf's doc promises something the code doesn't do

packages/core/src/schema-flow.ts:97 says "including the seeds themselves", but the loop's !requiresOf.has(current) guard continues before seen.add(current). Both current call sites pre-filter, so nothing is wrong today — but the obvious next caller is one seeding with a raw apply.requires, which is exactly the field the doc talks about, and it would silently change what the resolved.every(id => needs.has(id)) superset test means.

7. schemaLayout.ts:257's comment now says the opposite of the code

"why a derived edge may not imply a declared one away" is left over from 9490bb6, which d900865 reversed. drawableEdges' own doc now says declared and derived hops are counted alike, and there's a test pinning it (layoutGraph: a redundant declared edge is reduced away by the derived path). As written, the comment invites someone to "fix" the behaviour back.

8. CLAUDE.md:217 still points at the dead function

It says the diagram draws drawableRequires, which after this PR has no production caller; the schema-flow export list a few lines up (:204) lists drawableRequires but not drawableEdges. Since CLAUDE.md is meant to describe master's implementation, both send a reader to the wrong place.

9. DERIVED_EDGE_MEANING has three surfaces but only two readers

packages/web/src/utils/schemaView.ts:80's comment says three surfaces read it, but only two do (SchemaGraph.tsx:123 and the legend at SchemaFlow.tsx:217). The selected-step detail at SchemaFlow.tsx:106-111 spells the sentence out inline. That's the most-read of the three, and the drift the constant exists to prevent — especially given the comment anticipates editing it when OpenSpec #1456 lands.

10. drawableEdges and drawableRequires are one traversal written twice

packages/core/src/schema-flow.ts:257 and :314 build the same childrenOf map, run the same "reachable without the direct hop" DFS with the same seen guard, and apply the same declared.has(...) && !implied(...) filter. Only drawableEdges is reachable from the app, yet the eight tests (cycle termination, undeclared-entry drop, declared-order preservation, levelling invariance) all sit on drawableRequires and no longer exercise the live rule. drawableRequires can delegate — wrap each id as {from, origin: "declared"}, map the result back — leaving one traversal and one test suite over the code that runs.

11. OriginEdge.from changes meaning halfway through drawableEdges

packages/core/src/schema-flow.ts:263 stores outgoing adjacency in an OriginEdge, whose from is documented as the prerequisite — so inside childrenOf it holds the successor, and impliedBy's e.from !== to reads backwards. A { to, origin } shape, or a plain Map<string, string[]> for adjacency, removes the trap at no cost.

12. The same small graph is walked four times per render

resolveImplementationOrdering builds applyContext at :183; postApplyArtifacts rebuilds the identical requiresOf/beforeApply and additionally runs a full levelArtifacts(artifacts) at :141 purely for the cyclic flag; buildFlowSteps runs levelArtifacts again on the step graph, and drawableEdges runs once there and once in layoutGraph. applyContext exists precisely so "the two rules cannot disagree" — hoisting it and the cyclicity answer up and passing them down keeps that guarantee and collapses the traversals.

13. The derived edge's <title> is hard to reach

packages/web/src/components/SchemaGraph.tsx:123 — hit-testing on a fill="none" path covers only the painted stroke, and stroke-dasharray="5 4" removes 44% of a 1.5px line. Under max-w-full on a phone the target is sub-pixel. A transparent wide companion path with pointer-events: stroke would make it reachable; otherwise the meaning really only lives in the legend and the detail panel.

14. Three dash patterns for one meaning

SchemaGraph.tsx:120 uses DERIVED_DASH = "5 4" under a comment about sharing the archive node's vocabulary for "not declared by this schema", the archive node at :165 hardcodes "4 3", and the legend swatch (SchemaFlow.tsx:202) uses Tailwind's border-dashed (3px 3px). The dash is the only non-colour cue carrying that distinction, so either export the archive pattern alongside DERIVED_DASH and share it, or drop the "same vocabulary" claim.

Thanks again — happy to talk through any of these.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants