@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/cli/doctor.mjs CHANGED
@@ -43,7 +43,16 @@ import { fileURLToPath, pathToFileURL } from "node:url";
43
43
  // it looks like enforcement that quietly stopped covering something. Same reasoning that has
44
44
  // ./gate.mjs import the matcher instead of writing a second one. Zero
45
45
  // dependencies on both sides, so nothing is added to what this tool needs to run.
46
- import { parse, backends, resolvePack, rootPlan, packContributions, composeFragments } from "./compile.mjs";
46
+ import {
47
+ parse,
48
+ backends,
49
+ resolvePack,
50
+ rootPlan,
51
+ packContributions,
52
+ composeFragments,
53
+ packDifferences,
54
+ shadowedCopy,
55
+ } from "./compile.mjs";
47
56
  // The containment test the memory-index siting rule turns on, imported for the reason directly
48
57
  // above: the copy that used to live here drifted into the identical fail-open as the original.
49
58
  import { isInside, recordType } from "./index.mjs";
@@ -385,7 +394,11 @@ export function parseProvenance(source) {
385
394
  // the line a demotion pass retires by. Anchored to the bolded field at line start, so prose that
386
395
  // merely discusses retiring never matches — the same false-red caution parseProvenance takes with
387
396
  // backticked tokens, for the same reason: a false red is what gets a whole check switched off.
388
- const RETIRE_WHEN = /^\s*\*\*retire when:\*\*/im;
397
+ // **Exported for `./skill-goldens.mjs`**, which binds `core/skills/consolidate/SKILL.md` step 1 —
398
+ // *"Every record carries a `Retire when:` line"* — to the live store. Exported rather than re-spelled:
399
+ // a second regex there would be a second carrier of this field's spelling, and the anchoring caution
400
+ // above is the part a re-speller gets wrong first.
401
+ export const RETIRE_WHEN = /^\s*\*\*retire when:\*\*/im;
389
402
 
390
403
  // ===========================================================================================
391
404
  // 3. Claims against the tree
@@ -1815,6 +1828,50 @@ export async function inspect(workspaceDir, options = {}) {
1815
1828
  "packs",
1816
1829
  `\`${name}\` resolves${from} to ${path.relative(dir, found.dir)} and validates against Pack Definition ${manifest.portulan.pack} — contributes ${parts.length ? parts.join(", ") : "nothing"}`,
1817
1830
  );
1831
+
1832
+ // **A SHADOWED pack is reported, and the report says WHAT DIFFERS** (#264). Resolution is
1833
+ // first-match-wins and discovered roots come first, so where the host carries a copy of a
1834
+ // pack the tree also carries, the installed one answers and nothing said so. Silence about
1835
+ // the shadowed copy is the part that makes the failure unreproducible: the compiled artifact
1836
+ // moves, the rail reds, and the deciding input is a directory outside the repository.
1837
+ //
1838
+ // A REPORT, never a verdict — an installed pack shadowing a tree copy is a fact about the
1839
+ // machine, and this tool must not turn a fact about the machine into a verdict about the
1840
+ // repository (the boundary this file already states for discovered roots).
1841
+ {
1842
+ const shadowed = shadowedCopy(name, origin, roots, (r) => originOf.get(r)) ?? { dir: null };
1843
+ if (shadowed.dir) {
1844
+ let other = null;
1845
+ try {
1846
+ other = JSON.parse(fs.readFileSync(shadowed.manifest, "utf8"));
1847
+ } catch (cause) {
1848
+ // Could-not-compare, in so many words. A shadow we cannot read is not a shadow
1849
+ // we can call harmless, and saying nothing here would be the silence this whole
1850
+ // report exists to end.
1851
+ report(
1852
+ "packs",
1853
+ `\`${name}\` is SHADOWED — the tree-derived root also carries it at ${path.relative(dir, shadowed.dir)}, and that copy could not be read (${cause.message}), so what differs could not be compared`,
1854
+ );
1855
+ }
1856
+ if (other) {
1857
+ // **The comparison lives in `compile.mjs`, and this calls it.** It grew here,
1858
+ // and its two earlier spellings each claimed more than they compared — a
1859
+ // projection over `[id, tier, action]` that read a copy differing in `reason`
1860
+ // as agreeing, and a *byte-identical* claim `JSON.stringify` of a parsed value
1861
+ // cannot make. `compile` now has to answer the same question in order to refuse
1862
+ // a shadow it cannot honestly pick between (#316), and a second copy of a
1863
+ // comparison already corrected twice is exactly proposal `0020`'s defect. The
1864
+ // rationale for parsed-and-key-sorted moved with the code.
1865
+ const differs = packDifferences(manifest, other);
1866
+ report(
1867
+ "packs",
1868
+ `\`${name}\` is SHADOWED — the installed copy answered and the tree-derived root also carries it at ` +
1869
+ `${path.relative(dir, shadowed.dir)}; ${differs.length ? `they differ by ${differs.join(" and ")}` : "the two agree today"}. ` +
1870
+ "An unpinned `compile` or `recipe-set` REFUSES this rather than picking, and so does `index` for a workspace declaring a scope index — name the root: `--pack-root packs` for the tree, which is what the recipes check",
1871
+ );
1872
+ }
1873
+ }
1874
+ }
1818
1875
  }
1819
1876
  }
1820
1877