@orkestrel/scaffold 0.0.2 → 0.0.3

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.
Files changed (78) hide show
  1. package/README.md +29 -15
  2. package/dist/bin/scaffold.js +1162 -1200
  3. package/dist/bin/scaffold.js.map +1 -1
  4. package/dist/host/AGENTS.md +124 -925
  5. package/dist/host/CLAUDE.md +260 -495
  6. package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +53 -0
  7. package/dist/host/agents/skills/orkestrel-align-packages/agents/openai.yaml +4 -0
  8. package/dist/host/agents/skills/orkestrel-align-packages/references/fleet.md +50 -0
  9. package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +56 -0
  10. package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +64 -0
  11. package/dist/host/agents/skills/orkestrel-build-application/agents/openai.yaml +4 -0
  12. package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
  13. package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +64 -0
  14. package/dist/host/agents/skills/orkestrel-harden-package/agents/openai.yaml +4 -0
  15. package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +85 -0
  16. package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +57 -0
  17. package/dist/host/agents/skills/orkestrel-harden-package/references/hardening.md +93 -0
  18. package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +61 -0
  19. package/dist/host/claude/agents/application.md +32 -0
  20. package/dist/host/claude/agents/builder.md +12 -7
  21. package/dist/host/claude/agents/checker.md +15 -8
  22. package/dist/host/claude/agents/codex.md +40 -0
  23. package/dist/host/claude/agents/grok.md +20 -37
  24. package/dist/host/claude/agents/orkestrel.md +89 -223
  25. package/dist/host/claude/agents/planner.md +14 -34
  26. package/dist/host/claude/agents/reviewer.md +39 -24
  27. package/dist/host/claude/agents/verifier.md +15 -10
  28. package/dist/host/claude/rules/application.md +57 -0
  29. package/dist/host/claude/rules/architecture.md +179 -0
  30. package/dist/host/claude/rules/browser.md +28 -0
  31. package/dist/host/claude/rules/documentation.md +64 -0
  32. package/dist/host/claude/rules/names.md +209 -0
  33. package/dist/host/claude/rules/patterns.md +130 -0
  34. package/dist/host/claude/rules/quality.md +45 -0
  35. package/dist/host/claude/rules/styles.md +64 -0
  36. package/dist/host/claude/rules/tests.md +123 -0
  37. package/dist/host/claude/rules/typescript.md +78 -0
  38. package/dist/host/claude/rules/workspace.md +180 -0
  39. package/dist/host/claude/settings.json +116 -0
  40. package/dist/host/claude/skills/orkestrel-align-packages/SKILL.md +10 -0
  41. package/dist/host/claude/skills/orkestrel-build-application/SKILL.md +12 -0
  42. package/dist/host/claude/skills/orkestrel-harden-package/SKILL.md +10 -0
  43. package/dist/host/codex/agents/analyst.toml +15 -0
  44. package/dist/host/codex/agents/application.toml +25 -0
  45. package/dist/host/codex/agents/builder.toml +24 -0
  46. package/dist/host/codex/agents/checker.toml +15 -0
  47. package/dist/host/codex/agents/grok.toml +17 -0
  48. package/dist/host/codex/agents/implementer.toml +17 -0
  49. package/dist/host/codex/agents/orkestrel.toml +16 -0
  50. package/dist/host/codex/agents/planner.toml +20 -0
  51. package/dist/host/codex/agents/reviewer.toml +18 -0
  52. package/dist/host/codex/agents/verifier.toml +14 -0
  53. package/dist/host/codex/config.toml +64 -0
  54. package/dist/host/dotfiles/oxlintrc.json +307 -1
  55. package/dist/host/guides/src/scaffold.md +2059 -2007
  56. package/dist/host/manifest.json +355 -137
  57. package/dist/host/scripts/codex.sh +49 -0
  58. package/dist/host/scripts/cursor.sh +33 -47
  59. package/dist/host/scripts/deps.sh +34 -16
  60. package/dist/host/scripts/ollama.sh +6 -149
  61. package/dist/host/tests/setupPolicy.ts +354 -0
  62. package/dist/src/core/index.cjs +6600 -1199
  63. package/dist/src/core/index.cjs.map +1 -1
  64. package/dist/src/core/index.d.cts +801 -266
  65. package/dist/src/core/index.d.ts +801 -266
  66. package/dist/src/core/index.js +6518 -1191
  67. package/dist/src/core/index.js.map +1 -1
  68. package/dist/src/server/index.cjs +3132 -593
  69. package/dist/src/server/index.cjs.map +1 -1
  70. package/dist/src/server/index.d.cts +643 -95
  71. package/dist/src/server/index.d.ts +643 -95
  72. package/dist/src/server/index.js +3054 -594
  73. package/dist/src/server/index.js.map +1 -1
  74. package/package.json +15 -12
  75. package/dist/host/claude/agents/composer.md +0 -64
  76. package/dist/host/claude/agents/researcher.md +0 -38
  77. package/dist/host/claude/agents/scout.md +0 -35
  78. package/dist/host/github/workflows/ci.yml +0 -64
@@ -1,79 +1,88 @@
1
1
  # Scaffold
2
2
 
3
- > A synchronous, deterministic **package-blueprint compiler** for the `@orkestrel` line:
4
- > a closed, JSON-serializable **`Blueprint`** (name, surfaces, dependencies, overrides…)
5
- > is compiled into a **`Plan`**an ordered list of **`Artifact`**s, each carrying an
6
- > `origin` that says whether its content was host-copied, template-filled, or computed —
7
- > and every downstream product (the files on disk, a review document, an audit of an
8
- > existing package, a dry-run summary) is **projected** from that one `Plan`, never
9
- > authored separately.
3
+ > A deterministic workspace-blueprint compiler: a closed, JSON-serializable `Blueprint` compiles
4
+ > into a `Plan` of ordered `Artifact`s, and every downstream product — the files on disk, a review
5
+ > document, an audit of an existing package, a freshness report is projected from that one plan
6
+ > rather than authored separately.
10
7
  >
11
- > FORWARD: a `Blueprint` is **drafted** into artifacts the per-surface variant matrix as
12
- > data (`SURFACE_MATRIX`) selects the `exports` map, the per-surface configs, and the test
13
- > projects; caller **`overrides`** layer over the shipped defaults the fail-closed
14
- > **gate** validates the name, surfaces, and dependencies, and a passing plan is **pinned**
15
- > (`trace` + `hash` derived from content, never authored).
8
+ > The core face is pure and synchronous: no `node:*`, no clocks, no randomness, no I/O. A plan's
9
+ > `trace` and `hash` derive from its own content. The server face owns the only two impure
10
+ > entities `Materializer`, which writes a plan to disk behind an explicit call, and `Sync`,
11
+ > which reads upstream guides and registry versions over HTTPS. The `scaffold` executable is a
12
+ > thin command-line shell around both.
16
13
  >
17
- > REVERSE: `planToReview` / `planToSummary` render the plan for humans; `diffPlan` audits
18
- > it against a target's current content and returns **drift findings as data**; the server
19
- > surface's `Materializer` is the impure WRITE step.
14
+ > Every discriminant names its own axis. `origin` says how an artifact's content is produced,
15
+ > `group` says which artifact group it belongs to, `environment` says which environment owns it,
16
+ > `category` says what a declared member is, `drift` says how a target compares to its plan,
17
+ > `freshness` says how a mirror compares to upstream, `stage` says which pipeline phase ran, and
18
+ > `code` says which coded failure was raised.
20
19
  >
21
- > LIVE: the server surface's `Sync` entity fetches each declared dependency's guide and
22
- > registry version from upstream reporting freshness, refreshing mirrors under an explicit
23
- > apply the ONLY part of the system that touches the network.
24
- >
25
- > This module runs no `git`, invokes no `npm`, and embeds no LLM; its only network access is
26
- > the server `Sync` entity's read-only fetch of upstream guides and registry versions. The
27
- > core is pure (no `node:*`, no clocks, no randomness `trace` and `hash` derive from
28
- > content alone), and writing lives behind an explicit apply on the server surface. A
29
- > blueprint that fails the gate yields a visible INCOMPLETE `Scaffolding` carrying the
30
- > questions — a half-formed package is worse than a question, so the gate fails closed rather
31
- > than emitting.
32
- > Every discriminant names its axis, never `kind` / `type` (AGENTS §4.4): `origin` splits
33
- > how an artifact's content is produced, `group` splits the artifact groups, `surface`
34
- > splits the environment faces, `category` splits declared members, `drift` splits audit
35
- > verdicts, `freshness` splits sync currency, `stage` splits the pipeline phases, `code`
36
- > splits coded errors. Source: [`src/core`](../../src/core) + [`src/server`](../../src/server)
37
- >
38
- > - the [`src/bin`](../../src/bin) CLI. The core surfaces through `@src/core`, the materializer
39
- > and sync through `@src/server`; the bin is an executable, not a barrel.
40
-
41
- The problem this module solves: standing up (or auditing) an `@orkestrel` package is a
42
- mechanical projection of the line's conventions onto a name — the exports map for the
43
- variant, the per-surface build configs, the barrels, the guide stubs, the parity harness —
44
- and this package IS that projection: creation and repair through the bin's `new` / `pull` /
45
- `audit` / `repair` verbs, and fleet-wide propagation through the bin's `fleet` verb.
46
- Rendered defaults ship as **versioned package data** frozen `TemplateDefinition`s filled
47
- by `@orkestrel/template`'s pure engine so a convention change is a version bump here, not
48
- a hand-edit in every repo. The module is deliberately **mechanism, never policy** (AGENTS
49
- §21): the judgment calls (the name, the description, the keywords, which surfaces, which
50
- dependencies, any template override) belong to the caller — a human, or an agent following
51
- a `/scaffold` command while this module supplies the closed vocabularies, the variant
52
- matrix as data, the exact-record validation, the fail-closed gate, the deterministic pin,
53
- and the lossless projections. Separating the WHAT (the `Blueprint`) from the HOW (the
54
- `Plan` and its writes) is the whole design: because the plan and the audit are pure data,
55
- the same engine that _creates_ a package can **audit** an existing one (`diffPlan` against
56
- its current content the per-file conformance checklist, now returned as
57
- findings) and **repair** only what drifted. Scaffold is the line's conformance engine, not
58
- merely its generator. And because the vendored dependency mirrors and pinned ranges
59
- themselves drift as upstream moves, the server `Sync` entity is the freshness arm it
60
- fetches each declared `@orkestrel` dependency's guide and registry version from upstream and
61
- reports (or, under an explicit apply, refreshes) what has fallen behind.
62
-
63
- The compiler's core stands on four runtime dependencies `@orkestrel/contract` (the shape
64
- DSL behind the `Blueprint` / `Plan` contracts), `@orkestrel/emitter` (the observation
65
- side-channels), `@orkestrel/markdown` (the AST + `renderMarkdown` the guide-table emitter
66
- rides), and `@orkestrel/template` (whose pure `fillTemplate` LEAF carries the rendered
67
- defaults, with NO `TemplateManager` inside the compiler the core stays pure and
68
- stateless). The bin adds two more, consumed ONLY at the executable: `@orkestrel/terminal`
69
- (interactive blueprint prompts) and `@orkestrel/console` (the reporter + spinner). Because
70
- `@orkestrel/terminal` is an L3 package, `@orkestrel/scaffold` sits at **L4** in the line's
71
- dependency layering.
20
+ > Source: [`src/core`](../../src/core) and [`src/server`](../../src/server), with
21
+ > [`src/bin`](../../src/bin) as an executable build target. Core exports through
22
+ > `@orkestrel/scaffold`; the materializer and sync export through `@orkestrel/scaffold/server`.
23
+
24
+ Standing up or auditing a workspace in this style is a mechanical projection of a fixed set of
25
+ conventions onto a name: the exports map for the selected src environments, the per-environment build
26
+ configuration, the barrels, the test projects, the guide stubs, the parity harness. This package is
27
+ that projection, expressed as data. Rendered defaults ship as versioned package data (frozen
28
+ `TemplateDefinition` values filled by a pure fill engine), so a convention change is a version bump
29
+ here rather than a hand edit in every workspace.
30
+
31
+ The module is mechanism, never product policy. The judgment calls the name, the description, the
32
+ keywords, which src and app environments, which dependencies, any template override
33
+ belong to the caller. What this module supplies is the closed vocabularies, the variant matrix as
34
+ data, exact-record validation, a fail-closed gate, a deterministic pin, and lossless projections.
35
+
36
+ Separating the _what_ (the `Blueprint`) from the _how_ (the `Plan` and its writes) is the whole
37
+ design. Because the plan and the audit are pure data, the same engine that creates a workspace can
38
+ audit an existing one `diffPlan` against its current bytes and repair only what drifted. And
39
+ because vendored dependency mirrors and pinned ranges themselves fall behind as upstream moves,
40
+ `Sync` reports (and, under an explicit apply, refreshes) what has aged.
41
+
42
+ ## Faces and dependency direction
43
+
44
+ The package has three code faces. Generated workspaces use the separate `Environment` vocabulary
45
+ (`core`, `browser`, `server`) to identify an environment selected on the `src` or `app` axis; the
46
+ three faces below are this package's own.
47
+
48
+ - **core** [`src/core`](../../src/core), published as `@orkestrel/scaffold`. Pure, synchronous,
49
+ host-independent. Compiling, validating, diffing, projecting, and every rendered default.
50
+ - **server**[`src/server`](../../src/server), published as `@orkestrel/scaffold/server`. Node
51
+ only. Filesystem writes (`Materializer`), upstream fetches (`Sync`), the write-transaction
52
+ machinery, and the host-staging primitive.
53
+ - **bin** [`src/bin`](../../src/bin), built to the `scaffold` executable. Not a barrel and not
54
+ published as a module: it exports nothing to consumers, so it carries no guide parity of its own
55
+ and is documented here in prose.
56
+
57
+ Core imports neither of the others. Server imports core. The bin imports both. The same direction
58
+ is what a generated workspace is held to, and the compiled workspace makes it enforceable rather
59
+ than aspirational:
60
+
61
+ - `src/core` and `app/core` are host-independent — no DOM, no `node:*`, no stylesheet imports.
62
+ - `src/browser` and `app/browser` may import their own core plus browser libraries; they may never
63
+ reach a Node builtin or a `/server` subpath.
64
+ - `src/server` and `app/server` may import their own core plus server libraries; they may never
65
+ reach Vue, a `/browser` subpath, or a stylesheet.
66
+ - Published `src/*` may never import private `app/*`.
67
+ - `app/browser` reaches server behavior only through shared `app/core` contracts and transports,
68
+ never through a server implementation import.
69
+
70
+ A generated `app/server` owns strict `APP_HOST`, `APP_PORT`, and `APP_START_TIMEOUT` parsing, a
71
+ repeat-safe HTTP lifecycle, bounded connection behavior, and process signal cleanup. Its exported
72
+ `reportApplicationServerError` handler writes only a stable configuration, lifecycle, or unknown
73
+ failure code; process-owned diagnostics never serialize a rejected value, nested cause, stack, or
74
+ other error context.
75
+
76
+ Every environment barrel is an export-star barrel: `index.ts` contains only `export * from './x.js'`
77
+ rows and nothing else. Named, default, namespace, and type-only barrel rows are absent by design,
78
+ so a star-export collision is a naming failure to fix at the owner rather than something to paper
79
+ over with a selective row. Both of this package's own barrels follow that rule, and every generated
80
+ barrel is emitted the same way.
72
81
 
73
82
  ## Surface
74
83
 
75
- Compile a `Blueprint` into a `Scaffolding`, then project the `Plan` it carries the whole
76
- core path is pure and synchronous; writing lives on the server surface:
84
+ Compile a blueprint into a `Scaffolding`, then project the `Plan` it carries. The whole core path
85
+ is pure and synchronous; writing lives on the server face.
77
86
 
78
87
  ```ts
79
88
  import { blueprint, createCompiler, dependency, planToReview } from '@orkestrel/scaffold'
@@ -82,16 +91,16 @@ const compiler = createCompiler()
82
91
 
83
92
  const scaffolding = compiler.compile(
84
93
  blueprint('router', {
85
- description: 'A tiny hash-router. Part of the @orkestrel line.',
86
- keywords: ['router', 'hash', 'spa'],
87
- surfaces: ['core', 'browser', 'server'],
88
- dependencies: [dependency('@orkestrel/contract', '^0.0.5')],
94
+ description: 'A tiny hash router.',
95
+ keywords: ['router', 'hash'],
96
+ src: ['core', 'browser', 'server'],
97
+ dependencies: [dependency('@orkestrel/contract', '^0.0.7')],
89
98
  }),
90
99
  )
91
100
 
92
101
  scaffolding.complete // true — the gate passed
93
102
  if (scaffolding.plan) {
94
- scaffolding.plan.artifacts.length // every file the package needs, ordered
103
+ scaffolding.plan.artifacts.length // every file the workspace needs, ordered
95
104
  planToReview(scaffolding.plan) // the copy-ready dry-run review document
96
105
  }
97
106
 
@@ -99,2092 +108,2135 @@ compiler.emitter.on('block', (questions) => questions.length)
99
108
  compiler.destroy()
100
109
  ```
101
110
 
