@orkestrel/scaffold 0.0.24 → 0.0.26
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 +5 -0
- package/dist/bin/main.js +45 -4
- package/dist/bin/main.js.map +1 -1
- package/dist/host/agents/orchestration.md +40 -11
- package/dist/host/claude/agents/orkestrel.md +63 -48
- package/dist/host/claude/rules/documentation.md +1 -1
- package/dist/host/guides/guide.md +211 -100
- package/dist/host/guides/scaffold.md +165 -10
- package/dist/host/manifest.json +5 -5
- package/dist/host/tests/config.test.ts +30 -7
- package/dist/src/core/index.cjs +242 -149
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +185 -98
- package/dist/src/core/index.d.ts +185 -98
- package/dist/src/core/index.js +240 -150
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +171 -66
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +140 -26
- package/dist/src/server/index.d.ts +140 -26
- package/dist/src/server/index.js +173 -69
- package/dist/src/server/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -12,6 +12,10 @@ does not work. Scaffold makes the shared set data — a vendored data root shipp
|
|
|
12
12
|
— and gives it three verbs: create a workspace from it, report how a workspace differs from it, and
|
|
13
13
|
write the difference back.
|
|
14
14
|
|
|
15
|
+
Every code fence below is illustrative. Nothing runs one, so a trailing `// value` comment inside a
|
|
16
|
+
fence is this guide's claim rather than a measured answer; the driven examples are the ones the
|
|
17
|
+
shipped declarations print. Limits states what that leaves unproven and what covers it instead.
|
|
18
|
+
|
|
15
19
|
```sh
|
|
16
20
|
npm install --save-dev @orkestrel/scaffold
|
|
17
21
|
```
|
|
@@ -98,13 +102,15 @@ Exported from `@orkestrel/scaffold`, and reachable from
|
|
|
98
102
|
| `EXTRA_RANGE_PATTERN` | const | The registry-only semver subset accepted for a development extra's range. |
|
|
99
103
|
| `GLOBAL_SETUP_PATH` | const | The shared Vitest global-setup module whose presence makes a workspace `global`. |
|
|
100
104
|
| `GUIDES_TEST_PATH` | const | The guide-parity proof whose physical file selects the fixed `guides` project. |
|
|
105
|
+
| `EXECUTABLE_PATHS` | const | The vendored paths a target receives with its executable bit set, frozen. |
|
|
101
106
|
| `GROUPS` | const | The seven `Group` values in plan order, frozen. |
|
|
102
107
|
| `HEX_PATTERN` | const | Exact lowercase hexadecimal bytes: two digits per byte, and empty content is valid. |
|
|
103
108
|
| `HOST_PATHS` | const | The paths byte-copied from the vendored data root, frozen. |
|
|
104
109
|
| `INTEGRATION_TEST_PATH` | const | The installed-package proof whose presence makes a workspace `integration`. |
|
|
105
|
-
| `INVALID_PATH_CHARACTER_PATTERN` | const | Visible characters a
|
|
110
|
+
| `INVALID_PATH_CHARACTER_PATTERN` | const | Visible characters a target-relative path and a Markdown path cell both forbid. |
|
|
106
111
|
| `MAX_ARTIFACT_BYTES` | const | Maximum bytes accepted for one artifact. |
|
|
107
112
|
| `MAX_ARTIFACT_HEX_LENGTH` | const | Maximum length of the hexadecimal string carrying one artifact's bytes. |
|
|
113
|
+
| `MAX_AUDIT_FINDINGS` | const | Maximum findings one audit can produce from a bounded plan and snapshot. |
|
|
108
114
|
| `MAX_COLLECTION_ITEMS` | const | Maximum items accepted in one public collection. |
|
|
109
115
|
| `MAX_DEPENDENCY_NAME_LENGTH` | const | Maximum dependency package name length, scope included, as the registry caps it. |
|
|
110
116
|
| `MAX_MANIFEST_BYTES` | const | Maximum bytes accepted for one package or vendored-host manifest. |
|
|
@@ -145,7 +151,7 @@ Exported from `@orkestrel/scaffold`, and reachable from
|
|
|
145
151
|
| `isHex` | const | Narrow a value to exact lowercase hexadecimal bytes within one artifact's limit. |
|
|
146
152
|
| `isMirror` | const | Narrow a value to a `Mirror`. |
|
|
147
153
|
| `isOverride` | const | Narrow a value to an `Override`. |
|
|
148
|
-
| `isPath` | function | Narrow a value to a
|
|
154
|
+
| `isPath` | function | Narrow a value to a logical target-relative path. |
|
|
149
155
|
| `isPlan` | const | Narrow a value to a `Plan`. |
|
|
150
156
|
| `isQuestion` | const | Narrow a value to a `Question`. |
|
|
151
157
|
| `isScaffoldError` | function | Narrow a caught value to a `ScaffoldError`. |
|
|
@@ -176,6 +182,7 @@ Exported from `@orkestrel/scaffold`, and reachable from
|
|
|
176
182
|
| `inferGroup` | function | Infer the `Group` a path belongs to. |
|
|
177
183
|
| `manifestToDependencies` | function | Project a package manifest's text to the `@orkestrel/*` packages it declares. |
|
|
178
184
|
| `manifestToName` | function | Project a package manifest's text to its own name. |
|
|
185
|
+
| `matchesDriftReachability` | function | Test whether `inferDrift` could have produced a finding for an ownership. |
|
|
179
186
|
| `matchesEngines` | function | Test whether a declared engines floor is at or above the supported minimum. |
|
|
180
187
|
| `matchesOrchestrationPath` | function | Test whether a path instructs or wires an agent rather than the toolchain. |
|
|
181
188
|
| `matchesRange` | function | Test whether a declared range already admits a published version. |
|
|
@@ -313,6 +320,7 @@ Exported from `@orkestrel/scaffold/server`, and reachable from
|
|
|
313
320
|
| `listDirectories` | function | List a directory's descendant directories as sorted root-relative paths. |
|
|
314
321
|
| `listFiles` | function | List a directory's files as sorted root-relative paths. |
|
|
315
322
|
| `matchesAnchor` | function | Test whether a captured directory is still the same directory. |
|
|
323
|
+
| `matchesExecutablePath` | function | Test whether a vendored path is one a target receives executable. |
|
|
316
324
|
| `matchesExpectation` | function | Test whether a destination still holds what was captured of it. |
|
|
317
325
|
| `matchesGitPath` | function | Test whether a path addresses a target's own repository metadata. |
|
|
318
326
|
| `matchesMissingPath` | function | Test whether a caught filesystem error reports an absent path. |
|
|
@@ -388,7 +396,7 @@ no interface and is documented directly.
|
|
|
388
396
|
| Method | Summary |
|
|
389
397
|
| ----------- | ---------------------------------------------------------------------------------- |
|
|
390
398
|
| `write` | Stage one text file. |
|
|
391
|
-
| `copy` | Stage one byte-for-byte copy
|
|
399
|
+
| `copy` | Stage one byte-for-byte copy in executable or non-executable destination mode. |
|
|
392
400
|
| `directory` | Establish one directory inside the target, one segment at a time. |
|
|
393
401
|
| `remove` | Mark one file for deletion at commit. |
|
|
394
402
|
| `commit` | Promote every staged file and take every marked file, or roll the whole call back. |
|
|
@@ -510,7 +518,7 @@ standard error, so a piped value is never polluted.
|
|
|
510
518
|
| Verb | Value |
|
|
511
519
|
| ----------- | -------------------------------------------------------------------------- |
|
|
512
520
|
| `new` | `MaterializeResult` — `target`, `written`, `skipped`, `removed` |
|
|
513
|
-
| `audit` | `Audit` — `findings` and `questions`
|
|
521
|
+
| `audit` | `Audit` — `findings` and `questions`; planned findings carry `ownership` |
|
|
514
522
|
| `repair` | `MaterializeResult` plus `audit`, the terminal audit taken after the write |
|
|
515
523
|
| `catalog` | `MaterializeResult` plus `entries`, `mirrors`, and `dropped` |
|
|
516
524
|
| `overwrite` | The `catalog` value plus `audit`, `releases`, and `note` on a partial run |
|
|
@@ -543,10 +551,24 @@ The two axes are independent, so a library-only, an application-only, and a mixe
|
|
|
543
551
|
first class. `dependencies` and `peers` are runtime `@orkestrel/*` packages; `extras` are
|
|
544
552
|
development dependencies and may carry any valid npm name.
|
|
545
553
|
|
|
554
|
+
One published environment owns the package root directly. Several published environments require
|
|
555
|
+
`core`, which owns that root while each other environment keeps its subpath. A multi-environment
|
|
556
|
+
`src` selection without `core` therefore emits entry fields naming a `core` build the workspace
|
|
557
|
+
never runs. The gate reports that as a non-blocking `src` question rather than refusing the compile,
|
|
558
|
+
because the shape is chosen once and read afterwards: `new` refuses the advisory, while `audit` and
|
|
559
|
+
`repair` need the plan to describe and restore a target that already has that shape. A library
|
|
560
|
+
caller creating a workspace holds the same refusal, and the Compile section below states it.
|
|
561
|
+
|
|
546
562
|
`bin`, `integration`, `services`, `global`, and `showcase` are structural facts. Each is set only
|
|
547
563
|
when the workspace physically ships the directory or exact-case file that defines it, never because
|
|
548
564
|
of the workspace's name and never because a sibling fact is set.
|
|
549
565
|
|
|
566
|
+
An axis-dependent structural fact projects only when its required axis exists. `integration`
|
|
567
|
+
projects a published `src`, and `showcase` projects the browser `app` environment. When that axis is
|
|
568
|
+
absent the flag adds no artifact, configuration, script, or dependency, and the gate reports a
|
|
569
|
+
non-blocking question on that field so the caller who set it learns it emitted nothing. The same
|
|
570
|
+
rule applies to both facts.
|
|
571
|
+
|
|
550
572
|
`createBlueprint` enforces shape only. Whether the name is a name, the version a version, and the
|
|
551
573
|
axis combination one this package can generate are the gate's laws, and the gate answers them with
|
|
552
574
|
questions. A blueprint the gate will refuse is still constructible, so one law lives in one place.
|
|
@@ -577,12 +599,40 @@ refused blueprint is answered rather than raised, so a caller reads the refusal
|
|
|
577
599
|
asked for. Each stage records its input and its output, a failed stage records the coded reason
|
|
578
600
|
beside them, and the stages after a failed one never run.
|
|
579
601
|
|
|
602
|
+
A plan says the blueprint can be built. It does not say the blueprint should be created. Every
|
|
603
|
+
question beside the plan is advice the compile could not settle, and the caller that chose the shape
|
|
604
|
+
is the one that answers it. So `new` refuses on any question, blocking or not, before it writes,
|
|
605
|
+
while `audit` and `repair` carry the same questions through, because a target that already has that
|
|
606
|
+
shape still has to be described and restored.
|
|
607
|
+
|
|
608
|
+
A library caller creating a fresh workspace applies `new`'s rule itself:
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
import { createBlueprint, createCompiler } from '@orkestrel/scaffold'
|
|
612
|
+
|
|
613
|
+
const compiler = createCompiler()
|
|
614
|
+
const scaffolding = compiler.compile(createBlueprint('router', { src: ['browser', 'server'] }))
|
|
615
|
+
|
|
616
|
+
scaffolding.plan === undefined || scaffolding.questions.length > 0 // true — do not write this shape
|
|
617
|
+
compiler.destroy()
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
`materialize` does not apply that rule for you, and it could not: `compile` returns `questions`
|
|
621
|
+
beside `plan`, and the writer receives the plan alone. It refuses what only a writer can see — a
|
|
622
|
+
target that is not vacant — and writes the plan it is given otherwise. Choosing a shape is a policy
|
|
623
|
+
about which workspace to want, the plan has already answered whether that workspace can be built,
|
|
624
|
+
and the questions are where the package says what it thinks of the choice. That is the same line
|
|
625
|
+
`createBlueprint` draws when it constructs a blueprint the gate will refuse: one law lives in one
|
|
626
|
+
place, and the caller that picked the shape is the one holding it.
|
|
627
|
+
|
|
580
628
|
Off-contract input is different. A value that is not the exact shape raises `ScaffoldError` coded
|
|
581
629
|
`INVALID`, because it is not a question anyone can answer. Both entry points snapshot the caller's
|
|
582
630
|
value first and then guard the snapshot, so a property backed by an accessor is refused rather than
|
|
583
631
|
read.
|
|
584
632
|
|
|
585
|
-
Overrides replace a drafted artifact's content whole.
|
|
633
|
+
Overrides replace a drafted artifact's content whole. The gate checks each override against the
|
|
634
|
+
blueprint's full draft before a group selection narrows the returned plan, so an override outside a
|
|
635
|
+
selected group does not block that compile. An override that matches no artifact in the full draft,
|
|
586
636
|
that targets a host-origin artifact, or that targets the manifest is a blocking question rather
|
|
587
637
|
than a silent no-op.
|
|
588
638
|
|
|
@@ -627,7 +677,19 @@ selected by their defining paths, as `guides` is. A workspace that needs other l
|
|
|
627
677
|
must keep those edits outside a content-owned file; `repair` restores that file to the canonical
|
|
628
678
|
project set.
|
|
629
679
|
|
|
630
|
-
An audit reports one `Finding` per
|
|
680
|
+
An audit reports one `Finding` per planned path, followed by any foreign path beneath the groups
|
|
681
|
+
the plan covers. Every planned finding carries its artifact's `ownership`. A foreign finding has
|
|
682
|
+
no ownership because no artifact was planned for its path. `Ownership` says what scaffold claims at
|
|
683
|
+
a path, not what one run did there. Counting planned findings by `content`, `presence`, and `birth`
|
|
684
|
+
therefore says what audit is entitled to compare and stays the same against a vacant target and a
|
|
685
|
+
repaired one. What one run compared comes from `ownership`, `drift`, and `observed` together. A
|
|
686
|
+
content-owned finding carrying `observed` had its bytes compared. A content-owned `missing` finding
|
|
687
|
+
and every presence-owned finding were decided by existence alone. A birth-owned finding was not
|
|
688
|
+
examined. The foreign findings are exactly the ones no ownership accounts for. The audit stores no
|
|
689
|
+
aggregate tally.
|
|
690
|
+
|
|
691
|
+
The plan and snapshot are each bounded at `MAX_COLLECTION_ITEMS`. An audit may therefore carry one
|
|
692
|
+
finding per planned artifact plus one per unplanned snapshot path, up to `MAX_AUDIT_FINDINGS`.
|
|
631
693
|
|
|
632
694
|
| `Drift` | Means |
|
|
633
695
|
| --------- | ------------------------------------- |
|
|
@@ -642,6 +704,25 @@ removes a foreign file each fail when the destination no longer matches what the
|
|
|
642
704
|
The requirement sits in the type rather than in prose, because a deletion that cannot bind to what
|
|
643
705
|
the audit showed is the one thing the destructive verb must never do.
|
|
644
706
|
|
|
707
|
+
The shape a `Finding` admits is wider than the set an audit produces. Which combinations of
|
|
708
|
+
`ownership`, `drift`, and `observed` a real comparison reaches is `inferDrift`'s law — birth is
|
|
709
|
+
always aligned, presence compares existence only, and bytes are recorded only where they were
|
|
710
|
+
read — so the shape admits a birth-owned path reported stale, which no audit produces. That is
|
|
711
|
+
deliberate: restating the comparison's case analysis in the type would be a second copy of it, able
|
|
712
|
+
to disagree with the one that decides. `isFinding` proves the shape a reader may destructure and
|
|
713
|
+
nothing about whether the verdict is one an audit could have reached. `repair` and `remove`
|
|
714
|
+
re-derive every verdict themselves and act only on what they derived, so a verdict the comparison
|
|
715
|
+
could not have produced is refused by name rather than acted on.
|
|
716
|
+
|
|
717
|
+
That shape is versioned, and the guard runs at runtime. `repair` and `remove` guard the whole audit
|
|
718
|
+
before reading any of it, so an audit persisted or built against an earlier version of this package
|
|
719
|
+
is refused with a coded `INVALID` failure rather than accepted and partly understood. A planned
|
|
720
|
+
finding carries `ownership`, which findings made before that field existed do not. `remove` acts
|
|
721
|
+
only on foreign findings, which never carried ownership, but the guard reads every finding, so one
|
|
722
|
+
older planned finding refuses that call too. Take a fresh audit rather than replaying a stored one:
|
|
723
|
+
a stored audit records what a target looked like then, and both verbs bind their writes to what a
|
|
724
|
+
target holds now. The refusal is deliberate at `0.0.x` and there is no migration path.
|
|
725
|
+
|
|
645
726
|
## Vendored data root
|
|
646
727
|
|
|
647
728
|
The vendored data root is the shared file set, staged into the published package as plain data. It
|
|
@@ -744,6 +825,19 @@ result.written // every path created
|
|
|
744
825
|
materializer.destroy()
|
|
745
826
|
```
|
|
746
827
|
|
|
828
|
+
`resolveContainedPath` refuses a lexical escape, a physical link out of the root, and a dangling
|
|
829
|
+
link whose raw target contains a `..` segment. It returns the lexical join of `root` and `path` — an
|
|
830
|
+
absolute path under `root`, which is what its shipped example prints — after checking the namespace,
|
|
831
|
+
not an open filesystem handle. Its contract therefore excludes a concurrent rename or link swap
|
|
832
|
+
during the check or before the caller finishes using that path. A caller that admits hostile
|
|
833
|
+
concurrent namespace mutation needs a handle-bound operation instead.
|
|
834
|
+
|
|
835
|
+
`resolveRealPath` answers the caller's own text collapsed lexically, then resolved through every link
|
|
836
|
+
in what survives that collapse. A `..` the caller wrote cancels the segment before it as text, so
|
|
837
|
+
`<root>/hop/..` answers `<root>` even where `hop` links elsewhere, rather than the directory holding
|
|
838
|
+
what `hop` points at. The collapse only ever shortens the path, so nothing reaches outside it this
|
|
839
|
+
way; the answer is a lexical location resolved through links, not a physical one.
|
|
840
|
+
|
|
747
841
|
Read the registry and the guide host:
|
|
748
842
|
|
|
749
843
|
```ts
|
|
@@ -762,6 +856,17 @@ verdict carrying its cause rather than thrown, so one unreachable package never
|
|
|
762
856
|
rest of the answer. The organization package list is the exception, because without it there is no
|
|
763
857
|
fleet to report.
|
|
764
858
|
|
|
859
|
+
Both bounds count decoded bytes, and a version lookup asks the registry for the abbreviated
|
|
860
|
+
packument — `dist-tags` and a trimmed version map, rather than the full per-version metadata no
|
|
861
|
+
verdict reads. That is the smallest form the registry publishes, and `limit` is capped at
|
|
862
|
+
`MAX_ARTIFACT_BYTES`, so a package with enough published releases to pass it cannot be looked up at
|
|
863
|
+
all. It comes back as a `failed` verdict naming the limit, which is this reader's bound and not a
|
|
864
|
+
statement about the package.
|
|
865
|
+
|
|
866
|
+
A status that carries no representation — a `204` or a `205` — is a `failed` verdict naming the
|
|
867
|
+
status, never a `found` answer holding no bytes. A genuinely empty file arrives as a `200` and does
|
|
868
|
+
read as found.
|
|
869
|
+
|
|
765
870
|
Stage and swap a set of files yourself:
|
|
766
871
|
|
|
767
872
|
```ts
|
|
@@ -780,6 +885,9 @@ A transaction owns a private root beside the target on the same volume, so every
|
|
|
780
885
|
rename rather than a copy. A failure part way through commit restores every destination it already
|
|
781
886
|
promoted and removes every directory it created. No destination ever receives half-written bytes.
|
|
782
887
|
It is not a journal: a process killed between two promotions leaves a mixed target.
|
|
888
|
+
The transaction binds each directory's location rather than its lifetime, so an ancestor swapped
|
|
889
|
+
for another path, a file, a symlink, or nothing is refused, and one deleted and recreated in place
|
|
890
|
+
may not be.
|
|
783
891
|
|
|
784
892
|
Narrow a refusal by its code:
|
|
785
893
|
|
|
@@ -793,9 +901,16 @@ try {
|
|
|
793
901
|
}
|
|
794
902
|
```
|
|
795
903
|
|
|
796
|
-
`INVALID` is off-contract input, `BLOCKED` is a refused blueprint
|
|
797
|
-
|
|
798
|
-
|
|
904
|
+
`INVALID` is off-contract input, `BLOCKED` is a refused blueprint, `TARGET` is a destination that is
|
|
905
|
+
not what the caller's observation said it was, `WRITE` is a mutation that could not be completed, and
|
|
906
|
+
`FETCH` is an upstream read that produced no answer.
|
|
907
|
+
|
|
908
|
+
`BLOCKED` covers both refusals a blueprint can meet, because they are one fact — this blueprint will
|
|
909
|
+
not be built — and the questions say which. The compiler answers its refusal rather than throwing it:
|
|
910
|
+
the gate fails closed and records `BLOCKED` on its stage, so a caller reads that refusal from the
|
|
911
|
+
value it asked for. A verb that creates a workspace throws it, because it chose the shape and has
|
|
912
|
+
nothing to hand back. A blocking question closed the gate; a non-blocking one is a shape the package
|
|
913
|
+
can describe and declines to create.
|
|
799
914
|
|
|
800
915
|
Every entity publishes an emitter. The compiler emits `compile`, `audit`, `block`, `error`, and
|
|
801
916
|
`destroy`; the materializer emits `write`, `remove`, `finish`, `error`, and `destroy`; the upstream
|
|
@@ -804,13 +919,53 @@ are thrown, so an observer sees a refusal even where the caller catches it.
|
|
|
804
919
|
|
|
805
920
|
## Limits
|
|
806
921
|
|
|
807
|
-
|
|
922
|
+
Six things a reader will look for and not find.
|
|
923
|
+
|
|
924
|
+
**A code fence in this guide is unverified.** [`tests/guides.test.ts`](../tests/guides.test.ts)
|
|
925
|
+
proves that every fence imports only real exports of the two barrels, and that every backticked name
|
|
926
|
+
in this file resolves to one. It neither runs a fence nor typechecks one, so a trailing `// value`
|
|
927
|
+
comment inside a fence states what this guide claims rather than what the build answered. The
|
|
928
|
+
verdicts that are measured are the ones a consumer hovers:
|
|
929
|
+
[`tests/integration.test.ts`](../tests/integration.test.ts) drives every `@example` the built
|
|
930
|
+
declarations print against the installed package, scores each verdict it can read as a value, and
|
|
931
|
+
names exactly the ones it cannot. Fences are not added to that instrument, because most of them
|
|
932
|
+
cannot be run: several declare an ambient value that has no runtime, and several write to a
|
|
933
|
+
directory or read the network, so executing them would be a mutation rather than a check.
|
|
934
|
+
|
|
935
|
+
**The library does not enforce the creating verb's policy.** `new` refuses a blueprint carrying any
|
|
936
|
+
question, and `materialize` writes any plan into any vacant target. A workspace of several published
|
|
937
|
+
`src` environments without `core` is therefore constructible, compilable, and writable through the
|
|
938
|
+
library, and its manifest names a `core` build the workspace never runs — which is exactly what the
|
|
939
|
+
advisory said. The refusal lives in the verb that chose the shape because that verb is the only one
|
|
940
|
+
holding the advice: `compile` returns `questions` beside `plan`, and `materialize` receives the plan
|
|
941
|
+
alone, so it has nothing to refuse on. The Compile section states the rule a library caller applies
|
|
942
|
+
in its place.
|
|
943
|
+
|
|
944
|
+
**`isPath` does not prove host portability.** It proves bounded target-relative syntax and rejects
|
|
945
|
+
traversal, separators, controls, and reserved syntax characters. It deliberately admits host-specific
|
|
946
|
+
segment spellings such as a Windows device name, a trailing dot or space, and a segment beyond a
|
|
947
|
+
filesystem's byte ceiling. The compiler emits none of those names. A caller-supplied plan may carry
|
|
948
|
+
one, and the writer reports the host's refusal rather than treating logical path syntax as a promise
|
|
949
|
+
that every filesystem can create it. `isFilesystemPath` is the separate server guard for target and
|
|
950
|
+
vendored-root locations on the host. Both `/` and `\` are separators to every reading in this
|
|
951
|
+
package, on every host, including the server's reading of a raw symbolic-link target. A POSIX
|
|
952
|
+
filename that legally contains a backslash — `weird\..\name` — is therefore refused as three
|
|
953
|
+
segments rather than admitted as one name. That is one separator law with a conservative side, not a
|
|
954
|
+
host-dependent second one.
|
|
808
955
|
|
|
809
956
|
**Scaffold emits no styles axis.** `SRC_MATRIX` is exactly `core`, `browser`, and `server`, and
|
|
810
957
|
`Blueprint` carries no styles field. A workspace that needs `src/styles/` adds the directory, its
|
|
811
958
|
configuration, and its Vitest project by hand. `.claude/rules/workspace.md` describes styles as an
|
|
812
959
|
environment because the fleet has one; scaffold simply does not generate it.
|
|
813
960
|
|
|
961
|
+
**No host path is normalized before it is guarded.** `isFilesystemPath` refuses an empty segment, so
|
|
962
|
+
`packages//router` is off contract. A trailing separator does not produce one: it terminates a
|
|
963
|
+
directory rather than opening a segment, and every Node path API reads `./packages/router/` and
|
|
964
|
+
`./packages/router` as one location, so both are admitted. Every server entry point and the
|
|
965
|
+
`--target` option guard the text they were handed and resolve it afterwards, so a directory taken
|
|
966
|
+
from a shell completion arrives carrying the separator the shell appended and names the directory it
|
|
967
|
+
appears to name.
|
|
968
|
+
|
|
814
969
|
**A generated workspace has empty barrels and no starter entity.** Every emitted `index.ts` exports
|
|
815
970
|
nothing. This is deliberate: a generated sample entity is repeatedly mistaken for real
|
|
816
971
|
implementation. What a consumer does first is write the module's `types.ts`, then the
|
package/dist/host/manifest.json
CHANGED
|
@@ -463,22 +463,22 @@
|
|
|
463
463
|
{
|
|
464
464
|
"storage": "scripts/codex.sh",
|
|
465
465
|
"destination": "scripts/codex.sh",
|
|
466
|
-
"executable":
|
|
466
|
+
"executable": true
|
|
467
467
|
},
|
|
468
468
|
{
|
|
469
469
|
"storage": "scripts/cursor.sh",
|
|
470
470
|
"destination": "scripts/cursor.sh",
|
|
471
|
-
"executable":
|
|
471
|
+
"executable": true
|
|
472
472
|
},
|
|
473
473
|
{
|
|
474
474
|
"storage": "scripts/deps.sh",
|
|
475
475
|
"destination": "scripts/deps.sh",
|
|
476
|
-
"executable":
|
|
476
|
+
"executable": true
|
|
477
477
|
},
|
|
478
478
|
{
|
|
479
479
|
"storage": "scripts/ollama.sh",
|
|
480
480
|
"destination": "scripts/ollama.sh",
|
|
481
|
-
"executable":
|
|
481
|
+
"executable": true
|
|
482
482
|
},
|
|
483
483
|
{
|
|
484
484
|
"storage": "tests/config.test.ts",
|
|
@@ -531,5 +531,5 @@
|
|
|
531
531
|
".codex/agents",
|
|
532
532
|
".cursor/rules"
|
|
533
533
|
],
|
|
534
|
-
"digest": "
|
|
534
|
+
"digest": "ae9a32c599f4ca43278496001c659731504b3e669252ca231aaaf1c61d84810c"
|
|
535
535
|
}
|
|
@@ -110,6 +110,12 @@ describe('root configuration', () => {
|
|
|
110
110
|
})
|
|
111
111
|
}
|
|
112
112
|
expected.set('probe', { include: 'tmp/probe/**/*.test.ts', setup: ['./tests/setup.ts'] })
|
|
113
|
+
// A row that is a configuration rather than a factory. A workspace with a
|
|
114
|
+
// browser application emits one, because that factory refuses overrides and
|
|
115
|
+
// so is not a value Vitest may call. It is required here, in a workspace that
|
|
116
|
+
// has no browser application, so this proof exercises that resolution
|
|
117
|
+
// wherever it runs instead of only where the shape happens to occur.
|
|
118
|
+
expected.set('concrete', { include: 'tests/concrete.test.ts', setup: ['./tests/setup.ts'] })
|
|
113
119
|
|
|
114
120
|
const projects = configuration.test?.projects
|
|
115
121
|
if (!Array.isArray(projects)) throw new Error('The root configuration carries no projects')
|
|
@@ -128,7 +134,14 @@ describe('root configuration', () => {
|
|
|
128
134
|
'name',
|
|
129
135
|
{ value: 'control' },
|
|
130
136
|
)
|
|
131
|
-
const
|
|
137
|
+
const concrete = {
|
|
138
|
+
test: {
|
|
139
|
+
name: { label: 'concrete' },
|
|
140
|
+
include: ['tests/concrete.test.ts'],
|
|
141
|
+
setupFiles: ['./tests/setup.ts'],
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
const controlled = projects.concat(control, concrete)
|
|
132
145
|
const configured = new Map<
|
|
133
146
|
string,
|
|
134
147
|
{ readonly include: string; readonly setup: readonly string[] }
|
|
@@ -137,13 +150,23 @@ describe('root configuration', () => {
|
|
|
137
150
|
const factoryName = requiredLabel.replace(/:([a-z])/gu, (_match, letter: string) =>
|
|
138
151
|
letter.toUpperCase(),
|
|
139
152
|
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
153
|
+
// A row is either the factory named for the project or the configuration
|
|
154
|
+
// that project resolves to, and a required project is found as whichever
|
|
155
|
+
// it is. Only the required row is read, so an extra factory is still
|
|
156
|
+
// selected by name and never called.
|
|
157
|
+
const row = controlled.find((candidate) => {
|
|
158
|
+
if (typeof candidate === 'function') return candidate.name === factoryName
|
|
159
|
+
if (typeof candidate !== 'object' || candidate === null) return false
|
|
160
|
+
const block: unknown = Object.getOwnPropertyDescriptor(candidate, 'test')?.value
|
|
161
|
+
if (typeof block !== 'object' || block === null) return false
|
|
162
|
+
const named: unknown = Object.getOwnPropertyDescriptor(block, 'name')?.value
|
|
163
|
+
if (typeof named !== 'object' || named === null) return false
|
|
164
|
+
return Object.getOwnPropertyDescriptor(named, 'label')?.value === requiredLabel
|
|
165
|
+
})
|
|
166
|
+
if (row === undefined) {
|
|
167
|
+
throw new Error(`${requiredLabel} has no project factory or configuration`)
|
|
145
168
|
}
|
|
146
|
-
const project: unknown = Reflect.apply(
|
|
169
|
+
const project: unknown = typeof row === 'function' ? Reflect.apply(row, undefined, []) : row
|
|
147
170
|
if (typeof project !== 'object' || project === null) {
|
|
148
171
|
throw new Error('A project factory returned no configuration')
|
|
149
172
|
}
|