clearotron 0.2.0 → 0.2.1

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.
Files changed (43) hide show
  1. package/.env.example +52 -0
  2. package/INSTALL.md +4 -3
  3. package/README.md +2 -1
  4. package/bin/example.mjs +88 -27
  5. package/bin/onboard.mjs +40 -2
  6. package/bin/start.mjs +7 -0
  7. package/build-info.json +2 -2
  8. package/docs/RELEASES.md +6 -4
  9. package/docs/architecture/04-configuration-reference.md +1 -1
  10. package/driver/CHANGELOG.md +30 -0
  11. package/driver/ask-ledger.mjs +69 -1
  12. package/driver/package.json +1 -1
  13. package/driver/pipeline.mjs +41 -2
  14. package/driver/predelivery-lint.mjs +1 -1
  15. package/driver/publish/seed-pool.mjs +24 -9
  16. package/driver/record-carry.mjs +139 -0
  17. package/driver/reference-score.mjs +53 -3
  18. package/driver/reference-strip-signatures.mjs +68 -0
  19. package/driver/register-digest-record.mjs +31 -1
  20. package/driver/repairs.mjs +1 -1
  21. package/driver/suite-census.json +46 -16
  22. package/driver/verify.mjs +2 -2
  23. package/mcp-server/CHANGELOG.md +8 -0
  24. package/mcp-server/lib/whatif.mjs +10 -1
  25. package/mcp-server/package.json +1 -1
  26. package/package.json +1 -1
  27. package/portal-ui/package.json +1 -1
  28. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  29. package/providers/oauth-mcp-bridge/package.json +1 -1
  30. package/scripts/ai-page-render-check.mjs +2 -1
  31. package/scripts/clearances-render-check.mjs +2 -1
  32. package/scripts/env-audit.mjs +20 -0
  33. package/scripts/headless-page.mjs +225 -0
  34. package/scripts/home-render-check.mjs +2 -1
  35. package/scripts/mint-reference-strip-backlog.mjs +41 -0
  36. package/scripts/release-await-cut.mjs +95 -7
  37. package/scripts/release-version-pr-checks.mjs +25 -1
  38. package/scripts/report-frame-check.mjs +12 -0
  39. package/scripts/report-screenshot.mjs +62 -2
  40. package/scripts/revisit-render-check.mjs +3 -2
  41. package/scripts/score.mjs +14 -0
  42. package/shared/access-audience.mjs +215 -0
  43. package/shared/tracked-files.mjs +31 -0
