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