@sleepy_panda_srl/portulan 0.1.3 → 0.3.0
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 +7 -6
- package/cli/README.md +145 -358
- package/cli/advisory.mjs +354 -0
- package/cli/comments.mjs +932 -0
- package/cli/compile.mjs +1719 -1699
- package/cli/context.mjs +883 -0
- package/cli/control-chars.mjs +12 -301
- package/cli/discover.mjs +17 -574
- package/cli/doctor.mjs +269 -1080
- package/cli/drills.mjs +92 -504
- package/cli/feedback.mjs +16 -237
- package/cli/finish.mjs +437 -0
- package/cli/form.mjs +644 -0
- package/cli/fuzz-shell.mjs +20 -305
- package/cli/gate.mjs +7 -199
- package/cli/goldens.mjs +26 -184
- package/cli/index.mjs +305 -655
- package/cli/init.mjs +243 -566
- package/cli/inside.mjs +3 -28
- package/cli/instructions.mjs +829 -0
- package/cli/ledger.mjs +882 -0
- package/cli/librarian.mjs +135 -599
- package/cli/manifest.mjs +2 -39
- package/cli/mutants.mjs +9 -170
- package/cli/new.mjs +12 -243
- package/cli/pack-identity.mjs +5 -74
- package/cli/pack-version.mjs +13 -255
- package/cli/plugin-lint.mjs +42 -658
- package/cli/portulan.mjs +6 -125
- package/cli/recipe-set.mjs +15 -245
- package/cli/release-eval.mjs +28 -348
- package/cli/review-meter.mjs +29 -265
- package/cli/rule-carriers.mjs +12 -175
- package/cli/sessions.mjs +82 -0
- package/cli/skill-goldens.mjs +10 -176
- package/cli/skills-set.mjs +12 -414
- package/cli/stop-gate.mjs +83 -514
- package/cli/symbols.mjs +1132 -0
- package/cli/telemetry.mjs +82 -505
- package/cli/upgrade.mjs +171 -314
- package/cli/vendor.mjs +146 -476
- package/cli/version-carriers.mjs +8 -86
- package/core/README.md +1 -1
- package/core/engine.md +3 -9
- package/core/operating/autonomy.md +1 -1
- package/core/operating/context.md +258 -0
- package/core/operating/evolution.md +3 -3
- package/core/operating/loop.md +13 -20
- package/core/operating/memory.md +12 -3
- package/core/operating/sessions.md +100 -0
- package/core/personas/README.md +33 -0
- package/core/personas/librarian.md +2 -2
- package/core/skills/clarify/SKILL.md +1 -1
- package/core/skills/consolidate/SKILL.md +5 -3
- package/core/templates/handoff.md +28 -15
- package/core/templates/memory-entry.md +1 -0
- package/evals/README.md +271 -845
- package/evals/releases/0.2.0.json +146 -0
- package/evals/releases/0.2.0.md +67 -0
- package/evals/releases/0.3.0.json +150 -0
- package/evals/releases/0.3.0.md +68 -0
- package/evals/releases/README.md +3 -3
- package/package.json +5 -2
- package/packs/.claude-plugin/plugin.json +2 -1
- package/packs/README.md +7 -4
- package/packs/rituals/checkpoints/README.md +1 -1
- package/packs/tools/README.md +8 -8
- package/packs/tools/github/verify/actions-pinned.sh +10 -55
- package/packs/tools/local-llm/README.md +136 -0
- package/packs/tools/local-llm/pack.json +32 -0
- package/packs/tools/local-llm/personas/local-worker.md +44 -0
- package/packs/tools/local-llm/runner/local-llm.mjs +1665 -0
- package/packs/tools/local-llm/skills/delegate-local/SKILL.md +64 -0
- package/packs/tools/local-llm/verify/README.md +18 -0
- package/spec/README.md +113 -6
- package/spec/migrations/0001-repository-declares-its-tree.mjs +1 -26
- package/spec/migrations/0002-bundle-fallback-path.mjs +13 -74
- package/spec/migrations/0003-handoff-index-not-kept.mjs +66 -0
- package/spec/migrations/0004-changelog-fragments.mjs +53 -0
- package/spec/migrations/0005-session-log-retired.mjs +69 -0
- package/spec/migrations/0006-boot-card.mjs +77 -0
- package/spec/migrations/0007-guidance-compiled.mjs +31 -0
- package/spec/migrations/0008-card-reading.mjs +55 -0
- package/spec/migrations/0009-instruction-sections-on-read.mjs +60 -0
- package/spec/migrations/0010-comment-rail.mjs +57 -0
- package/spec/migrations/README.md +45 -10
- package/spec/pack.schema.json +1 -1
- package/spec/slots.md +343 -27
- package/spec/workspace.schema.json +145 -4
package/cli/portulan.mjs
CHANGED
|
@@ -1,63 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Portulan — the one entry point the `npx` package exposes.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// `vendor` · `index` · `upgrade` · `new` · `feedback`. This is milestone 7's packaging clause.
|
|
6
|
-
//
|
|
7
|
-
// It named six until 2026-08-03. `new` and `feedback` arrived in the CLI first, licensed by row 7 of
|
|
8
|
-
// `docs/plan.md` naming them in its own ratified text, and the maintainer then folded both into the
|
|
9
|
-
// constitution — so the two-carrier state that licence created lasted one pull request and is gone. The
|
|
10
|
-
// history is worth one sentence rather than none: a subcommand may be licensed by the row before the
|
|
11
|
-
// constitution catches up, and when that happens this file says which document names it. Everything
|
|
12
|
-
// beyond these eight is still the maintainer's call.
|
|
13
|
-
//
|
|
14
|
-
// ## What it is NOT
|
|
15
|
-
//
|
|
16
|
-
// **Not a reimplementation.** `doctor`, `compile`, `index` and `init` each live in their own file and
|
|
17
|
-
// are exercised by their own suites; this dispatches to them and adds nothing to what they do. A
|
|
18
|
-
// wrapper that re-derived any part of their behaviour would be a second carrier of one fact, which is
|
|
19
|
-
// the class `0020` names — a rule enforced at two sites and repaired at one — so the dispatch imports each tool's
|
|
20
|
-
// exported `run` and returns its exit code untouched.
|
|
21
|
-
//
|
|
22
|
-
// **Not a home for the tools that are off the list.** Several tools live in `cli/` and are deliberately
|
|
23
|
-
// absent here. `docs/vision.md` names eight, it is human-owned, and whether any of the others ever joins
|
|
24
|
-
// that list is the maintainer's call and not an implementer's. Wiring one in would mint a ninth
|
|
25
|
-
// subcommand into a list this file does not own. They stay runnable exactly as they are today —
|
|
26
|
-
// `node cli/plugin-lint.mjs …` — and every verify recipe still invokes them that way.
|
|
27
|
-
//
|
|
28
|
-
// **The roster itself is `cli/README.md`'s and is cited rather than repeated here.** This paragraph
|
|
29
|
-
// said **two**, then **three**, and named a different three from the root `README.md`'s three while
|
|
30
|
-
// **four** were on disk — one rule with three carriers, obeyed at the narrowest, in the file whose own
|
|
31
|
-
// comments name that class. _(It also named the wrong ordinal once — *a seventh subcommand*, written
|
|
32
|
-
// when the list held six. A count in prose beside a list nothing derives it from is the drift this
|
|
33
|
-
// file's own suite catches for `SUBCOMMANDS.length` and cannot catch here.)_
|
|
34
|
-
//
|
|
35
|
-
// ## All eight are built, and the shape of the answer while one was not is kept
|
|
36
|
-
//
|
|
37
|
-
// `upgrade` carried `module: null` from milestone 7 session 0 until session 9 and exited **2 — could
|
|
38
|
-
// not run**, naming where it arrived. It dispatches now like the rest. The paragraph is kept rather
|
|
39
|
-
// than deleted, because the state it describes recurs: the constitution may name a verb before the
|
|
40
|
-
// tree carries it, and both alternatives to that answer were worse — hiding the entry makes the
|
|
41
|
-
// package look like a smaller tool and the row like it is further along than it is, and stubbing it
|
|
42
|
-
// to exit 0 is a fail-open in the one place a user is most likely to trust silence. "Nothing looked"
|
|
43
|
-
// is never "nothing wrong" — the same three-code discipline the verify recipes hold
|
|
44
|
-
// (`.portulan/memory/verify-preconditions-fail-closed.md`). The machinery for the next such entry is
|
|
45
|
-
// still here: `module: null` plus `arrives`, and the refusal below reads them.
|
|
46
|
-
//
|
|
47
|
-
// _(`vendor` was one of three until milestone 7 session 3, and `feedback` one of two until session 6.
|
|
48
|
-
// The count is written out here and derived in the suite, which is the split that has held: the
|
|
49
|
-
// sentence explains, the assertion counts.)_
|
|
50
|
-
//
|
|
51
|
-
// ## Exit codes, which are the tools' own
|
|
52
|
-
//
|
|
53
|
-
// `0` the subcommand succeeded · `1` it rendered a red verdict · `2` it could not run — including
|
|
54
|
-
// an unknown subcommand, an unbuilt one, and being invoked with **no arguments at all**, because
|
|
55
|
-
// none of those is a verdict about anybody's workspace. An **explicit** `--help` exits 0: asking for
|
|
56
|
-
// help is a request, and it succeeded.
|
|
57
|
-
//
|
|
58
|
-
// Zero dependencies and no build step, which is a ruling rather than an accident: the maintainer
|
|
59
|
-
// settled on 2026-07-31 that this ships as plain ESM on Node, against `.portulan/identity.md`'s
|
|
60
|
-
// older "TypeScript on Node" line, so that the repository stays checkable by cloning it.
|
|
4
|
+
// Exit codes are the subcommand's own; else 0 for `--help` or `--version`, and 2 for no arguments or a subcommand that cannot run.
|
|
61
5
|
|
|
62
6
|
import fs from "node:fs";
|
|
63
7
|
import path from "node:path";
|
|
@@ -65,23 +9,11 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
65
9
|
|
|
66
10
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
67
11
|
|
|
68
|
-
//
|
|
69
|
-
// rather than here, since milestone 7 session 6.
|
|
70
|
-
//
|
|
71
|
-
// It was written down twice — here and in `package.json` — which is this repository's dominant defect
|
|
72
|
-
// class committed in the file whose own comments name it. The read moved down a level when `feedback`
|
|
73
|
-
// needed the same value: importing it *from this file* made the subcommand circular with the entry
|
|
74
|
-
// point and hung the command line on an unsettled top-level await, which `manifest.mjs`'s header
|
|
75
|
-
// records in full. Re-exported here so this file's published surface is unchanged.
|
|
12
|
+
// A subcommand imports VERSION from ./manifest.mjs: importing it from here would cycle through this file's top-level await and exit 13.
|
|
76
13
|
export { VERSION } from "./manifest.mjs";
|
|
77
14
|
import { VERSION } from "./manifest.mjs";
|
|
78
15
|
|
|
79
|
-
//
|
|
80
|
-
// subcommand's `run`; `null` means the subcommand exists in the vision and not yet in the tree.
|
|
81
|
-
//
|
|
82
|
-
// `arrives` is not decoration. Condition 4 of `.portulan/dod.md` is that a document describing
|
|
83
|
-
// capability either has the capability or names where it arrives, and a help screen listing a verb
|
|
84
|
-
// that does nothing is exactly that document.
|
|
16
|
+
// In `docs/vision.md`'s order. An unbuilt entry sets `module: null` and `arrives`, and `namedIn` where another document names it.
|
|
85
17
|
export const SUBCOMMANDS = [
|
|
86
18
|
{
|
|
87
19
|
name: "init",
|
|
@@ -91,7 +23,7 @@ export const SUBCOMMANDS = [
|
|
|
91
23
|
{
|
|
92
24
|
name: "doctor",
|
|
93
25
|
module: "doctor.mjs",
|
|
94
|
-
summary: "validate a workspace
|
|
26
|
+
summary: "validate a workspace and report branch-owned host tokens beside local receipts; --static skips usage records",
|
|
95
27
|
},
|
|
96
28
|
{
|
|
97
29
|
name: "compile",
|
|
@@ -101,11 +33,6 @@ export const SUBCOMMANDS = [
|
|
|
101
33
|
{
|
|
102
34
|
name: "vendor",
|
|
103
35
|
module: "vendor.mjs",
|
|
104
|
-
// BOTH directions, because the constitution now glosses both. It said "a self-contained
|
|
105
|
-
// AGENTS.md + .portulan/ for any host" until 2026-08-03, when the maintainer widened the gloss
|
|
106
|
-
// to cover materialising a workspace out of a repository as well as into one — which is what
|
|
107
|
-
// settled the verb row 7 had deliberately left unassigned. A summary carrying only the older
|
|
108
|
-
// half would be the narrower of two carriers of one fact, which is the shape a reader obeys.
|
|
109
36
|
summary: "materialise a workspace where it is needed: into a host, or between residences",
|
|
110
37
|
},
|
|
111
38
|
{
|
|
@@ -116,11 +43,6 @@ export const SUBCOMMANDS = [
|
|
|
116
43
|
{
|
|
117
44
|
name: "upgrade",
|
|
118
45
|
module: "upgrade.mjs",
|
|
119
|
-
// BOTH kinds, because the chain carries both. A summary naming only the version half would
|
|
120
|
-
// be the narrower of two carriers of one fact — and the narrower is the one a reader obeys.
|
|
121
|
-
// What a step is lives in `spec/migrations/README.md`; this line only has to not contradict
|
|
122
|
-
// it, which the older wording ("migrate a workspace to a newer Workspace Definition") would
|
|
123
|
-
// have started doing the moment the maintainer admitted repairs on 2026-08-12.
|
|
124
46
|
summary: "migrate a workspace: the Workspace Definition steps it owes, and the repairs a rewriter owes it",
|
|
125
47
|
},
|
|
126
48
|
{
|
|
@@ -148,8 +70,6 @@ export function usage() {
|
|
|
148
70
|
"",
|
|
149
71
|
];
|
|
150
72
|
for (const entry of SUBCOMMANDS) {
|
|
151
|
-
// The state is printed per subcommand rather than as a footnote, because a reader scanning
|
|
152
|
-
// this list is deciding what to type, and a footnote is read after that decision.
|
|
153
73
|
const state = entry.module ? "" : ` (not built — arrives at ${entry.arrives})`;
|
|
154
74
|
lines.push(` ${entry.name.padEnd(width)} ${entry.summary}${state}`);
|
|
155
75
|
}
|
|
@@ -172,12 +92,6 @@ export async function run(argv, options = {}) {
|
|
|
172
92
|
|
|
173
93
|
if (name === undefined || name === "--help" || name === "-h" || name === "help") {
|
|
174
94
|
say(usage());
|
|
175
|
-
// The split is the point: a bare `portulan` was asked to do something and did nothing, which
|
|
176
|
-
// is could-not-run; an explicit `--help` asked for the help screen and got it, which is
|
|
177
|
-
// success. Written out because the two look alike in a shell and a reader scripting against
|
|
178
|
-
// this will meet both. _(These lines previously argued that explicit `--help` should exit 2
|
|
179
|
-
// as well — an argument the code never implemented and the tests contradict. Corrected in
|
|
180
|
-
// the direction of the behaviour, which is the conventional one.)_
|
|
181
95
|
return name === undefined ? 2 : 0;
|
|
182
96
|
}
|
|
183
97
|
|
|
@@ -189,28 +103,17 @@ export async function run(argv, options = {}) {
|
|
|
189
103
|
const entry = find(name);
|
|
190
104
|
if (!entry) {
|
|
191
105
|
warn(`portulan: unknown subcommand \`${name}\`.`);
|
|
192
|
-
// Derived, never a literal. A hard-coded count is wrong the hour a subcommand lands — which is
|
|
193
|
-
// exactly what happened to a literal `3` in this file's own suite when `init` shipped.
|
|
194
106
|
warn(`portulan: the ${SUBCOMMANDS.length} subcommands are ${SUBCOMMANDS.map((s) => s.name).join(", ")}. Run \`portulan --help\`.`);
|
|
195
107
|
return 2;
|
|
196
108
|
}
|
|
197
109
|
|
|
198
110
|
if (!entry.module) {
|
|
199
|
-
// WHERE the name comes from, not a blanket "docs/vision.md". All eight are in the constitution
|
|
200
|
-
// as of 2026-08-03, so `namedIn` is unset on every entry today and the fallback is the whole
|
|
201
|
-
// behaviour — but the field stays, because the state it exists for is real and recurred once
|
|
202
|
-
// already: `feedback` was named by row 7 and by nothing else for one pull request, and the
|
|
203
|
-
// shipped tool told users the constitution named something it did not. A test asserts the
|
|
204
|
-
// sentence now, which is what that round was missing.
|
|
205
111
|
warn(`portulan: \`${name}\` is named in ${entry.namedIn ?? "docs/vision.md"} and is not built yet — it arrives at ${entry.arrives}.`);
|
|
206
112
|
warn("portulan: refusing to exit 0 on a subcommand that did nothing.");
|
|
207
113
|
return 2;
|
|
208
114
|
}
|
|
209
115
|
|
|
210
|
-
//
|
|
211
|
-
// repository: a tool that fails to parse takes down only its own subcommand rather than the
|
|
212
|
-
// whole command line, and the built modules together are large enough that eagerly importing
|
|
213
|
-
// them would make `portulan --help` pay for `doctor`.
|
|
116
|
+
// Imported on demand: a module that fails to load takes down only its own subcommand, and `--help` loads none.
|
|
214
117
|
let module;
|
|
215
118
|
try {
|
|
216
119
|
module = await load(entry.module);
|
|
@@ -220,36 +123,14 @@ export async function run(argv, options = {}) {
|
|
|
220
123
|
}
|
|
221
124
|
|
|
222
125
|
if (typeof module.run !== "function") {
|
|
223
|
-
// A guard rather than an assumption. Every built tool exports `run` today; if one stops, the
|
|
224
|
-
// failure must name itself here instead of surfacing as `module.run is not a function`.
|
|
225
126
|
warn(`portulan: ${entry.module} does not export a \`run\` function — refusing to guess at its entry point.`);
|
|
226
127
|
return 2;
|
|
227
128
|
}
|
|
228
129
|
|
|
229
|
-
// The tool's own exit code, returned unchanged. Re-mapping it here would put a second opinion
|
|
230
|
-
// about a workspace between the tool and its user.
|
|
231
130
|
return await module.run(rest);
|
|
232
131
|
}
|
|
233
132
|
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
// Every other tool here guards with a bare `pathToFileURL(process.argv[1])` comparison and is right
|
|
237
|
-
// to: nothing ever invokes them through a link. This one is a `bin`, and **npm installs a bin as a
|
|
238
|
-
// symlink** — `node_modules/.bin/portulan` → `cli/portulan.mjs`. Node realpaths the main module for
|
|
239
|
-
// `import.meta.url` while `process.argv[1]` keeps the link path, so the bare comparison is FALSE
|
|
240
|
-
// through the link and `run` never executes.
|
|
241
|
-
//
|
|
242
|
-
// Measured on the staged tree before this line existed, by packing the package and installing the
|
|
243
|
-
// tarball: through `node_modules/.bin/portulan`, `--version` printed nothing and exited 0, and
|
|
244
|
-
// `doctor <nonexistent>` exited **0** where the same command exits 1 from a checkout. **Every
|
|
245
|
-
// verdict became a silent success** — "nothing looked" reported as "nothing wrong", at the one
|
|
246
|
-
// surface a user reaches by the documented route. Found by the pre-commit checkpoint, which
|
|
247
|
-
// installed the tarball rather than reading the guard.
|
|
248
|
-
//
|
|
249
|
-
// `realpathSync` rather than `import.meta.main`, which needs a newer Node than `engines` asks for.
|
|
250
|
-
// Wrapped, because `argv[1]` may be absent or unreadable and a guard that throws would be a worse
|
|
251
|
-
// failure than the one it prevents — falling back to the unresolved path leaves the old behaviour
|
|
252
|
-
// rather than crashing.
|
|
133
|
+
// npm installs a bin as a symlink, and Node realpaths `import.meta.url` but not `argv[1]`; `import.meta.main` needs a newer Node than `engines`.
|
|
253
134
|
function isMain() {
|
|
254
135
|
const invoked = process.argv[1];
|
|
255
136
|
if (!invoked) return false;
|
package/cli/recipe-set.mjs
CHANGED
|
@@ -1,105 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
* The recipe set — the one carrier of *what this workspace runs*.
|
|
3
|
-
*
|
|
4
|
-
* Row 7's amendment of 2026-07-31 (argument in `docs/milestones/m07.md`) ruled that a composed
|
|
5
|
-
* pack's verify recipes reach the adopting workspace's runnable set, under a contract stated in the
|
|
6
|
-
* row rather than left to this file: **additive only** — a pack may add a recipe and may never
|
|
7
|
-
* redefine, remove or replace one the workspace declares, nor become the workspace's
|
|
8
|
-
* `verify.default` — **namespaced by its pack, so a collision is impossible rather than resolved**,
|
|
9
|
-
* and **could-not-run — exit 2, never silently absent** when a composed recipe cannot resolve.
|
|
10
|
-
*
|
|
11
|
-
* ## Why this is a module and not four implementations
|
|
12
|
-
*
|
|
13
|
-
* Until this landed the recipe set was read independently in four places — `.github/workflows/verify.yml`,
|
|
14
|
-
* `cli/doctor.mjs`, `cli/vendor.mjs` and `cli/stop-gate.mjs` — with none authoritative. Composing into
|
|
15
|
-
* fewer than all four would have left the rest disagreeing about what the set IS, which is
|
|
16
|
-
* `../.portulan/proposals/0020-a-fix-is-not-done-at-the-site-it-was-found.md`'s defect in its code
|
|
17
|
-
* material: one rule, several enforcement sites, repaired at fewer than all of them. The repair that
|
|
18
|
-
* proposal prescribes is `core/operating/evolution.md`'s — **one carrier, and the others reach it** —
|
|
19
|
-
* and here the rule can be a function, which is where that repair applies.
|
|
20
|
-
*
|
|
21
|
-
* The row makes CI part of the contract in its own words: *CI runs the recipe set the manifest yields*,
|
|
22
|
-
* and a composed recipe missing from it *"would be a green computed over something nobody ran"*. So
|
|
23
|
-
* `verify.yml` calls this module rather than carrying its own enumeration, and `cli/recipe-set.test.mjs`
|
|
24
|
-
* pins the roster so a fifth reader reds instead of drifting.
|
|
25
|
-
*
|
|
26
|
-
* ## What the namespace buys, and why the refusal stays anyway
|
|
27
|
-
*
|
|
28
|
-
* A workspace-declared recipe id is a bare slug (`$defs/slug`, `^[a-z0-9]+(-[a-z0-9]+)*$`) in
|
|
29
|
-
* `spec/workspace.schema.json`, and so is `verify.default`. A composed id is `<category>/<name>:<id>`,
|
|
30
|
-
* whose `/` and `:` are outside that grammar. So a composed recipe cannot carry a workspace id and
|
|
31
|
-
* cannot be named by `verify.default`: **both are impossible by construction, not refused by a check
|
|
32
|
-
* somebody has to remember.**
|
|
33
|
-
*
|
|
34
|
-
* The refusal below is kept as belt and braces behind that construction — the pattern `verify.yml`
|
|
35
|
-
* already names in its own comment — and it is not a claim to an enforcement that cannot fire, which
|
|
36
|
-
* `../.portulan/memory/a-stated-enforcer-must-be-the-real-one.md` would forbid. Two classes reach it:
|
|
37
|
-
*
|
|
38
|
-
* 1. **A pack declaring one id twice with different runs.** Schema-legal: `contributes.verify`'s
|
|
39
|
-
* `uniqueItems` compares whole recipe objects, so two entries differing only in `run` validate.
|
|
40
|
-
* The namespacer gives both the same composed id and only this refusal catches it. It is forced
|
|
41
|
-
* red in the suite.
|
|
42
|
-
* 2. **A hand-edited manifest reaching a reader that does not validate against the schema** — CI and
|
|
43
|
-
* the Stop gate both read `workspace.json` raw.
|
|
44
|
-
*
|
|
45
|
-
* ## The composed `run` contract
|
|
46
|
-
*
|
|
47
|
-
* `spec/pack.schema.json` glosses a recipe's `run` as *"the command, as it would be typed from the
|
|
48
|
-
* repository root"*. A pack has no repository until it is adopted, and its script ships inside the
|
|
49
|
-
* pack — so a composed `run` needs a way to name its own files from a root it cannot know. It does
|
|
50
|
-
* that with **`${PACK_ROOT}`**, expanded here to the resolved pack root before the command is handed
|
|
51
|
-
* to anyone. After expansion the gloss is true again, which is the point: what runs is a command
|
|
52
|
-
* typeable from the repository root.
|
|
53
|
-
*
|
|
54
|
-
* Expansion happens **before** the newline and tab check, so a pack root carrying one cannot smuggle
|
|
55
|
-
* it past a validation that ran too early. A workspace recipe's `run` is never expanded — the token
|
|
56
|
-
* has no meaning outside a pack, and rewriting it would be this module inventing a feature the
|
|
57
|
-
* Workspace Definition does not declare.
|
|
58
|
-
*/
|
|
1
|
+
// The recipe set: the verify recipes a workspace yields, its own plus each composed pack's, namespaced by pack.
|
|
59
2
|
|
|
60
3
|
import fs from "node:fs";
|
|
61
4
|
import path from "node:path";
|
|
62
5
|
import { pathToFileURL } from "node:url";
|
|
63
6
|
|
|
64
|
-
// `shadowedCopy` rides along: one carrier for *is this pack shadowed*, per #316.
|
|
65
7
|
import { resolvePack, rootPlan, shadowedCopy } from "./compile.mjs";
|
|
66
8
|
import { AUTO, discoverPackRoots, namedWithAuto } from "./discover.mjs";
|
|
67
9
|
|
|
68
|
-
/**
|
|
69
|
-
* Every reader of the recipe set, by path.
|
|
70
|
-
*
|
|
71
|
-
* **Pinned in two halves, because this list alone holds less than it looks like it holds.**
|
|
72
|
-
* `cli/recipe-set.test.mjs` asserts the roster's contents, which catches a reader being removed or
|
|
73
|
-
* renamed — but a *fifth* reader that never adds itself here touches nothing that assertion reads. So
|
|
74
|
-
* `cli/recipe-set.live.test.mjs` sweeps `cli/*.mjs` and the workflows for an undeclared enumeration of
|
|
75
|
-
* `verify.recipes`, which is the half that actually catches drift. `cli/collisions.test.mjs` carries
|
|
76
|
-
* the same pair for the three `collisions()`, and the sweep is the half it had to add for the same
|
|
77
|
-
* reason: a copy nothing asserts is invisible to every other test.
|
|
78
|
-
*
|
|
79
|
-
* Two sites enumerate `verify.recipes` directly and are exempt by argument, not by oversight: this
|
|
80
|
-
* module, and `cli/doctor.mjs`, which validates the workspace's own DECLARATION rather than reading
|
|
81
|
-
* the runnable set.
|
|
82
|
-
*/
|
|
10
|
+
/** Add any new reader here: `recipe-set.live.test.mjs` catches only a direct enumeration of `verify.recipes`. */
|
|
83
11
|
export const RECIPE_SET_READERS = Object.freeze([
|
|
84
12
|
".github/workflows/verify.yml",
|
|
85
13
|
"cli/doctor.mjs",
|
|
86
|
-
// Added 2026-08-25 with milestone 8 clause (d). `cli/drills.mjs` reads the set to know which rails
|
|
87
|
-
// it must drill — and it is the reader that shows the live sweep's own boundary: it calls
|
|
88
|
-
// `recipeSet()` rather than enumerating `verify.recipes`, so the sweep for an undeclared
|
|
89
|
-
// *enumeration* could not see it and this roster went stale in the one way the pair above was
|
|
90
|
-
// arranged not to allow. Raised as a suppressed note by Copilot, round 1 on #343.
|
|
91
14
|
"cli/drills.mjs",
|
|
15
|
+
"cli/finish.mjs",
|
|
16
|
+
"cli/form.mjs",
|
|
92
17
|
"cli/stop-gate.mjs",
|
|
93
18
|
"cli/vendor.mjs",
|
|
94
19
|
]);
|
|
95
20
|
|
|
96
|
-
/** `$defs/slug`,
|
|
21
|
+
/** `$defs/slug`, as both schemas hold it. */
|
|
97
22
|
const SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
98
23
|
|
|
99
24
|
/** The token a composed `run` uses to name its own pack's files. */
|
|
100
25
|
const PACK_ROOT_TOKEN = "${PACK_ROOT}";
|
|
101
26
|
|
|
102
|
-
/**
|
|
27
|
+
/** Its `:` is outside `$defs/slug`, so a composed id never equals a workspace id or `verify.default`. */
|
|
103
28
|
export function composedId(packRef, id) {
|
|
104
29
|
return `${packRef}:${id}`;
|
|
105
30
|
}
|
|
@@ -108,71 +33,25 @@ function refuse(reason) {
|
|
|
108
33
|
return { ok: false, exitCode: 2, reason };
|
|
109
34
|
}
|
|
110
35
|
|
|
111
|
-
/**
|
|
112
|
-
* Validate one recipe's runnable parts. Returns an error string or null.
|
|
113
|
-
*
|
|
114
|
-
* These four refusals were carried by `.github/workflows/verify.yml`'s inline emitter alone until this
|
|
115
|
-
* module took them. Losing one in the move would have been a fix not done at the site it was found,
|
|
116
|
-
* committed inside the change that cites the doctrine — so they are asserted here in the suite rather
|
|
117
|
-
* than trusted to have survived.
|
|
118
|
-
*/
|
|
36
|
+
/** An error sentence, or null when the recipe is runnable. */
|
|
119
37
|
function invalidRunnable(id, run, where, { requireSlug = true } = {}) {
|
|
120
|
-
// `requireSlug` is off only on the deliberately-bypassed path. Without it `trustPackSpelling` could
|
|
121
|
-
// not do the one thing its JSDoc says it exists for: a genuinely pre-namespaced id carries `/` and
|
|
122
|
-
// `:`, so the slug check refused it *before* the shadow refusal it was meant to reach — an option
|
|
123
|
-
// that named a purpose it could not serve. Copilot round 6.
|
|
124
38
|
if (requireSlug && !SLUG.test(String(id ?? ""))) return `${where} declares a recipe id that is not a slug: ${JSON.stringify(id)}`;
|
|
125
39
|
const command = String(run ?? "");
|
|
126
40
|
if (!command.trim()) return `${where} declares an empty run command for recipe \`${id}\``;
|
|
127
|
-
// Names all three characters it rejects. It said "newline or tab" while the pattern also refused a
|
|
128
|
-
// carriage return, so a manifest carrying one got a message pointing at two characters it did not
|
|
129
|
-
// contain — a diagnosis that sends the reader looking for the wrong thing. Copilot round 1.
|
|
130
41
|
if (/[\n\r\t]/.test(command)) {
|
|
131
42
|
return `${where} declares a run containing a newline, carriage return or tab for recipe \`${id}\``;
|
|
132
43
|
}
|
|
133
44
|
return null;
|
|
134
45
|
}
|
|
135
46
|
|
|
136
|
-
/**
|
|
137
|
-
* The composed runnable set for a workspace manifest.
|
|
138
|
-
*
|
|
139
|
-
* @param {object} manifest a parsed Workspace Definition
|
|
140
|
-
* @param {object} [options]
|
|
141
|
-
* @param {string[]} [options.packs] the workspace's `packs` array; defaults to `manifest.packs`
|
|
142
|
-
* @param {(ref: string) => ({ ref: string, root: string, manifest: object } | null)} [options.resolve]
|
|
143
|
-
* resolves a pack reference to its root and manifest, or null when it cannot be found.
|
|
144
|
-
* Absent when packs are declared means nothing resolves, which is could-not-run — fail-closed,
|
|
145
|
-
* because a resolver that silently is not there would compose nothing and report green.
|
|
146
|
-
* @param {boolean} [options.trustPackSpelling] use a pack's own recipe id verbatim instead of
|
|
147
|
-
* namespacing it. Exists so the shadow refusal can be exercised against a real input; no
|
|
148
|
-
* production caller passes it.
|
|
149
|
-
*
|
|
150
|
-
* @returns {{ ok: true, recipes: object[], default: string|undefined }
|
|
151
|
-
* | { ok: false, exitCode: 2, reason: string }}
|
|
152
|
-
*/
|
|
47
|
+
/** `{ ok: true, recipes, default }`, or `{ ok: false, exitCode: 2, reason }` when the set cannot be composed. */
|
|
153
48
|
export function recipeSet(manifest, options = {}) {
|
|
154
49
|
const { packs = manifest?.packs ?? [], resolve = null, trustPackSpelling = false } = options;
|
|
155
50
|
|
|
156
|
-
// `packs` must be an array, for the same reason `verify.recipes` must be — and this is that fix's
|
|
157
|
-
// sibling, at the site round 1 did not reach. A string here would iterate CHARACTERS and report
|
|
158
|
-
// "the pack `t` could not be resolved", a refusal whose message names something the manifest never
|
|
159
|
-
// said; an object would throw a TypeError, which is a stack trace rather than a diagnosis. Both are
|
|
160
|
-
// could-not-run wearing a worse face. Copilot round 3.
|
|
161
51
|
if (!Array.isArray(packs)) {
|
|
162
52
|
return refuse(`this workspace's \`packs\` is not an array (${typeof packs}) — could-not-run rather than a set composed from a value nothing can enumerate`);
|
|
163
53
|
}
|
|
164
54
|
|
|
165
|
-
// A manifest with no `verify.recipes` ARRAY is could-not-run, not a workspace that declares none.
|
|
166
|
-
//
|
|
167
|
-
// This was `?? []`, and that was a fail-open the move introduced: the CI emitter this carrier
|
|
168
|
-
// replaced iterated `m.verify.recipes` directly, so an absent block threw and became exit 2. Under
|
|
169
|
-
// `?? []` an invalid manifest could still yield a runnable set as soon as a pack contributed one —
|
|
170
|
-
// CI would then run a green over a workspace whose own declaration nobody could read. One of the
|
|
171
|
-
// four validations that was supposed to survive the move, and the one that did not; the pre-commit
|
|
172
|
-
// checkpoint asked the question and neither pass caught it. Copilot round 1's suppressed note did.
|
|
173
|
-
//
|
|
174
|
-
// Absent is distinguishable from empty here only in the message: both refuse, because `spec/workspace.schema.json`
|
|
175
|
-
// requires the block and a workspace that declares no recipes cannot report green either way.
|
|
176
55
|
if (!Array.isArray(manifest?.verify?.recipes)) {
|
|
177
56
|
return refuse(
|
|
178
57
|
"the workspace manifest declares no `verify.recipes` array — could-not-run rather than a set " +
|
|
@@ -186,14 +65,7 @@ export function recipeSet(manifest, options = {}) {
|
|
|
186
65
|
for (const recipe of declared) {
|
|
187
66
|
const bad = invalidRunnable(recipe?.id, recipe?.run, "the workspace manifest");
|
|
188
67
|
if (bad) return refuse(bad);
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
// The validation above coerces with `String(...)`, so a non-string `id` that stringifies to a
|
|
192
|
-
// slug PASSED and was then pushed through unchanged — and every downstream comparison is an
|
|
193
|
-
// identity check on that value: `r.id === set.default` in the Stop gate, `seen.has(id)` here,
|
|
194
|
-
// the emitter's tab-separated line. One arm of this function stringified and the other did not,
|
|
195
|
-
// which is the two-sites-one-rule shape this whole change is about, inside the carrier that
|
|
196
|
-
// carries it. Copilot round 2's suppressed note.
|
|
68
|
+
// As strings: validation coerced them with `String`, and every reader compares ids with `===`.
|
|
197
69
|
const id = String(recipe.id);
|
|
198
70
|
const run = String(recipe.run);
|
|
199
71
|
if (seen.has(id)) return refuse(`the workspace manifest declares the recipe \`${id}\` more than once`);
|
|
@@ -204,23 +76,13 @@ export function recipeSet(manifest, options = {}) {
|
|
|
204
76
|
for (const ref of packs) {
|
|
205
77
|
const found = resolve ? resolve(ref) : null;
|
|
206
78
|
if (!found) {
|
|
207
|
-
// Never silently absent. The row's words: exit 2, and the run says which pack — and which
|
|
208
|
-
// recipe, where one is known. A composed recipe quietly missing from the set would be a
|
|
209
|
-
// green computed over something nobody ran, arriving through the composition path.
|
|
210
79
|
return refuse(
|
|
211
80
|
`the pack \`${ref}\` is composed by this workspace and could not be resolved, so its verify recipes ` +
|
|
212
81
|
`could not be read — could-not-run rather than a set that quietly lost them`,
|
|
213
82
|
);
|
|
214
83
|
}
|
|
215
84
|
|
|
216
|
-
//
|
|
217
|
-
// `packs/rituals/checkpoints`'s deliberate shape, by its own argued policy, and conflating
|
|
218
|
-
// "declared none" with "failed to resolve" would make the honest pack red.
|
|
219
|
-
//
|
|
220
|
-
// But a key that is PRESENT and not an array is neither of those: it is a manifest nobody can
|
|
221
|
-
// read, and `for..of` over it throws a TypeError — the carrier crashing where its whole
|
|
222
|
-
// contract is to exit 2 with a sentence naming the pack. Third site of round 1's rule, and the
|
|
223
|
-
// second one it did not reach. Copilot round 3.
|
|
85
|
+
// An absent `contributes.verify` is a pack that contributes no recipes, not an error.
|
|
224
86
|
const contributed = found.manifest?.contributes?.verify;
|
|
225
87
|
if (contributed !== undefined && !Array.isArray(contributed)) {
|
|
226
88
|
return refuse(`the pack \`${ref}\` declares \`contributes.verify\` as ${typeof contributed}, not an array — could-not-run`);
|
|
@@ -250,79 +112,26 @@ export function recipeSet(manifest, options = {}) {
|
|
|
250
112
|
}
|
|
251
113
|
}
|
|
252
114
|
|
|
253
|
-
// Reporting green having run nothing is the failure the whole gate exists to prevent, and
|
|
254
|
-
// enumeration is a precondition exactly as it is inside each recipe —
|
|
255
|
-
// `../.portulan/memory/verify-preconditions-fail-closed.md`.
|
|
256
115
|
if (recipes.length === 0) return refuse("the manifest yields no verify recipes — refusing to report green");
|
|
257
116
|
|
|
258
|
-
// `default` is normalised the same way the ids are, and this is round 2's own fix's sibling.
|
|
259
|
-
//
|
|
260
|
-
// Round 2 stringified every recipe's `id` on the way out and left `default` as whatever the
|
|
261
|
-
// manifest held. Every downstream lookup is an identity comparison — `r.id === set.default` in
|
|
262
|
-
// `cli/stop-gate.mjs` — so a non-string default that used to match a non-string id stopped matching
|
|
263
|
-
// the moment one side was normalised and the other was not. A repair that reached one of the two
|
|
264
|
-
// values it had to reach, in the change whose subject is exactly that. Copilot round 6.
|
|
265
117
|
const declaredDefault = manifest?.verify?.default;
|
|
266
118
|
return { ok: true, recipes, default: declaredDefault == null ? declaredDefault : String(declaredDefault) };
|
|
267
119
|
}
|
|
268
120
|
|
|
269
|
-
/**
|
|
270
|
-
* A resolver over a workspace's declared pack roots, for callers that actually compose.
|
|
271
|
-
*
|
|
272
|
-
* Reaches `compile.mjs`'s `rootPlan` and `resolvePack` rather than re-deriving where a pack lives —
|
|
273
|
-
* the precedence rule (**a named root wins outright; otherwise discovery is unioned with the derived
|
|
274
|
-
* root, discovered first**) lives once, in `resolutionRoots`, and a second derivation here would be a
|
|
275
|
-
* second carrier of the fact `cli/discover.mjs` was built to hold. _(It read "asked-for discovery" until
|
|
276
|
-
* 2026-08-13; the unasked arm consults a wired thunk too, and what asking still changes is the degrade —
|
|
277
|
-
* asked-and-could-not-look is exit 2, unasked-and-could-not-look keeps the derived root.)_ The pack root handed to
|
|
278
|
-
* `${PACK_ROOT}` is made **relative to the repository root**, because that is the root a recipe's
|
|
279
|
-
* `run` is typed from.
|
|
280
|
-
*
|
|
281
|
-
* **`forced` rides beside `discovery` because without it the pair WAS dead plumbing.** This function
|
|
282
|
-
* accepted `discovery` and passed it on while `resolutionRoots` consulted it only under `forced`, so no
|
|
283
|
-
* caller could ever have reached discovery through here — the parameter looked wired and was not. Both
|
|
284
|
-
* are still threaded and `forced` now carries less: since 2026-08-13 a wired `discovery` is consulted
|
|
285
|
-
* either way, and `forced` selects the strict degrade. A caller passing neither stays hermetic, which is
|
|
286
|
-
* the property `recipe-set.test.mjs` pins — and it is the reason `stop-gate`, which passes `packs: []`,
|
|
287
|
-
* still reaches no host at all.
|
|
288
|
-
*/
|
|
121
|
+
/** The `resolve` that `recipeSet` takes; throws on a refusal, a could-not-look or a shadowed pack. */
|
|
289
122
|
export function resolverFor({ workspaceDir, manifest, repoRoot = ".", named = [], discovery = null, forced = false }) {
|
|
290
123
|
const plan = rootPlan(workspaceDir, manifest, { named, discovery, forced });
|
|
291
|
-
// A refusal must not arrive as an empty root set. That is the silent drop this change exists to
|
|
292
|
-
// remove, and the first cut of this function reintroduced it one layer down — an API caller
|
|
293
|
-
// passing `named` beside `forced: true` would have resolved nothing and been told nothing.
|
|
294
|
-
// Raised by Copilot, round 1 on #233; swept to `skills-set.mjs`'s sibling in the same stroke.
|
|
295
124
|
if (plan.refusal) throw new Error(`recipe-set: ${plan.refusal}`);
|
|
296
125
|
if (plan.couldNotRun) throw new Error(`recipe-set: ${plan.couldNotRun}`);
|
|
297
126
|
const roots = plan.roots ?? [];
|
|
298
127
|
const originAt = (r) => (plan.origins ?? []).find((o) => path.resolve(o.root) === path.resolve(r))?.origin;
|
|
299
128
|
|
|
300
|
-
//
|
|
301
|
-
// three. `compile` refuses even an agreeing shadow because its artifact records which root
|
|
302
|
-
// answered; `index` refuses one although its artifact deliberately does not. Here the answering
|
|
303
|
-
// root is not merely recorded — **it is spliced into the output**: `${PACK_ROOT}` expands to the
|
|
304
|
-
// root that answered, so two copies whose manifests are byte-identical still yield different `run`
|
|
305
|
-
// lines pointing at different files. What CI runs and what a bare local run runs would diverge,
|
|
306
|
-
// with nothing in either output to say so.
|
|
307
|
-
//
|
|
308
|
-
// So the agreeing case is not a carve-out anyone could argue for here, and the honest predicate was
|
|
309
|
-
// never manifest comparison: it is output equality, computable only by resolving both worlds.
|
|
310
|
-
// Refusing the ambiguity is what remains.
|
|
311
|
-
//
|
|
312
|
-
// **Checked HERE rather than inside the returned closure**, which is where a first cut put it. The
|
|
313
|
-
// closure runs during `recipeSet`, outside the `try` that wraps this constructor — so the refusal
|
|
314
|
-
// escaped as an uncaught throw, exit 1 with a stack trace, in a tool whose whole contract is exit 2
|
|
315
|
-
// for could-not-run. Measured, not reasoned about. Throwing from the constructor puts it on the
|
|
316
|
-
// path `plan.refusal` already uses and the caller already catches.
|
|
317
|
-
//
|
|
318
|
-
// **Only where the caller did not choose** — a named root replaces the derived one, `auto` is
|
|
319
|
-
// discovery elected — which is `compile`'s guard shape, bound to it deliberately.
|
|
129
|
+
// Checked here, not in the returned closure: that runs inside `recipeSet`, beyond the caller's `try`.
|
|
320
130
|
if (!forced && plan.source !== "named") {
|
|
321
131
|
const declared = Array.isArray(manifest?.packs) ? manifest.packs : [];
|
|
322
132
|
for (const ref of declared) {
|
|
323
133
|
const found = resolvePack(String(ref), roots);
|
|
324
134
|
if (!found?.dir) continue;
|
|
325
|
-
// Path-resolved, matching `compile` — see the sibling note in `index.mjs`.
|
|
326
135
|
const behind = shadowedCopy(String(ref), originAt(found.root), roots, originAt);
|
|
327
136
|
if (!behind) continue;
|
|
328
137
|
throw new Error(
|
|
@@ -337,20 +146,12 @@ export function resolverFor({ workspaceDir, manifest, repoRoot = ".", named = []
|
|
|
337
146
|
|
|
338
147
|
return (ref) => {
|
|
339
148
|
const found = resolvePack(ref, roots);
|
|
340
|
-
// `resolvePack` returns `manifest` as the PATH to `pack.json`, not as the parsed object —
|
|
341
|
-
// reading it is this adapter's job. The first cut of this function treated it as parsed, and
|
|
342
|
-
// every pack then contributed nothing while the suite stayed green, because the fixture
|
|
343
|
-
// resolver in `recipe-set.test.mjs` handed back a parsed object and so inherited the same
|
|
344
|
-
// assumption the code made. Caught by running the emitter against this repository rather than
|
|
345
|
-
// against its own fixtures; `cli/recipe-set.live.test.mjs` is what stops it coming back.
|
|
346
149
|
if (!found?.dir || !found.manifest) return null;
|
|
347
150
|
let parsed;
|
|
348
151
|
try {
|
|
349
152
|
parsed = JSON.parse(fs.readFileSync(found.manifest, "utf8"));
|
|
350
153
|
} catch {
|
|
351
|
-
//
|
|
352
|
-
// Returning null here makes it could-not-run at the caller, which is the honest answer —
|
|
353
|
-
// only absence is absence.
|
|
154
|
+
// Unreadable is not absent: null makes it could-not-run, not a pack with no recipes.
|
|
354
155
|
return null;
|
|
355
156
|
}
|
|
356
157
|
const rel = path.relative(path.resolve(repoRoot), path.resolve(found.dir));
|
|
@@ -358,22 +159,8 @@ export function resolverFor({ workspaceDir, manifest, repoRoot = ".", named = []
|
|
|
358
159
|
};
|
|
359
160
|
}
|
|
360
161
|
|
|
361
|
-
/**
|
|
362
|
-
* The emitter CI runs: one `id<TAB>run` line per recipe in the composed set, exit 2 if the set cannot
|
|
363
|
-
* be produced.
|
|
364
|
-
*
|
|
365
|
-
* `.github/workflows/verify.yml` carried this as an inline `node -e` until milestone 7's composition
|
|
366
|
-
* amendment. It is here now because the row puts CI inside the contract — *CI runs the recipe set the
|
|
367
|
-
* manifest yields* — so a workflow computing its own set would be the required check reporting green
|
|
368
|
-
* over a recipe nobody ran, which is the false green the row names in words.
|
|
369
|
-
*/
|
|
162
|
+
/** CI's emitter: one `id<TAB>run` line per recipe in the set; exit 2 when the set cannot be produced. */
|
|
370
163
|
export function run(argv = [], { stdout = process.stdout, stderr = process.stderr } = {}) {
|
|
371
|
-
// **Three refusals, not one.** `init`, `new` and `vendor` each refuse a missing value, a FLAG as a
|
|
372
|
-
// value, and an empty one; session 9 recorded a fourth parser taking only the first, where
|
|
373
|
-
// `--tree --write <dir>` swallowed the write and silently reported instead of migrating. This was
|
|
374
|
-
// the fifth: `argv.indexOf` plus a truthiness test meant `--workspace --pack-root packs` bound the
|
|
375
|
-
// workspace to the string `--pack-root`, and the run then failed with a misleading *manifest could
|
|
376
|
-
// not be read* about a directory nobody asked for. Copilot, round 5 on #236.
|
|
377
164
|
let argError = null;
|
|
378
165
|
const arg = (flag, fallback) => {
|
|
379
166
|
const i = argv.indexOf(flag);
|
|
@@ -391,12 +178,6 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
|
|
|
391
178
|
return 2;
|
|
392
179
|
}
|
|
393
180
|
|
|
394
|
-
// `--pack-root`, repeatable, with `auto` and the shared refusal — the same surface the other six
|
|
395
|
-
// tools take (`compile`, `doctor`, `index`, `init`, `vendor`, `skills-set`). It was missing here, and the plumbing behind it (`resolverFor`'s `discovery` and
|
|
396
|
-
// `forced`) existed with no way to reach it: a capability that looked wired and was not, which is
|
|
397
|
-
// the defect this file's neighbour `skills-set` was caught with a day earlier. CI calls this
|
|
398
|
-
// command for the recipe set it runs, so without the flag a workspace composing from the host
|
|
399
|
-
// cache had no invocation that could enumerate its own recipes.
|
|
400
181
|
const named = [];
|
|
401
182
|
let forced = false;
|
|
402
183
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -412,17 +193,7 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
|
|
|
412
193
|
forced = true;
|
|
413
194
|
continue;
|
|
414
195
|
}
|
|
415
|
-
//
|
|
416
|
-
// and reporting it as the latter sends an author to the one file that is not at fault. A FILE
|
|
417
|
-
// is the sharper case: `resolvePack` only tests for `pack.json` under the root, so a file-valued
|
|
418
|
-
// root yields "unresolvable" for every pack while the argument is what was wrong. Fourth carrier
|
|
419
|
-
// of a rule `compile`, `doctor` and `index` already hold; raised by Copilot, round 1 on #236,
|
|
420
|
-
// as an inconsistency with them.
|
|
421
|
-
// **The RAW value is stat'd, and resolution happens after.** `path.resolve("")` is the current
|
|
422
|
-
// working directory, so resolving first would let `--pack-root ""` pass the directory check and
|
|
423
|
-
// silently adopt the cwd as a pack root — the empty-value refusal this file gained one round
|
|
424
|
-
// earlier for `--workspace`, still missing here. `doctor`'s `directoryRoot` stats the raw value
|
|
425
|
-
// for exactly this reason. Copilot, round 6 on #236.
|
|
196
|
+
// The raw value is stat'd: `path.resolve("")` is the cwd, so resolving first would pass an empty root.
|
|
426
197
|
let rootStat = null;
|
|
427
198
|
try {
|
|
428
199
|
rootStat = fs.statSync(value);
|
|
@@ -454,7 +225,6 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
|
|
|
454
225
|
try {
|
|
455
226
|
resolve = resolverFor({ workspaceDir, manifest, repoRoot, named, discovery: () => discoverPackRoots(), forced });
|
|
456
227
|
} catch (err) {
|
|
457
|
-
// `resolverFor` throws on a refusal or a could-not-look; both are exit 2 rather than a set.
|
|
458
228
|
stderr.write(`${err.message}\n`);
|
|
459
229
|
return 2;
|
|
460
230
|
}
|