@orkestrel/scaffold 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -67,11 +67,14 @@ error.
67
67
  its plan from your `package.json` and stays clean over the addition.
68
68
  - **`pull`** — fetches the latest vendored dependency guides and registry versions
69
69
  for an existing package and reports drift.
70
- - **`audit`** — a whole-plan conformance report: diffs a target against the FULL plan
71
- its own manifest implies (host AND generated artifacts alike) and reports drift as
72
- data, findings and all; exits nonzero the moment any drift is found, so it gates CI
73
- cleanly. `--live` additionally checks upstream guide/version freshness `audit` is the
74
- ONLY verb that carries `--live`.
70
+ - **`audit`** — a conformance report over the artifacts the plan actually gates: the
71
+ shared host-origin files (presence, or content once hydrated) AND the generated
72
+ configs/manifest (content); reports drift as data, findings and all; exits nonzero
73
+ the moment any drift is found, so it gates CI cleanly. Starter files — source/test
74
+ stubs, starter guides, README — are written once at scaffold time and are
75
+ legitimately outgrown, so they are birth-only and never audited; the build and
76
+ parity gates police their substance instead. `--live` additionally checks upstream
77
+ guide/version freshness — `audit` is the ONLY verb that carries `--live`.
75
78
  - **`repair`** — restores the shared HOST set only (generated source/tests/configs are
76
79
  never touched); re-derives the plan from the audit and re-applies only the drifted
77
80
  host artifacts; dry-run by default, `--apply` writes the fixes, `--prune` also removes
@@ -60,6 +60,7 @@ edit the rows by hand; edit source guides and re-run instead.
60
60
  | @orkestrel/reason | 0.0.3 | A zero-dependency, synchronous, deterministic reasoning engine: declarative, JSON-serializable definitions are evaluated against subjects (plain data records) to produce traceable results. Four strategies behind one dispatch surface — quantitative (factor-based numeric scoring), logical (rule-based boolean deduction with forward / backward chaining), symbolic (algebraic equation solving by variable isolation), inferential (fact derivation with unification variables and proof trees) — each a ReasonerInterface registered on the thin Reason orchestrator, with three injectable operators (Evaluator / Transformer / Aggregator) doing the shared arithmetic. Every result is a fresh object carrying success, a human-readable trace, and accumulated errors; nothing mutates its inputs. |
61
61
  | @orkestrel/relation | 0.0.3 | A small, declarative ORM layer over the database module: name a table's relations once, then load / find records with their related rows already attached. Loading is batched — one query per relation across the whole record set (where(col).any(keys)), grouped in memory and merged on — so a hundred parents cost the same number of round-trips as one. Five relation kinds (belongs / many / one / through / morph) cover the FK shapes; nested includes recurse through the registry; link / unlink / links manage a many-to-many junction without hand-writing join rows. |
62
62
  | @orkestrel/router | 0.0.4 | This package's ONE guide, covering all three faces (AGENTS §22 — one guide per package): the pure, environment-agnostic core — a registry-and-match engine (Router) plus a fetch-standard, method-dimensioned dispatcher (Dispatcher) layered over one internal Router<RouteRecord<TState>> — the browser navigation face (Navigator), and the node adapter face (buildRequest / sendResponse / createListener). Router is the ONE shared machine both Navigator and Dispatcher compose — literal-over-param-over-wildcard precedence, trailing-slash folding, tolerant percent-decoding, and the answers native-override seam all come from this single engine (AGENTS §21 "one engine, native overrides"); the core-first story is what makes the other two faces thin. Source: src/core, src/browser, src/server. Surfaced through the @orkestrel/router barrel (aliased @src/core / @src/browser / @src/server inside this repo). |
63
+ | @orkestrel/scaffold | 0.0.1 | A synchronous, deterministic package-blueprint compiler for the @orkestrel line: a closed, JSON-serializable Blueprint (name, surfaces, dependencies, overrides…) is compiled into a Plan — an ordered list of Artifacts, each carrying an origin that says whether its content was host-copied, template-filled, or computed — and every downstream product (the files on disk, a review document, an audit of an existing package, a dry-run summary) is projected from that one Plan, never authored separately. |
63
64
  | @orkestrel/sea | 0.0.3 | Node.js SEA builder — compress, blob, assemble, sign, and embed assets into a standalone binary. Pure TypeScript, no WASM, no external tools. Source: src/server. Surfaced through the @orkestrel/sea barrel. |
64
65
  | @orkestrel/server | 0.0.6 | This package's ONE guide (AGENTS §22 — one guide per package), covering its single published surface: the middleware seam (compose, MiddlewareContext/NextFunction/MiddlewareHandler), the HTTPError vocabulary, the shared substrate (cookies, WebCrypto tokens, content negotiation via Negotiator, ETag/Range, security primitives, SSE, and the body pipeline), and the deliberately node-bound Server lifecycle entity binding node:http via @orkestrel/router's adapter helpers, the upgrade seam, connection-fact injection, and discoverPort. The server consumes @orkestrel/router — routing, matching, and dispatch are that package's, never re-implemented here (AGENTS §21 "mechanism, never policy"). Source: src/server. Surfaced through the @orkestrel/server barrel (aliased @src/server inside this repo). |