102
- `compile()` is genuinely SYNCHRONOUS and runs the fixed three-stage pipeline
103
- `[draft, gate, pin]`; a failing gate yields a visible INCOMPLETE `Scaffolding` (`plan`
104
- absent, `questions` populated) rather than throwing. Compilation, review, summary, and
105
- audit are ALL pure — no clocks, no randomness, no `node:*`, no I/O. The package has THREE
106
- faces: the pure **core** (`@orkestrel/scaffold`), the server face
107
- (`@orkestrel/scaffold/server`) — the impure `Materializer` writes and the `Sync` fetches —
108
- and the **bin** CLI (`src/bin/scaffold.ts`, the `scaffold` executable). The Surface below
109
- documents the two LIBRARY faces, marked **(server)** where server-only; the bin is an
110
- EXECUTABLE, not a barrel — it exports NO public members, so `SURFACES` stays closed at three
111
- (`Surface` names the SCAFFOLDED package's environment faces, unrelated to scaffold's own
112
- three code faces). The core and the `Materializer` are deterministic and synchronous; the
113
- bin AND the server `Sync` entity are legitimately Promise-based — the bin's interactive
114
- prompt flow (`@orkestrel/terminal`) and `Sync`'s upstream fetches are async orchestration
115
- AROUND the synchronous `compile` / write, never inside them.
116
-
117
- ### Types
118
-
119
- | Type | Kind | Shape |
120
- | ----------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
121
- | `Surface` | type | `'core' \| 'browser' \| 'server'` — the environment surface an artifact or member belongs to (the SCAFFOLDED package's faces, not scaffold's own). |
122
- | `Origin` | type | `'host' \| 'template' \| 'computed'` — how an `Artifact`'s content is produced: `host` byte-copied from the vendored data root, `template` filled from a frozen `TemplateDefinition` by `@orkestrel/template`'s pure fill engine, `computed` derived by the core's manifest/exports combination logic; the axis that decides whether it carries `source` (host) or `content` (template / computed). |
123
- | `Group` | type | `'manifest' \| 'configs' \| 'source' \| 'tests' \| 'guides' \| 'docs' \| 'orchestration'` — the closed artifact-group vocabulary a plan selects over. |
124
- | `Category` | type | `'type' \| 'constant' \| 'factory' \| 'entity'` — what a declared `Member` IS in the scaffolded surface. |
125
- | `CatalogEntry` | interface | `{ name, version, description }` — one fleet package's catalog row; `description` is the flattened text of that package's guide's FIRST blockquote, `''` when the guide is missing, unreadable, or carries no blockquote. |
126
- | `Drift` | type | `'aligned' \| 'stale' \| 'missing' \| 'foreign'` — one `Finding`'s verdict against the target's current content. |
127
- | `Freshness` | type | `'current' \| 'behind' \| 'missing' \| 'failed'` — one `GuideSync` / `VersionSync`'s currency against upstream (`missing` = an upstream `404`, `failed` = a transport fault). |
128
- | `CompileStage` | type | `'draft' \| 'gate' \| 'pin'` — the three fixed pipeline phases, in order. |
129
- | `ScaffoldErrorCode` | type | `'INVALID' \| 'BLOCKED' \| 'DESTROYED' \| 'TARGET' \| 'WRITE' \| 'FETCH'` — coded `ScaffoldError` reasons. |
130
- | `Dependency` | interface | `{ name, range, optional? }` — one runtime `@orkestrel/*` dependency; drives its `package.json` entry, the build externals, and its `guides/src/<dep>.md` mirror — byte-correct for a dep this package vendors (contract / emitter / markdown / template / terminal / console / guide), a `host`-origin POINTER the caller syncs otherwise. `optional` is meaningful only when the `Dependency` appears in a `Blueprint`'s `peers` — `true` emits a `peerDependenciesMeta` `{ optional: true }` entry alongside it. |
131
- | `Override` | interface | `{ path, content }` — one caller template override; `content` REPLACES the rendered artifact at `path`, never partially merges. An override whose `path` matches no planned artifact, or targets a `host`-origin path, is a BLOCKING question — never a silent add. |
132
- | `Blueprint` | interface | `{ name, description, keywords, surfaces, dependencies, peers, extras, version, engines, overrides }` — the closed, JSON-serializable package spec. `peers` are runtime `@orkestrel/*` peers emitted as `peerDependencies` (an `optional` peer also gets a `peerDependenciesMeta` entry); `extras` are package-specific `devDependencies` merged into the generated uniform baseline (extras win on a name collision) — the middleware pattern of shipping `@orkestrel/{database,router,server}` for its tests. |
133
- | `Member` | interface | `{ name, category, summary, surface }` — one declared public export of the scaffolded package; derived by `blueprintToMembers`, never authored. |
134
- | `Artifact` | interface | `{ path, group, origin, surface?, content?, source? }` — one file in a `Plan`; `content` present for `template` / `computed`, `source` (a host-relative path) for `host`. |
135
- | `Plan` | interface | `{ blueprint, groups, artifacts, trace?, hash? }` — the compiled, ordered artifact list plus the selection it covers; `trace` / `hash` filled by the pin. |
136
- | `Finding` | interface | `{ path, group, drift }` — one audit drift result. |
137
- | `Audit` | interface | `{ findings, clean, complete, questions, drifted, missing, foreign }` — the whole diff of a plan against a target's content; a `Compiler.audit` over a gate-failing blueprint sets `complete: false` with the gate's `questions` and zero findings, while `diffPlan` over an existing plan is always `complete: true`. |
138
- | `Question` | interface | `{ field, text, blocking, candidates? }` — one validation issue; `blocking: true` fails the gate closed, `false` is an advisory that rides a complete result. |
139
- | `Validation` | interface | `{ valid, questions, warnings }` — the semantic pass over a blueprint; returns, never throws. |
140
- | `GuideSync` | interface | `{ name, path, content, freshness, note? }` — one dependency guide fetched from upstream (`content`) at its `path`, plus its `freshness` verdict against the caller-supplied reference (see `guides`). `note` carries the failure/anomaly CAUSE (an `HTTP <status>`, a transport message, a redirect-blocked or oversized-body notice) on any non-`current` outcome that has one — absent on `current` and `behind`. |
141
- | `VersionSync` | interface | `{ name, range, latest, freshness, note? }` — one dependency's declared `range` against the registry `latest`, plus its `freshness` verdict. `note` carries the failure/anomaly CAUSE — see `GuideSync.note`. |
142
- | `SyncReport` | interface | `{ target, guides, versions, clean, failed }` — the whole outcome of a `Sync.pull`: the fetched `guides` + `versions`, `clean` (no drift AND no failures), and the `failed` count. |
143
- | `PlanSummary` | interface | `{ name, surfaces, groups, artifacts, host, template, computed }` — the dry-run tally. |
144
- | `CompileRecord` | interface | `{ stage, input, output, failed, error? }` — a structured input/output snapshot of one pipeline phase. |
145
- | `CompileFailure` | interface | `{ stage, code, message }` — a visible marker for a stage that failed. |
146
- | `Scaffolding` | interface | `{ blueprint, plan?, questions, stages, failures, complete, digest }` — the full, replayable outcome of one `compile()` call. |
147
- | `PlanRecord` | interface | `{ id, plan, version, hash }` — a versioned, content-hashed `Plan` inside a `PlanManager`. |
148
- | `MaterializeResult` | interface | `{ target, written, copied, skipped, removed }` — the outcome of one materialization **(server)**; `removed` lists paths a `prune` deleted. |
149
- | `ManifestEntry` | interface | `{ storage, destination, executable }` — one entry of the vendored `host/manifest.json` **(server)**; `storage` is the un-dotted vendored path, `destination` the destination-relative path a caller-supplied artifact `source` resolves against, `executable` a post-copy `chmod` bit. |
150
- | `CompilerEventMap` | type | `Compiler`'s push observation surface (AGENTS §13) — `compile(scaffolding)` · `audit(audit)` · `block(questions)` · `error(error)` · `destroy()`. |
151
- | `CompilerOptions` | interface | `{ on?, error? }` — input to `createCompiler`. |
152
- | `CompilerInterface` | interface | The compilation orchestrator contract — `emitter` + `compile` / `audit` / `destroy`. |
153
- | `PlanManagerEventMap` | type | `PlanManager`'s push observation surface — `add(id)` · `remove(id)` · `destroy()`. |
154
- | `PlanManagerOptions` | interface | `{ plans?, on?, error? }` — input to `createPlanManager`. |
155
- | `PlanManagerInterface` | interface | The plan registry contract (AGENTS §9) — `emitter` / `size` + `has` / `plan` / `plans` / `add` / `remove` / `destroy`. |
156
- | `MaterializerEventMap` | type | `Materializer`'s push observation surface **(server)** — `copy(path)` · `write(path)` · `remove(path)` · `done(result)` · `error(error)` · `destroy()`. |
157
- | `MaterializerOptions` | interface | `{ host?, on?, error? }` — input to `createMaterializer` **(server)**; `host` is the vendored-data root host-origin artifacts are copied FROM, defaulting to the PACKAGE's own vendored `dist/host` — resolved from the installed module's own location via a file URL, never `process.cwd()` — so a caller-supplied `host` (a sibling repo, for `mirror`) maps 1:1 onto the same host-relative paths. |
158
- | `MaterializerInterface` | interface | The materialization contract **(server)** — `emitter` + `materialize` / `repair` / `prune` / `destroy`. |
159
- | `SyncEventMap` | type | `Sync`'s push observation surface **(server)** — `guide(name)` · `version(name)` · `package(name, note)` · `write(path)` · `done(report)` · `error(error)` · `destroy()`. |
160
- | `SyncOptions` | interface | `{ on?, error?, guides?, registry?, concurrency?, retries?, strict?, limit? }` — input to `createSync` **(server)**; the endpoint bases + branch are INJECTABLE (`guides.base` default `raw.githubusercontent.com`, `guides.branch` default `main`, `registry.base` default `registry.npmjs.org`, `guides.timeout` / `registry.timeout` default 10s — `registry.base` also anchors `catalog()`'s org package-list + packument fetches), `concurrency` default 6, `retries` default 0, `strict` default false, `limit` (max response body bytes; declared `Content-Length` or streamed total, whichever trips first — an overflow is a transport fault) default 5,242,880 (5 MiB). Every fetch is UNAUTHENTICATED — no token option exists; every fleet repo is public. |
161
- | `SyncInterface` | interface | The upstream-synchronization contract **(server)** — `emitter` + `guides` / `versions` / `catalog` / `pull` / `write` / `destroy`. |
162
-
163
- The `Blueprint` and the `Plan` are the two closed contracts — every field is a `string`,
164
- `readonly` array, or record, so both round-trip JSON and both cross a tool / RPC boundary
165
- unchanged. `Artifact` is discriminated by `origin`: a `host` artifact names a `source` (the
166
- host-relative path the server byte-copies from the vendored data root) and carries NO
167
- `content` (the pure core never reads host bytes); a `template` artifact carries `content`
168
- FILLED from a frozen `TemplateDefinition` by `@orkestrel/template`'s pure `fillTemplate`
169
- engine (`missing: 'error'` — an unresolved token fails loud, never silently blanks); a
170
- `computed` artifact carries `content` DERIVED by the core's own manifest/exports combination logic
171
- (the `exports` map, the entry re-pointing). The token-collision boundary is a HARD rule:
172
- only genuinely templated PROSE artifacts (the README, the guide stubs, file headers) pass
173
- through the fill engine; a STRUCTURAL file (a JSON or TS config, anything that could
174
- legitimately contain a literal `{{…}}`) is ALWAYS `computed`, never `template`, so a stray
175
- `{{` in a tsconfig can never be mistaken for a placeholder. That single `origin` axis is
176
- what keeps the core pure while still describing files it cannot itself read.
177
-
178
- ### Constants
179
-
180
- | API | Kind | Summary |
181
- | ------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
182
- | `SURFACES` | const | The three `Surface` values, frozen — compose with `literalOf(...)` / `parseEnum(...)`. |
183
- | `ORIGINS` | const | The three `Origin` values, frozen. |
184
- | `GROUPS` | const | The seven `Group` values, frozen — the artifact-group selection vocabulary. |
185
- | `CATEGORIES` | const | The four `Category` values, frozen. |
186
- | `FRESHNESS` | const | The four `Freshness` values, frozen — the currency axis `Sync` reports on. |
187
- | `COMPILE_STAGES` | const | `['draft', 'gate', 'pin']`, frozen — the pipeline phases in order. |
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
- | `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.2'` — the exact devDependency range a scaffolded package's `package.json` pins `@orkestrel/scaffold` at. |
191
- | `NAME_PATTERN` | const | The `/^[a-z][a-z0-9-]*$/` package-name RegExp, closed vocabulary as data. |
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
- | `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
- | `DEFAULT_VERSION` | const | `'0.0.1'` — the starting version the `blueprint` builder fills. |
195
- | `DEFAULT_ENGINES` | const | `'>=22'` — the `engines.node` range the `blueprint` builder fills. |
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. |
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. |
111
+ An application-only blueprint uses an empty published set and an independent app set:
200
112
 
201
113
  ```ts
202
- import {
203
- CATEGORIES,
204
- DEPENDENCY_NAME_PATTERN,
205
- EXTRA_NAME_PATTERN,
206
- FRESHNESS,
207
- GROUPS,
208
- HOST_PATHS,
209
- NAME_PATTERN,
210
- ORIGINS,
211
- SCAFFOLD_RANGE,
212
- SURFACES,
213
- TEMPLATES,
214
- } from '@orkestrel/scaffold'
215
-
216
- SURFACES // ['core', 'browser', 'server']
217
- ORIGINS // ['host', 'template', 'computed']
218
- GROUPS // ['manifest', 'configs', 'source', 'tests', 'guides', 'docs', 'orchestration']
219
- CATEGORIES // ['type', 'constant', 'factory', 'entity']
220
- FRESHNESS // ['current', 'behind', 'missing', 'failed']
221
- SCAFFOLD_RANGE // '^0.0.2' — the pinned devDependency range for @orkestrel/scaffold
222
- NAME_PATTERN.test('router') // true
223
- NAME_PATTERN.test('Router') // false — the package-name law rejects a leading capital
224
- DEPENDENCY_NAME_PATTERN.test('@orkestrel/contract') // true
225
- DEPENDENCY_NAME_PATTERN.test('@orkestrel/../etc') // false — closes the traversal vector
226
- EXTRA_NAME_PATTERN.test('zod') // true — extras accept any valid npm package name
227
- EXTRA_NAME_PATTERN.test('@types/node') // true
228
- EXTRA_NAME_PATTERN.test('../etc') // false — still structurally traversal-closed
229
- HOST_PATHS.includes('scripts/deps.sh') // true — orchestration-grouped host artifact
230
- HOST_PATHS.includes('src/core/index.ts') // false — the host set covers shared artifacts, not source
231
- TEMPLATES.entity.placeholders // [{ name: 'pascal', … }] — the entity stub's one token
232
- ```
114
+ import { blueprint, blueprintToPlan } from '@orkestrel/scaffold'
233
115
 
234
- A closed-set field that does not fit a listed value is a signal the request is mis-scoped,
235
- not licence to invent a value — the exact-record validators below reject an off-vocabulary
236
- literal, and the shapers compile the same tuples into the JSON Schema `enum`s, so the
237
- vocabulary cannot drift between the guard, the parser, and the schema.
238
-
239
- ### Errors
240
-
241
- | API | Kind | Summary |
242
- | ----------------- | -------- | --------------------------------------------------- |
243
- | `ScaffoldError` | class | Carries a `ScaffoldErrorCode` + optional `context`. |
244
- | `isScaffoldError` | function | Narrow a caught value to a `ScaffoldError`. |
245
-
246
- ```ts
247
- import { isScaffoldError, ScaffoldError } from '@orkestrel/scaffold'
116
+ const workspace = blueprint('console', {
117
+ src: [],
118
+ app: ['core', 'browser', 'server'],
119
+ })
248
120
 
249
- try {
250
- throw new ScaffoldError('INVALID', 'Blueprint failed the exact-record contract')
251
- } catch (error) {
252
- if (isScaffoldError(error)) error.code // 'INVALID'
253
- }
121
+ const plan = blueprintToPlan(workspace)
122
+ plan.artifacts.some((artifact) => artifact.path === 'app/browser/index.html') // true
123
+ plan.artifacts.some((artifact) => artifact.path === 'app/server/main.ts') // true
254
124
  ```
255
125
 
256
- Throws are reserved for caller misuse (AGENTS §12): `createBlueprint` on off-contract data
257
- throws `INVALID`, any method after `destroy()` throws `DESTROYED`, and on the server surface
258
- a non-vacant target throws `TARGET` while a failed write throws `WRITE`. A failing gate is
259
- NOT an error — it fails closed into an incomplete `Scaffolding` whose `failures` carry a
260
- `BLOCKED` marker, mirroring the brief compiler's visible-incomplete outcome. `FETCH` is
261
- server/bin-only: the `Sync` entity throws it ONLY under `strict` mode when an upstream fetch
262
- fails (or on wrap-level misuse), naming the failing URL in `context`; in the default COLLECT
263
- mode a per-dependency `404` becomes `freshness: 'missing'` and any transport error / other
264
- non-2xx becomes `freshness: 'failed'`, captured on the `SyncReport` rather than thrown.
265
-
266
- ### Validators
267
-
268
- Total guards (AGENTS §14) COMPILED from the shapers below via the contract package's
269
- `createContract` one shape declaration is the single source, so `isBlueprint`,
270
- `parseBlueprint`, and the JSON Schema can never drift. Adversarial input (junk, cycles,
271
- hostile prototypes) returns `false`, never throws. Every record guard is EXACT: an extra
272
- key fails, which is why the builders below omit absent optional keys.
273
-
274
- | API | Kind | Narrows to |
275
- | -------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
276
- | `isDependency` | const | `Dependency` — `name` a non-empty string, `range` a non-empty string, `optional` (when present) a boolean. |
277
- | `isOverride` | const | `Override` — `path` / `content` non-empty strings. |
278
- | `isBlueprint` | const | `Blueprint` — `surfaces` on-vocabulary and non-empty; `name` a non-empty string (the `NAME_PATTERN` law is the semantic pass's, not the guard's). |
279
- | `isMember` | const | `Member` — `category` an on-vocabulary `Category`, `surface` an on-vocabulary `Surface`. |
280
- | `isArtifact` | const | `Artifact` — `group` / `origin` on-vocabulary; `content` xor `source` per `origin`. |
281
- | `isPlan` | const | `Plan` — the whole exact-record contract, section guards composed. |
282
- | `isSyncReport` | const | `SyncReport` — the whole exact-record sync contract, `guide` / `version` sections composed. |
126
+ ### Types core
127
+
128
+ From [`types.ts`](../../src/core/types.ts).
129
+
130
+ | Name | Kind |
131
+ | ---------------------- | --------- |
132
+ | `Environment` | type |
133
+ | `BuildFormat` | type |
134
+ | `SrcDefinition` | interface |
135
+ | `AppDefinition` | interface |
136
+ | `Origin` | type |
137
+ | `Group` | type |
138
+ | `Category` | type |
139
+ | `CatalogEntry` | interface |
140
+ | `Drift` | type |
141
+ | `Freshness` | type |
142
+ | `CompileStage` | type |
143
+ | `ScaffoldErrorCode` | type |
144
+ | `Dependency` | interface |
145
+ | `Override` | interface |
146
+ | `Blueprint` | interface |
147
+ | `Member` | interface |
148
+ | `ArtifactBase` | interface |
149
+ | `HostArtifact` | interface |
150
+ | `ContentArtifact` | interface |
151
+ | `Artifact` | type |
152
+ | `Snapshot` | type |
153
+ | `Plan` | interface |
154
+ | `Finding` | interface |
155
+ | `Audit` | interface |
156
+ | `Question` | interface |
157
+ | `Validation` | interface |
158
+ | `GuideSync` | interface |
159
+ | `VersionSync` | interface |
160
+ | `SyncReport` | interface |
161
+ | `PlanSummary` | interface |
162
+ | `CompileRecord` | interface |
163
+ | `CompileFailure` | interface |
164
+ | `Scaffolding` | interface |
165
+ | `PlanRecord` | interface |
166
+ | `CompilerEventMap` | type |
167
+ | `CompilerOptions` | interface |
168
+ | `CompilerInterface` | interface |
169
+ | `PlanManagerEventMap` | type |
170
+ | `PlanManagerOptions` | interface |
171
+ | `PlanManagerInterface` | interface |
172
+
173
+ The closed vocabularies are small and total. `Environment` is `'core' | 'browser' | 'server'`.
174
+ `BuildFormat` is `'es' | 'cjs'`. `Origin` is `'host' | 'template' | 'computed'`. `Group` is
175
+ `'manifest' | 'configs' | 'source' | 'tests' | 'guides' | 'docs' | 'orchestration'`. `Category` is
176
+ `'type' | 'alias' | 'constant' | 'factory' | 'entity' | 'parser' | 'guard' | 'handler' | 'error'`.
177
+ `Drift` is `'aligned' | 'stale' | 'missing' | 'foreign'`. `Freshness` is
178
+ `'current' | 'behind' | 'missing' | 'failed'`, where `missing` is an upstream `404` and `failed` is
179
+ a transport fault. `CompileStage` is `'draft' | 'gate' | 'pin'`, in that order. `ScaffoldErrorCode`
180
+ is `'INVALID' | 'BLOCKED' | 'DESTROYED' | 'TARGET' | 'WRITE' | 'FETCH'`.
181
+
182
+ `SrcDefinition` and `AppDefinition` are the per-environment matrix rows: the configuration files an
183
+ environment contributes, its test-project label, and — on the `src` axis — its `exports` subpath
184
+ and build formats, or — on the `app` axis — its optional runtime entry.
185
+
186
+ `Blueprint` is the closed input spec:
283
187
 
284
188
  ```ts
285
- import {
286
- blueprint,
287
- isBlueprint,
288
- isDependency,
289
- isPlan,
290
- validateBlueprint,
291
- } from '@orkestrel/scaffold'
292
-
293
- isDependency({ name: '@orkestrel/contract', range: '^0.0.5' }) // true
294
- isBlueprint({ name: 'router', surfaces: ['core'] }) // false — sections missing (exact record)
295
-
296
- // NAME_PATTERN is the semantic pass's job, not the shape's — so the guard passes an
297
- // off-pattern name and validateBlueprint is what rejects it:
298
- const offPattern = blueprint('Router', { surfaces: ['core'] }) // a complete spec; name off NAME_PATTERN
299
- isBlueprint(offPattern) // true — the shape polices STRUCTURE only
300
- validateBlueprint(offPattern).valid // false — the semantic pass owns the NAME_PATTERN law
301
-
302
- isPlan({ blueprint: {}, groups: [], artifacts: [] }) // false — blueprint off-contract
189
+ interface Blueprint {
190
+ readonly name: string
191
+ readonly description?: string
192
+ readonly keywords: readonly string[]
193
+ readonly src: readonly Environment[]
194
+ readonly app: readonly Environment[]
195
+ readonly dependencies: readonly Dependency[]
196
+ readonly peers: readonly Dependency[]
197
+ readonly extras: readonly Dependency[]
198
+ readonly version: string
199
+ readonly engines: string
200
+ readonly overrides: readonly Override[]
201
+ readonly engine: boolean
202
+ }
303
203
  ```
304
204
 
305
- ### Parsers
205
+ `src` selects published library environments under `src`; `app` selects private runtime environments under
206
+ `app`. The two axes are independent, so library-only, application-only, and mixed workspaces are
207
+ all first class. `dependencies` and `peers` are runtime `@orkestrel/*` packages — a peer flagged
208
+ `optional` also gets a `peerDependenciesMeta` entry. `extras` are package-specific development
209
+ dependencies merged over the generated baseline, and may carry any valid npm package name. `engine`
210
+ is structural, never inferred from a name: it is `true` only for a workspace that ships its own
211
+ `src/bin`, and it alone turns on the self-hosting extras (a `bin` field, the `scaffold` script
212
+ pointed at the built executable, the bin check, test, and build scripts, `build:host`, and the
213
+ `src:bin` test project).
214
+
215
+ `Override` replaces a rendered artifact's content at a path, never partially merges it. `Member` is
216
+ one declared public export of the scaffolded workspace, derived rather than authored.
217
+
218
+ `Artifact` is origin-discriminated. `ArtifactBase` carries `path`, `group`, and an optional
219
+ `environment`. A `HostArtifact` has `origin: 'host'`, an optional `source` (defaulting to `path`), and
220
+ an optional `hex` of exact lowercase bytes; it never carries `content`. A `ContentArtifact` has
221
+ `origin: 'template' | 'computed'` and always carries `content`; it never carries `hex` or `source`.
222
+ `Snapshot` is `Readonly<Record<string, string>>` — exact lowercase hexadecimal target bytes keyed
223
+ by artifact-relative path.
224
+
225
+ `Plan` carries the originating `blueprint`, the `groups` it covers, the ordered `artifacts`, and the
226
+ `trace` and `hash` the pin fills. The trace names both independent axes as `src:<selection>` and
227
+ `app:<selection>`, using `none` when one axis is empty, so app-only and mixed plans stay
228
+ self-describing. `PlanSummary` is the dry-run tally by origin and carries both selections. `Finding` is one
229
+ drift verdict with an optional bounded `observed` byte hex for a stale destination, and `Audit` is
230
+ the whole diff plus its `clean` and `complete` flags, `questions`, and `drifted` / `missing` /
231
+ `foreign` counts. `Question` is one validation issue; `blocking: true` fails the gate closed while
232
+ `false` rides a complete result as an advisory. `Validation` is the semantic pass result and never
233
+ throws.
234
+
235
+ `Scaffolding` is the replayable outcome of one compile: the `blueprint`, the `plan` when complete,
236
+ the accumulated `questions`, one `CompileRecord` per stage, any `CompileFailure` markers, the
237
+ `complete` flag, and the content `digest`. `PlanRecord` is a versioned, content-hashed plan inside a
238
+ `PlanManager`.
239
+
240
+ `GuideSync`, `VersionSync`, and `SyncReport` are the freshness shapes. `GuideSync` carries the
241
+ fetched `content`, its `freshness`, an optional `note` explaining a non-clean outcome, and an
242
+ optional `baseline` — the SHA-256 of the observed local mirror, or the literal `absent`, present
243
+ only on target-aware pulls. `VersionSync` compares a declared `range` to the registry `latest`.
244
+ `SyncReport` is `clean` only when nothing drifted and nothing failed. `CatalogEntry` is one fleet
245
+ package row; its `description` is the flattened text of that package's own guide's first
246
+ blockquote, and the empty string when that guide is missing, unreadable, or carries no blockquote.
247
+
248
+ `CompilerEventMap`, `CompilerOptions`, and `CompilerInterface` are the compiler triad;
249
+ `PlanManagerEventMap`, `PlanManagerOptions`, and `PlanManagerInterface` are the registry triad.
250
+ Both options records take `on` initial listeners and an `error` listener-failure handler, and
251
+ `PlanManagerOptions` additionally seeds `plans`.
252
+
253
+ ### Types — server
254
+
255
+ From [`types.ts`](../../src/server/types.ts).
256
+
257
+ | Name | Kind |
258
+ | ----------------------- | --------- |
259
+ | `MaterializeResult` | interface |
260
+ | `MaterializerEventMap` | type |
261
+ | `MaterializerOptions` | interface |
262
+ | `ManifestEntry` | interface |
263
+ | `HostManifest` | interface |
264
+ | `WriteExpectation` | interface |
265
+ | `WritePrecondition` | interface |
266
+ | `WriteAnchor` | interface |
267
+ | `WriteDirectoryResult` | interface |
268
+ | `SyncAllowance` | type |
269
+ | `CatalogAllowance` | type |
270
+ | `SyncBase` | type |
271
+ | `SyncBranch` | type |
272
+ | `GuideWrite` | interface |
273
+ | `MaterializerInterface` | interface |
274
+ | `SyncEventMap` | type |
275
+ | `SyncOptions` | interface |
276
+ | `SyncInterface` | interface |
277
+
278
+ `MaterializeResult` reports the `target` plus the `written`, `copied`, `skipped`, and `removed`
279
+ paths of one call. `MaterializerOptions` accepts a `host` root override plus emitter `on` hooks and
280
+ an `error` handler; the default host is this package's own vendored data root, resolved from the
281
+ installed module's own location rather than the caller's working directory. A caller-supplied host
282
+ pointing at a raw repository root — one with no `manifest.json` beside it — maps artifact paths 1:1
283
+ instead of through the manifest.
284
+
285
+ `ManifestEntry` is one vendored-host file record — its un-dotted `storage` name, its `destination`
286
+ relative to a target, and an `executable` bit. `HostManifest` pairs the sorted file `entries` with
287
+ the complete sorted directory `roots` inventory, so a destructive consumer can tell a
288
+ declared-empty root from a truncated manifest.
289
+
290
+ The write-transaction shapes are the fail-closed mutation vocabulary. `WriteExpectation` is one
291
+ destination snapshot captured before mutation (`absent`, `file`, or `directory`, with device,
292
+ inode, modification time, size, and digest where they apply). `WritePrecondition` is the narrower
293
+ caller-observed state a transaction must still match. `WriteAnchor` is a physical directory
294
+ identity, and `WriteDirectoryResult` pairs the final anchor with the subset a call created.
295
+ `GuideWrite` pairs one validated guide update with its contained destination. `SyncAllowance` and
296
+ `CatalogAllowance` are one-cell `Float64Array` allowances: the former shares a byte budget across
297
+ concurrent network readers, while the latter shares one entry budget across every fleet root and
298
+ child visited by a catalog operation. `SyncBase` and `SyncBranch` are normalized strings returned
299
+ only by their corresponding boundary parsers.
300
+
301
+ `SyncOptions` groups the injectable endpoints under the entity they configure — `guides` with
302
+ `base`, `branch`, and `timeout`; `registry` with `base` and `timeout` — alongside `concurrency`,
303
+ `retries`, `strict`, `limit`, `items`, `budget`, and the emitter `on` and `error` keys.
304
+
305
+ ### Constants — core
306
+
307
+ From [`constants.ts`](../../src/core/constants.ts).
308
+
309
+ | Name | Kind |
310
+ | --------------------------------- | ----- |
311
+ | `ENVIRONMENTS` | const |
312
+ | `ORIGINS` | const |
313
+ | `GROUPS` | const |
314
+ | `CATEGORIES` | const |
315
+ | `FRESHNESS` | const |
316
+ | `COMPILE_STAGES` | const |
317
+ | `SRC_MATRIX` | const |
318
+ | `APP_MATRIX` | const |
319
+ | `HOST_PATHS` | const |
320
+ | `NAME_PATTERN` | const |
321
+ | `MAX_NAME_LENGTH` | const |
322
+ | `MAX_DEPENDENCY_NAME_LENGTH` | const |
323
+ | `MAX_PATH_LENGTH` | const |
324
+ | `CONTROL_CHARACTER_PATTERN` | const |
325
+ | `INVALID_PATH_CHARACTER_PATTERN` | const |
326
+ | `MAX_RANGE_LENGTH` | const |
327
+ | `MAX_COLLECTION_ITEMS` | const |
328
+ | `MAX_DATA_GRAPH_NODES` | const |
329
+ | `MAX_DATA_GRAPH_KEYS` | const |
330
+ | `VERSION_PATTERN` | const |
331
+ | `ORKESTREL_RANGE_PATTERN` | const |
332
+ | `EXTRA_RANGE_PATTERN` | const |
333
+ | `ENGINES_PATTERN` | const |
334
+ | `MINIMUM_NODE_VERSION` | const |
335
+ | `EXPORT_KEYWORD` | const |
336
+ | `CONST_KEYWORD` | const |
337
+ | `IMPORT_KEYWORD` | const |
338
+ | `FUNCTION_KEYWORD` | const |
339
+ | `HEX_PATTERN` | const |
340
+ | `MAX_ARTIFACT_BYTES` | const |
341
+ | `MAX_TOTAL_ARTIFACT_BYTES` | const |
342
+ | `MAX_SERIALIZED_INPUT_BYTES` | const |
343
+ | `MAX_MANIFEST_BYTES` | const |
344
+ | `MAX_ARTIFACT_HEX_LENGTH` | const |
345
+ | `SYNC_BASELINE_PATTERN` | const |
346
+ | `DEPENDENCY_NAME_PATTERN` | const |
347
+ | `EXTRA_NAME_PATTERN` | const |
348
+ | `DEFAULT_VERSION` | const |
349
+ | `DEFAULT_ENGINES` | const |
350
+ | `SCAFFOLD_RANGE` | const |
351
+ | `BASE_DEV_DEPENDENCIES` | const |
352
+ | `SOURCE_BROWSER_DEV_DEPENDENCIES` | const |
353
+ | `APP_BROWSER_DEV_DEPENDENCIES` | const |
354
+ | `CHECKOUT_ACTION_SHA` | const |
355
+ | `SETUP_NODE_ACTION_SHA` | const |
356
+ | `COMPILER_ID` | const |
357
+ | `TYPESCRIPT_EXTENSIONS` | const |
358
+ | `JSON_PRINT_WIDTH` | const |
359
+ | `JSON_TAB_WIDTH` | const |
360
+
361
+ `ENVIRONMENTS`, `ORIGINS`, `GROUPS`, `CATEGORIES`, `FRESHNESS`, and `COMPILE_STAGES` are the frozen
362
+ value lists behind their literal unions. `SRC_MATRIX` is the `src` environment matrix as
363
+ data — each environment's `configs/src` files, test-project label, `exports` subpath, and build
364
+ formats. `APP_MATRIX` is its application sibling, adding the runtime entry where an environment produces
365
+ one (`app/browser/index.html`, `app/server/main.ts`). `HOST_PATHS` is the ordered list of
366
+ byte-copied host artifacts.
367
+
368
+ The bounds are public because they are part of the contract, not implementation trivia.
369
+ `MAX_ARTIFACT_BYTES` caps one artifact at 5 MiB and `MAX_TOTAL_ARTIFACT_BYTES` caps one blueprint,
370
+ plan, audit, or report at 100 MiB in aggregate. `MAX_SERIALIZED_INPUT_BYTES` is four times that
371
+ aggregate ceiling so serialized hexadecimal records have a bounded envelope before JSON parsing,
372
+ and `MAX_MANIFEST_BYTES` caps every package or host manifest at 1 MiB.
373
+ `MAX_ARTIFACT_HEX_LENGTH` is the hexadecimal form of the per-artifact bound.
374
+ `MAX_COLLECTION_ITEMS` bounds one public collection at 1,000 entries.
375
+ `MAX_DATA_GRAPH_NODES` and `MAX_DATA_GRAPH_KEYS` cap recursive ownership inspection even when an
376
+ adversarial proxy produces a fresh identity at every step.
377
+ `MAX_NAME_LENGTH` is 203 so the published scoped name fits npm's 214-character limit, which
378
+ `MAX_DEPENDENCY_NAME_LENGTH` records directly. `MAX_PATH_LENGTH` and `MAX_RANGE_LENGTH` bound
379
+ serialized path and range tokens.
380
+
381
+ The patterns are the shape laws. `NAME_PATTERN` is the lowercase, letter-first workspace name.
382
+ `DEPENDENCY_NAME_PATTERN` closes `dependencies` and `peers` to `@orkestrel/<name>` — a name-shaped
383
+ law at the gate, because those are the only names that ever feed a derived `guides/src/<name>.md`
384
+ path. `EXTRA_NAME_PATTERN` is deliberately broader (any valid npm package name, scoped or not),
385
+ because `extras` names are manifest content and never feed a path. `VERSION_PATTERN` is exact
386
+ three-component semver; `ORKESTREL_RANGE_PATTERN` is the caret-pinned pre-1.0 range;
387
+ `EXTRA_RANGE_PATTERN` is the registry-only semver subset; `ENGINES_PATTERN` is the minimum-Node
388
+ form. `HEX_PATTERN` requires whole lowercase byte pairs, and `SYNC_BASELINE_PATTERN` accepts either
389
+ `absent` or an exact SHA-256 digest. `CONTROL_CHARACTER_PATTERN` and
390
+ `INVALID_PATH_CHARACTER_PATTERN` reject control characters and non-portable path characters.
391
+
392
+ `MINIMUM_NODE_VERSION` is `22.12.0`, `DEFAULT_ENGINES` derives from it, and `DEFAULT_VERSION` is
393
+ `0.0.1`. `BASE_DEV_DEPENDENCIES` is the host-neutral tooling baseline every generated workspace
394
+ gets; `SOURCE_BROWSER_DEV_DEPENDENCIES` adds the real browser providers a published browser environment
395
+ needs, and `APP_BROWSER_DEV_DEPENDENCIES` extends that with the Vue toolchain a private browser
396
+ application needs. `SCAFFOLD_RANGE` is the range generated workspaces pin this package at.
397
+ `CHECKOUT_ACTION_SHA` and `SETUP_NODE_ACTION_SHA` pin the two official CI actions to immutable
398
+ commits. `TYPESCRIPT_EXTENSIONS` is the module extension set every generated scoped check covers.
399
+ `JSON_PRINT_WIDTH` and `JSON_TAB_WIDTH` mirror the formatter configuration, so computed JSON is
400
+ format-stable by construction. `EXPORT_KEYWORD`, `CONST_KEYWORD`, `IMPORT_KEYWORD`, and
401
+ `FUNCTION_KEYWORD` keep declaration tokens out of rendered template literals, so a line-based
402
+ parity scan reading this package's own source never mistakes emitted file text for a real export.
403
+ `COMPILER_ID` is the default orchestrator id.
404
+
405
+ ### Constants — server
406
+
407
+ From [`constants.ts`](../../src/server/constants.ts).
408
+
409
+ | Name | Kind |
410
+ | -------------------------------- | ----- |
411
+ | `PRUNE_DIRECTORIES` | const |
412
+ | `HOST_MANIFEST_PATH` | const |
413
+ | `SENSITIVE_HOST_PATH_PATTERN` | const |
414
+ | `RESERVED_TARGET_PATH_PATTERN` | const |
415
+ | `MAX_CATALOG_DESCRIPTION_LENGTH` | const |
416
+ | `MAX_GUIDE_BYTES` | const |
417
+ | `MAX_HOST_ENTRIES` | const |
418
+ | `MAX_HOST_DEPTH` | const |
419
+ | `MAX_FILESYSTEM_DEPTH` | const |
420
+ | `MAX_PATH_SEGMENT_BYTES` | const |
421
+ | `RESERVED_PATH_SEGMENT_PATTERN` | const |
422
+ | `MAX_SYNC_CONCURRENCY` | const |
423
+ | `DEFAULT_SYNC_CONCURRENCY` | const |
424
+ | `MAX_SYNC_RETRIES` | const |
425
+ | `MAX_SYNC_TIMEOUT` | const |
426
+ | `DEFAULT_SYNC_TIMEOUT` | const |
427
+ | `MAX_SYNC_LIMIT` | const |
428
+ | `DEFAULT_SYNC_LIMIT` | const |
429
+ | `DEFAULT_SYNC_ITEMS` | const |
430
+ | `MAX_SYNC_ITEMS` | const |
431
+ | `DEFAULT_SYNC_BUDGET` | const |
432
+ | `MAX_SYNC_BUDGET` | const |
433
+ | `MAX_SYNC_BASE_LENGTH` | const |
434
+ | `MAX_SYNC_BRANCH_LENGTH` | const |
435
+ | `WRITE_DIGEST_PATTERN` | const |
436
+ | `SYNC_BRANCH_PATTERN` | const |
437
+
438
+ `PRUNE_DIRECTORIES` is the closed set of prune-owned directories — `.claude/agents`,
439
+ `.codex/agents`, and `scripts`. Nothing outside those roots is ever a deletion candidate, which is
440
+ why project-owned skills under `.agents/skills` and `.claude/skills` are structurally safe.
441
+ `HOST_MANIFEST_PATH` is the reserved `manifest.json` written at the root of every staged host.
442
+ `SENSITIVE_HOST_PATH_PATTERN` rejects credential-like, key-store, certificate-key, and
443
+ local-configuration paths at the staging boundary. `RESERVED_TARGET_PATH_PATTERN` protects `.git`
444
+ and every descendant from materialization, including when a hand-built plan targets a directory
445
+ that is otherwise vacant. `RESERVED_PATH_SEGMENT_PATTERN` rejects Windows device names even when
446
+ they carry an extension. `MAX_HOST_ENTRIES` and `MAX_HOST_DEPTH` bound vendored-host walks;
447
+ `MAX_FILESYSTEM_DEPTH` and `MAX_PATH_SEGMENT_BYTES` bound caller-supplied filesystem paths before
448
+ traversal. `MAX_GUIDE_BYTES` limits a catalog guide to the per-artifact ceiling before Markdown
449
+ parsing.
450
+
451
+ The `Sync` bounds come in matched default and maximum pairs: `concurrency` defaults to 6 and is
452
+ capped at 64, `timeout` defaults to 10 seconds and is capped at 5 minutes, `retries` is capped at
453
+ 5, the per-response byte `limit` defaults to and is capped at the 5 MiB artifact limit, `items`
454
+ defaults to 256 and is capped at 1,000, and the cumulative `budget` defaults to 16 MiB and is
455
+ capped at 100 MiB. Endpoint bases and branch names are additionally bounded by
456
+ `MAX_SYNC_BASE_LENGTH` and `MAX_SYNC_BRANCH_LENGTH`. `WRITE_DIGEST_PATTERN` is the exact SHA-256
457
+ form a write precondition accepts; `SYNC_BRANCH_PATTERN` is the initial safe-character law for the
458
+ upstream guide URL boundary, followed by Git-ref structural checks in `parseSyncBranch`.
459
+ `MAX_CATALOG_DESCRIPTION_LENGTH` bounds a normalized catalog description at 500 characters.
460
+
461
+ ### Templates
462
+
463
+ From [`templates.ts`](../../src/core/templates.ts).
464
+
465
+ | Name | Kind |
466
+ | ----------- | ----- |
467
+ | `TEMPLATES` | const |
468
+
469
+ `TEMPLATES` is the shipped, versioned `TemplateDefinition` data behind every `template`-origin
470
+ artifact. Only genuinely templated prose and source live here — starter README and guide text,
471
+ source stubs, application stubs, test stubs. Every structural file (`package.json`, the tsconfigs,
472
+ the build configuration) is `computed` instead, so a literal `{{…}}` inside a configuration can
473
+ never be mistaken for a placeholder. Changing a convention is a version bump of this package rather
474
+ than a hand edit of a generated workspace's copy.
306
475
 
307
- The coercing counterparts of the guards, COMPILED from the same shapes through the contract
308
- package's `createContract` — a guard-valid value round-trips unchanged, an off-contract
309
- value returns `undefined`, and neither ever throws (AGENTS §14). This is the parse-then-trust
310
- boundary for a stored plan, a tool argument, or an agent's emission.
311
-
312
- | API | Kind | Returns |
313
- | ----------------- | ----- | ---------------------------------------------------------------- |
314
- | `parseBlueprint` | const | a `Blueprint` from `unknown` / a JSON string, else `undefined`. |
315
- | `parsePlan` | const | a `Plan` from `unknown` / a JSON string, else `undefined`. |
316
- | `parseSyncReport` | const | a `SyncReport` from `unknown` / a JSON string, else `undefined`. |
317
-
318
- ```ts
319
- import { blueprint, isBlueprint, parseBlueprint } from '@orkestrel/scaffold'
320
-
321
- const json = JSON.stringify(blueprint('router', { surfaces: ['core'] })) // a complete, on-contract spec
322
- const parsed = parseBlueprint(json) // Blueprint | undefined
323
- parsed && isBlueprint(parsed) // true — a non-undefined parse always satisfies the guard
324
- parseBlueprint('{"name":"router"}') // undefined — sections missing (exact record), never throws
325
- ```
326
-
327
- ### Shapers
476
+ ### Errors
328
477
 
329
- The `Blueprint` and `Plan` contracts declared ONCE as contract `ContractShape` values
330
- (AGENTS §14 heavy machinery, earned here: validation, the JSON Schema a tool boundary needs,
331
- and seeded test blueprints must stay in lockstep). Each shaper is a function returning a
332
- fresh shape value; `blueprintShape()` / `planShape()` compose the section shapes, and the
333
- module's own validators and parsers are compiled from them at the barrel.
478
+ From [`errors.ts`](../../src/core/errors.ts).
479
+
480
+ | Name | Kind |
481
+ | ----------------- | -------- |
482
+ | `ScaffoldError` | class |
483
+ | `isScaffoldError` | function |
484
+
485
+ `ScaffoldError` carries a machine-readable `code` and an optional `context`, and `isScaffoldError`
486
+ is its total narrowing guard for a `catch`. Throwing is reserved for caller misuse:
487
+ `createBlueprint` on off-contract data throws `INVALID`; any method called after `destroy()` throws
488
+ `DESTROYED`; on the server face a non-vacant materialize target throws `TARGET` and a failed write
489
+ throws `WRITE`; a strict-mode upstream failure throws `FETCH`. A failing gate is deliberately _not_
490
+ an error — it fails closed into an incomplete `Scaffolding` whose `failures` carry a `BLOCKED`
491
+ marker.
492
+
493
+ ### Validators — core
494
+
495
+ From [`validators.ts`](../../src/core/validators.ts).
496
+
497
+ | Name | Kind |
498
+ | ------------------------- | -------- |
499
+ | `isDependency` | const |
500
+ | `isOverride` | const |
501
+ | `hasValidOverrideBytes` | function |
502
+ | `isWorkspaceName` | function |
503
+ | `hasOnlyDataProperties` | function |
504
+ | `isDenseDataArray` | function |
505
+ | `isEmitterErrorHandler` | function |
506
+ | `isCompilerEventHooks` | function |
507
+ | `isPlanManagerEventHooks` | function |
508
+ | `hasBlueprintEnvironment` | function |
509
+ | `hasValidBlueprintBytes` | function |
510
+ | `isBlueprint` | const |
511
+ | `isMember` | const |
512
+ | `hasValidArtifactHex` | function |
513
+ | `hasValidArtifactBytes` | function |
514
+ | `hasValidPlanHex` | function |
515
+ | `hasValidPlanBytes` | function |
516
+ | `hasValidAuditBytes` | function |
517
+ | `hasValidSnapshotBytes` | function |
518
+ | `isArtifact` | const |
519
+ | `isPlan` | const |
520
+ | `validatePlan` | function |
521
+ | `hasValidSyncReportBytes` | function |
522
+ | `isSyncReport` | const |
523
+
524
+ The seven `is*` constants are total guards compiled from their shapes and refined by the `has*`
525
+ predicates beside them. A guard never throws — adversarial input, hostile prototypes, deep nesting,
526
+ and cycles all return `false`. The refinements are exported separately because they carry real
527
+ laws: `hasBlueprintEnvironment` requires at least one selected environment across the two axes;
528
+ `hasValidArtifactHex` applies the lowercase byte-pair law; and the `*Bytes` predicates apply the
529
+ per-item and aggregate byte limits to overrides, blueprints, artifacts, plans, audits, snapshots,
530
+ and sync reports. `isWorkspaceName` is the bounded bare-name guard used wherever a manifest name is
531
+ read back. `hasOnlyDataProperties` and `isDenseDataArray` are core guards because every environment
532
+ uses the same accessor-free graph and dense-array boundary. `isEmitterErrorHandler`,
533
+ `isCompilerEventHooks`, and `isPlanManagerEventHooks` validate callable observation seams before
534
+ entity allocation.
535
+
536
+ `validatePlan` is the pre-mutation gate: it runs the semantic pass over the plan's own blueprint and
537
+ then checks every override against the exact artifact set the plan would write. An override whose
538
+ `path` matches no planned artifact, targets a `host`-origin artifact, or targets the
539
+ blueprint-owned `package.json` publication boundary is a blocking question rather than a silent
540
+ no-op.
541
+
542
+ ### Validators — server
543
+
544
+ From [`validators.ts`](../../src/server/validators.ts).
545
+
546
+ | Name | Kind |
547
+ | -------------------------- | -------- |
548
+ | `isPortablePath` | function |
549
+ | `isFilesystemPath` | function |
550
+ | `isTerminalText` | function |
551
+ | `isDependencyData` | function |
552
+ | `isSensitiveHostPath` | function |
553
+ | `isReservedTargetPath` | function |
554
+ | `isCatalogAllowance` | function |
555
+ | `isCatalogDescription` | function |
556
+ | `isMissingPathError` | function |
557
+ | `isWritePrecondition` | function |
558
+ | `isManifestEntry` | function |
559
+ | `isHostManifest` | function |
560
+ | `isSyncEventHooks` | function |
561
+ | `isMaterializerEventHooks` | function |
562
+
563
+ `isPortablePath` is the law every write and read is held to: a non-empty relative POSIX path, under
564
+ the length bound, free of control characters and non-portable characters, with no empty, `.`, `..`,
565
+ trailing-dot, trailing-space, or reserved-device segment. `isFilesystemPath` is the looser bound for
566
+ a host path a caller supplies, and `isTerminalText` is the bound for anything rendered into a
567
+ terminal or a JSON diagnostic. `isDependencyData` combines the data-only reflection with the core
568
+ dependency guard. `isReservedTargetPath` identifies preserved `.git` metadata, while
569
+ `isCatalogAllowance` bounds the single fleet counter at `MAX_HOST_ENTRIES` before directory
570
+ traversal and reads the typed array's intrinsic backing buffer, rejecting shared storage even when a
571
+ caller shadows the public `buffer` property.
572
+
573
+ `hasOnlyDataProperties` and `isDenseDataArray` exist because a boundary that copies a caller's graph
574
+ must never invoke a caller-defined accessor: the first walks a record or array graph and rejects any
575
+ non-data property within the public node/key budgets, while the second rejects a sparse,
576
+ symbol-bearing, or method-bearing array. Together they make a structured clone of an untrusted
577
+ input safe without admitting unbounded traversal. `isWritePrecondition`, `isManifestEntry`, and
578
+ `isHostManifest` are the exact-shape guards for the mutation and vendored-host records, and
579
+ `isSyncEventHooks`, `isMaterializerEventHooks`, and `isEmitterErrorHandler` reject an options object
580
+ carrying an unknown or non-callable hook. `isMissingPathError` narrows a caught filesystem error to
581
+ exactly `ENOENT`, so an absent path is never conflated with a permission failure.
582
+
583
+ ### Parsers — core
584
+
585
+ From [`parsers.ts`](../../src/core/parsers.ts).
586
+
587
+ | Name | Kind |
588
+ | ------------------------- | -------- |
589
+ | `parseBoundedJSON` | function |
590
+ | `parseCompilerOptions` | function |
591
+ | `parseBlueprint` | function |
592
+ | `parsePlan` | function |
593
+ | `parsePlanIds` | function |
594
+ | `parsePlanManagerOptions` | function |
595
+ | `parseSyncReport` | function |
596
+
597
+ `parseBoundedJSON` measures serialized UTF-8 bytes before allocating a parsed graph, applies a
598
+ caller-supplied `@orkestrel/contract` guard, and returns `undefined` for an invalid budget,
599
+ oversized or malformed JSON, or an off-contract result. The three domain parsers are the coercing
600
+ counterparts of their guards. Given a value they return it when the guard accepts it; given a string
601
+ they pass through the shared serialized-input ceiling before JSON parsing. A guard-valid value
602
+ round-trips unchanged, and malformed or off-contract input returns `undefined` rather than throwing.
603
+ `parsePlanIds` snapshots a bounded dense unique string array entirely through own data descriptors;
604
+ it never invokes a caller's iterator, accessor, symbol member, or sparse index.
605
+ `parseCompilerOptions` accepts only own `on` and `error` data properties and copies the compiler's
606
+ declared listener hooks before its emitter is allocated.
607
+ `parsePlanManagerOptions` performs the same fail-closed work for constructor options: it accepts
608
+ only own `plans`, `on`, and `error` data properties, bounds and snapshots seed plans without calling
609
+ their iterator, and copies only the three declared listener hooks.
610
+
611
+ ### Cloners — core
612
+
613
+ From [`cloners.ts`](../../src/core/cloners.ts).
614
+
615
+ | Name | Kind |
616
+ | -------------- | -------- |
617
+ | `snapshotPlan` | function |
618
+
619
+ `snapshotPlan` validates a data-only plan, detaches it through its canonical JSON representation,
620
+ and recursively freezes the entire owned graph. A `PlanManager` therefore never aliases a caller's
621
+ blueprint, artifacts, arrays, or returned record.
622
+
623
+ ### Parsers — server
624
+
625
+ From [`parsers.ts`](../../src/server/parsers.ts).
626
+
627
+ | Name | Kind |
628
+ | -------------------------- | -------- |
629
+ | `parseSyncDependencies` | function |
630
+ | `parseFilesystemPaths` | function |
631
+ | `parsePortablePaths` | function |
632
+ | `parseWritePreconditions` | function |
633
+ | `parseSyncBase` | function |
634
+ | `parseSyncCurrent` | function |
635
+ | `parseSyncBranch` | function |
636
+ | `parseMaterializerOptions` | function |
637
+ | `parseSyncOptions` | function |
638
+
639
+ These are the boundary coercers that run before any resource is allocated or any request is issued.
640
+ `parseMaterializerOptions` and `parseSyncOptions` reject an unknown key, an accessor-backed
641
+ property, or a malformed nested endpoint group, then compile the remainder through the shared
642
+ contract. `parseSyncBase` rejects an overlong token before URL allocation, then normalizes an
643
+ endpoint to an absolute `https:` origin — plain `http:` is accepted only for loopback — and rejects
644
+ embedded credentials, a query, or a fragment. `parseSyncBranch` implements the Git ref-name safety
645
+ subset used in raw-guide URLs: it rejects overlong values, empty or dot-leading components, `..`,
646
+ `@{`, the single `@`, trailing dots, and `.lock` suffixes without regard to case.
647
+ `parseSyncCurrent` snapshots only the declared guide references, enforcing both the per-file and
648
+ cumulative byte allowance. The three array parsers return frozen copies read through property
649
+ descriptors, so a caller-supplied array can never smuggle in a getter.
650
+
651
+ ### Shapers — core
652
+
653
+ From [`shapers.ts`](../../src/core/shapers.ts).
654
+
655
+ | Name | Kind |
656
+ | ----------------- | -------- |
657
+ | `dependencyShape` | function |
658
+ | `overrideShape` | function |
659
+ | `blueprintShape` | function |
660
+ | `memberShape` | function |
661
+ | `artifactShape` | function |
662
+ | `planShape` | function |
663
+ | `syncReportShape` | function |
664
+
665
+ Each returns a fresh declarative contract shape that compiles into a guard, a parser, a schema, and
666
+ a seeded generator. The shapes stay structural on purpose: `blueprintShape` declares `name` as a
667
+ plain bounded string rather than a pattern so the generator stays satisfiable, and the
668
+ `NAME_PATTERN` law lives in the semantic pass instead. Likewise `artifactShape` splits on `origin` —
669
+ host artifacts may carry `source` and `hex`, content artifacts require `content` — while the
670
+ lowercase byte-pair law stays a semantic refinement.
671
+
672
+ ### Shapers — server
673
+
674
+ From [`shapers.ts`](../../src/server/shapers.ts).
675
+
676
+ | Name | Kind |
677
+ | -------------------------- | -------- |
678
+ | `syncGuideOptionsShape` | function |
679
+ | `syncRegistryOptionsShape` | function |
680
+ | `syncOptionsShape` | function |
681
+ | `materializerOptionsShape` | function |
682
+
683
+ The closed data-only option shapes. Every numeric option is an integer shape bounded by its own
684
+ maximum constant, so an out-of-range `concurrency`, `retries`, `limit`, `items`, `budget`, or
685
+ `timeout` fails at the boundary rather than deep inside a request loop.
686
+
687
+ ### Contracts — server
688
+
689
+ From [`contracts.ts`](../../src/server/contracts.ts).
690
+
691
+ | Name | Kind |
692
+ | ----------------------------- | ----- |
693
+ | `syncOptionsContract` | const |
694
+ | `materializerOptionsContract` | const |
695
+
696
+ The compiled, closed data-only option contracts the two server parsers run their inputs through.
697
+
698
+ ### Helpers — core
699
+
700
+ From [`helpers.ts`](../../src/core/helpers.ts).
701
+
702
+ | Name | Kind |
703
+ | --------------------------- | -------- |
704
+ | `dependency` | function |
705
+ | `ownDataValue` | function |
706
+ | `override` | function |
707
+ | `member` | function |
708
+ | `blueprint` | function |
709
+ | `pascalCase` | function |
710
+ | `escapeHtmlText` | function |
711
+ | `serializeTypeScriptString` | function |
712
+ | `blueprintToMembers` | function |
713
+ | `catalogNames` | function |
714
+ | `alignTable` | function |
715
+ | `splitTableRow` | function |
716
+ | `padCell` | function |
717
+ | `delimiterCell` | function |
718
+ | `planToSummary` | function |
719
+ | `planToReview` | function |
720
+ | `auditToReview` | function |
721
+ | `isBehind` | function |
722
+ | `syncToReview` | function |
723
+ | `catalogToBlock` | function |
724
+ | `inferGroup` | function |
725
+ | `diffPlan` | function |
726
+ | `bytesToHex` | function |
727
+ | `contentCodePoint` | function |
728
+ | `contentToBytes` | function |
729
+ | `contentByteLength` | function |
730
+ | `contentToHex` | function |
731
+ | `snapshotOf` | function |
732
+ | `findPathConflict` | function |
733
+ | `findFileConflict` | function |
734
+ | `validateDependencyArray` | function |
735
+ | `validateBlueprint` | function |
736
+ | `manifestToDependencies` | function |
737
+ | `rangeToFreshness` | function |
738
+ | `computeHash` | function |
739
+ | `stableStringify` | function |
740
+ | `planPayload` | function |
741
+ | `computeColumnWidth` | function |
742
+ | `renderArray` | function |
743
+ | `renderObject` | function |
744
+ | `renderValue` | function |
745
+ | `formatJson` | function |
746
+ | `pinPlan` | function |
747
+
748
+ `dependency`, `override`, `member`, and `blueprint` are the builders. `ownDataValue` reads only an
749
+ own data descriptor, so parsed JSON cannot acquire manifest fields through a polluted prototype
750
+ and accessors are never invoked. Each builder omits an absent optional
751
+ field entirely rather than writing `undefined`, so a built value round-trips its own exact-record
752
+ guard. `blueprint` fills the defaults: `version` and `engines` from their constants, `src` to
753
+ `['core']`, and every other collection to empty. `pascalCase` derives the entity name from a
754
+ lowercase-hyphen package name, and `blueprintToMembers` derives the declared public `Member[]` — a
755
+ full entity, options type, interface, and factory per published environment, plus the exact declaration
756
+ inventory each selected application environment contributes.
757
+
758
+ `escapeHtmlText` and `serializeTypeScriptString` are the two escaping leaves used when a
759
+ caller-supplied name reaches generated HTML or generated TypeScript source; the latter preserves
760
+ every UTF-16 code unit, escaping lone surrogates and line separators.
761
+
762
+ `alignTable` builds a formatter-width-aligned GFM table by rendering a real table node and then
763
+ re-padding both the cells and the delimiter row to per-column codepoint width. `splitTableRow`,
764
+ `padCell`, and `delimiterCell` are its exported leaves — the row splitter honours an escaped pipe
765
+ as literal text rather than a column boundary, and `padCell` measures codepoints so a surrogate
766
+ pair counts once. `catalogNames` is the mirror-image reader: it extracts `@orkestrel/<name>` package
767
+ names from a catalog table by a pure line scan, and returns `[]` rather than throwing when the text
768
+ has no rows.
769
+
770
+ `planToSummary`, `planToReview`, `auditToReview`, `syncToReview`, and `catalogToBlock` are the
771
+ lossless projections. The review documents are copy-ready markdown; `auditToReview` groups findings
772
+ by drift, elides the aligned ones, and rejects an unsafe finding path outright. `catalogToBlock`
773
+ deduplicates by name, sorts by code unit, prefixes a standing trust notice, and emits only the
774
+ `Package` and `Version` columns — network-controlled descriptions are deliberately omitted, because
775
+ that block enters agent instruction context. `isBehind` is the shared freshness predicate both
776
+ report projections count with.
777
+
778
+ `diffPlan` is the audit engine, and `inferGroup` classifies a target file the plan does not own.
779
+ `snapshotOf`, `contentToHex`, `contentToBytes`, `contentByteLength`, `contentCodePoint`, and
780
+ `bytesToHex` are the host-independent byte leaves that make exact comparison possible without a
781
+ host encoder or buffer; an unpaired surrogate encodes as `U+FFFD` rather than throwing.
782
+ `findPathConflict` finds the first exact or case-insensitive collision in a path list, and
783
+ `findFileConflict` additionally rejects a file that would sit inside another planned path.
784
+
785
+ `validateBlueprint` and `validateDependencyArray` are the semantic pass. The array validator is
786
+ pure — it returns its questions and the set of names it saw, so the caller can apply the
787
+ cross-array overlap rules on top. `manifestToDependencies` reads a manifest's `dependencies`,
788
+ `devDependencies`, and `peerDependencies` in that order, keeps only own data sections and scoped
789
+ names, deduplicates, and never throws. `rangeToFreshness` applies the exact-pin comparison; the `missing` and `failed`
790
+ verdicts come from the fetch layer, never from this pure comparison.
791
+
792
+ `computeHash` is a deterministic FNV-1a digest and `stableStringify` a key-order-independent
793
+ canonical serialization, so two logically equal blueprints hash identically. `planPayload`
794
+ serializes exactly the blueprint, groups, and artifacts that establish plan identity, and `pinPlan`
795
+ hashes that payload while filling an explicit `src:<selection> · app:<selection>` trace (`none`
796
+ marks an empty axis). `PlanManager` compares the canonical payload whenever an
797
+ id is already registered: an identical plan is idempotent, while a distinct payload with the same
798
+ 32-bit digest fails closed with `ScaffoldError('INVALID', 'Plan hash collision')`.
799
+ `formatJson` and its leaves — `renderValue`,
800
+ `renderArray`, `renderObject`, and `computeColumnWidth` — emit JSON that matches the fleet
801
+ formatter byte for byte, collapsing a short array onto one line and breaking a long one, so
802
+ computed configuration JSON is format-stable by construction.
803
+
804
+ ### Helpers — server
805
+
806
+ From [`helpers.ts`](../../src/server/helpers.ts).
807
+
808
+ | Name | Kind |
809
+ | -------------------------- | -------- |
810
+ | `isRealDirectory` | function |
811
+ | `digestFile` | function |
812
+ | `digestHex` | function |
813
+ | `digestText` | function |
814
+ | `guideStub` | function |
815
+ | `packageShortName` | function |
816
+ | `readGuideReferences` | function |
817
+ | `syncReportOf` | function |
818
+ | `hostRoot` | function |
819
+ | `resolveRealPath` | function |
820
+ | `resolveContainedPath` | function |
821
+ | `resolvePhysicalPath` | function |
822
+ | `validateWriteAnchor` | function |
823
+ | `createWriteDirectory` | function |
824
+ | `validateWriteDirectories` | function |
825
+ | `validateWriteTarget` | function |
826
+ | `discardWriteTransaction` | function |
827
+ | `commitWriteTransaction` | function |
828
+ | `resolveGuideWrites` | function |
829
+ | `restoreFiles` | function |
830
+ | `replaceDirectory` | function |
831
+ | `selectOrkestrelEntries` | function |
832
+ | `deriveBlueprint` | function |
833
+ | `isVacant` | function |
834
+ | `readTarget` | function |
835
+ | `readManifest` | function |
836
+ | `readHostManifest` | function |
837
+ | `readFileHex` | function |
838
+ | `readFileText` | function |
839
+ | `listFiles` | function |
840
+ | `listDirectories` | function |
841
+ | `storagePath` | function |
842
+ | `stageHost` | function |
843
+ | `locateHostSource` | function |
844
+ | `remapArtifactPath` | function |
845
+ | `hydratePlan` | function |
846
+ | `vendoredPruneSet` | function |
847
+ | `pruneTargets` | function |
848
+ | `consumeCatalogAllowance` | function |
849
+ | `discoverPackages` | function |
850
+ | `guideToDescription` | function |
851
+ | `catalogPackages` | function |
852
+
853
+ `hostRoot` resolves this module's own installed package root — the nearest ancestor of its own file
854
+ holding a `package.json` — and returns its vendored `dist/host` directory. Walking up from the
855
+ module rather than from the working directory is what makes the default host correct once installed:
856
+ the package ships its vendored data with itself.
857
+
858
+ `resolveRealPath`, `resolveContainedPath`, and `resolvePhysicalPath` are the containment ladder.
859
+ The first resolves the deepest existing ancestor through symlinks with bounded iterative traversal;
860
+ the second rejects any candidate that escapes its root after that resolution; the third additionally
861
+ requires every existing ancestor between the root and the destination to be a real, unlinked
862
+ directory. All three reject malformed paths before filesystem access. Containment is therefore
863
+ realpath-aware rather than merely lexical, so a symlinked subdirectory planted inside an otherwise
864
+ legitimate root cannot smuggle a write or a read outside it.
865
+
866
+ `digestFile`, `digestHex`, and `digestText` are the three SHA-256 leaves. The file digest is
867
+ bounded-memory and revalidates device, inode, size, and modification time before and after reading,
868
+ so a file swapped mid-read is a failure rather than a silent wrong digest. `readFileHex` and
869
+ `readFileText` read one contained file under the same revalidation, and the text reader decodes
870
+ strictly, rejecting invalid UTF-8. Manifest reads stop at `MAX_MANIFEST_BYTES`; catalog guide reads
871
+ stop at `MAX_GUIDE_BYTES`. `listFiles` and `listDirectories` walk a real, unlinked root under the
872
+ entry and depth bounds, returning sorted POSIX-relative paths and `[]` for an absent root.
873
+ `isRealDirectory` is the physical-directory predicate they all lean on.
874
+
875
+ The write-transaction helpers are the fail-closed mutation path. `createWriteDirectory` establishes
876
+ a directory one segment at a time behind captured identities; `validateWriteAnchor`,
877
+ `validateWriteDirectories`, and `validateWriteTarget` revalidate those identities before each step;
878
+ `commitWriteTransaction` promotes a complete staged set and rolls every earlier destination back
879
+ when a later promotion fails; `discardWriteTransaction` removes the private residue of an
880
+ uncommitted or already-committed transaction; `restoreFiles` returns quarantined files to their
881
+ original paths in reverse order; and `replaceDirectory` atomically swaps a completed staging
882
+ directory for its target, preserving a recoverable backup. `resolveGuideWrites` is the sync-side
883
+ preflight: it resolves every behind-guide destination, enforces the canonical
884
+ `guides/src/<short>.md` path for its dependency name, rejects collisions, and rejects a destination
885
+ that is not a plain physical file — all before any mutation.
886
+
887
+ `isVacant` is the green-field target law: a path is vacant when it is absent, empty, or contains
888
+ nothing but a real `.git` directory. `readTarget` reads a target's current bytes at a set of paths
889
+ into an exact-byte snapshot, mapping a directly requested directory to the empty string and
890
+ omitting an absent path entirely. `readManifest` reads `package.json` text, and
891
+ `selectOrkestrelEntries` filters a manifest field to its scoped name-and-range entries.
892
+
893
+ `deriveBlueprint` is the faithful inverse an audit needs: it reconstructs a blueprint from an
894
+ existing workspace so a mature package is diffed against its own would-be scaffold rather than a
895
+ dependency-less stand-in. Environments come from `src/<environment>/` and `app/<environment>/`;
896
+ `engine` is `true` only when `src/bin/` exists; dependencies and peers come from the manifest's
897
+ scoped entries, with an optional peer recovered from `peerDependenciesMeta`; and `extras` is every
898
+ development dependency minus the generated baseline and minus anything already declared as a
899
+ dependency or peer, so a hand-added development dependency round-trips and stays audit-clean.
900
+ `overrides` is always empty, because derivation cannot know a caller's override intent.
901
+
902
+ `storagePath`, `stageHost`, `readHostManifest`, `locateHostSource`, `remapArtifactPath`, and
903
+ `hydratePlan` are the vendored-host path. `storagePath` maps a repo-relative path to its un-dotted
904
+ storage name, `stageHost` copies the vendored set into an output directory behind a full preflight
905
+ and an atomic swap, and `readHostManifest` reads and validates the resulting `manifest.json`,
906
+ returning `undefined` when a host has none — the raw-repository-root fallback that maps sources 1:1.
907
+ `locateHostSource` resolves one source to its storage file, `remapArtifactPath` maps a manifest
908
+ destination back onto an artifact's target prefix, and `hydratePlan` rehydrates a plan's host
909
+ artifacts with their exact bytes, expanding a directory-shaped host artifact into one artifact per
910
+ file.
911
+
912
+ `vendoredPruneSet` establishes the allowlist for one prune directory and fails closed rather than
913
+ returning an unestablished empty set — a missing host root, or a host with neither a manifest nor
914
+ that directory, is a coded failure, while a host that genuinely vendors nothing there remains a
915
+ valid empty allowlist. `pruneTargets` is the single source of truth for prune drift: it lists the
916
+ paths under a target's prune directories that the allowlist does not declare, and it never deletes
917
+ anything.
918
+
919
+ `consumeCatalogAllowance` decrements the single shared entry allowance and throws `TARGET` before an
920
+ over-budget traversal continues. `discoverPackages` requires a real, unlinked root and lists its
921
+ immediate child directories whose bounded manifest names a scoped package, skipping anything else
922
+ silently. `catalogPackages` applies one allowance across every root and directory rather than
923
+ resetting a per-root budget, then draws each description from the first paragraph of the first
924
+ blockquote of that package's own bounded guide via `guideToDescription`; a missing guide, an
925
+ unreadable or oversized one, or one with no blockquote yields an empty description rather than an
926
+ error.
927
+
928
+ `packageShortName` strips the canonical scope, `guideStub` renders the pointer written when a
929
+ dependency guide is not vendored yet, `readGuideReferences` reads a target's existing local mirrors
930
+ so a pull's verdicts are target-relative, and `syncReportOf` assembles one report from already
931
+ ordered guide and version outcomes.
932
+
933
+ ### Compilers — core
934
+
935
+ From [`compilers.ts`](../../src/core/compilers.ts).
936
+
937
+ | Name | Kind |
938
+ | ----------------------- | -------- |
939
+ | `hostGroup` | function |
940
+ | `fillArtifact` | function |
941
+ | `srcVariant` | function |
942
+ | `entryFields` | function |
943
+ | `dualCondition` | function |
944
+ | `exportsMap` | function |
945
+ | `compareCodeUnit` | function |
946
+ | `devDependenciesFor` | function |
947
+ | `packageManifest` | function |
948
+ | `rootTsconfig` | function |
949
+ | `viteHeader` | function |
950
+ | `policyViteProject` | function |
951
+ | `singleSrcViteConfig` | function |
952
+ | `rootViteConfig` | function |
953
+ | `applicationViteConfig` | function |
954
+ | `coreTsconfig` | function |
955
+ | `coreViteConfig` | function |
956
+ | `srcTsconfig` | function |
957
+ | `srcViteConfig` | function |
958
+ | `appTsconfig` | function |
959
+ | `appViteConfig` | function |
960
+ | `ciWorkflow` | function |
961
+ | `configArtifacts` | function |
962
+ | `sourceArtifacts` | function |
963
+ | `applicationArtifacts` | function |
964
+ | `paritySpecifiers` | function |
965
+ | `testArtifacts` | function |
966
+ | `guideMemberTable` | function |
967
+ | `guideUsage` | function |
968
+ | `guideMethods` | function |
969
+ | `guideTests` | function |
970
+ | `guideArtifacts` | function |
971
+ | `applyOverrides` | function |
972
+ | `blueprintToPlan` | function |
973
+
974
+ `blueprintToPlan` is the whole pure compilation: draft each selected group's artifacts, append the
975
+ host set, apply overrides, and pin. Everything above it is an exported leaf of that drafting, each
976
+ independently callable and independently tested.
977
+
978
+ `srcVariant` classifies an `src` environment selection into its manifest variant — one environment, or
979
+ several. `entryFields`, `dualCondition`, and `exportsMap` build the manifest entry fields and the
980
+ `exports` map from that variant; a browser-only package exports a single module condition, while
981
+ core and server src get dual import and require conditions with matching declaration files.
982
+ `devDependenciesFor` merges a blueprint's extras over the shared baseline, extras winning a name
983
+ collision, sorted by `compareCodeUnit` so ordering is stable across locales. `packageManifest`
984
+ assembles the whole file — name, publication mode, files, scripts, dependencies, peers and their
985
+ optional metadata, and engines.
986
+
987
+ `rootTsconfig` emits the root compiler options and one path alias per declared environment;
988
+ `coreTsconfig`, `srcTsconfig`, and `appTsconfig` emit the scoped configurations that remove the
989
+ wrong host's globals from each environment. `viteHeader` renders the shared header — the alias block
990
+ derived from the tsconfig paths, plus the environment-boundary plugin. `coreViteConfig`,
991
+ `srcViteConfig`, and `appViteConfig` emit the thin per-target wrappers; `rootViteConfig`,
992
+ `singleSrcViteConfig`, and `applicationViteConfig` emit the root configuration for a
993
+ library-only, single non-core `src` environment, and application-bearing workspace respectively;
994
+ `policyViteProject` emits the dedicated Node-only repository-policy test project.
995
+
996
+ `configArtifacts`, `sourceArtifacts`, `applicationArtifacts`, `testArtifacts`, and `guideArtifacts`
997
+ are the per-group drafters. `paritySpecifiers` computes the self-specifier and module map the
998
+ generated parity suite resolves fence imports through. `guideMemberTable`, `guideUsage`,
999
+ `guideMethods`, and `guideTests` render the generated guide's member tables, usage examples, method
1000
+ contract, and test inventory. `fillArtifact` fills one template entry into a `template`-origin
1001
+ artifact with missing placeholders treated as an error, and `hostGroup` resolves which group a
1002
+ byte-copied host path belongs to. `applyOverrides` replaces a matching artifact's content in place
1003
+ and deliberately leaves an unmatched, host-owned, or `package.json` override unapplied, because the
1004
+ gate reports it as a blocking question. `ciWorkflow` renders the generated workflow.
334
1005
 
335
- | API | Kind | Builds… |
336
- | ----------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
337
- | `dependencyShape` | function | the `Dependency` object shape. |
338
- | `overrideShape` | function | the `Override` object shape. |
339
- | `blueprintShape` | function | the `Blueprint` object shape — `surfaces` a `literalShape(SURFACES)` array with `min: 1`; `peers` / `extras` are `dependencyShape()` arrays alongside `dependencies`; `name` a plain `min: 1` string, NOT pattern-constrained, so `generate` stays satisfiable (the `NAME_PATTERN` law, and the cross-array uniqueness/overlap rules, live in the semantic pass). |
340
- | `memberShape` | function | the `Member` object shape — `category` / `surface` literal shapes. |
341
- | `artifactShape` | function | the `Artifact` object shape — `origin` a `literalShape(ORIGINS)`; `content` / `source` optional. |
342
- | `planShape` | function | the whole `Plan` object shape, section shapes composed; `trace` / `hash` optional. |
343
- | `syncReportShape` | function | the `SyncReport` object shape — `guide` + `version` array sub-shapes (each with a `literalShape(FRESHNESS)`) composed; `isSyncReport` / `parseSyncReport` compile from it. |
1006
+ ### Factories
344
1007
 
345
- ```ts
346
- import {
347
- artifactShape,
348
- blueprintShape,
349
- dependencyShape,
350
- memberShape,
351
- overrideShape,
352
- planShape,
353
- syncReportShape,
354
- } from '@orkestrel/scaffold'
355
- import { createContract, schemaToParameters, seededRandom } from '@orkestrel/contract'
356
-
357
- const contract = createContract(blueprintShape())
358
- contract.schema // the full JSON Schema — hand to a tool boundary via schemaToParameters
359
- contract.generate(seededRandom(42)) // a reproducible, on-contract seed blueprint for tests
360
- schemaToParameters(contract.schema) // the open tool-parameters record, no `as` anywhere
361
-
362
- // The section shapes `blueprintShape` / `planShape` compose each is a fresh, independent
363
- // `ContractShape` value, usable on its own contract:
364
- createContract(dependencyShape()).schema // the `Dependency` section schema alone
365
- createContract(overrideShape()).schema // the `Override` section schema alone
366
- createContract(memberShape()).schema // the `Member` section schema alone
367
- createContract(artifactShape()).schema // the `Artifact` section schema alone
368
- createContract(planShape()).schema // the whole `Plan` schema, section shapes composed
369
- createContract(syncReportShape()).schema // the `SyncReport` schema — the compiled `isSyncReport` source
370
- ```
1008
+ From [`factories.ts`](../../src/core/factories.ts) and
1009
+ [`factories.ts`](../../src/server/factories.ts).
1010
+
1011
+ | Name | Kind |
1012
+ | -------------------- | -------- |
1013
+ | `createCompiler` | function |
1014
+ | `createPlanManager` | function |
1015
+ | `createBlueprint` | function |
1016
+ | `createMaterializer` | function |
1017
+ | `createSync` | function |
1018
+
1019
+ `createBlueprint` is the validating constructor: it fills the builder defaults and then checks both
1020
+ the exact-record shape and the semantic pass, throwing `INVALID` when either fails. The other four
1021
+ construct their entities from their options records.
1022
+
1023
+ ### `Compiler`
1024
+
1025
+ The compilation orchestrator, from [`Compiler.ts`](../../src/core/Compiler.ts). It runs the fixed
1026
+ three-stage `draft → gate → pin` pipeline over a blueprint and owns a typed emitter whose event map
1027
+ is `compile`, `audit`, `block`, `error`, and `destroy`. Both public methods are genuinely
1028
+ synchronous and pure. `compile` emits `compile` only for a complete compilation and `block` for a
1029
+ gated one; `audit` emits `block` when gated and then always emits `audit`, never `compile`. After
1030
+ `destroy()` every method other than the getter and `destroy` itself throws `DESTROYED`, and teardown
1031
+ is idempotent with the emitter destroyed last.
1032
+
1033
+ ### `PlanManager`
1034
+
1035
+ The versioned, content-hashed plan registry, from
1036
+ [`PlanManager.ts`](../../src/core/PlanManager.ts). Its event map is `add`, `remove`, and `destroy`.
1037
+ Construction parses its exact options before allocating the emitter. `add` re-pins an immutable,
1038
+ detached plan snapshot and mints the record id from that content hash, so re-adding an unchanged plan
1039
+ resolves to the same frozen record, a changed plan mints a fresh id, and a distinct canonical payload
1040
+ with a colliding digest throws `INVALID` before mutation or emission. `remove` follows the
1041
+ batch-overload convention with the array overload declared first. Its list form is all-or-nothing:
1042
+ if any listed id is unregistered the collection is untouched and `false` is returned; on success all
1043
+ selected records are removed before the first stable-order event, so synchronous listeners observe
1044
+ the committed state and cannot create reentrant duplicate removals. After `destroy()` every method
1045
+ other than the getters and `destroy` throws `DESTROYED`.
1046
+
1047
+ ### `Materializer`
1048
+
1049
+ The materialization entity, from [`Materializer.ts`](../../src/server/Materializer.ts) — the only
1050
+ filesystem writer in the package. Its event map is `copy`, `write`, `remove`, `done`, `error`, and
1051
+ `destroy`. Every call preflights completely before mutating: a structural plan match, the semantic
1052
+ and contextual validation result, portable-path checks on every artifact path and source, collision
1053
+ detection, destination-shape checks, and realpath-anchored containment against both the target and
1054
+ the host root. Only then does staging begin, inside a private same-volume write transaction that is
1055
+ promoted atomically and rolled back on any failure. After `destroy()` every method throws
1056
+ `DESTROYED`.
1057
+
1058
+ ### `Sync`
1059
+
1060
+ The upstream-synchronization entity, from [`Sync.ts`](../../src/server/Sync.ts) — the only network
1061
+ reader in the package. Its event map is `guide`, `version`, `package`, `write`, `done`, `error`, and
1062
+ `destroy`. Every request runs under a per-request abort timeout and a bounded worker pool rather
1063
+ than an unbounded parallel await, follows no redirects, sends no credentials, and reads its response
1064
+ body incrementally against both the per-response limit and a shared cumulative allowance. The
1065
+ default posture collects failures into the result as `missing` or `failed` verdicts; `strict` mode
1066
+ turns those into a thrown `FETCH` naming the failing URL. `destroy()` aborts every in-flight request
1067
+ and is idempotent, and every method afterwards throws `DESTROYED`.
1068
+
1069
+ ### `WriteTransaction`
1070
+
1071
+ The nominal, same-volume write-transaction state, from
1072
+ [`WriteTransaction.ts`](../../src/server/WriteTransaction.ts). It is constructed only through its
1073
+ static `create`, which derives every filesystem path from a target plus portable relative paths — a
1074
+ caller can neither supply a deletion root nor mutate the captured arrays. Creation snapshots every
1075
+ destination into a frozen `WriteExpectation`, verifies any supplied preconditions against what is
1076
+ actually on disk, captures the parent anchor identity, and creates private staging and backup
1077
+ directories with restrictive permissions. Its readonly getters — `target`, `root`, `stage`,
1078
+ `backup`, `expectations`, `parents`, `directories`, `anchor`, and `existing` — are the only way to
1079
+ observe it; every operation over it lives in the exported transaction helpers.
371
1080
 
372
- ### Builders
1081
+ ## Methods
373
1082
 
374
- Lowercase value builders every builder returns a fresh object and OMITS absent optional
375
- keys entirely, so its output round-trips the exact-record validators above.
1083
+ The public methods of each behavioral interface, one table per type.
376
1084
 
377
- | API | Kind | Builds… |
378
- | ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
379
- | `dependency` | function | a `Dependency` from name / range / optional `optional` flag (omitted entirely when absent). |
380
- | `override` | function | an `Override` from path / content. |
381
- | `member` | function | a `Member` from name / category / summary / surface (`surface` defaults `'core'`). |
382
- | `blueprint` | function | a `Blueprint` from a name + a partial of the rest — `version` / `engines` default (`DEFAULT_VERSION` / `DEFAULT_ENGINES`), `surfaces` defaults `['core']`, and `keywords` / `dependencies` / `peers` / `extras` / `overrides` default `[]`. |
1085
+ #### `CompilerInterface`
383
1086
 
384
- ```ts
385
- import { blueprint, dependency, override } from '@orkestrel/scaffold'
1087
+ | Method | Returns |
1088
+ | --------- | ------------- |
1089
+ | `compile` | `Scaffolding` |
1090
+ | `audit` | `Audit` |
1091
+ | `destroy` | `void` |
386
1092
 
387
- const spec = blueprint('router', {
388
- description: 'A tiny hash-router. Part of the @orkestrel line.',
389
- keywords: ['router', 'hash'],
390
- surfaces: ['core', 'browser'],
391
- dependencies: [dependency('@orkestrel/contract', '^0.0.5')],
392
- overrides: [override('README.md', '# @orkestrel/router\n\nHand-written readme.\n')],
393
- })
394
- spec.version // '0.0.1' — the builder default
395
- spec.engines // '>=22' — the builder default
396
- ```
1093
+ `compile(blueprint, groups?)` runs the pipeline and returns a complete or visibly incomplete
1094
+ `Scaffolding`; the optional group selection scopes the plan to those artifact groups.
1095
+ `audit(blueprint, current, groups?)` compiles and then diffs the resulting plan against the
1096
+ caller-supplied current content; a gated blueprint returns `complete: false` with the gate's
1097
+ blocking questions and zero findings. `destroy()` is idempotent teardown. The interface also
1098
+ exposes the readonly `emitter`.
397
1099
 
398
- ### Compilers
399
-
400
- Pure, exported leaves of `blueprintToPlan`'s pipeline (AGENTS §5 no-nested-functions law) —
401
- one function per drafting concern, each independently unit-tested; `blueprintToPlan` (below,
402
- Helpers) is the sole orchestrator that calls them in sequence, never the sole holder of the
403
- logic itself.
404
-
405
- | API | Kind | Contract |
406
- | ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
407
- | `hostGroup` | function | Resolve the `Group` a byte-copied `HOST_PATHS` entry belongs to (docs / orchestration / guides / configs fallback). |
408
- | `fillArtifact` | function | Fill one `TEMPLATES` entry into a `template`-origin `Artifact`, optionally tagged with the owning `Surface`. |
409
- | `surfaceVariant` | function | Classify a blueprint's surfaces into the manifest/exports variant class — the sole declared `Surface`, or `'multi'` when two or more are declared. |
410
- | `entryFields` | function | Build the `package.json` `main` / `module` / optional top-level `types` fields for a declared `Surface[]`. |
411
- | `dualCondition` | function | Build one dual-format (`import` + `require`) `exports` condition block for an extensionless dist path. |
412
- | `exportsMap` | function | Build the `package.json` `exports` map for a declared `Surface[]`. |
413
- | `compareCodeUnit` | function | A code-unit (not locale-sensitive) string comparator — matches the `keywords` sort and keeps ordering stable across locales/environments. |
414
- | `devDependenciesFor` | function | Merge a blueprint's `extras` (code-unit sorted) over the shared devDependency baseline, extras winning on a name collision. |
415
- | `packageManifest` | function | Compute the `package.json` artifact's `content`, applying the manifest/exports combination rules over a blueprint's surfaces. |
416
- | `rootTsconfig` | function | The root `tsconfig.json` — one `@src/<surface>` path alias per declared surface, in declared order. |
417
- | `viteHeader` | function | The rendered import/`resolve` header block every `rootViteConfig` shape prefixes — the Playwright import lines + `createBrowserProvider` present only when `needsPlaywright`. |
418
- | `singleSurfaceViteConfig` | function | The root `vite.config.ts` body for a single non-`core` surface — the surface's own `viteHeader` (Playwright only when the surface is `browser`) prefixes the factory that IS the base (Shape 3 of `rootViteConfig`). |
419
- | `rootViteConfig` | function | The root `vite.config.ts` — three grounded shapes chosen by a blueprint's `surfaces` (core-only, multi-surface, single non-`core` surface). |
420
- | `coreTsconfig` | function | `configs/src/tsconfig.core.json` — the unchanged core shape. |
421
- | `coreViteConfig` | function | `configs/src/vite.core.config.ts` — inlines its own `build.lib` / `rollupOptions`. |
422
- | `surfaceTsconfig` | function | `configs/src/tsconfig.<browser\|server>.json` — `rootDir`/`outDir` point at the whole `src`/`dist/src` tree. |
423
- | `surfaceViteConfig` | function | `configs/src/vite.<browser\|server>.config.ts` — a thin `dts`-only wrapper anchored on the root `srcBrowser` / `srcServer` export. |
424
- | `configArtifacts` | function | Draft the `configs` group's `computed` artifacts — the root `tsconfig.json` / `vite.config.ts` plus each declared surface's `configs/src/*` pair. |
425
- | `sourceArtifacts` | function | Draft the `source` group's `template` artifacts — one full `{types, <Pascal>, factories, index}` stub set per declared surface. |
426
- | `paritySpecifiers` | function | Build the computed `SELF_SPECIFIERS` / `SPECIFIER_MODULES` / `exportsFor` block the `parityTest` template's `{{specifiers}}` placeholder fills — one shape for every surface count. |
427
- | `testArtifacts` | function | Draft the `tests` group's `template` artifacts — shared recorder setup, conditional per-surface environment setup, per-surface entity/factory test stubs, and the guides-parity drop-in. |
428
- | `guideMemberTable` | function | Build an `alignTable` markdown table over a member category's rows, deduped by name — one row per declared member across all its surfaces. |
429
- | `guideArtifacts` | function | Draft the `guides` group's artifacts — the package's own filled guide stub, the guides index, and any vendored dependency guide mirrors (the seven grounded `@orkestrel/*` names). |
430
- | `applyOverrides` | function | Apply a blueprint's `overrides` over a drafted artifact list — an override REPLACES the matching artifact's `content` in place; a non-matching or `host`-origin-targeting override is left unapplied here (the gate stage surfaces it as a blocking question). |
1100
+ #### `PlanManagerInterface`
431
1101
 
432
- ```ts
433
- import { fillArtifact, hostGroup } from '@orkestrel/scaffold'
1102
+ | Method | Returns |
1103
+ | --------- | ------------------------- |
1104
+ | `has` | `boolean` |
1105
+ | `plan` | `PlanRecord \| undefined` |
1106
+ | `plans` | `readonly PlanRecord[]` |
1107
+ | `add` | `PlanRecord` |
1108
+ | `remove` | `boolean \| void` |
1109
+ | `destroy` | `void` |
1110
+
1111
+ `has(id)` tests registration. `plan(id)` is the singular accessor and returns `undefined` for an
1112
+ unregistered id; `plans()` is the plural accessor and returns a snapshot array. `add(plan)`
1113
+ registers or re-registers one plan. `remove()` removes every plan and returns `void`; `remove(id)`
1114
+ removes one and returns whether it existed; `remove(ids)` is all-or-nothing over a list. The
1115
+ interface also exposes the readonly `emitter` and `size` properties.
434
1116
 
435
- hostGroup('AGENTS.md') // 'docs'
436
- hostGroup('.claude') // 'orchestration'
437
- fillArtifact('README.md', 'docs', 'readme', { name: 'router', pascal: 'Router' })
438
- // { path: 'README.md', group: 'docs', origin: 'template', content: '# router\n…' }
439
- ```
1117
+ #### `MaterializerInterface`
440
1118
 
441
- ```ts
442
- import {
443
- compareCodeUnit,
444
- devDependenciesFor,
445
- dualCondition,
446
- entryFields,
447
- exportsMap,
448
- surfaceVariant,
449
- } from '@orkestrel/scaffold'
1119
+ | Method | Returns |
1120
+ | ------------- | ------------------- |
1121
+ | `materialize` | `MaterializeResult` |
1122
+ | `repair` | `MaterializeResult` |
1123
+ | `prune` | `MaterializeResult` |
1124
+ | `destroy` | `void` |
1125
+
1126
+ `materialize(plan, target)` is green-field: it refuses any target `isVacant` rejects, then copies
1127
+ each host artifact and writes each template and computed artifact. `repair(plan, audit, target)` is
1128
+ into-existing: it skips the vacancy check, re-verifies that the target still matches the audit
1129
+ preview, and writes only the missing and stale artifacts that audit names, leaving aligned ones
1130
+ untouched and reporting them as `skipped`. `prune(target, expected)` deletes exactly the unexpected
1131
+ files the vendored host no longer declares under the prune directories, and only after the observed
1132
+ bytes still match the `expected` snapshot it was previewed with. `destroy()` is idempotent teardown.
1133
+ The interface also exposes the readonly `emitter`.
450
1134
 
451
- surfaceVariant(['core', 'server']) // 'multi'
452
- entryFields(['browser']).main // './dist/src/browser/index.js'
453
- dualCondition('./dist/src/core/index') // { import: {…}, require: {…} }
454
- exportsMap(['core'])['.'] // dual import/require condition block
455
- devDependenciesFor([])['typescript'] // '^6.0.3'
456
- ;[...['b', 'a']].sort(compareCodeUnit) // ['a', 'b']
457
- ```
1135
+ #### `SyncInterface`
458
1136
 
459
- ```ts
460
- import { applyOverrides, override } from '@orkestrel/scaffold'
1137
+ | Method | Returns |
1138
+ | ---------- | ---------------------------------- |
1139
+ | `guides` | `Promise<readonly GuideSync[]>` |
1140
+ | `versions` | `Promise<readonly VersionSync[]>` |
1141
+ | `catalog` | `Promise<readonly CatalogEntry[]>` |
1142
+ | `pull` | `Promise<SyncReport>` |
1143
+ | `write` | `Promise<readonly string[]>` |
1144
+ | `destroy` | `void` |
1145
+
1146
+ `guides(deps, current?)` fetches each dependency's upstream guide. The optional `current` map is
1147
+ keyed by dependency name: with it, a fetched guide byte-equal to its entry verdicts `current` and
1148
+ anything else verdicts `behind`; without it, every successful fetch verdicts `behind`, because no
1149
+ reference means it needs syncing. `versions(deps)` compares each declared range to the registry
1150
+ latest. `catalog()` enumerates the fleet from the registry's exact organization package list — an
1151
+ unreachable or malformed list is always a coded failure, since without it there is no catalog — then
1152
+ degrades gracefully per package. `pull(target, dependencies?)` builds the reference map from the
1153
+ target's own mirrors, so its verdicts are target-relative, and rejects a selection the target does
1154
+ not declare. `write(report, target)` commits only the `behind` guides. `destroy()` aborts every
1155
+ in-flight request. The interface also exposes the readonly `emitter`.
1156
+
1157
+ ## The compile pipeline
1158
+
1159
+ `compile` runs three stages in fixed order and records each as a `CompileRecord` carrying its input,
1160
+ its output, whether it failed, and any error text.
1161
+
1162
+ 1. **draft** — `blueprintToPlan` selects the covered groups, drafts each group's artifacts, appends
1163
+ the host set, applies overrides, and pins the draft. A throw here records a `draft` failure coded
1164
+ `INVALID`, emits `error`, marks the remaining two stages skipped, and returns incomplete.
1165
+ 2. **gate** — `validatePlan` runs the semantic pass over the blueprint and checks every override
1166
+ against the drafted artifact set. Blocking questions fail the stage; a dependency outside the
1167
+ vendored guide set contributes a non-blocking advisory question instead.
1168
+ 3. **pin** — a host-origin pointer artifact is appended for each non-vendored dependency, and
1169
+ `pinPlan` fills `trace` and `hash` from the plan's own content.
1170
+
1171
+ The gate fails closed. A blueprint that fails validation, or that carries an override matching no
1172
+ planned artifact or targeting a host-origin path, yields a visible incomplete `Scaffolding` — `plan`
1173
+ absent, `questions` populated, a `BLOCKED` failure marker recorded — rather than throwing and rather
1174
+ than emitting a half-formed workspace. A half-formed workspace is worse than a question.
1175
+
1176
+ `validateBlueprint` is the semantic law in one place. It checks the name against `NAME_PATTERN` and
1177
+ the length bound, the version and engines patterns, and that the declared engines floor is not below
1178
+ the supported Node minimum. It requires at least one selected environment across the two axes, keeps
1179
+ both axes on-vocabulary with no repeats, and blocks the one combination that has no defined
1180
+ configuration class: `browser` plus `server` without `core` in the same axis. It validates each
1181
+ dependency array
1182
+ for a well-formed name and range with no duplicates — scoped names for `dependencies` and `peers`,
1183
+ any valid npm name for `extras` — and blocks a name declared in two of the three arrays. It bounds
1184
+ the description and every override by the per-item and aggregate byte limits, and blocks a repeated
1185
+ or empty override path.
1186
+
1187
+ Because `pinPlan` derives `hash` from a canonical, key-order-independent serialization of the
1188
+ blueprint, groups, and artifacts, two logically equal blueprints built in different field orders
1189
+ produce the same digest — and a `PlanManager` id is that digest.
1190
+
1191
+ ## Origin and ownership
1192
+
1193
+ `origin` is the ownership axis, and it decides everything downstream: how an artifact is produced,
1194
+ how it is audited, and whether it may ever be overwritten.
1195
+
1196
+ - **`host`** — byte-copied from the vendored data root. These are the shared files a whole fleet
1197
+ keeps identical: the root instruction documents and licence, the agent, rule, and skill
1198
+ directories, the session scripts, the repository coding-law policy module, the byte-identical root
1199
+ dotfiles, and the two guide mirrors every workspace carries. `HOST_PATHS` is the exact list.
1200
+ - **`template`** — filled from a frozen template definition by a pure fill engine. These are
1201
+ starter files: source stubs, test stubs, the starter guide, the README.
1202
+ - **`computed`** — derived by this package's own combination logic. These are the structural files:
1203
+ the manifest, the tsconfigs, the build configuration, the generated CI workflow.
1204
+
1205
+ Audit semantics follow directly from that.
1206
+
1207
+ - A **template** artifact is birth-only and audit-exempt. It is always reported `aligned`, whatever
1208
+ the target holds. Starter files are written once and are legitimately outgrown — real code
1209
+ replaces the stub, a hand-authored guide replaces the scaffold prose, an entity gets renamed.
1210
+ Comparing a mature workspace against its birth stub is a category error, and it would make any
1211
+ unscoped repair a data-loss hazard. Template findings therefore never contribute to the drifted,
1212
+ missing, or clean tallies.
1213
+ - A **computed** artifact is content-aware canon: `missing`, `aligned`, or `stale`, and it gates the
1214
+ audit like any other drift.
1215
+ - A **host** artifact is audited by presence alone — `missing` or `aligned`, never `stale` — unless
1216
+ it has been hydrated with its real host bytes, in which case it is content-compared exactly like a
1217
+ computed artifact and can be `stale`. Hydration also expands a directory-shaped host artifact into
1218
+ one artifact per file, so agent configuration and skills are audited file by file.
1219
+ - A target file the plan does not own is `foreign`, and `inferGroup` classifies it by its leading
1220
+ path segment.
1221
+
1222
+ The same ownership boundary is what makes mutation safe. **`repair` and `fleet` mutate host-origin
1223
+ artifacts only.** Both scope the compiled plan to host origin before hydrating, diffing, or applying
1224
+ anything, so a mature workspace's hand-written source, tests, guides, and manifest are never
1225
+ overwritten with a stub. A consequence worth stating plainly: the generated
1226
+ `.github/workflows/ci.yml` is a **computed** artifact, so **user-owned CI is never repaired**. Once
1227
+ a workspace has its own workflow, that copy stands, and any change to it is an ordinary edit in that
1228
+ workspace.
1229
+
1230
+ Overrides respect the same boundary from the other direction. `applyOverrides` never replaces a
1231
+ host-origin artifact and never replaces `package.json`; the gate turns either attempt — and an
1232
+ override matching no planned artifact at all — into a blocking question rather than a silent no-op.
1233
+
1234
+ Guide mirrors are the one place ownership is conditional. A dependency this package vendors a
1235
+ byte-identical mirror for gets a real host-origin copy of `guides/src/<short>.md`. Any other
1236
+ dependency gets a host-origin _pointer_ artifact plus a non-blocking question, never a fabricated
1237
+ mirror; on materialization that pointer degrades to a short stub, and `scaffold pull` fetches the
1238
+ real thing. That degrade is scoped exactly to guide pointers: any other missing manifest entry
1239
+ means a corrupt or truncated vendored manifest, and fails closed.
1240
+
1241
+ ## Audit, repair, and prune
1242
+
1243
+ An audit is a pure function of a plan and a snapshot, so the same engine that creates a workspace
1244
+ checks one. `readTarget` supplies the snapshot as exact bytes; `diffPlan` returns findings as data;
1245
+ `auditToReview` renders them for a human. Nothing in that path writes.
1246
+
1247
+ `repair` turns those findings back into the narrowest possible write. It re-reads the target,
1248
+ re-diffs it, and refuses to proceed if the findings changed since the preview it was given — a
1249
+ target that moved under the caller is a `TARGET` failure, not a race to win. It then derives a write
1250
+ precondition per artifact from the audit itself: a `missing` finding requires the destination to
1251
+ still be absent, a `stale` finding requires it to still carry exactly the bytes that were observed.
1252
+ Those preconditions are checked again inside the write transaction before any promotion.
1253
+
1254
+ `prune` is the deletion arm, and it is deliberately narrow. Its candidate set comes from
1255
+ `pruneTargets`, which is also what the executable's audit and preview read, so what is reported and
1256
+ what is deleted cannot diverge. Only the three prune directories are in scope; the allowlist must be
1257
+ positively established from the vendored host, or the call fails closed rather than treating an
1258
+ unresolved host as "vendors nothing" and proposing to delete everything. Each candidate is verified
1259
+ as a plain physical file whose bytes still match the preview, moved into a private quarantine rather
1260
+ than unlinked, re-verified after the move, and only then reported as removed — with a full restore
1261
+ attempt if any candidate fails mid-way.
1262
+
1263
+ ## Upstream sync, pull, and catalog
1264
+
1265
+ `Sync` is the only network reader, and its posture is conservative by construction.
1266
+
1267
+ Every request is unauthenticated: no token, no authorization header, anywhere. Every fleet
1268
+ repository is public, so plain reachability is the only signal, and a guide `404` degrades
1269
+ gracefully instead of needing credentials. Redirects are never followed — a 3xx, or the opaque
1270
+ response a manual redirect policy produces, is treated as a distinct named transport fault, so a
1271
+ compromised or misconfigured endpoint cannot silently redirect cross-host. Guide URLs are therefore
1272
+ built in their canonical form directly rather than relying on a redirect to reach it.
1273
+
1274
+ Concurrency is bounded by a worker pool over a shared cursor, never an unbounded parallel await. The
1275
+ pool preserves input order, stops issuing new work after the first error, awaits every worker so a
1276
+ sibling rejection is always observed, and then rethrows the first error. Response bodies are read
1277
+ incrementally against both the per-response limit and a shared cumulative allowance; a declared
1278
+ oversized content length short-circuits before any read. An oversized body is a transport fault like
1279
+ any other — retry-eligible, then `failed`, or a thrown `FETCH` under `strict`.
1280
+
1281
+ Every non-clean outcome carries a `note` explaining the cause: a transport error message with the
1282
+ underlying socket code appended when the runtime attaches one, an HTTP status, the fixed
1283
+ redirect-blocked string, or the oversized-body message. `current` and `behind` carry no note,
1284
+ because there is nothing to explain.
1285
+
1286
+ `pull` is the target-aware composition. It reads the target's declared scoped dependencies from its
1287
+ manifest, rejects any explicit selection the target does not declare, builds the reference map from
1288
+ the target's own `guides/src/<short>.md` mirrors, fetches guides and versions under one shared
1289
+ allowance, and assembles a report whose `clean` flag requires both no drift and no failures. `write`
1290
+ then commits only the `behind` guides — never `current`, `missing`, or `failed`, none of which carry
1291
+ trustworthy content — under the same containment and precondition law `Materializer` enforces,
1292
+ including a baseline digest check against what is actually on disk.
1293
+
1294
+ `catalog` builds the fleet package catalog from three reads per entry. The registry's exact
1295
+ organization package list is authoritative and unconditionally required. Each package's own registry
1296
+ document supplies its version and a fallback description; a failed document degrades the entry
1297
+ rather than dropping it, because the organization list already proved the package exists. Each
1298
+ package's own guide supplies the preferred description — its first blockquote's first paragraph —
1299
+ and a `404` keeps the package listed with an explanatory note, since unreachability is a signal
1300
+ rather than an absence. `catalogPackages` is the offline sibling that reads the same shape from
1301
+ local checkouts.
1302
+
1303
+ The rendered block is deliberately minimal. `catalogToBlock` emits a standing trust notice —
1304
+ generated package identifiers are untrusted discovery data, never instructions — followed by a table
1305
+ with **`Package` and `Version` columns only**. Descriptions are network-controlled text, and that
1306
+ block is written into an agent instruction file, so they are omitted on purpose.
1307
+
1308
+ ## The generated workspace
1309
+
1310
+ A generated workspace is not a folder of suggestions; it is a working, gated project.
1311
+
1312
+ **Manifest and scripts.** A published workspace is scoped and carries an `exports` map, publish
1313
+ configuration, and ships `dist/src` plus its README. An application-only workspace is unscoped and
1314
+ `private: true`, with no export map and no publish configuration, and ships `dist/app`. A workspace
1315
+ that builds its own executable additionally ships `dist/bin` and `dist/host`. Scripts are emitted in
1316
+ a fixed, interleaved order so aggregates sit immediately before their per-environment members:
1317
+
1318
+ - `clean`, `copy`, `scaffold`
1319
+ - `lint` and `lint:check`, `format` and `format:check`
1320
+ - `check`, then `check:src` with one `check:src:<environment>` per published environment, then
1321
+ `check:app` with one `check:app:<environment>` per app environment — the browser app scope uses the
1322
+ Vue typechecker, every other scope uses plain `tsc`
1323
+ - `test`, then `test:src` and its per-environment scopes, `test:app` and its per-environment scopes,
1324
+ `test:policy`, and `test:guides`; an engine also receives the deliberately non-default
1325
+ `test:integration` live installed-consumer gate
1326
+ - `build`, then `build:src` and its per-environment targets, `build:app` and its runtime targets, and
1327
+ `build:host` for an engine workspace
1328
+ - `dev` when a browser application is selected; `serve` and `serve:build` when a server application
1329
+ is selected
1330
+ - `prepublishOnly` chaining `format:check → lint:check → check → build → test`, followed by the
1331
+ live generated-consumer integration gate for the scaffold engine itself
1332
+
1333
+ **Environment isolation.** Scoped TypeScript projects remove the wrong host's globals from each
1334
+ environment: core scopes carry no DOM, no Node, and no host globals at all; browser scopes carry DOM
1335
+ and no Node; server scopes carry Node and no DOM. Lint restricts declared package, alias, and
1336
+ conventional relative imports in the same directions. Neither replaces the other, and neither
1337
+ replaces the build.
1338
+
1339
+ **The generated build boundary.** The emitted configuration carries an environment-boundary plugin
1340
+ that resolves the real module graph rather than re-implementing a parser. **TypeScript and
1341
+ JavaScript references are read through Vite's own Oxc/Rolldown AST**; **Vue single-file components
1342
+ are read through the official SFC compiler**, block by block, including `src`-referenced blocks;
1343
+ **CSS dependencies are parsed by Vite's bundled Lightning CSS analyzer**; and **HTML attributes,
1344
+ entities, candidate lists, and metadata use Vite's own HTML parser callbacks**. The plugin runs at
1345
+ resolve and transform time, checks the finished module graph at build end, rescans every emitted
1346
+ JavaScript chunk's remaining dynamic imports after optimization and tree-shaking, and audits every
1347
+ emitted asset's physical source path. Source-level asset URLs are checked before Vite transforms
1348
+ them, so generated runtime `new URL(...)` expressions are not mistaken for caller input. HTML
1349
+ `vite-ignore` tokens are reversibly encoded as an HTML character reference before Vite parses the
1350
+ document, so the attribute cannot opt an element out of Vite's normal HTML graph while the same
1351
+ text inside a resource URL still decodes to its original filename before resolution. Existing
1352
+ equivalent character references are shifted before encoding and unshifted afterward, which keeps
1353
+ comments, text, raw blocks, attributes, adjacent tokens, casing, and user-authored entity spelling
1354
+ byte-stable. The trusted preparation hook owns the final pre-parse phase; inline proxy code is
1355
+ restored before module analysis, and the first normal post-parse hook restores the original HTML
1356
+ spelling. The browser entry begins with a generated, byte-stable security prologue: the doctype,
1357
+ document and head opening, and a `Content-Security-Policy` meta element are one required prefix.
1358
+ Preparation rejects a missing, moved, or changed prologue before Vite parses the document, and the
1359
+ final trusted post-hook verifies that Vite retained the policy. CRLF and LF files are both accepted;
1360
+ the prologue's markup and ordering are otherwise exact. Vite's `%ENV%` HTML substitution is rejected
1361
+ before parsing because Vite performs that expansion after every plugin pre-hook, where it could
1362
+ otherwise create a late control attribute. The guard walks the exact left-to-right `%(\S+?)%`
1363
+ tokens Vite recognizes instead of performing a substring search, and each preparation plugin owns
1364
+ the resolved environment/definition keys for its configuration, so one build cannot contaminate
1365
+ another and overlapping percent text remains ordinary text. Read environment values from the
1366
+ application's module graph through `import.meta.env` instead.
1367
+ Asset URLs that force `?inline` are rejected before Vite can read them outside that auditable output
1368
+ graph. Dynamic imports must use a static quoted string or expression-free template string; even
1369
+ `/* @vite-ignore */` static values repeat the same environment and containment checks inside the
1370
+ transform boundary, including inline HTML proxy modules and trusted dependency modules. Trusted
1371
+ dependency modules also pass through a bounded, no-follow, identity-checked load inspection before
1372
+ Vite transforms or tree-shakes their raw source, so dependency-side asset references cannot escape
1373
+ the physical package root by disappearing from the later graph.
1374
+
1375
+ Browser application scripts are modules. Vite's parsed HTML asset callback rejects a classic
1376
+ external `<script src>` before resolution and directs the author to `type="module"`. A module
1377
+ script URL must be a non-empty local Vite-graph URL: schemes, protocol-relative URLs, data URLs,
1378
+ fragments, surrounding URL whitespace, and ASCII C0 controls or DEL are rejected rather than left
1379
+ as unaudited browser loads. This is deliberately broader than the URL parser's edge stripping.
1380
+ Numeric
1381
+ HTML character references and semicolon-terminated named references are rejected before resolution,
1382
+ so neither control references nor entity-built scheme characters can bypass the boundary. Vite can
1383
+ begin resolving an entity-decoded module URL before its parsed per-asset callback runs; that earlier
1384
+ path remains Vite-owned and passes through the environment resolver, which rejects the same full
1385
+ ASCII control range and every non-Node URL scheme before loading or output. No second HTML parser or global
1386
+ reference rewrite is involved, so comments, text, non-script attributes, and entity-spelled asset
1387
+ filenames retain Vite's native parsing and resolution behavior.
1388
+ The resolver leaves NUL-prefixed Rolldown/Vite virtual module IDs to the tool that owns that
1389
+ namespace; author module and asset URLs are extracted and validated before they reach that resolver
1390
+ exception.
1391
+ SVG script `href` and `xlink:href` attributes are parsed too and rejected as classic script loads.
1392
+ Inline module scripts enter Vite's HTML proxy graph and receive the same Oxc boundary analysis as
1393
+ module files. Classic inline scripts cannot enter that graph, so the required security prologue places
1394
+ `Content-Security-Policy` before every author-controlled document token with `script-src 'self'`
1395
+ and `script-src-attr 'none'`: inline classic code and inline event handlers cannot execute, while
1396
+ Vite's same-origin external module entry remains usable. `appBrowser()` accepts no configuration
1397
+ arguments. The returned Vite configuration is one closed trusted unit: its Vue and boundary
1398
+ plugins, CSS analyzer, dependency optimizer, environment, builder, output pipeline, and HTML asset
1399
+ callbacks cannot be extended or replaced through the factory. This deliberately excludes arbitrary
1400
+ Vite, Rolldown, esbuild, PostCSS, worker, environment, builder, externalization, output-injection,
1401
+ and URL-rewrite hooks that could mutate a dependency, worker graph, bundle, or final asset after
1402
+ the boundary has inspected it. The computed root Vite configuration is trusted generated code:
1403
+ wrapping, mutating, or replacing the object returned by `appBrowser()` is outside the factory
1404
+ contract and is reported as computed-artifact drift by `scaffold audit`. The output-boundary plugin
1405
+ still rejects public directories, browser asset inlining, and output path overrides in a
1406
+ post-factory composition as defense in depth; that narrow check is not a general extension seam.
1407
+
1408
+ The browser development server applies the same trust boundary before Vite's internal middleware.
1409
+ Its explicit filesystem allowlist contains only browser/core source roots, browser tests, their
1410
+ exact setup files, and installed dependencies. The pre-internal middleware decodes direct,
1411
+ alias-shaped, and `/@fs/` requests, resolves existing targets through their physical paths, and
1412
+ returns a path-free 403 response unless the target remains in one of those roots. It also rejects
1413
+ an allowed root whose physical identity escapes the workspace, so neither a nested symlink nor a
1414
+ linked root can expose `app/server`, `src/server`, repository metadata, or unrelated files.
1415
+
1416
+ What it allows is deliberately real-world:
1417
+
1418
+ - safe stylesheet `@import`s and `url()` assets;
1419
+ - HTML-referenced assets, including candidate lists, inline style blocks, and inline module scripts;
1420
+ - static `new URL('./asset', import.meta.url)` asset references;
1421
+ - static-string dynamic imports whose decoded source passes the same environment and containment law.
1422
+
1423
+ What it rejects is equally deliberate:
1424
+
1425
+ - a published `src/*` module reaching into private `app/*`;
1426
+ - a core module reaching a stylesheet, a browser module, a server module, a Node builtin, or a
1427
+ browser or server package subpath;
1428
+ - a browser module reaching a Node builtin or a server subpath;
1429
+ - a server module reaching a stylesheet, Vue, or a browser subpath;
1430
+ - a workspace-relative import that resolves outside the workspace, or a dependency import that
1431
+ escapes the exact physical package root established by the nearest bounded, unlinked
1432
+ `package.json` whose own `name` exactly matches the resolved dependency;
1433
+ - a package `#imports` mapping that resolves outside both the declaring package and another exact
1434
+ physical package root;
1435
+ - an HTML reference carrying `vite-ignore` that violates the same environment or containment law
1436
+ as an ordinary reference, a Vite `%ENV%` HTML substitution, a classic external script, or a
1437
+ computed dynamic import in the module graph that would bypass graph resolution;
1438
+ - a computed or expression-bearing `new URL` asset source that could escape at runtime;
1439
+ - malformed URI encoding, encoded traversal segments, or a local `file:` URL outside the owning
1440
+ environment/package root; file schemes are matched case-insensitively and converted to physical
1441
+ paths before containment.
1442
+
1443
+ Unsupported stylesheet `@import` or `url()` syntax is an error rather than a silently skipped
1444
+ dependency. **`publicDir` is disabled on every generated build target**: an asset that is not
1445
+ reachable through the module graph is not silently copied past the boundary. The output plugin
1446
+ fails during configuration when a caller attempts to enable `publicDir`; browser builds likewise
1447
+ reject a nonzero `assetsInlineLimit`, keeping asset bytes external and visible to output auditing
1448
+ before any output directory mutation. A caller-supplied Rolldown `output.dir` or `output.file` is
1449
+ also rejected during configuration; the exact generated `build.outDir` is the sole write root.
1450
+
1451
+ **The policy suite.** [`tests/setupPolicy.ts`](../../tests/setupPolicy.ts) is a narrow structural
1452
+ policy pass built on the official TypeScript compiler. It exists for exactly the laws a linter
1453
+ cannot express — that a centralized module exports every top-level declaration it holds, that
1454
+ implementation files hold one class and no stray module-scope declaration, that no function is
1455
+ declared inside another function outside a directly-passed callback, that interface properties are
1456
+ readonly, that privacy is a runtime `#` field rather than a TypeScript modifier, that a barrel
1457
+ re-exports only through `export *`, and that a computed dynamic import cannot smuggle a
1458
+ cross-environment dependency past the declared import rules. Vue components are inspected for the
1459
+ same evasions. It is a complement to lint and typecheck, never a second type system, and it is not a
1460
+ general-purpose source analyzer. Generated workspaces receive the same exported policy module as a
1461
+ host-origin file and run it as a dedicated Node-only `policy` test project over
1462
+ `tests/policy.test.ts`.
1463
+
1464
+ **Real browser capability.** Browser test projects are gated on the real executable: the generated
1465
+ configuration and the generated policy test both probe `existsSync(chromium.executablePath())`. A
1466
+ browser suite runs when a real Chromium is installed and is skipped honestly when it is not, rather
1467
+ than being faked.
1468
+
1469
+ **Continuous integration.** The generated workflow runs on push and pull request, on
1470
+ `ubuntu-latest`, with read-only contents permission, a 60-minute timeout, and a matrix that **tests
1471
+ Node `22.12.0` and `26`** with fail-fast disabled. Checkout and Node setup are pinned to immutable
1472
+ action commits, and checkout does not persist credentials. Dependencies install with
1473
+ `npm ci --ignore-scripts`; Chromium is installed only when the workspace selects a browser environment
1474
+ or builds its own executable. The gates then run in order: `format:check`, `lint:check`, `check`,
1475
+ `build`, `test`; engine workspaces then run the separate live installed-consumer integration gate.
1476
+
1477
+ **Agent orchestration files.** The session hooks in the generated `.claude/settings.json` run the
1478
+ dependency, model, and external-tool readiness scripts at session start. The **`Stop` hook runs only
1479
+ `git diff --check`** — a whitespace and conflict-marker check over the working tree, nothing more.
1480
+ Bash invocation and sensitive reads are controlled by the **settings permission list, not by a guard
1481
+ script**: every Bash command requires explicit approval, including commands Claude Code otherwise
1482
+ classifies as read-only. Read-only reviewer, checker, and ecosystem roles carry no Bash tool; the
1483
+ orchestrator supplies their diff and status evidence. Bridge, writer, and verifier roles request
1484
+ approval when their bounded shell work is needed. Read patterns covering environment files,
1485
+ package-manager credentials, credential stores, private keys, key stores, SSH, cloud credentials,
1486
+ container configuration, `.kube`, kubeconfig, and service-account JSON are denied. There is no
1487
+ guard script in the vendored set, and none is expected.
1488
+
1489
+ The generated `.codex/config.toml` and `.codex/agents/` mirror the same bounded research, design,
1490
+ implementation, checking, and review roles for Codex. Codex has no repository settings/hook file:
1491
+ each Codex agent's declared `sandbox_mode` is its mechanical permission floor, while the shared
1492
+ `AGENTS.md`, rules, and skills provide the same writing and acceptance contract to both providers.
1493
+
1494
+ ## The `scaffold` executable
1495
+
1496
+ The bin is a thin command-line shell over the two library faces. It exports nothing, so it carries
1497
+ no module API of its own. Six verbs:
1498
+
1499
+ | Verb | Purpose |
1500
+ | --------- | -------------------------------------------------------- |
1501
+ | `new` | scaffold a workspace into `./<name>` |
1502
+ | `pull` | refresh vendored guides and versions, report drift |
1503
+ | `audit` | whole-plan conformance report |
1504
+ | `repair` | restore the shared host-owned set |
1505
+ | `fleet` | audit or repair every workspace under the cwd's children |
1506
+ | `catalog` | regenerate the fleet package-catalog table |
1507
+
1508
+ **Environment selection.** `new` takes `--src a,b` for published library environments and
1509
+ `--app a,b` for private application environments. They are independent: `--src core,server` builds a library,
1510
+ `--app core,browser,server` builds an application, and passing both builds a mixed workspace. Each
1511
+ accepts any subset of `core`, `browser`, and `server`, and the gate rejects the one combination that
1512
+ has no defined configuration class. `--deps x,y` adds runtime dependencies and requires each flag
1513
+ token to use its full valid package name; only the interactive dependency prompt expands an
1514
+ Orkestrel short name. Other npm packages are not a creation-time flag — add them to the generated
1515
+ manifest's development dependencies afterwards, and they round-trip through `deriveBlueprint`'s
1516
+ extras so the workspace stays audit-clean.
1517
+
1518
+ **Other flags.** `--target <path>` selects the directory a verb operates on. `--from <path>` is
1519
+ repeatable and points at a local template or catalog source instead of the bundled one.
1520
+ On `pull`, `--deps x,y` limits refresh to those declared Orkestrel dependencies; without it, every
1521
+ declared dependency mirror is considered.
1522
+ `--groups a,b` scopes an audit to artifact groups. `--live` adds an upstream freshness check to an
1523
+ audit. `--strict` makes a pull throw on a network fault. `--offline` restricts a catalog to local
1524
+ sources. `--prune` opts a repair or fleet run into deleting unexpected files under the three prune
1525
+ directories. `--json` emits one machine-readable value. `--apply` writes, `--yes` skips the
1526
+ confirmation, and `-h` or `--help` prints usage.
1527
+
1528
+ **Safety model.** Every verb is a dry run by default. On a terminal a write asks for confirmation
1529
+ first, defaulting to no; in a script, `--apply` writes and `--yes` skips the question. Every write is
1530
+ confined to the working directory, so the instruction is to change into it first rather than to pass
1531
+ a root. `repair` asks a second, separately defaulted question before deleting anything, and a
1532
+ non-interactive session without `--apply` or `--yes` skips pruning rather than guessing. `fleet`
1533
+ operates on the immediate children of the working directory and never on the directory itself, and
1534
+ it has no root flag at all — `repair` is the single-workspace tool.
1535
+
1536
+ Both `repair` and `fleet` are scoped to host-origin artifacts, and the executable states that scope
1537
+ in its own output: starter and generated files are never touched.
1538
+
1539
+ **Catalog markers.** `catalog` rewrites the block between `<!-- catalog:start -->` and
1540
+ `<!-- catalog:end -->` in `.claude/agents/orkestrel.md`. **Ambiguous markers fail before any
1541
+ mutation**: the file must contain exactly one ordered pair. A missing marker, a reversed pair, or a
1542
+ repeated marker of either kind is a coded `TARGET` failure raised before the file is touched, and
1543
+ the run reports the drift and any row-count shrink rather than rewriting a file it cannot bound.
1544
+
1545
+ **Certificates.** **When the running Node release exposes the system-CA APIs**, the executable
1546
+ merges the operating system trust store into the default certificates, so fetches behind a
1547
+ TLS-inspecting proxy behave like other tooling instead of failing against the bundled list alone.
1548
+ The check is a feature detection: **earlier supported Node 22 releases simply use Node's default
1549
+ roots**. It only ever adds trusted issuers — nothing disables verification — and a failure is a
1550
+ silent no-op rather than a crash. Custom PEMs are added through the standard environment variable.
1551
+
1552
+ **Exit codes.** `0` is clean or successful, `1` is drift or failure, `2` is a usage error. An audit
1553
+ exits non-zero on any drift, foreign files included, which makes it usable directly as a CI gate. A
1554
+ pull exits non-zero on any drift or failure whether or not `--strict` was passed; `--strict`
1555
+ additionally throws on a network fault. Every unknown verb is a usage error and gets a nearest-match
1556
+ suggestion when one is sufficiently close.
1557
+
1558
+ ## Package contents
1559
+
1560
+ The published package is `@orkestrel/scaffold`. Its entry points are the core barrel at `.` and the
1561
+ server barrel at `./server`, both with dual import and require conditions and matching declaration
1562
+ files, plus `./package.json`. The `scaffold` binary maps to the built executable.
1563
+
1564
+ The published file set is exactly `dist/src`, `dist/bin`, `dist/host`, and `README.md`. `dist/host`
1565
+ is the vendored data root: the byte-preserved host files plus the `manifest.json` recording their
1566
+ storage names, destinations, and executable bits. Storage names are un-dotted, because a leading dot
1567
+ does not survive packaging intact; the manifest is what maps a storage name back to its real
1568
+ destination. That is also why the default host is resolved from the installed module's own
1569
+ location — the package carries its host data with itself, and a caller-supplied raw repository root
1570
+ is the explicit alternative, mapping sources 1:1 with no manifest indirection.
1571
+
1572
+ Six runtime dependencies, all scoped: the contract toolkit behind the shape, guard, parser, and
1573
+ safe-attempt primitives; the emitter behind every entity's observation channel; the markdown AST and
1574
+ renderer behind the table and blockquote work; the template engine behind every template-origin
1575
+ artifact; and, consumed only at the executable boundary, the terminal prompt toolkit and the console
1576
+ reporter. The core face uses the first four and stays pure; the server face adds only `node:*`
1577
+ builtins. Development dependencies are the shared tooling baseline plus the guide-parity toolkit
1578
+ that drives [`parity.test.ts`](../../tests/guides/src/parity.test.ts). The engines floor is Node
1579
+ `>=22.12.0`, and the build emits ES and CJS for both library faces plus an ES executable.
461
1580
 
462
- applyOverrides(
463
- [{ path: 'README.md', group: 'docs', origin: 'template', content: '# old' }],
464
- [override('README.md', '# custom')],
465
- )[0].content // '# custom'
466
- ```
1581
+ ## Patterns
467
1582
 
468
- ### Helpers
469
-
470
- Pure, exported utility functions (AGENTS §4.3) — the referentially-transparent leaves
471
- behind the `Compiler`, the `Sync` entity, and the projection surface. Projections use the
472
- `{noun}To{Noun}` idiom (AGENTS §4.6.1): each consumes a WHOLE and returns a derived view of it.
473
-
474
- | API | Kind | Summary |
475
- | ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
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. |
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). |
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. |
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. |
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. |
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. |
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. |
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. |
489
- | `isRecord` | function | Narrow an unknown value to a plain (non-null, non-array) JSON object. |
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. |
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`. |
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`. |
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. |
494
- | `auditToReview` | function | Project an `Audit` into a markdown drift report — findings grouped by `drift`, aligned entries elided; what `repair` will touch. |
495
- | `syncToReview` | function | Project a `SyncReport` into a markdown freshness report via `alignTable` — the sibling of `auditToReview`, guides + versions grouped by `freshness`. |
496
- | `isBehind` | function | Test whether a `Freshness` verdict counts toward "behind" — `true` iff `freshness` is `'behind'`. |
497
- | `planToSummary` | function | Project a `Plan` into a `PlanSummary` — the artifact tally by `origin`, the surfaces, and the covered groups. |
498
- | `pascalCase` | function | Derive the PascalCase entity name from a lowercase-hyphen package name (`'my-router'` → `'MyRouter'`) — hyphens are word breaks. |
499
- | `alignTable` | function | Build a formatter-width-aligned GFM table string from header + row cell strings (+ optional `readonly TableAlign[]`) — the guide Surface-table emitter. |
500
- | `catalogNames` | function | Extract the `@orkestrel/<name>` names from a catalog markdown block/table (the `orkestrel.md` embedded shape), in row order — pure, `[]` when none. The single source both `runCatalog`'s shrink-count and the interactive `new` deps prompt's catalog resolution read. |
501
- | `splitTableRow` | function | Split one rendered GFM table row into its trimmed cell strings, splitting on an UNESCAPED `\|` (a `\\\|` is a literal pipe inside a cell, not a column boundary). |
502
- | `padCell` | function | Right-pad a cell to a codepoint width, oxfmt-style — measured via `Array.from` so a surrogate pair or wide codepoint counts once. |
503
- | `delimiterCell` | function | Build one delimiter-row cell for a GFM table column per its `TableAlign` (`left` / `right` / `center` / `none`). |
504
- | `catalogToBlock` | function | Project a `CatalogEntry[]` into a markdown table via `alignTable` — deduplicated by `name` (a repeated name's LAST entry wins), code-unit sorted; an empty `description` renders `—` (an em dash). The block the `catalog` bin verb splices between `.claude/agents/orkestrel.md`'s markers. |
1583
+ ### Authoring and validating a blueprint
505
1584
 
506
1585
  ```ts
507
1586
  import {
508
- alignTable,
1587
+ blueprint,
509
1588
  blueprintToMembers,
510
- blueprintToPlan,
511
- catalogToBlock,
512
- diffPlan,
513
- manifestToDependencies,
1589
+ createBlueprint,
1590
+ dependency,
1591
+ hasBlueprintEnvironment,
1592
+ hasValidBlueprintBytes,
1593
+ hasValidOverrideBytes,
1594
+ isWorkspaceName,
1595
+ member,
1596
+ override,
514
1597
  pascalCase,
515
- planToReview,
516
- planToSummary,
517
- rangeToFreshness,
518
1598
  validateBlueprint,
1599
+ validateDependencyArray,
519
1600
  } from '@orkestrel/scaffold'
520
1601
 
521
- const plan = blueprintToPlan(spec)
522
- plan.hash // '7b1c9e04' — canonical FNV-1a digest of the plan's content, stable across runs
523
- plan.trace // 'router · core+browser · groups:7 · artifacts:21' — derived, never authored
524
-
525
- pascalCase('my-router') // 'MyRouter' — hyphens are word breaks
526
- blueprintToMembers(spec) // [{ name: 'RouterOptions', category: 'type', surface: 'core' }, …]
527
- planToSummary(plan) // { name: 'router', artifacts: 21, host: 12, template: 6, computed: 3, … }
528
- planToReview(plan) // '# Scaffolding router\n## Artifacts\n| Path | Group | Origin |\n…'
529
- validateBlueprint(spec) // { valid: true, questions: [], warnings: [] }
530
-
531
- const current = { 'package.json': '{ "name": "@orkestrel/router" }' }
532
- diffPlan(plan, current) // { findings: [...], clean: false, complete: true, drifted: 1, missing: 20, foreign: 0 }
533
-
534
- manifestToDependencies('{"dependencies":{"@orkestrel/contract":"^0.0.5"}}') // [{ name: '@orkestrel/contract', range: '^0.0.5' }]
535
- rangeToFreshness('^0.0.5', '0.0.5') // 'current' — pinned to latest
536
- rangeToFreshness('^0.0.5', '0.0.7') // 'behind' — a newer patch is published
537
-
538
- catalogToBlock([
539
- { name: '@orkestrel/router', version: '0.0.5', description: 'A tiny hash-router.' },
540
- { name: '@orkestrel/contract', version: '0.0.5', description: '' },
541
- ]) // '| Package | Version | Description |\n| … |\n| @orkestrel/contract | 0.0.5 | — |\n…' — empty description renders as an em dash
1602
+ const spec = blueprint('router', {
1603
+ src: ['core', 'browser'],
1604
+ dependencies: [dependency('@orkestrel/contract', '^0.0.7')],
1605
+ peers: [dependency('@orkestrel/server', '^0.0.3', true)],
1606
+ overrides: [override('README.md', '# router\n')],
1607
+ })
542
1608
 
543
- alignTable(['API', 'Kind'], [['`createRouter`', 'function']]) // '| API | Kind |\n| … |'
1609
+ pascalCase('my-router') // 'MyRouter'
1610
+ isWorkspaceName('router') // true
1611
+ hasBlueprintEnvironment(spec) // true
1612
+ hasValidBlueprintBytes(spec) // true
1613
+ hasValidOverrideBytes(override('README.md', '# router\n')) // true
1614
+ validateDependencyArray('dependencies', spec.dependencies).questions // []
1615
+ validateBlueprint(spec).valid // true
1616
+ blueprintToMembers(spec)[0] // { name: 'Router', category: 'entity', … }
1617
+ member('RouterOptions', 'type', 'Options for creating a Router.')
1618
+
1619
+ // The validating constructor throws instead of returning questions.
1620
+ createBlueprint({ name: 'router', src: ['core'] })
544
1621
  ```
545
1622
 
546
- ```ts
547
- import { computeHash, stableStringify, validateDependencyArray } from '@orkestrel/scaffold'
548
-
549
- computeHash('hello-world') // '428d118e' — same input, same digest, every run
550
- stableStringify({ b: 1, a: 2 }) // '{"a":2,"b":1}' — key order independent
551
- computeHash(stableStringify({ b: 1, a: 2 })) === computeHash(stableStringify({ a: 2, b: 1 })) // true
552
-
553
- validateDependencyArray('dependencies', [{ name: '', range: '^1' }])
554
- // { questions: [{ field: 'dependencies', text: 'A dependency name must not be empty', … }], seen: Set(0) {} }
555
- ```
1623
+ ### Compiling, gating, and pinning
556
1624
 
557
1625
  ```ts
558
1626
  import {
559
- computeColumnWidth,
560
- formatJson,
561
- renderArray,
562
- renderObject,
563
- renderValue,
1627
+ applyOverrides,
1628
+ blueprint,
1629
+ blueprintToPlan,
1630
+ computeHash,
1631
+ createCompiler,
1632
+ hasValidArtifactBytes,
1633
+ hasValidArtifactHex,
1634
+ hasValidPlanBytes,
1635
+ hasValidPlanHex,
1636
+ pinPlan,
1637
+ planPayload,
1638
+ stableStringify,
1639
+ validatePlan,
564
1640
  } from '@orkestrel/scaffold'
565
1641
 
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
-
573
- `alignTable` builds a markdown `TableNode` (each cell's string parsed with `parseInline`)
574
- and serializes it through `@orkestrel/markdown`'s `renderMarkdown`, which contributes the
575
- STRUCTURE — `\|`-escaping any literal pipe inside a cell and emitting the alignment delimiter
576
- row — at a flat 1-space cell padding. `alignTable` then re-pads BOTH the cells AND the
577
- delimiter row to per-column codepoint width; that re-pad is the whole capability, matching
578
- oxfmt's markdown re-padding so a generated guide passes `format:check` without a formatter
579
- run — codepoint-aware re-padding so a CJK or other wide cell keeps every column aligned,
580
- typed and tested. The `\|`-escape is load-bearing: an unescaped
581
- pipe in a cell would
582
- split it into two columns and silently corrupt the table. Its optional `readonly
583
- TableAlign[]` is the `@orkestrel/markdown` alignment type, imported at the call site, never
584
- re-exported here (AGENTS §6).
1642
+ const compiler = createCompiler()
1643
+ const spec = blueprint('router', { src: ['core'] })
585
1644
 
586
- ### Factories
1645
+ const scaffolding = compiler.compile(spec)
1646
+ scaffolding.stages.map((record) => record.stage) // ['draft', 'gate', 'pin']
587
1647
 
588
- | API | Kind | Builds… |
589
- | -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
590
- | `createCompiler` | function | A `CompilerInterface` — the compilation orchestrator, seeded from `CompilerOptions`. |
591
- | `createPlanManager` | function | A working `PlanManagerInterface`. |
592
- | `createBlueprint` | function | Validate and return a `Blueprint` from plain data — throws `ScaffoldError('INVALID', …)` on failure (structure AND the semantic pass, so an off-`NAME_PATTERN` name throws too). |
593
- | `createMaterializer` | function | A `MaterializerInterface` **(server)** — the materialization entity, seeded from `MaterializerOptions`. |
594
- | `createSync` | function | A `SyncInterface` **(server)** — the upstream-synchronization entity, seeded from `SyncOptions`. |
1648
+ const audit = compiler.audit(spec, {})
1649
+ audit.missing // every artifact nothing exists at the target yet
595
1650
 
596
- ```ts
597
- import { createBlueprint, createCompiler, createPlanManager } from '@orkestrel/scaffold'
1651
+ const plan = pinPlan(blueprintToPlan(spec, ['manifest', 'configs']))
1652
+ validatePlan(plan).valid // true
1653
+ plan.trace?.includes('src:core · app:none') // true
1654
+ planPayload(plan) === planPayload({ ...plan, trace: 'ignored by identity' }) // true
1655
+ hasValidPlanHex(plan) // true
1656
+ hasValidPlanBytes(plan) // true
1657
+ plan.artifacts.every(hasValidArtifactHex) // true
1658
+ plan.artifacts.every(hasValidArtifactBytes) // true
1659
+ computeHash(stableStringify(plan.blueprint)) === computeHash(stableStringify(spec)) // true
1660
+ applyOverrides(plan.artifacts, spec.overrides).length // unchanged when nothing matches
598
1661
 
599
- const compiler = createCompiler() // owns a typed emitter, no sub-engines
600
1662
  compiler.destroy()
601
-
602
- const plans = createPlanManager()
603
- plans.size // 0
604
- plans.destroy()
605
-
606
- createBlueprint({ name: 'Router', surfaces: [] }) // throws ScaffoldError('INVALID', …)
607
1663
  ```
608
1664
 
609
- ### Entities
610
-
611
- | API | Kind | Summary |
612
- | -------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
613
- | `Compiler` | class | The compilation orchestrator — runs the three-stage pipeline and the audit projection, owns a typed emitter. |
614
- | `PlanManager` | class | The self-owning, versioned/hashed plan registry (AGENTS §9) — record ids default to each plan's own content hash. |
615
- | `Materializer` | class | The materialization entity **(server)** — the impure WRITE surface; writes a plan (green-field) or repairs drift (into-existing). |
616
- | `Sync` | class | The upstream-synchronization entity **(server)** — the impure FETCH sibling of `Materializer`; fetches dependency guides + registry versions, refreshes vendored mirrors under the containment law. Promise-based. |
617
-
618
- The server surface also ships nineteen helpers and its factories:
619
-
620
- | API | Kind | Summary |
621
- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
622
- | `isVacant` | function | **(server)** Whether a target path is absent, empty, or contains nothing but a `.git` directory — the green-field target law. |
623
- | `readTarget` | function | **(server)** Read a target's current content at a set of relative paths into a `Record<string, string>` — the I/O that feeds the pure `diffPlan`. |
624
- | `readManifest` | function | **(server)** Read `target/package.json` text; an absent manifest throws `ScaffoldError('TARGET', …)` — the read that feeds `manifestToDependencies`. |
625
- | `locateHostSource` | function | **(server)** Resolve the absolute host-storage path for a host-origin artifact's `source`, manifest-aware — `join(host, source)` when `manifest` is `undefined` (no vendored staging indirection), else the SINGLE matching entry's `storage` path, or `undefined` when zero or more than one entry matches `destination`. |
626
- | `hydratePlan` | function | **(server)** Fill a `host`-origin artifact's `content` from the resolved `host` root (manifest-aware, via `locateHostSource`) so `diffPlan` can content-compare it — a `host` file's drift becomes `'stale'`-detectable rather than presence-only. |
627
- | `discoverPackages` | function | **(server)** List a root's immediate child directories whose `package.json` name starts `@orkestrel/`, as absolute paths, code-unit sorted — the fleet-discovery primitive `fleet` walks. |
628
- | `hostRoot` | function | **(server)** Resolve THIS module's own installed package root (walking up from `import.meta.url`, never `process.cwd()`) and return its vendored `dist/host` path — the single source of truth for the default `Materializer` / bin host; throws `ScaffoldError('TARGET', …)` when no ancestor holds a `package.json`. |
629
- | `selectOrkestrelEntries` | function | **(server)** Filter a manifest record's entries down to `@orkestrel/`-prefixed keys with string values — the shared `dependencies` / `peerDependencies` reader `deriveBlueprint` uses for those two fields (and for computing the `peers ∩ dependencies` exclusion set); `[]` when `value` is not a plain object. |
630
- | `deriveBlueprint` | function | **(server)** Reconstruct a `Blueprint` from an EXISTING repo at `target` — `name` strips the `@orkestrel/` prefix, `surfaces` is read off which `src/<surface>/` directories exist, `dependencies` / `peers` come from `manifest.dependencies` / `manifest.peerDependencies` via `selectOrkestrelEntries` (a `peerDependenciesMeta`-flagged peer carries `optional: true`), `extras` is EVERY entry of `manifest.devDependencies` — not only `@orkestrel/`-prefixed ones, so an external extra (e.g. `zod`) round-trips too — EXCLUDING the generated devDependency baseline (`devDependenciesFor([])`'s keys, read from that SAME source of truth rather than a duplicated literal, which already covers `@orkestrel/guide` / `@orkestrel/scaffold`) and any name also present in `dependencies` / `peerDependencies`, and `overrides` is always `[]`; throws a coded `TARGET` failure on an unreadable/non-`@orkestrel` manifest or no surface directory. |
631
- | `isManifestEntry` | function | **(server)** Whether `value` is a well-formed `host/manifest.json` entry — a string `storage`, a string `destination`, and a boolean `executable`. |
632
- | `readHostManifest` | function | **(server)** Read and validate a vendored host root's `manifest.json`, when present — returns its `readonly ManifestEntry[]`, or `undefined` when `host` has no `manifest.json` (the raw-root 1:1 fallback); throws `ScaffoldError('TARGET', …)` when the file exists but is unreadable, invalid JSON, or not an array of `ManifestEntry`. |
633
- | `listFiles` | function | **(server)** Recursively list a directory's files as root-relative, posix-style paths — `[]` when the directory is absent; the walk `Materializer.prune` uses to enumerate a vendored/target directory's current files. |
634
- | `PRUNE_DIRECTORIES` | const | **(server)** `['.claude/agents', 'scripts']`, frozen — the prune-owned directories: the ONLY directories `pruneTargets` scans and `Materializer.prune` deletes under; a file anywhere else is never prune's business. |
635
- | `vendoredPruneSet` | function | **(server)** The vendored allowlist for ONE prune directory — the destination-relative paths under `directory` (a `PRUNE_DIRECTORIES` entry) that `host` declares and `pruneTargets` must NOT report: read from `manifest.json` `destination`s when the host carries one, else listed straight off `host/<directory>`. FAIL CLOSED: the vendored source must be POSITIVELY established before ANY allowlist (even an empty one) returns — a missing `host` root, or no `manifest.json` AND no `host/<directory>`, throws `ScaffoldError('TARGET', …)`, so an unresolved host never reads as "vendors nothing"; a host that EXISTS and vendors zero files in `directory` remains a valid empty allowlist. |
636
- | `pruneTargets` | function | **(server)** List the repo-relative POSIX paths under `target`'s prune directories (`.claude/agents`, `scripts`) that the vendored `host` allowlist does NOT declare — THE single source of truth for prune drift, consumed by both `Materializer.prune` (which deletes exactly these paths) and the bin's audit/preview UX (which shows them honestly instead of a structurally-always-zero foreign count). Pure read — never deletes anything; `[]` when a prune directory is absent under `target`, or when none of its files are unexpected. Throws `ScaffoldError('TARGET', …)` when `host` cannot positively establish a vendored allowlist for a prune directory that DOES exist under `target` (fail-closed — an unresolved host never reads as "vendors nothing"). |
637
- | `isRecord` | function | **(server)** Whether `value` is a plain object — not `null`, not an array — narrowing to `Record<string, unknown>`. |
638
- | `catalogPackages` | function | **(server)** Build the fleet package catalog — one `CatalogEntry` per `@orkestrel/*` package discovered under each root (`discoverPackages`), its `description` the flattened text of the first paragraph of the guide's opening blockquote (parsed via `@orkestrel/markdown`'s `parseDocument` + `walkNodes`), `''` when the guide is missing/unreadable/blockquote-less/paragraph-less; merged across roots (a later root wins on a repeated name), code-unit sorted. An unreadable ROOT propagates whatever `discoverPackages` throws, unwrapped. |
639
- | `storagePath` | function | **(server)** Map a repo-relative path to its vendored-host STAGING path — a leading-dot TOP-LEVEL FILE maps to `dotfiles/<name-without-dot>`, a leading-dot DIRECTORY segment loses its dot wherever it appears, an undotted path is unchanged. |
640
- | `stageHost` | function | **(server)** The BUILD-time staging primitive the `build:host` npm script calls directly (replacing a standalone build script) — wipes `out`, byte-copies (`copyFileSync`) every file under each `paths` entry (default `HOST_PATHS`) into `<out>/<storagePath(path)>`, deriving each entry's `executable` flag from the `.sh` suffix on `destination` (deterministic on every build platform — Windows `stat` carries no execute bit; all vendored executables are shell scripts by construction), and writes `<out>/manifest.json` (entries code-unit sorted by `destination`, tab-indented, trailing newline); throws `ScaffoldError('TARGET', …)` naming a missing source path, or naming BOTH colliding destinations on a `storagePath` collision (checked BEFORE the manifest is written). Returns the written entries. |
1665
+ ### Registering plans by content hash
641
1666
 
642
1667
  ```ts
643
- import { createMaterializer, isVacant } from '@orkestrel/scaffold/server'
644
-
645
- const target = './packages/router'
646
- isVacant(target) // true — absent, empty, or nothing but a .git dir
647
-
648
- const materializer = createMaterializer()
649
- const result = materializer.materialize(plan, target) // writes every artifact; throws TARGET if not vacant
650
- result.written // ['package.json', 'tsconfig.json', 'src/core/index.ts', …] — rendered files
651
- result.copied // ['AGENTS.md', 'LICENSE', '.claude/settings.json', …] — host-origin byte copies
652
- materializer.destroy()
653
- ```
654
-
655
- ```ts
656
- import { diffPlan } from '@orkestrel/scaffold'
657
- import { discoverPackages, hydratePlan, readTarget } from '@orkestrel/scaffold/server'
658
-
659
- // hydratePlan: content-compare a host-origin artifact instead of presence-only.
660
- const hydrated = hydratePlan(plan, './packages/router') // fills each `host` artifact's `content`
661
- const audit = diffPlan(
662
- hydrated,
663
- readTarget(
664
- './packages/router',
665
- hydrated.artifacts.map((artifact) => artifact.path),
666
- ),
667
- )
668
- audit.findings.filter((finding) => finding.drift === 'stale') // now catches drifted host files too
669
-
670
- // discoverPackages: the fleet-walk `fleet` runs per-repo.
671
- discoverPackages('/repos') // ['/repos/contract', '/repos/scaffold', …] — @orkestrel/* children, sorted
672
- ```
673
-
674
- ```ts
675
- import { hostRoot } from '@orkestrel/scaffold/server'
676
-
677
- hostRoot() // '/…/node_modules/@orkestrel/scaffold/dist/host' — the package's own vendored data root
678
- ```
1668
+ import { blueprint, blueprintToPlan, createPlanManager } from '@orkestrel/scaffold'
679
1669
 
680
- ```ts
681
- import { deriveBlueprint } from '@orkestrel/scaffold/server'
1670
+ const plans = createPlanManager()
1671
+ const record = plans.add(blueprintToPlan(blueprint('router', { src: ['core'] })))
682
1672
 
683
- const spec = deriveBlueprint('./packages/router')
684
- spec.surfaces // ['core', 'browser', 'server'] — read off the live src/<surface>/ directories
685
- spec.peers // e.g. [{ name: '@orkestrel/database', range: '^0.0.5', optional: true }]
686
- spec.overrides // [] — derivation cannot know a caller's template-override intent
1673
+ record.id === record.hash // true — the id is minted from content
1674
+ record.version // 1
1675
+ plans.has(record.id) // true
1676
+ plans.plan(record.id) // the record
1677
+ plans.plans().length // 1
1678
+ plans.remove([record.id]) // true — all-or-nothing over a list
1679
+ plans.remove() // removes everything
1680
+ plans.destroy()
687
1681
  ```
688
1682
 
689
- ```ts
690
- import { hostRoot, isRecord, listFiles, readHostManifest } from '@orkestrel/scaffold/server'
691
-
692
- const host = hostRoot()
693
- const manifest = readHostManifest(host) // readonly ManifestEntry[] | undefined
694
- listFiles(`${host}/.claude/agents`) // ['scout.md', 'builder.md', …] — root-relative, posix-style
695
-
696
- isRecord({ a: 1 }) // true
697
- isRecord(null) // false
698
- ```
1683
+ ### Projecting a plan, an audit, and a report
699
1684
 
700
1685
  ```ts
1686
+ import type { Audit, Plan, SyncReport } from '@orkestrel/scaffold'
701
1687
  import {
702
- PRUNE_DIRECTORIES,
703
- hostRoot,
704
- pruneTargets,
705
- vendoredPruneSet,
706
- } from '@orkestrel/scaffold/server'
707
-
708
- // pruneTargets: the single source of truth Materializer.prune consumes — a pure
709
- // read, never a deletion; [] when the target carries no unexpected files.
710
- pruneTargets('./packages/router', hostRoot()) // ['.claude/agents/rogue.md']
711
-
712
- PRUNE_DIRECTORIES // ['.claude/agents', 'scripts'] — the only directories prune ever touches
1688
+ alignTable,
1689
+ auditToReview,
1690
+ catalogNames,
1691
+ catalogToBlock,
1692
+ delimiterCell,
1693
+ guideMemberTable,
1694
+ padCell,
1695
+ planToReview,
1696
+ planToSummary,
1697
+ splitTableRow,
1698
+ syncToReview,
1699
+ } from '@orkestrel/scaffold'
713
1700
 
714
- // vendoredPruneSet: one prune directory's allowlist — fail-closed when the
715
- // vendored source cannot be positively established.
716
- vendoredPruneSet(hostRoot(), 'scripts') // Set { 'scripts/deps.sh', 'scripts/cursor.sh', 'scripts/ollama.sh' }
717
- ```
1701
+ declare const plan: Plan
1702
+ declare const audit: Audit
1703
+ declare const report: SyncReport
718
1704
 
719
- ```ts
720
- import { stageHost, storagePath } from '@orkestrel/scaffold/server'
1705
+ alignTable(['API', 'Kind'], [['`createRouter`', 'function']])
1706
+ splitTableRow('| a | b |') // ['a', 'b']
1707
+ padCell('ab', 5) // 'ab '
1708
+ delimiterCell('left', 5) // ':----'
721
1709
 
722
- storagePath('.gitignore') // 'dotfiles/gitignore'
723
- storagePath('.claude/agents/scout.md') // 'claude/agents/scout.md'
1710
+ catalogToBlock([{ name: '@orkestrel/router', version: '0.0.5', description: '' }])
1711
+ catalogNames('| @orkestrel/router | 0.0.5 |') // ['@orkestrel/router']
724
1712
 
725
- // The `build:host` script line staging THIS package's own vendored set into dist/host:
726
- const entries = stageHost(process.cwd(), 'dist/host')
727
- entries.length // number of files staged
1713
+ planToSummary(plan).artifacts // the artifact count
1714
+ planToReview(plan) // the copy-ready dry-run review document
1715
+ auditToReview(audit) // findings grouped by drift, aligned entries elided
1716
+ syncToReview(report) // guides and versions, each in its own table
1717
+ guideMemberTable('entity', [])
728
1718
  ```
729
1719
 
730
- ```ts
731
- import { catalogPackages } from '@orkestrel/scaffold/server'
732
- import { catalogToBlock } from '@orkestrel/scaffold'
733
-
734
- const entries = catalogPackages(['/repos'])
735
- entries[0] // { name: '@orkestrel/contract', version: '0.0.5', description: '…' }
736
- catalogToBlock(entries) // the markdown table `scaffold catalog` splices into orkestrel.md
737
- ```
1720
+ ### Exact bytes, snapshots, and drift
738
1721
 
739
1722
  ```ts
1723
+ import type { Plan } from '@orkestrel/scaffold'
740
1724
  import {
741
- isManifestEntry,
742
- locateHostSource,
743
- selectOrkestrelEntries,
744
- } from '@orkestrel/scaffold/server'
745
-
746
- selectOrkestrelEntries({ '@orkestrel/core': '^1.0.0', lodash: '^4.0.0' })
747
- // [['@orkestrel/core', '^1.0.0']]
748
-
749
- isManifestEntry({ storage: 'gitignore', destination: '.gitignore', executable: false }) // true
750
- isManifestEntry({ storage: 'gitignore', destination: '.gitignore' }) // false — missing `executable`
751
-
752
- locateHostSource(undefined, 'package.json', './dist/host') // './dist/host/package.json'
753
- locateHostSource(
754
- [{ storage: 'pkg.tmpl', destination: 'package.json', executable: false }],
755
- 'package.json',
756
- './dist/host',
757
- ) // './dist/host/pkg.tmpl'
758
- ```
1725
+ bytesToHex,
1726
+ contentByteLength,
1727
+ contentCodePoint,
1728
+ contentToBytes,
1729
+ contentToHex,
1730
+ diffPlan,
1731
+ findFileConflict,
1732
+ findPathConflict,
1733
+ hasValidAuditBytes,
1734
+ hasValidSnapshotBytes,
1735
+ inferGroup,
1736
+ snapshotOf,
1737
+ } from '@orkestrel/scaffold'
759
1738
 
760
- ````
1739
+ declare const plan: Plan
761
1740
 
762
- ## Methods
1741
+ contentCodePoint('a', 0) // 97
1742
+ contentByteLength('ab') // 2
1743
+ bytesToHex(contentToBytes('ab')) === contentToHex('ab') // true
763
1744
 
764
- The public methods of each behavioral interface — one table per type, keyed by its
765
- backticked name, every call-signature member listed (the `readonly` data members —
766
- `emitter` on `Compiler`; `emitter` / `size` on `PlanManager`; `emitter` on `Materializer`
767
- and `Sync` — stay in the Surface rows above). Each implementing class exposes exactly its
768
- interface's methods, so this doubles as the per-instance method surface (AGENTS §22). The
769
- bin (`src/bin/scaffold.ts`) is a thin procedural entrypoint — it implements NO behavioral
770
- interface and carries no Methods table (it exports no public members, and §22 parity
771
- excludes `src/bin`).
1745
+ const current = snapshotOf({ 'package.json': '{}\n' })
1746
+ hasValidSnapshotBytes(current) // true
772
1747
 
773
- #### `CompilerInterface`
774
-
775
- `compile` and `audit` are genuinely SYNCHRONOUS and pure — the compiler holds no I/O. After
776
- `destroy()` every method except the getter and `destroy` itself throws
777
- `ScaffoldError('DESTROYED', …)`; `destroy()` is idempotent and tears the emitter down LAST.
1748
+ const audit = diffPlan(plan, current)
1749
+ hasValidAuditBytes(audit) // true
1750
+ inferGroup('src/core/index.ts') // 'source'
778
1751
 
779
- | Method | Returns | Behavior |
780
- | --------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
781
- | `compile` | `Scaffolding` | Run the three-stage pipeline over a `Blueprint` (optionally scoped to a `Group[]` selection), returning a complete or visible-incomplete result. |
782
- | `audit` | `Audit` | Compile the blueprint (optionally group-scoped), then diff the plan against the caller-supplied current target content — drift findings as data, no I/O. A gate-failing blueprint returns an `Audit` with `complete: false`, the gate's blocking `questions`, and ZERO findings — no plan to diff. |
783
- | `destroy` | `void` | Idempotent teardown — emits `destroy`, then destroys the emitter LAST. |
1752
+ findPathConflict(['a/b.ts', 'A/B.ts']) // the first case-insensitive collision
1753
+ findFileConflict(['a', 'a/b.ts']) // a file nested inside another planned path
1754
+ ```
784
1755
 
785
- A groups-scoped `Scaffolding`'s `plan`, materialized into a VACANT target, writes only THOSE
786
- groups' artifacts — a deliberate partial tree, not a complete package. Full package creation
787
- uses the unscoped `compile` (no `groups` argument); `repair` — which reads an existing target's
788
- `Audit` rather than assuming vacancy — is the primary scoped consumer.
1756
+ ### Format-stable JSON and generated text
789
1757
 
790
1758
  ```ts
791
- import { blueprint, createCompiler } from '@orkestrel/scaffold'
792
-
793
- const compiler = createCompiler()
794
- const spec = blueprint('timeout', {
795
- description: 'A typed timeout. Part of the @orkestrel line.',
796
- surfaces: ['core'],
797
- })
798
-
799
- const scaffolding = compiler.compile(spec)
800
- scaffolding.stages.map((record) => record.stage) // ['draft', 'gate', 'pin']
801
- scaffolding.complete // true
802
- scaffolding.plan?.hash // pinned
803
-
804
- const audit = compiler.audit(spec, { 'package.json': '{ "name": "@orkestrel/timeout" }' })
805
- audit.clean // false — the current target is nearly empty
806
- audit.missing // 19 — everything but package.json is absent
807
- compiler.destroy()
808
- ````
809
-
810
- #### `PlanManagerInterface`
811
-
812
- The self-owning, ordered registry over plans (AGENTS §9). `add` re-pins the plan and mints
813
- each record's `id` FROM its content `hash` — the hash IS the identity, so distinct content
814
- always mints a fresh record at `version: 1`; re-adding a plan whose content is unchanged
815
- resolves to the SAME id and returns the existing record untouched, `version` never
816
- incrementing. The array overload of `remove` is declared FIRST (AGENTS §9.2) so an id list
817
- resolves to the batch form. A call after `destroy()` throws `ScaffoldError('DESTROYED', …)`.
818
-
819
- | Method | Returns | Behavior |
820
- | --------- | ------------------------- | ----------------------------------------------------------------------------------------------------- |
821
- | `has` | `boolean` | Whether a plan with the given id is registered. |
822
- | `plan` | `PlanRecord \| undefined` | Look up ONE registered plan record by id (AGENTS §9.1 singular accessor). |
823
- | `plans` | `readonly PlanRecord[]` | List ALL registered plan records (AGENTS §9.1 plural accessor). |
824
- | `add` | `PlanRecord` | Register (or re-register) one plan; emits `add`. |
825
- | `remove` | `boolean` (or `void`) | Remove LISTED plans by id, ONE plan by id, or ALL plans (AGENTS §9.2); emits `remove` per removed id. |
826
- | `destroy` | `void` | Idempotent teardown — clears the collection, emits `destroy`, then destroys the emitter LAST. |
1759
+ import {
1760
+ compareCodeUnit,
1761
+ computeColumnWidth,
1762
+ escapeHtmlText,
1763
+ formatJson,
1764
+ renderArray,
1765
+ renderObject,
1766
+ renderValue,
1767
+ serializeTypeScriptString,
1768
+ } from '@orkestrel/scaffold'
827
1769
 
828
- ```ts
829
- import { blueprint, blueprintToPlan, createPlanManager } from '@orkestrel/scaffold'
1770
+ formatJson({ lib: ['ESNext', 'DOM'] }) // '{\n\t"lib": ["ESNext", "DOM"]\n}\n'
1771
+ renderValue('ESNext', '', '', '') // '"ESNext"'
1772
+ renderArray(['ESNext', 'DOM'], '', '', '') // '["ESNext", "DOM"]'
1773
+ renderObject({ lib: ['ESNext'] }, '') // '{\n\t"lib": ["ESNext"]\n}'
1774
+ computeColumnWidth('\t"a"') // 3
830
1775
 
831
- const plans = createPlanManager()
832
- const record = plans.add(blueprintToPlan(blueprint('budget', { surfaces: ['core'] })))
833
- record.id === record.hash // true — id minted from content, deterministic
834
- record.version // 1
835
- plans.has(record.id) // true
836
- plans.plan(record.id) // the PlanRecord, or undefined
837
- plans.plans() // every registered record
838
- plans.remove(record.id) // true
839
- plans.destroy()
1776
+ escapeHtmlText('<app & "team">') // '&lt;app &amp; &quot;team&quot;&gt;'
1777
+ serializeTypeScriptString("app's") // "'app\\'s'"
1778
+ const sorted = ['b', 'a'].sort(compareCodeUnit) // ['a', 'b']
840
1779
  ```
841
1780
 
842
- #### `MaterializerInterface`
843
-
844
- **(server surface.)** The impure WRITE entity — `node:fs` writes behind an explicit call.
845
- `materialize` is green-field: it refuses any target `isVacant` rejects
846
- (throwing `ScaffoldError('TARGET', …)`), then byte-copies each `host` artifact from the
847
- `host` root and writes each `template` / `computed` artifact's rendered `content`, failing
848
- fast on any write error (`WRITE`). `repair` is into-existing: it skips the vacancy check and
849
- writes ONLY the `missing` / `stale` artifacts an `Audit` names. `prune` is the DELETION
850
- counterpart: it removes `foreign` (target-only) files an `Audit` names, but ONLY under the
851
- two directories the fleet trues wholesale (`.claude/agents/` and `scripts/`) — a `foreign`
852
- file elsewhere is left untouched, never guessed at. After `destroy()` every method throws
853
- `DESTROYED`; teardown is idempotent, emitter last.
854
-
855
- A dependency's `guides/src/<dep>.md` is the ONE write-side degrade: it materializes as a
856
- short one-line stub at `new`-time (the vendored host never carries other packages' guides),
857
- and `scaffold pull` later replaces that stub with the real fetched guide. Any OTHER host
858
- artifact missing from the vendored manifest is a hard `TARGET` failure, never a stub — a
859
- non-guide zero-match manifest lookup means a corrupted or truncated `manifest.json`, not a
860
- legitimate degrade.
861
-
862
- | Method | Returns | Behavior |
863
- | ------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
864
- | `materialize` | `MaterializeResult` | Write a whole plan into a VACANT target — host copies + rendered writes; throws `TARGET` if the target is non-empty beyond `.git`. |
865
- | `repair` | `MaterializeResult` | Write ONLY the artifacts an `Audit` marks `missing` / `stale`, into an EXISTING target — the drift-repair path, no vacancy check. |
866
- | `prune` | `MaterializeResult` | Delete the target-only files under `.claude/agents/` or `scripts/` that the vendored host set does not carry — the bounded-deletion counterpart to `repair`; takes only the target path. |
867
- | `destroy` | `void` | Idempotent teardown — emits `destroy`, then destroys the emitter LAST. |
1781
+ ### Shapes, guards, and parsers
868
1782
 
869
1783
  ```ts
870
- import { blueprint, blueprintToPlan, diffPlan } from '@orkestrel/scaffold'
871
- import { createMaterializer, readTarget } from '@orkestrel/scaffold/server'
872
-
873
- const plan = blueprintToPlan(blueprint('budget', { surfaces: ['core'] }))
874
- const materializer = createMaterializer()
875
-
876
- // Green-field: write everything into a fresh, vacant directory.
877
- materializer.materialize(plan, './packages/budget-new')
1784
+ import {
1785
+ artifactShape,
1786
+ blueprintShape,
1787
+ dependencyShape,
1788
+ hasValidSyncReportBytes,
1789
+ isArtifact,
1790
+ isBlueprint,
1791
+ isCompilerEventHooks,
1792
+ isDependency,
1793
+ isMember,
1794
+ isOverride,
1795
+ isPlan,
1796
+ isPlanManagerEventHooks,
1797
+ isScaffoldError,
1798
+ isSyncReport,
1799
+ memberShape,
1800
+ overrideShape,
1801
+ ownDataValue,
1802
+ parseBoundedJSON,
1803
+ parseCompilerOptions,
1804
+ parseBlueprint,
1805
+ parsePlan,
1806
+ parsePlanIds,
1807
+ parsePlanManagerOptions,
1808
+ parseSyncReport,
1809
+ planShape,
1810
+ ScaffoldError,
1811
+ snapshotPlan,
1812
+ syncReportShape,
1813
+ } from '@orkestrel/scaffold'
878
1814
 
879
- // Repair: audit an existing package, then write back only what drifted.
880
- const audit = diffPlan(
881
- plan,
882
- readTarget(
883
- './packages/budget',
884
- plan.artifacts.map((a) => a.path),
885
- ),
886
- )
887
- materializer.repair(plan, audit, './packages/budget')
1815
+ declare const value: unknown
1816
+
1817
+ dependencyShape()
1818
+ overrideShape()
1819
+ blueprintShape()
1820
+ memberShape()
1821
+ artifactShape()
1822
+ planShape()
1823
+ syncReportShape()
1824
+
1825
+ isDependency({ name: '@orkestrel/contract', range: '^0.0.7' }) // true
1826
+ isOverride({ path: 'README.md', content: '# router\n' }) // true
1827
+ isMember({ name: 'Router', category: 'entity', summary: 'The Router entity.', environment: 'core' })
1828
+ isArtifact({ path: 'README.md', group: 'docs', origin: 'template', content: '# router\n' })
1829
+ ownDataValue({ name: 'router' }, 'name') // 'router'
1830
+
1831
+ parseBoundedJSON('"ready"', (candidate): candidate is string => typeof candidate === 'string', 7)
1832
+ parseCompilerOptions({ on: { destroy: () => undefined } })
1833
+ parseBlueprint('{"not":"a blueprint"}') // undefined — never throws
1834
+ parsePlan(undefined) // undefined
1835
+ parsePlanIds(['first', 'second']) // frozen owned ids
1836
+ parsePlanManagerOptions({ plans: [] }) // exact owned constructor options
1837
+ parseSyncReport('{}') // undefined
1838
+ const parsedPlan = parsePlan(value)
1839
+ if (parsedPlan !== undefined) Object.isFrozen(snapshotPlan(parsedPlan).blueprint)
1840
+
1841
+ if (isBlueprint(value)) value.src
1842
+ if (isPlan(value)) value.artifacts
1843
+ isCompilerEventHooks({ compile: () => undefined }) // true
1844
+ isPlanManagerEventHooks({ add: (id) => id.length > 0 }) // true
1845
+ if (isSyncReport(value)) hasValidSyncReportBytes(value)
888
1846
 
889
- // prune: delete only foreign files under the two bounded directories.
890
- const pruned = materializer.prune('./packages/budget') // deletes .claude/agents/ + scripts/ foreigns only
891
- pruned.removed // ['scripts/legacy-hook.sh'] — a foreign file elsewhere is left untouched
892
- materializer.destroy()
1847
+ try {
1848
+ throw new ScaffoldError('INVALID', 'Blueprint failed the exact-record contract')
1849
+ } catch (error) {
1850
+ if (isScaffoldError(error)) error.code // 'INVALID'
1851
+ }
893
1852
  ```
894
1853
 
895
- #### `SyncInterface`
896
-
897
- **(server surface.)** The impure FETCH sibling of `Materializer` — Promise-based,
898
- network-only. Every method reads upstream over HTTPS with a 10-second per-request timeout
899
- (`AbortSignal.timeout`) and bounded `concurrency` (default 6, never an unbounded
900
- `Promise.all`); the default COLLECT posture captures each dependency's `freshness` (`404` →
901
- `missing`, transport / non-2xx → `failed`) into the report, while `strict` mode instead
902
- throws `ScaffoldError('FETCH', …)` naming the failing URL. `pull` and `write` are the two
903
- halves of a sync — `pull` reads and reports (NO writes), `write` commits the fetched guides
904
- under the containment law. Every non-`current` `GuideSync` / `VersionSync` that has a
905
- discoverable cause carries it in `note` — the last attempt's transport error (with an
906
- `ECONNREFUSED`-style code appended when the runtime attaches one), an `HTTP <status>`, the
907
- fixed redirect-blocked string, or the oversized-body message — so a `failed` entry is never a
908
- bare unexplained verdict (an `ETIMEDOUT` behind a corporate proxy reads identically to any
909
- other transport fault). Guide URLs are built in the CANONICAL
910
- `<base>/orkestrel/<short>/refs/heads/<branch>/guides/src/<short>.md` form directly (never the
911
- legacy `/orkestrel/<short>/<branch>/…` shorthand raw.githubusercontent.com now 301-redirects
912
- from) — `redirect: 'manual'` (A1) stays a deliberate security posture, so the fix is building
913
- the redirect-free URL, never relaxing that policy. Every fetch is UNAUTHENTICATED — no token,
914
- no `Authorization` header, anywhere; every fleet repo is public, so reachability alone is the
915
- signal. After `destroy()` every method throws `DESTROYED`; teardown is idempotent, emitter last.
916
-
917
- | Method | Returns | Behavior |
918
- | ---------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
919
- | `guides` | `Promise<readonly GuideSync[]>` | Fetch each named dependency's guide from the branch HEAD and verdict its `freshness` against an OPTIONAL `current` reference — a caller-supplied `Readonly<Record<string, string>>` of local-mirror content keyed by dependency name (the caller-supplied-reference pattern `diffPlan` uses for target content). WITH the map: a fetched guide byte-equal to its entry is `current`, differing or absent-from-map is `behind`. WITHOUT the map: a successful fetch is ALWAYS `behind` (no reference means it needs syncing). An HTTP `404` is `missing`, a transport fault `failed`, either way. Emits `guide` per resolution. |
920
- | `versions` | `Promise<readonly VersionSync[]>` | Fetch each named dependency's registry `latest` and compare it to the declared `range` via `rangeToFreshness`; emits `version` per resolution. |
921
- | `catalog` | `Promise<readonly CatalogEntry[]>` | The registry-AUTHORITATIVE fleet catalog: enumerates every `@orkestrel/*` name from the org's exact package list (`-/org/orkestrel/package` — never a fuzzy search), then per name fetches its packument (`version` + a registry-path `description` fallback) and its guide (the PREFERRED `description`, its first blockquote's first paragraph). A packument failure keeps the entry degraded (`version: ''`) rather than dropping it; a guide `404` likewise STAYS LISTED (public repos make unreachability a readiness signal, not an absence) with a note like `guide unreachable (HTTP 404 — repo private or guide missing?)`. Sorted code-unit by `name`; emits `package(name, note)` per entry (`note` empty when both fetches succeeded). |
922
- | `pull` | `Promise<SyncReport>` | Read `target/package.json` (`readManifest`), resolve its declared `@orkestrel` deps (`manifestToDependencies`), READ the target's existing `guides/src/<short>.md` mirrors into the `current` reference map (absent files simply omitted), then fetch guides (WITH that map) + versions and return a `SyncReport` — so `pull`'s `GuideSync` freshness is genuinely target-relative. NO writes; emits `done`. |
923
- | `write` | `Promise<readonly string[]>` | Write a report's fetched guides into `target/guides/src` under the containment law (filenames derived from `DEPENDENCY_NAME_PATTERN`-validated names, never a traversal); returns the written paths and emits `write` per file. |
924
- | `destroy` | `void` | Idempotent teardown — emits `destroy`, then destroys the emitter LAST. |
1854
+ ### Drafting artifacts group by group
925
1855
 
926
1856
  ```ts
927
- import { createSync } from '@orkestrel/scaffold/server'
928
-
929
- const sync = createSync() // defaults: raw.githubusercontent.com, branch main, registry.npmjs.org
930
- const report = await sync.pull('.') // reads ./package.json, fetches guides + versions, NO writes
931
- report.clean // false — a mirror or a range fell behind
932
- report.guides.filter((guide) => guide.freshness === 'behind') // stale vendored mirrors
933
- report.versions.filter((version) => version.freshness === 'behind') // out-of-date ranges
1857
+ import {
1858
+ applicationArtifacts,
1859
+ blueprint,
1860
+ blueprintToMembers,
1861
+ ciWorkflow,
1862
+ configArtifacts,
1863
+ devDependenciesFor,
1864
+ dualCondition,
1865
+ entryFields,
1866
+ exportsMap,
1867
+ fillArtifact,
1868
+ guideArtifacts,
1869
+ guideMethods,
1870
+ guideTests,
1871
+ guideUsage,
1872
+ hostGroup,
1873
+ packageManifest,
1874
+ paritySpecifiers,
1875
+ sourceArtifacts,
1876
+ srcVariant,
1877
+ testArtifacts,
1878
+ } from '@orkestrel/scaffold'
934
1879
 
935
- const written = await sync.write(report, '.') // commit the refreshed guides under guides/src
1880
+ const spec = blueprint('router', { src: ['core'], app: ['core', 'server'] })
1881
+ const members = blueprintToMembers(spec)
936
1882
 
937
- const catalog = await sync.catalog() // the registry-authoritative fleet catalog
938
- catalog.length // every published @orkestrel/* package
939
- sync.destroy()
1883
+ hostGroup('AGENTS.md') // 'docs'
1884
+ srcVariant(['core', 'server']) // 'multi'
1885
+ entryFields(['browser']).main // './dist/src/browser/index.js'
1886
+ dualCondition('./dist/src/core/index')
1887
+ exportsMap(['core'])['.']
1888
+ devDependenciesFor(spec.extras).typescript
1889
+ packageManifest(spec) // the whole manifest, newline-terminated
1890
+
1891
+ configArtifacts(spec).length
1892
+ sourceArtifacts(spec, 'Router').length
1893
+ applicationArtifacts(spec).length
1894
+ testArtifacts(spec, 'Router').length
1895
+ guideArtifacts(spec, 'Router', members).length
1896
+ paritySpecifiers(spec).includes('SELF_SPECIFIERS') // true
1897
+ guideUsage(spec, 'Router')
1898
+ guideMethods(spec)
1899
+ guideTests(spec, 'Router')
1900
+ ciWorkflow(spec).includes("node: ['22.12.0', '26']") // true
1901
+
1902
+ fillArtifact('README.md', 'docs', 'readme', {
1903
+ name: 'router',
1904
+ title: '@orkestrel/router',
1905
+ description: 'A tiny hash router.',
1906
+ install: '',
1907
+ usage: '',
1908
+ })
940
1909
  ```
941
1910
 
942
- ## Contract
943
-
944
- These invariants hold across `src/core` + `src/server` ↔ `scaffold.md`:
945
-
946
- 1. **DOC ↔ SOURCE bijection.** Every `function` / `class` / `const` / `interface` / `type`
947
- row in the `## Surface` tables is a real export of the scaffold library source (core or
948
- server), and every such export appears as a Surface row — exhaustive, both directions
949
- (AGENTS §22). The scan covers `src/core` + `src/server` ONLY; `src/bin` is EXCLUDED — the
950
- bin is an executable with no public exports. Adding, renaming, or removing a library
951
- export breaks the parity gate until the doc is reconciled.
952
- 2. **Deterministic, synchronous, immutable — in the core and the `Materializer` (§11).** Same
953
- `Blueprint` + same `Group` selection → the same `Scaffolding`, every time — no clocks, no
954
- randomness, no I/O in the core, nothing async. `pinPlan`'s `trace` and `hash` derive from
955
- the plan's CONTENT alone (paths, origins, sources, and rendered content — everything the
956
- blueprint fully determines), and the `PlanManager` mints record ids from that hash, so
957
- re-adding an unchanged plan is a version no-op. The **bin** AND the server `Sync` entity
958
- are legitimately Promise-based — the bin's prompt flow and `Sync`'s upstream fetches
959
- orchestrate AROUND the synchronous `compile` / write, never inside them; the core and the
960
- `Materializer` stay synchronous. No input is ever mutated; every builder, projection, and
961
- pipeline stage returns a fresh value.
962
- 3. **Three origins, one token boundary.** An `Artifact`'s `origin` is exhaustive and
963
- load-bearing: `host` artifacts byte-copy from the vendored data root (server-only I/O, no
964
- inline `content`); `template` artifacts fill a frozen `TemplateDefinition` through
965
- `@orkestrel/template`'s pure `fillTemplate` with `missing: 'error'` (an unresolved token
966
- fails LOUD, never silently blanks); `computed` artifacts derive from the core's own
967
- manifest/exports combination logic. Only genuinely templated PROSE artifacts fill; every
968
- STRUCTURAL (JSON / TS) file is `computed`, so a literal `{{…}}` in a config is never
969
- mistaken for a placeholder. There is NO `TemplateManager` inside the compiler — the core
970
- uses only the template package's fill LEAF and stays pure and stateless.
971
- 4. **Fail closed at the gate.** A non-empty set of BLOCKING questions (a bad name, empty or
972
- off-vocabulary `surfaces`, a malformed dependency, an override that matches no planned
973
- artifact or targets a `host`-origin path) yields `complete: false`, an ABSENT `plan`, the
974
- `questions` on `Scaffolding.questions`, and a `CompileFailure` coded `BLOCKED` — never a
975
- throw, never a half-formed plan. A NON-blocking question (e.g. a non-vendored dependency's
976
- mirror pointer) rides a COMPLETE result as an advisory. Emitting a partly-valid package
977
- skeleton is worse than returning the question that blocks it.
978
- 5. **One plan, many projections; projections never add.** `planToReview`, `planToSummary`,
979
- `diffPlan`, `auditToReview`, and (on the server) `materialize` are pure views over the
980
- pinned plan — the review renders exactly the plan's artifacts, the summary counts exactly
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.
989
- 6. **The variant matrix is data (§21).** A blueprint's `surfaces` mints EVERY live variant
990
- class the line carries: core-only, core+server, core+browser+server, server-only,
991
- browser-only, core+browser — driving the `package.json` `exports` shape, the per-surface
992
- `configs/src` files, the Vitest projects, the per-surface `src/<surface>/*` and
993
- `tests/src/<surface>/*` source/test artifacts, and the conditional test-setup consequences
994
- (`tests/setupServer.ts` IFF a `server` surface, `tests/setupBrowser.ts` IFF a `browser`
995
- surface — `@vitest/browser-playwright` itself ships in the generated `devDependencies`
996
- baseline UNCONDITIONALLY, grounded against the live exemplars rather than gated on a
997
- browser surface). `SURFACE_MATRIX` is the per-surface layer; ABOVE it `blueprintToPlan`
998
- applies the manifest and exports COMBINATION rules — a multi-surface package OMITS the
999
- top-level `types` field, a single-variant (server-only / browser-only) retargets its lone
1000
- surface to the `.` root with `main` / `module` re-pointed (browser-only using flat ESM
1001
- conditions). Adding a surface changes the PLAN, not the compiler; every variant, including
1002
- the core-only single path, is one row of a table.
1003
- 7. **Mechanism, never policy + scoped mirrors (§21).** The module decides NOTHING about a
1004
- package's identity: the caller owns `name` / `description` / `keywords` / `dependencies`
1005
- and any template `overrides`; the compiler owns the rendering, the closed vocabularies,
1006
- the gate, the pin, and the projections. An absent override means the canonical shipped
1007
- default; a present override REPLACES the rendered artifact at its `path`, never partially
1008
- merges. Dependency guide mirrors scope by the vendored-guides law (Law #2 — one vendored
1009
- copy per runtime dependency): THIS repo vendors all six runtime deps' guides (contract /
1010
- emitter / markdown / template / terminal / console) plus `guide.md` alongside its own —
1011
- seven mirror files. A scaffolded package's `Dependency` therefore gets a BYTE-CORRECT
1012
- OFFLINE mirror only when scaffold vendors that dep's guide (the seven above); any OTHER
1013
- `@orkestrel` dependency yields NO fabricated mirror — the plan emits a `host`-origin
1014
- POINTER artifact, surfaced as a NON-blocking Question. Those shipped mirrors are the
1015
- OFFLINE BASELINE — correct for offline creation; the server `Sync` entity is the FRESHNESS
1016
- path for ANY declared `@orkestrel` dependency (vendored or not), fetching the current guide
1017
- and range from upstream and superseding the baseline when the network is available.
1018
- 8. **Diff-first, write-last.** `compile`, `audit`, `blueprintToPlan`, `diffPlan`,
1019
- `planToReview`, `planToSummary`, and `Sync.pull` are report-only; the ONLY writing acts in
1020
- the package are the server surface's `materialize` / `repair` / `prune` / `Sync.write`,
1021
- each gated behind an explicit call (and the bin's `--apply` / `--prune`, or an accepted
1022
- terminal confirm). The fleet flow (`scaffold fleet`) is the same shape one level up:
1023
- `discoverPackages` finds the repos, `hydratePlan` + `diffPlan` reports drift per repo, and
1024
- only a confirmed write (or `--apply`) writes. The dry-run review is the default posture
1025
- everywhere — you always see the plan (or the drift, or the freshness) before a byte is
1026
- written.
1027
- 9. **Guard totality and single-source parity (§14).** Every validator is a total `Guard` —
1028
- adversarial input returns `false`, never throws. `isBlueprint` / `isPlan` / `isSyncReport`
1029
- / the section guards are COMPILED from `blueprintShape()` / `planShape()` /
1030
- `syncReportShape()` through the contract package's `createContract`, so the guard, the
1031
- parser, the JSON Schema, and the seeded generator are lockstep by construction — an
1032
- off-vocabulary literal, a missing section, or an extra key fails all four identically.
1033
- `NAME_PATTERN` is deliberately NOT a shape refinement (contract's `compileGenerator` throws
1034
- on a pattern-constrained string it cannot sample), so `generate` stays satisfiable; the
1035
- name law lives in the SEMANTIC pass (`validateBlueprint`, the gate, and `createBlueprint`),
1036
- not the compiled contract.
1037
- 10. **Coded errors (§12).** Every throw out of this module is a `ScaffoldError` with a
1038
- machine-readable code (`INVALID` / `DESTROYED` from the core, `TARGET` / `WRITE` / `FETCH`
1039
- from the server) and a `context` carrying the offending path, field, or URL; `BLOCKED` is
1040
- a contained failure marker on a `Scaffolding`, never thrown, and in `Sync`'s default
1041
- collect mode a fetch fault is a captured `freshness`, not a throw. `catch` blocks narrow
1042
- with `isScaffoldError`, never `as`.
1043
- 11. **Observation is a pure side-channel (§13).** The `Compiler` owns a typed emitter
1044
- (`CompilerEventMap` — `compile` / `audit` / `block` / `error` / `destroy`); the
1045
- `PlanManager`, the server `Materializer`, and the server `Sync` own their own. Every event
1046
- is emitted directly and synchronously, AFTER the outcome it reports; only complete
1047
- `compile()` calls emit `compile`, and a gated one emits `block` instead. `audit()` emits
1048
- `audit` after its outcome and NEVER `compile`; a gated `audit()` emits `block` then
1049
- `audit`. A stage throw inside `compile` / `audit` is CONTAINED as a `CompileFailure` on the
1050
- result AND emitted on the domain `error` event for observability. Listener isolation is the
1051
- emitter's own — a throwing listener routes to the `error` OPTION handler, never onto the
1052
- domain `error` event. `destroy()` is idempotent and tears the emitter down LAST.
1053
- 12. **Network is server/bin-only.** ONLY the server `Sync` entity touches the network — the
1054
- core, the `Compiler`, the `Materializer`, every projection, and every guard are
1055
- network-free. `Sync` fetches over HTTPS with a per-request 10-second timeout
1056
- (`AbortSignal.timeout`), no retries by default (opt in via `retries`), bounded concurrency
1057
- (default 6, never an unbounded `Promise.all`), and TLS / proxy configured through the
1058
- ENVIRONMENT (never a verification bypass); it reads guides from `raw.githubusercontent.com`
1059
- and versions from `registry.npmjs.org` (both `base`s injectable). A failed fetch is EITHER
1060
- a thrown `ScaffoldError('FETCH', …)` naming the URL (under `strict`) OR a captured
1061
- `freshness: 'failed'` (the default collect mode) — never an unhandled rejection.
1062
- 13. **DOC ↔ SOURCE method bijection.** Every behavioral interface's `## Methods` table lists
1063
- exactly its public methods (call-signature members) — exhaustive, both directions — and
1064
- each implementing class exposes the same public methods, no more (AGENTS §22). The bin
1065
- implements no interface and is excluded, as in invariant 1.
1066
-
1067
- This package is the line's sole scaffolding and fleet-conformance tool: it renders the
1068
- whole per-surface variant matrix from versioned `TemplateDefinition` data and trues the
1069
- fleet through `scaffold fleet`, so a convention change is a version bump here rather than
1070
- a hand-edit in every repo's copy. Every repo's shared artifacts flow from its vendored host.
1071
-
1072
- Deliberately absent: any **git** operation (no `git init` / `git clone` — the caller prepares
1073
- the vacant target, and the package stops at the file boundary), any **npm** INVOCATION (no
1074
- `npm install`, no lockfile generation — the caller runs the gates; the `Sync` entity's read
1075
- of registry version METADATA is an HTTPS GET, not an npm invocation), any **LLM** (the
1076
- authoring judgment is the caller's, per invariant 7), a foreign template ecosystem (the
1077
- module renders only the `@orkestrel` line's own conventions, versioned in this package),
1078
- asynchronous compilation, and plan persistence (`JSON.stringify(plan)` out, `parsePlan` back
1079
- in). Three sibling engines were considered and REJECTED, each for a concrete reason:
1080
- **`@orkestrel/reason`** — the gate is regex / set-membership / path-matching checks a reason
1081
- `Check`'s comparisons cannot express, and facet deduction already IS `SURFACE_MATRIX` plus the
1082
- manifest/exports combination rules, so there is no inference gap for a reasoner to fill;
1083
- **`@orkestrel/interpret`** — there is no natural-language input to interpret and no
1084
- `ReasonResult` to render; and **`@orkestrel/relation`** — a plan's artifacts are one ORDERED
1085
- list, fully served by the guards, `diffPlan`, and the summary, so no graph layer is needed
1086
- (revisit only if cross-artifact dependency edges ever earn their keep).
1087
-
1088
- ## Patterns
1089
-
1090
- ### Compiling a package with full variant control
1091
-
1092
- The forward path end to end: blueprint → draft → gate → pin → materialize. The
1093
- `SURFACE_MATRIX` selects the `exports` shape, the per-surface configs, and the test projects
1094
- from the declared `surfaces`, so one call scaffolds any variant.
1911
+ ### Emitting the generated build and check configuration
1095
1912
 
1096
1913
  ```ts
1097
- import { blueprint, createCompiler, dependency } from '@orkestrel/scaffold'
1098
- import { createMaterializer } from '@orkestrel/scaffold/server'
1914
+ import {
1915
+ appTsconfig,
1916
+ appViteConfig,
1917
+ applicationViteConfig,
1918
+ coreTsconfig,
1919
+ coreViteConfig,
1920
+ policyViteProject,
1921
+ rootTsconfig,
1922
+ rootViteConfig,
1923
+ singleSrcViteConfig,
1924
+ srcTsconfig,
1925
+ srcViteConfig,
1926
+ viteHeader,
1927
+ } from '@orkestrel/scaffold'
1099
1928
 
1100
- const compiler = createCompiler()
1101
- const scaffolding = compiler.compile(
1102
- blueprint('database', {
1103
- description: 'A minimal-interface data layer. Part of the @orkestrel line.',
1104
- keywords: ['database', 'storage', 'query'],
1105
- surfaces: ['core', 'browser', 'server'], // the full three-surface variant
1106
- dependencies: [dependency('@orkestrel/contract', '^0.0.5')],
1107
- }),
1108
- )
1929
+ rootTsconfig(['core'], ['core', 'server'])
1930
+ coreTsconfig()
1931
+ srcTsconfig('server')
1932
+ appTsconfig('browser', true)
1109
1933
 
1110
- scaffolding.complete // true
1111
- scaffolding.plan?.groups // ['manifest', 'configs', 'source', 'tests', 'guides', 'docs', 'orchestration']
1934
+ viteHeader(true, true) // the shared header, with browser and Vue support
1935
+ coreViteConfig()
1936
+ srcViteConfig('browser')
1937
+ appViteConfig('server')
1938
+ policyViteProject()
1112
1939
 
1113
- if (scaffolding.plan) {
1114
- const materializer = createMaterializer()
1115
- materializer.materialize(scaffolding.plan, './packages/database') // green-field, vacant target
1116
- materializer.destroy()
1117
- }
1118
- compiler.destroy()
1940
+ rootViteConfig(['core', 'server'])
1941
+ singleSrcViteConfig('server').includes('srcServer') // true
1942
+ applicationViteConfig([], ['core', 'server']).includes('appServer') // true
1119
1943
  ```
1120
1944
 
1121
- ### Selecting artifact groups partial generation
1122
-
1123
- `compile`'s optional `Group[]` scopes the plan to a subset — regenerate just the configs and
1124
- guides after a convention bump, leaving hand-written source untouched.
1945
+ ### Reading declared dependencies and comparing freshness
1125
1946
 
1126
1947
  ```ts
1127
- import { blueprint, createCompiler } from '@orkestrel/scaffold'
1948
+ import { isBehind, manifestToDependencies, rangeToFreshness } from '@orkestrel/scaffold'
1949
+ import {
1950
+ guideStub,
1951
+ packageShortName,
1952
+ readGuideReferences,
1953
+ syncReportOf,
1954
+ } from '@orkestrel/scaffold/server'
1128
1955
 
1129
- const compiler = createCompiler()
1130
- const spec = blueprint('sqlite', { surfaces: ['server'] })
1956
+ manifestToDependencies('{"dependencies":{"@orkestrel/contract":"^0.0.7"}}')
1957
+ rangeToFreshness('^0.0.7', '0.0.7') // 'current'
1958
+ isBehind(rangeToFreshness('^0.0.7', '0.0.9')) // true
1131
1959
 
1132
- const scaffolding = compiler.compile(spec, ['configs', 'guides']) // only these two groups
1133
- scaffolding.plan?.artifacts.every(
1134
- (artifact) => artifact.group === 'configs' || artifact.group === 'guides',
1135
- ) // true
1136
- compiler.destroy()
1960
+ packageShortName('@orkestrel/contract') // 'contract'
1961
+ guideStub('guides/src/contract.md') // the local pointer content
1962
+ readGuideReferences('./packages/router', [{ name: '@orkestrel/contract', range: '^0.0.7' }])
1963
+ syncReportOf('./packages/router', [], []) // { clean: true, failed: 0, … }
1137
1964
  ```
1138
1965
 
1139
- ### Failing closed the blocking path
1140
-
1141
- An off-`NAME_PATTERN` name (or an override that matches nothing, or one targeting a host path)
1142
- is a BLOCKING question: the gate stops, no plan is pinned, and the `Scaffolding` carries the
1143
- question — the caller fixes it and re-compiles. No half-formed package ever leaves the
1144
- pipeline.
1966
+ ### Materializing, repairing, and pruning a target
1145
1967
 
1146
1968
  ```ts
1147
- import { blueprint, createCompiler } from '@orkestrel/scaffold'
1148
-
1149
- const compiler = createCompiler()
1150
- const scaffolding = compiler.compile(blueprint('My-Router', { surfaces: ['core'] }))
1151
-
1152
- scaffolding.complete // false — the gate failed closed
1153
- scaffolding.plan // undefined — nothing to project, deliberately
1154
- scaffolding.questions // [{ field: 'name', text: 'Name must match ^[a-z][a-z0-9-]*$', blocking: true }]
1155
- scaffolding.failures // [{ stage: 'gate', code: 'BLOCKED', message: '1 blocking question' }]
1156
- compiler.emitter.on('block', (questions) => questions.length) // fires instead of `compile`
1157
- compiler.destroy()
1158
- ```
1159
-
1160
- ### Auditing an existing package — the conformance engine
1969
+ import { blueprint, blueprintToPlan, diffPlan } from '@orkestrel/scaffold'
1970
+ import {
1971
+ createMaterializer,
1972
+ hostRoot,
1973
+ hydratePlan,
1974
+ isVacant,
1975
+ locateHostSource,
1976
+ readHostManifest,
1977
+ readManifest,
1978
+ readTarget,
1979
+ remapArtifactPath,
1980
+ stageHost,
1981
+ storagePath,
1982
+ } from '@orkestrel/scaffold/server'
1161
1983
 
1162
- The audit is pure core: the server reads the target's current content (`readTarget`), the
1163
- core diffs it against the plan (`diffPlan`), and the drift comes back as data — the
1164
- per-file conformance checklist, mechanized. No byte is written.
1984
+ const host = hostRoot()
1985
+ readHostManifest(host) // the vendored manifest, or undefined for a raw root
1986
+ storagePath('.claude/agents/reviewer.md') // 'claude/agents/reviewer.md'
1987
+ locateHostSource(undefined, 'package.json', host)
1988
+
1989
+ const plan = hydratePlan(blueprintToPlan(blueprint('router', { src: ['core'] })), host)
1990
+ remapArtifactPath(
1991
+ { path: '.claude/agents', group: 'orchestration', origin: 'host' },
1992
+ '.claude/agents',
1993
+ )
1165
1994
 
1166
- ```ts
1167
- import { auditToReview, blueprint, blueprintToPlan, diffPlan } from '@orkestrel/scaffold'
1168
- import { readTarget } from '@orkestrel/scaffold/server'
1995
+ const materializer = createMaterializer()
1996
+ isVacant('./packages/router-new') // true — absent, empty, or only a .git dir
1997
+ materializer.materialize(plan, './packages/router-new')
1169
1998
 
1170
- const plan = blueprintToPlan(blueprint('abort', { surfaces: ['core'] }))
1999
+ readManifest('./packages/router')
1171
2000
  const current = readTarget(
1172
- './packages/abort',
2001
+ './packages/router',
1173
2002
  plan.artifacts.map((artifact) => artifact.path),
1174
2003
  )
2004
+ materializer.repair(plan, diffPlan(plan, current), './packages/router')
2005
+ materializer.prune('./packages/router', {})
2006
+ materializer.destroy()
1175
2007
 
1176
- const audit = diffPlan(plan, current)
1177
- audit.clean // false — the repo drifted from the line's conventions
1178
- audit.findings.filter((finding) => finding.drift === 'stale') // e.g. [{ path: '.oxfmtrc.json', … }]
1179
- auditToReview(audit) // '# Drift — abort\n## Stale\n| Path | Group |\n…'
2008
+ stageHost(process.cwd(), 'dist/host').length // the number of files staged
1180
2009
  ```
1181
2010
 
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
1197
- directory-shaped host artifact (`.claude`) has no single storage file for `hydratePlan` to
1198
- read, so it stays presence-only regardless — a KNOWN, documented boundary, not a promise:
1199
- `pruneTargets` separately covers UNEXPECTED files under it (the prune allowlist scan), but a
1200
- byte-modified file that IS on the allowlist inside `.claude` is not detected by either path.
1201
-
1202
- ### Repairing drift — write only what changed
1203
-
1204
- Repair chains the audit into the server surface: `materialize` refuses a non-vacant target,
1205
- so repairing an EXISTING package goes through `repair`, which writes only the `missing` /
1206
- `stale` artifacts the audit named.
2011
+ ### Pulling guides and versions
1207
2012
 
1208
2013
  ```ts
1209
- import { blueprint, blueprintToPlan, diffPlan } from '@orkestrel/scaffold'
1210
- import { createMaterializer, readTarget } from '@orkestrel/scaffold/server'
1211
-
1212
- const plan = blueprintToPlan(blueprint('abort', { surfaces: ['core'] }))
1213
- const audit = diffPlan(
1214
- plan,
1215
- readTarget(
1216
- './packages/abort',
1217
- plan.artifacts.map((a) => a.path),
1218
- ),
1219
- )
1220
-
1221
- const materializer = createMaterializer()
1222
- const result = materializer.repair(plan, audit, './packages/abort') // only the drifted files
1223
- result.written // ['.oxfmtrc.json', 'configs/src/tsconfig.core.json'] — nothing aligned is touched
1224
- materializer.destroy()
1225
- ```
2014
+ import { createSync } from '@orkestrel/scaffold/server'
1226
2015
 
1227
- ### Layering template overrides
2016
+ const sync = createSync({ concurrency: 4, retries: 1 })
1228
2017
 
1229
- Mechanism-never-policy in practice: the package renders the canonical defaults; a caller who
1230
- needs a bespoke file supplies an `override` whose `content` replaces the rendered artifact at
1231
- that path. An absent override means the default — the caller opts into exactly the files they
1232
- want to own. An override that matches NO planned artifact, or that targets a `host`-origin
1233
- path (host bytes are governed by the mirror, not per-package overrides), is a BLOCKING
1234
- question — a typo'd path fails the gate closed rather than silently adding a stray file.
2018
+ const report = await sync.pull('.')
2019
+ if (report.failed === 0) await sync.write(report, '.')
1235
2020
 
1236
- ```ts
1237
- import { blueprint, blueprintToPlan, override } from '@orkestrel/scaffold'
2021
+ const deps = [{ name: '@orkestrel/contract', range: '^0.0.7' }]
2022
+ await sync.guides(deps)
2023
+ await sync.versions(deps)
2024
+ await sync.catalog()
1238
2025
 
1239
- const readme = '# @orkestrel/router\n\nA hash-router with a hand-written readme.\n'
1240
- const plan = blueprintToPlan(
1241
- blueprint('router', { surfaces: ['core'], overrides: [override('README.md', readme)] }),
1242
- )
1243
- plan.artifacts.find((artifact) => artifact.path === 'README.md')?.content === readme // true
2026
+ sync.destroy()
1244
2027
  ```
1245
2028
 
1246
- ### Serving blueprints at a tool boundary
1247
-
1248
- The shape DSL payoff: the SAME declaration that compiled the guard serves the tool schema and
1249
- the test data — an MCP tool that accepts blueprints cannot drift from the validator that
1250
- checks them, and the plan it returns is JSON all the way down.
2029
+ ### Fleet discovery, prune scanning, and the local catalog
1251
2030
 
1252
2031
  ```ts
1253
- import { blueprintShape, blueprintToPlan, parseBlueprint } from '@orkestrel/scaffold'
1254
- import { createContract, schemaToParameters, seededRandom } from '@orkestrel/contract'
1255
-
1256
- const contract = createContract(blueprintShape())
1257
-
1258
- const tool = {
1259
- name: 'scaffold_package',
1260
- description: 'Compile an @orkestrel package blueprint into a plan.',
1261
- parameters: schemaToParameters(contract.schema), // the JSON Schema, no `as` anywhere
1262
- }
1263
-
1264
- // In the handler: the string boundary is parseBlueprint; the payload is then trusted typed data.
1265
- function handle(argument: string): string {
1266
- const incoming = parseBlueprint(argument)
1267
- return incoming ? blueprintToPlan(incoming).hash : 'Rejected: not a valid blueprint.'
1268
- }
1269
-
1270
- contract.generate(seededRandom(7)) // a reproducible on-contract blueprint — the test fixture, for free
1271
- ```
1272
-
1273
- ### Targeted sync in an existing repo
2032
+ import {
2033
+ catalogPackages,
2034
+ consumeCatalogAllowance,
2035
+ deriveBlueprint,
2036
+ discoverPackages,
2037
+ guideToDescription,
2038
+ isRealDirectory,
2039
+ listDirectories,
2040
+ listFiles,
2041
+ pruneTargets,
2042
+ selectOrkestrelEntries,
2043
+ vendoredPruneSet,
2044
+ } from '@orkestrel/scaffold/server'
1274
2045
 
1275
- `Sync.pull` reads the target's `package.json`, resolves its declared `@orkestrel`
1276
- dependencies, and fetches each one's upstream guide and registry version — reporting freshness
1277
- as data, writing nothing. `syncToReview` renders it; only an explicit `write` (the bin's
1278
- `--apply`) commits the refreshed mirrors. To scope to a dependency SUBSET, resolve with
1279
- `manifestToDependencies` and call `guides(deps)` / `versions(deps)` directly.
2046
+ const catalogAllowance = new Float64Array([2])
2047
+ consumeCatalogAllowance(catalogAllowance, './packages') // one aggregate slot remains
2048
+ discoverPackages('./packages') // every scoped workspace directly under the root
2049
+ deriveBlueprint('./packages/router') // the faithful inverse an audit diffs against
2050
+ selectOrkestrelEntries({ '@orkestrel/contract': '^0.0.7', vite: '^8.1.5' })
1280
2051
 
1281
- ```ts
1282
- import { syncToReview } from '@orkestrel/scaffold'
1283
- import { createSync } from '@orkestrel/scaffold/server'
2052
+ isRealDirectory('./packages/router')
2053
+ listFiles('./packages/router/.claude/agents')
2054
+ listDirectories('./packages/router/.claude')
1284
2055
 
1285
- const sync = createSync({ concurrency: 6 })
1286
- const report = await sync.pull('.') // all declared @orkestrel deps
1287
- syncToReview(report) // '# Sync — 2 behind\n## Guides\n| Name | Freshness |\n…'
1288
- report.guides.filter((guide) => guide.freshness !== 'current') // the stale / missing mirrors
2056
+ vendoredPruneSet('./dist/host', '.claude/agents')
2057
+ pruneTargets('./packages/router', './dist/host') // never deletes; reports only
1289
2058
 
1290
- if (report.failed === 0) await sync.write(report, '.') // refresh the vendored mirrors under guides/src
1291
- sync.destroy()
2059
+ guideToDescription('> A tiny hash router.\n>\n> More detail.') // 'A tiny hash router.'
2060
+ catalogPackages(['./packages'], 4_096)
1292
2061
  ```
1293
2062
 
1294
- ### Auditing with live drift
1295
-
1296
- `scaffold audit` is a WHOLE-PLAN conformance report — host AND generated artifacts alike, unlike
1297
- `repair`'s host-only scope — layering TWO drift sources: the structural `diffPlan` (the plan vs
1298
- the target on disk) and — under `--live` — the `Sync` freshness pass (each dependency's guide vs
1299
- upstream HEAD, each range vs the registry latest). Any drift is a nonzero exit, so it doubles as
1300
- a CI conformance gate. `audit` NEVER writes.
2063
+ ### The write-transaction boundary
1301
2064
 
1302
2065
  ```ts
1303
- import { blueprintToPlan, diffPlan } from '@orkestrel/scaffold'
1304
- import { createSync, readTarget } from '@orkestrel/scaffold/server'
1305
-
1306
- const plan = blueprintToPlan(spec) // `spec` — the blueprint reconstructed for this repo
1307
- const structural = diffPlan(
1308
- plan,
1309
- readTarget(
1310
- '.',
1311
- plan.artifacts.map((artifact) => artifact.path),
1312
- ),
1313
- )
1314
-
1315
- // --live: `pull` reads the target's own guides/src mirrors into the reference map ITSELF, so
1316
- // its GuideSync freshness is genuinely target-relative (a target-free `guides(deps)` with no
1317
- // reference would instead read every fetched guide as 'behind').
1318
- const sync = createSync()
1319
- const report = await sync.pull('.')
1320
- sync.destroy()
1321
-
1322
- const drifted =
1323
- !structural.clean ||
1324
- report.guides.some((guide) => guide.freshness !== 'current') ||
1325
- report.versions.some((version) => version.freshness !== 'current')
1326
- process.exitCode = drifted ? 1 : 0 // ANY drift fails the CI gate
1327
- ```
1328
-
1329
- ### Offline and failure posture
2066
+ import {
2067
+ commitWriteTransaction,
2068
+ createWriteDirectory,
2069
+ digestFile,
2070
+ digestHex,
2071
+ digestText,
2072
+ discardWriteTransaction,
2073
+ readFileHex,
2074
+ readFileText,
2075
+ replaceDirectory,
2076
+ resolveContainedPath,
2077
+ resolveGuideWrites,
2078
+ resolvePhysicalPath,
2079
+ resolveRealPath,
2080
+ restoreFiles,
2081
+ validateWriteAnchor,
2082
+ validateWriteDirectories,
2083
+ validateWriteTarget,
2084
+ WriteTransaction,
2085
+ } from '@orkestrel/scaffold/server'
1330
2086
 
1331
- `Sync` is built for an enterprise network. Each request carries a 10-second
1332
- `AbortSignal.timeout`, there are no retries by default (opt in with `retries`), concurrency is
1333
- bounded (default 6, never an unbounded `Promise.all`), and TLS / proxy come from the
1334
- environment (never a verification bypass). The DEFAULT posture is COLLECT-and-report: a
1335
- per-dependency failure becomes a captured `freshness` (`404` → `missing`, transport →
1336
- `failed`) on the `SyncReport`, so one unreachable dep never sinks the whole run. `strict` flips
1337
- a failure into a thrown `ScaffoldError('FETCH', …)` that names the URL — for a CI gate that
1338
- must go red on any network fault.
2087
+ resolveRealPath('./packages/router/src')
2088
+ resolveContainedPath('./packages/router', 'src/core/index.ts', 'TARGET', 'target')
2089
+ const full = resolvePhysicalPath('./packages/router', 'package.json', 'TARGET', 'target')
1339
2090
 
1340
- ```ts
1341
- import { isScaffoldError } from '@orkestrel/scaffold'
1342
- import { createSync } from '@orkestrel/scaffold/server'
2091
+ digestText(readFileText('./packages/router', 'package.json', 'TARGET', 'target')) ===
2092
+ digestFile(full)
2093
+ digestHex(readFileHex('./packages/router', 'package.json', 'TARGET', 'target'))
1343
2094
 
1344
- // Collect mode (default): partial failure is DATA, not a throw.
1345
- const collect = createSync({ registry: { base: 'https://registry.example.internal' } })
1346
- const report = await collect.pull('.')
1347
- report.failed // 1 — one dep's registry was unreachable; the rest resolved
1348
- report.versions.find((version) => version.freshness === 'failed') // the captured failure
1349
- collect.destroy()
2095
+ const transaction = WriteTransaction.create('./packages/router', ['package.json'])
2096
+ validateWriteAnchor(transaction.anchor, 'anchor')
2097
+ validateWriteDirectories(transaction)
2098
+ validateWriteTarget(transaction, undefined)
2099
+ createWriteDirectory(transaction.stage, 'staging')
1350
2100
 
1351
- // Strict mode: any fetch fault throws, naming the URL — the CI-gate posture.
1352
- const strict = createSync({ strict: true, retries: 2 })
1353
2101
  try {
1354
- await strict.pull('.')
1355
- } catch (error) {
1356
- if (isScaffoldError(error)) error.code // 'FETCH'
2102
+ commitWriteTransaction(transaction, ['package.json'])
2103
+ } catch {
2104
+ restoreFiles(transaction, ['package.json'])
2105
+ discardWriteTransaction(transaction)
1357
2106
  }
1358
- strict.destroy()
2107
+
2108
+ replaceDirectory('./staged', './target', './backup')
2109
+ resolveGuideWrites([], './packages/router') // preflighted destinations, before any write
1359
2110
  ```
1360
2111
 
1361
- ### The `scaffold` bin six subcommands, one build target
1362
-
1363
- The CLI is its OWN build target — `src/bin/scaffold.ts`, an executable, not a barrel. It
1364
- opens with a `#!/usr/bin/env node` shebang, strips a single leading literal `--` off `argv`
1365
- (npm's passthrough residue, mangled by PowerShell on Windows — `npm run scaffold -- new x`
1366
- still parses as `new x`), parses the remainder with `node:util`'s `parseArgs` (no foreign arg
1367
- parser), widens Node's trusted-issuer set to the OS certificate store via
1368
- `trustSystemCertificates` (feature-detected, try/catch no-op, never touching
1369
- `rejectUnauthorized` — so `fetch` survives a corporate TLS-inspecting proxy the way npm
1370
- and browsers already do), and dispatches on SIX subcommands: **`new`** creates a package
1371
- (resolving any `--deps` — `@orkestrel/*` runtime deps, landing in `Blueprint.dependencies`
1372
- — to the registry `latest` → `^latest` ranges, fetching their guides into the plan; a
1373
- range-less `--deps` name the registry could not resolve (`freshness` `'missing'`/`'failed'`,
1374
- or an empty `latest` — `createSync()` is non-strict here, it never throws on its own) is a
1375
- hard failure BEFORE any write, rather than silently landing an unwritable `"^"` range in
1376
- `package.json` with exit `0` (U12c FIX 1); on a real terminal, `--deps`'s free-text prompt
1377
- is an interactive question (Q1) taking `@orkestrel` SHORT names (`contract, emitter`; a bare
1378
- token normalizes to `@orkestrel/<token>`, an already-prefixed one passes through unchanged)
1379
- and validates each against the vendored `@orkestrel` catalog embedded in
1380
- `.claude/agents/orkestrel.md` (re-asking, with a nearest-match suggestion, on an unresolved
1381
- token — degrading to shape-only `DEPENDENCY_NAME_PATTERN` validation with a one-line note
1382
- when the vendored catalog itself cannot be resolved) — TTY-only, `--deps` works identically
1383
- off a terminal or under `--json`. Other npm packages are NOT a `new`-time concept — hand-add
1384
- them to the generated `package.json`'s `devDependencies` after scaffolding;
1385
- `deriveBlueprint`'s `extras` round-trip (below) recompiles them back into the plan on the
1386
- next `audit`/`repair`/`pull`, so a hand-added `devDependencies` entry stays audit-clean
1387
- without the CLI ever collecting it), **`pull`**
1388
- refreshes an existing repo's vendored dependency mirrors and
1389
- reports range drift, **`audit`** / **`repair`** / **`fleet`** all reconstruct the target's
1390
- `Blueprint` with `deriveBlueprint` (never a hand-built stand-in) before compiling — **`audit`**
1391
- runs the structural conformance check — hydration-aware, so host drift is content-detectable,
1392
- not presence-only — (plus, under `--live`, guide-vs-HEAD and range-vs-latest freshness), and
1393
- NOW MERGES the prune scan (`pruneTargets`) into its report: an unexpected file under
1394
- `.claude/agents/` or `scripts/` is a real `foreign` row, its count included under `--json`,
1395
- and it counts as drift like any other finding — `audit` exits `1` on a foreign file exactly as
1396
- it does on a `missing` / `stale` one, so an unaudited stray file in a fleet repo is never
1397
- invisible to the CI gate. `audit` accepts an optional `--groups a,b` to restrict the compiled plan to the listed `Group`s
1398
- (validated against `GROUPS`; an unrecognized name is a USAGE error — exit `2`, not a coded
1399
- failure) — default absent compiles the FULL plan, unchanged; **`repair`** is the HOST-RESTORATION tool, full
1400
- stop — after compiling, it filters the plan to `origin === 'host'` artifacts ONLY (including
1401
- `.github/workflows/ci.yml` — single-target explicit intent keeps full HOST scope, unlike
1402
- `fleet`) BEFORE hydrate/diff/apply, so a mature repo's hand-written `src` / `tests` / `guides`
1403
- / `package.json` is NEVER overwritten with a generated stub — writes a SINGLE target's
1404
- missing + stale HOST artifacts and, behind a SECOND confirm (or `--prune`), deletes
1405
- target-only files under `.claude/agents/` and `scripts/` ONLY — the preview (and `fleet`'s
1406
- equivalent preview) LISTS the exact paths `pruneTargets` found rather than a bare count, the
1407
- confirm states the TRUE count derived from that same list, and a count of `0` prints
1408
- "no unexpected files to delete" and skips the prune question entirely (there is nothing to
1409
- ask about) — `--prune` reaches this scan/preview/confirm/deletion flow whenever there is prune
1410
- work to do, REGARDLESS of whether the host itself audits clean: a clean-host repo with a
1411
- planted foreign file still gets pruned; only a clean host WITH nothing to prune skips straight
1412
- to the "nothing to write" verdict. **`repair` WITHOUT `--prune` reports and restores template-owned (host-origin)
1413
- files only** — its own audit is the raw `diffPlan` over the host-scoped plan, so it never
1414
- reports (or acts on) unexpected files at all, an asymmetry with `audit`'s merged, honest
1415
- foreign-aware report that is defensible (repair's scope is host-restoration, not deletion) but
1416
- worth naming explicitly: run `audit` for the honest whole-picture report including unexpected
1417
- files, and `repair --prune` to actually delete them. And **`fleet`** is the fleet
1418
- verb: it walks the CURRENT WORKING DIRECTORY's IMMEDIATE CHILDREN via `discoverPackages`
1419
- (never the cwd itself, and there is NO `--root` flag at all — the cd-model IS the interface:
1420
- the caller `cd`s into the folder that CONTAINS the checkouts first, exactly as `repair` is the
1421
- one-repo counterpart) → per-repo host-origin audit/repair, printing a per-repo AND total drift
1422
- table and exiting nonzero on residual drift — fleet-wide writes EXCLUDE
1423
- `.github/workflows/ci.yml` from their scope (repo-flavored CI genuinely diverges across two
1424
- live repos in the fleet; a fleet write must never clobber that divergence), while
1425
- single-target `repair` keeps FULL host scope, including `ci.yml`, since it operates on one
1426
- repo the caller is intentionally editing. **`new`** / **`audit`** / **`repair`** / **`fleet`**
1427
- default their read-only source to the resolved `hostRoot()` (the package's own vendored
1428
- `dist/host`) and accept a repeatable `--from` to override it (a sibling repo's checkout, for
1429
- fleet-wide mirroring; `--from` is UNCONFINED — a read-only source sits outside the
1430
- write-destination containment law below) — the DEFAULT host degrades silently to
1431
- presence-only auditing when it cannot resolve (dev ergonomics), but an EXPLICITLY-passed
1432
- `--from` that fails to resolve to usable data is a coded `TARGET` failure on `audit` /
1433
- `repair` / `fleet`, never a silent downgrade, since the caller named that source on purpose.
1434
- **`catalog`** is the sixth: its default source is the npm REGISTRY (`Sync.catalog()`), the
1435
- AUTHORITATIVE package list, sourced UNAUTHENTICATED (every fleet repo is public); the SAME
1436
- repeatable `--from` (unified — the prior separate `--host` name and catalog-only `--root` name
1437
- are BOTH gone) ADDS local-only discoveries the registry doesn't know about yet (registry wins
1438
- overlapping `version`, the local guide's description wins overlapping `description` when a
1439
- `--from` root carries one — it may be ahead of GitHub); `--offline` skips the registry/GitHub
1440
- entirely and sources `--from`(s) only (default `[process.cwd()]`) — the prior, fully-local
1441
- behavior, now opt-in rather than the silent default (a single stale `--from` can no longer
1442
- quietly shrink the catalog to whatever that one root sees). The merged/local entries are
1443
- spliced (via the pure core's `catalogToBlock`) into the fleet package catalog table embedded
1444
- in `<target>/.claude/agents/orkestrel.md` between its `<!-- catalog:start -->` /
1445
- `<!-- catalog:end -->` markers — a `target` missing those markers is a coded `TARGET` failure,
1446
- never a silent skip — and a SHRINK WARNING prints (dry-run and write alike) whenever the new
1447
- table has fewer rows than the one currently embedded, so an accidental catalog shrink is
1448
- never silent either. The TERMINAL preview (`catalogTable`, printed before the apply prompt)
1449
- lists Package + Version only, one line per package — descriptions can run long and would wrap
1450
- each preview row across the terminal, so they stay out of the preview and appear only in the
1451
- written `orkestrel.md` table and in `--json`'s entries. It narrates through `@orkestrel/console` and
1452
- prompts interactively through `@orkestrel/terminal`'s `createTerminal` when a required argument
1453
- is absent, but ONLY on a real TTY (§ non-TTY ceiling below); a piped run instead falls back to
1454
- its flags, or fails a coded USAGE error naming which flag to pass.
1455
-
1456
- An unknown verb (`scaffold sync`, `scaffold mirror`, or any typo) resolves through the
1457
- did-you-mean helper — `sync` and `mirror` are the two RENAMED former verbs and print an
1458
- EXPLICIT redirect (`'sync' has been renamed — use 'scaffold pull'`,
1459
- `'mirror' has been renamed — use 'scaffold fleet'`) rather than a fuzzy guess; every other
1460
- unrecognized verb still gets the nearest-match suggestion. Either way it is a USAGE error —
1461
- exit `2`.
1462
-
1463
- Dry-run is the default posture everywhere. On a real terminal, every WRITE verb PREVIEWS its
1464
- plan (or drift, or freshness) and then ASKS — one confirm question, default **No**; `repair`'s
1465
- (and `fleet`'s) prune deletion sits behind a SECOND default-**No** question, asked only once
1466
- the first write is accepted and ONLY when `pruneTargets` found at least one unexpected file
1467
- (a `0` count skips the question — nothing to ask about, see the prune-UX paragraph above).
1468
-
1469
- `audit` NEVER writes — not even under `--apply` or `--yes` — full stop. Those two flags gate
1470
- ONLY the plain-old `resolveApply` write-confirm every OTHER verb reads; `audit` never calls it
1471
- for itself, so passing them to `audit` changes nothing about `audit`'s own read-only pass.
1472
- What `audit` DOES offer, purely as an INTERACTIVE convenience, is a repair HANDOFF — a confirm
1473
- question asking whether to launch a real `repair` run on the audited target — and that handoff
1474
- is gated STRICTLY on the session being a real TTY. `--apply` / `--yes` are NOT handoff consent:
1475
- on a non-TTY session (any piped/CI run, `--json` or not) the handoff is never even offered,
1476
- regardless of which flags were passed — the flags that DO reach the handoff are the ones
1477
- `audit` forwards to the `repair` run it launches AFTER the human accepts the question, exactly
1478
- as if `repair` had been invoked directly. The handoff is offered when there is host/template-
1479
- origin (shared-file) drift, OR when there are unexpected (foreign) files AND `--prune` was
1480
- passed (a repair without `--prune` cannot delete a foreign file, so offering the handoff for
1481
- foreign-only drift without `--prune` would be a dead end that still exits `1`) — its question
1482
- names exactly what will happen (`N template-owned files have drift`, plus `and M unexpected
1483
- files will be deleted` only when `--prune` is active and there are foreign files to delete),
1484
- never promising a deletion it will not perform. When foreign files exist but the handoff can't
1485
- help them (no `--prune`, or no handoff offered at all — e.g. non-TTY, or no owned drift and no
1486
- `--prune`), `audit` instead prints a plain hint pointing at `scaffold repair --prune`.
1487
- Generated-file-only drift never offers a handoff either way — `repair` cannot fix it, so
1488
- `audit` prints a plain note that those files are generated instead. After an ACCEPTED handoff,
1489
- `audit` re-diffs the FULL plan (not just the host-scoped slice `repair` wrote) and exits `1` if
1490
- ANY drift remains — e.g. the generated-file drift `repair` structurally cannot touch — so a
1491
- green handoff always means the WHOLE target is clean, never just the host-owned slice. A
1492
- `ctrl-c` at any prompt prints `cancelled — nothing written` and exits `1` — an interrupted run
1493
- is never mistaken for a clean one. `--yes` pre-answers every OTHER verb's write/prune prompts
1494
- affirmatively, so a scripted run never blocks on a TTY that will never come; `--apply` goes
1495
- further for those verbs, skipping the ask ENTIRELY and writing unasked, on a terminal or off
1496
- one alike.
1497
-
1498
- **The non-TTY prompt ceiling.** A non-interactive session (piped stdin, CI, any non-TTY) issues
1499
- AT MOST ONE prompt EVER — the single write confirm above. Any SECOND question a verb would
1500
- otherwise ask on a terminal (the prune confirm) instead resolves straight to its safe default
1501
- (never prune) and prints a one-line explanation of why it was skipped, rather than blocking on
1502
- input that will never arrive; `audit`'s repair-handoff question is not a "second prompt" that
1503
- degrades this way — it is TTY-ONLY from the start (see above), so a non-TTY `audit` never asks
1504
- it at all, degrading straight to the plain hint/note instead. A REQUIRED input missing on a
1505
- non-TTY session (e.g. `new` with no name and no `--surfaces`) is a USAGE error — exit `2` —
1506
- naming the flag to pass, directing the human to either run the verb bare on a real terminal or
1507
- supply the flag; it is never silently defaulted. Full multi-prompt guidance (the name prompt,
1508
- the surfaces checkbox, the write confirm, the prune confirm, the handoff offer) is TTY-ONLY — a
1509
- non-TTY session never sees more than the one write confirm, and `--json` (which never prompts
1510
- at all, TTY or not) is the strictest case of this ceiling.
1511
-
1512
- `--json` emits EXACTLY ONE JSON value per verb on stdout — the same serializable contract each
1513
- verb already returns (`SyncReport` for `pull`, `Audit` for `audit` / `repair` — now including
1514
- the merged foreign-file data, plus a `live` field mirroring the freshness verdict when `--live`
1515
- was passed to `audit` — `{ entries, drift, shrink? }` for `catalog`, a fleet drift array for
1516
- `fleet`) — with NO prose alongside it, and implies non-interactive (no prompt, ever, under
1517
- `--json`; combine with `--apply` to also write). EVERY path emits exactly one JSON value under
1518
- `--json` — including an unknown-verb usage error and any otherwise-unexpected failure — as a
1519
- single error envelope (`{ error: { code, message } }`) whose `code` carries the real
1520
- `ScaffoldError` code (or `USAGE` for a bad flag / unknown verb), never a bare stderr line that
1521
- breaks the "exactly one JSON value" contract. Every
1522
- write destination (`new`'s resolved target, `--target` on `pull` / `audit` / `repair` /
1523
- `catalog`, and `fleet`'s cwd-relative per-repo targets) is confined to the current working
1524
- directory — equal to it or nested beneath — so the CLI is safe to run as a global command
1525
- anywhere; a read-only source (`--from`) is EXEMPT from that containment law, and an escaping
1526
- destination is a coded `INVALID` failure, never a silent clamp. Exit codes are UNIFORM across
1527
- every verb — `0` clean, `1` drift or failure (including a cancelled prompt), `2` usage — and
1528
- `pull`'s prior posture of exiting `0` on an unresolved failure outside `--strict` is GONE: any
1529
- drift or failure now exits `1` regardless of `--strict` (`--strict` still additionally THROWS
1530
- `ScaffoldError('FETCH', …)` on a network fault, rather than merely exiting nonzero).
2112
+ ### Server boundary parsing and guards
1531
2113
 
1532
2114
  ```ts
1533
- // The `#!/usr/bin/env node` shebang is re-emitted by the build's `output.banner`, not source.
1534
- import { parseArgs } from 'node:util'
2115
+ import { hasOnlyDataProperties, isDenseDataArray, isEmitterErrorHandler } from '@orkestrel/scaffold'
1535
2116
  import {
1536
- blueprint,
1537
- blueprintToPlan,
1538
- createCompiler,
1539
- dependency,
1540
- diffPlan,
1541
- planToReview,
1542
- planToSummary,
1543
- SURFACES,
1544
- syncToReview,
1545
- } from '@src/core'
1546
- import {
1547
- createMaterializer,
1548
- createSync,
1549
- deriveBlueprint,
1550
- discoverPackages,
1551
- hostRoot,
1552
- hydratePlan,
1553
- pruneTargets,
1554
- readTarget,
1555
- } from '@src/server'
1556
- import { createReporter, createSpinner } from '@orkestrel/console'
1557
- import { createServerSink } from '@orkestrel/console/server'
1558
- import { createTerminal } from '@orkestrel/terminal/server'
1559
-
1560
- const { values, positionals } = parseArgs({
1561
- allowPositionals: true,
1562
- options: {
1563
- surfaces: { type: 'string' },
1564
- deps: { type: 'string' },
1565
- target: { type: 'string' },
1566
- from: { type: 'string', multiple: true },
1567
- apply: { type: 'boolean', default: false },
1568
- yes: { type: 'boolean', default: false },
1569
- json: { type: 'boolean', default: false },
1570
- strict: { type: 'boolean', default: false },
1571
- live: { type: 'boolean', default: false },
1572
- prune: { type: 'boolean', default: false },
1573
- offline: { type: 'boolean', default: false },
1574
- groups: { type: 'string' },
1575
- },
1576
- })
1577
-
1578
- const sink = createServerSink()
1579
- const reporter = createReporter({ sink, width: sink.columns })
1580
- const [command] = positionals // 'new' | 'pull' | 'audit' | 'repair' | 'fleet' | 'catalog'
1581
- const target = values.target ?? '.'
1582
- const from = values.from ?? []
1583
- const host = from[0] ?? hostRoot() // default: the package's own vendored dist/host
1584
- const materializerOptions = from[0] ? { host: from[0] } : {}
1585
- const terminal = createTerminal()
1586
-
1587
- // Every write verb shares this: --apply skips the ask entirely; --json NEVER prompts and
1588
- // NEVER writes on its own (json without --apply is always a pure dry-run, regardless of
1589
- // --yes — json + apply is the only way to write under --json); --yes auto-answers yes on
1590
- // a non-json run; otherwise only a real terminal ever asks (a non-TTY session here is the
1591
- // ONE prompt the non-TTY ceiling allows).
1592
- async function confirmWrite(message: string): Promise<boolean> {
1593
- if (values.apply) return true
1594
- if (values.json) return false
1595
- if (values.yes) return true
1596
- try {
1597
- return await terminal.confirm({ message, default: false })
1598
- } catch {
1599
- reporter.status('error', 'cancelled — nothing written')
1600
- process.exit(1)
1601
- }
1602
- }
1603
-
1604
- // The SECOND, prune-only confirm — never bundled into confirmWrite's question, and only
1605
- // ever asked once (the non-TTY ceiling: a second question off a real terminal instead
1606
- // resolves to its safe default with a printed explanation, never a second prompt).
1607
- async function confirmPrune(target: string, message: string): Promise<readonly string[]> {
1608
- const found = pruneTargets(target, host) // the exact paths the preview already listed
1609
- if (found.length === 0) return [] // "no unexpected files to delete" — no question asked
1610
- if (values.apply || values.prune) return found
1611
- if (values.json || values.yes) return values.yes ? found : []
1612
- if (!terminal.isTTY) {
1613
- reporter.status('info', 'non-TTY: skipping the second prune question — defaulting to No')
1614
- return []
1615
- }
1616
- return (await confirmWrite(message)) ? found : []
1617
- }
1618
-
1619
- // --json emits exactly one JSON value (the verb's own serializable contract), no prose.
1620
- function emit(value: unknown, prose: () => void): void {
1621
- if (values.json) reporter.line(JSON.stringify(value))
1622
- else prose()
1623
- }
1624
-
1625
- if (command === 'pull') {
1626
- const sync = createSync({ strict: values.strict })
1627
- const report = await sync.pull(target)
1628
- emit(report, () => reporter.line(syncToReview(report)))
1629
- if (!report.clean && (await confirmWrite('Write refreshed guide mirrors?'))) {
1630
- await sync.write(report, target)
1631
- }
1632
- sync.destroy()
1633
- process.exit(report.clean ? 0 : 1) // uniform: any drift/failure is nonzero, --strict or not
1634
- } else if (command === 'audit') {
1635
- // deriveBlueprint reconstructs the target's spec from its own package.json + src/<surface>/ dirs.
1636
- const plan = hydratePlan(blueprintToPlan(deriveBlueprint(target)), host)
1637
- const audit = diffPlan(
1638
- plan,
1639
- readTarget(
1640
- target,
1641
- plan.artifacts.map((a) => a.path),
1642
- ),
1643
- )
1644
- // The prune scan MERGES into the audit report: `pruneTargets`'s count becomes `audit.foreign`
1645
- // (a real finding, not a structurally-always-zero placeholder), counted toward drift — audit
1646
- // exits 1 on a foreign file exactly like a missing/stale one — and carried under --json.
1647
- const foreignPaths = pruneTargets(target, host)
1648
- const merged = {
1649
- ...audit,
1650
- foreign: foreignPaths.length,
1651
- clean: audit.clean && foreignPaths.length === 0,
1652
- }
1653
- emit(merged, () => reporter.line(planToReview(plan)))
1654
- // The handoff is a TTY-ONLY interactive convenience — `values.apply` / `values.yes` are
1655
- // NEVER handoff consent (audit itself never writes on their account; they only gate the
1656
- // SEPARATE `repair` run launched below). It is offered when there is host/template-origin
1657
- // drift, OR foreign files AND `--prune` was passed (a repair without `--prune` cannot
1658
- // delete a foreign file, so offering the handoff for foreign-only drift without `--prune`
1659
- // would be a dead end). Generated-only drift never offers a handoff either way.
1660
- const originOf = new Map(plan.artifacts.map((a) => [a.path, a.origin]))
1661
- const drifted = audit.findings.filter((f) => f.drift !== 'aligned')
1662
- const ownedDrift = drifted.some((f) => originOf.get(f.path) === 'host')
1663
- const generatedDrift = drifted.some((f) => originOf.get(f.path) !== 'host')
1664
- const offerHandoff = terminal.isTTY && (ownedDrift || (foreignPaths.length > 0 && values.prune))
1665
- let handoffAccepted = false
1666
- if (offerHandoff) {
1667
- handoffAccepted = await terminal.confirm({ message: 'Hand off to repair?', default: false })
1668
- if (handoffAccepted) {
1669
- reporter.status('info', `run: scaffold repair --target ${target}`)
1670
- // After an ACCEPTED handoff, re-diff the FULL plan (not just repair's host-scoped
1671
- // slice) so a "clean" handoff always means the whole target, not just the host set.
1672
- }
1673
- }
1674
- if (!handoffAccepted && !values.json) {
1675
- if (foreignPaths.length > 0 && !values.prune) {
1676
- reporter.status(
1677
- 'info',
1678
- "unexpected files found — run 'scaffold repair --prune' to delete them",
1679
- )
1680
- }
1681
- if (generatedDrift) {
1682
- reporter.status(
1683
- 'info',
1684
- 'generated-file drift found — these files are generated, not hand-repaired',
1685
- )
1686
- }
1687
- }
1688
- process.exit(merged.clean ? 0 : 1) // ANY drift — including a foreign file — fails the CI gate
1689
- } else if (command === 'repair') {
1690
- // `scaffold repair` — single target, dry-run default, HOST-ORIGIN scope ONLY
1691
- // (ci.yml included — full HOST scope, unlike fleet's ci.yml exclusion), so
1692
- // hand-written src/tests/guides/package.json are never overwritten.
1693
- const compiled = blueprintToPlan(deriveBlueprint(target))
1694
- const scopedToHost = {
1695
- ...compiled,
1696
- artifacts: compiled.artifacts.filter((a) => a.origin === 'host'),
1697
- }
1698
- const plan = hydratePlan(scopedToHost, host)
1699
- const audit = diffPlan(
1700
- plan,
1701
- readTarget(
1702
- target,
1703
- plan.artifacts.map((a) => a.path),
1704
- ),
1705
- )
1706
- emit(audit, () => reporter.line(planToReview(plan)))
1707
- const materializer = createMaterializer(materializerOptions)
1708
- const wrote = !audit.clean && (await confirmWrite('Write missing/stale host files?'))
1709
- if (wrote) materializer.repair(plan, audit, target)
1710
- // pruning sits behind its OWN second confirm, only once the first write is accepted — and
1711
- // the preview above already LISTED the exact paths `pruneTargets` found (never a bare
1712
- // count); a count of 0 skips this question entirely (nothing to ask about).
1713
- if (wrote) {
1714
- const foreignPaths = pruneTargets(target, host)
1715
- const pruneNow = await confirmPrune(
1716
- target,
1717
- `Also prune ${foreignPaths.length} foreign file(s)?`,
1718
- )
1719
- if (pruneNow.length > 0) materializer.prune(target) // .claude/agents/ + scripts/ foreigns ONLY
1720
- }
1721
- materializer.destroy()
1722
- process.exit(!audit.clean && !wrote ? 1 : 0)
1723
- } else if (command === 'fleet') {
1724
- // `scaffold fleet` — the CURRENT WORKING DIRECTORY's IMMEDIATE CHILDREN only, never the
1725
- // cwd itself: the cd-model IS the interface, no --root flag exists — the caller cd's into
1726
- // the folder that CONTAINS the checkouts first; `repair` is the one-repo counterpart.
1727
- const materializer = createMaterializer(materializerOptions)
1728
- let totalDrift = 0
1729
- const rows: Array<readonly [string, number]> = []
1730
- for (const repoTarget of discoverPackages('.')) {
1731
- const repoPlan = blueprintToPlan(deriveBlueprint(repoTarget))
1732
- const scoped = {
1733
- ...repoPlan,
1734
- artifacts: repoPlan.artifacts.filter((a) => a.path !== '.github/workflows/ci.yml'),
1735
- }
1736
- const hydrated = hydratePlan(scoped, host)
1737
- const audit = diffPlan(
1738
- hydrated,
1739
- readTarget(
1740
- repoTarget,
1741
- hydrated.artifacts.map((a) => a.path),
1742
- ),
1743
- )
1744
- if (!audit.clean) {
1745
- totalDrift += audit.drifted + audit.missing
1746
- if (await confirmWrite(`Write drift for ${repoTarget}?`)) {
1747
- materializer.repair(hydrated, audit, repoTarget)
1748
- }
1749
- }
1750
- rows.push([repoTarget, audit.drifted + audit.missing])
1751
- }
1752
- materializer.destroy()
1753
- emit(
1754
- rows.map(([repo, drift]) => ({ repo, drift })),
1755
- () =>
1756
- reporter.table({
1757
- columns: [{ label: 'Repo' }, { label: 'Drift', align: 'right' }],
1758
- rows: rows.map(([repo, drift]) => [repo, String(drift)]),
1759
- }),
1760
- )
1761
- process.exit(totalDrift > 0 ? 1 : 0)
1762
- } else if (command === 'catalog') {
1763
- // --from ADDS local-only discoveries to the registry-authoritative default; --offline
1764
- // sources --from(s) only (default [cwd]); merged/local entries splice via catalogToBlock
1765
- // into <target>/.claude/agents/orkestrel.md between its markers, shrink-warning either way.
1766
- // --json's value is `{ entries, drift, shrink? }` — NEVER a bare `CatalogEntry[]`, so a
1767
- // consumer can read the drift verdict and any shrink warning without re-deriving them.
1768
- // (Illustrative — see catalogToBlock / Sync.catalog in the Surface above.)
1769
- process.exit(0)
1770
- } else {
1771
- // `scaffold new <name>` — creation.
1772
- const name =
1773
- positionals[1] ??
1774
- (await terminal.input({ message: 'Package name', validate: { pattern: '^[a-z][a-z0-9-]*$' } }))
1775
- const picked =
1776
- values.surfaces?.split(',') ??
1777
- (await terminal.checkbox({ message: 'Surfaces', choices: [...SURFACES], min: 1 }))
1778
- const surfaces = SURFACES.filter((surface) => picked.includes(surface)) // narrow to Surface[], no `as`
1779
-
1780
- // --deps (@orkestrel/* runtime deps) resolves an absent range through the registry —
1781
- // ranges pin ^latest; its guides additionally fetch into the plan. On a real terminal,
1782
- // this prompt becomes an interactive question instead — @orkestrel short names,
1783
- // catalog-validated — illustrated in prose above, omitted here for brevity. Other npm
1784
- // packages are hand-added to `package.json`'s `devDependencies` AFTER scaffolding —
1785
- // `deriveBlueprint`'s `extras` round-trip (below) picks them back up on the next
1786
- // `audit`/`repair`/`pull`, so `new` never collects them itself.
1787
- const sync = createSync()
1788
- const versions = await sync.versions(
1789
- (values.deps?.split(',') ?? []).map((depName) => dependency(depName, '*')),
1790
- )
1791
- sync.destroy()
1792
- const deps = versions.map((version) => dependency(version.name, `^${version.latest}`))
1793
-
1794
- const compiler = createCompiler()
1795
- const scaffolding = compiler.compile(blueprint(name, { surfaces, dependencies: deps }))
1796
- if (!scaffolding.plan) {
1797
- reporter.status('error', scaffolding.questions.map((question) => question.text).join('; '))
1798
- compiler.destroy()
1799
- process.exit(2) // usage: an off-contract name/surfaces is caller error, not drift
1800
- }
1801
- emit(scaffolding.plan, () => {
1802
- reporter.section('Plan')
1803
- reporter.line(planToReview(scaffolding.plan)) // dry-run default: show the review
1804
- const summary = planToSummary(scaffolding.plan)
1805
- reporter.table({
1806
- columns: [{ label: 'Origin' }, { label: 'Count', align: 'right' }],
1807
- rows: [
1808
- ['host', String(summary.host)],
1809
- ['template', String(summary.template)],
1810
- ['computed', String(summary.computed)],
1811
- ],
1812
- })
1813
- })
1814
- if (await confirmWrite('Write the package to disk?')) {
1815
- const spinner = createSpinner({ message: 'materializing', sink })
1816
- spinner.start()
1817
- const materializer = createMaterializer()
1818
- const result = materializer.materialize(scaffolding.plan, values.target ?? `./${name}`)
1819
- materializer.destroy()
1820
- spinner.success(`wrote ${result.written.length + result.copied.length} files`)
1821
- }
1822
- compiler.destroy()
1823
- process.exit(0)
1824
- }
1825
- ```
2117
+ isCatalogAllowance,
2118
+ isCatalogDescription,
2119
+ isDependencyData,
2120
+ isFilesystemPath,
2121
+ isHostManifest,
2122
+ isManifestEntry,
2123
+ isMaterializerEventHooks,
2124
+ isMissingPathError,
2125
+ isPortablePath,
2126
+ isReservedTargetPath,
2127
+ isSensitiveHostPath,
2128
+ isSyncEventHooks,
2129
+ isTerminalText,
2130
+ isWritePrecondition,
2131
+ materializerOptionsContract,
2132
+ materializerOptionsShape,
2133
+ parseFilesystemPaths,
2134
+ parseMaterializerOptions,
2135
+ parsePortablePaths,
2136
+ parseSyncBase,
2137
+ parseSyncBranch,
2138
+ parseSyncCurrent,
2139
+ parseSyncDependencies,
2140
+ parseSyncOptions,
2141
+ parseWritePreconditions,
2142
+ syncGuideOptionsShape,
2143
+ syncOptionsContract,
2144
+ syncOptionsShape,
2145
+ syncRegistryOptionsShape,
2146
+ } from '@orkestrel/scaffold/server'
1826
2147
 
1827
- The build wiring follows the §7 two-file wrapper pattern: `configs/src/tsconfig.bin.json`
1828
- sets `types: ["node"]` and uses the `rootDir` trick (the broad `../../src` root with a scoped
1829
- `include: ["../../src/bin/**/*.ts"]`) so the bin can type-check against `@src/core` source;
1830
- `configs/src/vite.bin.config.ts` is a lib build with `entry` → `dist/bin/scaffold.js`,
1831
- `formats: ['es']`, externals `node:*` / `@orkestrel/*` / `@src/*`, an `output.banner`
1832
- re-emitting the `#!/usr/bin/env node` shebang, and NO dts plugin (an executable ships no
1833
- declarations). `package.json` declares `"bin": { "scaffold": "./dist/bin/scaffold.js" }`, and
1834
- `build:src` chains the bin build LAST (after core and server) so the executable links against
1835
- fresh sibling builds. Invocation follows the tool's life: `npm run scaffold` pre-publish (the
1836
- repo's own script), `npx @orkestrel/scaffold` post-publish, and `node_modules/.bin/scaffold`
1837
- once it is a devDependency of a consumer.
1838
-
1839
- ```sh
1840
- # new — create a package (dry-run previews; on a terminal it then asks, default No; --apply
1841
- # writes unasked); --deps is @orkestrel/* runtime deps (dependencies). Other npm packages are
1842
- # NOT a new-time flag — hand-add them to the generated package.json's devDependencies after
1843
- # scaffolding; audit/repair/pull recompile them back into the plan (deriveBlueprint's extras
1844
- # round-trip), so a hand-added devDependency stays audit-clean:
1845
- npx @orkestrel/scaffold new router --surfaces core,browser,server
1846
- npx @orkestrel/scaffold new router --deps @orkestrel/contract --apply --target ./packages/router
1847
-
1848
- # pull — refresh vendored dep mirrors + report range drift (any drift/failure exits 1,
1849
- # --strict or not; --strict additionally THROWS on a network fault):
1850
- npx @orkestrel/scaffold pull --target . --apply
1851
- npx @orkestrel/scaffold pull --deps @orkestrel/contract,@orkestrel/emitter --strict --json
1852
-
1853
- # audit — structural conformance (now merging the prune scan: a stray file under
1854
- # .claude/agents/ or scripts/ is a real foreign finding, counted as drift), +live freshness;
1855
- # nonzero on ANY drift, foreign files included (the CI gate); offers a repair handoff on a
1856
- # terminal whenever host-origin drift OR a foreign file is found (never for generated-only
1857
- # drift, which gets a plain note instead); --json emits exactly one Audit value, its `foreign`
1858
- # count now real and a `live` field present when --live ran:
1859
- npx @orkestrel/scaffold audit --live
1860
- npx @orkestrel/scaffold audit --json
1861
-
1862
- # repair single target, dry-run default; on a terminal it asks before writing (--apply
1863
- # skips the ask), then a SECOND default-No question (or --prune) before deleting
1864
- # target-only files under .claude/agents/ and scripts/ ONLY:
1865
- npx @orkestrel/scaffold repair --target . --apply --prune
1866
- npx @orkestrel/scaffold repair --target . --from ../contract # audit against a sibling's host
1867
-
1868
- # fleet — the CURRENT WORKING DIRECTORY's IMMEDIATE CHILDREN (never the cwd itself) then
1869
- # per-repo host-origin audit/repair; dry-run default, confirms per repo on a terminal
1870
- # (--apply writes unasked), nonzero on residual drift; excludes
1871
- # .github/workflows/ci.yml (repo-flavored — use `repair --apply` per repo for that one
1872
- # file); NO --root flag at all — cd into the folder that CONTAINS your checkouts first
1873
- # (repair is the single-repo tool: run it from inside one repo instead):
1874
- cd ~/repos && npx @orkestrel/scaffold fleet
1875
- cd ~/repos && npx @orkestrel/scaffold fleet --apply --json
1876
-
1877
- # catalog — regenerate the fleet package catalog embedded in orkestrel.md between its
1878
- # markers; the npm registry is authoritative by default (unauthenticated), --from ADDS
1879
- # local-only discoveries (repeatable — the SAME flag the read-only-source verbs use,
1880
- # replacing the old --host and catalog's old --root), --offline sources --from(s) only,
1881
- # dry-run reports drift (nonzero) plus any shrink warning, --apply writes:
1882
- npx @orkestrel/scaffold catalog --target . --apply
1883
- npx @orkestrel/scaffold catalog --from ~/repos --from ~/other-repos --target . --apply
1884
- npx @orkestrel/scaffold catalog --offline --from ~/repos --target . --apply
1885
-
1886
- # unknown verb — the two RENAMED former names redirect explicitly rather than a fuzzy guess;
1887
- # any other typo still gets the nearest-match suggestion. Either way: usage error, exit 2.
1888
- npx @orkestrel/scaffold sync # 'sync' has been renamed — use 'scaffold pull'
1889
- npx @orkestrel/scaffold mirror # 'mirror' has been renamed — use 'scaffold fleet'
2148
+ declare const caught: unknown
2149
+
2150
+ syncGuideOptionsShape()
2151
+ syncRegistryOptionsShape()
2152
+ syncOptionsShape()
2153
+ materializerOptionsShape()
2154
+ syncOptionsContract.parse({ concurrency: 4 })
2155
+ materializerOptionsContract.parse({ host: './dist/host' })
2156
+
2157
+ parseSyncOptions({ guides: { branch: 'main' }, registry: { timeout: 5_000 } })
2158
+ parseMaterializerOptions({ host: './dist/host' })
2159
+ parseSyncBase('registry.npmjs.org') // 'https://registry.npmjs.org'
2160
+ parseSyncBranch('main')
2161
+ parseSyncCurrent({ '@orkestrel/contract': '# contract\n' }, ['@orkestrel/contract'], 16_777_216)
2162
+ parseSyncDependencies([{ name: '@orkestrel/contract', range: '^0.0.7' }], false)
2163
+ parsePortablePaths(['src/core/index.ts'], 1_000)
2164
+ parseFilesystemPaths(['./packages'], 1_000)
2165
+ parseWritePreconditions([{ path: 'package.json', shape: 'absent' }], 1_000)
2166
+
2167
+ isPortablePath('src/core/index.ts') // true
2168
+ isFilesystemPath('./packages/router') // true
2169
+ isTerminalText('router') // true
2170
+ isDependencyData({ name: '@orkestrel/contract', range: '^0.0.7' }) // true
2171
+ isSensitiveHostPath('.env.local') // true
2172
+ isReservedTargetPath('.git/config') // true
2173
+ isCatalogAllowance(new Float64Array([1])) // true
2174
+ isCatalogDescription('A tiny hash router.') // true
2175
+ hasOnlyDataProperties({ a: 1 }) // true
2176
+ isDenseDataArray(['a'], 10, isPortablePath) // true
2177
+ isWritePrecondition({ path: 'package.json', shape: 'absent' }) // true
2178
+ isManifestEntry({ storage: 'AGENTS.md', destination: 'AGENTS.md', executable: false }) // true
2179
+ isHostManifest({ entries: [], roots: [] }) // true
2180
+ isSyncEventHooks({ done: () => undefined }) // true
2181
+ isMaterializerEventHooks({ done: () => undefined }) // true
2182
+ isEmitterErrorHandler(() => undefined) // true
2183
+ isMissingPathError(caught) // true only for an ENOENT error
1890
2184
  ```
1891
2185
 
1892
- ### Fleet wiring
1893
-
1894
- The rendered defaults ship as **versioned package data**: each is a frozen
1895
- `TemplateDefinition` (a `name`, a `content` string with `{{token}}` placeholders, and its
1896
- `placeholders`) filled by `@orkestrel/template`'s pure `fillTemplate` with `missing: 'error'`
1897
- — NOT bespoke string interpolation, and NOT a `TemplateManager` (the compiler carries no
1898
- sub-engine). The byte-copied governance files (`HOST_PATHS`) ship VENDORED inside the
1899
- published tarball at `dist/host/` (staged there by the build's `build:host` step, alongside a
1900
- `dist/host/manifest.json` recording each entry's storage name, destination, and executable
1901
- bit), which the server's `Materializer` copies from its `host` root — the package's OWN
1902
- vendored copy by default, or an explicit `--from` sibling for fleet-wide mirroring. There is
1903
- ONE versioned source of truth, and `npm update @orkestrel/scaffold` propagates a convention
1904
- change to every consumer.
1905
-
1906
- This package is the line's sole scaffolding and fleet-conformance spec, and this guide is its
1907
- sole living document — the variant matrix, the per-file inventory, the exports shapes, the
1908
- config wrappers, and the audit checklist all live here, projected from the same `Plan` the
1909
- compiler emits. Every repo in the fleet carries `@orkestrel/scaffold` as a devDependency
1910
- (pinned at `SCAFFOLD_RANGE`, joining `@orkestrel/guide` as line-wide dev tooling) and a
1911
- `"scaffold": "scaffold"` script against the installed bin. Fleet-truing runs as
1912
- `scaffold fleet` — either from the package's own vendored `dist/host` (the common case) or
1913
- from an explicit `--from` sibling repo — trueing every repo's shared artifacts across the
1914
- workspace; `repair` and `audit` operate per repo. `.github/workflows/ci.yml` is the one
1915
- fleet exception: two repos carry repo-flavored CI, so `fleet` never writes it — a
1916
- single-target `repair --apply` does, per repo, for that one file.
1917
-
1918
- ### Practices
1919
-
1920
- - **Dry-run first, always** — `compile` / `blueprintToPlan` / `planToReview` and `Sync.pull` /
1921
- `syncToReview` are report-only; read the plan (or the audit, or the freshness) before ever
1922
- writing. Writing is opt-in (the server's `materialize` / `repair` / `Sync.write`, the bin's
1923
- `--apply`).
1924
- - **One blueprint, one package** — a compound request (two packages) is two `compile` calls,
1925
- not one blueprint with a wider `surfaces` list; `surfaces` selects the variant of ONE
1926
- package, never bundles several.
1927
- - **Audit before you edit a fleet repo** — `diffPlan` turns the per-file conformance checklist
1928
- into findings; add `--live` for guide + range freshness, or `--groups a,b` to gate CI on a
1929
- subset (e.g. `--groups configs,docs,orchestration`); repair the `missing` / `stale` HOST-ORIGIN
1930
- set with `repair` (never a hand-written `src` / `tests` / `package.json` file — `repair` is
1931
- the host-restoration tool ONLY), refresh mirrors with `Sync.write`, and leave `aligned` /
1932
- `current` untouched.
1933
- - **A named `--from` must resolve** — the default host degrades to presence-only silently when
1934
- absent (dev ergonomics), but an explicitly-passed `--from` that fails to resolve is a coded
1935
- `TARGET` failure on `audit` / `repair` / `fleet` — never a silent downgrade for a source the
1936
- caller named on purpose.
1937
- - **True the fleet with `fleet`, not by hand** — run `scaffold fleet` from the folder that
1938
- CONTAINS your checkouts (no `--root` flag — the cd-model is the interface); it runs
1939
- `discoverPackages` → per-repo hydrated audit → repair on an accepted confirm (or `--apply`),
1940
- EXCLUDING `.github/workflows/ci.yml` (repo-flavored, never fleet-clobbered — use
1941
- single-target `repair --apply` for that one file); reach for `repair --prune` only for the
1942
- bounded `.claude/agents/` / `scripts/` cleanup a single target needs, never a wider deletion.
1943
- - **Override, don't fork** — need a bespoke file? Add one `override` for that path; the rest
1944
- stay canonical and keep tracking the shipped templates. Never copy the whole plan to change
1945
- one file.
1946
- - **Reference deps by their real range** — a `dependency('@orkestrel/contract', '^0.0.5')`
1947
- drives the `package.json` entry, the vendored guide mirror (when scaffold ships it), and the
1948
- build externals from one declaration; declare exactly what `src/` imports (the exports combination rules).
1949
- - **Collect by default, `strict` for CI** — leave `Sync` in collect mode for an interactive
1950
- freshness report; flip `strict: true` only where a network fault MUST fail the run (a CI
1951
- gate), and inject `guides.base` / `registry.base` at a local fixture for hermetic tests.
1952
- - **Gate untrusted blueprints twice** — `parseBlueprint` for shape at the boundary,
1953
- `validateBlueprint` for semantics; reserve `createBlueprint`'s throw for programmer-error
1954
- contexts where invalidity is a bug (§12).
1955
- - **Store `pinPlan` output, not drafts** — the `hash` is the identity;
1956
- `JSON.stringify(plan)` out, `parsePlan` back in, and the `PlanManager` recognizes the
1957
- unchanged content as the same version.
1958
- - **Keep the target vacant for creation** — `materialize` refuses a non-empty target
1959
- (throwing `TARGET`); repair into an existing package with `repair`, never by clearing it
1960
- first.
1961
- - **Destroy when done** — `destroy()` releases the emitter; a destroyed `Compiler` /
1962
- `PlanManager` / `Materializer` / `Sync` throws `DESTROYED` on use (narrow with
1963
- `isScaffoldError`).
1964
-
1965
2186
  ## Tests
1966
2187
 
1967
- Environment-dependent cases degrade gracefully rather than false-redding: `tests/setupServer.ts`
1968
- probes the running host's actual capability once at load (`canSymlink`, `canSocket`, `hasModes` —
1969
- the last is platform-as-semantics, since POSIX mode bits have no Windows equivalent to probe for),
1970
- and the handful of tests that need a real symlink, a real Unix domain socket, or a real exec bit
1971
- guard themselves with `it.skipIf` naming exactly what goes unverified on a host lacking the
1972
- capability. Every one of those cases runs — and must pass — unconditionally on a capable POSIX
1973
- host; the skip is the environment's ceiling, never a hidden failure.
1974
-
1975
- - [`tests/guides/src/parity.test.ts`](../../tests/guides/src/parity.test.ts) — the
1976
- `## Surface` `src/core` + `src/server` bijection (value + type exports; `src/bin` is
1977
- EXCLUDED — the executable has no public exports) and the `## Methods` ↔ interface-method
1978
- bijection, across both library surfaces.
2188
+ - [`tests/src/core/helpers.test.ts`](../../tests/src/core/helpers.test.ts) the pure leaves: table
2189
+ alignment, byte encoding, snapshots, conflicts, projections, hashing, and format-stable JSON.
2190
+ - [`tests/src/core/builders.test.ts`](../../tests/src/core/builders.test.ts) the blueprint,
2191
+ dependency, override, and member builders, including optional-field omission.
2192
+ - [`tests/src/core/validators.test.ts`](../../tests/src/core/validators.test.ts) every guard and
2193
+ refinement against valid, off-contract, hostile, and boundary input.
2194
+ - [`tests/src/core/shapers.test.ts`](../../tests/src/core/shapers.test.ts) per-shape guard
2195
+ exactness, schema essentials, seeded generation, and parse round-trips.
2196
+ - [`tests/src/core/compilers.test.ts`](../../tests/src/core/compilers.test.ts) — every drafted
2197
+ group, the manifest and exports combination rules, and the emitted configuration text.
1979
2198
  - [`tests/src/core/Compiler.test.ts`](../../tests/src/core/Compiler.test.ts) — the three-stage
