@orkestrel/scaffold 0.0.45 → 0.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/bin/main.js +179 -44
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/AGENTS.md +1 -0
  4. package/dist/host/agents/orchestration.md +3 -1
  5. package/dist/host/claude/agents/codex.md +4 -4
  6. package/dist/host/claude/agents/orkestrel.md +6 -5
  7. package/dist/host/claude/rules/documentation.md +2 -1
  8. package/dist/host/claude/rules/quality.md +3 -0
  9. package/dist/host/claude/rules/tests.md +19 -0
  10. package/dist/host/claude/rules/workspace.md +28 -21
  11. package/dist/host/claude/settings.json +781 -2
  12. package/dist/host/codex/config.toml +4 -0
  13. package/dist/host/configs/policy.ts +167 -10
  14. package/dist/host/cursor/mcp.json +4 -0
  15. package/dist/host/dotfiles/mcp.json +4 -0
  16. package/dist/host/dotfiles/oxlintrc.json +9 -0
  17. package/dist/host/guides/scaffold.md +143 -44
  18. package/dist/host/scripts/codex.sh +0 -0
  19. package/dist/host/scripts/cursor.sh +0 -0
  20. package/dist/host/scripts/deps.sh +0 -0
  21. package/dist/host/scripts/ollama.sh +0 -0
  22. package/dist/host/tests/config.test.ts +126 -11
  23. package/dist/host/tests/policy.test.ts +7 -0
  24. package/dist/host/tests/setupPolicy.ts +222 -8
  25. package/dist/src/core/index.cjs +420 -23
  26. package/dist/src/core/index.cjs.map +1 -1
  27. package/dist/src/core/index.d.cts +102 -9
  28. package/dist/src/core/index.d.ts +102 -9
  29. package/dist/src/core/index.js +416 -24
  30. package/dist/src/core/index.js.map +1 -1
  31. package/dist/src/server/index.cjs +41 -49
  32. package/dist/src/server/index.cjs.map +1 -1
  33. package/dist/src/server/index.d.cts +9 -8
  34. package/dist/src/server/index.d.ts +9 -8
  35. package/dist/src/server/index.js +42 -50
  36. package/dist/src/server/index.js.map +1 -1
  37. package/package.json +8 -5
@@ -37,3 +37,7 @@ contract owns when each is used; this is only the mapping:
37
37
  enabled = true
38
38
  max_concurrent_threads_per_session = 3
39
39
  interrupt_message = true
