Skip to content

FE-784: Petrinaut export: colour-fold per-slice subnet#160

Open
kostandinang wants to merge 5 commits into
ka/fe-763-petri-event-streamfrom
ka/fe-784-petrinaut-colour-fold
Open

FE-784: Petrinaut export: colour-fold per-slice subnet#160
kostandinang wants to merge 5 commits into
ka/fe-763-petri-event-streamfrom
ka/fe-784-petrinaut-colour-fold

Conversation

@kostandinang
Copy link
Copy Markdown
Contributor

@kostandinang kostandinang commented Jun 1, 2026

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)

  1. Colour-fold projectionserializeBlueprint folds places (strip slice:<sid>:), transitions (collapse uniform lifecycle, keep divergent dep-gates concrete), and the initial marking; net.json gains tokenTypes + place typeId (schema 0.1.0→0.2.0). Event stream maps concrete→folded, keeping slice identity on the token.
  2. NetFolding extraction — one createNetFolding(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 of emitInitialMarking, silently degrading if a firing arrived first). Fold primitives are now private.
  3. Divergence oracle — asserts the only transitions kept concrete are the dependency gates (slice-ready, return-done); anything else staying slice-scoped fails loudly instead of silently re-expanding the graph.
  4. SDCPN naming oracle — confirms the fold dissolved the collision-counter naming (SliceSliceSpecReady2): clean PascalCase names, one shared lifecycle place, sub-2× place count, loader schema still validates.
  5. Naming alignment — brunch-owned identifiers use color (matching Petrinaut's colorId wire field); SPEC §Lexicon gains color 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 verify green 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

kostandinang and others added 5 commits June 1, 2026 16:18
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>
@cursor
Copy link
Copy Markdown

cursor Bot commented Jun 1, 2026

PR Summary

Medium Risk
Changes Petrinaut wire shape (schema 0.2.0, folded ids in events) and integration tests; execution semantics stay on the concrete net, but consumers of net.json/JSONL must accept the new projection rules.

Overview
Adds a Petrinaut-only color fold so multi-slice cook runs export one shared lifecycle graph instead of N duplicated slice:<sid>:* subnets. Slice identity moves onto token color (sliceId, epicId, budgets); node ids become role names like spec-ready and evaluate:dispatch.

createNetFolding(blueprint) in new petrinaut-fold.ts owns the full concrete→folded map (places, transitions, markings, foldTransition). Uniform transitions merge when folded shape matches; dependency gates (slice-ready:*, return-done) stay concrete when arcs differ. Runtime compiler/interpreter is unchanged—only serializeBlueprint, the live event adapter, and SDCPN projection consume the fold.

net.json bumps to schema 0.2.0: tokenTypes + SliceColor, optional place typeId. createPetrinautEventStream now requires folding at construction (fixes ordering footgun); initial marking and transition_fired use folded place/transition names while tokens keep slice fields. engine.ts threads one folding into the JSONL stream; export builds its own folding internally (same rules). Tests pin fold counts (e.g. depPlan 42→23 places), divergence set, SDCPN collision-free names, and e2e event names like evaluate:dispatch. Docs: SPEC lexicon (color fold, token color, folded net), PLAN FE-784; memory/CARDS.md removed as queue exhausted. Identifier spelling aligned to Petrinaut colorId (slice-color / SliceColor).

Reviewed by Cursor Bugbot for commit 01f5f73. Bugbot is set up for automated code reviews on this repo. Configure here.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 01f5f73. Configure here.

@augmentcode
Copy link
Copy Markdown

augmentcode Bot commented Jun 1, 2026

🤖 Augment PR Summary

Summary: 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:

  • Adds petrinaut-fold.ts with createNetFolding() (folded places/transitions/marking, transition id folding, and SliceColor token type)
  • Updates serializeBlueprint to export folded topology + initial marking, adds tokenTypes and optional place typeId, bumps schema to 0.2.0
  • Threads a single folding instance into the live Petrinaut event stream so exported net.json and emitted events fold identically
  • Adds focused fold-rule tests and “divergence oracle” tests to pin which transitions remain per-slice
  • Extends SDCPN export tests to validate naming/collision behavior on a real 2-slice folded net
  • Updates PLAN/SPEC lexicon entries and retires memory/CARDS.md

Technical notes: Folding keeps certain dependency-gated transitions concrete (e.g. slice-ready, return-done) while folding uniform lifecycle transitions and moving slice identity to token color dimensions.

🤖 Was this summary useful? React with 👍 or 👎

Copy link
Copy Markdown

@augmentcode augmentcode Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 1 suggestion posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

* 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 {
Copy link
Copy Markdown

@augmentcode augmentcode Bot Jun 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:126
  • src/orchestrator/src/petrinaut-fold.ts:205

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

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.

1 participant