1980
- pipeline, stage order and records, group-scoped compilation, the `audit` projection,
1981
- override layering, fail-closed blocking (questions + `BLOCKED` failure + absent plan), event
1982
- sequences (`compile` vs `block`, `audit`), idempotent `destroy`, `DESTROYED` throws.
1983
- - [`tests/src/core/PlanManager.test.ts`](../../tests/src/core/PlanManager.test.ts) —
1984
- content-hash IS the id, distinct content mints a fresh record at `version: 1`, an
1985
- unchanged re-add returns the existing record with `version` never incrementing, batch
1986
- `remove` all-or-nothing, per-event emissions, destroy semantics.
1987
- - [`tests/src/core/helpers.test.ts`](../../tests/src/core/helpers.test.ts) — every projection
1988
- (`blueprintToMembers` inventory, `blueprintToPlan` PER-VARIANT generation conformance across
1989
- ALL SIX live classes — core-only, core+server, core+browser+server, server-only,
1990
- browser-only, core+browser — asserting the right `src/<surface>/*` + `tests/src/<surface>/*`
1991
- artifact set, the conditional `setupServer.ts` / `setupBrowser.ts`, the computed
1992
- `{{specifiers}}` parity fill, and the three `rootViteConfig` shapes per surface count, plus
1993
- `SURFACE_MATRIX` wiring and the manifest/exports combination rules — `peerDependencies` /
1994
- `peerDependenciesMeta` emission for `peers`, `extras` merging into `devDependencies` with
1995
- extras winning a collision — template-fill vs computed origins + the token-collision
1996
- boundary, `planToReview` / `auditToReview` / `syncToReview` table emission, `planToSummary`
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),
2000
- `manifestToDependencies` across all three sections deduplicated, `rangeToFreshness` exact-pin
2001
- law, `pinPlan` determinism), `validateBlueprint` errors + warnings (incl. the per-array
2002
- `peers` / `extras` name/range/duplicate rules and the three cross-array overlap blocks),
2003
- `validateDependencyArray`'s pure `{ questions, seen }` return, `pascalCase`, `alignTable`
2004
- (oxfmt-width padding, `\|` escaping, alignment delimiter row) and its `splitTableRow` /
2005
- `padCell` / `delimiterCell` leaves, `isBehind`'s `'behind'`-only verdict, `inferGroup`'s
2006
- ordered prefix classification, `isRecord`'s plain-object narrowing, and `computeHash` /
2007
- `stableStringify` determinism (key-order-independent hashing).
2008
- - [`tests/src/core/compilers.test.ts`](../../tests/src/core/compilers.test.ts) — every drafting
2009
- leaf `blueprintToPlan` orchestrates: `hostGroup` classification, `fillArtifact` template
2010
- filling (with/without a `surface` tag, throwing on an unknown template id), `surfaceVariant` /
2011
- `entryFields` / `dualCondition` / `exportsMap` across every variant, `compareCodeUnit` /
2012
- `devDependenciesFor` merging, `packageManifest` shape, `rootTsconfig`, the three
2013
- `rootViteConfig` / `singleSurfaceViteConfig` shapes, `coreTsconfig` / `coreViteConfig`,
2014
- `surfaceTsconfig` / `surfaceViteConfig`, `configArtifacts` / `sourceArtifacts` drafting,
2015
- `paritySpecifiers` primary-surface resolution, `testArtifacts` drafting, `guideArtifacts` /
2016
- `guideMemberTable` dedup-across-surfaces, `applyOverrides` replace/no-op/host-skip/no-match
2017
- semantics, plus byte-for-byte cross-consistency between each direct leaf's output and the
2018
- matching artifact `blueprintToPlan` emits, across every surface variant.
2019
- - [`tests/src/core/builders.test.ts`](../../tests/src/core/builders.test.ts) — every builder's
2020
- output shape (defaults filled, absent optional keys omitted, exact-guard round-trips).
2021
- - [`tests/src/core/validators.test.ts`](../../tests/src/core/validators.test.ts) — each guard
2022
- accepts valid / rejects invalid + adversarial junk, exact-record semantics, off-vocabulary
2023
- literal rejection, `parseBlueprint` / `parsePlan` / `parseSyncReport` ↔ guard soundness.
2024
- - [`tests/src/core/shapers.test.ts`](../../tests/src/core/shapers.test.ts) — `blueprintShape` /
2025
- `planShape` / `syncReportShape` compilation through `createContract`: guard/parser/schema/generator
2026
- lockstep, generated values satisfy their guards.
2199
+ pipeline, the fail-closed gate, the emission sequences, and post-destroy behavior.
2200
+ - [`tests/src/core/PlanManager.test.ts`](../../tests/src/core/PlanManager.test.ts) content-hash
2201
+ ids, the batch-overload semantics, and all-or-nothing list removal.
2202
+ - [`tests/src/core/policy.test.ts`](../../tests/src/core/policy.test.ts) — the repository coding-law
2203
+ policy module against this workspace and against deliberately hostile fixtures.
2204
+ - [`tests/src/server/helpers.test.ts`](../../tests/src/server/helpers.test.ts) containment,
2205
+ digests, host staging, hydration, derivation, prune scanning, and the local catalog.
2206
+ - [`tests/src/server/validators.test.ts`](../../tests/src/server/validators.test.ts) — the portable
2207
+ path law, data-only reflection, and the exact-shape record guards.
2027
2208
  - [`tests/src/server/Materializer.test.ts`](../../tests/src/server/Materializer.test.ts) —
