@sleepy_panda_srl/portulan 0.1.2 → 0.2.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 +142 -291
- package/cli/advisory.mjs +354 -0
- package/cli/comments.mjs +932 -0
- package/cli/compile.mjs +1443 -1210
- package/cli/context.mjs +883 -0
- package/cli/control-chars.mjs +12 -301
- package/cli/discover.mjs +17 -574
- package/cli/doctor.mjs +225 -1071
- package/cli/drills.mjs +1107 -0
- package/cli/feedback.mjs +16 -237
- package/cli/finish.mjs +437 -0
- package/cli/form.mjs +644 -0
- package/cli/fuzz-shell.mjs +595 -0
- package/cli/gate.mjs +7 -198
- package/cli/goldens.mjs +311 -0
- package/cli/index.mjs +305 -655
- package/cli/init.mjs +244 -564
- package/cli/inside.mjs +3 -28
- package/cli/instructions.mjs +829 -0
- package/cli/ledger.mjs +778 -0
- package/cli/librarian.mjs +135 -599
- package/cli/manifest.mjs +2 -39
- package/cli/mutants.mjs +764 -0
- package/cli/new.mjs +12 -243
- package/cli/pack-identity.mjs +6 -68
- package/cli/pack-version.mjs +13 -248
- package/cli/plugin-lint.mjs +42 -658
- package/cli/portulan.mjs +5 -124
- package/cli/recipe-set.mjs +16 -240
- package/cli/release-eval.mjs +748 -0
- package/cli/review-meter.mjs +500 -0
- package/cli/rule-carriers.mjs +12 -175
- package/cli/sessions.mjs +82 -0
- package/cli/skill-goldens.mjs +424 -0
- package/cli/skills-set.mjs +12 -414
- package/cli/stop-gate.mjs +126 -454
- package/cli/symbols.mjs +1132 -0
- package/cli/telemetry.mjs +582 -0
- package/cli/upgrade.mjs +171 -314
- package/cli/vendor.mjs +146 -476
- package/cli/version-carriers.mjs +9 -74
- package/core/README.md +1 -1
- package/core/engine.md +3 -9
- package/core/operating/autonomy.md +1 -1
- package/core/operating/context.md +183 -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/librarian.md +2 -2
- package/core/skills/consolidate/SKILL.md +4 -2
- package/core/templates/gate-policy.md +27 -8
- package/core/templates/handoff.md +28 -15
- package/core/templates/memory-entry.md +1 -0
- package/evals/README.md +320 -0
- package/evals/releases/0.1.3.json +134 -0
- package/evals/releases/0.1.3.md +64 -0
- package/evals/releases/0.2.0.json +146 -0
- package/evals/releases/0.2.0.md +67 -0
- package/evals/releases/README.md +32 -0
- package/package.json +9 -2
- package/packs/rituals/checkpoints/README.md +9 -2
- package/packs/rituals/checkpoints/pack.json +1 -1
- package/packs/rituals/checkpoints/skills/pre-commit/SKILL.md +32 -2
- package/packs/tools/github/verify/actions-pinned.sh +10 -55
- package/spec/README.md +112 -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 +359 -23
- package/spec/workspace.schema.json +145 -4
package/cli/pack-version.mjs
CHANGED
|
@@ -1,67 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// A change to a pack's `contributes` must move that pack's `portulan.version`.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// convention; **a prose-only edit counts**, so a fragment whose `reason` alone changes owes the bump;
|
|
6
|
-
// and the field is **`pack.json`'s `portulan.version`**, not the packs plugin's bundle version.
|
|
7
|
-
//
|
|
8
|
-
// ## Why the field is the pack's own
|
|
9
|
-
//
|
|
10
|
-
// `packs/.claude-plugin/plugin.json` versions the *bundle*. Measured on `c93a819`: it reads `0.2.1`,
|
|
11
|
-
// `rituals/checkpoints` reads `0.2.1` and `tools/github` reads `0.1.0` — so the bundle agrees with one
|
|
12
|
-
// pack by coincidence and cannot track both, and two packs changing in one pull request would contend
|
|
13
|
-
// for a single field. `portulan.version` is per-pack, so it cannot contend. Tying the bundle to the pack
|
|
14
|
-
// manifests is arm 4 of the issue and is deliberately NOT here.
|
|
15
|
-
//
|
|
16
|
-
// ## Why the whole `contributes` block, not only `gates`
|
|
17
|
-
//
|
|
18
|
-
// The issue's title asks about gate fragments; arm 3's own text says *"touching a pack's `contributes`
|
|
19
|
-
// block"*, and arm 3 is what was ruled — confirmed by the maintainer when the two readings were put to
|
|
20
|
-
// him. Skills, personas and verify recipes reach every composing workspace by the same route a gate
|
|
21
|
-
// fragment does.
|
|
22
|
-
//
|
|
23
|
-
// ## THREE-DOT, and it is not a detail
|
|
24
|
-
//
|
|
25
|
-
// The comparison is `base...head` — from the **merge-base** to the head — never `base..head`.
|
|
26
|
-
// Demonstrated rather than asserted, and pinned in `./pack-version.test.mjs`: with a branch that changes
|
|
27
|
-
// `pack.json` while the base branch independently changes another file, two-dot reports BOTH files and
|
|
28
|
-
// three-dot reports only the branch's. A rail on two-dot would red a pull request because somebody else
|
|
29
|
-
// bumped a version upstream, and would read a `contributes` change the pull request never made.
|
|
30
|
-
//
|
|
31
|
-
// `git diff A...B` is exactly `git diff $(git merge-base A B) B`, so this resolves the merge-base once
|
|
32
|
-
// and reads each base manifest at that commit. No diff is parsed: the manifests are compared as VALUES,
|
|
33
|
-
// which is what makes a reformatting-only edit — whitespace, key order — correctly *not* a change.
|
|
34
|
-
//
|
|
35
|
-
// ## Exit codes, and the middle one is the point
|
|
36
|
-
//
|
|
37
|
-
// 0 every changed pack moved its version
|
|
38
|
-
// 1 a pack changed `contributes` and did not move `portulan.version`
|
|
39
|
-
// 2 could not run — no repository, no base ref, no merge-base, or a manifest that will not parse
|
|
40
|
-
//
|
|
41
|
-
// **2 is never 1.** A rail that cannot read its input must not report green, and must not report red
|
|
42
|
-
// either: a red verdict is a claim about the work, and "I could not look" is a claim about the check.
|
|
43
|
-
// That is `../.portulan/memory/verify-preconditions-fail-closed.md`'s rule and the doctrine `#208` is
|
|
44
|
-
// open about for the sibling scanner. It matters most here because `actions/checkout` is shallow by
|
|
45
|
-
// default — `../cli/librarian.mjs` calls that "not a theoretical clone; it is the *normal* one" — so a
|
|
46
|
-
// missing merge-base is the ordinary case on an unprepared runner, not an exotic one.
|
|
47
|
-
//
|
|
48
|
-
// ## What "moved" means, stated because the ruling's word was "moves"
|
|
49
|
-
//
|
|
50
|
-
// **Any change to the string counts, including absent → present.** It is deliberately NOT "the SemVer
|
|
51
|
-
// increased": this rail enforces the maintainer's word, and reading an ordering into it would be the
|
|
52
|
-
// rail deciding a policy it was not given — `0.2.0` → `0.1.0` is a different question, and a bad answer
|
|
53
|
-
// to it, but it is not this one. Absent → present has to count, or a pack red for having no version
|
|
54
|
-
// could never discharge that red. Present → absent is the `unversioned` red, from the head side.
|
|
55
|
-
//
|
|
56
|
-
// ## What this rail does NOT see, named rather than left to be discovered
|
|
57
|
-
//
|
|
58
|
-
// It compares the `contributes` **block**. Content the block merely *points at* — a skill's `SKILL.md`
|
|
59
|
-
// body, a persona file, the shell script a contributed verify recipe names — reaches every adopting
|
|
60
|
-
// workspace and can change without touching `pack.json` at all. Inline material is covered: gate
|
|
61
|
-
// fragments live in the manifest, and so do recipe objects. Pointed-at files are not. That is `#259`'s
|
|
62
|
-
// class — a rail narrower than the sentence around it — and it is stated here rather than closed,
|
|
63
|
-
// because widening it is a different change with a different argument. A rename, or a delete-plus-add
|
|
64
|
-
// under a new name, likewise passes as `deleted` + `added`.
|
|
4
|
+
// Exit 0 every changed pack moved its version · 1 one did not · 2 could not run.
|
|
65
5
|
|
|
66
6
|
import fs from "node:fs";
|
|
67
7
|
import path from "node:path";
|
|
@@ -73,12 +13,6 @@ export class CannotRun extends Error {}
|
|
|
73
13
|
|
|
74
14
|
const DEFAULT_BASE = "origin/main";
|
|
75
15
|
|
|
76
|
-
/**
|
|
77
|
-
* Run git, or say why it could not.
|
|
78
|
-
*
|
|
79
|
-
* `stdio` pins stderr to a pipe so a failure's own message rides into the exception rather than
|
|
80
|
-
* leaking to the terminal and leaving the thrown error to say only "command failed".
|
|
81
|
-
*/
|
|
82
16
|
function git(root, args, what) {
|
|
83
17
|
try {
|
|
84
18
|
return execFileSync("git", ["-C", root, ...args], {
|
|
@@ -90,38 +24,10 @@ function git(root, args, what) {
|
|
|
90
24
|
}
|
|
91
25
|
}
|
|
92
26
|
|
|
93
|
-
|
|
94
|
-
* The merge-base of `base` and `HEAD`, which IS the three-dot comparison point.
|
|
95
|
-
*
|
|
96
|
-
* **A shallow clone reaches this function by TWO different routes, and an earlier draft of these
|
|
97
|
-
* messages knew about only one.** Measured on git, both shapes built as fixtures:
|
|
98
|
-
*
|
|
99
|
-
* - `--depth 1` alone is also `--single-branch`, so `origin/main` is **not fetched at all** and the
|
|
100
|
-
* failure is an unknown ref. This is the common CI shape.
|
|
101
|
-
* - `--depth 1 --no-single-branch` fetches the ref and truncates its history, so the ref resolves and
|
|
102
|
-
* **no merge-base exists**.
|
|
103
|
-
*
|
|
104
|
-
* So both messages name `fetch-depth: 0` — a reader who lands on either one is most likely looking at a
|
|
105
|
-
* shallow checkout — while staying distinguishable, because the *other* causes differ: an unknown ref
|
|
106
|
-
* can also be a typo or an unfetched remote, and a missing merge-base can also be genuinely unrelated
|
|
107
|
-
* histories. Naming only one of the two would send half of the readers to the wrong repair.
|
|
108
|
-
*/
|
|
109
|
-
// **`--end-of-options` on every call that takes the caller's ref**, because `base` is user-supplied —
|
|
110
|
-
// `PORTULAN_BASE_REF`, or `--base` — and git will otherwise read a value that starts with `-` as a FLAG.
|
|
111
|
-
// Measured on git 2.50.1: `git merge-base --is-ancestor HEAD` is parsed as the `--is-ancestor` option and
|
|
112
|
-
// exits 129, so the command silently stops being a merge-base lookup and becomes an ancestry test. With
|
|
113
|
-
// the guard it exits 128, refused as a ref, which is what a bad ref should do.
|
|
114
|
-
//
|
|
115
|
-
// `rev-parse` was already shielded by accident rather than by design: the `^{commit}` suffix makes
|
|
116
|
-
// `--help^{commit}` an unparseable revision rather than a flag. Guarded anyway, so the protection is
|
|
117
|
-
// stated instead of resting on a suffix somebody could reasonably remove.
|
|
118
|
-
//
|
|
119
|
-
// _(Copilot proposed `--` for both, and it is the wrong separator here: measured,
|
|
120
|
-
// `git rev-parse --verify -- HEAD^{commit}` exits **128** — `--` marks the start of PATHS for rev-parse,
|
|
121
|
-
// so the fix would have broken every call. Raised on #274, round 2; the concern was right and the
|
|
122
|
-
// mechanism was not.)_
|
|
27
|
+
// `base` is user-supplied; `--end-of-options` keeps a leading `-` from reading as a flag (`--` means paths).
|
|
123
28
|
const END = "--end-of-options";
|
|
124
29
|
|
|
30
|
+
/** The merge-base of `base` and HEAD: comparing from it, never from `base`, leaves upstream changes out. */
|
|
125
31
|
export function mergeBase(root, base = DEFAULT_BASE) {
|
|
126
32
|
const shallowHint =
|
|
127
33
|
"`actions/checkout` is shallow by default — `../cli/librarian.mjs` calls that the normal clone rather than a " +
|
|
@@ -129,16 +35,6 @@ export function mergeBase(root, base = DEFAULT_BASE) {
|
|
|
129
35
|
try {
|
|
130
36
|
git(root, ["rev-parse", "--verify", END, `${base}^{commit}`], `resolve ${base}`);
|
|
131
37
|
} catch (cause) {
|
|
132
|
-
// The cause rides along. A bare `catch {}` here reported EVERY failure of this command as an
|
|
133
|
-
// unknown ref — including a corrupt object store, a permissions failure, or a git that would not
|
|
134
|
-
// start — so the message named the one cause it could think of and discarded the one git gave.
|
|
135
|
-
// `git()` has already captured that stderr; throwing it away and substituting a guess is the
|
|
136
|
-
// wrong-mechanism-in-a-user-facing-string class this repository keeps finding. Raised by Copilot
|
|
137
|
-
// on #274, round 4 — and its round 6 caught that the FIX was half-done: the cause rode along
|
|
138
|
-
// while the leading clause still asserted `is not in this repository`, so the sentence stated one
|
|
139
|
-
// cause as fact and listed the alternatives underneath it. It now says what HAPPENED (could not
|
|
140
|
-
// resolve) and ranks the causes as causes. The defect class named in this very comment, surviving
|
|
141
|
-
// one round inside the repair for it.
|
|
142
38
|
throw new CannotRun(
|
|
143
39
|
`could not resolve base ref \`${base}\` — refusing to report a verdict against a ref nothing could read. ` +
|
|
144
40
|
`The usual cause is a SHALLOW single-branch clone, which does not fetch it at all; a typo or an ` +
|
|
@@ -157,14 +53,7 @@ export function mergeBase(root, base = DEFAULT_BASE) {
|
|
|
157
53
|
}
|
|
158
54
|
}
|
|
159
55
|
|
|
160
|
-
/**
|
|
161
|
-
* Is this path THERE — as a directory entry, not as something openable?
|
|
162
|
-
*
|
|
163
|
-
* `fs.existsSync` follows symlinks, so it answers **false** for a dangling link and the entry disappears
|
|
164
|
-
* from every listing built on it. That is the same absent-versus-unreadable confusion this file has now
|
|
165
|
-
* repaired at three sites, and this helper is the sweep `0020` asks for *at the first fix* rather than
|
|
166
|
-
* after a third round found the third one: existence is `lstat`, readability is whoever opens the file.
|
|
167
|
-
*/
|
|
56
|
+
/** `lstat`, because `existsSync` follows a symlink and answers false for a dangling one. */
|
|
168
57
|
function presentOnDisk(file) {
|
|
169
58
|
try {
|
|
170
59
|
fs.lstatSync(file);
|
|
@@ -174,27 +63,7 @@ function presentOnDisk(file) {
|
|
|
174
63
|
}
|
|
175
64
|
}
|
|
176
65
|
|
|
177
|
-
/**
|
|
178
|
-
* `--packs` as a repository-relative path, or a refusal.
|
|
179
|
-
*
|
|
180
|
-
* The flag is documented as *relative to the repository root* and, until #274's round 2, the parser took
|
|
181
|
-
* that on trust. Measured: `path.join("/repo", "../../etc")` is `/etc`, so `--packs ../..` sent the
|
|
182
|
-
* filesystem scan **outside the repository** — and the escape was invisible in the result, because
|
|
183
|
-
* `git ls-tree` then refused the same path and the run ended at exit 2 for a reason that named the wrong
|
|
184
|
-
* thing. Containment is checked after resolution rather than by pattern, since a `..` chain satisfies any
|
|
185
|
-
* regex and still escapes; the same shape `../compile.mjs`'s `policyPath` uses for the same reason.
|
|
186
|
-
*
|
|
187
|
-
* An absolute path is refused outright rather than silently re-rooted: `path.join` turns `/etc` into
|
|
188
|
-
* `<root>/etc`, which is *contained* and is not what the caller asked for, and a flag that quietly means
|
|
189
|
-
* something else is worse than one that refuses.
|
|
190
|
-
*/
|
|
191
|
-
// **It returns the CANONICAL path, and the first draft returned the caller's spelling.** That was a
|
|
192
|
-
// correctness defect rather than untidiness, and it produced a false green. `--packs packs/../packs`
|
|
193
|
-
// validated fine — it resolves inside — and then the two halves of `compare` disagreed about what a pack
|
|
194
|
-
// is called: the filesystem scan joined the raw value (`packs/../packs/tools/github/pack.json`) while
|
|
195
|
-
// `git ls-tree` canonicalised its pathspec (`packs/tools/github/pack.json`). Measured: every pack was
|
|
196
|
-
// reported TWICE, once `added` from the uncanonical spelling and once `unchanged` from git's — and
|
|
197
|
-
// `added` is exempt, so a real unbumped change would have passed. Raised by Copilot on #274, round 3.
|
|
66
|
+
/** `--packs`, canonical and repository-relative, so the disk scan and `git ls-tree` spell a pack alike. */
|
|
198
67
|
export function insideRepo(value) {
|
|
199
68
|
if (path.isAbsolute(value)) {
|
|
200
69
|
throw new CannotRun(
|
|
@@ -202,9 +71,6 @@ export function insideRepo(value) {
|
|
|
202
71
|
`joining an absolute path would silently re-root it under the repository instead of refusing.`,
|
|
203
72
|
);
|
|
204
73
|
}
|
|
205
|
-
// Normalised, then checked — a `..` chain satisfies any pattern and still escapes, so containment is
|
|
206
|
-
// decided on the resolved form. Separators are forced to `/` because every downstream consumer is a
|
|
207
|
-
// git pathspec or a repository-relative id, neither of which is a platform path.
|
|
208
74
|
const canonical = path.normalize(value).split(path.sep).join("/").replace(/\/+$/, "");
|
|
209
75
|
if (canonical === "" || canonical === "." || canonical === ".." || canonical.startsWith("../")) {
|
|
210
76
|
throw new CannotRun(
|
|
@@ -215,17 +81,6 @@ export function insideRepo(value) {
|
|
|
215
81
|
return canonical;
|
|
216
82
|
}
|
|
217
83
|
|
|
218
|
-
/**
|
|
219
|
-
* Every pack manifest in the tree, as repository-relative paths, sorted so output is stable.
|
|
220
|
-
*
|
|
221
|
-
* **ENOENT is the only benign failure, and an earlier draft of this function treated every failure as
|
|
222
|
-
* benign.** A bare `catch { return [] }` turns an unreadable `packs/` — wrong permissions, a broken
|
|
223
|
-
* symlink, a mount that went away — into *the empty set*, and an empty set is a **green**. That is the
|
|
224
|
-
* shape `../.portulan/memory/verify-preconditions-fail-closed.md` exists about: a recipe that enumerates
|
|
225
|
-
* nothing and reports success has described the machine rather than the work. So the absent directory is
|
|
226
|
-
* an answer (a workspace that composes nothing has no manifests to check) and everything else is a
|
|
227
|
-
* refusal. Found by the session-open checkpoint, which asked what guards the enumeration.
|
|
228
|
-
*/
|
|
229
84
|
export function packManifests(root, packsDir = "packs") {
|
|
230
85
|
const base = path.join(root, packsDir);
|
|
231
86
|
const found = [];
|
|
@@ -255,21 +110,7 @@ export function packManifests(root, packsDir = "packs") {
|
|
|
255
110
|
return found.sort();
|
|
256
111
|
}
|
|
257
112
|
|
|
258
|
-
|
|
259
|
-
// this wrong in the same way, one function apart from the `readdir` guard that got it right — a fix that
|
|
260
|
-
// missed its own siblings inside a single change, which is `0020`'s shape at its smallest. Demonstrated:
|
|
261
|
-
// `chmod 000` on a tracked `pack.json` made this report the pack **deleted** and exit **0**, so a
|
|
262
|
-
// permissions accident read as an intentional removal and the whole rail went quiet. Found by the
|
|
263
|
-
// pre-commit checkpoint, which attacked the readers after the enumeration guard was added rather than
|
|
264
|
-
// assuming the class had been swept.
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* A manifest as it stood at `commit`, or `null` where it genuinely did not exist there.
|
|
268
|
-
*
|
|
269
|
-
* `present` is the caller's listing from `ls-tree` at that same commit, and it is what decides absence —
|
|
270
|
-
* `git show` failing is then always a refusal, never a shrug. Matching on git's stderr would be the
|
|
271
|
-
* other way to tell the two apart, and it would be a string comparison against another tool's prose.
|
|
272
|
-
*/
|
|
113
|
+
/** A manifest at `commit`, or `null` where the caller's `ls-tree` listing lacks it; a failed read throws. */
|
|
273
114
|
export function manifestAt(root, commit, rel, present) {
|
|
274
115
|
if (!present) return null;
|
|
275
116
|
const raw = git(root, ["show", `${commit}:${rel}`], `read ${rel} at ${commit.slice(0, 7)}`);
|
|
@@ -283,26 +124,10 @@ export function manifestAt(root, commit, rel, present) {
|
|
|
283
124
|
}
|
|
284
125
|
}
|
|
285
126
|
|
|
286
|
-
/**
|
|
287
|
-
* The manifest in the working tree, or `null` where the pack has genuinely been deleted.
|
|
288
|
-
*
|
|
289
|
-
* **`lstat` decides whether the file is there; `readFileSync`'s ENOENT does not.** Third instance of one
|
|
290
|
-
* class in this file, and the sharpest: a **dangling symlink** at `pack.json` makes `readFileSync` throw
|
|
291
|
-
* ENOENT while the path itself plainly exists — measured, `lstat` succeeds and reports
|
|
292
|
-
* `isSymbolicLink: true` — so an ENOENT-only carve-out reported a present-but-unreadable manifest as a
|
|
293
|
-
* pack somebody deleted, at exit 0. The same shape as the `readdir` guard and the absent-vs-unreadable
|
|
294
|
-
* repair before it, at a site both of those left alone.
|
|
295
|
-
*
|
|
296
|
-
* The lstat-first pattern is not invented here: `./control-chars.mjs`'s `bytesOf` already does exactly
|
|
297
|
-
* this, for exactly this reason, down to the sentence — *"Refusing to report it clean: this is a fact
|
|
298
|
-
* about the filesystem, not about the file."* Raised by Copilot on #274, round 7, which also supplied the
|
|
299
|
-
* precedent.
|
|
300
|
-
*/
|
|
301
127
|
export function manifestHere(root, rel) {
|
|
302
128
|
const file = path.join(root, rel);
|
|
303
129
|
try {
|
|
304
|
-
// `lstat`, not `stat`:
|
|
305
|
-
// which is the very confusion this guard exists to end.
|
|
130
|
+
// `lstat`, not `stat`: a dangling symlink is there and unreadable, never a deleted pack.
|
|
306
131
|
fs.lstatSync(file);
|
|
307
132
|
} catch (cause) {
|
|
308
133
|
if (cause.code === "ENOENT") return null;
|
|
@@ -315,8 +140,6 @@ export function manifestHere(root, rel) {
|
|
|
315
140
|
try {
|
|
316
141
|
raw = fs.readFileSync(file, "utf8");
|
|
317
142
|
} catch (cause) {
|
|
318
|
-
// The path EXISTS — `lstat` just said so — so every failure here is the check unable to look:
|
|
319
|
-
// a dangling symlink, EACCES, EISDIR, a symlink loop. None of them is a deletion.
|
|
320
143
|
throw new CannotRun(
|
|
321
144
|
`cannot read ${rel} — ${cause.code ?? cause.message}. The path exists, so this is not a removal: a ` +
|
|
322
145
|
`dangling symlink, an unreadable mode or a directory in its place all land here.`,
|
|
@@ -329,20 +152,13 @@ export function manifestHere(root, rel) {
|
|
|
329
152
|
}
|
|
330
153
|
}
|
|
331
154
|
|
|
332
|
-
/**
|
|
333
|
-
* Deep value equality over parsed JSON, which is deliberately NOT a text comparison.
|
|
334
|
-
*
|
|
335
|
-
* Reformatting a manifest — reindenting, reordering keys — changes its bytes and changes nothing a
|
|
336
|
-
* composing workspace yields, so it must not owe a version bump. Comparing values is what makes that
|
|
337
|
-
* true, and it is why this reads the manifests rather than parsing `git diff`.
|
|
338
|
-
*/
|
|
155
|
+
/** Deep equality over parsed JSON, so a reformatted manifest is not a changed one. */
|
|
339
156
|
export function sameValue(a, b) {
|
|
340
157
|
if (a === b) return true;
|
|
341
158
|
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
342
159
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
343
160
|
if (Array.isArray(a)) {
|
|
344
|
-
// Order
|
|
345
|
-
// applies fragments in the order it finds them, so a reordering can change what a workspace yields.
|
|
161
|
+
// Order is significant: gate fragments apply in the order they are listed.
|
|
346
162
|
return a.length === b.length && a.every((item, i) => sameValue(item, b[i]));
|
|
347
163
|
}
|
|
348
164
|
const ka = Object.keys(a).sort();
|
|
@@ -350,22 +166,12 @@ export function sameValue(a, b) {
|
|
|
350
166
|
return ka.length === kb.length && ka.every((k, i) => k === kb[i]) && ka.every((k) => sameValue(a[k], b[k]));
|
|
351
167
|
}
|
|
352
168
|
|
|
353
|
-
/**
|
|
354
|
-
* Judge one pack. Returns `{ rel, verdict, why }` with verdict `ok` | `unchanged` | `added` | `deleted`
|
|
355
|
-
* | `stale` | `unversioned`.
|
|
356
|
-
*
|
|
357
|
-
* The two reds are told apart because they need different sentences: `stale` has a version and did not
|
|
358
|
-
* move it, `unversioned` has no version to move.
|
|
359
|
-
*/
|
|
360
169
|
export function judge(rel, before, after) {
|
|
361
170
|
if (after === null) return { rel, verdict: "deleted", why: "the pack was removed" };
|
|
362
171
|
if (before === null) {
|
|
363
|
-
// An added pack has no prior state to bump FROM, so no bump is owed. Whether a new pack must
|
|
364
|
-
// declare a version at all is a Pack Definition question — `spec/pack.schema.json` currently
|
|
365
|
-
// makes `portulan.version` optional — and annexing it here would be this rail deciding a schema
|
|
366
|
-
// rule it was not given. Named in `#265` as a hole with a shape, left to the schema.
|
|
367
172
|
return { rel, verdict: "added", why: "the pack is new — nothing to bump from" };
|
|
368
173
|
}
|
|
174
|
+
// The block alone: a file it points at, such as a skill's `SKILL.md`, can change unseen.
|
|
369
175
|
if (sameValue(before?.contributes, after?.contributes)) {
|
|
370
176
|
return { rel, verdict: "unchanged", why: "`contributes` is unchanged" };
|
|
371
177
|
}
|
|
@@ -391,24 +197,9 @@ export function judge(rel, before, after) {
|
|
|
391
197
|
return { rel, verdict: "ok", why: `\`contributes\` changed and \`portulan.version\` moved \`${was ?? "(absent)"}\` → \`${now}\`` };
|
|
392
198
|
}
|
|
393
199
|
|
|
394
|
-
/**
|
|
395
|
-
* Compare every pack in the tree against the merge-base.
|
|
396
|
-
*
|
|
397
|
-
* `packsNamed` says whether the caller *chose* `packsDir`, and it decides what an empty result means.
|
|
398
|
-
* **A directory that is absent is an answer only when nobody named it.** The default `packs/` missing is
|
|
399
|
-
* a workspace that composes nothing — green, correctly. But `--packs paks`, a typo, found nothing at head
|
|
400
|
-
* and nothing at the merge-base and reported *"nothing to check"* at exit **0**: a false green, and a
|
|
401
|
-
* bypass anyone experimenting locally could reach by accident. Measured. So a named directory that exists
|
|
402
|
-
* in neither tree is a refusal, and the ENOENT carve-out in `packManifests` keeps its narrower job.
|
|
403
|
-
* Raised by Copilot on #274, round 5 — on the `Copilot` login of the comments endpoint rather than the
|
|
404
|
-
* `copilot-pull-request-reviewer[bot]` login of `/reviews`, which is the two-logins gotcha this
|
|
405
|
-
* repository already records.
|
|
406
|
-
*/
|
|
200
|
+
/** Every pack against the merge-base; a `packsDir` the caller named that neither tree holds is refused. */
|
|
407
201
|
export function compare(root, { base = DEFAULT_BASE, packsDir = "packs", packsNamed = false } = {}) {
|
|
408
202
|
const at = mergeBase(root, base);
|
|
409
|
-
// The union of what exists NOW and what existed at the merge-base — a pack deleted in this change
|
|
410
|
-
// has no working-tree manifest and would otherwise never be looked at. It is green either way, but
|
|
411
|
-
// being green by having been considered is not the same as being green by being invisible.
|
|
412
203
|
const here = packManifests(root, packsDir);
|
|
413
204
|
const there = git(root, ["ls-tree", "-r", "--name-only", at, "--", `${packsDir}/`], `list packs at ${at.slice(0, 7)}`)
|
|
414
205
|
.split("\n")
|
|
@@ -431,10 +222,6 @@ export function compare(root, { base = DEFAULT_BASE, packsDir = "packs", packsNa
|
|
|
431
222
|
|
|
432
223
|
function usage() {
|
|
433
224
|
return [
|
|
434
|
-
// NOT `portulan pack-version` — `./portulan.mjs` has eight subcommands and this is not one of
|
|
435
|
-
// them (measured: it answers "unknown subcommand"), and its own header forbids minting a ninth.
|
|
436
|
-
// The sibling checkers carry no `portulan <name>` banner either. This is the form the verify
|
|
437
|
-
// recipe actually runs, which is the only invocation that exists.
|
|
438
225
|
"pack-version — a change to a pack's `contributes` must move its `portulan.version`",
|
|
439
226
|
"",
|
|
440
227
|
" node cli/pack-version.mjs [--base <ref>] [--packs <dir>] [<repository-root>]",
|
|
@@ -450,8 +237,7 @@ function usage() {
|
|
|
450
237
|
}
|
|
451
238
|
|
|
452
239
|
export function run(argv = [], { stdout = process.stdout, stderr = process.stderr, cwd = process.cwd() } = {}) {
|
|
453
|
-
//
|
|
454
|
-
// the rest of the command line — the contract `./portulan.mjs` states.
|
|
240
|
+
// First, so a request for help is never outranked by a complaint about the other arguments.
|
|
455
241
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
456
242
|
stdout.write(`${usage()}\n`);
|
|
457
243
|
return 0;
|
|
@@ -466,12 +252,6 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
|
|
|
466
252
|
const which = argv[i];
|
|
467
253
|
const value = argv[i + 1];
|
|
468
254
|
i += 1;
|
|
469
|
-
// A single leading `-` is refused too, not just `--`: `--base --packs` would otherwise
|
|
470
|
-
// consume the next flag as a ref and fail later as an unreadable one. The three tools in
|
|
471
|
-
// `cli/` that take a directory already read it this way; this aligns with them rather
|
|
472
|
-
// than inventing. _(A draft used `--base -h` as the example, which never reaches here:
|
|
473
|
-
// `--help` is answered before any other argument decision, so that spelling exits 0 on
|
|
474
|
-
// the help path. Measured.)_
|
|
475
255
|
if (value === undefined || value.startsWith("-")) throw new CannotRun(`${which} needs a value`);
|
|
476
256
|
if (which === "--base") base = value;
|
|
477
257
|
else {
|
|
@@ -487,9 +267,6 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
|
|
|
487
267
|
}
|
|
488
268
|
}
|
|
489
269
|
const where = path.resolve(root ?? cwd);
|
|
490
|
-
// Resolved to the repository top level rather than trusting the argument: a caller invoking this
|
|
491
|
-
// from a subdirectory would otherwise have every `packs/…` path resolved against the wrong root,
|
|
492
|
-
// which is #131's class — paths against the author's layout.
|
|
493
270
|
const top = git(where, ["rev-parse", "--show-toplevel"], `find a git repository at ${where}`).trim();
|
|
494
271
|
|
|
495
272
|
const { at, results } = compare(top, { base, packsDir, packsNamed });
|
|
@@ -497,10 +274,7 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
|
|
|
497
274
|
const moved = results.filter((r) => r.verdict === "ok");
|
|
498
275
|
|
|
499
276
|
stdout.write(`pack-version: comparing against \`${base}\` at merge-base ${at.slice(0, 7)} (three-dot)\n`);
|
|
500
|
-
// Every pack is printed,
|
|
501
|
-
// one that examined nothing — the same reason `compile` prints its refusals on every run. The
|
|
502
|
-
// `ok` rows carry their `why` too, so a green states WHICH version moved rather than leaving a
|
|
503
|
-
// reader to diff the manifest to find out what the rail approved.
|
|
277
|
+
// Every pack is printed, green included, so a green shows what was examined.
|
|
504
278
|
for (const r of results) {
|
|
505
279
|
stdout.write(` ${r.verdict.padEnd(12)} ${r.rel}${r.verdict === "ok" ? ` — ${r.why}` : ""}\n`);
|
|
506
280
|
}
|
|
@@ -519,15 +293,6 @@ export function run(argv = [], { stdout = process.stdout, stderr = process.stder
|
|
|
519
293
|
stderr.write(`pack-version: ${error.message}\n`);
|
|
520
294
|
return 2;
|
|
521
295
|
}
|
|
522
|
-
// **A CRASH IS A COULD-NOT-RUN, NOT A RED**, and this file's own header says so two screens up
|
|
523
|
-
// while an earlier draft did the opposite: an uncaught throw left node exiting **1**, which the
|
|
524
|
-
// recipe faithfully printed as *"RED — verify recipe failed"*. So a TypeError in this checker
|
|
525
|
-
// accused the pull request of a policy breach it had not committed — the exact defect `#208` is
|
|
526
|
-
// open about in the sibling scanner, shipped new in the change that cites it. Demonstrated by
|
|
527
|
-
// the pre-commit checkpoint with an injected throw.
|
|
528
|
-
//
|
|
529
|
-
// The stack is printed rather than swallowed: a crash must stay loud, it just must not wear a
|
|
530
|
-
// verdict it did not earn.
|
|
531
296
|
stderr.write(`pack-version: CRASHED — ${error?.stack ?? error}\n`);
|
|
532
297
|
stderr.write("pack-version: reporting could-not-run (2) rather than a red verdict — a defect in this checker is not a finding about the work.\n");
|
|
533
298
|
return 2;
|