FE-784: Petrinaut export: colour-fold per-slice subnet#160
FE-784: Petrinaut export: colour-fold per-slice subnet#160kostandinang wants to merge 5 commits into
Conversation
Fold the per-slice concrete subnet (slice:<sid>:*) N→1 in the Petrinaut export projection, carrying slice identity on the token colour instead of in the node id. Petrinaut's canvas is flat (no hierarchy/grouping), so collapsing the N structurally-identical slice subnets into one is the only way the imported net stays legible at scale — and it dissolves most of the per-slice naming problem. Projection only — runtime (petri-net.ts / net-compiler.ts) is untouched; the live event adapter maps concrete firings onto the same folded net. - petrinaut-fold.ts (new): pure fold rules. foldPlaceId strips slice:<sid>: (per-edge dep-signal:<dependent> places stay unique); foldTransitionId removes the owning slice-id segment; buildTransitionFoldMap collapses groups with identical folded shape but keeps divergent ones (dep-gated slice-ready, dep-signalling return-done) at concrete ids. Defines the SliceColour token type. - petrinaut-export.ts: serializeBlueprint folds places/transitions/arcs and the initial marking; adds tokenTypes + place typeId; schema 0.1.0 → 0.2.0. - petrinaut-events.ts: transition_fired / initial_marking fold concrete → folded ids (blueprint-derived map, with per-event fallback), keeping slice colour on the token. - SDCPN export stays count-fold (colorId: null) until Petrinaut supports discrete string token dimensions (H-6518/H-6519). net.json envelope is additive; folded counts pinned (depPlan 42→23 places, 37→21 transitions). Full gate green (check/test/build). Co-Authored-By: Claude <noreply@anthropic.com>
…stream The colour-fold was split across petrinaut-fold.ts (primitives), serializeBlueprint, and the event adapter, with the event stream capturing fold context as a side effect of emitInitialMarking (silently degrading if a firing arrived before it). Extract a single NetFolding object that owns the whole concrete→folded projection. - petrinaut-fold.ts: add createNetFolding(blueprint) → NetFolding with foldedPlaces / foldedTransitions / foldedMarking<T> / foldTransition / tokenTypes. The id maps stay private; foldPlaceId, foldTransitionId, collectSliceIds, buildTransitionFoldMap, foldedShapeSignature are no longer exported (no parallel API to drift from the folded net). - serializeBlueprint: consumes the folding; keeps schemaVersion, UUID mint, shortPlaceLabel, seedToToken. - createPetrinautEventStream: takes `folding` at construction; deletes the let sliceIds/transitionFoldMap capture and the per-event collectSliceIds fallback. foldedMarking<T> replaces the duplicated groupTokens + byPlace grouping in both consumers. - engine.ts threads one createNetFolding(blueprint) into the stream. Seam invariant: static net.json and the live stream fold identically because both derive from one NetFolding (engine-contract e2e covers it). Behaviour- preserving: export/events/engine-contract assertions unchanged; fold-rule tests re-expressed against the public surface. Full gate green. Co-Authored-By: Claude <noreply@anthropic.com>
The fold keeps divergent transitions concrete and folds uniform ones, but
nothing asserted *which* transitions diverge — a future compiler change that
made a uniform lifecycle transition split per slice would silently re-expand
the graph while reading as "fold worked".
Add an oracle over createNetFolding(compileTopology(depPlan)): the set of
foldedTransitions whose id still carries a slice-id segment must equal exactly
{slice-ready:slice-a, slice-ready:slice-b, slice-a:return-done,
slice-b:return-done}. Exact-set equality, so any extra divergence fails it.
Pure test addition. Full gate green (1504 tests).
Co-Authored-By: Claude <noreply@anthropic.com>
The fold's original motivation was clean SDCPN names — unfolded, every slice's `slice:slice-N:spec-ready` PascalCased to the same base and the name allocator appended collision counters (SliceSliceSpecReady2). Nothing tested that the fold dissolved this. Add oracles over toSdcpnFile(realNet(depPlan)): no place name carries an allocator digit suffix (pascalCaseLetters strips source digits, so any digit is a collision counter) and names are unique; the shared lifecycle place appears once and the 2-slice place count stays well under 2× the single-slice net; the file still satisfies the Petrinaut loader schema. Pure test addition. Gate green (orchestrator 161, check 0 errors, build ✓). Co-Authored-By: Claude <noreply@anthropic.com>
Brunch-owned fold identifiers were British (SLICE_COLOUR_TYPE, 'slice-colour', SliceColour) while the Petrinaut wire field is American (colorId). Rename to a single spelling matching the wire contract: SLICE_COLOR_TYPE / SLICE_COLOR_TYPE_ID / 'slice-color' / SliceColor, plus colour→color across the fold's comments. Petrinaut's own colorId field is unchanged. Add SPEC §Lexicon entries for the durable concepts: `color fold`, `token color`, `folded net`. Retire memory/CARDS.md — the FE-784 follow-up queue (#3, #4, #6) is exhausted. Pure rename + docs. Gate green (check 0 errors, orchestrator 161, build ✓). Co-Authored-By: Claude <noreply@anthropic.com>
PR SummaryMedium Risk Overview
Reviewed by Cursor Bugbot for commit 01f5f73. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 01f5f73. Configure here.
| transitionById.set(exportedId, { | ||
| id: exportedId, | ||
| inputs: [...new Set(t.inputs.map(foldPlaceId))], | ||
| outputs: [...new Set([...enumerateCandidateOutputs(t)].map(foldPlaceId))].sort(), |
There was a problem hiding this comment.
Folded arcs collapse multi-slice inputs
Medium Severity
When building folded transition arcs, createNetFolding dedupes folded place ids with Set, so epic-level transitions that consume one token from each slice’s same role (e.g. two completed places) export a single input arc instead of two. Static net.json understates firing arity versus the live stream, where foldedMarking still lists both tokens under completed.
Reviewed by Cursor Bugbot for commit 01f5f73. Configure here.
🤖 Augment PR SummarySummary: Implements FE-784 “color fold” for Petrinaut exports by collapsing per-slice subnets into a single slice-independent projection while preserving slice identity on per-token color. Changes:
Technical notes: Folding keeps certain dependency-gated transitions concrete (e.g. 🤖 Was this summary useful? React with 👍 or 👎 |
| * dependent id (they are genuinely per-edge, so they fold to a unique role). | ||
| * Epic, pool, and bare places are returned unchanged. | ||
| */ | ||
| function foldPlaceId(placeId: string): string { |
There was a problem hiding this comment.
src/orchestrator/src/petrinaut-fold.ts:176 — foldPlaceId collapses slice:<upstream>:dep-signal:<dependent> to dep-signal:<dependent>, which will collide when a slice has multiple upstream dependencies (distinct dep-signal edges fold into the same place id). This can make slice-ready:<dependent> require fewer dep signals than the concrete net in the folded export/event stream.
Severity: high
Other Locations
src/orchestrator/src/petrinaut-fold.ts:126src/orchestrator/src/petrinaut-fold.ts:205
🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.



What
Colour-fold the Petrinaut export so the per-slice subnet collapses N→1: one shared lifecycle carrying slice identity on the token colour instead of in the node id. Petrinaut's canvas is flat (no hierarchy/grouping), so fewer nodes is the only legibility lever — a 2-slice plan drops from 42→23 places and 37→21 transitions.
Projection only — the runtime (
petri-net.ts/net-compiler.ts) is untouched; the live event adapter maps concrete firings onto the same folded net.How (commit by commit)
serializeBlueprintfolds places (stripslice:<sid>:), transitions (collapse uniform lifecycle, keep divergent dep-gates concrete), and the initial marking;net.jsongainstokenTypes+ placetypeId(schema 0.1.0→0.2.0). Event stream maps concrete→folded, keeping slice identity on the token.NetFoldingextraction — onecreateNetFolding(blueprint)owns the whole concrete→folded projection; both the static export and the event stream consume it. The stream takes the folding at construction, removing a temporal-coupling footgun (fold context was previously captured as a side effect ofemitInitialMarking, silently degrading if a firing arrived first). Fold primitives are now private.slice-ready,return-done); anything else staying slice-scoped fails loudly instead of silently re-expanding the graph.SliceSliceSpecReady2): clean PascalCase names, one shared lifecycle place, sub-2× place count, loader schema still validates.color(matching Petrinaut'scolorIdwire field); SPEC §Lexicon gainscolor fold/token color/folded net.Fidelity & scope
Faithful mirror — the folded net is a re-encoding of the compiled blueprint, not a divergent view. SDCPN stays count-fold (
colorId: null) until Petrinaut ships discrete string token dimensions (H-6518/H-6519); at that point the colour type attaches with no topology change.Verification
npm run verifygreen throughout — fmt + lint (0 errors), full test suite, build. Behaviour-preserving refactor in commit 2 (export/events/engine-contract assertions unchanged); commits 3–4 are pure oracle additions.🤖 Generated with Claude Code