40
+
41
+ [mcp_servers.probe]
42
+ command = "node"
43
+ args = ["node_modules/@orkestrel/probe/dist/bin/main.js"]
@@ -1,13 +1,25 @@
1
- /** The expression fields inspected by the policy rules. */
2
- export interface PolicyExpression {
1
+ /** The syntax-node fields supplied to every policy visitor. */
2
+ export interface PolicyNode {
3
3
  readonly type: string
4
4
  readonly range: [number, number]
5
+ }
6
+
7
+ /** The expression fields inspected by the policy rules. */
8
+ export interface PolicyExpression extends PolicyNode {
9
+ readonly parent?: PolicyExpression | null
5
10
  readonly name?: unknown
6
11
  readonly value?: unknown
12
+ readonly key?: PolicyExpression
13
+ readonly id?: unknown
14
+ readonly method?: boolean
15
+ readonly expression?: boolean
7
16
  readonly object?: PolicyExpression
8
17
  readonly property?: PolicyExpression
9
18
  readonly computed?: boolean
10
19
  readonly callee?: PolicyExpression
20
+ readonly argument?: PolicyExpression | null
21
+ readonly arguments?: readonly PolicyExpression[]
22
+ readonly body?: PolicyExpression
11
23
  readonly quasis?: readonly PolicyExpression[]
12
24
  readonly expressions?: readonly PolicyExpression[]
13
25
  readonly accessibility?: 'private' | 'protected' | 'public' | null
@@ -40,14 +52,17 @@ export interface PolicyMeta {
40
52
 
41
53
  /** The Oxlint visitor entries used by the policy rules. */
42
54
  export interface PolicyVisitor {
43
- readonly [key: string]: ((node: PolicyExpression) => void) | undefined
44
- readonly CallExpression?: (node: PolicyExpression) => void
45
- readonly MethodDefinition?: (node: PolicyExpression) => void
46
- readonly PropertyDefinition?: (node: PolicyExpression) => void
47
- readonly AccessorProperty?: (node: PolicyExpression) => void
48
- readonly TSAbstractMethodDefinition?: (node: PolicyExpression) => void
49
- readonly TSAbstractPropertyDefinition?: (node: PolicyExpression) => void
50
- readonly TSAbstractAccessorProperty?: (node: PolicyExpression) => void
55
+ readonly [key: string]: ((node: PolicyNode) => void) | undefined
56
+ readonly CallExpression?: (node: PolicyNode) => void
57
+ readonly FunctionDeclaration?: (node: PolicyNode) => void
58
+ readonly FunctionExpression?: (node: PolicyNode) => void
59
+ readonly ArrowFunctionExpression?: (node: PolicyNode) => void
60
+ readonly MethodDefinition?: (node: PolicyNode) => void
61
+ readonly PropertyDefinition?: (node: PolicyNode) => void
62
+ readonly AccessorProperty?: (node: PolicyNode) => void
63
+ readonly TSAbstractMethodDefinition?: (node: PolicyNode) => void
64
+ readonly TSAbstractPropertyDefinition?: (node: PolicyNode) => void
65
+ readonly TSAbstractAccessorProperty?: (node: PolicyNode) => void
51
66
  }
52
67
 
53
68
  /** The complete behavior exposed by one policy rule. */
@@ -56,6 +71,126 @@ export interface PolicyRuleInterface {
56
71
  create(context: PolicyContext): PolicyVisitor
57
72
  }
58
73
 
74
+ /** Whether a policy expression is runtime function syntax. */
75
+ export function isPolicyFunction(node: PolicyExpression): boolean {
76
+ return (
77
+ node.type === 'FunctionDeclaration' ||
78
+ node.type === 'FunctionExpression' ||
79
+ node.type === 'ArrowFunctionExpression'
80
+ )
81
+ }
82
+
83
+ /** Whether a policy function is anonymous. */
84
+ export function isPolicyAnonymous(node: PolicyExpression): boolean {
85
+ return node.type === 'ArrowFunctionExpression' || node.id === null
86
+ }
87
+
88
+ /** Return the outermost parenthesized expression holding a policy function. */
89
+ export function functionToPolicyPosition(node: PolicyExpression): PolicyExpression {
90
+ let position = node
91
+ while (position.parent?.type === 'ParenthesizedExpression') {
92
+ position = position.parent
93
+ }
94
+ return position
95
+ }
96
+
97
+ /** Whether a policy function is an anonymous callback passed directly as an argument. */
98
+ export function isPolicyCallback(node: PolicyExpression): boolean {
99
+ if (!isPolicyAnonymous(node)) return false
100
+ const position = functionToPolicyPosition(node)
101
+ const parent = position.parent
102
+ return (
103
+ (parent?.type === 'CallExpression' || parent?.type === 'NewExpression') &&
104
+ parent.arguments?.includes(position) === true
105
+ )
106
+ }
107
+
108
+ /** Whether a policy function is an anonymous function returned directly as a result. */
109
+ export function isPolicyResult(node: PolicyExpression): boolean {
110
+ if (!isPolicyAnonymous(node)) return false
111
+ const position = functionToPolicyPosition(node)
112
+ const parent = position.parent
113
+ return (
114
+ (parent?.type === 'ReturnStatement' && parent.argument === position) ||
115
+ (parent?.type === 'ArrowFunctionExpression' && parent.body === position)
116
+ )
117
+ }
118
+
119
+ /** Whether an Oxlint function expression represents method syntax. */
120
+ export function isPolicyMethod(node: PolicyExpression): boolean {
121
+ const parent = node.parent
122
+ return (
123
+ node.type === 'FunctionExpression' &&
124
+ parent?.value === node &&
125
+ (parent.type === 'MethodDefinition' || (parent.type === 'Property' && parent.method === true))
126
+ )
127
+ }
128
+
129
+ /** Whether a policy function sits inside another function before any class-expression boundary. */
130
+ export function hasPolicyFunctionAncestor(node: PolicyExpression): boolean {
131
+ let parent = node.parent
132
+ let method = false
133
+ while (parent !== undefined && parent !== null) {
134
+ if (parent.type === 'ClassExpression') return false
135
+ if (parent.type === 'ClassDeclaration' && method) return true
136
+ if (isPolicyFunction(parent)) {
137
+ if (!isPolicyMethod(parent)) return true
138
+ method = true
139
+ }
140
+ parent = parent.parent
141
+ }
142
+ return method
143
+ }
144
+
145
+ /** Whether an arrow is the policy plugin's sanctioned visitor-table delegation. */
146
+ export function isPolicyVisitor(node: PolicyExpression): boolean {
147
+ if (
148
+ node.type !== 'ArrowFunctionExpression' ||
149
+ node.expression !== true ||
150
+ node.body?.type !== 'CallExpression' ||
151
+ node.body.callee?.type !== 'Identifier' ||
152
+ typeof node.body.callee.name !== 'string' ||
153
+ !node.body.callee.name.startsWith('report')
154
+ ) {
155
+ return false
156
+ }
157
+ const property = node.parent
158
+ const object = property?.parent
159
+ const returned = object?.parent
160
+ const block = returned?.parent
161
+ const create = block?.parent
162
+ const definition = create?.parent
163
+ return (
164
+ property?.type === 'Property' &&
165
+ property.method === false &&
166
+ property.value === node &&
167
+ object?.type === 'ObjectExpression' &&
168
+ returned?.type === 'ReturnStatement' &&
169
+ returned.argument === object &&
170
+ block?.type === 'BlockStatement' &&
171
+ create?.type === 'FunctionExpression' &&
172
+ definition?.type === 'Property' &&
173
+ definition.method === true &&
174
+ definition.value === create &&
175
+ definition.key?.type === 'Identifier' &&
176
+ definition.key.name === 'create'
177
+ )
178
+ }
179
+
180
+ /** Report function syntax nested inside another function body. */
181
+ export function reportNested(context: PolicyContext, node: PolicyExpression): void {
182
+ if (
183
+ !hasPolicyFunctionAncestor(node) ||
184
+ isPolicyMethod(node) ||
185
+ isPolicyCallback(node) ||
186
+ isPolicyResult(node) ||
187
+ isPolicyVisitor(node)
188
+ ) {
189
+ return
190
+ }
191
+ context.report({ node, messageId: 'nested' })
192
+ }
193
+
59
194
  /** Report banned calls on the named Vitest and Jest framework objects. */
60
195
  export function reportMocking(context: PolicyContext, node: PolicyExpression): void {
61
196
  const callee = node.callee
@@ -128,6 +263,27 @@ export function reportPrivacy(context: PolicyContext, node: PolicyExpression): v
128
263
  }
129
264
  }
130
265
 
266
+ /** Ban function declarations and assignments inside another function body. */
267
+ export const NESTED_RULE: PolicyRuleInterface = {
268
+ meta: {
269
+ type: 'problem',
270
+ docs: {
271
+ description: 'Disallow function declarations and assignments inside another function body.',
272
+ },
273
+ messages: {
274
+ nested:
275
+ 'Extract the function to module scope or make instance-bound work a method; only direct anonymous callbacks and returned anonymous functions may stay in a function body.',
276
+ },
277
+ },
278
+ create(context) {
279
+ return {
280
+ FunctionDeclaration: (node) => reportNested(context, node),
281
+ FunctionExpression: (node) => reportNested(context, node),
282
+ ArrowFunctionExpression: (node) => reportNested(context, node),
283
+ }
284
+ },
285
+ }
286
+
131
287
  /** Ban framework mocking, spying, fake clocks, and global or environment stubs. */
132
288
  export const MOCKING_RULE: PolicyRuleInterface = {
133
289
  meta: {
@@ -181,5 +337,6 @@ export default {
181
337
  rules: {
182
338
  'no-mocking': MOCKING_RULE,
183
339
  'no-keyword-privacy': PRIVACY_RULE,
340
+ 'no-nested-functions': NESTED_RULE,
184
341
  },
185
342
  }
@@ -7,6 +7,10 @@
7
7
  "claude": {
8
8
  "command": "claude",
9
9
  "args": ["mcp", "serve"]
10
+ },
11
+ "probe": {
12
+ "command": "node",
13
+ "args": ["node_modules/@orkestrel/probe/dist/bin/main.js"]
10
14
  }
11
15
  }
12
16
  }
@@ -3,6 +3,10 @@
3
3
  "codex": {
4
4
  "command": "codex",
5
5
  "args": ["mcp-server"]
6
+ },
7
+ "probe": {
8
+ "command": "node",
9
+ "args": ["node_modules/@orkestrel/probe/dist/bin/main.js"]
6
10
  }
7
11
  }
8
12
  }