65
66
  | @orkestrel/sqlite | 0.0.4 | A lean, typed, synchronous wrapper over Node's built-in node:sqlite — zero npm dependencies, just a thin typed skin on DatabaseSync / StatementSync. It surfaces exactly SQLite's native power — prepared statements, transactions, and pragmas — and deliberately no query / filter / sort / aggregate builder: it is the raw native handle, not an ORM, so a caller reaching for typed querying builds that layer on top. Source: src/server. Surfaced through the @src/server barrel. |
@@ -187,13 +187,15 @@ what keeps the core pure while still describing files it cannot itself read.
187
187
  | `COMPILE_STAGES` | const | `['draft', 'gate', 'pin']`, frozen — the pipeline phases in order. |
188
188
  | `SURFACE_MATRIX` | const | The per-surface variant matrix as data: per `Surface`, its `configs/src` files, Vitest project label, `exports` subpath, and build formats — the per-surface layer `blueprintToPlan` reads BENEATH the manifest and exports combination rules it applies on top. |
189
189
  | `HOST_PATHS` | const | The byte-copied host artifact paths (`AGENTS.md`, `CLAUDE.md`, `LICENSE`, `.claude`, `scripts/deps.sh`, `scripts/cursor.sh`, `scripts/ollama.sh` — the SessionStart hooks, orchestration-grouped — `guides/src/guide.md` — the line-wide dev-tooling guide — and `guides/src/scaffold.md` — the scaffold engine's own self-guide, both guides-grouped — `.editorconfig`, `.gitattributes`, `.gitignore`, `.oxfmtrc.json`, `.oxlintrc.json`, `.oxlintignore`, `.prettierignore`, `.github/workflows/ci.yml`), frozen — the shared artifacts every `@orkestrel` repo's vendored host carries. |
190
- | `SCAFFOLD_RANGE` | const | `'^0.0.1'` — the exact devDependency range a scaffolded package's `package.json` pins `@orkestrel/scaffold` at. |
190
+ | `SCAFFOLD_RANGE` | const | `'^0.0.2'` — the exact devDependency range a scaffolded package's `package.json` pins `@orkestrel/scaffold` at. |
191
191
  | `NAME_PATTERN` | const | The `/^[a-z][a-z0-9-]*$/` package-name RegExp, closed vocabulary as data. |
192
192
  | `DEPENDENCY_NAME_PATTERN` | const | The `/^@orkestrel\/[a-z][a-z0-9-]*$/` dependency-name RegExp — every `Dependency.name` must be `@orkestrel`-scoped and NAME_PATTERN-shaped after the scope, closing the traversal vector a hand-built `../`-laced name would open through the pointer-artifact and `Sync.write` path derivation. |
193
193
  | `EXTRA_NAME_PATTERN` | const | The `extras` dependency-name RegExp — an optional single `@scope/` prefix, then lowercase letters/digits/hyphens/dots/underscores (never leading). Broader than `DEPENDENCY_NAME_PATTERN` on purpose: `extras` names are `devDependencies`-content only, never path-derived, so any valid npm package name (not just `@orkestrel/*`) is accepted while staying structurally traversal-closed. |
194
194
  | `DEFAULT_VERSION` | const | `'0.0.1'` — the starting version the `blueprint` builder fills. |
195
195
  | `DEFAULT_ENGINES` | const | `'>=22'` — the `engines.node` range the `blueprint` builder fills. |
196
196
  | `COMPILER_ID` | const | `'compiler'` — the default id for a `Compiler` orchestrator. |
197
+ | `JSON_PRINT_WIDTH` | const | `100` — the fleet's `.oxfmtrc.json` `printWidth`, `formatJson`'s array-collapse threshold. |
198
+ | `JSON_TAB_WIDTH` | const | `2` — the fleet's `.oxfmtrc.json` `tabWidth`, the column width `formatJson` counts each rendered tab as. |
197
199
  | `TEMPLATES` | const | The shipped, versioned `TemplateDefinition` data every `template`-origin artifact fills against (README, the own-guide stub, the guides index, the per-surface source stubs, the shared test recorder plus `parityTest` — the frozen `tests/guides/src/parity.test.ts` body — and `setupServer` / `setupBrowser`, the per-surface test-setup stubs) — placeholders documented per entry, frozen. |
198
200
 
199
201
  ```ts
@@ -216,7 +218,7 @@ ORIGINS // ['host', 'template', 'computed']
216
218
  GROUPS // ['manifest', 'configs', 'source', 'tests', 'guides', 'docs', 'orchestration']
217
219
  CATEGORIES // ['type', 'constant', 'factory', 'entity']
218
220
  FRESHNESS // ['current', 'behind', 'missing', 'failed']
219
- SCAFFOLD_RANGE // '^0.0.1' — the pinned devDependency range for @orkestrel/scaffold
221
+ SCAFFOLD_RANGE // '^0.0.2' — the pinned devDependency range for @orkestrel/scaffold
220
222
  NAME_PATTERN.test('router') // true
221
223
  NAME_PATTERN.test('Router') // false — the package-name law rejects a leading capital
222
224
  DEPENDENCY_NAME_PATTERN.test('@orkestrel/contract') // true
@@ -472,16 +474,21 @@ behind the `Compiler`, the `Sync` entity, and the projection surface. Projection
472
474
  | API | Kind | Summary |
473
475
  | ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
474
476
  | `blueprintToMembers` | function | Derive the declared public `Member[]` from a blueprint (name → Pascal → the canonical inventory per surface) — the SINGLE source both the source stubs and the guide Surface tables read. The skeleton vocabulary is deliberately the four `Category` buckets (`type` / `constant` / `factory` / `entity`); standalone helpers, validators, and shapers are hand-authored in implementation, not scaffolded. |
475
- | `blueprintToPlan` | function | The full pure compilation, orchestrating the Compilers section's exported stages in sequence (never nesting them): draft the artifacts — `packageManifest` (the manifest and exports combination rules; multi-surface OMITS the top-level `package.json` `types`; a single-variant server-/browser-only retargets its lone surface to the `.` root, `main` / `module` re-pointed), `configArtifacts`, `sourceArtifacts`, `testArtifacts`, `guideArtifacts` OVER the per-surface `SURFACE_MATRIX` rows, plus `hostGroup`-classified `HOST_PATHS` and `applyOverrides` — then `pinPlan`; the computed `package.json` also emits `peerDependencies` / `peerDependenciesMeta` for `peers` (an `optional` peer gains a meta entry) and merges `extras` into the generated `devDependencies` baseline (an extra's range wins on a name collision), plus a `"scaffold": "scaffold"` script and a `'@orkestrel/scaffold': SCAFFOLD_RANGE` devDependency, so every scaffolded package ships already wired for its own future `repair` / `audit`; optionally scoped to a `Group[]` selection (default: all groups). |
477
+ | `blueprintToPlan` | function | The full pure compilation, orchestrating the Compilers section's exported stages in sequence (never nesting them): draft the artifacts — `packageManifest` (the manifest and exports combination rules; multi-surface OMITS the top-level `package.json` `types`; a single-variant server-/browser-only retargets its lone surface to the `.` root, `main` / `module` re-pointed), `configArtifacts`, `sourceArtifacts`, `testArtifacts`, `guideArtifacts` OVER the per-surface `SURFACE_MATRIX` rows, plus `hostGroup`-classified `HOST_PATHS` and `applyOverrides` — then `pinPlan`; the computed `package.json` also emits `peerDependencies` / `peerDependenciesMeta` for `peers` (an `optional` peer gains a meta entry), merges `extras` into the generated `devDependencies` baseline (an extra's range wins on a name collision), and merges every peer into `devDependencies` at its peer range too (a peer is ALSO dev-installed — grounded against the live @orkestrel/middleware and @orkestrel/mcp exemplars — so `deriveBlueprint`'s reverse read never loses it), plus a `"scaffold": "scaffold"` script and a `'@orkestrel/scaffold': SCAFFOLD_RANGE` devDependency, so every scaffolded package ships already wired for its own future `repair` / `audit`; optionally scoped to a `Group[]` selection (default: all groups). |
476
478
  | `pinPlan` | function | Return a fresh `Plan` with `trace` (the one-line derivation summary) and `hash` (a canonical structural digest, via `computeHash` over `stableStringify`) filled — deterministic, no timestamps, no run-specific data. |
477
479
  | `computeHash` | function | Compute a canonical FNV-1a digest (32-bit offset basis/prime, `Math.imul` wraparound multiply) of a text string as an 8-hex-digit zero-padded string — deterministic, no clocks or randomness. |
478
480
  | `stableStringify` | function | Serialize a value to a canonical, key-order-INDEPENDENT JSON-like string — object keys code-unit sorted, array order preserved — so two logically-equal blueprints built in a different field order still hash identically once fed through `computeHash`. |
481
+ | `formatJson` | function | Serialize a value to newline-terminated JSON that matches the fleet's own `oxfmt` output byte-for-byte — objects one key per line, arrays collapsed onto one line when the rendered width (each tab counted as `JSON_TAB_WIDTH` columns) fits `JSON_PRINT_WIDTH`, one item per line otherwise. `rootTsconfig` / `coreTsconfig` / `surfaceTsconfig` render their computed configs through it instead of `JSON.stringify(…, '\t')` (which always breaks arrays one item per line) so `oxfmt --check` never has anything left to rewrite. |
482
+ | `renderValue` | function | `formatJson`'s per-value dispatch — arrays via `renderArray`, plain objects via `renderObject`, everything else via `JSON.stringify`. |
483
+ | `renderArray` | function | Render a JSON array through `formatJson`'s inline-or-broken rule — inline when the rendered width (via `computeColumnWidth`) fits `JSON_PRINT_WIDTH`, one item per line otherwise; mutually recursive with `renderValue`. |
484
+ | `renderObject` | function | Render a JSON object through `formatJson`'s one-key-per-line rule; mutually recursive with `renderValue`. |
485
+ | `computeColumnWidth` | function | Measure a rendered fragment's column width against `JSON_PRINT_WIDTH`, counting each literal tab as `JSON_TAB_WIDTH` columns and every other character as one. |
479
486
  | `validateBlueprint` | function | The semantic pass over a blueprint — name against `NAME_PATTERN` (and a 203-char bound, so the published `@orkestrel/<name>` fits npm's 214-character cap), non-empty on-vocabulary `surfaces` with no repeats (a single surface — `core`-only, `server`-only, `browser`-only — is fully first-class, no `core` required), the ONE exemplar-less combination — `browser`+`server` declared together with no `core` — is blocking ("The browser+server combination without core has no defined configuration class — declare core alongside them, or declare a single surface"; closes the silent surface-drop `rootViteConfig`'s dispatch would otherwise produce), well-formed `dependencies` / `peers` / `extras` via `validateDependencyArray` (non-empty name/range, no duplicate names within an array, and no name overlap ACROSS the three arrays — a name in both `dependencies` and `peers`, or an `extras` name repeating either, is blocking; `dependencies`/`peers` names are `DEPENDENCY_NAME_PATTERN`-shaped — closed to `@orkestrel/*`, since only those two arrays are path-derived — while `extras` names are `EXTRA_NAME_PATTERN`-shaped, broader, since `extras` is `devDependencies`-content only), `version` shaped `\d+.\d+.\d+`, `engines` shaped `>=\d+`, no duplicate override paths, and no empty override content. Returns a `Validation`, never throws. |
480
487
  | `validateDependencyArray` | function | Validate one dependency-shaped array under the name/range/duplicate rules — pure, returns `{ questions, seen }` rather than mutating a closed-over array, so `validateBlueprint` can apply the cross-array (`dependencies` / `peers` / `extras`) overlap rules over the returned `seen` sets. |
481
488
  | `manifestToDependencies` | function | Parse a `package.json` text into `readonly Dependency[]`, keeping the `DEPENDENCY_NAME_PATTERN` entries across `dependencies` / `devDependencies` / `peerDependencies` (all three, deduplicated) — pure, never throws. |
482
489
  | `isRecord` | function | Narrow an unknown value to a plain (non-null, non-array) JSON object. |
483
490
  | `rangeToFreshness` | function | Compare a declared `range` to the registry `latest`: `'current'` iff the range's `^0.0.N` exact pin equals `latest`, else `'behind'` (the `0.0.x` exact-pin law); the `missing` / `failed` verdicts come from the fetch layer, not this pure comparison. |
484
- | `diffPlan` | function | The AUDIT projection: diff a plan's artifacts against a caller-supplied `Readonly<Record<string, string>>` of the target's current content, returning an `Audit` of drift findings — pure, no I/O. |
491
+ | `diffPlan` | function | The AUDIT projection: diff a plan's artifacts against a caller-supplied `Readonly<Record<string, string>>` of the target's current content, returning an `Audit` of drift findings — pure, no I/O. Per-origin semantics: `host` stays presence-only (`missing`/`aligned`) unless hydrated with real bytes, then content-compared and can be `stale`; `computed` is always content-aware canon (`missing`/`aligned`/`stale`), gating the audit; `template` is BIRTH-ONLY and AUDIT-EXEMPT — always `aligned` regardless of presence or content, since starter files are written once by `materialize` and are legitimately outgrown, the build/parity gates police their substance, and content-comparing them would make an unscoped repair a data-loss footgun. A target file the plan does not own is `foreign`. |
485
492
  | `inferGroup` | function | Infer a foreign path's `Group` from its leading path segment — ordered prefix match (`src/`, `tests/`, `guides/`, `docs/`, `configs/`, `.github/` / `scripts/` as `orchestration`, then the two manifest file names), falling back to `configs`. |
486
493
  | `planToReview` | function | Project a `Plan` into a copy-ready markdown review document — the artifact table by group, the members table, the summary; the diff-first dry run. |
487
494
  | `auditToReview` | function | Project an `Audit` into a markdown drift report — findings grouped by `drift`, aligned entries elided; what `repair` will touch. |
@@ -547,6 +554,22 @@ validateDependencyArray('dependencies', [{ name: '', range: '^1' }])
547
554
  // { questions: [{ field: 'dependencies', text: 'A dependency name must not be empty', … }], seen: Set(0) {} }
548
555
  ```
549
556
 
557
+ ```ts
558
+ import {
559
+ computeColumnWidth,
560
+ formatJson,
561
+ renderArray,
562
+ renderObject,
563
+ renderValue,
564
+ } from '@orkestrel/scaffold'
565
+
566
+ formatJson({ lib: ['ESNext', 'DOM'] }) // '{\n\t"lib": ["ESNext", "DOM"]\n}\n' — a thin orchestrator
567
+ renderValue({ lib: ['ESNext'] }, '', '', '') // '{\n\t"lib": ["ESNext"]\n}' — dispatches to renderObject
568
+ renderObject({ lib: ['ESNext'] }, '') // '{\n\t"lib": ["ESNext"]\n}' — one key per line
569
+ renderArray(['ESNext', 'DOM'], '', '', '') // '["ESNext", "DOM"]' — inline when it fits JSON_PRINT_WIDTH
570
+ computeColumnWidth('\t"a"') // 3 — one tab counted as JSON_TAB_WIDTH, plus two characters
571
+ ```
572
+
550
573
  `alignTable` builds a markdown `TableNode` (each cell's string parsed with `parseInline`)
551
574
  and serializes it through `@orkestrel/markdown`'s `renderMarkdown`, which contributes the
552
575
  STRUCTURE — `\|`-escaping any literal pipe inside a cell and emitting the alignment delimiter
@@ -955,9 +978,14 @@ These invariants hold across `src/core` + `src/server` ↔ `scaffold.md`:
955
978
  5. **One plan, many projections; projections never add.** `planToReview`, `planToSummary`,
956
979
  `diffPlan`, `auditToReview`, and (on the server) `materialize` are pure views over the
957
980
  pinned plan — the review renders exactly the plan's artifacts, the summary counts exactly
958
- them, the audit compares exactly them, and materialization writes exactly them. Nothing
959
- downstream is authored separately, so the files on disk, the review, the audit, and the
960
- summary cannot disagree with the plan or one another.
981
+ them, and materialization writes exactly them. `diffPlan`'s audit compares per-origin: a
982
+ `host` artifact by presence (or content once hydrated), a `computed` artifact by content
983
+ always, and a `template` artifact NEVER birth-only starter files (source/test stubs,
984
+ starter guides, README) are written once by `materialize` and are legitimately outgrown, so
985
+ they always report `aligned` regardless of the target's presence or bytes; the build and
986
+ parity gates police their substance instead. Nothing downstream is authored separately, so
987
+ the files on disk, the review, the audit, and the summary cannot disagree with the plan or
988
+ one another.
961
989
  6. **The variant matrix is data (§21).** A blueprint's `surfaces` mints EVERY live variant
962
990
  class the line carries: core-only, core+server, core+browser+server, server-only,
963
991
  browser-only, core+browser — driving the `package.json` `exports` shape, the per-surface
@@ -1151,13 +1179,21 @@ audit.findings.filter((finding) => finding.drift === 'stale') // e.g. [{ path: '
1151
1179
  auditToReview(audit) // '# Drift — abort\n## Stale\n| Path | Group |\n…'
1152
1180
  ```
1153
1181
 
1154
- `diffPlan` compares by content equality: a `template` / `computed` artifact whose rendered
1155
- content the target does not match is `stale`; one the target lacks is `missing`; a target
1156
- file the plan does not own is `foreign`. A `host`-origin artifact carries no `content` on a
1157
- RAW plan (the pure core never read the canonical host bytes), so an un-hydrated audit sees it
1158
- by PRESENCE only `missing` or `aligned`, never `stale`; running the server's `hydratePlan`
1159
- first fills each `host` artifact's `content` from the resolved host root, so the SAME
1160
- `diffPlan` becomes content-aware and a drifted host file surfaces as `stale` too. A
1182
+ `diffPlan` compares per origin, not uniformly across the whole plan. A `computed` artifact
1183
+ whose rendered content the target does not match is `stale`; one the target lacks is
1184
+ `missing` the generated configs/manifest are content-aware canon, and gate the audit. A
1185
+ `template` artifact is BIRTH-ONLY and AUDIT-EXEMPT: it always reports `aligned`, whatever the
1186
+ target holds or lacks. Starter files source/test stubs, starter guides, README — are
1187
+ written ONCE by `materialize` and are legitimately outgrown (real code replaces the stub, a
1188
+ hand-authored guide replaces the scaffold prose); content- or presence-comparing a mature
1189
+ package against its birth stub is a category error the build and parity gates already police,
1190
+ and it would make an unscoped `repair` a data-loss footgun (a stub overwrite clobbering real
1191
+ code). A target file the plan does not own is `foreign`. A `host`-origin artifact carries no
1192
+ `content` on a RAW plan (the pure core never read the canonical host bytes), so an
1193
+ un-hydrated audit sees it by PRESENCE only — `missing` or `aligned`, never `stale`; running
1194
+ the server's `hydratePlan` first fills each `host` artifact's `content` from the resolved
1195
+ host root, so the SAME `diffPlan` becomes content-aware and a drifted host file surfaces as
1196
+ `stale` too. A
1161
1197
  directory-shaped host artifact (`.claude`) has no single storage file for `hydratePlan` to
1162
1198
  read, so it stays presence-only regardless — a KNOWN, documented boundary, not a promise:
1163
1199
  `pruneTargets` separately covers UNEXPECTED files under it (the prune allowlist scan), but a
@@ -1958,7 +1994,9 @@ host; the skip is the environment's ceiling, never a hidden failure.
1958
1994
  `peerDependenciesMeta` emission for `peers`, `extras` merging into `devDependencies` with
1959
1995
  extras winning a collision — template-fill vs computed origins + the token-collision
1960
1996
  boundary, `planToReview` / `auditToReview` / `syncToReview` table emission, `planToSummary`
1961
- counts, `diffPlan` drift verdicts incl. host presence-only on a RAW (unhydrated) plan,
1997
+ counts, `diffPlan` drift verdicts incl. host presence-only on a RAW (unhydrated) plan and
1998
+ `template`-origin birth-only audit-exemption (always `aligned`, present or absent, matching
1999
+ content or not),
1962
2000
  `manifestToDependencies` across all three sections deduplicated, `rangeToFreshness` exact-pin
1963
2001
  law, `pinPlan` determinism), `validateBlueprint` errors + warnings (incl. the per-array
1964
2002
  `peers` / `extras` name/range/duplicate rules and the three cross-array overlap blocks),
@@ -129,9 +129,13 @@ var DEFAULT_VERSION = "0.0.1";
129
129
  /** The `engines.node` range the `blueprint` builder fills. */
130
130
  var DEFAULT_ENGINES = ">=22";
131
131
  /** The devDependency range generated packages pin `@orkestrel/scaffold` at. */
132
- var SCAFFOLD_RANGE = "^0.0.1";
132
+ var SCAFFOLD_RANGE = "^0.0.2";
133
133
  /** The default id for a `Compiler` orchestrator. */
134
134
  var COMPILER_ID = "compiler";
135
+ /** The fleet's `.oxfmtrc.json` `printWidth` — `formatJson`'s array-collapse threshold. */
136
+ var JSON_PRINT_WIDTH = 100;
137
+ /** The fleet's `.oxfmtrc.json` `tabWidth` — the column width `formatJson` counts each tab as. */
138
+ var JSON_TAB_WIDTH = 2;
135
139
  //#endregion
136
140
  //#region src/core/errors.ts
137
141
  /**
@@ -985,14 +989,25 @@ function inferGroup(path) {
985
989
  * @param plan - The plan whose artifacts are the source of truth.
986
990
  * @param current - The target's current content, keyed by artifact-relative path.
987
991
  * @remarks
988
- * A `template` / `computed` artifact whose rendered content the target does not
989
- * match is `stale`; one the target lacks is `missing`; a target file the plan
990
- * does not own is `foreign`. A `host`-origin artifact is audited by PRESENCE
991
- * only `missing` or `aligned`, never `stale` UNLESS it has been hydrated
992
- * with its real host bytes (`hydratePlan`'s `content`), in which case it is
993
- * content-compared exactly like a `template` / `computed` artifact and CAN be
992
+ * Audit semantics are per-origin. A `host`-origin artifact is audited by
993
+ * PRESENCE only `missing` or `aligned`, never `stale` UNLESS it has been
994
+ * hydrated with its real host bytes (`hydratePlan`'s `content`), in which case
995
+ * it is content-compared exactly like a `computed` artifact and CAN be
994
996
  * `stale`. A degrade-path or directory-shaped host artifact (never hydrated)
995
- * stays presence-only.
997
+ * stays presence-only. A `computed` artifact is content-aware canon —
998
+ * `missing` / `aligned` / `stale` — and gates the audit like any drifted
999
+ * finding. A `template`-origin artifact is BIRTH-ONLY and AUDIT-EXEMPT: it is
1000
+ * always reported `aligned`, regardless of whether the target has it at all
1001
+ * or what its bytes are. Starter files (source stubs, test stubs, starter
1002
+ * guides, README) are written ONCE by `materialize` and are legitimately
1003
+ * outgrown — real code replaces the stub, a hand-authored guide replaces the
1004
+ * scaffold prose, an entity gets renamed. Content- or presence-comparing a
1005
+ * mature package against its birth stub is a category error (the build and
1006
+ * parity gates already police the package's substance) AND makes any
1007
+ * unscoped repair a data-loss footgun — a stub overwrite would clobber real,
1008
+ * hand-authored code. `template` findings therefore never contribute to
1009
+ * `drifted` / `missing` / `clean`. A target file the plan does not own is
1010
+ * `foreign`.
996
1011
  * @returns The `Audit` of drift findings — pure, no I/O.
997
1012
  *
998
1013
  * @example
@@ -1008,6 +1023,14 @@ function diffPlan(plan, current) {
1008
1023
  for (const artifact of plan.artifacts) {
1009
1024
  owned.add(artifact.path);
1010
1025
  const seen = current[artifact.path];
1026
+ if (artifact.origin === "template") {
1027
+ findings.push({
1028
+ path: artifact.path,
1029
+ group: artifact.group,
1030
+ drift: "aligned"
1031
+ });
1032
+ continue;
1033
+ }
1011
1034
  if (artifact.origin === "host") {
1012
1035
  let drift;
1013
1036
  if (seen === void 0) drift = "missing";
@@ -1380,6 +1403,125 @@ function stableStringify(value) {
1380
1403
  return JSON.stringify(value);
1381
1404
  }
1382
1405
  /**
1406
+ * Measure a rendered fragment's column width, counting each literal tab as
1407
+ * `JSON_TAB_WIDTH` columns (matching `.oxfmtrc.json`'s `tabWidth`) and every
1408
+ * other character as one.
1409
+ *
1410
+ * @param text - The rendered fragment to measure.
1411
+ * @returns The fragment's column width against `JSON_PRINT_WIDTH`.
1412
+ *
1413
+ * @example
1414
+ * ```ts
1415
+ * import { computeColumnWidth } from '@orkestrel/scaffold'
1416
+ *
1417
+ * computeColumnWidth('\t"a"') // 3 — one tab counted as JSON_TAB_WIDTH, plus two characters
1418
+ * ```
1419
+ */
1420
+ function computeColumnWidth(text) {
1421
+ let width = 0;
1422
+ for (const char of text) width += char === " " ? 2 : 1;
1423
+ return width;
1424
+ }
1425
+ /**
1426
+ * Render a JSON array through `formatJson`'s inline-or-broken rule — inline
1427
+ * when the rendered width (via `computeColumnWidth`) fits `JSON_PRINT_WIDTH`, one
1428
+ * item per line otherwise.
1429
+ *
1430
+ * @param entries - The array's elements, in order.
1431
+ * @param indent - The current indentation prefix.
1432
+ * @param prefix - The text already emitted on this line before the array.
1433
+ * @param suffix - The text that will follow the array on this line.
1434
+ * @returns The rendered array fragment (no trailing newline).
1435
+ *
1436
+ * @example
1437
+ * ```ts
1438
+ * import { renderArray } from '@orkestrel/scaffold'
1439
+ *
1440
+ * renderArray(['ESNext', 'DOM'], '', '', '') // '["ESNext", "DOM"]'
1441
+ * ```
1442
+ */
1443
+ function renderArray(entries, indent, prefix, suffix) {
1444
+ if (entries.length === 0) return "[]";
1445
+ const items = entries.map((entry) => renderValue(entry, indent, "", ""));
1446
+ const inline = `[${items.join(", ")}]`;
1447
+ if (computeColumnWidth(`${prefix}${inline}${suffix}`) <= 100) return inline;
1448
+ const childIndent = `${indent}\t`;
1449
+ return `[\n${items.map((item) => `${childIndent}${item}`).join(",\n")}\n${indent}]`;
1450
+ }
1451
+ /**
1452
+ * Render a JSON object through `formatJson`'s one-key-per-line rule.
1453
+ *
1454
+ * @param entry - The object to render.
1455
+ * @param indent - The current indentation prefix.
1456
+ * @returns The rendered object fragment (no trailing newline).
1457
+ *
1458
+ * @example
1459
+ * ```ts
1460
+ * import { renderObject } from '@orkestrel/scaffold'
1461
+ *
1462
+ * renderObject({ lib: ['ESNext'] }, '') // '{\n\t"lib": ["ESNext"]\n}'
1463
+ * ```
1464
+ */
1465
+ function renderObject(entry, indent) {
1466
+ const keys = Object.keys(entry);
1467
+ if (keys.length === 0) return "{}";
1468
+ const childIndent = `${indent}\t`;
1469
+ return `{\n${keys.map((key, index) => {
1470
+ const prefix = `${childIndent}${JSON.stringify(key)}: `;
1471
+ const suffix = index === keys.length - 1 ? "" : ",";
1472
+ return `${prefix}${renderValue(entry[key], childIndent, prefix, suffix)}${suffix}`;
1473
+ }).join("\n")}\n${indent}}`;
1474
+ }
1475
+ /**
1476
+ * Render one JSON value through `formatJson`'s dispatch — arrays via
1477
+ * `renderArray`, objects via `renderObject`, everything else via
1478
+ * `JSON.stringify`.
1479
+ *
1480
+ * @param entry - The value to render.
1481
+ * @param indent - The current indentation prefix.
1482
+ * @param prefix - The text already emitted on this line before `entry`.
1483
+ * @param suffix - The text that will follow `entry` on this line.
1484
+ * @returns The rendered fragment (no trailing newline).
1485
+ *
1486
+ * @example
1487
+ * ```ts
1488
+ * import { renderValue } from '@orkestrel/scaffold'
1489
+ *
1490
+ * renderValue('ESNext', '', '', '') // '"ESNext"'
1491
+ * ```
1492
+ */
1493
+ function renderValue(entry, indent, prefix, suffix) {
1494
+ if (Array.isArray(entry)) return renderArray(entry, indent, prefix, suffix);
1495
+ if (isRecord(entry)) return renderObject(entry, indent);
1496
+ return JSON.stringify(entry);
1497
+ }
1498
+ /**
1499
+ * Serialize a value to newline-terminated JSON that matches the fleet's own
1500
+ * `oxfmt` output byte-for-byte — objects one key per line, arrays collapsed
1501
+ * onto one line when they fit `JSON_PRINT_WIDTH`, one item per line
1502
+ * otherwise.
1503
+ *
1504
+ * @param value - The value to serialize (config JSON — objects/arrays/primitives).
1505
+ * @remarks
1506
+ * `JSON.stringify(value, undefined, '\t')` always breaks arrays one item per
1507
+ * line; `oxfmt` collapses short ones. Emitting through `formatJson` keeps
1508
+ * computed config JSON format-stable by construction — `oxfmt --check` never
1509
+ * has anything left to rewrite. The rendering itself is delegated to
1510
+ * `renderValue` / `renderArray` / `renderObject` / `computeColumnWidth`, so
1511
+ * `formatJson` is a thin orchestrator around them.
1512
+ * @returns The rendered value, newline-terminated.
1513
+ *
1514
+ * @example
1515
+ * ```ts
1516
+ * import { formatJson } from '@orkestrel/scaffold'
1517
+ *
1518
+ * formatJson({ lib: ['ESNext', 'DOM'] }) // '{\n\t"lib": ["ESNext", "DOM"]\n}\n'
1519
+ * ```
1520
+ */
1521
+ function formatJson(value) {
1522
+ return `${renderValue(value, "", "", "")}\n`;
1523
+ }
1524
+ /**
1383
1525
  * Return a fresh `Plan` with `trace` and `hash` filled.
1384
1526
  *
1385
1527
  * @param plan - The plan to pin.
@@ -2252,6 +2394,8 @@ function packageManifest(spec) {
2252
2394
  for (const peer of [...spec.peers].sort((a, b) => compareCodeUnit(a.name, b.name))) peerDependencies[peer.name] = peer.range;
2253
2395
  const peerDependenciesMeta = {};
2254
2396
  for (const peer of spec.peers) if (peer.optional === true) peerDependenciesMeta[peer.name] = { optional: true };
2397
+ const peerDevDependencies = {};
2398
+ for (const peer of [...spec.peers].sort((a, b) => compareCodeUnit(a.name, b.name))) peerDevDependencies[peer.name] = peer.range;
2255
2399
  const scripts = {
2256
2400
  clean: "node -e \"try{require('node:fs').rmSync('dist',{recursive:true,force:true})}catch{}\"",
2257
2401
  copy: "node -e \"const fs=require('node:fs'),p=require('node:path'),a=process.argv[1],b=process.argv[2];fs.mkdirSync(p.dirname(b),{recursive:true});fs.cpSync(a,b,{force:true});console.log('Copied: '+a+' to '+b)\"",
@@ -2295,7 +2439,10 @@ function packageManifest(spec) {
2295
2439
  publishConfig: { access: "public" },
2296
2440
  scripts,
2297
2441
  dependencies,
2298
- devDependencies: devDependenciesFor(spec.extras),
2442
+ devDependencies: Object.fromEntries(Object.entries({
2443
+ ...devDependenciesFor(spec.extras),
2444
+ ...peerDevDependencies
2445
+ }).sort(([a], [b]) => compareCodeUnit(a, b))),
2299
2446
  ...Object.keys(peerDependencies).length > 0 ? { peerDependencies } : {},
2300
2447
  ...Object.keys(peerDependenciesMeta).length > 0 ? { peerDependenciesMeta } : {},
2301
2448
  engines: { node: spec.engines }
@@ -2317,7 +2464,7 @@ function packageManifest(spec) {
2317
2464
  function rootTsconfig(surfaces) {
2318
2465
  const paths = {};
2319
2466
  for (const surface of surfaces) paths[`@src/${surface}`] = [`./src/${surface}/index.ts`];
2320
- return `${JSON.stringify({
2467
+ return formatJson({
2321
2468
  compilerOptions: {
2322
2469
  target: "ESNext",
2323
2470
  module: "ESNext",
@@ -2347,7 +2494,7 @@ function rootTsconfig(surfaces) {
2347
2494
  "dist",
2348
2495
  "tmp"
2349
2496
  ]
2350
- }, void 0, " ")}\n`;
2497
+ });
2351
2498
  }
2352
2499
  /**
2353
2500
  * The rendered import / `resolve` header block every `rootViteConfig` shape
@@ -2702,7 +2849,7 @@ export default defineConfig({
2702
2849
  * ```
2703
2850
  */
2704
2851
  function coreTsconfig() {
2705
- return `${JSON.stringify({
2852
+ return formatJson({
2706
2853
  extends: "../../tsconfig.json",
2707
2854
  compilerOptions: {
2708
2855
  lib: ["ESNext"],
@@ -2713,7 +2860,7 @@ function coreTsconfig() {
2713
2860
  outDir: "../../dist/src/core"
2714
2861
  },
2715
2862
  include: ["../../src/core/**/*.ts"]
2716
- }, void 0, " ")}\n`;
2863
+ });
2717
2864
  }
