@sleepy_panda_srl/portulan 0.1.1 → 0.1.3
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 +16 -19
- package/cli/README.md +155 -125
- package/cli/compile.mjs +797 -40
- package/cli/doctor.mjs +59 -2
- package/cli/drills.mjs +1541 -0
- package/cli/fuzz-shell.mjs +880 -0
- package/cli/gate.mjs +3 -2
- package/cli/goldens.mjs +469 -0
- package/cli/index.mjs +92 -42
- package/cli/init.mjs +4 -1
- package/cli/inside.mjs +38 -0
- package/cli/mutants.mjs +925 -0
- package/cli/pack-identity.mjs +9 -2
- package/cli/pack-version.mjs +7 -0
- package/cli/recipe-set.mjs +48 -1
- package/cli/release-eval.mjs +1068 -0
- package/cli/review-meter.mjs +741 -0
- package/cli/skill-goldens.mjs +590 -0
- package/cli/skills-set.mjs +166 -4
- package/cli/stop-gate.mjs +301 -28
- package/cli/telemetry.mjs +1047 -0
- package/cli/version-carriers.mjs +224 -0
- package/core/operating/memory.md +7 -8
- package/core/operating/verification.md +4 -12
- package/core/templates/gate-policy.md +27 -8
- package/evals/README.md +910 -0
- package/evals/releases/0.1.3.json +134 -0
- package/evals/releases/0.1.3.md +64 -0
- package/evals/releases/README.md +32 -0
- package/package.json +7 -2
- package/packs/README.md +11 -21
- package/packs/rituals/checkpoints/README.md +8 -1
- package/packs/rituals/checkpoints/pack.json +1 -1
- package/packs/rituals/checkpoints/skills/pre-commit/SKILL.md +32 -2
- package/packs/tools/README.md +1 -4
- package/spec/README.md +19 -22
- package/spec/migrations/README.md +2 -3
- package/spec/slots.md +30 -11
package/cli/index.mjs
CHANGED
|
@@ -74,7 +74,10 @@ import { pathToFileURL } from "node:url";
|
|
|
74
74
|
// compiler and `doctor` already resolve it. A second resolver here would be a second answer to
|
|
75
75
|
// "which pack is that" — and issue #74 already records what a memory store cost this project when
|
|
76
76
|
// three copies decided what counted as a record.
|
|
77
|
-
|
|
77
|
+
// `shadowedCopy` rides along with the two this file already imports: the predicate for *is this pack
|
|
78
|
+
// shadowed* is one carrier by #316's own argument, and a second spelling here would be the defect that
|
|
79
|
+
// change was filed against, committed one tool over.
|
|
80
|
+
import { resolvePack, rootPlan, shadowedCopy } from "./compile.mjs";
|
|
78
81
|
// Host plugin-cache discovery (#123) — imported, never re-implemented, for the reason above.
|
|
79
82
|
import { AUTO, discoverPackRoots, namedWithAuto } from "./discover.mjs";
|
|
80
83
|
|
|
@@ -101,34 +104,14 @@ const NOT_A_RECORD = new Set(["README.md"]);
|
|
|
101
104
|
|
|
102
105
|
const KB = 1024;
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
* into the store, reported `ok`, and `doctor` then counted it as a second record, reporting it for
|
|
113
|
-
* stating no retirement condition. That is precisely the outcome the siting rule exists to prevent,
|
|
114
|
-
* in the check chosen over a filename exemption **because** an exemption would be a door any record
|
|
115
|
-
* could walk through. The door was in the containment test. Found by Copilot on #72, in the
|
|
116
|
-
* suppressed half of the round; ninth fail-open found in this repository's scaffolding, and the
|
|
117
|
-
* first one written by the change that cites the class.
|
|
118
|
-
*
|
|
119
|
-
* Only `..` exactly, or `..` followed by a separator, means outside. An absolute result means the
|
|
120
|
-
* two paths share no root and cannot contain one another.
|
|
121
|
-
*
|
|
122
|
-
* It lives here and `doctor` imports it, on the reasoning that already put `compile`'s accounting
|
|
123
|
-
* behind one import: two copies of a rule drift, and these two copies drifted into the identical
|
|
124
|
-
* defect before either had shipped.
|
|
125
|
-
*/
|
|
126
|
-
export function isInside(parent, child) {
|
|
127
|
-
const rel = path.relative(parent, child);
|
|
128
|
-
if (rel === "") return true;
|
|
129
|
-
if (path.isAbsolute(rel)) return false;
|
|
130
|
-
return rel !== ".." && !rel.startsWith(`..${path.sep}`);
|
|
131
|
-
}
|
|
107
|
+
// `isInside` moved to `./inside.mjs` and is re-exported here, so `./doctor.mjs` and every other
|
|
108
|
+
// importer is untouched. It left because `./compile.mjs` needs it too and this file imports FROM
|
|
109
|
+
// `compile.mjs` — taking it from here would have closed an import cycle, which this repository has
|
|
110
|
+
// already had exit 13 in silence once. The predicate was not copied: one implementation, no new edges.
|
|
111
|
+
// Imported AND re-exported: a bare `export … from` re-exports without binding the name in this
|
|
112
|
+
// module's own scope, and this file calls `isInside` itself — 104 tests said so immediately.
|
|
113
|
+
import { isInside } from "./inside.mjs";
|
|
114
|
+
export { isInside };
|
|
132
115
|
|
|
133
116
|
// ===========================================================================================
|
|
134
117
|
// Titles
|
|
@@ -455,7 +438,12 @@ export function readScopes(dir, workspace, options = {}) {
|
|
|
455
438
|
const plan = rootPlan(dir, workspace, {
|
|
456
439
|
named: options.packRoots ?? [],
|
|
457
440
|
namedGiven: options.packRoots !== undefined && options.packRoots !== null,
|
|
458
|
-
|
|
441
|
+
// **Injectable, defaulting to the real reader.** It was hardcoded to `discoverPackRoots()`,
|
|
442
|
+
// which meant no case could exercise this tool's discovery path without the machine's own
|
|
443
|
+
// plugin cache deciding the outcome — so the shadow refusal below would have been testable
|
|
444
|
+
// only by arranging a real install. `recipe-set`'s `resolverFor` already takes the thunk;
|
|
445
|
+
// this is the same seam, and the default keeps every existing caller byte-identical.
|
|
446
|
+
discovery: options.discovery ?? (() => discoverPackRoots()),
|
|
459
447
|
forced: options.discoverPacks === true,
|
|
460
448
|
});
|
|
461
449
|
if (plan.refusal) throw new IndexError(plan.refusal);
|
|
@@ -478,6 +466,53 @@ export function readScopes(dir, workspace, options = {}) {
|
|
|
478
466
|
unresolved.push(found);
|
|
479
467
|
continue;
|
|
480
468
|
}
|
|
469
|
+
// **A shadowed pack is refused here too** (#318), on the same ground `compile` refuses it and
|
|
470
|
+
// NOT on the same argument — the difference matters enough to write down, because copying
|
|
471
|
+
// compile's sentence here would state something this file spends four lines denying.
|
|
472
|
+
//
|
|
473
|
+
// The ground is the ambiguity: two roots answered for one declared name, the invocation does
|
|
474
|
+
// not say which the caller meant, and this tool goes on to digest the answering copy's memory
|
|
475
|
+
// scope into a **committed** index. Picking silently makes a tracked artifact a function of
|
|
476
|
+
// what happens to be installed.
|
|
477
|
+
//
|
|
478
|
+
// What does NOT transfer is compile's bytes argument. Compile refuses even an agreeing shadow
|
|
479
|
+
// because `recordedOrigin` tags the answering root into its artifact; this index records no
|
|
480
|
+
// such thing, by the design stated above and in this tool's own help. So an agreeing shadow
|
|
481
|
+
// here really would produce identical bytes — and the refusal is still unconditional, because
|
|
482
|
+
// the alternative is a per-tool predicate for *which* differences reach the digest. That
|
|
483
|
+
// predicate is narrower than "the manifests differ" and would silently widen with every key a
|
|
484
|
+
// later Pack Definition adds: fail-open by construction, and a third carrier of a comparison
|
|
485
|
+
// this repository has already had wrong twice.
|
|
486
|
+
//
|
|
487
|
+
// **Only where the caller did not choose**, exactly as in `compile`: a named root replaces the
|
|
488
|
+
// derived one, and `auto` is discovery elected.
|
|
489
|
+
if (!(options.discoverPacks === true) && plan.source !== "named") {
|
|
490
|
+
// Path-resolved, matching `compile`. `resolutionRoots` builds `origins` and `roots` from
|
|
491
|
+
// the same strings today, so an exact-string lookup is equal — but two resolve-equal
|
|
492
|
+
// spellings of one directory would make `compile` refuse where this passed, and three
|
|
493
|
+
// tools answering one question two ways is the shape this whole family is about.
|
|
494
|
+
const originAt = (r) => (plan.origins ?? []).find((o) => path.resolve(o.root) === path.resolve(r))?.origin;
|
|
495
|
+
const behind = shadowedCopy(name, originAt(found.root), roots, originAt);
|
|
496
|
+
if (behind) {
|
|
497
|
+
// **Relative to the REPOSITORY, so the path matches the flag the reader will type.**
|
|
498
|
+
// `dir` is the WORKSPACE directory, so relativising against it printed `../packs`
|
|
499
|
+
// beside a remedy reading `--pack-root packs` — one sentence disagreeing with itself.
|
|
500
|
+
// A first fix printed the absolute root, which made the sentence self-consistent and
|
|
501
|
+
// still showed a path nobody would type. `compile` and `recipe-set` relativise against
|
|
502
|
+
// the repository root; this derives the same root from the manifest's own `tree`, and
|
|
503
|
+
// falls back to the absolute path where a workspace declares none, since a workspace
|
|
504
|
+
// with no `tree` has no repository for a relative path to be relative to.
|
|
505
|
+
const repoRoot = typeof workspace?.tree === "string" && workspace.tree.trim() ? path.resolve(dir, workspace.tree) : null;
|
|
506
|
+
const behindShown = repoRoot ? path.relative(repoRoot, behind.root) : behind.root;
|
|
507
|
+
throw new IndexError(
|
|
508
|
+
`\`${name}\` is SHADOWED — it resolved under ${found.root}, a root discovered on this host, ` +
|
|
509
|
+
`while the root ${behindShown} also carries it. This index digests the ` +
|
|
510
|
+
`answering copy's memory scope, so which root answered decides what a committed file says. ` +
|
|
511
|
+
"Refusing to pick: name the root — `--pack-root packs` for the tree, which is what " +
|
|
512
|
+
"`verify/index.sh` checks, or `--pack-root auto` for the installed copy.",
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
481
516
|
|
|
482
517
|
let manifest;
|
|
483
518
|
try {
|
|
@@ -759,9 +794,24 @@ export function inspect(dir, { write = false, packRoots: extraRoots, discoverPac
|
|
|
759
794
|
const findings = [];
|
|
760
795
|
const fail = (series, check, message) => findings.push({ severity: "fail", series, check, message });
|
|
761
796
|
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
797
|
+
// **The remedy a finding prints must be runnable on the machine that printed it** (#318). These
|
|
798
|
+
// findings surface through the PINNED `verify/index.sh` run — so on a pack developer’s host, where
|
|
799
|
+
// a declared pack is both installed and in the tree, a bare `node cli/index.mjs <dir>` now exits 2
|
|
800
|
+
// with the SHADOWED refusal instead of regenerating. That is the same defect this change repaired
|
|
801
|
+
// in `verify/index.sh`’s own comment, left standing one layer down: one rule, two sites, repaired
|
|
802
|
+
// at one — which is proposal `0020`’s shape and what the pre-commit sweep exists to catch.
|
|
803
|
+
//
|
|
804
|
+
// Echoing the caller’s flags is provably runnable: a finding only prints under an invocation that
|
|
805
|
+
// reached judgment, so that invocation’s own spelling resolved. A hardcoded `--pack-root packs`
|
|
806
|
+
// would be wrong for an adopter whose packs sit elsewhere.
|
|
807
|
+
const remedyFlags = [
|
|
808
|
+
...(Array.isArray(extraRoots) ? extraRoots.map((r) => ` --pack-root ${r}`) : []),
|
|
809
|
+
...(discoverPacks ? [" --pack-root auto"] : []),
|
|
810
|
+
].join("");
|
|
811
|
+
|
|
812
|
+
const memory = judgeMemory(dir, workspace, { write, fail, remedyFlags });
|
|
813
|
+
const handoffs = judgeHandoffs(dir, workspace, { write, fail, remedyFlags });
|
|
814
|
+
const scopes = judgeScopes(dir, workspace, { write, fail, packRoots: extraRoots, discoverPacks, remedyFlags });
|
|
765
815
|
|
|
766
816
|
return {
|
|
767
817
|
dir,
|
|
@@ -814,7 +864,7 @@ function siteOutside(dir, declaredPath, slot, word) {
|
|
|
814
864
|
* in `./doctor.mjs`, `./new.mjs`, `./init.mjs`, `./plugin-lint.mjs` and `./compile.mjs` — this was the
|
|
815
865
|
* one read in the repository that did not carry it.
|
|
816
866
|
*/
|
|
817
|
-
function compareOrWrite({ dir, declaredPath, indexPath, expected, write, series, source, fail }) {
|
|
867
|
+
function compareOrWrite({ dir, declaredPath, indexPath, expected, write, series, source, fail, remedyFlags = "" }) {
|
|
818
868
|
if (write) {
|
|
819
869
|
try {
|
|
820
870
|
fs.mkdirSync(path.dirname(indexPath), { recursive: true });
|
|
@@ -846,14 +896,14 @@ function compareOrWrite({ dir, declaredPath, indexPath, expected, write, series,
|
|
|
846
896
|
}
|
|
847
897
|
|
|
848
898
|
if (actual === null) {
|
|
849
|
-
fail(series, "index", `${declaredPath} is declared and absent — run \`node cli/index.mjs ${dir}\` to generate it`);
|
|
899
|
+
fail(series, "index", `${declaredPath} is declared and absent — run \`node cli/index.mjs${remedyFlags} ${dir}\` to generate it`);
|
|
850
900
|
} else if (!actual.equals(Buffer.from(expected, "utf8"))) {
|
|
851
|
-
fail(series, "index", `${declaredPath} is out of date against the ${source} — run \`node cli/index.mjs ${dir}\` to regenerate it`);
|
|
901
|
+
fail(series, "index", `${declaredPath} is out of date against the ${source} — run \`node cli/index.mjs${remedyFlags} ${dir}\` to regenerate it`);
|
|
852
902
|
}
|
|
853
903
|
}
|
|
854
904
|
|
|
855
905
|
/** The memory store's index and its budgets. */
|
|
856
|
-
function judgeMemory(dir, workspace, { write, fail }) {
|
|
906
|
+
function judgeMemory(dir, workspace, { write, fail, remedyFlags = "" }) {
|
|
857
907
|
const memory = workspace.memory;
|
|
858
908
|
if (!memory) return { declared: false, path: null, expected: null, budgets: 0 };
|
|
859
909
|
|
|
@@ -890,7 +940,7 @@ function judgeMemory(dir, workspace, { write, fail }) {
|
|
|
890
940
|
if (broken) return { declared: true, path: indexPath, expected: null, budgets: 0 };
|
|
891
941
|
|
|
892
942
|
const expected = render(workspace, store);
|
|
893
|
-
compareOrWrite({ dir, declaredPath, indexPath, expected, write, series: "memory", source: "store", fail });
|
|
943
|
+
compareOrWrite({ dir, declaredPath, indexPath, expected, write, series: "memory", source: "store", fail, remedyFlags });
|
|
894
944
|
const budgets = budgetFindings(memory, store, expected, fail);
|
|
895
945
|
|
|
896
946
|
return { declared: true, path: indexPath, expected, budgets };
|
|
@@ -900,7 +950,7 @@ function judgeMemory(dir, workspace, { write, fail }) {
|
|
|
900
950
|
* The handoff series' index. No budget — see spec/slots.md: every remedy a budget could ask for on an
|
|
901
951
|
* append-only series is barred, so a rail here is one built to be broken.
|
|
902
952
|
*/
|
|
903
|
-
function judgeHandoffs(dir, workspace, { write, fail }) {
|
|
953
|
+
function judgeHandoffs(dir, workspace, { write, fail, remedyFlags = "" }) {
|
|
904
954
|
const declaredPath = workspace.handoffs?.index?.path;
|
|
905
955
|
if (!declaredPath) return { declared: false, path: null, expected: null };
|
|
906
956
|
|
|
@@ -935,7 +985,7 @@ function judgeHandoffs(dir, workspace, { write, fail }) {
|
|
|
935
985
|
if (broken) return { declared: true, path: indexPath, expected: null };
|
|
936
986
|
|
|
937
987
|
const expected = renderHandoffIndex(workspace, series);
|
|
938
|
-
compareOrWrite({ dir, declaredPath, indexPath, expected, write, series: "handoffs", source: "series", fail });
|
|
988
|
+
compareOrWrite({ dir, declaredPath, indexPath, expected, write, series: "handoffs", source: "series", fail, remedyFlags });
|
|
939
989
|
|
|
940
990
|
return { declared: true, path: indexPath, expected };
|
|
941
991
|
}
|
|
@@ -954,7 +1004,7 @@ function judgeHandoffs(dir, workspace, { write, fail }) {
|
|
|
954
1004
|
* that sweep the layer would accept anything at all and the positive control would only ever examine the
|
|
955
1005
|
* ones it already expected.
|
|
956
1006
|
*/
|
|
957
|
-
function judgeScopes(dir, workspace, { write, fail, packRoots: extraRoots, discoverPacks }) {
|
|
1007
|
+
function judgeScopes(dir, workspace, { write, fail, packRoots: extraRoots, discoverPacks, remedyFlags = "" }) {
|
|
958
1008
|
const declaredPath = workspace.personas?.index?.path;
|
|
959
1009
|
if (!declaredPath) return { declared: false, path: null, expected: null };
|
|
960
1010
|
|
|
@@ -1064,7 +1114,7 @@ function judgeScopes(dir, workspace, { write, fail, packRoots: extraRoots, disco
|
|
|
1064
1114
|
}
|
|
1065
1115
|
|
|
1066
1116
|
const expected = renderScopeIndex(workspace, series);
|
|
1067
|
-
compareOrWrite({ dir, declaredPath, indexPath, expected, write, series: "scopes", source: "packs this workspace composes", fail });
|
|
1117
|
+
compareOrWrite({ dir, declaredPath, indexPath, expected, write, series: "scopes", source: "packs this workspace composes", fail, remedyFlags });
|
|
1068
1118
|
|
|
1069
1119
|
// The landing itself, and it happens after the index is written rather than before: if the render
|
|
1070
1120
|
// could not be derived there is nothing to land, and a directory created for a scope no index
|
package/cli/init.mjs
CHANGED
|
@@ -118,7 +118,10 @@ export const SLUG = (() => {
|
|
|
118
118
|
const SPEC = "2.7";
|
|
119
119
|
|
|
120
120
|
/** The gate-policy spec `cli/compile.mjs` reads. Checked by its own suite, not guessed at here. */
|
|
121
|
-
|
|
121
|
+
// Exported so `cli/new.test.mjs` can assert that the OTHER policy-generating carrier —
|
|
122
|
+
// `core/templates/gate-policy.md`, which `new gate-policy` emits — declares the same version.
|
|
123
|
+
// #329 was precisely these two disagreeing, with only this one exercised.
|
|
124
|
+
export const GATE_POLICY_SPEC = "2.2";
|
|
122
125
|
|
|
123
126
|
const RESIDENCES = new Set(["in-repo", "pointer"]);
|
|
124
127
|
|
package/cli/inside.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Is one path inside another? One predicate, one file, no dependencies.
|
|
2
|
+
//
|
|
3
|
+
// **It lives alone because two large modules need it and must not import each other.** It began in
|
|
4
|
+
// `./index.mjs`, which `./doctor.mjs` imports; when `./compile.mjs` needed it too, importing it from
|
|
5
|
+
// there would have closed a cycle — `index.mjs` already imports `resolvePack`/`rootPlan` from
|
|
6
|
+
// `compile.mjs`. This repository has paid for a cycle once already: `../docs/plan.md`'s session log
|
|
7
|
+
// records **an import cycle exiting 13 in silence**, and three modules (`./plugin-lint.mjs:1369`,
|
|
8
|
+
// `./skills-set.mjs:48` and `:114`) carry comments explaining that they chose their import direction
|
|
9
|
+
// to avoid making one. Copying the predicate instead was the other option and is the one its own
|
|
10
|
+
// docblock forbids: two copies of this rule drifted into the identical defect before either shipped.
|
|
11
|
+
//
|
|
12
|
+
// So the rule keeps one implementation and gains no edges. `./index.mjs` re-exports it, so every
|
|
13
|
+
// existing importer is untouched.
|
|
14
|
+
//
|
|
15
|
+
// Found by Copilot on the change that closed #264, which had reached for the `index.mjs` export.
|
|
16
|
+
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Is `child` inside `parent`?
|
|
21
|
+
*
|
|
22
|
+
* **The obvious spelling — `!path.relative(parent, child).startsWith("..")` — is wrong, and it fails
|
|
23
|
+
* OPEN.** A name beginning with `..` is an ordinary filename, not a traversal: `path.relative` of
|
|
24
|
+
* `memory/..index.md` against `memory/` is `..index.md`, which `startsWith("..")` calls *outside*.
|
|
25
|
+
* Measured end to end before this was written — an index declared at `memory/..index.md` was written
|
|
26
|
+
* into the store, reported `ok`, and `doctor` then counted it as a second record. That is the ninth
|
|
27
|
+
* fail-open found in this repository's scaffolding, and the first one written by the change that
|
|
28
|
+
* cites the class; `recordedOrigin` in `./compile.mjs` was the second, which is why this moved here.
|
|
29
|
+
*
|
|
30
|
+
* Only `..` exactly, or `..` followed by a separator, means outside. An absolute result means the two
|
|
31
|
+
* paths share no root and cannot contain one another. A path is inside itself.
|
|
32
|
+
*/
|
|
33
|
+
export function isInside(parent, child) {
|
|
34
|
+
const rel = path.relative(parent, child);
|
|
35
|
+
if (rel === "") return true;
|
|
36
|
+
if (path.isAbsolute(rel)) return false;
|
|
37
|
+
return rel !== ".." && !rel.startsWith(`..${path.sep}`);
|
|
38
|
+
}
|