@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/skill-goldens.mjs
CHANGED
|
@@ -1,111 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// The core-skill golden corpus — every mandate a core skill states, bound to the live artifacts it
|
|
3
|
-
// governs, and graded.
|
|
4
|
-
//
|
|
5
|
-
// Milestone 8, the row's **original first subject**: *Golden tasks per core skill.* Untouched by either
|
|
6
|
-
// amendment — the 2026-07-28 expansion says it "stands exactly as written", and clause (a) *widened its
|
|
7
|
-
// subject* to reach the gates "as well as the skills" without narrowing the subject it already had
|
|
8
|
-
// (`../docs/milestones/m08.md` line 18). The gates half landed at (a) as `./goldens.mjs`; this is the
|
|
9
|
-
// half that sentence always named and nobody had built.
|
|
10
|
-
//
|
|
11
|
-
// ## What a golden task IS here, and it was RULED rather than derived
|
|
12
|
-
//
|
|
13
|
-
// The criterion reads two ways — a deterministic corpus, or task specifications run against a live
|
|
14
|
-
// model — and `../.portulan/gate-map.md`'s *"Session-open runs `clarify` against the milestone row
|
|
15
|
-
// itself"* exists because milestone 4 guessed at exactly this shape of ambiguity and cost a
|
|
16
|
-
// session-blocking question. So it was put to the maintainer instead of argued. **His ruling: a
|
|
17
|
-
// deterministic corpus graded against LIVE ARTIFACTS in the tree.** The model-run sense belongs to the
|
|
18
|
-
// A/B clause, which is separately named in the same row.
|
|
19
|
-
//
|
|
20
|
-
// ## The object: a mandate, its live artifacts, and the carrier that enforces it
|
|
21
|
-
//
|
|
22
|
-
// A case binds one **numbered step** of one skill's `## The pass` to the artifact set it governs, and
|
|
23
|
-
// records which existing rail enforces it — or `null`, which is the interesting value.
|
|
24
|
-
//
|
|
25
|
-
// Four properties, and every one of them exists because a checkpoint found the version without it:
|
|
26
|
-
//
|
|
27
|
-
// 1. **The denominator is DERIVED, not chosen.** The runner enumerates the numbered steps under each
|
|
28
|
-
// skill's `## The pass` heading and requires every one to be either `bound` or `unbindable`. A
|
|
29
|
-
// fifteenth step is red. The first draft let the author pick which mandates to write cases for,
|
|
30
|
-
// and a census over a set its own author drew reports "5 of 5" and means nothing — the difference
|
|
31
|
-
// `./goldens.mjs` already draws by deriving its denominator from the yielded policy.
|
|
32
|
-
// 2. **The heading is PREFIX-matched, and an empty step list is could-not-run.** Three spellings exist
|
|
33
|
-
// — `## The pass`, `## The pass (bounded)`, `## The pass, in order` — so an exact match finds one
|
|
34
|
-
// skill of three and the other two get an empty denominator that satisfies "every step is bound"
|
|
35
|
-
// vacuously. That is `../evals/README.md`'s own sentence about clause (d): *"a check whose
|
|
36
|
-
// enumeration went empty … reports green and has stopped being a rail."* The match is also anchored
|
|
37
|
-
// at `The pass`, because a loose `/pass/` grabs `consolidate`'s `## The one move this pass may not
|
|
38
|
-
// make`.
|
|
39
|
-
// 3. **The quote must anchor EXACTLY ONCE** in the skill's own file, or the case is could-not-run —
|
|
40
|
-
// `./mutants.mjs`'s discipline, adopted rather than re-derived. A skill whose wording is reworded,
|
|
41
|
-
// softened or deleted reddens instead of drifting, and nothing in this tree did that before.
|
|
42
|
-
// 4. **The predicates use the carriers' own exported functions rather than re-spelling them.** This
|
|
43
|
-
// module imports `RETIRE_WHEN` and `parseProvenance` from `./doctor.mjs`, so the retire-when check
|
|
44
|
-
// is `doctor`'s regex and not a second one.
|
|
45
|
-
//
|
|
46
|
-
// **What that does NOT do, stated because three carriers of this file's prose once said it did:**
|
|
47
|
-
// it does not verify the `carrier` field. Nothing links `c.carrier.symbol` to an import — the
|
|
48
|
-
// pre-commit checkpoint rewrote every carrier in a corpus file to a module that does not exist and
|
|
49
|
-
// the corpus stayed green, and moved `.portulan/verify/index.sh` out of the tree with the same
|
|
50
|
-
// result. `carrier` is a **declared, reviewed field**: it records which rail a mandate's enforcement
|
|
51
|
-
// lives in, for the census, and a reviewer is what checks it. And a carrier that deleted an export
|
|
52
|
-
// would fail this module's own import with a stack trace and exit **1**, not the 2 the earlier
|
|
53
|
-
// sentence promised.
|
|
54
|
-
//
|
|
55
|
-
// ## What the carrier field does NOT establish, said here because a census implies more than it means
|
|
56
|
-
//
|
|
57
|
-
// An import proves the carrier **contains** the check. It does not prove the carrier **runs** it: a
|
|
58
|
-
// rail could keep `RETIRE_WHEN` exported, stop calling it, and this corpus would not notice, because
|
|
59
|
-
// `expect` is computed here over the artifacts rather than by asking the rail. Closing that needs a
|
|
60
|
-
// per-mandate drill — perturb a record, require the named rail to fire with its own tell — which is
|
|
61
|
-
// `./drills.mjs`'s shape at a finer grain and a second clause's work.
|
|
62
|
-
//
|
|
63
|
-
// ## `unbindable` is adjudicated, never asserted
|
|
64
|
-
//
|
|
65
|
-
// The dodge is obvious: call a mandate unbindable and it needs no case. So the reason takes a **closed
|
|
66
|
-
// vocabulary** rather than free prose, on `./mutants.mjs`'s rule that a `survives` record is admissible
|
|
67
|
-
// only as a proof and never as a standing note that a gap exists:
|
|
68
|
-
//
|
|
69
|
-
// * `judgement-only` — the mandate is about a human or agent decision and no artifact can witness it
|
|
70
|
-
// ("Get the answers from the human"). **Such a step may name no artifact path**:
|
|
71
|
-
// if you can name the artifacts, it is not judgement, it is unbuilt.
|
|
72
|
-
// * `no-artifact` — the mandate governs something that leaves no trace in this tree.
|
|
73
|
-
// * `cross-language` — a carrier exists but in another language, so binding it here would put two
|
|
74
|
-
// spellings of one rule across a language boundary.
|
|
75
|
-
// * `already-carried` — a carrier exists IN THIS LANGUAGE and already holds the mandate, so a case
|
|
76
|
-
// here would be a third carrier of a rail that is already drilled. Added at the
|
|
77
|
-
// pre-commit checkpoint, which found `consolidate` step 5 classed `bound` with
|
|
78
|
-
// a carrier the runner never reads: deleting `.portulan/verify/index.sh` left
|
|
79
|
-
// the corpus green. The row was inflating the headline, and the vocabulary had
|
|
80
|
-
// no honest term for what it actually was.
|
|
81
|
-
//
|
|
82
|
-
// **And the ratio is a finding, not bookkeeping.** How much of a core skill is artifact discipline and
|
|
83
|
-
// how much is agent judgement is a measurement about the engine, which is what this row exists to
|
|
84
|
-
// produce. It is printed as such.
|
|
2
|
+
// The core-skill golden corpus — every mandate a core skill states, bound to the live artifacts it governs, and graded.
|
|
85
3
|
|
|
86
4
|
import fs from "node:fs";
|
|
87
5
|
import path from "node:path";
|
|
88
6
|
import { pathToFileURL } from "node:url";
|
|
89
7
|
|
|
90
|
-
// The carriers,
|
|
91
|
-
// fails this module at load, which is the strongest form of the check available for free.
|
|
8
|
+
// The carriers' own exports, never a second spelling of their checks.
|
|
92
9
|
import { RETIRE_WHEN, parseProvenance } from "./doctor.mjs";
|
|
93
10
|
|
|
94
11
|
export const CASE_KINDS = ["load-bearing", "census"];
|
|
95
12
|
export const UNBINDABLE_REASONS = ["judgement-only", "no-artifact", "cross-language", "already-carried"];
|
|
96
13
|
|
|
97
|
-
//
|
|
14
|
+
// Prefix-matched, as three spellings exist; anchored, as a bare `pass` matches another `consolidate` heading.
|
|
98
15
|
const PASS_HEADING = /^##\s+The pass\b/i;
|
|
99
|
-
//
|
|
100
|
-
// `/^(\d+)\.\s+\*\*(.+?)\*\*/`, which made an UNBOLDED step invisible: the pre-commit checkpoint
|
|
101
|
-
// added `5. A brand new fifth mandate, not bolded` to a pass and the corpus stayed green at 5 of 15.
|
|
102
|
-
// The empty-enumeration guard catches the all-or-nothing case; this catches the incremental one, which
|
|
103
|
-
// is the realistic one.
|
|
16
|
+
// On the number alone, the bold read separately, so an unbolded step still counts.
|
|
104
17
|
const STEP = /^(\d+)\.\s+(.*)$/;
|
|
105
18
|
|
|
106
19
|
export class CouldNotRun extends Error {}
|
|
107
20
|
|
|
108
|
-
/** Every core skill in the engine, derived from the tree. A skill with no corpus file is a finding. */
|
|
109
21
|
export function skillSet(repoRoot) {
|
|
110
22
|
const dir = path.join(repoRoot, "core/skills");
|
|
111
23
|
let entries;
|
|
@@ -122,7 +34,6 @@ export function skillSet(repoRoot) {
|
|
|
122
34
|
return skills;
|
|
123
35
|
}
|
|
124
36
|
|
|
125
|
-
/** The numbered steps under a skill's `## The pass`. Zero is could-not-run, never a vacuous green. */
|
|
126
37
|
export function passSteps(repoRoot, skill) {
|
|
127
38
|
const file = path.join(repoRoot, "core/skills", skill, "SKILL.md");
|
|
128
39
|
let text;
|
|
@@ -154,7 +65,6 @@ export function passSteps(repoRoot, skill) {
|
|
|
154
65
|
return steps;
|
|
155
66
|
}
|
|
156
67
|
|
|
157
|
-
/** A quote must place exactly once in the skill's own file, or the case cannot be graded. */
|
|
158
68
|
export function anchorQuote(repoRoot, skill, quote) {
|
|
159
69
|
const text = fs.readFileSync(path.join(repoRoot, "core/skills", skill, "SKILL.md"), "utf8");
|
|
160
70
|
let count = 0;
|
|
@@ -171,11 +81,6 @@ export function anchorQuote(repoRoot, skill, quote) {
|
|
|
171
81
|
}
|
|
172
82
|
}
|
|
173
83
|
|
|
174
|
-
/**
|
|
175
|
-
* The artifact set a case grades, resolved through the workspace's DECLARED slots rather than a
|
|
176
|
-
* hard-coded path. A slot rename must not silently empty a denominator, and an empty match is
|
|
177
|
-
* could-not-run for the same reason a zero-step list is.
|
|
178
|
-
*/
|
|
179
84
|
export function slotFiles(workspaceDir, manifest, slot) {
|
|
180
85
|
const rel = manifest?.slots?.[slot];
|
|
181
86
|
if (typeof rel !== "string" || rel.length === 0) {
|
|
@@ -196,11 +101,9 @@ export function slotFiles(workspaceDir, manifest, slot) {
|
|
|
196
101
|
}
|
|
197
102
|
|
|
198
103
|
// ---------------------------------------------------------------------------------------------
|
|
199
|
-
// The predicates
|
|
200
|
-
// how both of this session's opening passes produced a different number for the same question.
|
|
104
|
+
// The predicates
|
|
201
105
|
// ---------------------------------------------------------------------------------------------
|
|
202
106
|
|
|
203
|
-
/** A bolded field's block: the `**Field.**` line, then lines until the next bolded field or heading. */
|
|
204
107
|
export function fieldBlock(text, field) {
|
|
205
108
|
const lines = text.split("\n");
|
|
206
109
|
const start = lines.findIndex((l) => new RegExp(`^\\s*\\*\\*${field}\\.?\\*\\*`, "i").test(l));
|
|
@@ -213,7 +116,6 @@ export function fieldBlock(text, field) {
|
|
|
213
116
|
return out.join("\n");
|
|
214
117
|
}
|
|
215
118
|
|
|
216
|
-
/** Bullets with wrapped continuation lines rejoined — a wrapped criterion is one criterion. */
|
|
217
119
|
export function bulletsOf(block) {
|
|
218
120
|
const out = [];
|
|
219
121
|
for (const line of block.split("\n")) {
|
|
@@ -223,17 +125,7 @@ export function bulletsOf(block) {
|
|
|
223
125
|
return out.filter(Boolean);
|
|
224
126
|
}
|
|
225
127
|
|
|
226
|
-
// EARS as `core/templates/task.md` states it
|
|
227
|
-
// template is the rule and this is a reduction of it, cited rather than re-spelled.
|
|
228
|
-
//
|
|
229
|
-
// **Three carriers state the shape** — that template, `core/operating/operating`-adjacent
|
|
230
|
-
// `core/operating/verification.md`, and `core/skills/clarify/SKILL.md` — all `core/` at tier `propose`.
|
|
231
|
-
// Reducing them to one is deferred on **budget** and on a real design question (making a CLI module the
|
|
232
|
-
// carrier that core doctrine cites inverts the cascade), filed as
|
|
233
|
-
// https://github.com/sleepy-panda-srl/portulan/issues/359. _(An earlier version of this comment said
|
|
234
|
-
// there were four, one of them `docs/vision.md` at tier `prohibited`, and concluded the reduction was
|
|
235
|
-
// impossible to an agent. `docs/vision.md` names EARS in a comparison-table row and nowhere states the
|
|
236
|
-
// shape; the count and the conclusion were both wrong, and the pre-commit checkpoint measured it.)_
|
|
128
|
+
// A reduction of EARS as `core/templates/task.md` states it; the template is the rule.
|
|
237
129
|
export const isEars = (b) => /\bwhen\b/i.test(b) && /\bshall\b/i.test(b);
|
|
238
130
|
|
|
239
131
|
export const PREDICATES = {
|
|
@@ -247,25 +139,15 @@ export const PREDICATES = {
|
|
|
247
139
|
? { ok: true, total: bs.length }
|
|
248
140
|
: { ok: false, why: `${bad.length} of ${bs.length} criteria are not EARS-shaped`, total: bs.length };
|
|
249
141
|
},
|
|
250
|
-
// `codify` step 3 — *"Attach how it earns its place"*. core/templates/proposal.md mandates an
|
|
251
|
-
// **Enforcement.** field, and nothing checks it. The pre-commit checkpoint found this step classed
|
|
252
|
-
// judgement-only on the argument that presence-checking would be "the presence floor with none of
|
|
253
|
-
// the value" — an argument this module refutes in its own printed output, and which equally refutes
|
|
254
|
-
// the provenance binding it did ship. Same spelling tolerance, same reason.
|
|
255
142
|
"enforcement-present": (text) => {
|
|
256
143
|
const has = /^\s*\*\*Enforcement\.?\*\*/im.test(text) || /^##+\s+Enforcement/im.test(text);
|
|
257
144
|
return has ? { ok: true } : { ok: false, why: "no enforcement field in any spelling" };
|
|
258
145
|
},
|
|
259
|
-
// Any spelling of a provenance field. Deliberately shape-tolerant: the tree carries `**Provenance.**`
|
|
260
|
-
// and `## Provenance` and a literal predicate would report the renamed ones as absent — the false-red
|
|
261
|
-
// this session's own opening pass produced once against the task corpus.
|
|
262
146
|
"provenance-present": (text) => {
|
|
263
147
|
const has = /^\s*\*\*Provenance\.?\*\*/im.test(text) || /^##+\s+Provenance/im.test(text);
|
|
264
148
|
return has ? { ok: true } : { ok: false, why: "no provenance field in any spelling" };
|
|
265
149
|
},
|
|
266
|
-
//
|
|
267
|
-
// is the scope the mandate has and the scope `doctor` enforces at — a `decision` carrying prose
|
|
268
|
-
// provenance is reported by `doctor` and is not a violation of this mandate.
|
|
150
|
+
// Scoped to `type: rule`, the scope `doctor` enforces the two forms at.
|
|
269
151
|
"provenance-two-form": (text) => {
|
|
270
152
|
const type = (/^\s*\*\*type:\*\*\s*(\w+)/im.exec(text) || [])[1];
|
|
271
153
|
if (type !== "rule") return { ok: true, skipped: "not a rule" };
|
|
@@ -276,11 +158,6 @@ export const PREDICATES = {
|
|
|
276
158
|
RETIRE_WHEN.test(text) ? { ok: true } : { ok: false, why: "no **Retire when:** line" },
|
|
277
159
|
};
|
|
278
160
|
|
|
279
|
-
/**
|
|
280
|
-
* The containment case: every budget id the Workspace Definition declares must be named in the
|
|
281
|
-
* routing sentence `consolidate` states. Derived from the schema on both sides — a fifth budget, or a
|
|
282
|
-
* rename, reddens rather than drifting.
|
|
283
|
-
*/
|
|
284
161
|
export function budgetIds(repoRoot) {
|
|
285
162
|
const schema = JSON.parse(fs.readFileSync(path.join(repoRoot, "spec/workspace.schema.json"), "utf8"));
|
|
286
163
|
const mem = schema.properties?.memory?.properties ?? {};
|
|
@@ -294,8 +171,7 @@ export function budgetIds(repoRoot) {
|
|
|
294
171
|
}
|
|
295
172
|
|
|
296
173
|
// ---------------------------------------------------------------------------------------------
|
|
297
|
-
// Grading
|
|
298
|
-
// artifacts; nothing here is graded against a fixture the corpus invented.
|
|
174
|
+
// Grading
|
|
299
175
|
// ---------------------------------------------------------------------------------------------
|
|
300
176
|
|
|
301
177
|
function gradeBound(skill, c, ctx) {
|
|
@@ -322,13 +198,6 @@ function gradeBound(skill, c, ctx) {
|
|
|
322
198
|
return { total: considered, failing, detail: `${considered - failing.length}/${considered} compliant` };
|
|
323
199
|
}
|
|
324
200
|
|
|
325
|
-
/**
|
|
326
|
-
* Grade one skill's corpus file against the tree.
|
|
327
|
-
*
|
|
328
|
-
* Every numbered step must be accounted for. `unbindable` is adjudicated: the reason comes from a
|
|
329
|
-
* closed vocabulary, and a `judgement-only` step may name no artifact — if you can name the artifacts
|
|
330
|
-
* the mandate governs, it is not judgement, it is unbuilt.
|
|
331
|
-
*/
|
|
332
201
|
export function gradeSkill(skill, corpus, ctx) {
|
|
333
202
|
const findings = [];
|
|
334
203
|
const steps = passSteps(ctx.repoRoot, skill);
|
|
@@ -345,9 +214,7 @@ export function gradeSkill(skill, corpus, ctx) {
|
|
|
345
214
|
);
|
|
346
215
|
}
|
|
347
216
|
}
|
|
348
|
-
// Step 0 is
|
|
349
|
-
// under `## When to use it`. Numbered 0 so the pass accounting stays a clean 1..N and the row cannot
|
|
350
|
-
// be mistaken for a step. Any OTHER number the pass does not have is a finding.
|
|
217
|
+
// Step 0 is a mandate outside `## The pass`, such as `consolidate`'s routing sentence.
|
|
351
218
|
for (const n of declared.keys()) {
|
|
352
219
|
if (n !== 0 && !steps.some((s) => s.n === n)) {
|
|
353
220
|
findings.push(`${skill}: case names step ${n}, which the pass does not have`);
|
|
@@ -363,11 +230,6 @@ export function gradeSkill(skill, corpus, ctx) {
|
|
|
363
230
|
if (!UNBINDABLE_REASONS.includes(c.reason)) {
|
|
364
231
|
findings.push(`${skill}: step ${step.n} is unbindable for ${JSON.stringify(c.reason)}, which is not one of ${UNBINDABLE_REASONS.join(" | ")}`);
|
|
365
232
|
}
|
|
366
|
-
// **A reason without an argument is a label, not an adjudication.** `./mutants.mjs` refuses
|
|
367
|
-
// an operator carrying no `why` — *"a mutation nobody can read is not reviewable"* — and
|
|
368
|
-
// `./goldens.mjs` refuses a `documented-hole` naming no record. This carried neither until
|
|
369
|
-
// the pre-commit checkpoint stripped every `why` from all ten unbindable cases and watched
|
|
370
|
-
// the corpus stay green.
|
|
371
233
|
if (typeof c.why !== "string" || c.why.trim().length < 40) {
|
|
372
234
|
findings.push(
|
|
373
235
|
`${skill}: step ${step.n} is unbindable and carries no argument. ` +
|
|
@@ -393,11 +255,6 @@ export function gradeSkill(skill, corpus, ctx) {
|
|
|
393
255
|
if (e instanceof CouldNotRun) throw e;
|
|
394
256
|
throw new CouldNotRun(`${skill}: step ${step.n} could not be graded — ${e.message}`);
|
|
395
257
|
}
|
|
396
|
-
// **`expect.accepted` is the larger dodge, and it was entirely unadjudicated.** Any finding
|
|
397
|
-
// could be silenced by appending a bare filename — the pre-commit checkpoint created a
|
|
398
|
-
// non-compliant task file, listed it, and the corpus went green over eighteen silenced
|
|
399
|
-
// findings. So an entry is an object carrying its own `why`, on the same rule as `unbindable`
|
|
400
|
-
// above: the both-directions check stops a STALE entry, and this stops an UNJUSTIFIED one.
|
|
401
258
|
const acceptedEntries = c.expect?.accepted ?? [];
|
|
402
259
|
for (const a of acceptedEntries) {
|
|
403
260
|
if (typeof a !== "object" || typeof a.file !== "string") {
|
|
@@ -413,8 +270,6 @@ export function gradeSkill(skill, corpus, ctx) {
|
|
|
413
270
|
const unexpected = result.failing.filter((f) => !accepted.has(f.split(" — ")[0]));
|
|
414
271
|
const repaired = [...accepted].filter((a) => !result.failing.some((f) => f.split(" — ")[0] === a));
|
|
415
272
|
for (const u of unexpected) findings.push(`${skill}: step ${step.n} — ${u}`);
|
|
416
|
-
// Both directions, as `documented-hole` has on the gate side: an accepted drift that is
|
|
417
|
-
// REPAIRED is a finding too, or an accepted-drift list outlives the drift it records.
|
|
418
273
|
for (const r of repaired) {
|
|
419
274
|
findings.push(
|
|
420
275
|
`${skill}: step ${step.n} — ${r} now complies, and is still listed as accepted drift. ` +
|
|
@@ -438,10 +293,6 @@ export function run(argv = process.argv.slice(2), io = console) {
|
|
|
438
293
|
let repoRoot = ".";
|
|
439
294
|
let workspaceDir = null;
|
|
440
295
|
for (let i = 0; i < argv.length; i += 1) {
|
|
441
|
-
// **A missing option value must not swallow the next flag.** `argv[++i]` on a trailing
|
|
442
|
-
// `--repo-root` yields `undefined` and on `--repo-root --workspace x` yields `"--workspace"`,
|
|
443
|
-
// so the tool would grade a directory nobody named and say nothing. `./rule-carriers.mjs`
|
|
444
|
-
// treats a missing value as exit 2 and this now matches it. Copilot round 1 on #360.
|
|
445
296
|
const value = (flag) => {
|
|
446
297
|
const v = argv[i + 1];
|
|
447
298
|
if (v === undefined || v.startsWith("--")) throw new CouldNotRun(`${flag} needs a value`);
|
|
@@ -478,8 +329,6 @@ export function run(argv = process.argv.slice(2), io = console) {
|
|
|
478
329
|
for (const skill of skills) {
|
|
479
330
|
const file = path.join(corpusDir, `${skill}.json`);
|
|
480
331
|
if (!fs.existsSync(file)) {
|
|
481
|
-
// The derived skill set is what makes this a coverage rail: a new core skill cannot ship
|
|
482
|
-
// with no mandates accounted for.
|
|
483
332
|
findings.push(`${skill}: core/skills/${skill}/ exists and evals/goldens/skills/${skill}.json does not`);
|
|
484
333
|
continue;
|
|
485
334
|
}
|
|
@@ -491,8 +340,6 @@ export function run(argv = process.argv.slice(2), io = console) {
|
|
|
491
340
|
return 2;
|
|
492
341
|
}
|
|
493
342
|
if (corpus.skill !== skill) {
|
|
494
|
-
// The filename is checked against the file's own field, the repair `../evals/README.md`
|
|
495
|
-
// records for the gate corpus after a misfiled fixture graded cleanly.
|
|
496
343
|
io.error(`skill-goldens: ${skill}.json declares skill ${JSON.stringify(corpus.skill)}`);
|
|
497
344
|
return 2;
|
|
498
345
|
}
|
|
@@ -530,22 +377,12 @@ export function run(argv = process.argv.slice(2), io = console) {
|
|
|
530
377
|
io.log(` ${bound} of ${bound + unbindable} mandate(s) bound to live artifacts — ${loadBearing} load-bearing, ${census} census`);
|
|
531
378
|
const reasons = {};
|
|
532
379
|
for (const g of all) for (const r of g.rows) if (r.state === "unbindable") reasons[r.reason] = (reasons[r.reason] ?? 0) + 1;
|
|
533
|
-
// **The split is printed, because the aggregate hides which unbindable means what.** Three prose
|
|
534
|
-
// carriers of this file's own output once said "two thirds of these mandates are judgement" when
|
|
535
|
-
// two thirds were merely UNBINDABLE and `cross-language` and `already-carried` mean the opposite —
|
|
536
|
-
// a carrier exists. Caught at the pre-commit checkpoint.
|
|
537
380
|
io.log(` ${unbindable} adjudicated unbindable — ${Object.entries(reasons).map(([k, v]) => `${v} ${k}`).join(", ")}`);
|
|
538
381
|
io.log("");
|
|
539
382
|
io.log(" THE RATIO IS A FINDING, not bookkeeping: it measures how much of a core skill is artifact");
|
|
540
383
|
io.log(" discipline and how much is agent judgement. Read the SPLIT, not the total — the");
|
|
541
384
|
io.log(" judgement-only rows are the A/B clause's FIRST subject; cross-language and already-carried");
|
|
542
385
|
io.log(" mean a carrier exists and is somewhere else.");
|
|
543
|
-
// **Widened 2026-08-28 on the maintainer's ruling**; this printed "only the judgement-only rows are
|
|
544
|
-
// the A/B clause's subject" until then. That reading could not stand, and the reason is measured:
|
|
545
|
-
// `vendor --host` carries `core/skills/` not at all, so all five judgement rows reach
|
|
546
|
-
// nothing in a vendored arm. The A/B clause's subject is the mandates core SHIPS that an arm
|
|
547
|
-
// actually receives. Four carriers held the narrower reading and all four moved together —
|
|
548
|
-
// ../evals/ab/corpus.md, and the claim is registered in ../.portulan/rule-carriers.json.
|
|
549
386
|
io.log(" A `census` row re-indexes a figure an existing recipe already prints; it is not a new check.");
|
|
550
387
|
io.log(" `unbindable` is the dodge, so its reason comes from a closed vocabulary and is adjudicated.");
|
|
551
388
|
io.log(" `carrier` is a DECLARED, reviewed field. Nothing links it to a check: a corpus naming a");
|
|
@@ -572,10 +409,7 @@ export function run(argv = process.argv.slice(2), io = console) {
|
|
|
572
409
|
}
|
|
573
410
|
}
|
|
574
411
|
|
|
575
|
-
//
|
|
576
|
-
// differently from `import.meta.url` and this working copy lives under a path with spaces, so the naive
|
|
577
|
-
// spelling exits 0 having run nothing — met four times in this repository, once in the session before
|
|
578
|
-
// this one.
|
|
412
|
+
// Not `file://${argv[1]}`: `import.meta.url` percent-encodes, so a path with a space would never match.
|
|
579
413
|
function isMain() {
|
|
580
414
|
const invoked = process.argv[1];
|
|
581
415
|
if (!invoked) return false;
|