2718
2865
  /**
2719
2866
  * `configs/src/vite.core.config.ts` — inlines its own `build.lib` /
@@ -2768,7 +2915,7 @@ export default defineConfig(
2768
2915
  * ```
2769
2916
  */
2770
2917
  function surfaceTsconfig(surface) {
2771
- const config = {
2918
+ return formatJson({
2772
2919
  extends: "../../tsconfig.json",
2773
2920
  compilerOptions: {
2774
2921
  lib: surface === "browser" ? [
@@ -2784,8 +2931,7 @@ function surfaceTsconfig(surface) {
2784
2931
  outDir: "../../dist/src"
2785
2932
  },
2786
2933
  include: [`../../src/${surface}/**/*.ts`]
2787
- };
2788
- return `${JSON.stringify(config, void 0, " ")}\n`;
2934
+ });
2789
2935
  }
2790
2936
  /**
2791
2937
  * `configs/src/vite.<browser|server>.config.ts` — a thin `dts`-only wrapper;
@@ -3643,6 +3789,8 @@ exports.EXTRA_NAME_PATTERN = EXTRA_NAME_PATTERN;
3643
3789
  exports.FRESHNESS = FRESHNESS;
3644
3790
  exports.GROUPS = GROUPS;
3645
3791
  exports.HOST_PATHS = HOST_PATHS;
3792
+ exports.JSON_PRINT_WIDTH = JSON_PRINT_WIDTH;
3793
+ exports.JSON_TAB_WIDTH = JSON_TAB_WIDTH;
3646
3794
  exports.NAME_PATTERN = NAME_PATTERN;
3647
3795
  exports.ORIGINS = ORIGINS;
3648
3796
  exports.PlanManager = PlanManager;
@@ -3662,6 +3810,7 @@ exports.blueprintToPlan = blueprintToPlan;
3662
3810
  exports.catalogNames = catalogNames;
3663
3811
  exports.catalogToBlock = catalogToBlock;
3664
3812
  exports.compareCodeUnit = compareCodeUnit;
3813
+ exports.computeColumnWidth = computeColumnWidth;
3665
3814
  exports.computeHash = computeHash;
3666
3815
  exports.configArtifacts = configArtifacts;
3667
3816
  exports.coreTsconfig = coreTsconfig;
@@ -3678,6 +3827,7 @@ exports.dualCondition = dualCondition;
3678
3827
  exports.entryFields = entryFields;
3679
3828
  exports.exportsMap = exportsMap;
3680
3829
  exports.fillArtifact = fillArtifact;
3830
+ exports.formatJson = formatJson;
3681
3831
  exports.guideArtifacts = guideArtifacts;
3682
3832
  exports.guideMemberTable = guideMemberTable;
3683
3833
  exports.hostGroup = hostGroup;
@@ -3709,6 +3859,9 @@ exports.planShape = planShape;
3709
3859
  exports.planToReview = planToReview;
3710
3860
  exports.planToSummary = planToSummary;
3711
3861
  exports.rangeToFreshness = rangeToFreshness;
3862
+ exports.renderArray = renderArray;
3863
+ exports.renderObject = renderObject;
3864
+ exports.renderValue = renderValue;
3712
3865
  exports.rootTsconfig = rootTsconfig;
3713
3866
  exports.rootViteConfig = rootViteConfig;
3714
3867
  exports.singleSurfaceViteConfig = singleSurfaceViteConfig;