@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.
@@ -43,8 +43,15 @@ class CannotRun extends Error {}
43
43
 
44
44
  const run = (cmd, args, opts = {}) => execFileSync(cmd, args, { encoding: "buffer", maxBuffer: 64 * 1024 * 1024, ...opts });
45
45
 
46
- /** What the package would contain, as repo-relative paths. */
47
- function packedPaths(root) {
46
+ /**
47
+ * What the package would contain, as repo-relative paths.
48
+ *
49
+ * **Exported because `./payload.mjs` classifies exactly this roster**, and two enumerations of *what
50
+ * npm would pack* is the second-carrier defect this repository keeps paying for. That rail asks which
51
+ * files ship; this one asks whether each is byte-identical to its source. One question about
52
+ * membership, asked once, answered here.
53
+ */
54
+ export function packedPaths(root) {
48
55
  let out;
49
56
  try {
50
57
  out = run("npm", ["pack", "--dry-run", "--json"], { cwd: root, stdio: ["ignore", "pipe", "pipe"] }).toString("utf8");
@@ -13,6 +13,13 @@
13
13
  // for a single field. `portulan.version` is per-pack, so it cannot contend. Tying the bundle to the pack
14
14
  // manifests is arm 4 of the issue and is deliberately NOT here.
15
15
  //
16
+ // **Re-measured 2026-08-29: the bundle now agrees with NEITHER pack** — `rituals/checkpoints` moved to
17
+ // `0.3.0` when proposal `0035` added an obligation to its pre-commit verdict, while the bundle stayed
18
+ // `0.2.1` and `tools/github` stayed `0.1.0`. The measurement above is stamped to `c93a819` and remains
19
+ // true of that commit; what changed is that the coincidence it names has ended, which STRENGTHENS the
20
+ // argument rather than dating it. A reader finding three different numbers is looking at three
21
+ // independently versioned things, not at drift.
22
+ //
16
23
  // ## Why the whole `contributes` block, not only `gates`
17
24
  //
18
25
  // The issue's title asks about gate fragments; arm 3's own text says *"touching a pack's `contributes`
@@ -61,7 +61,8 @@ import fs from "node:fs";
61
61
  import path from "node:path";
62
62
  import { pathToFileURL } from "node:url";
63
63
 
64
- import { resolvePack, rootPlan } from "./compile.mjs";
64
+ // `shadowedCopy` rides along: one carrier for *is this pack shadowed*, per #316.
65
+ import { resolvePack, rootPlan, shadowedCopy } from "./compile.mjs";
65
66
  import { AUTO, discoverPackRoots, namedWithAuto } from "./discover.mjs";
66
67
 
67
68
  /**
@@ -82,6 +83,12 @@ import { AUTO, discoverPackRoots, namedWithAuto } from "./discover.mjs";
82
83
  export const RECIPE_SET_READERS = Object.freeze([
83
84
  ".github/workflows/verify.yml",
84
85
  "cli/doctor.mjs",
86
+ // Added 2026-08-25 with milestone 8 clause (d). `cli/drills.mjs` reads the set to know which rails
87
+ // it must drill — and it is the reader that shows the live sweep's own boundary: it calls
88
+ // `recipeSet()` rather than enumerating `verify.recipes`, so the sweep for an undeclared
89
+ // *enumeration* could not see it and this roster went stale in the one way the pair above was
90
+ // arranged not to allow. Raised as a suppressed note by Copilot, round 1 on #343.
91
+ "cli/drills.mjs",
85
92
  "cli/stop-gate.mjs",
86
93
  "cli/vendor.mjs",
87
94
  ]);
@@ -288,6 +295,46 @@ export function resolverFor({ workspaceDir, manifest, repoRoot = ".", named = []
288
295
  if (plan.refusal) throw new Error(`recipe-set: ${plan.refusal}`);
289
296
  if (plan.couldNotRun) throw new Error(`recipe-set: ${plan.couldNotRun}`);
290
297
  const roots = plan.roots ?? [];
298
+ const originAt = (r) => (plan.origins ?? []).find((o) => path.resolve(o.root) === path.resolve(r))?.origin;
299
+
300
+ // **A shadowed pack is refused here too** (#318), and this tool's ground is the strongest of the
301
+ // three. `compile` refuses even an agreeing shadow because its artifact records which root
302
+ // answered; `index` refuses one although its artifact deliberately does not. Here the answering
303
+ // root is not merely recorded — **it is spliced into the output**: `${PACK_ROOT}` expands to the
304
+ // root that answered, so two copies whose manifests are byte-identical still yield different `run`
305
+ // lines pointing at different files. What CI runs and what a bare local run runs would diverge,
306
+ // with nothing in either output to say so.
307
+ //
308
+ // So the agreeing case is not a carve-out anyone could argue for here, and the honest predicate was
309
+ // never manifest comparison: it is output equality, computable only by resolving both worlds.
310
+ // Refusing the ambiguity is what remains.
311
+ //
312
+ // **Checked HERE rather than inside the returned closure**, which is where a first cut put it. The
313
+ // closure runs during `recipeSet`, outside the `try` that wraps this constructor — so the refusal
314
+ // escaped as an uncaught throw, exit 1 with a stack trace, in a tool whose whole contract is exit 2
315
+ // for could-not-run. Measured, not reasoned about. Throwing from the constructor puts it on the
316
+ // path `plan.refusal` already uses and the caller already catches.
317
+ //
318
+ // **Only where the caller did not choose** — a named root replaces the derived one, `auto` is
319
+ // discovery elected — which is `compile`'s guard shape, bound to it deliberately.
320
+ if (!forced && plan.source !== "named") {
321
+ const declared = Array.isArray(manifest?.packs) ? manifest.packs : [];
322
+ for (const ref of declared) {
323
+ const found = resolvePack(String(ref), roots);
324
+ if (!found?.dir) continue;
325
+ // Path-resolved, matching `compile` — see the sibling note in `index.mjs`.
326
+ const behind = shadowedCopy(String(ref), originAt(found.root), roots, originAt);
327
+ if (!behind) continue;
328
+ throw new Error(
329
+ `recipe-set: \`${ref}\` is SHADOWED — it resolved under ${found.root}, a root discovered on ` +
330
+ `this host, while the root ${path.relative(path.resolve(repoRoot), behind.root)} also carries it. ` +
331
+ "A composed recipe's ${PACK_ROOT} expands to whichever root answered, so the two compose to " +
332
+ "run lines pointing at different files. Refusing to pick: name the root — `--pack-root packs` " +
333
+ "for the tree, which is what CI runs, or `--pack-root auto` for the installed copy.",
334
+ );
335
+ }
336
+ }
337
+
291
338
  return (ref) => {
292
339
  const found = resolvePack(ref, roots);
293
340
  // `resolvePack` returns `manifest` as the PATH to `pack.json`, not as the parsed object —