You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Project flag fields as flat string arrays in canonical JSON
A flag-word spec entry now surfaces in canonical-doc as a flat sorted
`string[]` instead of a `{flags, flagsRaw}` wrapper. Each entry is either
a canonical slug for a named set bit or `bit<N>` for set bits the spec
table doesn't name. Canonical sort: named slugs alphabetical first, then
`bit<N>` numerically.
Why: the wrapper had a redundant key collision when the parent field
itself was named `flags` (e.g. `header.flags = {flags: [...], flagsRaw: ...}`),
the wrapper-vs-array split made named and unnamed bits diff differently,
and `flagsRaw` was hostile to hand-edits since toggling one bit required
recomputing the hex. The flat array gives uniform diffs across named and
unnamed bits, makes hand-edits one entry per bit, and preserves the
strict-disjoint invariant via per-element refines (a `bit<N>` whose
position falls inside the named-mask is rejected at both the schema
layer and the wire boundary, and `bit<N>` with N >= codec width is also
rejected).
`slugifyCodedName` rejects display strings whose slug would collide with
the `bit<N>` sentinel namespace, reserving it for the projection's
synthesized entries.
This is a wire-shape change for `*.pro.json` snapshots. Snapshots
produced by previous versions need to be re-saved through this version's
CLI before they can be loaded.
Copy file name to clipboardExpand all lines: binary/INTERNALS.md
+5-5Lines changed: 5 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -321,15 +321,15 @@ These are non-negotiable across PRO and MAP:
321
321
5.**Linked structures.** Same-struct: array length drives count via `enforceLinkedCounts(spec, doc)` + zod refinement. Cross-struct (`fromCtx`): orchestrator owns the binding; the count flows in via the read-time ctx.
322
322
6.**No work-time artifacts in the repo.** Exception: `tmp/` (in `.gitignore`).
323
323
324
-
7.**Sorted-array projection for flag fields.** A flag-word spec entry (`{codec, flags: Table}`) surfaces in canonical-doc as a `{flags: string[], flagsRaw?: hexString}` strict-object wrapper, not as the raw int. `flags` lists the slugified-camelCase names of every set bit, alphabetically sorted; toggling one bit adds or removes one entry. `flagsRaw` is an optional hex-string reservoir for unnamed bits in the wire word, omitted when every set bit has a name. Strict-disjoint invariant: named bits never appear in `flagsRaw`, enforced at the wire boundary by `flagArrayToInt`. `compileFlagTable` slugifies the table's display strings to camelCase canonical keys (`"NoBlock"` -> `noBlock`); `intToFlagArray` / `flagArrayToInt` translate at the wire codec boundary via the `FlagArraySchema` wrapper.
324
+
7.**Flat-array projection for flag fields.** A flag-word spec entry (`{codec, flags: Table}`) surfaces in canonical-doc as a flat sorted `string[]`, not as the raw int. Each entry is either a named slug (slugified-camelCase from the table's display string) or `bit<N>` (zero-based bit position) for set bits the table doesn't name. Canonical sort order: named slugs first alphabetically, then `bit<N>` in ascending bit position. Toggling one bit adds or removes one entry at its sorted position - same shape for named and unnamed bits, so diffs read uniformly. `compileFlagTable` slugifies display strings to camelCase canonical keys (`"NoBlock"` -> `noBlock`);`slugifyCodedName` rejects display strings whose slug would collide with the `bit<N>` sentinel namespace.`intToFlagArray` / `flagArrayToInt` translate at the wire codec boundary via the `FlagArraySchema` wrapper.
325
325
326
-
Slugified identifiers (rather than the raw display strings) are the canonical token shape because the construction API (`docs/todo.md`) surfaces flags as TS members typed against a literal-name union - identifier-shaped names get the canonical dot-trigger autocomplete with per-flag JSDoc visible inline, which a quoted-display-string union does not. Schema validation messages and JSON Schema `items.enum` autocomplete also benefit from identifier tokens (no spacing/casing ambiguities like "No LOS required" vs "No los required"). The display string remains the parsed-tree label; the slug is the toolchain token, with one translation point (label <-> slug) at the projection boundary.
326
+
Strict-disjoint invariant: a `bit<N>` entry whose position falls inside the named-mask is rejected at both the schema layer and the wire boundary - hand-edits must use the canonical slug for any spec-named bit. `bit<N>` with N >= codec width is also rejected at both layers, so synthesized entries cannot reference a bit past the wire word.
327
327
328
-
The wrapper-at-canonical-doc shape (`header.flags = {flags: [...], flagsRaw: "..."}`) keeps the spec -> canonical-doc orchestration one-key-per-spec-field; a flat sidecar `header.flagsRaw` would require a special case in `toZodSchema` to emit two parent keys per flag spec entry.
328
+
Slugified identifiers (rather than the raw display strings) are the canonical token shape because the construction API (`docs/todo.md`) surfaces flags as TS members typed against a literal-name union - identifier-shaped names get the canonical dot-trigger autocomplete with per-flag JSDoc visible inline, which a quoted-display-string union does not. Schema validation messages and JSON Schema `items.enum` autocomplete also benefit from identifier tokens (no spacing/casing ambiguities like "No LOS required" vs "No los required"). The display string remains the parsed-tree label; the slug is the toolchain token, with one translation point (label <-> slug) at the projection boundary.
329
329
330
-
This is a consistent application of rule #1 - `packedAs`+`bitRange` already exposes byte-packed sub-fields as peer scalar entries; the named-bit projection exposes bit-packed sub-fields the same way (the wire packs N independent semantic units into one int; canonical separates them).
330
+
This is a consistent application of rule #1 - `packedAs`+`bitRange` already exposes byte-packed sub-fields as peer scalar entries; the flat-array projection exposes bit-packed sub-fields the same way (the wire packs N independent semantic units into one int; canonical separates them into one entry per set bit).
331
331
332
-
8.**Lossless reservoirs preserve unnamed bits.** Adding a name to a flag table is a non-breaking spec evolution: old snapshots load via the `flagsRaw` path, re-saving promotes the bit to its new name. `schemaVersion` does NOT bump for additive name changes; bumping is reserved for _re-interpretive_ spec changes (a previously-parsed field's meaning changes), which require explicit migration code in the snapshot codec. The byte round-trip invariant `serialize(parse(b)) === b` is the load-bearing property - every existing `*-roundtrip.test.ts` enforces it.
332
+
8.**Lossless preservation of unnamed bits.** Adding a name to a flag table is a non-breaking spec evolution: old snapshots load via `bit<N>` entries, re-saving promotes the bit to its new slug. `schemaVersion` does NOT bump for additive name changes; bumping is reserved for _re-interpretive_ spec changes (a previously-parsed field's meaning changes), which require explicit migration code in the snapshot codec. The byte round-trip invariant `serialize(parse(b)) === b` is the load-bearing property - every existing `*-roundtrip.test.ts` enforces it.
333
333
334
334
9.**Enums and PIDs stay numeric in canonical-doc by design.** Where flag fields project to sorted-array name lists (rule #7), enum and PID fields stay as raw integers - the diff-friendliness gain doesn't justify the complication. Half the enum fields drive dispatch (`objectType`, `subType`, `scriptType`, MAP `version` / `rotation` / `elevation`) and would force a conversion at every dispatch site if projected to strings; the rest produce diffs of the same line count whether named or numeric (`5 -> 0` vs `"Items" -> "Background"`), unlike flags where the diff is fundamentally lossy. The display layer's `enum` table resolves names for editor dropdowns and hover; the snapshot stays close to the wire.
0 commit comments