@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.
- package/README.md +29 -15
- package/dist/bin/scaffold.js +1162 -1200
- package/dist/bin/scaffold.js.map +1 -1
- package/dist/host/AGENTS.md +124 -925
- package/dist/host/CLAUDE.md +260 -495
- package/dist/host/agents/skills/orkestrel-align-packages/SKILL.md +53 -0
- package/dist/host/agents/skills/orkestrel-align-packages/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-align-packages/references/fleet.md +50 -0
- package/dist/host/agents/skills/orkestrel-align-packages/references/integration.md +56 -0
- package/dist/host/agents/skills/orkestrel-build-application/SKILL.md +64 -0
- package/dist/host/agents/skills/orkestrel-build-application/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-build-application/references/application.md +129 -0
- package/dist/host/agents/skills/orkestrel-harden-package/SKILL.md +64 -0
- package/dist/host/agents/skills/orkestrel-harden-package/agents/openai.yaml +4 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/centralization.md +85 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/contract.md +57 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/hardening.md +93 -0
- package/dist/host/agents/skills/orkestrel-harden-package/references/research.md +61 -0
- package/dist/host/claude/agents/application.md +32 -0
- package/dist/host/claude/agents/builder.md +12 -7
- package/dist/host/claude/agents/checker.md +15 -8
- package/dist/host/claude/agents/codex.md +40 -0
- package/dist/host/claude/agents/grok.md +20 -37
- package/dist/host/claude/agents/orkestrel.md +89 -223
- package/dist/host/claude/agents/planner.md +14 -34
- package/dist/host/claude/agents/reviewer.md +39 -24
- package/dist/host/claude/agents/verifier.md +15 -10
- package/dist/host/claude/rules/application.md +57 -0
- package/dist/host/claude/rules/architecture.md +179 -0
- package/dist/host/claude/rules/browser.md +28 -0
- package/dist/host/claude/rules/documentation.md +64 -0
- package/dist/host/claude/rules/names.md +209 -0
- package/dist/host/claude/rules/patterns.md +130 -0
- package/dist/host/claude/rules/quality.md +45 -0
- package/dist/host/claude/rules/styles.md +64 -0
- package/dist/host/claude/rules/tests.md +123 -0
- package/dist/host/claude/rules/typescript.md +78 -0
- package/dist/host/claude/rules/workspace.md +180 -0
- package/dist/host/claude/settings.json +116 -0
- package/dist/host/claude/skills/orkestrel-align-packages/SKILL.md +10 -0
- package/dist/host/claude/skills/orkestrel-build-application/SKILL.md +12 -0
- package/dist/host/claude/skills/orkestrel-harden-package/SKILL.md +10 -0
- package/dist/host/codex/agents/analyst.toml +15 -0
- package/dist/host/codex/agents/application.toml +25 -0
- package/dist/host/codex/agents/builder.toml +24 -0
- package/dist/host/codex/agents/checker.toml +15 -0
- package/dist/host/codex/agents/grok.toml +17 -0
- package/dist/host/codex/agents/implementer.toml +17 -0
- package/dist/host/codex/agents/orkestrel.toml +16 -0
- package/dist/host/codex/agents/planner.toml +20 -0
- package/dist/host/codex/agents/reviewer.toml +18 -0
- package/dist/host/codex/agents/verifier.toml +14 -0
- package/dist/host/codex/config.toml +64 -0
- package/dist/host/dotfiles/oxlintrc.json +307 -1
- package/dist/host/guides/src/scaffold.md +2059 -2007
- package/dist/host/manifest.json +355 -137
- package/dist/host/scripts/codex.sh +49 -0
- package/dist/host/scripts/cursor.sh +33 -47
- package/dist/host/scripts/deps.sh +34 -16
- package/dist/host/scripts/ollama.sh +6 -149
- package/dist/host/tests/setupPolicy.ts +354 -0
- package/dist/src/core/index.cjs +6600 -1199
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +801 -266
- package/dist/src/core/index.d.ts +801 -266
- package/dist/src/core/index.js +6518 -1191
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +3132 -593
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +643 -95
- package/dist/src/server/index.d.ts +643 -95
- package/dist/src/server/index.js +3054 -594
- package/dist/src/server/index.js.map +1 -1
- package/package.json +15 -12
- package/dist/host/claude/agents/composer.md +0 -64
- package/dist/host/claude/agents/researcher.md +0 -38
- package/dist/host/claude/agents/scout.md +0 -35
- package/dist/host/github/workflows/ci.yml +0 -64
|
@@ -1,79 +1,88 @@
|
|
|
1
1
|
# Scaffold
|
|
2
2
|
|
|
3
|
-
> A
|
|
4
|
-
> a
|
|
5
|
-
>
|
|
6
|
-
>
|
|
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
|
-
>
|
|
12
|
-
>
|
|
13
|
-
>
|
|
14
|
-
>
|
|
15
|
-
>
|
|
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
|
-
>
|
|
18
|
-
>
|
|
19
|
-
>
|
|
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
|
-
>
|
|
22
|
-
>
|
|
23
|
-
>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
and the
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
76
|
-
|
|
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
|
|
86
|
-
keywords: ['router', 'hash'
|
|
87
|
-
|
|
88
|
-
dependencies: [dependency('@orkestrel/contract', '^0.0.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
`
|
|
270
|
-
`
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
|
275
|
-
|
|
|
276
|
-
| `
|
|
277
|
-
| `
|
|
278
|
-
| `
|
|
279
|
-
| `
|
|
280
|
-
| `
|
|
281
|
-
| `
|
|
282
|
-
| `
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
1081
|
+
## Methods
|
|
373
1082
|
|
|
374
|
-
|
|
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
|
-
|
|
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
|
-
|
|
385
|
-
|
|
1087
|
+
| Method | Returns |
|
|
1088
|
+
| --------- | ------------- |
|
|
1089
|
+
| `compile` | `Scaffolding` |
|
|
1090
|
+
| `audit` | `Audit` |
|
|
1091
|
+
| `destroy` | `void` |
|
|
386
1092
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
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
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
1587
|
+
blueprint,
|
|
509
1588
|
blueprintToMembers,
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
567
|
-
|
|
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
|
-
|
|
1645
|
+
const scaffolding = compiler.compile(spec)
|
|
1646
|
+
scaffolding.stages.map((record) => record.stage) // ['draft', 'gate', 'pin']
|
|
587
1647
|
|
|
588
|
-
|
|
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
|
-
|
|
597
|
-
|
|
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
|
-
###
|
|
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 {
|
|
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
|
-
|
|
681
|
-
|
|
1670
|
+
const plans = createPlanManager()
|
|
1671
|
+
const record = plans.add(blueprintToPlan(blueprint('router', { src: ['core'] })))
|
|
682
1672
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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
|
-
|
|
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
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
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
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
```
|
|
1701
|
+
declare const plan: Plan
|
|
1702
|
+
declare const audit: Audit
|
|
1703
|
+
declare const report: SyncReport
|
|
718
1704
|
|
|
719
|
-
|
|
720
|
-
|
|
1705
|
+
alignTable(['API', 'Kind'], [['`createRouter`', 'function']])
|
|
1706
|
+
splitTableRow('| a | b |') // ['a', 'b']
|
|
1707
|
+
padCell('ab', 5) // 'ab '
|
|
1708
|
+
delimiterCell('left', 5) // ':----'
|
|
721
1709
|
|
|
722
|
-
|
|
723
|
-
|
|
1710
|
+
catalogToBlock([{ name: '@orkestrel/router', version: '0.0.5', description: '' }])
|
|
1711
|
+
catalogNames('| @orkestrel/router | 0.0.5 |') // ['@orkestrel/router']
|
|
724
1712
|
|
|
725
|
-
//
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
1741
|
+
contentCodePoint('a', 0) // 97
|
|
1742
|
+
contentByteLength('ab') // 2
|
|
1743
|
+
bytesToHex(contentToBytes('ab')) === contentToHex('ab') // true
|
|
763
1744
|
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
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 {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
-
|
|
829
|
-
|
|
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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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">') // '<app & "team">'
|
|
1777
|
+
serializeTypeScriptString("app's") // "'app\\'s'"
|
|
1778
|
+
const sorted = ['b', 'a'].sort(compareCodeUnit) // ['a', 'b']
|
|
840
1779
|
```
|
|
841
1780
|
|
|
842
|
-
|
|
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 {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
)
|
|
887
|
-
|
|
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
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
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
|
-
|
|
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 {
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
|
1880
|
+
const spec = blueprint('router', { src: ['core'], app: ['core', 'server'] })
|
|
1881
|
+
const members = blueprintToMembers(spec)
|
|
936
1882
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
-
|
|
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 {
|
|
1098
|
-
|
|
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
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
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
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
-
###
|
|
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 {
|
|
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
|
-
|
|
1130
|
-
|
|
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
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
-
###
|
|
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,
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
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
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
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
|
-
|
|
1999
|
+
readManifest('./packages/router')
|
|
1171
2000
|
const current = readTarget(
|
|
1172
|
-
'./packages/
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
2016
|
+
const sync = createSync({ concurrency: 4, retries: 1 })
|
|
1228
2017
|
|
|
1229
|
-
|
|
1230
|
-
|
|
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
|
-
|
|
1237
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
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 {
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
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
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
2052
|
+
isRealDirectory('./packages/router')
|
|
2053
|
+
listFiles('./packages/router/.claude/agents')
|
|
2054
|
+
listDirectories('./packages/router/.claude')
|
|
1284
2055
|
|
|
1285
|
-
|
|
1286
|
-
|
|
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
|
-
|
|
1291
|
-
|
|
2059
|
+
guideToDescription('> A tiny hash router.\n>\n> More detail.') // 'A tiny hash router.'
|
|
2060
|
+
catalogPackages(['./packages'], 4_096)
|
|
1292
2061
|
```
|
|
1293
2062
|
|
|
1294
|
-
###
|
|
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 {
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
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
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
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
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
2091
|
+
digestText(readFileText('./packages/router', 'package.json', 'TARGET', 'target')) ===
|
|
2092
|
+
digestFile(full)
|
|
2093
|
+
digestHex(readFileHex('./packages/router', 'package.json', 'TARGET', 'target'))
|
|
1343
2094
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
-
|
|
1355
|
-
} catch
|
|
1356
|
-
|
|
2102
|
+
commitWriteTransaction(transaction, ['package.json'])
|
|
2103
|
+
} catch {
|
|
2104
|
+
restoreFiles(transaction, ['package.json'])
|
|
2105
|
+
discardWriteTransaction(transaction)
|
|
1357
2106
|
}
|
|
1358
|
-
|
|
2107
|
+
|
|
2108
|
+
replaceDirectory('./staged', './target', './backup')
|
|
2109
|
+
resolveGuideWrites([], './packages/router') // preflighted destinations, before any write
|
|
1359
2110
|
```
|
|
1360
2111
|
|
|
1361
|
-
###
|
|
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
|
-
|
|
1534
|
-
import { parseArgs } from 'node:util'
|
|
2115
|
+
import { hasOnlyDataProperties, isDenseDataArray, isEmitterErrorHandler } from '@orkestrel/scaffold'
|
|
1535
2116
|
import {
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
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
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
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
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
- [`tests/
|
|
1976
|
-
|
|
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,
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
- [`tests/src/core/
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
- [`tests/src/
|
|
1988
|
-
|
|
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
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
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
|
-
- [`
|
|
2177
|
-
|
|
2178
|
-
- [`
|
|
2179
|
-
|
|
2180
|
-
- [`
|
|
2181
|
-
|
|
2182
|
-
- [`template.md`](template.md) — the
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
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.
|