2028
- green-field `materialize` into a vacant temp dir (manifest-aware host copies incl. the
2029
- executable bit off `manifest.json`, plus rendered writes), `TARGET` refusal on a non-vacant
2030
- target, `repair` writing only drifted artifacts, `prune` deleting ONLY `foreign` artifacts
2031
- under `.claude/agents/` / `scripts/` (the bounded containment law) and leaving a foreign file
2032
- elsewhere untouched, PLUS `prune`'s FAIL-CLOSED law (H1): a `--host`-style unresolvable host
2033
- root throws `TARGET` BEFORE any deletion (target files untouched), while a host that GENUINELY
2034
- EXISTS and vendors zero files in a `directory` (an existing empty dir, or a manifest with zero
2035
- entries there) still prunes every foreign file under it — the distinction is missing-host vs
2036
- empty-vendor, never conflated; `isVacant` / `readTarget` / `hydratePlan` / `discoverPackages` /
2037
- `hostRoot` / `deriveBlueprint` against a real `node:fs` fixture — `deriveBlueprint` fixtures
2038
- covering surface detection off `src/<surface>/` directories, `peers` with
2039
- `peerDependenciesMeta`-sourced `optional`, `extras` excluding the generated devDependency
2040
- baseline (`devDependenciesFor([])`'s keys, covering `@orkestrel/guide` / `@orkestrel/scaffold`)
2041
- AND any devDependency ALSO present in `peerDependencies` / `dependencies` (H3: the middleware
2042
- pattern of dev-installing a peer for its own tests never double-lands in `extras`), an EXTERNAL
2043
- (non-`@orkestrel`) devDependency (e.g. `zod`) surviving that exclusion as a genuine `extras`
2044
- round-trip (U12c FIX 3: closes the hand-added-devDependency immediate self-audit-DRIFTED
2045
- regression the CLI never collects `extras` itself; a reader hand-adds the entry to
2046
- `package.json` and `deriveBlueprint` picks it back up), and the coded `TARGET` failures
2047
- (unreadable/non-JSON/
2048
- non-`@orkestrel` manifest, no surface directory); `hostRoot` resolving to the package's own
2049
- BUILT `dist/host` bundle (never `process.cwd()`); `WRITE` fail-fast, `remove` event emission,
2050
- destroy semantics.
2051
- - [`tests/src/server/helpers.test.ts`](../../tests/src/server/helpers.test.ts) — `hostRoot`,
2052
- `deriveBlueprint`, `discoverPackages`, `hydratePlan`, and `catalogPackages` against a real
2053
- `node:fs` fixture; `diffPlan` content-comparing a HYDRATED host-origin artifact (a
2054
- byte-mutated target is `stale`, counted in `drifted`) against the SAME target read by the
2055
- UNHYDRATED plan (still `aligned` — presence-only preserved when there is no `content` to
2056
- compare), plus a `Materializer.repair` round-trip proving a hydrated `'stale'` finding
2057
- re-copies the artifact byte-equal from `host`; `pruneTargets` (a real fixture: an unexpected file under
2058
- `.claude/agents/` / `scripts/` is reported, a vendored one is not, an absent prune directory
2059
- under `target` yields `[]`, and the fail-closed `TARGET` throw when `host` cannot positively
2060
- establish an allowlist for a prune directory that DOES exist under `target` — the same law
2061
- `Materializer.prune` and the bin's audit/repair UX both consume), plus the three
2062
- no-nested-functions leaves standalone:
2063
- `selectOrkestrelEntries` (`@orkestrel/`-prefixed string-valued filtering, `[]` on a
2064
- non-object), `isManifestEntry` (valid entry accepted; missing/mistyped `executable` and a
2065
- non-object rejected), and `locateHostSource` (the manifest-`undefined` raw-join fallback, the
2066
- single-match resolution, and `undefined` on zero or duplicate `destination` matches);
2067
- `storagePath` (dotfile-top-level-file, `.claude`/`.github` directory-segment un-dotting,
2068
- nested `.github/workflows/ci.yml`, and a plain undotted name) and `stageHost` against a real
2069
- temp-directory fixture with a hand-built `paths` list — byte-preserving copies, the
2070
- owner-execute bit captured AND propagated onto the staged copy, a wipe-first `out` (a stale
2071
- file left over from a prior run disappears), the written `manifest.json`'s
2072
- destination-sorted/tab-indented/trailing-newline shape, a missing-source `TARGET` naming the
2073
- path, and a `storagePath` collision's `TARGET` naming both destinations.
2074
- - [`tests/src/server/Sync.test.ts`](../../tests/src/server/Sync.test.ts) — a real `node:http`
2075
- fixture serving guide bytes at `/<name>/<branch>/guides/src/<name>.md` and registry JSON
2076
- `{"dist-tags":{"latest":"0.0.N"}}` at the URL-encoded scoped path, with `guides.base` /
2077
- `registry.base` injected (§16 no-mocks): asserts fetching + writing under the containment
2078
- law, the `freshness` verdicts (incl. `404` → `missing` and timeout → `failed`), the `strict`
2079
- `FETCH` throw naming the URL, the bounded `concurrency`, the `guide` / `version` / `write` /
2080
- `done` event order, and `DESTROYED`. `Sync.catalog` adds an org-list fixture
2081
- (`/-/org/orkestrel/package`) plus per-package packument + guide routes: registry entries
2082
- prefer the guide blockquote description over the packument's, a guide `404` STAYS LISTED
2083
- with the packument-description fallback and the exact `guide unreachable (HTTP 404 — repo
2084
- private or guide missing?)` note, a failed packument keeps the entry degraded (`version: ''`)
2085
- rather than dropping it, an unreachable/malformed org-list response throws a coded `FETCH`,
2086
- entries sort code-unit by `name` regardless of org-list key order, and no request anywhere
2087
- (org list, packument, guide) ever carries an `Authorization` header.
2088
- - [`tests/src/server/integration.test.ts`](../../tests/src/server/integration.test.ts) —
2089
- the full flow against the fixture: `new` → `pull` → `audit --live` (compile → materialize →
2090
- audit clean → mutate a file → audit drift → repair clean; then a stale mirror synced current);
2091
- a scaffolded package whose deps are all vendored (contract / emitter / markdown / template /
2092
- terminal / console) runs its own gates green by construction, while a dep outside that set
2093
- leaves its mirror a pointer plus a non-blocking Question.
2094
- - [`tests/src/bin/scaffold.test.ts`](../../tests/src/bin/scaffold.test.ts) — the bin's six
2095
- subcommands: `new` (`parseArgs` flag decoding, a non-interactive `--json` compile emitting
2096
- exactly one JSON value, dry-run review + summary table, the interactive preview-then-confirm
2097
- flow driven by a scripted fake terminal — accept AND default-No decline — `--yes`
2098
- pre-answering that confirm, `--apply` writing into a temp directory unasked, and the
2099
- positional package name validated against the SAME shape the interactive prompt enforces
2100
- (`^[a-z][a-z0-9-]*$`) — an invalid positional name exits `2` naming the expected shape, under
2101
- `--json` and without alike (F4); the extras UX was removed from `new` entirely (`--extras` is
2102
- now an unrecognized flag — `parseArgs`'s strict mode rejects it, exit `2`, nothing written);
2103
- U12c FIX 3 (THE VERIFIER SMOKE, the closure regression): `new --apply` followed by a
2104
- hand-added `package.json` `devDependencies` entry, then `audit --target <name>` exits `0`
2105
- CLEAN — an external extra round-trips through `deriveBlueprint` instead of drifting on its
2106
- own generated `package.json`; a dedicated offline case also resolves the package's own
2107
- BUILT `dist/host` vendored `.claude/agents/orkestrel.md` through `hostRoot()` +
2108
- `readHostManifest` + `locateHostSource`, proving `catalogNames` parses real `@orkestrel/*`
2109
- rows off it — the exact primitive chain Q1's interactive catalog validation reads), `pull`
2110
- (report + confirm-then-write, `--apply` writing unasked, `--json` emitting exactly one
2111
- `SyncReport`, `--strict` still THROWING `FETCH` on a network fault, and any drift/failure
2112
- exiting `1` regardless of `--strict` — `pull`'s prior only-nonzero-under-`--strict` posture
2113
- is gone), `audit` (`deriveBlueprint` reconstruction, `--live` drift → exit `1`,
2114
- hydration-aware host drift, the MERGED prune scan — a `pruneTargets`-found foreign file is a
2115
- real finding that counts toward drift and exits `1`, its count carried under `--json`
2116
- (`Audit.foreign`) alongside a `live` field when `--live` ran, `--groups a,b` scoping the
2117
- compiled plan validated against `GROUPS` with exit `2` (a plain USAGE error, not a coded
2118
- failure) on an unrecognized name, an EXPLICIT `--from` that fails to resolve exiting `TARGET`
2119
- rather than silently downgrading — M1, an unscannable `--from` host that DOES resolve but
2120
- cannot establish a vendored allowlist for a prune directory `target` actually has degrading
2121
- the audit to its un-scanned findings with a printed `scanSkipped` note instead of crashing
2122
- (F3), the repair-handoff confirm offered ONLY on a real TTY — `--apply` / `--yes` are NEVER
2123
- handoff consent, so `audit --apply` (with or without `--prune`) on a drifted/foreign target
2124
- is asserted to leave every file exactly as found and exit `1`, never auto-repairing or
2125
- auto-pruning (F1) — and, when offered, ONLY when there is host/template-origin drift OR a
2126
- foreign file present AND `--prune` was passed (a foreign-only handoff without `--prune` would
2127
- be a dead end — F2); a foreign file with no `--prune` instead prints the `foreignHint` pointing
2128
- at `scaffold repair --prune` (never a generated-file note, which stays reserved for
2129
- computed-only drift), and — on an ACCEPTED handoff — a FULL-PLAN re-diff afterward that still
2130
- exits `1` if any drift (e.g. generated-file drift `repair` cannot touch) remains), `repair`
2131
- (`deriveBlueprint` reconstruction,
2132
- dry-run exit code, confirm-then-write (or `--apply`) scoped to HOST-ORIGIN artifacts ONLY
2133
- (H2) — INCLUDING `.github/workflows/ci.yml` (full HOST scope, no exclusion, unlike `fleet`)
2134
- but a hand-modified `src` file is NEVER touched even when the target also carries drifted
2135
- host files — a SECOND default-No confirm (or `--prune`) that LISTS the exact foreign paths
2136
- before deleting `.claude/agents/` / `scripts/` foreigns ONLY, a `0`-count skipping the
2137
- question entirely with a "no unexpected files to delete" note, `--prune` reaching the
2138
- preview/confirm/deletion flow even on a CLEAN host audit — a clean audit alone no longer
2139
- bypasses pruning, only a clean audit WITH nothing to prune does (U11 F2) — `--from` override defaulting
2140
- to `hostRoot()`), a shared non-TTY CASE across every write verb asserting the ONE-PROMPT
2141
- ceiling — a piped/non-TTY run never sees a second question (the prune confirm resolves to its
2142
- safe default with a printed `pruneSkipped` explanation instead of blocking; `audit`'s
2143
- repair-handoff is TTY-only from the start, so a non-TTY `audit` never asks it at all —
2144
- degrading straight to the `foreignHint` / `generatedNote` prose instead), and a missing
2145
- REQUIRED input off a TTY (e.g. `new` with no name/`--surfaces`) exits
2146
- `2` naming the flag rather than hanging, and `fleet` (a FLEET
2147
- fixture: multiple `discoverPackages`-discovered repos under the CURRENT WORKING DIRECTORY's
2148
- immediate children — no `--root` flag anywhere, the cd-model IS the interface — `.github/
2149
- workflows/ci.yml` EXCLUDED from the scoped plan regardless of confirm/`--apply`, per-repo +
2150
- total drift table, `--json` emitting exactly one fleet-drift array, `--apply` writing
2151
- unasked, exit `1` on residual drift) — all against the `node:http` fixture, PLUS a shared
2152
- `ctrl-c`-at-prompt case asserting `cancelled — nothing written` and exit `1` — and `catalog`
2153
- (`--offline` exercises the fully-local path against a fixture fleet under two `--from` roots,
2154
- one guide carrying a blockquote and one missing its guide entirely: dry-run reports the
2155
- no-description list and exits `1` on marker drift, `--apply` writes the spliced table and a
2156
- re-run exits `0`, a target `.claude/agents/orkestrel.md` missing either marker exits coded
2157
- `TARGET`, multiple `--from` values merge into one sorted, deduplicated table, `--json` emitting
2158
- exactly `{ entries, drift, shrink? }` — NEVER a bare `CatalogEntry[]` — and a shrink
2159
- warning prints on both dry-run and `--apply` when the new table has fewer rows than the
2160
- currently-embedded one — the registry-authoritative default path's fetch/merge logic is
2161
- covered at the `Sync.catalog()` unit level, `tests/src/server/Sync.test.ts`, against a
2162
- fixture org-list + packument + guide host). A shared unknown-command case asserts `scaffold
2163
- sync` / `scaffold mirror` print the explicit "renamed" redirect (`pull` / `fleet`
2164
- respectively) rather than a fuzzy guess, any other typo gets the nearest-`KNOWN_VERBS`
2165
- suggestion, and either way exits `2` with a single JSON error envelope (`{ error: { code,
2166
- message } }`, `code: 'USAGE'`) under `--json` — the same envelope shape asserted for every
2167
- other usage/unexpected failure across every verb, so `--json` NEVER emits a bare stderr line.
2168
- - a BUILT-BIN end-to-end pack→install→scaffold-new proof — `npm pack` the built tarball,
2169
- install it into a scratch project as a devDependency, then invoke the installed `scaffold
2170
- new` bin with NO `--from` flag and assert the DEFAULT host resolution (`hostRoot()`) lands on
2171
- the vendored `dist/host` bundle inside the installed package (not the source tree, not
2172
- `process.cwd()`) — proving the published artifact is self-sufficient.
2209
+ green-field writes, scoped repair, prune quarantine and rollback, and every fail-closed preflight.
2210
+ - [`tests/src/server/Sync.test.ts`](../../tests/src/server/Sync.test.ts) freshness verdicts,
2211
+ bounded concurrency, redirect and oversize handling, strict mode, pull, write, and catalog against
2212
+ protocol-faithful fixture servers.
2213
+ - [`tests/src/server/integration.test.ts`](../../tests/src/server/integration.test.ts) the whole
2214
+ compile, materialize, audit, repair round trip against real directories.
2215
+ - [`tests/src/bin/helpers.test.ts`](../../tests/src/bin/helpers.test.ts) the executable's rendered
2216
+ verdicts, tables, notes, and suggestion machinery.
2217
+ - [`tests/src/bin/parsers.test.ts`](../../tests/src/bin/parsers.test.ts) argument parsing, token
2218
+ splitting, and pull-selection resolution against a target's declared dependencies.
2219
+ - [`tests/src/bin/validators.test.ts`](../../tests/src/bin/validators.test.ts) the verb
2220
+ vocabulary.
2221
+ - [`tests/src/bin/errors.test.ts`](../../tests/src/bin/errors.test.ts) the executable's exit
2222
+ signalling.
2223
+ - [`tests/src/bin/scaffold.test.ts`](../../tests/src/bin/scaffold.test.ts) each verb's dry-run,
2224
+ confirm, apply, and JSON paths.
2225
+ - [`tests/src/bin/e2e.test.ts`](../../tests/src/bin/e2e.test.ts) the built executable driven end
2226
+ to end over real directories.
2227
+ - [`tests/guides/src/parity.test.ts`](../../tests/guides/src/parity.test.ts) this guide against
2228
+ the two barrels: every export documented, every documented symbol real, every interface method
2229
+ matched, every documented function exampled, and every link resolvable.
2173
2230
 
2174
2231
  ## See also
2175
2232
 
2176
- - [`contract.md`](contract.md) — the guards, shapers, and `createContract` machinery the
2177
- validators compile from, and `schemaToParameters` / `seededRandom` for the tool boundary.
2178
- - [`emitter.md`](emitter.md) — the typed emitter behind the compiler's, manager's,
2179
- materializer's, and sync's observation surfaces.
2180
- - [`markdown.md`](markdown.md) — the AST + `renderMarkdown` writer `alignTable` builds the
2181
- guide Surface tables on (`parseInline`, `TableNode`, `TableAlign`).
2182
- - [`template.md`](template.md) — the `TemplateDefinition` + pure `fillTemplate` engine (`missing:
2183
- 'error'`) that carries the rendered defaults.
2184
- - [`terminal.md`](terminal.md) the `createTerminal` `PromptFormInterface` the bin drives for
2185
- interactive blueprint building (with a non-TTY readline fallback).
2186
- - [`console.md`](console.md) — the `createReporter` / `createSpinner` + server `createServerSink`
2187
- the bin narrates the plan, sync, and materialization through.
2188
- - [`AGENTS.md`](../../AGENTS.md) — the rules; §4 naming, §9 managers, §11 determinism, §12
2189
- errors, §13 emitters, §14 totality, §21 mechanism-never-policy, §22 documentation-as-contracts.
2190
- - [`README.md`](../README.md) — the package index.
2233
+ - [`AGENTS.md`](../../AGENTS.md) — the coding contract every generated workspace inherits.
2234
+ - [`README.md`](../README.md) the guides index.
2235
+ - [`contract.md`](contract.md) — the shape, guard, parser, and outcome primitives the blueprint and
2236
+ plan contracts compile through.
2237
+ - [`emitter.md`](emitter.md) — the observation channel every entity here composes.
2238
+ - [`markdown.md`](markdown.md) the AST and renderer behind the table and blockquote work.
2239
+ - [`template.md`](template.md) — the pure fill engine behind every template-origin artifact.
2240
+ - [`terminal.md`](terminal.md) and [`console.md`](console.md) the prompt and reporter toolkits
2241
+ consumed only at the executable boundary.
2242
+ - [`guide.md`](guide.md) the guides-parity toolkit this guide is checked with.