@@ -79,6 +79,15 @@
79
79
  "import/no-default-export": "off"
80
80
  }
81
81
  },
82
+ {
83
+ "files": [
84
+ "src/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}",
85
+ "app/**/*.{cjs,cts,js,jsx,mjs,mts,ts,tsx,vue}"
86
+ ],
87
+ "rules": {
88
+ "policy/no-nested-functions": "error"
89
+ }
90
+ },
82
91
  {
83
92
  "files": ["*.vue"],
84
93
  "rules": {
@@ -52,7 +52,7 @@ Exported from `@orkestrel/scaffold`, and reachable from
52
52
  | `Mirror` | type | One dependency guide fetched from upstream, beside the local mirror it answers for. |
53
53
  | `Origin` | type | How an artifact's content is produced. |
54
54
  | `Ownership` | type | What scaffold claims at an artifact's path. |
55
- | `Release` | type | One declared dependency range measured against the registry's latest release. |
55
+ | `Release` | type | One declared dependency range measured against a registry release. |
56
56
  | `ScaffoldErrorCode` | type | The coded reasons a scaffold error is raised. |
57
57
  | `Snapshot` | type | Exact lowercase hexadecimal target bytes keyed by artifact-relative path. |
58
58
 
@@ -123,7 +123,9 @@ Exported from `@orkestrel/scaffold`, and reachable from
123
123
  | `MAX_NAME_LENGTH` | const | Maximum bare workspace name length. |
124
124
  | `MAX_PATH_LENGTH` | const | Maximum length of one path, matching the longest a supported filesystem accepts. |
125
125
  | `MAX_RANGE_LENGTH` | const | Maximum length of one declared package range. |
126
+ | `MAX_REGISTRY_BYTES` | const | Maximum decoded bytes accepted from one registry response. |
126
127
  | `MAX_TOTAL_ARTIFACT_BYTES` | const | Maximum bytes retained across one whole plan or audit. |
128
+ | `MAX_TOTAL_REGISTRY_BYTES` | const | Maximum decoded bytes accepted across one registry-reading call. |
127
129
  | `MINIMUM_NODE_VERSION` | const | The oldest Node version the generated toolchain supports. |
128
130
  | `NAME_PATTERN` | const | The bare workspace name syntax: lowercase alphanumeric with hyphens, letter first. |
129
131
  | `ORCHESTRATION_PATH_NAMES` | const | The exact root filenames that wire an agent bench rather than the toolchain, frozen. |
@@ -189,6 +191,7 @@ Exported from `@orkestrel/scaffold`, and reachable from
189
191
  | `computeBytes` | function | Count the UTF-8 bytes text encodes to. |
190
192
  | `computeHash` | function | Compute the deterministic content identity of text. |
191
193
  | `contentToHex` | function | Encode text as the exact lowercase hexadecimal form of its UTF-8 bytes. |
194
+ | `extractRangeMajor` | function | Extract the major component of an admitted dependency range. |
192
195
  | `extractVersion` | function | Extract the major, minor, and patch components of an exact version. |
193
196
  | `inferDrift` | function | Infer how one target path compares to the artifact planned for it. |
194
197
  | `inferGroup` | function | Infer the `Group` a path belongs to. |
@@ -232,6 +235,8 @@ Exported from `@orkestrel/scaffold`, and reachable from
232
235
  | `pathToCondition` | function | Build one `exports` condition block for a built environment. |
233
236
  | `planToFindings` | function | Compare a plan against a target's current content. |
234
237
  | `planToHash` | function | Compute a plan's content identity. |
238
+ | `replaceManifestRanges` | function | Replace declared dependency ranges in package manifest text. |
239
+ | `replacePlanRanges` | function | Replace dependency ranges in a plan's manifest and recompute its identity. |
235
240
  | `srcToEntry` | function | Project a published selection into the manifest's entry fields. |
236
241
  | `srcToExports` | function | Project a published selection into the manifest's `exports` map. |
237
242
  | `srcToRoot` | function | Select the single published environment a package root points at. |
@@ -392,7 +397,7 @@ no interface and is documented directly.
392
397
 
393
398
  | Method | Summary |
394
399
  | --------- | -------------------------------------------------------------------------- |
395
- | `lookup` | Look up the registry's latest release for each declared dependency. |
400
+ | `lookup` | Look up the newest release each declared range admits. |
396
401
  | `fetch` | Fetch each named package's guide, beside the local mirror it answers for. |
397
402
  | `catalog` | Catalog the published fleet from the registry's organization package list. |
398
403
  | `destroy` | Tear the reader down, aborting every request in flight. |
@@ -413,13 +418,13 @@ no interface and is documented directly.
413
418
  Authority is the verb's: every verb except `audit` writes when it is typed, and no
414
419
  option grants a write.
415
420
 
416
- | Verb | Writes |
417
- | ----------- | --------------------------------------------------------------------------------- |
418
- | `new` | A whole workspace, into a target that holds nothing the plan would collide with |
419
- | `audit` | Nothing |
420
- | `repair` | Each planned path the target is missing or has let drift |
421
- | `catalog` | The package table and the guide mirrors |
422
- | `overwrite` | Everything `repair` and `catalog` write, plus deletions and the dependency ranges |
421
+ | Verb | Writes |
422
+ | ----------- | ------------------------------------------------------------------------------- |
423
+ | `new` | A whole workspace, into a target that holds nothing the plan would collide with |
424
+ | `audit` | Nothing |
425
+ | `repair` | Each planned path the target is missing or has let drift, and the ranges |
426
+ | `catalog` | The package table, the guide mirrors, and the ranges |
427
+ | `overwrite` | Everything `repair` and `catalog` write, plus deletions |
423
428
 
424
429
  `scaffold --help` prints the whole reference:
425
430
 
@@ -448,6 +453,7 @@ options
448
453
  --from <path> read the data root from a local path instead of the bundled one; catalog alone accepts it more than once
449
454
  --target <path> the directory the verb operates on; the working directory when absent
450
455
  --json emit one machine-readable value instead of a report
456
+ ORKESTREL_SCAFFOLD_REGISTRY the registry base mapped to upstream.registry.base
451
457
 
452
458
  exit codes
453
459
  0 clean
@@ -458,9 +464,13 @@ exit codes
458
464
  An option a verb does not list is refused by name rather than parsed and ignored. `--help` is the
459
465
  one exception, because it replaces the run rather than modifying it: a command line carrying
460
466
  `--help` anywhere prints the whole reference and exits `0` before the line is read as a command, so
461
- no verb has to list it. `--deps` reaches the registry, so `new` fails when the registry names no
462
- release for a package it was given: the workspace would otherwise declare a dependency that does not
463
- resolve.
467
+ no verb has to list it. Every verb reaches the registry, and none of them invents a range when the
468
+ read produces no answer. Dependency floors states what each verb reads and what it does then.
469
+
470
+ Every read addresses the published registry. Set `ORKESTREL_SCAFFOLD_REGISTRY` to address a loopback
471
+ or private one instead. The process entry maps it to `upstream.registry.base` and nothing else, so
472
+ the seam changes which host answers a read and grants no verb any write authority it did not already
473
+ have.
464
474
 
465
475
  `new --bin` creates the executable entry, its test, and its scoped Vite and TypeScript wrappers. The
466
476
  other structural facts do not need creation flags. Add a root `tests/setup*.test.ts` proof for
@@ -529,8 +539,9 @@ verb would restore. An advisory alone does not make an aligned target drift.
529
539
  The same plan-reading verbs compare the tooling set the derived blueprint plans against
530
540
  `dependencies` and `devDependencies` together. A missing planned package produces one non-blocking
531
541
  `dependencies` question naming every missing package and the exact manifest lines to add, in stable
532
- order. The comparison measures membership only: range differences and workspace-owned extras are
533
- outside it, and a planned tool may live in either section. A present section that is not an object
542
+ order. The comparison measures membership: a workspace-owned extra is outside it, a planned tool may
543
+ live in either section, and how current a declared range is belongs to the registry evidence
544
+ Dependency floors describes rather than to this question. A present section that is not an object
534
545
  produces a question instead of a crash. `audit` reports the question without changing its exit
535
546
  semantics. `repair` and `overwrite` refuse before writing configuration, and no verb edits the
536
547
  birth-owned `package.json`.
@@ -554,13 +565,13 @@ unless `--dirty` waives that refusal. A target that is not a git repository is r
554
565
  `--json` replaces the report with one JSON value on standard output. Warnings and refusals go to
555
566
  standard error, so a piped value is never polluted.
556
567
 
557
- | Verb | Value |
558
- | ----------- | -------------------------------------------------------------------------- |
559
- | `new` | `MaterializeResult` — `target`, `written`, `skipped`, `removed` |
560
- | `audit` | `Audit` — `findings` and `questions`; planned findings carry `ownership` |
561
- | `repair` | `MaterializeResult` plus `audit`, the terminal audit taken after the write |
562
- | `catalog` | `MaterializeResult` plus `entries`, `mirrors`, and `dropped` |
563
- | `overwrite` | The `catalog` value plus `audit`, `releases`, and `note` on a partial run |
568
+ | Verb | Value |
569
+ | ----------- | ------------------------------------------------------------------------------------------ |
570
+ | `new` | `MaterializeResult` — `target`, `written`, `skipped`, `removed` |
571
+ | `audit` | `Audit` — `findings` and `questions` — plus `releases`; findings carry `ownership` |
572
+ | `repair` | `MaterializeResult` plus `audit`, the terminal audit taken after the write, and `releases` |
573
+ | `catalog` | `MaterializeResult` plus `entries`, `mirrors`, `dropped`, and `releases` |
574
+ | `overwrite` | The `catalog` value plus `audit` and `note` on a partial run |
564
575
 
565
576
  Every failure reports the same envelope instead: `{ "error": { "code": …, "message": … } }`. The
566
577
  code is a `ScaffoldErrorCode`, or `USAGE` for a command line that never became a command, or
@@ -597,6 +608,25 @@ valid npm name. A peer reaches the generated workspace through separate represen
597
608
  every name in that binding, so a peer the workspace declares by hand, such as `vitest`, is left as
598
609
  an import in the emitted bundle rather than inlined into it.
599
610
 
611
+ A range is admitted by the shape and refused by the gate, and a `file:` specifier is where a
612
+ consumer meets that split. `isDependency` reads `range` as a non-empty string bounded at
613
+ `MAX_RANGE_LENGTH` and nothing more, because which ranges a blueprint may declare is a gate law
614
+ that reports its accepted candidates rather than a bare `false`. So a blueprint naming
615
+ `file:vendor/orkestrel-form-0.0.1.tgz` is a valid `Dependency` and reaches the gate.
616
+ `dependenciesToQuestions` then tests every declared range against the pattern its own field
617
+ accepts — `ORKESTREL_RANGE_PATTERN` for a runtime dependency and a fleet peer,
618
+ `FLOOR_RANGE_PATTERN` for a foreign peer, `EXTRA_RANGE_PATTERN` for a development extra — and none
619
+ of them admits a `file:` specifier. The question is blocking, so `audit` reports it and compares no
620
+ path:
621
+
622
+ ```text
623
+ dependencies: @orkestrel/form declares the range file:vendor/orkestrel-form-0.0.1.tgz, which dependencies does not accept.
624
+ Audit did not compare the target because the blueprint was refused.
625
+ ```
626
+
627
+ A workspace pinned to a committed tarball therefore has no drift detection until it re-pins to a
628
+ registry range. Read that audit as unavailable, not as clean.
629
+
600
630
  One published environment owns the package root directly. Several published environments require
601
631
  `core`, which owns that root while each other environment keeps its subpath. A multi-environment
602
632
  `src` selection without `core` therefore emits entry fields naming a `core` build the workspace
@@ -807,15 +837,10 @@ removes a foreign file each fail when the destination no longer matches what the
807
837
  The requirement sits in the type rather than in prose, because a deletion that cannot bind to what
808
838
  the audit showed is the one thing the destructive verb must never do.
809
839
 
810
- The shape a `Finding` admits is wider than the set an audit produces. Which combinations of
811
- `ownership`, `drift`, and `observed` a real comparison reaches is `inferDrift`'s law — birth is
812
- always aligned, presence compares existence only, and bytes are recorded only where they were
813
- read so the shape admits a birth-owned path reported stale, which no audit produces. That is
814
- deliberate: restating the comparison's case analysis in the type would be a second copy of it, able
815
- to disagree with the one that decides. `isFinding` proves the shape a reader may destructure and
816
- nothing about whether the verdict is one an audit could have reached. `repair` and `remove`
817
- re-derive every verdict themselves and act only on what they derived, so a verdict the comparison
818
- could not have produced is refused by name rather than acted on.
840
+ `isFinding` proves the shape a reader may destructure and nothing about whether the verdict is one
841
+ an audit could have reached. `repair` and `remove` re-derive every verdict themselves and act only
842
+ on what they derived, so a verdict the comparison could not have produced is refused by name rather
843
+ than acted on.
819
844
 
820
845
  That shape is versioned, and the guard runs at runtime. `repair` and `remove` guard the whole audit
821
846
  before reading any of it, so an audit persisted or built against an earlier version of this package
@@ -868,6 +893,69 @@ in an order that would be wrong. It also omits each row whose `lookup` field is
868
893
  inspect an omitted row's `lookup` field: an omitted `found` row belongs to a cycle, while another
869
894
  lookup verdict records why the registry row could not enter a layer.
870
895
 
896
+ ## Dependency floors
897
+
898
+ Every scaffold-owned range from its dependency tables is a floor: a caret over a whole
899
+ `major.minor.patch` version. The triple is the newest release the registry served when that floor
900
+ was last raised, so a workspace generated with no network still receives the latest floor scaffold
901
+ knew rather than a bare `major.0.0`. Caller extras and peers pass through unchanged. Extras follow
902
+ `EXTRA_RANGE_PATTERN`; fleet peers follow `ORKESTREL_RANGE_PATTERN`; foreign peers follow
903
+ `FLOOR_RANGE_PATTERN`.
904
+
905
+ The floors live in scaffold's own `package.json`. `BASE_DEV_DEPENDENCIES` and the tables beside it
906
+ derive each row scaffold installs from that manifest, and the self-pin from its `version` field, so
907
+ the toolchain a generated workspace receives is the toolchain scaffold runs. The rows scaffold does
908
+ not install are seeds — `@vitejs/plugin-vue`, `vue`, `vue-tsc`, `vite-plugin-singlefile`, and the
909
+ application-server fleet packages — and each carries the newest triple its supported major served
910
+ when it was written.
911
+ [`tests/src/core/constants.test.ts`](../tests/src/core/constants.test.ts) names that seeded set, so a
912
+ row entering or leaving the manifest moves a test rather than passing unnoticed.
913
+
914
+ A newer major is never crossed for you. `audit` reports one as a non-blocking `dependencies`
915
+ question, and a person decides whether the generated toolchain supports it. Inside the declared
916
+ major the verbs raise the floor themselves, which is what makes the caret's own width beside the
917
+ point: `^0.64.0` admits no `0.65.0`, and `repair` rewrites the range to `^0.65.0` rather than
918
+ widening it.
919
+
920
+ Compatibility and staleness are separate questions, so they are read by separate helpers.
921
+ `extractRangeMajor` answers which major a range names, which is what a compatibility bound is
922
+ measured against. `matchesRange` answers whether a published version satisfies a range, which is
923
+ admission rather than currency: a drift check that asked it would read a raised floor and a stale one
924
+ alike.
925
+
926
+ ### What each verb reads
927
+
928
+ | Verb | Reads | Writes on a complete answer | With no answer |
929
+ | ----------- | ----------------------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------- |
930
+ | `new` | Every `@orkestrel/*` row the compiled manifest declares | The plan's ranges, before the target is opened | `FETCH`, exit `1`, nothing written |
931
+ | `audit` | Every declared fleet row and planned foreign row | Nothing | Failed verdicts reported, exit `1`, nothing written |
932
+ | `repair` | Every declared fleet row and planned foreign row | The manifest's ranges, beside the repair | `FETCH`, exit `1`, nothing written |
933
+ | `catalog` | The organization package list and every packument behind it | The manifest's ranges, beside the package table | `FETCH`, exit `1`, nothing written |
934
+ | `overwrite` | Everything `repair` and `catalog` read | Everything `repair` and `catalog` write | The offline half stands, `note` names the step, exit `1` |
935
+
936
+ Each verb resolves the whole set before it opens a write transaction, so a partial answer never
937
+ becomes a partial pin set. `overwrite` is the exception, and deliberately: its offline repair and
938
+ deletion have already landed by the time the network half runs, so a step that produces no answer is
939
+ collected into `note` and reported rather than discarding work that succeeded.
940
+
941
+ A fleet row is compared exactly — `^0.1.0` is stale the moment the registry serves `0.1.2` — and
942
+ that inequality alone raises `audit` to exit `1`. A foreign row is compared inside its declared
943
+ major, and each verdict is a non-blocking question: one for a floor below the newest release that
944
+ major serves, and one for a newer major the registry publishes.
945
+
946
+ ### Raising the floor before a release
947
+
948
+ The floors ship inside the package, so a release carrying stale ones propagates them to every
949
+ workspace generated from it until the next release. Raise them as the opening step of a release,
950
+ while the registry is reachable:
951
+
952
+ 1. Run `scaffold audit` against this repository and read its `dependencies` questions.
953
+ 2. Run `scaffold repair`, or `npm update` followed by the same audit, until no question remains.
954
+ 3. Run the gate chain, then bump the version and publish.
955
+
956
+ That cycle is what keeps every release shipping the then-latest floor, and it is what a consumer
957
+ generating a workspace with no network receives.
958
+
871
959
  ## Vendored data root
872
960
 
873
961
  The vendored data root is the shared file set, staged into the published package as plain data. It
@@ -876,6 +964,13 @@ directories, the bench scripts, the shared policy register, the byte-identical r
876
964
  the guide mirrors a generated workspace starts from. `HOST_PATHS` is the candidate list; a plan
877
965
  carries the subset its target selects, because a workspace never mirrors its own guide.
878
966
 
967
+ `.claude/settings.json` is in that set, and the artifact planned for it is content-owned. `repair`
968
+ and `overwrite` restore its bytes, so an edit made to it inside a target is reverted at the next
969
+ visit and reported as drift until then. Put an operator grant in `.claude/settings.local.json`
970
+ instead. That path is outside `HOST_PATHS` and matches the vendoring deny-list
971
+ `matchesSensitivePath` reads, so `stageHost` never copies it into a host root and no plan carries
972
+ it.
973
+
879
974
  `stageHost` fills the root from a real checkout at build time:
880
975
 
881
976
  ```ts
@@ -907,7 +1002,10 @@ A workspace's file set is a function of its axes plus its structural facts. Noth
907
1002
  except the manifest.
908
1003
 
909
1004
  - One computed artifact: `package.json`, with the entry points, `exports` map, scripts, and
910
- development dependencies its selection implies.
1005
+ development dependencies its selection implies. A publishing manifest carries
1006
+ `"prepack": "npm run build"` so a publish rebuilds `dist/` and cannot ship a stale artifact;
1007
+ the hook is publish-time only, and every generated distribution proof passes
1008
+ `--ignore-scripts` to `npm pack` so a suite never re-runs the build it already gates.
911
1009
  - One template artifact per configuration file the selection needs: the root `tsconfig.json` and
912
1010
  `vite.config.ts`, plus a Vite config and a scoped TypeScript config per selected environment and
913
1011
  for `bin` when it is set.
@@ -1016,12 +1114,17 @@ verdict carrying its cause rather than thrown, so one unreachable package never
1016
1114
  rest of the answer. The organization package list is the exception, because without it there is no
1017
1115
  fleet to report.
1018
1116
 
1117
+ A found verdict carries the newest version the declared range admits, not whatever `dist-tags.latest`
1118
+ names. `lookup` reads the packument's version map, selects across it before any collection bound can
1119
+ truncate the map, and falls back to the latest tag only when that tag is itself admitted. A range of
1120
+ `*` admits every version, which is how a caller asks for the newest release outright.
1121
+
1019
1122
  Each bound counts decoded bytes, and a version lookup asks the registry for the abbreviated
1020
1123
  packument — `dist-tags` and a trimmed version map, rather than the full per-version metadata no
1021
- verdict reads. That is the smallest form the registry publishes, and `limit` is capped at
1022
- `MAX_ARTIFACT_BYTES`, so a package with enough published releases to pass it cannot be looked up at
1023
- all. It comes back as a `failed` verdict naming the limit, which is this reader's bound and not a
1024
- statement about the package.
1124
+ verdict reads. That is the smallest form the registry publishes. The default response limit is
1125
+ `MAX_REGISTRY_BYTES`, and the default call budget is `MAX_TOTAL_REGISTRY_BYTES`. A package that
1126
+ passes the response limit comes back as a `failed` verdict naming the limit, which is this reader's
1127
+ bound and not a statement about the package.
1025
1128
 
1026
1129
  A status that carries no representation — a `204` or a `205` — is a `failed` verdict naming the
1027
1130
  status, never a `found` answer holding no bytes. A genuinely empty file arrives as a `200` and does
@@ -1096,13 +1199,8 @@ ambient value plus isolated filesystem and network drivers for the mutating exam
1096
1199
  drivers is separate test capability rather than name-resolution parity.
1097
1200
 
1098
1201
  **The library does not enforce the creating verb's policy.** `new` refuses a blueprint carrying any
1099
- question, and `materialize` writes any plan into any vacant target. A workspace of several published
1100
- `src` environments without `core` is therefore constructible, compilable, and writable through the
1101
- library, and its manifest names a `core` build the workspace never runs — which is exactly what the
1102
- advisory said. The refusal lives in the verb that chose the shape because that verb is the only one
1103
- holding the advice: `compile` returns `questions` beside `plan`, and `materialize` receives the plan
1104
- alone, so it has nothing to refuse on. The Compile section states the rule a library caller applies
1105
- in its place.
1202
+ question, and `materialize` writes any plan into any vacant target. The Compile section states the
1203
+ rule a library caller applies in its place.
1106
1204
 
1107
1205
  **`isPath` does not prove host portability.** It proves bounded target-relative syntax and rejects
1108
1206
  traversal, separators, controls, and reserved syntax characters. It deliberately admits host-specific
@@ -1188,8 +1286,9 @@ port, so the run drives nothing external and stays in `test`.
1188
1286
  taken from a hostile value.
1189
1287
  - [`tests/src/core/templates.test.ts`](../tests/src/core/templates.test.ts) — the frozen template
1190
1288
  definitions.
1191
- - [`tests/src/core/constants.test.ts`](../tests/src/core/constants.test.ts) — the scaffold pin every
1192
- generated workspace inherits, held to the version this manifest declares.
1289
+ - [`tests/src/core/constants.test.ts`](../tests/src/core/constants.test.ts) — the seeded rows named
1290
+ as a set, the floor form every shared table and this manifest carry, and the emitted TypeScript
1291
+ bound.
1193
1292
  - [`tests/src/server/Materializer.test.ts`](../tests/src/server/Materializer.test.ts) — every
1194
1293
  mutation verb against a real temporary target and a real vendored root.
1195
1294
  - [`tests/src/server/WriteTransaction.test.ts`](../tests/src/server/WriteTransaction.test.ts) —
File without changes
File without changes
File without changes
File without changes