@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/manifest.mjs
CHANGED
|
@@ -1,39 +1,9 @@
|
|
|
1
|
-
// The two facts every tool here needs out of `package.json`, read once and in a module that imports
|
|
2
|
-
// nothing of ours.
|
|
3
|
-
//
|
|
4
|
-
// ## Why this is its own file, which is the part worth reading
|
|
5
|
-
//
|
|
6
|
-
// `VERSION` lived in `portulan.mjs` because that is where it was first needed, and it was right there:
|
|
7
|
-
// one carrier, read from the manifest that publishes it rather than written down twice.
|
|
8
|
-
//
|
|
9
|
-
// Then `feedback.mjs` needed the same value and imported it from `portulan.mjs` — still one carrier,
|
|
10
|
-
// and **it hung the command line**. `portulan.mjs` ends in `process.exitCode = await run(…)`, `run`
|
|
11
|
-
// dynamically imports the subcommand, and the subcommand statically imported `portulan.mjs` back. Node
|
|
12
|
-
// cannot finish evaluating a module that is waiting on a module waiting on it: `portulan feedback
|
|
13
|
-
// --help` printed nothing and exited **13**, `Detected unsettled top-level await`.
|
|
14
|
-
//
|
|
15
|
-
// Nothing in the suite could see it. Every dispatch case injects the loader — correctly, since
|
|
16
|
-
// re-asserting what each tool proves itself would make the entry point's suite a second carrier of it
|
|
17
|
-
// — and an injected loader never imports the real module, so the cycle does not exist under test. It
|
|
18
|
-
// was found by the one case that spawns the real binary, written in the same change for exactly this
|
|
19
|
-
// reason. **A harness you write to check your own change inherits your change's blind spot**, for the
|
|
20
|
-
// third recorded time in this repository.
|
|
21
|
-
//
|
|
22
|
-
// So the shared read moved down here, where it has no dependencies to be circular with. `portulan.mjs`
|
|
23
|
-
// re-exports `VERSION` so its published surface is unchanged.
|
|
1
|
+
// The two facts every tool here needs out of `package.json`, read once and in a module that imports nothing of ours.
|
|
24
2
|
|
|
25
3
|
import fs from "node:fs";
|
|
26
4
|
import path from "node:path";
|
|
27
5
|
import { fileURLToPath } from "node:url";
|
|
28
6
|
|
|
29
|
-
/**
|
|
30
|
-
* The manifest, in both layouts that matter: from a checkout it is one level above `cli/`, and the
|
|
31
|
-
* published package keeps that shape because `files` ships `cli/` under the package root.
|
|
32
|
-
*
|
|
33
|
-
* Falls back rather than throwing. A version string and a repository name are not worth taking the
|
|
34
|
-
* command line down for — and the callers that need the repository to be real say so themselves
|
|
35
|
-
* rather than inheriting a crash from here.
|
|
36
|
-
*/
|
|
37
7
|
const manifest = (() => {
|
|
38
8
|
try {
|
|
39
9
|
const at = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
@@ -45,14 +15,7 @@ const manifest = (() => {
|
|
|
45
15
|
|
|
46
16
|
export const VERSION = manifest.version ?? "unknown";
|
|
47
17
|
|
|
48
|
-
/**
|
|
49
|
-
* The repository issues are filed into, derived from the `bugs.url` the manifest already publishes
|
|
50
|
-
* rather than minted as a second constant — `portulan feedback` would otherwise carry a copy that
|
|
51
|
-
* nothing holds to this one.
|
|
52
|
-
*
|
|
53
|
-
* `null` when the manifest carries no usable URL, which the sender turns into a refusal naming
|
|
54
|
-
* `--repo`; guessing a repository to file a public issue into is the one wrong answer here.
|
|
55
|
-
*/
|
|
18
|
+
/** The repository issues are filed into, as `owner/name` from `bugs.url`; null, never a guess, when that names none. */
|
|
56
19
|
export const REPOSITORY = (() => {
|
|
57
20
|
const match = /github\.com\/([^/]+\/[^/]+?)(?:\/issues)?\/?$/.exec(manifest.bugs?.url ?? "");
|
|
58
21
|
return match ? match[1] : null;
|
package/cli/mutants.mjs
CHANGED
|
@@ -1,66 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Mutation-test this repository's two gate matchers against the corpus that claims to cover them.
|
|
3
3
|
//
|
|
4
|
-
// Milestone 8, clause (b), first half: *mutation testing over both matchers.* The second half —
|
|
5
|
-
// grammar-aware fuzzing over the shell segmenter — is ./fuzz-shell.mjs, and the two are separate
|
|
6
|
-
// because a surviving mutant and a grammar bypass are different verdicts with different repairs.
|
|
7
|
-
//
|
|
8
|
-
// ## The question this answers, and it is the one `./goldens.mjs` says out loud that it cannot
|
|
9
|
-
//
|
|
10
|
-
// That runner prints on every green: *"this is a PRESENCE floor — whether a corpus is a real attack
|
|
11
|
-
// is a reviewer's judgement, not this rail's."* One trivial fixture per rule satisfies it while
|
|
12
|
-
// proving nothing adversarial. This runner stands in exactly that stated gap. It does not ask whether
|
|
13
|
-
// a fixture exists; it breaks a matcher on purpose and asks whether the corpus NOTICES.
|
|
14
|
-
//
|
|
15
|
-
// A corpus that cannot tell a working matcher from a broken one is a corpus whose green means
|
|
16
|
-
// nothing, and until this file existed no check in this repository could tell those two apart.
|
|
17
|
-
//
|
|
18
|
-
// ## What a mutant is here — anchored, not generated
|
|
19
|
-
//
|
|
20
|
-
// An operator is a **declared textual substitution** into `./compile.mjs`, anchored to a named
|
|
21
|
-
// construct, required to match **exactly once**. Not a random AST perturbation: a random mutator over
|
|
22
|
-
// a 2681-line module spends most of its budget outside the matchers, and this clause is about the
|
|
23
|
-
// matchers. Every operator names the region member it attacks, and the census prints coverage per
|
|
24
|
-
// member INCLUDING THE ZEROES — otherwise "mutation testing over both matchers" is satisfiable by
|
|
25
|
-
// three operators inside `matchesRule` and no reader would see the narrowing.
|
|
26
|
-
//
|
|
27
|
-
// ## The record, and why it reads in both directions
|
|
28
|
-
//
|
|
29
|
-
// Each operator declares the outcome expected of it:
|
|
30
|
-
//
|
|
31
|
-
// - `killed` — the corpus must catch this. If it stops catching it, the kill-set has weakened.
|
|
32
|
-
// - `survives` — the corpus cannot catch it AND THAT IS PROVEN, not observed. Two proofs are
|
|
33
|
-
// admissible and each entry states which: the mutant is semantically **equivalent**
|
|
34
|
-
// to the original, or it is equivalent **under the yielded policy** — no rule of the
|
|
35
|
-
// shape that would distinguish it is declared, so no fixture could exist to kill it.
|
|
36
|
-
//
|
|
37
|
-
// **A survivor that is neither is not a resting state.** `matchesRule` is a pure function of
|
|
38
|
-
// `(rule, tool, input)` and a fixture is exactly that triple, so any non-equivalent mutant is killable
|
|
39
|
-
// by adding one case. A standing ledger of named-but-unfilled gaps would rebuild the prose hole list
|
|
40
|
-
// that clause (a) exists to have replaced, one altitude up. The repair for such a survivor is a new
|
|
41
|
-
// fixture in the corpus, in the corpus's own shape — never a new `survives` entry.
|
|
42
|
-
//
|
|
43
|
-
// Both directions are red, which is `documented-hole`'s discipline in `./goldens.mjs` applied to a
|
|
44
|
-
// second subject: an operator recorded `killed` that now survives, and an operator recorded `survives`
|
|
45
|
-
// that is now killed. The second is good news and the message says so — but a record that still
|
|
46
|
-
// describes a gap somebody closed is as wrong as one that hides a gap somebody opened.
|
|
47
|
-
//
|
|
48
|
-
// ## Three things it deliberately does not do
|
|
49
|
-
//
|
|
50
|
-
// **It does not run the `node --test` suite as a kill-set.** The corpus is the kill-set this
|
|
51
|
-
// repository designated — `../evals/README.md` says clause (b) *"needs this corpus as its kill-set
|
|
52
|
-
// and this fixture format as its output shape"* — running the suite per mutant would need the
|
|
53
|
-
// subprocesses this design refuses, and a `survives` record citing the suite as the killer would put
|
|
54
|
-
// the kill-claim in a second carrier.
|
|
55
|
-
//
|
|
56
|
-
// **It executes nothing.** Fixture command strings are data here exactly as they are in
|
|
57
|
-
// `./goldens.mjs`; the corpus holds `git push --force` and constitution-write spellings by design.
|
|
58
|
-
// The only thing this module runs is `import()` on a JavaScript file it wrote itself, and
|
|
59
|
-
// `./mutants.test.mjs` asserts it imports no process-spawning API.
|
|
60
|
-
//
|
|
61
|
-
// **It writes nothing inside the tree.** Mutants live under `os.tmpdir()`, one fresh directory each,
|
|
62
|
-
// removed in a `finally`.
|
|
63
|
-
//
|
|
64
4
|
// Exit 0 green · 1 red · 2 could not run.
|
|
65
5
|
|
|
66
6
|
import fs from "node:fs";
|
|
@@ -71,20 +11,9 @@ import { pathToFileURL } from "node:url";
|
|
|
71
11
|
import { CompileError } from "./compile.mjs";
|
|
72
12
|
import { CouldNotRun, partition, readCorpus, yieldedRules } from "./goldens.mjs";
|
|
73
13
|
|
|
74
|
-
/** The module under mutation, relative to the repository root. */
|
|
75
14
|
export const SUBJECT = "cli/compile.mjs";
|
|
76
15
|
|
|
77
|
-
/**
|
|
78
|
-
* The matcher region — every function and table `matchesRule` can reach, named.
|
|
79
|
-
*
|
|
80
|
-
* This list is the **coverage floor**, and it is written down rather than derived because deriving it
|
|
81
|
-
* would mean a call-graph walker, and a walker that is wrong is wrong silently. Each member must carry
|
|
82
|
-
* at least one operator or the table below must say why not; `census` prints the per-member count
|
|
83
|
-
* including zeroes, the way `./goldens.mjs` prints its per-matcher-path census including zeroes.
|
|
84
|
-
*
|
|
85
|
-
* The three entry branches of `matchesRule` are the two matchers the clause names plus `read:`, which
|
|
86
|
-
* shares `matchesPath` with the write branch and so cannot be mutated separately from it.
|
|
87
|
-
*/
|
|
16
|
+
/** Every function and table `matchesRule` can reach, listed by hand rather than derived. */
|
|
88
17
|
export const REGION = [
|
|
89
18
|
"matchesRule",
|
|
90
19
|
"matchesPath",
|
|
@@ -107,30 +36,13 @@ export const REGION = [
|
|
|
107
36
|
"SEGMENT_LEADERS",
|
|
108
37
|
"OPERATOR",
|
|
109
38
|
"ASSIGNMENT",
|
|
110
|
-
// Added at the pre-commit checkpoint, which derived the reachable set rather than reading this
|
|
111
|
-
// list and found both missing while the sentence above said "every function and table
|
|
112
|
-
// `matchesRule` can reach". They are the tool tables the write and read branches dispatch on —
|
|
113
|
-
// `compile.mjs:92` and `:93`, read at `:1003` and `:1033` — and a list that omits a dispatch
|
|
114
|
-
// table is exactly the narrowing this census exists to make visible. Adding them found a real
|
|
115
|
-
// gap: the corpus had no `NotebookEdit` case at all.
|
|
116
39
|
"WRITE_TOOLS",
|
|
117
40
|
"READ_TOOLS",
|
|
118
41
|
];
|
|
119
42
|
|
|
120
|
-
/** The two outcomes an operator may record. */
|
|
121
43
|
export const OUTCOMES = ["killed", "survives"];
|
|
122
44
|
|
|
123
|
-
/**
|
|
124
|
-
* The operators, and the outcome recorded for each.
|
|
125
|
-
*
|
|
126
|
-
* **One table, in this module, deliberately.** An operator is an anchor into `./compile.mjs`'s source
|
|
127
|
-
* text plus the outcome expected of it; splitting those across code and a JSON file would make two
|
|
128
|
-
* carriers of one operator, which is the defect this repository names more often than any other. The
|
|
129
|
-
* corpus is data because it is per-rule and large; an operator is neither.
|
|
130
|
-
*
|
|
131
|
-
* `find` must occur **exactly once** in the subject. A missing or ambiguous anchor is could-not-run,
|
|
132
|
-
* never a skip — see `mutate`.
|
|
133
|
-
*/
|
|
45
|
+
/** Record `survives` only for a mutant equivalent outright or under the yielded policy: a fixture can kill any other. */
|
|
134
46
|
export const OPERATORS = [
|
|
135
47
|
// ---------------------------------------------------------------------------------- matchesRule
|
|
136
48
|
{
|
|
@@ -233,12 +145,7 @@ export const OPERATORS = [
|
|
|
233
145
|
{
|
|
234
146
|
id: "matchesPath-stops-stripping-a-leading-dot-slash",
|
|
235
147
|
member: "matchesPath",
|
|
236
|
-
//
|
|
237
|
-
// same file re-spells this strip — deliberately, so that predicate reads without `matchesPath`
|
|
238
|
-
// in front of you — and the moment it did, this operator placed TWICE and the whole census
|
|
239
|
-
// exited 2: could-not-run wearing a pass's clothes, over a cause no reader would guess from the
|
|
240
|
-
// exit code. The recipe's own advice is to lengthen the anchor until it is unique, and the
|
|
241
|
-
// `if (clean === ...)` guard is the line only `matchesPath` has. Measured 2026-09-03.
|
|
148
|
+
// The second line keeps the anchor unique: `neverMatches` repeats the first.
|
|
242
149
|
find: "const clean = String(target ?? \"\").replace(/^\\.\\//, \"\").replace(/^\\/+/, \"\");\n if (clean === \"\" || clean === \"/\") return false;",
|
|
243
150
|
replace: "const clean = String(target ?? \"\").replace(/^\\/+/, \"\");\n if (clean === \"\" || clean === \"/\") return false;",
|
|
244
151
|
outcome: "killed",
|
|
@@ -578,14 +485,6 @@ export const OPERATORS = [
|
|
|
578
485
|
},
|
|
579
486
|
];
|
|
580
487
|
|
|
581
|
-
/**
|
|
582
|
-
* Apply one operator to the subject's source.
|
|
583
|
-
*
|
|
584
|
-
* **A missing or ambiguous anchor is could-not-run, never a skip.** If `compile.mjs` moves and an
|
|
585
|
-
* anchor no longer places, a harness that skipped it would report a clean sweep of the operators it
|
|
586
|
-
* happened to apply — `../.portulan/memory/a-checker-must-refuse-what-it-cannot-check.md`, in the one
|
|
587
|
-
* tool whose entire subject is checks that must not be trusted on their own word.
|
|
588
|
-
*/
|
|
589
488
|
export function mutate(source, op) {
|
|
590
489
|
let count = 0;
|
|
591
490
|
let at = 0;
|
|
@@ -610,33 +509,15 @@ export function mutate(source, op) {
|
|
|
610
509
|
return source.replace(op.find, () => op.replace);
|
|
611
510
|
}
|
|
612
511
|
|
|
613
|
-
/**
|
|
614
|
-
* Rewrite the subject's relative imports to absolute `file://` URLs.
|
|
615
|
-
*
|
|
616
|
-
* A mutant lives in a temp directory, so `./discover.mjs` and `./inside.mjs` would not resolve from
|
|
617
|
-
* there. Both import node builtins only, so pointing at the real modules costs nothing and copies
|
|
618
|
-
* nothing. Measured before this was written rather than assumed: `${SUBJECT}` has exactly two relative
|
|
619
|
-
* imports, no dynamic `import(`, and its only top-level `import.meta.url` use is the entry guard —
|
|
620
|
-
* which compares against `process.argv[1]` and therefore cannot fire for a module imported from a
|
|
621
|
-
* temp directory while argv[1] is this runner.
|
|
622
|
-
*/
|
|
512
|
+
/** Only `from "./….mjs"` imports are rewritten, and a mutant's `import.meta.url` names its temp directory. */
|
|
623
513
|
export function absolutiseImports(source, cliDir) {
|
|
624
514
|
return source.replace(/from "\.\/([A-Za-z0-9._-]+\.mjs)"/g, (_, name) => `from "${pathToFileURL(path.join(cliDir, name)).href}"`);
|
|
625
515
|
}
|
|
626
516
|
|
|
627
|
-
/** Run the whole corpus through one `matchesRule`, and report the first disagreement. */
|
|
628
517
|
export function runCorpus(matchesRule, byId, corpus) {
|
|
629
518
|
for (const { where, doc } of corpus) {
|
|
630
519
|
const rule = byId.get(doc.rule);
|
|
631
|
-
//
|
|
632
|
-
// policy does not declare — renamed, misfiled, outlived its gate — sit ungraded while the
|
|
633
|
-
// census reported green. A coverage tool that silently thins its own kill-set and then reports
|
|
634
|
-
// on it is the loudest false green available here, and it is the failure
|
|
635
|
-
// `../.portulan/memory/a-checker-must-refuse-what-it-cannot-check.md` names, in the module
|
|
636
|
-
// whose whole subject is that class. `run` refuses the whole census before the baseline for
|
|
637
|
-
// exactly this, so this arm is unreachable from there and is kept because a direct caller of
|
|
638
|
-
// an exported function must not get the silent behaviour back. Reported as a suppressed note
|
|
639
|
-
// by Copilot, round 5 on #338.
|
|
520
|
+
// `run` refuses such a corpus first; this arm answers a direct caller.
|
|
640
521
|
if (!rule) {
|
|
641
522
|
throw new CouldNotRun(
|
|
642
523
|
`${where} attacks \`${doc.rule}\`, which the yielded policy does not declare — its cases cannot be ` +
|
|
@@ -648,8 +529,7 @@ export function runCorpus(matchesRule, byId, corpus) {
|
|
|
648
529
|
try {
|
|
649
530
|
actual = matchesRule(rule, c.tool, c.input);
|
|
650
531
|
} catch (error) {
|
|
651
|
-
//
|
|
652
|
-
// so a throw is a disagreement with the contract as much as a wrong boolean is.
|
|
532
|
+
// `matchesRule` never throws, so a mutant that throws is killed.
|
|
653
533
|
return { agreed: false, at: `${where} → ${c.id}`, how: `threw ${error?.name ?? "an error"}` };
|
|
654
534
|
}
|
|
655
535
|
if (actual !== c.expect) {
|
|
@@ -660,16 +540,7 @@ export function runCorpus(matchesRule, byId, corpus) {
|
|
|
660
540
|
return { agreed: true };
|
|
661
541
|
}
|
|
662
542
|
|
|
663
|
-
/**
|
|
664
|
-
* Import a mutated subject from a fresh temp directory.
|
|
665
|
-
*
|
|
666
|
-
* **A fresh directory per mutant, and that is load-bearing rather than tidy.** ESM caches by resolved
|
|
667
|
-
* URL and offers no invalidation, so writing every mutant to one reused path would import mutant 1
|
|
668
|
-
* and then grade it forty more times — silently, and *including while the record is first being
|
|
669
|
-
* written*, where the two-directional rail cannot see it because the record would be built from the
|
|
670
|
-
* same wrong readings. `./mutants.test.mjs` pins it with two mutants that must answer differently in
|
|
671
|
-
* one process.
|
|
672
|
-
*/
|
|
543
|
+
/** A fresh directory per mutant: ESM caches a module by its URL and never forgets one. */
|
|
673
544
|
async function importMutant(source, cliDir, op) {
|
|
674
545
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "portulan-mutant-"));
|
|
675
546
|
const file = path.join(dir, "compile.mjs");
|
|
@@ -678,10 +549,6 @@ async function importMutant(source, cliDir, op) {
|
|
|
678
549
|
try {
|
|
679
550
|
return { module: await import(pathToFileURL(file).href), dir };
|
|
680
551
|
} catch (cause) {
|
|
681
|
-
// **An unimportable mutant is could-not-run, never a kill.** A substitution that produces a
|
|
682
|
-
// syntax error checks nothing about the corpus, and counting it as killed would let a
|
|
683
|
-
// broken operator masquerade as a well-covered one — the loudest possible false green in a
|
|
684
|
-
// tool whose whole output is a coverage claim.
|
|
685
552
|
throw new CouldNotRun(
|
|
686
553
|
`operator \`${op.id}\` produced a module that will not import — ${cause?.message ?? cause}. ` +
|
|
687
554
|
`A mutant that cannot load tests nothing; fix the substitution`,
|
|
@@ -693,7 +560,6 @@ async function importMutant(source, cliDir, op) {
|
|
|
693
560
|
}
|
|
694
561
|
}
|
|
695
562
|
|
|
696
|
-
/** Per-region-member operator counts, zeroes included. */
|
|
697
563
|
export function census(operators = OPERATORS) {
|
|
698
564
|
const counts = new Map(REGION.map((m) => [m, 0]));
|
|
699
565
|
const stray = [];
|
|
@@ -754,9 +620,6 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
|
|
|
754
620
|
} else throw new CouldNotRun(`unknown argument ${JSON.stringify(argv[i])}`);
|
|
755
621
|
}
|
|
756
622
|
|
|
757
|
-
// The coverage floor is checked before a single mutant is written, because it is a property of
|
|
758
|
-
// the TABLE and a run that spends thirty seconds before reporting a structural gap is a run
|
|
759
|
-
// nobody waits for.
|
|
760
623
|
const { counts, stray, uncovered } = census();
|
|
761
624
|
if (stray.length) {
|
|
762
625
|
throw new CouldNotRun(
|
|
@@ -764,12 +627,7 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
|
|
|
764
627
|
`Add the member to REGION or correct the operator — an operator outside the census is coverage nobody counts`,
|
|
765
628
|
);
|
|
766
629
|
}
|
|
767
|
-
//
|
|
768
|
-
// — the mode a person reaches for precisely when something is wrong — validated nothing, and a
|
|
769
|
-
// malformed operator slipped through exactly where somebody was looking closely. `--only`
|
|
770
|
-
// narrows what RUNS; it does not narrow what must be well formed, because the table is the
|
|
771
|
-
// artifact under review and a reviewer reading it in `--only` mode is reading the same table.
|
|
772
|
-
// Reported by Copilot, round 1 on #338.
|
|
630
|
+
// Every operator, even under `--only`: it narrows what runs, not what must be well formed.
|
|
773
631
|
for (const op of OPERATORS) {
|
|
774
632
|
if (!OUTCOMES.includes(op.outcome)) {
|
|
775
633
|
throw new CouldNotRun(`operator \`${op.id}\` records outcome ${JSON.stringify(op.outcome)} — one of ${OUTCOMES.join(" / ")}`);
|
|
@@ -795,13 +653,6 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
|
|
|
795
653
|
const { matchable, exempt } = partition(rules);
|
|
796
654
|
const byId = new Map(matchable.map((r) => [r.id, r]));
|
|
797
655
|
|
|
798
|
-
// **The corpus and the policy must correspond before a single mutant is written.** A fixture
|
|
799
|
-
// naming a rule this census cannot grade is not a case that happens to pass — it is a piece of
|
|
800
|
-
// the kill-set that never runs, and the census's whole output is a claim about how much the
|
|
801
|
-
// kill-set catches. Refused here rather than at the first mutant so the message names the
|
|
802
|
-
// repair once instead of forty-nine times, and refused at all because the alternative measured
|
|
803
|
-
// green. `goldens` reds on the same condition; this does not lean on that recipe having run,
|
|
804
|
-
// since a rail that depends on a sibling rail is a rail with a precondition nobody states.
|
|
805
656
|
const ungradable = corpus
|
|
806
657
|
.filter(({ doc }) => !byId.has(doc.rule))
|
|
807
658
|
.map(({ where, doc }) => {
|
|
@@ -818,10 +669,6 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
|
|
|
818
669
|
);
|
|
819
670
|
}
|
|
820
671
|
|
|
821
|
-
// **The baseline, before any mutant.** A mutation census over a corpus that is already red
|
|
822
|
-
// measures nothing — every mutant would "survive" against a kill-set that cannot even agree
|
|
823
|
-
// with the unmutated matcher. `goldens` is the recipe that grades this; here it is a
|
|
824
|
-
// precondition, and a failed precondition is could-not-run rather than a verdict.
|
|
825
672
|
const { matchesRule } = await import(pathToFileURL(subject).href);
|
|
826
673
|
const baseline = runCorpus(matchesRule, byId, corpus);
|
|
827
674
|
if (!baseline.agreed) {
|
|
@@ -870,14 +717,9 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
|
|
|
870
717
|
}
|
|
871
718
|
|
|
872
719
|
say(`mutants: ${selected.length} operator(s) over ${corpus.length} fixture file(s) — ${killed} killed, ${survived} survived`);
|
|
873
|
-
// Per region member, zeroes included — see REGION. A total says nothing about where the
|
|
874
|
-
// operators landed, and "mutation testing over both matchers" is satisfiable by three
|
|
875
|
-
// operators in one function if nobody prints the distribution.
|
|
876
720
|
say(`mutants: by matcher-region member —`);
|
|
877
721
|
for (const member of REGION) say(` ${String(counts.get(member)).padStart(2)} ${member}`);
|
|
878
722
|
if (uncovered.length) {
|
|
879
|
-
// A gap in the floor is a RED rather than a could-not-run: the table is well-formed, it
|
|
880
|
-
// simply does not reach part of what the clause names.
|
|
881
723
|
findings.push({
|
|
882
724
|
where: "the operator table",
|
|
883
725
|
what:
|
|
@@ -907,10 +749,7 @@ export async function run(argv = [], { stdout = process.stdout, stderr = process
|
|
|
907
749
|
}
|
|
908
750
|
}
|
|
909
751
|
|
|
910
|
-
//
|
|
911
|
-
// form: `import.meta.url` percent-encodes, this working copy lives under a path with spaces, and the
|
|
912
|
-
// comparison fails — so the tool exits 0 having run nothing. Copied rather than re-derived, for the
|
|
913
|
-
// reason `./goldens.mjs` states after meeting the false green a third time.
|
|
752
|
+
// URLs on both sides, since `import.meta.url` percent-encodes; the realpath covers a symlinked `bin`.
|
|
914
753
|
function isMain() {
|
|
915
754
|
const invoked = process.argv[1];
|
|
916
755
|
if (!invoked) return false;
|