@@ -105,12 +105,20 @@ export function frozenSamples(examplesDir) {
105
105
  */
106
106
  export async function seedPool({ pool, examplesDir, republish }) {
107
107
  const existing = poolRunIds(pool);
108
- if (existing.length) {
109
- // Named with the count, not just refused. "It did not seed" and "it did not seed BECAUSE there are
110
- // already 14 runs here" are the same non-event to the code and different facts to a reader.
111
- return { seeded: [], skipped: `the pool already holds ${existing.length} run(s)`, problems: [] };
112
- }
113
-
108
+ // ── A POOL THAT ALREADY HOLDS SOMETHING IS TOPPED UP, NOT SKIPPED (tracker issue 277) ──────────────
109
+ //
110
+ // This used to return early on any non-empty pool. That was invisible while `demo/` shipped one child:
111
+ // seeding one and seeding all were the same act. When the other three landed, every box seeded before
112
+ // that day kept its single demo through every upgrade, because the pool was no longer empty and this
113
+ // returned without looking at what the package now carried.
114
+ //
115
+ // DEMOS ARE PACKAGE CONTENT, NOT USER DATA, so the package's set is the one that should be there. What
116
+ // is already published is left exactly as it is — this adds what is missing and removes nothing, so a
117
+ // pool holding a run this package does not ship keeps it.
118
+ //
119
+ // AND IT IS ONLY EVER A DEMO POOL. `bin/start.mjs` calls this inside its `--demo` branch alone; a real
120
+ // install is told its archive is empty and pointed at the demo command. Nothing here can put an example
121
+ // into a pool holding a customer's work, and that gate is the reason this can top up safely at all.
114
122
  const { samples, problems } = frozenSamples(examplesDir);
115
123
  if (!samples.length) {
116
124
  // The empty archive is now the SYMPTOM of something, and this is where the something is named.
@@ -118,11 +126,13 @@ export async function seedPool({ pool, examplesDir, republish }) {
118
126
  }
119
127
 
120
128
  const seeded = [];
129
+ const already = [];
121
130
  const failures = [];
122
131
  for (const s of samples) {
123
- // Belt and braces against a partial pool: the emptiness check above already means nothing is here,
124
- // but a sample list carrying the same runId twice would otherwise overwrite silently.
132
+ // A sample list carrying the same runId twice would otherwise overwrite silently.
125
133
  if (seeded.includes(s.meta.runId)) { failures.push(`${s.name}: runId ${s.meta.runId} appears twice under ${examplesDir}`); continue; }
134
+ // Already published: left alone, and NAMED. Silence here is what the old early return produced.
135
+ if (existing.includes(s.meta.runId)) { already.push(s.meta.runId); continue; }
126
136
  try {
127
137
  // poolUrl "" for the same reason the demo passes it: the report's link block addresses a
128
138
  // deployment that serves the pool at a public URL, and this one is served from this machine.
@@ -133,5 +143,10 @@ export async function seedPool({ pool, examplesDir, republish }) {
133
143
  failures.push(`${s.name}: ${String(e?.message ?? e)}`);
134
144
  }
135
145
  }
136
- return { seeded, skipped: null, problems: [...problems, ...failures] };
146
+ // `skipped` still carries a sentence when there was nothing to add, because "seeded 0" and "seeded 0
147
+ // BECAUSE all four were already here" are the same number and different facts.
148
+ const skipped = !seeded.length && already.length
149
+ ? `the pool already holds all ${already.length} example(s) this package ships`
150
+ : null;
151
+ return { seeded, already, skipped, problems: [...problems, ...failures] };
137
152
  }
@@ -825,3 +825,142 @@ export function silentlyLostFindings({ reconciliation = null, carryRows = null,
825
825
  population_empty: false, cross_checked: crossChecked,
826
826
  checked: ended.length, matched: seen.length, lost };
827
827
  }
828
+
829
+ /**
830
+ * Positions the DIGEST ended as findings that did not reach the findings, dropped WITH a stated reason.
831
+ *
832
+ * ── THE SIBLING'S BLIND SPOT, AND IT IS THE ONE THAT REACHED A CLIENT (tracker issue 248) ──────────
833
+ *
834
+ * `silentlyLostFindings` above is correct and must not be widened to cover this. Its population is
835
+ * `step-silent` — a finding-ending followed by silence — and its own header records why that boundary
836
+ * exists: silent drops are the norm (690 of 741 records on the evidence run), so a rule flagging them
837
+ * broadly would flag almost everything, and the defect it targets is the CONJUNCTION of silence after a
838
+ * finding-ending.
839
+ *
840
+ * It also anticipated this gap in writing: "nine divergences from a digest finding-ending, every one of
841
+ * them `step-stated`". Nine of the shape nothing checked.
842
+ *
843
+ * MEASURED ON R2 `russet-kestrel`, delivered 2026-09-06. The sibling ran and reported
844
+ * `{checked:5, matched:5, lost:0}` — correctly. On that same delivery two marks from the lawyer's final
845
+ * list, `OSLER DELPHI` and `DELFITY`, one rated HIGH, are absent from `findings.json`. They were dropped
846
+ * WITH a reason, so they sat outside the sibling's population by design:
847
+ *
848
+ * IMMATERIAL ask:recall:recall-osler-delphi: … — OSLER DELPHI / Osler Diagnostics Limited is
849
+ * already reasoned on the incumbent sheet in register-findings.md.
850
+ *
851
+ * WHY THE STATED CASE IS THE MORE DANGEROUS ONE. A silent drop leaves a hole. A stated drop leaves a
852
+ * SENTENCE, and the sentence reads as diligence. On that one delivery `doubt-closure.md` carries 92
853
+ * recall asks and 66 rulings of IMMATERIAL. A drop with a reason nobody verifies is not accounted for;
854
+ * it is unexamined with a paper trail.
855
+ *
856
+ * `step-structural` is deliberately NOT in this population: it is a mechanical screen verdict
857
+ * (`the in-line record screen returned "…"`), not a judgment sentence a reader would take on trust.
858
+ * `absent` belongs to the sibling's family, not this one.
859
+ *
860
+ * WHAT THIS DOES NOT DECIDE. Whether any given stated reason is RIGHT. That is a change to what the
861
+ * client receives and is the owner's call; this makes the class visible, which is worth having whichever
862
+ * way that lands, because today nobody would know the closures happened.
863
+ *
864
+ * Same contract as the sibling, deliberately: `computable:false` with a named reason rather than a clean
865
+ * `[]`; `population_empty` as its own state; `cross_checked` so a caller can tell "could not look" from
866
+ * "looked and found nothing"; and `matched` returned so a caller can insist the join actually joined —
867
+ * that field exists because a case-sensitive URI join once matched zero rows on every run and read as
868
+ * zero divergences. PURE.
869
+ */
870
+ export function statedDivergenceFindings({ reconciliation = null, carryRows = null, digestFindingUris = null } = {}) {
871
+ const no = (reason, crossChecked = false) => ({ computable: false, reason, population_empty: false,
872
+ cross_checked: crossChecked, checked: 0, matched: 0, diverged: [] });
873
+ if (!reconciliation || reconciliation.computable !== true) {
874
+ return no("no computable recall-reconciliation — the digest's own endings are the population and there is none");
875
+ }
876
+ if (!Array.isArray(carryRows)) {
877
+ return no("no record-carry rows — the knockout lane writes none, so this join cannot look at that product");
878
+ }
879
+ // ── THE POPULATION IS THE CARRY ROWS, NOT THE RECONCILIATION (corrected 2026-09-07) ─────────────
880
+ //
881
+ // The first cut of this function gated on the reconciliation's finding-ended positions, mirroring the
882
+ // sibling. That inherited the sibling's BLIND SPOT along with its shape, and the check was inert on
883
+ // the very delivery it was written for. Replayed against R2 `russet-kestrel`:
884
+ //
885
+ // silentlyLostFindings checked=5 matched=5 lost=0
886
+ // statedDivergenceFindings checked=5 matched=5 diverged=0 ← should have named two marks
887
+ //
888
+ // The reconciliation names five finding-ended positions and they are five OTHER marks — DELPHIS
889
+ // bioenergetische Kosmetik, DELPHIC HSE, DELPHI, DELPHIN & EMERENCE, DELPHI DIAGNOSTICS. The two that
890
+ // were lost sit in the CARRY rows and the reconciliation never mentions them:
891
+ //
892
+ // OSLER DELPHI reach=placed stopped_at=digest reason_source=step-stated reason=digest:reasoned-negative
893
+ // DELFITY reach=placed stopped_at=digest reason_source=step-stated reason=digest:reasoned-negative
894
+ //
895
+ // The unit arms all passed because their fixtures put the mark in BOTH populations, which the real run
896
+ // does not. That is the lesson worth keeping: a fixture that satisfies two joins at once cannot tell
897
+ // you the joins disagree.
898
+ //
899
+ // So the carry rows are the population — they are where a stated drop is RECORDED — and the
900
+ // reconciliation is demoted to optional corroboration. Measured on that delivery, the correct
901
+ // population is 70 rows over 34 distinct marks, all `digest:reasoned-negative`, and it contains both.
902
+ const arrived = new Set(["finding", "findings-surface"]);
903
+ const population = carryRows.filter((r) => r?.uri
904
+ && r.reason_source === "step-stated" // the sibling owns step-silent; step-structural is mechanical
905
+ && !arrived.has(r.reach)); // arrived, or arrived somewhere visible, is not a divergence
906
+
907
+ // Kept for corroboration only. `ended` no longer gates anything; where the reconciliation DOES name a
908
+ // position it agrees with, that is recorded on the row so a reader can weigh it.
909
+ const ended = [];
910
+ for (const bucket of ["top_slice", "residual"]) {
911
+ for (const row of reconciliation[bucket] ?? []) {
912
+ if (row?.ending !== "finding") continue;
913
+ for (const uri of row.position_records ?? []) ended.push({ uri: lc(uri), mark: row.mark_text ?? null });
914
+ }
915
+ }
916
+ const endedUris = new Set(ended.map((e) => e.uri));
917
+ const byUri = new Map();
918
+ for (const r of carryRows) if (r?.uri) byUri.set(lc(r.uri), r);
919
+
920
+ // The disjoint-population guard, for the sibling's reason: overlap is the signal, a shortfall is not.
921
+ if (Array.isArray(digestFindingUris) && digestFindingUris.length && ended.length) {
922
+ const digest = new Set(digestFindingUris.map(lc));
923
+ if (!ended.some((e) => digest.has(e.uri))) {
924
+ return no(`the reconciliation's ${ended.length} finding-ended position(s) share NOTHING with the `
925
+ + `${digest.size} finding row(s) the digest's own typed calls recorded — the two populations are `
926
+ + "disjoint, so this join is examining a different set and its answer cannot be trusted", true);
927
+ }
928
+ }
929
+ // A ZERO POPULATION IS ITS OWN STATE. No stated drop recorded is a real answer on a healthy run, and
930
+ // it must not be reported in the same shape as "there were some and none diverged".
931
+ if (!population.length) {
932
+ return { computable: true, reason: "no record-carry row records a stated drop on this run — there is "
933
+ + "no population here, which is the healthy answer and not a comparison that found nothing",
934
+ population_empty: true, cross_checked: false, checked: 0, matched: 0, diverged: [] };
935
+ }
936
+
937
+ const diverged = [];
938
+ for (const row of population) {
939
+ const e = { uri: lc(row.uri), mark: row.mark ?? null };
940
+ // NAME THE ARTIFACT THE REASON POINTS AT. The defect this check exists for is an absence discharged
941
+ // by the WRONG artifact — "already reasoned in register-findings.md" answers a question nobody asked,
942
+ // because the ask was about the findings. Surfacing the cited artifact is what lets a reader see the
943
+ // substitution rather than read the sentence as diligence.
944
+ const reason = row.reason ?? null;
945
+ const cites = typeof reason === "string" ? (reason.match(/[a-z0-9._-]+\.(?:md|json)\b/gi) ?? []) : [];
946
+ diverged.push({ uri: e.uri, mark: e.mark ?? row.mark ?? null, reach: row.reach ?? null,
947
+ stopped_at: row.stopped_at ?? null, reason,
948
+ cites_artifact: cites.length ? [...new Set(cites.map(String))] : null,
949
+ // CORROBORATION, NOT A GATE. The reconciliation naming this position is worth a reader knowing;
950
+ // its SILENCE is not evidence of anything, which is exactly what the first cut got wrong.
951
+ reconciliation_agrees: endedUris.has(e.uri),
952
+ why: "this position was dropped with a stated reason and never reached the findings, and the reason "
953
+ + "given points at a different artifact than the one the absence is about" });
954
+ }
955
+ const crossChecked = Array.isArray(digestFindingUris) && digestFindingUris.length > 0;
956
+ return { computable: true,
957
+ reason: crossChecked ? null
958
+ : "no cross-check was possible — this run recorded no typed digest finding rows, so the "
959
+ + "population was not verified against an independent one",
960
+ population_empty: false, cross_checked: crossChecked,
961
+ // `checked` is the population this check actually walked, and `matched` how many of them the
962
+ // reconciliation ALSO named. On the delivery this was written for those are 70 and 0 — which is the
963
+ // whole point: a `matched` of zero used to mean "report nothing" and now means "the reconciliation
964
+ // saw none of them", a fact about the reconciliation rather than about the run.
965
+ checked: population.length, matched: diverged.filter((d) => d.reconciliation_agrees).length, diverged };
966
+ }
@@ -667,14 +667,64 @@ export function scoreRecall({ reference, findings = [], retrieved = [], scopeCla
667
667
  // what it saw. Auto-promoting a collision to `found` would be the scorer manufacturing recall from
668
668
  // its own confusion, which is the defect one layer up from the one being fixed. score.mjs prints
669
669
  // these; a reader adjudicates.
670
+ // ── THE PREDICATE WAS THE OWNER, AND THE HEADING ABOVE SAYS RECORD (tracker issue 249) ──────────
671
+ //
672
+ // The only condition used to be `ownersMatch`. That implements a different class from the one the
673
+ // paragraph above states, and the justification — "they cannot both be true" — does not hold for any
674
+ // proprietor with more than one mark. A large filer can perfectly well have one mark the run withheld
675
+ // and a DIFFERENT mark, not in the reference, that it surfaced. Both rows are true.
676
+ //
677
+ // It fired on R2 russet-kestrel: `Novartis AG: reference "DELFITY" is withheld, surfaced "DELPHINA"
678
+ // is noise`. Different marks, different records, one proprietor that files a great many. And because
679
+ // score.mjs prints a collision as "do not read the recall numbers above", ONE such proprietor
680
+ // suppressed the whole run's recall measurement — a real 88% → 63% movement went unquoted on the
681
+ // regression issue because of a warning that was spurious.
682
+ //
683
+ // WHY NOT A RECORD JOIN, WHICH IS WHAT THE ISSUE ASKED FOR. Measured on the delivered artifact: a
684
+ // finding carries `{band, disposition, mark, meters, net, ordinal, owner, quadrant, source, …}` and
685
+ // NO record identity — `source.resolved_link` is empty on every row. Noise rows are built from
686
+ // findings, so there is nothing on that side to join a record URI to. Stating it here so the next
687
+ // reader does not re-derive it: the acceptance criterion is unmeetable on this side until a finding
688
+ // carries its record, and that is a change to the findings contract, not to the scorer.
689
+ //
690
+ // WHY NOT `matchesReference`, WHICH IS THE FILE'S OWN MARK MATCHER. It would make this check VACUOUS.
691
+ // The noise loop above already skips any finding that matches a reference entry, so by construction
692
+ // no noise row matches one — the collision would always be empty. This check exists precisely to
693
+ // catch the case where THE MATCHER DISAGREED WITH ITSELF, so it cannot be built out of the matcher.
694
+ //
695
+ // So the predicate is deliberately weaker than the matcher and stronger than the owner: same owner
696
+ // AND one mark contained in the other once normalised. That is the shape of the case this check was
697
+ // built for — `DELPHI GENETICS` in LOST beside `DG DELPHI GENETICS` in NOISE — and it is not the
698
+ // shape of `DELFITY` beside `DELPHINA`.
699
+ const collisionKey = (s) => String(s ?? "").normalize("NFKC").toUpperCase().replace(/[^A-Z0-9]/g, "");
700
+ // A FLOOR, because containment on a short string matches everything. `DEL` inside `DELPHINA` is not
701
+ // evidence of a shared record; four characters is the shortest reference mark shape worth trusting
702
+ // here, and a pair below it drops to the advisory rather than being dropped entirely.
703
+ const CONTAIN_FLOOR = 4;
670
704
  buckets.collisions = [];
705
+ buckets.ownerEchoes = [];
671
706
  for (const bucket of ["lost", "withheld"]) {
672
707
  for (const e of buckets[bucket]) {
673
708
  for (const n of buckets.noise) {
674
709
  if (!ownersMatch(e.owner, n.owner)) continue;
675
- buckets.collisions.push({ bucket, entry: e.mark ?? e.name ?? null, noise: n.mark,
676
- owner: ownerName(n.owner) ?? ownerName(e.owner),
677
- why: `the reference entry is reported ${bucket} while a surfaced record of the SAME owner is reported noise — one record cannot be both` });
710
+ const a = collisionKey(e.mark ?? e.name);
711
+ const b = collisionKey(n.mark);
712
+ const row = { bucket, entry: e.mark ?? e.name ?? null, noise: n.mark,
713
+ owner: ownerName(n.owner) ?? ownerName(e.owner) };
714
+ const sameRecord = a && b
715
+ && Math.min(a.length, b.length) >= CONTAIN_FLOOR
716
+ && (a === b || a.includes(b) || b.includes(a));
717
+ if (sameRecord) {
718
+ buckets.collisions.push({ ...row,
719
+ why: `the reference entry is reported ${bucket} while a surfaced record of the SAME owner whose `
720
+ + "mark contains or is contained by it is reported noise — these are one record in two buckets" });
721
+ } else {
722
+ // REPORTABLE, NEVER SUPPRESSING. A reader may still want to see that a proprietor appears on
723
+ // both sides; what they must not be told is that the recall numbers are unreadable.
724
+ buckets.ownerEchoes.push({ ...row,
725
+ why: `same proprietor on both sides with different marks — not a contradiction: a filer may hold `
726
+ + `a reference mark this run ${bucket} and another, outside the reference, that it surfaced` });
727
+ }
678
728
  }
679
729
  }
680
730
  }
@@ -0,0 +1,68 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ //
4
+ // THE TWO SIGNATURES THE REFERENCE STRIP LEFT BEHIND (tracker issue 185).
5
+ //
6
+ // The strip's job was to remove internal references from this repository before it went public, and it
7
+ // did that. Where the reference was the SUBJECT of the sentence, it took the subject with it:
8
+ //
9
+ // "[ref]'s design ruling — above any fold, only a statement…" became "'s design ruling — …"
10
+ // "renders the pre-[ref] section" became "renders the pre- section"
11
+ //
12
+ // Neither construction occurs in written English, which is the whole reason they can be counted rather
13
+ // than judged. A reader of a public repository meets them as sentences that do not finish, and one of
14
+ // them is in user-facing configuration documentation rather than in a comment.
15
+ //
16
+ // WHAT THIS MODULE IS AND IS NOT. It finds them. It does not repair them: the repair is per-sentence and
17
+ // needs somebody reading the surrounding code, because "the pre- section" means "the section as it was
18
+ // before the findings contract changed" and only that reader can say so. The finding is mechanical and
19
+ // costs nothing, so it is the part that ships as a check.
20
+ export const SIGNATURES = [
21
+ {
22
+ name: "a comment beginning with a bare possessive",
23
+ // Anchored at the comment leader, so `it's` and `the run's` mid-sentence are untouched — only the
24
+ // case where the possessive has nothing in front of it to possess.
25
+ re: /^\s*(?:\/\/|#|\*)\s*'s\b/,
26
+ },
27
+ {
28
+ name: "`pre- ` followed by a lowercase word",
29
+ // The hyphen is left dangling by a stripped number. `pre-flight` and `pre-delivery` do not match:
30
+ // the strip's residue always leaves whitespace after the hyphen.
31
+ re: /\bpre-\s+[a-z]/,
32
+ },
33
+ ];
34
+
35
+ // THE RULE'S OWN DEFINITION IS THE ONLY EXEMPTION, and it is named rather than pattern-matched.
36
+ //
37
+ // These three files QUOTE the residue in order to define it: the specimens in the arm, the examples in
38
+ // the header above. Scanning them counts the definition as an instance, which puts the guard's own text
39
+ // into the backlog it polices and makes every re-mint grow the number it exists to shrink. Caught by a
40
+ // plant, not by review — they were untracked when the table was first minted, so `git ls-files` did not
41
+ // list them and the census read a tree that did not include them yet.
42
+ export const RULE_DEFINITIONS = [
43
+ "driver/reference-strip-signatures.mjs",
44
+ "driver/test/the-reference-strip-left-sentences-unfinished.test.mjs",
45
+ "scripts/mint-reference-strip-backlog.mjs",
46
+ ];
47
+
48
+ /** Files worth scanning: prose-bearing, tracked, not generated, and not this rule's own definition. */
49
+ export const isScannable = (f) =>
50
+ /\.(mjs|md|yml|ts)$/.test(f) && !f.startsWith("portal-ui/dist/") && !RULE_DEFINITIONS.includes(f);
51
+
52
+ /**
53
+ * Count both signatures per file across `files`.
54
+ * `read` is injected so an arm can drive this over a synthetic tree — a census helper that can only
55
+ * read the real repository cannot be planted, and a plant is the only thing that proves it still counts.
56
+ */
57
+ export function censusOf(root, files, read) {
58
+ const out = {};
59
+ let total = 0;
60
+ for (const f of files.filter(isScannable)) {
61
+ let text;
62
+ try { text = read(f); } catch { continue; } // binary or unreadable — nothing to count
63
+ const counts = SIGNATURES.map((s) => text.split("\n").filter((l) => s.re.test(l)).length);
64
+ if (counts.some((n) => n > 0)) { out[f] = counts; total += counts.reduce((a, b) => a + b, 0); }
65
+ }
66
+ // Sorted, so a re-mint produces a reviewable diff instead of a reordered file.
67
+ return { total, files: Object.fromEntries(Object.entries(out).sort(([a], [b]) => a < b ? -1 : 1)) };
68
+ }
@@ -168,6 +168,36 @@ const FINDING_COLUMNS = Object.freeze(
168
168
  // finding it there together with screen_verdict / class / status.
169
169
  const NEGATIVE_COLUMNS = Object.freeze(["Mark", "Search Term / Variant", "Result", "Notes"]);
170
170
 
171
+ /**
172
+ * The "Mark" cell for one Negative-results row.
173
+ *
174
+ * A `duplicate-of-surfaced` row is not a statement about the MARK. It is a statement about one
175
+ * registration of a mark whose position is already reported above it — the digest's own rule is "one
176
+ * row per POSITION, never one per registration of the same right". Printing the bare mark under a
177
+ * column headed "Mark" says the opposite of what the row means, and the document then reads as both
178
+ * "keep this mark, here is the reasoning" in the incumbent table and "no separate row for this mark"
179
+ * here. Nine readers scan this file, and one of them is the drafting seat that decides what the client
180
+ * is shown.
181
+ *
182
+ * Naming the record removes the contradiction and changes no judgment the digest made: the row is
183
+ * still a drop, still on the same ground, still carrying the seat's own reason.
184
+ *
185
+ * ✕ EVERY OTHER GROUND IS LEFT ALONE, deliberately. `off-field`, `dead-status` and `out-of-class` are
186
+ * about the record on its own terms, where the bare mark is what a reader wants and is not ambiguous.
187
+ * A blanket change to this column would touch every negative row in every report for a defect that
188
+ * only exists on the duplicate class.
189
+ *
190
+ * The office comes from the record uri (`/mark/wo/…` -> `WO`); every uri in the archived corpus carries
191
+ * a two-letter office in that position. When it cannot be read the BARE MARK is printed rather than a
192
+ * broken qualifier — a missing qualifier is a smaller defect than "OSLER DELPHI — UNDEFINED record".
193
+ */
194
+ export function negativeMarkCell(row) {
195
+ const mark = String(row?.cells?.mark ?? "").trim();
196
+ if (row?.ground !== "duplicate-of-surfaced" || !mark) return mark;
197
+ const office = String(row?.cells?.uri ?? "").split("/")[2] ?? "";
198
+ return /^[a-z]{2}$/i.test(office) ? `${mark} \u2014 ${office.toUpperCase()} record` : mark;
199
+ }
200
+
171
201
  // ── THE FLOOR, AND WHAT ITS ZERO MEANS ────────────────────────────────────────────────────────────
172
202
  //
173
203
  // `validators.registerFindings` carried `nonEmpty(c)` with no character number, so there is no
@@ -353,7 +383,7 @@ export function renderRegisterFindings(model, facts = emptyFacts()) {
353
383
  out.push(DIGEST_SECTIONS.negative, "");
354
384
  out.push(model.negative_rows.length
355
385
  ? table(NEGATIVE_COLUMNS, model.negative_rows.map((r) => [
356
- r.cells.mark, r.variant || r.cells.mark, r.drop_reason,
386
+ negativeMarkCell(r), r.variant || r.cells.mark, r.drop_reason,
357
387
  [`URI ${r.cells.uri}`, r.screen_verdict ? `screen_verdict=${r.screen_verdict}` : "",
358
388
  r.cells.classes ? `class=${r.cells.classes}` : "", r.cells.status ? `status=${r.cells.status}` : ""]
359
389
  .filter(Boolean).join("; "),
@@ -555,7 +555,7 @@ export function countRecoveryLanes(history, { total = 0 } = {}) {
555
555
 
556
556
  // ── CAP PARKS: A PROVIDER SAYING "NOT YET" IS NOT A STAGE FAILING (tracker issue 103) ──────────────
557
557
  //
558
- // Owner, watching indigo-falcon spend 4 of its 6 recovery parks against one subscription cap:
558
+ // Owner, watching a run spend 4 of its 6 recovery parks against one subscription cap:
559
559
  //
560
560
  // "surely it can work out when the cap expires and try after that time and not just keep trying
561
561
  // and then die."
@@ -189,6 +189,12 @@
189
189
  "skips": 1,
190
190
  "todos": 0
191
191
  },
192
+ "a-door-that-is-not-there-is-not-a-door-that-agrees.test.mjs": {
193
+ "tests": 19,
194
+ "asserts": 69,
195
+ "skips": 0,
196
+ "todos": 0
197
+ },
192
198
  "a-doubt-cannot-answer-itself.test.mjs": {
193
199
  "tests": 17,
194
200
  "asserts": 40,
@@ -201,6 +207,12 @@
201
207
  "skips": 0,
202
208
  "todos": 0
203
209
  },
210
+ "a-drop-with-a-stated-reason-is-checked.test.mjs": {
211
+ "tests": 13,
212
+ "asserts": 32,
213
+ "skips": 0,
214
+ "todos": 0
215
+ },
204
216
  "a-fact-about-a-named-party.test.mjs": {
205
217
  "tests": 30,
206
218
  "asserts": 80,
@@ -376,8 +388,8 @@
376
388
  "todos": 0
377
389
  },
378
390
  "a-real-install-starts-with-an-empty-archive.test.mjs": {
379
- "tests": 3,
380
- "asserts": 8,
391
+ "tests": 6,
392
+ "asserts": 16,
381
393
  "skips": 0,
382
394
  "todos": 0
383
395
  },
@@ -483,6 +495,12 @@
483
495
  "skips": 0,
484
496
  "todos": 0
485
497
  },
498
+ "a-screenshot-of-an-error-page-is-not-a-report.test.mjs": {
499
+ "tests": 17,
500
+ "asserts": 44,
501
+ "skips": 4,
502
+ "todos": 0
503
+ },
486
504
  "a-secret-file-is-compared-not-printed.test.mjs": {
487
505
  "tests": 5,
488
506
  "asserts": 16,
@@ -501,6 +519,12 @@
501
519
  "skips": 0,
502
520
  "todos": 0
503
521
  },
522
+ "a-shared-proprietor-is-not-a-bucket-collision.test.mjs": {
523
+ "tests": 6,
524
+ "asserts": 14,
525
+ "skips": 0,
526
+ "todos": 0
527
+ },
504
528
  "a-signal-immune-fixture-is-reaped-by-its-owner.test.mjs": {
505
529
  "tests": 10,
506
530
  "asserts": 20,
@@ -724,8 +748,8 @@
724
748
  "todos": 0
725
749
  },
726
750
  "ask-ledger.test.mjs": {
727
- "tests": 21,
728
- "asserts": 100,
751
+ "tests": 26,
752
+ "asserts": 119,
729
753
  "skips": 0,
730
754
  "todos": 0
731
755
  },
@@ -1882,8 +1906,8 @@
1882
1906
  "todos": 0
1883
1907
  },
1884
1908
  "example-replay.test.mjs": {
1885
- "tests": 6,
1886
- "asserts": 24,
1909
+ "tests": 8,
1910
+ "asserts": 31,
1887
1911
  "skips": 0,
1888
1912
  "todos": 0
1889
1913
  },
@@ -2826,7 +2850,7 @@
2826
2850
  "production-dependencies.test.mjs": {
2827
2851
  "tests": 4,
2828
2852
  "asserts": 7,
2829
- "skips": 0,
2853
+ "skips": 3,
2830
2854
  "todos": 0
2831
2855
  },
2832
2856
  "products.test.mjs": {
@@ -2964,7 +2988,7 @@
2964
2988
  "reading-the-demo-does-not-edit-the-repository.test.mjs": {
2965
2989
  "tests": 2,
2966
2990
  "asserts": 8,
2967
- "skips": 0,
2991
+ "skips": 1,
2968
2992
  "todos": 0
2969
2993
  },
2970
2994
  "reason-cut-is-visible.test.mjs": {
@@ -3076,8 +3100,8 @@
3076
3100
  "todos": 0
3077
3101
  },
3078
3102
  "reference-score-owner-identity.test.mjs": {
3079
- "tests": 9,
3080
- "asserts": 30,
3103
+ "tests": 10,
3104
+ "asserts": 34,
3081
3105
  "skips": 0,
3082
3106
  "todos": 0
3083
3107
  },
@@ -3160,8 +3184,8 @@
3160
3184
  "todos": 0
3161
3185
  },
3162
3186
  "register-digest-record.test.mjs": {
3163
- "tests": 30,
3164
- "asserts": 102,
3187
+ "tests": 32,
3188
+ "asserts": 114,
3165
3189
  "skips": 0,
3166
3190
  "todos": 0
3167
3191
  },
@@ -3238,8 +3262,8 @@
3238
3262
  "todos": 0
3239
3263
  },
3240
3264
  "release-pipeline.test.mjs": {
3241
- "tests": 52,
3242
- "asserts": 243,
3265
+ "tests": 83,
3266
+ "asserts": 320,
3243
3267
  "skips": 0,
3244
3268
  "todos": 0
3245
3269
  },
@@ -4335,6 +4359,12 @@
4335
4359
  "skips": 0,
4336
4360
  "todos": 0
4337
4361
  },
4362
+ "the-reference-strip-left-sentences-unfinished.test.mjs": {
4363
+ "tests": 6,
4364
+ "asserts": 13,
4365
+ "skips": 3,
4366
+ "todos": 0
4367
+ },
4338
4368
  "the-repair-order-and-the-tool-schema-agree.test.mjs": {
4339
4369
  "tests": 6,
4340
4370
  "asserts": 27,
@@ -5043,8 +5073,8 @@
5043
5073
  "todos": 0
5044
5074
  },
5045
5075
  "whatif.test.mjs": {
5046
- "tests": 9,
5047
- "asserts": 29,
5076
+ "tests": 10,
5077
+ "asserts": 32,
5048
5078
  "skips": 0,
5049
5079
  "todos": 0
5050
5080
  }
package/driver/verify.mjs CHANGED
@@ -170,8 +170,8 @@ function needs(content, markers, label, names = []) {
170
170
  //
171
171
  // THE CLASS. Every gate below keys on prose the MODEL composes, so each is one phrasing drift away
172
172
  // from killing a run that produced the section perfectly. Measured across three runs and two engines:
173
- // pewter-lantern (codex) wrote "## Negative-results matrix" and was rejected twice, killing a client
174
- // run; umber-beacon (anthropic) wrote "## Negative results (per-cell detail)" — different spelling AND
173
+ // one run on codex wrote "## Negative-results matrix" and was rejected twice, killing the run; another
174
+ // on anthropic wrote "## Negative results (per-cell detail)" — different spelling AND
175
175
  // different trailing words, same skill. PR 336 widened the regex to `[\s-]`, which fixed those two
176
176
  // instances and left the class exactly where it was: the gate still asks the model to guess a spelling.
177
177
  //
@@ -1,5 +1,13 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.2.1
4
+
5
+ ## 0.2.1-beta.2
6
+
7
+ ## 0.2.1-beta.1
8
+
9
+ ## 0.2.1-beta.0
10
+
3
11
  ## 0.2.0
4
12
 
5
13
  ## 0.1.12
@@ -252,7 +252,16 @@ export async function whatIfRun({ confirmationToken } = {}, deps = {}) {
252
252
  // askArchivedRun answers {ok:true, memoPath, memoId, parentRunId, assumption, ratedUnder,
253
253
  // statedLimits} or {ok:false, fail, detail} — a stable MEMO_FAILS code, never a throw, because the
254
254
  // worker records what it is handed and a throw there becomes a string nobody can branch on.
255
- return await askArchivedRun({ runId, question: instructions, requestedBy: null });
255
+ // THE RESOLVER IS HANDED OVER, and this line is the whole of tracker issue 132's first defect.
256
+ // `askArchivedRun` takes its resolver from `deps` and has no default for it — `reason` was given one
257
+ // and `resolveRun` was not — so calling it bare returned `memo_run_unresolved` for every memo on
258
+ // every run, while `resolveRun(runId)` eight lines above had already resolved that same run fine.
259
+ // The capability was composed, the door opened, and no production caller could execute it.
260
+ // ✕ NOT fixed by defaulting inside driver/whatif-memo-run.mjs: `resolveRun` lives in this layer
261
+ // (./runs.mjs), and a default there would make the driver import the mcp-server, which is the
262
+ // dependency this module's own comment above keeps out of module scope. This is the only production
263
+ // call site — `driver/whatif-worker.mjs` does not call `askArchivedRun` at all.
264
+ return await askArchivedRun({ runId, question: instructions, requestedBy: null }, { resolveRun });
256
265
  }
257
266
 
258
267
  const refusal = whatIfRefusal({ location: run.location, state: run.state });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "MCP server to interrogate prelim trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the prelim-driver read-only; touches no driver/template/deploy files.",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clearotron",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
7
7
  "type": "git",
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.2.0",
5
+ "version": "0.2.1",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
8
8
  "engines": {
@@ -1,5 +1,13 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.2.1
4
+
5
+ ## 0.2.1-beta.2
6
+
7
+ ## 0.2.1-beta.1
8
+
9
+ ## 0.2.1-beta.0
10
+
3
11
  ## 0.2.0
4
12
 
5
13
  ## 0.1.12