clearotron 0.3.2 → 0.3.3-beta.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 (123) hide show
  1. package/CONTRIBUTING.md +12 -0
  2. package/INSTALL.md +8 -0
  3. package/bin/onboard.mjs +109 -13
  4. package/bin/start.mjs +1 -1
  5. package/build-info.json +2 -2
  6. package/demo/MANIFEST.json +27 -0
  7. package/docs/INTAKE.md +8 -0
  8. package/docs/architecture/04-configuration-reference.md +29 -11
  9. package/driver/CHANGELOG.md +49 -0
  10. package/driver/citation-census.json +3 -3
  11. package/driver/clearance-variants-record.mjs +12 -1
  12. package/driver/common-law-coverage-status.mjs +113 -0
  13. package/driver/config-inventory.mjs +1 -1
  14. package/driver/contract-audit.mjs +1 -1
  15. package/driver/contract-e3-backlog.mjs +37 -37
  16. package/driver/contract-vocabulary.mjs +8 -8
  17. package/driver/coverage-form-io.mjs +3 -1
  18. package/driver/coverage-form.mjs +38 -11
  19. package/driver/coverage-ledger.mjs +37 -7
  20. package/driver/coverage-union.mjs +2 -2
  21. package/driver/crowd-context.mjs +19 -6
  22. package/driver/dev-portal.mjs +3 -3
  23. package/driver/drainer-identity.mjs +1 -1
  24. package/driver/driver.config.mjs +80 -9
  25. package/driver/engine/CONTRACT.md +3 -2
  26. package/driver/engine/anthropic-agent.mjs +34 -7
  27. package/driver/engine/mcp/clarivate-server.mjs +4 -2
  28. package/driver/engine/mcp/corsearch-server.mjs +3 -1
  29. package/driver/engine/mcp/coverage-server.mjs +1 -1
  30. package/driver/engine/mcp/dispositions-server.mjs +47 -5
  31. package/driver/engine/mcp/euipo-server.mjs +2 -0
  32. package/driver/engine/mcp/free-tier-server.mjs +2 -0
  33. package/driver/engine/mcp/gather-config.mjs +8 -2
  34. package/driver/engine/mcp/probe-server.mjs +37 -0
  35. package/driver/engine/mcp/proposal-fields.mjs +45 -0
  36. package/driver/engine/mcp/recording-server.mjs +30 -0
  37. package/driver/engine/mcp/signa-server.mjs +2 -0
  38. package/driver/engine/mcp/supplemental.mjs +89 -12
  39. package/driver/engine/mcp/unit-note-server.mjs +50 -0
  40. package/driver/engine/mcp/uspto-local-server.mjs +2 -0
  41. package/driver/engine/openai-agent.mjs +7 -0
  42. package/driver/engine/probe.mjs +67 -14
  43. package/driver/engine/tool-refusal.mjs +16 -0
  44. package/driver/enqueue-schema.mjs +2 -2
  45. package/driver/envelope-settle.mjs +82 -13
  46. package/driver/findings-model.mjs +4 -4
  47. package/driver/gateway.mjs +18 -2
  48. package/driver/manager-groups-verdict.mjs +1 -1
  49. package/driver/matter-frame-record.mjs +24 -7
  50. package/driver/named-band.mjs +1 -1
  51. package/driver/package.json +1 -1
  52. package/driver/partial-payload-baseline.json +12 -3
  53. package/driver/pipeline-knockout.mjs +3 -3
  54. package/driver/pipeline.mjs +154 -50
  55. package/driver/plan-run-agreement-verdict.mjs +49 -0
  56. package/driver/portal-service.mjs +8 -4
  57. package/driver/progress.mjs +14 -3
  58. package/driver/publish/index.mjs +41 -26
  59. package/driver/publish/report-data.mjs +4 -3
  60. package/driver/publish/xlsx.mjs +26 -4
  61. package/driver/queue-markers.mjs +44 -0
  62. package/driver/queue-watch-verdict.mjs +2 -2
  63. package/driver/reference-score.mjs +10 -2
  64. package/driver/register-availability.mjs +2 -2
  65. package/driver/register-plan.mjs +313 -21
  66. package/driver/roster-verdict.mjs +1 -1
  67. package/driver/runner.mjs +26 -2
  68. package/driver/settle-stamp.mjs +10 -3
  69. package/driver/skills/clearance-common-law/SKILL.md +2 -0
  70. package/driver/skills/clearance-register/SKILL.md +44 -3
  71. package/driver/skills/clearance-register/digest.md +5 -5
  72. package/driver/skills/clearance-register/providers/clarivate.md +1 -1
  73. package/driver/skills/clearance-register/unit.md +39 -0
  74. package/driver/skills/clearance-variants/SKILL.md +1 -1
  75. package/driver/skills/matter-frame/SKILL.md +4 -2
  76. package/driver/stages.mjs +12 -5
  77. package/driver/status-snapshot.mjs +2 -2
  78. package/driver/suite-census.json +293 -29
  79. package/driver/synthesis-record.mjs +80 -2
  80. package/driver/unit-file-drift.mjs +3 -3
  81. package/driver/unit-inventory.mjs +2 -2
  82. package/driver/unit-state-verdict.mjs +1 -1
  83. package/driver/updater-identity.mjs +2 -3
  84. package/driver/variant-manifest-model.mjs +11 -1
  85. package/driver/verify.mjs +5 -5
  86. package/driver/withheld-families.mjs +104 -0
  87. package/mcp-server/CHANGELOG.md +8 -0
  88. package/mcp-server/lib/brief.mjs +16 -12
  89. package/mcp-server/lib/runs.mjs +1 -1
  90. package/mcp-server/package.json +1 -1
  91. package/mcp-server/server.mjs +3 -2
  92. package/package.json +2 -2
  93. package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
  94. package/portal-ui/dist/index.html +1 -1
  95. package/portal-ui/package.json +1 -1
  96. package/providers/_shared/count.mjs +2 -2
  97. package/providers/_shared/enumerate.mjs +15 -2
  98. package/providers/_shared/execute-plan.mjs +19 -1
  99. package/providers/_shared/plan-guards.mjs +40 -0
  100. package/providers/clarivate/src/capabilities.js +15 -5
  101. package/providers/clarivate/src/core.js +41 -5
  102. package/providers/corsearch/src/capabilities.js +4 -0
  103. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  104. package/providers/oauth-mcp-bridge/package.json +1 -1
  105. package/providers/signa/src/capabilities.js +22 -8
  106. package/providers/signa/src/core.js +12 -1
  107. package/scripts/demo-evidence.mjs +114 -0
  108. package/scripts/e2e.mjs +1 -1
  109. package/scripts/engine-probe.mjs +6 -5
  110. package/scripts/env-audit.mjs +1 -1
  111. package/scripts/freeze-example-run.mjs +3 -3
  112. package/scripts/live-surface-check.mjs +32 -33
  113. package/scripts/mint-suite-census.mjs +66 -0
  114. package/scripts/package-size-budget.mjs +117 -0
  115. package/scripts/register-plan-shape.mjs +259 -0
  116. package/scripts/release-note-required.mjs +38 -1
  117. package/scripts/repo-writes.mjs +1 -1
  118. package/scripts/report-sections-render-check.mjs +7 -3
  119. package/scripts/score.mjs +7 -1
  120. package/scripts/settings-render-check.mjs +36 -0
  121. package/scripts/travelling-predicates.mjs +1 -1
  122. package/shared/identifier-scan.mjs +22 -5
  123. package/shared/scroll-settle.mjs +67 -0
@@ -106,6 +106,7 @@ import { processTable } from "../shared/process-table.mjs";
106
106
  import { envFrom } from "../shared/env-aliases.mjs";
107
107
  import { gitTry, treeOf } from "../shared/tree-commit.mjs"; // — a packaged install has no git, and says its commit in build-info.json
108
108
  import { exitFor } from "../driver/surface-exit-verdict.mjs"; // — a check that could not look is not a drift, and they want different things done // — the name a reader is told to set is the one in force
109
+ import { planRunAgreementVerdict } from "../driver/plan-run-agreement-verdict.mjs";
109
110
 
110
111
  const HERE = dirname(fileURLToPath(import.meta.url));
111
112
  const asJson = process.argv.includes("--json");
@@ -500,8 +501,8 @@ if (!snapshot) {
500
501
  catch { return null; }
501
502
  })();
502
503
  const { groups: managerGroups, why } = readUserManagerGroups(uid);
503
- const { state, message } = managerGroupsVerdict({ idGroups, managerGroups, user, uid, why });
504
- ({ pass, fail, skip })[state]("user manager groups are current", message);
504
+ const v = managerGroupsVerdict({ idGroups, managerGroups, user, uid, why });
505
+ record("user manager groups are current", v.state, v.message, v.blocked === true);
505
506
  }
506
507
 
507
508
  // 1b. — HOW THIS BOX DIFFERS FROM PRODUCTION on the flags that change output without saying so.
@@ -582,9 +583,9 @@ try {
582
583
  try { caller = (await import("../driver/portal-service.mjs")).opsTokenPosture(OPS_TOKEN); }
583
584
  catch { /* stays unreadable — the verdict reports what it could not compare */ }
584
585
  if (bundledDemos) {
585
- const { state, message } = rosterVerdict({ keys, onDisk, bundledDemos, caller,
586
+ const v = rosterVerdict({ keys, onDisk, bundledDemos, caller,
586
587
  expectDemos: process.env.CLEAROTRON_E2E_EXPECT_DEMO_ROSTER === "1" });
587
- ({ pass, fail, skip })[state]("roster resolves", message);
588
+ record("roster resolves", v.state, v.message, v.blocked === true);
588
589
  }
589
590
  // A COMPANY IN THE STORE THAT THIS KEY CANNOT START is its own finding, not a roster disagreement. A
590
591
  // warning, not a failure: the portal re-takes its credential at the start of every call, so a Start for
@@ -639,7 +640,7 @@ try {
639
640
  effectiveMode: (process.env.TRADEMARK_MCP_AUTH_MODE || "").trim().toLowerCase() === "token" ? "token" : "cf-access",
640
641
  allowedHosts: (process.env.TRADEMARK_MCP_ALLOWED_HOSTS || "").split(",").map((h) => h.trim()).filter(Boolean),
641
642
  });
642
- record("the ops door's auth mode was chosen for it", posture.state, posture.message);
643
+ record("the ops door's auth mode was chosen for it", posture.state, posture.message, posture.blocked === true);
643
644
  }
644
645
  } catch (e) {
645
646
  if (e?.message === "__door_unset__") skip("ops-MCP reachable", "this instance does not say where its ops-MCP is — NOT PROBED");
@@ -735,20 +736,11 @@ if (mcpOptions && built) {
735
736
  //
736
737
  // plan_run needs an explicit profileKey: an accounts-scoped session refuses without one. The key
737
738
  // comes from what the door ITSELF resolved (below), so no customer is ever hardcoded here.
738
- const planDisagreements = [];
739
- for (const key of (probeProfileKey ? seen : [])) {
740
- try {
741
- const plan = await mcpToolCall({ url: MCP_URL, token: OPS_TOKEN, tool: "plan_run",
742
- args: { markName: "SURFACE CHECK", classes: [9], product: key, profileKey: probeProfileKey }, timeoutMs: 20000 });
743
- const unavailable = (plan?.blockers ?? []).some((b) => /not part of the current release|not switched on|unavailable/i.test(String(b)));
744
- if (unavailable !== !doorSays.get(key)) {
745
- planDisagreements.push(`${key}: describe_options=${doorSays.get(key) ? "available" : "unavailable"} plan_run=${unavailable ? "unavailable" : "available"}`);
746
- }
747
- } catch (e) { planDisagreements.push(`${key}: plan_run errored — ${e.message.slice(0, 120)}`); }
748
- }
749
- if (!probeProfileKey) skip("describe_options and plan_run agree", "no customer resolved to plan against — see the roster check");
750
- else if (planDisagreements.length) fail("describe_options and plan_run agree", planDisagreements.join(" · "));
751
- else pass("describe_options and plan_run agree", `${seen.length} products checked through both code paths`);
739
+ // A plan_run that throws (a 429, a timeout) compared nothing: a marked skip, exit 3, never a drift.
740
+ const pv = await planRunAgreementVerdict({ keys: seen, doorSays, probeProfileKey,
741
+ ask: (key) => mcpToolCall({ url: MCP_URL, token: OPS_TOKEN, tool: "plan_run",
742
+ args: { markName: "SURFACE CHECK", classes: [9], product: key, profileKey: probeProfileKey }, timeoutMs: 20000 }) });
743
+ record("describe_options and plan_run agree", pv.state, pv.message, pv.blocked === true);
752
744
  }
753
745
 
754
746
  // 5. Leak scan on what a door actually returned. An env name or a path in a live response is a defect
@@ -827,11 +819,12 @@ else {
827
819
  // process that is in no unit, so this arm derives from the PROCESS TABLE and from a stamp the drainer
828
820
  // writes about itself.
829
821
  //
830
- // IT FAILS ON COULD-NOT-LOOK, and that is deliberate. This script exits non-zero on `fail` only —
831
- // `skip` does not move the exit code — so recording an absent stamp as a skip would let the deploy
832
- // report a build live having never established what the executing process holds, which is the exact
833
- // state the incident's drainer was in. The fourth criterion of that issue is that the deploy does not
834
- // report a build live until this arm has looked; a skip here would be that criterion silently unmet.
822
+ // A COULD-NOT-LOOK HERE MOVES THE EXIT CODE, and that is deliberate. An ordinary skip does not, so
823
+ // recording an absent stamp as one would let the deploy report a build live having never established
824
+ // what the executing process holds, which is the exact state the incident's drainer was in. It is
825
+ // recorded through `blocked`, which exits 3 rather than the 1 a drift gives: the deploy still does not
826
+ // report a build live until this arm has looked, and the reader is told it could not look rather than
827
+ // that something drifted.
835
828
  {
836
829
  let workspaceRoot = null, resolveError = null;
837
830
  try { workspaceRoot = config.workspaceRoot; }
@@ -937,7 +930,7 @@ else {
937
930
  // which tree this deploy is. Reading it twice is how they would come to.
938
931
  deployClone,
939
932
  });
940
- record("the updater that deploys this box is the current one", v.state, v.message);
933
+ record("the updater that deploys this box is the current one", v.state, v.message, v.blocked === true);
941
934
  }
942
935
  }
943
936
  }
@@ -947,10 +940,10 @@ else {
947
940
  // inline string equality over a two-word vocabulary, which is why a `Type=oneshot` doing its job read as
948
941
  // a fault on the deploy's final gate. The decision now lives in driver/unit-state-verdict.mjs, where a
949
942
  // test can reach it — same move made for the roster arm, for the same reason.
950
- // `record`, not the ({pass, fail, skip})[state] shorthand used above: `warn` is a real outcome here.
943
+ // `record`, not a ({pass, fail, skip})[state] shorthand: `warn` is a real outcome here.
951
944
  {
952
945
  const v = unitsActiveVerdict({ units: clones, probe: unitProbe });
953
- record("units active", v.state, v.message);
946
+ record("units active", v.state, v.message, v.blocked === true);
954
947
  }
955
948
 
956
949
  // 10. — THE UNIT A BOX RUNS versus the unit the deployed commit SHIPS. The deploy syncs code, not
@@ -1013,7 +1006,7 @@ else {
1013
1006
  return { unit: fragName ?? c.unit, live, tracked, dropIns };
1014
1007
  });
1015
1008
  const v = unitFileDriftVerdict({ units: rows, probe: unitProbe });
1016
- record("units match the deployed commit", v.state, v.message);
1009
+ record("units match the deployed commit", v.state, v.message, v.blocked === true);
1017
1010
  }
1018
1011
 
1019
1012
  // 10a. — IS EVERY UNIT THIS BOX RUNS ACCOUNTED FOR AT ALL?
@@ -1082,7 +1075,7 @@ else {
1082
1075
  const box = deploymentBox(); // — the shared rule, so /portal/health cannot disagree with this
1083
1076
  const v = unitInventoryVerdict({ live: liveUnits, files: walk.files, collisions: walk.collisions,
1084
1077
  filesError: walk.error, box, probe, boxNames: DEPLOYMENT_BOXES });
1085
- record("every live unit is declared", v.state, v.message);
1078
+ record("every live unit is declared", v.state, v.message, v.blocked === true);
1086
1079
 
1087
1080
  // — AND WHETHER ANYTHING STILL STARTS THE TIMER-DRIVEN ONES. Reported separately from the line above
1088
1081
  // because it answers a different question: that one says a declared unit exists and is not adrift,
@@ -1090,7 +1083,7 @@ else {
1090
1083
  // fails the second, and reads `inactive` for both — which is why one line could not carry both.
1091
1084
  const t = declaredTimers();
1092
1085
  const tv = timerVerdict(t.rows, { probeFailed: t.probeFailed });
1093
- record("every declared timer is still armed", tv.state, tv.message);
1086
+ record("every declared timer is still armed", tv.state, tv.message, tv.blocked === true);
1094
1087
  }
1095
1088
 
1096
1089
  // ── — EVERY QUEUE THIS DEPLOYMENT WOULD DRAIN IS WATCHED BY SOMETHING ──────────────────────────
@@ -1116,7 +1109,7 @@ else {
1116
1109
  // job's life, and two readers of one unit file is how the deploy tick and a door come to different
1117
1110
  // conclusions about the same box. The unit path now has one home.
1118
1111
  const v = probeQueueWatch({ queueDirs, resolveError });
1119
- record("every queue this deployment would drain is watched", v.state, v.message);
1112
+ record("every queue this deployment would drain is watched", v.state, v.message, v.blocked === true);
1120
1113
  }
1121
1114
 
1122
1115
  // ── report ───────────────────────────────────────────────────────────────────────────────────────────
@@ -1129,8 +1122,14 @@ const couldNotLook = results.filter((r) => r.blocked);
1129
1122
  // — extracted for the reason roster-verdict and unit-state-verdict were: this file is a program, and a
1130
1123
  // decision that can only be reached by running it is a decision nobody can drive.
1131
1124
 
1125
+ // ONE DECISION, READ BY BOTH SURFACES. `--json` used to print `ok: failed.length === 0`, so a run that
1126
+ // could not look at a surface and found nothing else wrong told a machine reader `ok: true` while the
1127
+ // terminal said COULD NOT LOOK and the process exited 3. The human surface was fixed and the one a script
1128
+ // believes silently was not. `ok` is now the exit code's own answer, and `exit` carries which of the three.
1129
+ const code = exitFor({ failed: failed.length, couldNotLook: couldNotLook.length });
1130
+
1132
1131
  if (asJson) {
1133
- console.log(JSON.stringify({ ok: failed.length === 0, poolRoot: POOL_ROOT, register: wiredRegister, results }, null, 2));
1132
+ console.log(JSON.stringify({ ok: code === 0, exit: code, poolRoot: POOL_ROOT, register: wiredRegister, results }, null, 2));
1134
1133
  } else {
1135
1134
  const mark = { pass: " ok ", fail: " FAIL ", warn: " warn ", skip: " skip " };
1136
1135
  console.log(`\n== live surface check — ${POOL_ROOT} ==\n`);
@@ -1148,4 +1147,4 @@ if (asJson) {
1148
1147
  }
1149
1148
  }
1150
1149
 
1151
- process.exit(exitFor({ failed: failed.length, couldNotLook: couldNotLook.length }));
1150
+ process.exit(code);
@@ -148,6 +148,60 @@ export function laidPathVerdict({ laid = 0, laidPaths = [], cutRecordPresent = f
148
148
  * A tree with no HEAD cannot say what it published. That is a failure to look, and it exits rather
149
149
  * than reading as "nothing is laid here", which is the permissive answer and the one that passes.
150
150
  */
151
+ /**
152
+ * — A HALF-FINISHED MERGE CANNOT SAY WHAT ITS SUITE IS, and it does not know that it cannot.
153
+ *
154
+ * The refusal above catches one of the two ways a mid-merge tree lies to this script: a path arriving
155
+ * from the other parent is in the index and not in HEAD, so it is laid, named and refused. It cannot
156
+ * catch the other. A file both parents changed IS in HEAD, so nothing about it is laid — it is counted,
157
+ * and what is counted is a working copy holding `<<<<<<<`, `=======` and `>>>>>>>` as though they were
158
+ * source. Measured 2026-09-21 on a tree conflicted in one test file: the mint read all 980 driver
159
+ * files, counted the conflicted one, printed "unchanged — no file added, removed, grown, shrunk or
160
+ * newly skipped", and exited 0. The operator's confirmation and the defect are the same sentence.
161
+ *
162
+ * THE STATE IS THE TEST, NOT THE MARKERS. Scanning files for marker lines would find this instance and
163
+ * would also refuse a test that legitimately contains one in a string, while missing a conflict
164
+ * resolved into plausible nonsense. Git already publishes the fact: an operation is half-finished, so
165
+ * no tree it produced is a population anyone should stamp. That covers both mechanisms at once, and
166
+ * covers a rebase and a cherry-pick, where the same two lies are available for the same reason.
167
+ *
168
+ * REFUSES RATHER THAN REPAIRS. Reading the index instead of HEAD would fix the counting and would still
169
+ * mint over markers, so it is the weaker of the two. There is a correct tree a few seconds away —
170
+ * finish the operation and mint on the result — and the census is a deliberate act whose whole value is
171
+ * that somebody looked at the diff.
172
+ *
173
+ * Pure, and takes the list rather than reading it, so both branches can be driven without a test
174
+ * having to conflict a real tree.
175
+ */
176
+ export const MID_OPERATION_STATES = Object.freeze([
177
+ ["MERGE_HEAD", "merge"],
178
+ ["CHERRY_PICK_HEAD", "cherry-pick"],
179
+ ["REVERT_HEAD", "revert"],
180
+ ["rebase-merge", "rebase"],
181
+ ["rebase-apply", "rebase"],
182
+ ]);
183
+
184
+ export function midOperationVerdict({ inProgress = [] } = {}) {
185
+ if (!inProgress.length) return { refuse: false, message: null };
186
+ const what = [...new Set(inProgress.map(([, label]) => label))].join(" and a ");
187
+ return { refuse: true,
188
+ message: `mint-suite-census: this tree is in the middle of a ${what}, so the files in it are not a\n`
189
+ + " population anybody published. A path from the other parent is missing from HEAD and would be\n"
190
+ + " dropped from the count; a file both sides changed is in HEAD and would be counted WITH its\n"
191
+ + " conflict markers. Either way this would print \"unchanged\" for a population it had just\n"
192
+ + ` misread. Finish the ${what} and mint on the tree that comes out.` };
193
+ }
194
+
195
+ /** Which half-finished operations this worktree's own git directory is carrying. */
196
+ export function operationsInProgress(root, exists = existsSync) {
197
+ let gitDir;
198
+ try {
199
+ gitDir = execFileSync("git", ["-C", root, "rev-parse", "--absolute-git-dir"], { encoding: "utf8" }).trim();
200
+ } catch { return []; }
201
+ if (!gitDir) return [];
202
+ return MID_OPERATION_STATES.filter(([name]) => exists(join(gitDir, name)));
203
+ }
204
+
151
205
  /**
152
206
  * The paths the private overlay says it laid over this clone, or null where it laid nothing.
153
207
  *
@@ -219,6 +273,18 @@ export function buildCensus(root = ROOT) {
219
273
  const readManifest = (rel) => { try { return JSON.parse(readFileSync(join(ROOT, rel), "utf8")); } catch { return null; } };
220
274
 
221
275
  function main() {
276
+ // ── BEFORE EVEN THAT — IS THIS TREE A POPULATION AT ALL? ────────────────────────────────────
277
+ //
278
+ // First, because every check below reads files from this working tree, and mid-merge those files are
279
+ // not what either parent published. A census that is internally consistent about a tree nobody
280
+ // committed passes every other refusal in this file, `--check` included.
281
+ const mid = midOperationVerdict({ inProgress: operationsInProgress(ROOT) });
282
+ if (mid.refuse) {
283
+ console.error(`\nREFUSING — ${mid.message}`);
284
+ process.exitCode = 2;
285
+ return;
286
+ }
287
+
222
288
  // ── ITEM 1 — BEFORE ANY COUNTING, DOES THIS CENSUS DESCRIBE WHAT THE RUNNER RUNS? ───────────
223
289
  //
224
290
  // FIRST, and it refuses rather than warns. Everything below counts files inside a corpus this list
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
4
+ // package-size-budget.mjs — THE TARBALL CANNOT GROW WITHOUT SOMEBODY DECIDING THAT IT SHOULD.
5
+ //
6
+ // The package is large by design: most of it is bundled demo evidence, and that was ruled to be right.
7
+ // What was missing is not a smaller package, it is a MEASUREMENT — nothing recorded the size, so it
8
+ // could double across a few betas and no one would find out from the repository. A number nobody
9
+ // records is a number nobody can notice changing.
10
+ //
11
+ // WHAT THIS IS NOT. It is not a size limit chosen by taste, and it does not argue about what ships.
12
+ // It compares against a RECORDED baseline and allows a margin; anything inside the margin passes
13
+ // silently, anything past it fails and prints the ten largest paths so the growth has a name before
14
+ // anyone debates it.
15
+ //
16
+ // WHY A COMMITTED BASELINE RATHER THAN A LIVE LOOKUP of the last published version. A gate must give
17
+ // the same answer on every machine, including one with no network, and it must not start failing
18
+ // because a registry is slow or a version was unpublished. The baseline is minted into this repository
19
+ // the same way the suite census is, so the comparison is deterministic, reviewable in the diff that
20
+ // changes it, and honest about being a decision somebody made rather than a fact fetched at runtime.
21
+ //
22
+ // Usage:
23
+ // node scripts/package-size-budget.mjs measure and report against the baseline
24
+ // node scripts/package-size-budget.mjs --check …and exit non-zero when it is past the margin
25
+ // node scripts/package-size-budget.mjs --apply re-record the baseline as it is now
26
+
27
+ import { execFileSync } from "node:child_process";
28
+ import { readFileSync, writeFileSync, existsSync } from "node:fs";
29
+ import { dirname, join } from "node:path";
30
+ import { fileURLToPath } from "node:url";
31
+
32
+ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
33
+ const BASELINE = join(ROOT, "package-size-baseline.json");
34
+
35
+ // THE MARGIN, and why it is one number rather than per-field taste. A change that adds a demo run
36
+ // moves every field at once, so a single proportion catches the shape of growth that matters and
37
+ // leaves ordinary churn alone. 10% is roughly one more bundled run on today's package — big enough
38
+ // that nobody trips it by editing source, small enough that a doubling cannot arrive unannounced.
39
+ export const MARGIN = 0.10;
40
+
41
+ /** What `npm pack` says this tree would publish. */
42
+ export function measure({ root = ROOT } = {}) {
43
+ const out = execFileSync("npm", ["pack", "--dry-run", "--json"], {
44
+ cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], maxBuffer: 64 * 1024 * 1024,
45
+ });
46
+ const p = JSON.parse(out)[0];
47
+ return {
48
+ size: p.size,
49
+ unpackedSize: p.unpackedSize,
50
+ entryCount: p.entryCount,
51
+ files: (p.files ?? []).map((f) => ({ path: f.path, size: f.size })),
52
+ };
53
+ }
54
+
55
+ const mb = (n) => `${(n / 1_000_000).toFixed(1)} MB`;
56
+ const pct = (now, was) => `${now >= was ? "+" : ""}${(((now - was) / was) * 100).toFixed(1)}%`;
57
+
58
+ /** The ten largest paths, so growth has a name rather than a number. */
59
+ export function largest(files, n = 10) {
60
+ return [...files].sort((a, b) => b.size - a.size).slice(0, n);
61
+ }
62
+
63
+ /**
64
+ * Compare a measurement against a baseline. Returns `{ over: [...], rows: [...] }` — `over` names the
65
+ * fields past the margin and is empty when nothing is.
66
+ */
67
+ export function compare(now, was, { margin = MARGIN } = {}) {
68
+ const rows = [];
69
+ const over = [];
70
+ for (const [field, label] of [["size", "packed"], ["unpackedSize", "unpacked"], ["entryCount", "entries"]]) {
71
+ const limit = Math.floor(was[field] * (1 + margin));
72
+ const breached = now[field] > limit;
73
+ if (breached) over.push(field);
74
+ rows.push({ field, label, now: now[field], was: was[field], limit, breached });
75
+ }
76
+ return { over, rows };
77
+ }
78
+
79
+ if (import.meta.url === `file://${process.argv[1]}`) {
80
+ const apply = process.argv.includes("--apply");
81
+ const check = process.argv.includes("--check");
82
+ const now = measure();
83
+
84
+ if (apply) {
85
+ writeFileSync(BASELINE, `${JSON.stringify({
86
+ note: "What `npm pack` produced when this was recorded. The budget allows a margin over these; see scripts/package-size-budget.mjs.",
87
+ margin: MARGIN,
88
+ size: now.size, unpackedSize: now.unpackedSize, entryCount: now.entryCount,
89
+ }, null, 2)}\n`);
90
+ console.log(`recorded: ${mb(now.size)} packed, ${mb(now.unpackedSize)} unpacked, ${now.entryCount} entries`);
91
+ process.exit(0);
92
+ }
93
+
94
+ if (!existsSync(BASELINE)) {
95
+ console.error("package-size-budget: no baseline recorded. `node scripts/package-size-budget.mjs --apply` writes one.");
96
+ process.exit(check ? 1 : 0);
97
+ }
98
+ const was = JSON.parse(readFileSync(BASELINE, "utf8"));
99
+ const { over, rows } = compare(now, was, { margin: was.margin ?? MARGIN });
100
+
101
+ for (const r of rows) {
102
+ const shown = r.field === "entryCount" ? `${r.now} against ${r.was}` : `${mb(r.now)} against ${mb(r.was)}`;
103
+ console.log(` ${r.label.padEnd(9)} ${shown} (${pct(r.now, r.was)}${r.breached ? " — PAST THE MARGIN" : ""})`);
104
+ }
105
+
106
+ if (!over.length) {
107
+ console.log(`package-size-budget: inside the ${Math.round((was.margin ?? MARGIN) * 100)}% margin.`);
108
+ process.exit(0);
109
+ }
110
+
111
+ console.error(`\npackage-size-budget: ${over.join(" and ")} past the ${Math.round((was.margin ?? MARGIN) * 100)}% margin.`);
112
+ console.error("The ten largest paths in what would be published:\n");
113
+ for (const f of largest(now.files)) console.error(` ${mb(f.size).padStart(8)} ${f.path}`);
114
+ console.error("\nIf the growth is intended, re-record the baseline in the same commit that causes it:");
115
+ console.error(" node scripts/package-size-budget.mjs --apply");
116
+ process.exit(check ? 1 : 0);
117
+ }
@@ -0,0 +1,259 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
4
+ //
5
+ // register-plan-shape.mjs — recompile a recorded run's register plan from its own inputs and print
6
+ // the SHAPE of the result: how many questions run without being asked for, how many wait, and whether
7
+ // the waiting ones are waiting for the reading turn.
8
+ //
9
+ // node scripts/register-plan-shape.mjs <run-directory>
10
+ //
11
+ // WHY THIS PRINTS COUNTS AND NOTHING ELSE. It exists to be run against recorded client runs, where the
12
+ // mark, the goods words, the owners and the records are not ours to quote anywhere — and a plan entry's
13
+ // own qid contains the mark, so even an identifier list would be a disclosure. What comes out is
14
+ // arithmetic: numbers, axis names and predicate names, all of which are vocabulary this repository
15
+ // already publishes. Nothing here reads a record, and nothing here can print one.
16
+ //
17
+ // IT WRITES NOTHING, which is the other half of being safe to point at a real run. The ordinary compile
18
+ // path freezes the plan it built and writes run-journal rows; this reads the same inputs and compiles
19
+ // in memory. A recorded run is evidence, and a tool that measures it must leave it byte-identical.
20
+ //
21
+ // WHAT IT IS FOR. The plan a recorded run froze was compiled by the compiler of its day. Reading that
22
+ // artifact says what the engine DID ask; it cannot say what today's engine WOULD ask. Recompiling from
23
+ // the run's own inputs answers the second question, which is the one a rule change has to be judged on.
24
+ //
25
+ // AN ABSENT INPUT IS A FINDING, NOT A DEFAULT. Every input is named and checked, and a missing one
26
+ // stops the run with the path that was looked for. A compile over a half-read run directory would
27
+ // produce a number, and a number produced that way is worse than no number.
28
+
29
+ import { readFileSync, existsSync } from "node:fs";
30
+ import { resolve, join } from "node:path";
31
+ import { paths } from "../driver/stages.mjs";
32
+ import { driverDir } from "../shared/driver-dir.mjs"; // — the run-directory layout has one namer; a literal here is how two spellings drift
33
+ import { compileRegisterPlan, awaitsReadingTurn, planMaxOrWidth } from "../driver/register-plan.mjs";
34
+ import { parseVariantManifestModel } from "../driver/variant-manifest-model.mjs";
35
+ import { registerCapabilities, registerUnavailableOffices } from "../driver/register-unreachable.mjs";
36
+ import { frameIdentifiedClassRows } from "../driver/matter-frame-record.mjs";
37
+ import { excludeHouseElement, mintSupplementalQid, HOUSE_ELEMENT_RECEIPT } from "../driver/register-plan.mjs";
38
+ import { inScopeClassList, registerJurisdictions } from "../driver/pipeline.mjs";
39
+ import { isEntrypoint } from "../shared/is-entrypoint.mjs";
40
+
41
+ const readJson = (p) => JSON.parse(readFileSync(p, "utf8"));
42
+
43
+ /**
44
+ * The three kinds that run without being asked for (decision 10, as amended 2026-09-21).
45
+ *
46
+ * READ OFF THE ENTRY, deliberately, rather than re-deriving the compiler's own test: this is a
47
+ * measuring instrument, and an instrument that shares the mechanism it measures cannot disagree with
48
+ * it. The subtlety worth stating is the third line — the BARE contains entry on the mark waits, and
49
+ * only the goods-narrowed one is always on (ruled 2026-09-20), so `provenance === "mark"` alone is not the
50
+ * test and a reader who assumed it were would count a waiting entry as open.
51
+ */
52
+ function openKind(e) {
53
+ if (e.axis === "saturation-probe") return "saturation probe";
54
+ if (Array.isArray(e.goods_text) && e.goods_text.length) return "goods-narrowed";
55
+ if (e.provenance === "mark" && String(e.predicate) !== "default") return "identical mark";
56
+ if (e.unsupported === true) return "unsupported (a disclosure, not a search)";
57
+ return null;
58
+ }
59
+
60
+ export function planShape(plan) {
61
+ const open = [], waiting = [], other = [], unexpected = [];
62
+ for (const e of plan.entries ?? []) {
63
+ if (awaitsReadingTurn(e.when)) { waiting.push(e); continue; }
64
+ if (e.when) { other.push(e); continue; }
65
+ const kind = openKind(e);
66
+ if (kind) open.push({ e, kind }); else unexpected.push(e);
67
+ }
68
+ const byKind = {};
69
+ for (const { kind } of open) byKind[kind] = (byKind[kind] ?? 0) + 1;
70
+ const byAxis = {};
71
+ for (const e of waiting) byAxis[e.axis] = (byAxis[e.axis] ?? 0) + 1;
72
+ // EVERY entry by axis, waiting or not — the comparison against a frozen plan needs the whole
73
+ // population per lane, not only the part that waits.
74
+ const byAxisAll = {};
75
+ for (const e of plan.entries ?? []) byAxisAll[e.axis] = (byAxisAll[e.axis] ?? 0) + 1;
76
+ return { total: (plan.entries ?? []).length, open: open.length, byKind,
77
+ waiting: waiting.length, byAxis, byAxisAll, parentGated: other.length,
78
+ unexpected: unexpected.map((e) => `${e.axis}/${e.predicate}`) };
79
+ }
80
+
81
+ function inputsFor(runDir) {
82
+ const P = paths(runDir);
83
+ const need = (label, p) => {
84
+ if (!existsSync(p)) {
85
+ console.error(`register-plan-shape: ${label} is not in this run directory — looked for ${p}.`);
86
+ console.error(" Without it the compile would be over a population this run never had. Refusing.");
87
+ process.exit(2);
88
+ }
89
+ return p;
90
+ };
91
+ let manifest = parseVariantManifestModel(readFileSync(need("the variant manifest", P.variantManifestModel), "utf8"));
92
+ // ── THE CLIENT'S OWN ELEMENT, IF THIS RUN VERIFIED THAT IT OWNS IT ──────────────────────────────
93
+ //
94
+ // MISSING FROM THE FIRST CUT, and it mattered on the first real run this tool was pointed at. Where
95
+ // a run verified that the client owns an element of its own mark, that element leaves the conflict
96
+ // analysis: the compile is handed the element so its form band becomes unreachable rather than
97
+ // merely unasked-for, and a single confirmation question is appended in its place. On a matter that
98
+ // is a house mark plus a tagline, that element can account for more than half the questions — so a
99
+ // recompile that skipped it reported a population the run could never have had, in the direction of
100
+ // too many. The condition is the receipt's own, exactly as the run applies it: an absent,
101
+ // unreadable or unverified receipt excludes nothing, which is the safe direction and the one a
102
+ // client is never harmed by.
103
+ let houseElement = null, houseConfirmation = null;
104
+ try {
105
+ const receipt = readJson(driverDir(runDir, HOUSE_ELEMENT_RECEIPT));
106
+ if (receipt?.verified === true) {
107
+ const r = excludeHouseElement(manifest, { element: receipt.element, remainder: receipt.remainder });
108
+ // REFUSED IS NOT APPLIED. The receipt answers who owns the element; the transform answers whether
109
+ // this manifest's mark survives the cut. The run requires both, so this does too.
110
+ if (!r.refused) { manifest = r.manifest; houseConfirmation = r.confirmation; houseElement = receipt.element; }
111
+ }
112
+ } catch { /* no receipt — nothing was verified, so nothing is excluded */ }
113
+ const scope = readJson(need("the instructed scope", driverDir(runDir, "instructed-scope.json")));
114
+ const profile = readJson(need("the customer profile", driverDir(runDir, "profile.json")));
115
+ // OPTIONAL INPUTS ARE REPORTED, NOT DEFAULTED IN SILENCE. Each one that is absent removes questions
116
+ // from the recompile, and an absence nobody is told about is indistinguishable from a compiler that
117
+ // stopped asking them. A preserved run directory that lost an input recompiles exactly like a
118
+ // compiler that retired a lane, and those are different facts.
119
+ //
120
+ // EACH LINE SAYS HOW MUCH, not that something is missing. The first cut said the form band "compiles
121
+ // to nothing" — true, and read by a careful reader as a large number on every register. It is a large
122
+ // number only where the register has no OR surface. That wording was acted on before it was measured.
123
+ const absent = [];
124
+ let form = null;
125
+ if (existsSync(P.formNeighbourhood)) form = readJson(P.formNeighbourhood);
126
+ else {
127
+ // QUANTIFIED, AND BY THIS REGISTER, because the qualitative version of this line was misleading and
128
+ // was acted on. The form band is OR-BATCHED at the provider's declared width, so on a register with
129
+ // a wide OR surface the WHOLE band — eight terms or forty — compiles to one question plus one
130
+ // wildcard fringe. Measured: 40 terms cost 2 questions on a 500-wide register and 41 on one with no
131
+ // OR surface. "A whole lane compiles to nothing" reads as a large number on every register and is
132
+ // one only on the narrow ones.
133
+ const w = planMaxOrWidth(registerCapabilities());
134
+ absent.push(`form-neighbourhood.json — the form band does not compile. On this register `
135
+ + `(OR width ${w}) that is about ${w > 1 ? "2 questions however many terms the band held" : "one question per term in the band"}.`);
136
+ }
137
+ if (!existsSync(driverDir(runDir, HOUSE_ELEMENT_RECEIPT)))
138
+ absent.push(`${HOUSE_ELEMENT_RECEIPT} — no element was excluded, so the client's own element is searched as a conflict`);
139
+ if (!(manifest.goods_words ?? []).length)
140
+ absent.push("goods_words on the manifest — no goods-narrowed question can compile from it");
141
+
142
+ // THE RUN'S OWN RESOLVERS, NOT THE RAW FIELDS. An earlier cut read `scope.classes` and
143
+ // `scope.jurisdictions` straight off the file. Two of those reads are wrong:
144
+ //
145
+ // · A matter that instructed NO class falls back to the customer profile's defaults, so the raw
146
+ // read compiles an unscoped plan where the run compiled a scoped one — and a plan is always
147
+ // class-scoped, so the compiler would refuse outright rather than differ quietly.
148
+ // · Territories come off a shared ladder that reads the geography stamp, not off the field alone.
149
+ //
150
+ // MEASURED, because the obvious third claim is not true and was in this comment until it was
151
+ // checked: a worldwide matter records `jurisdictions: null` and the resolver answers `[]`, which is
152
+ // the SAME answer the raw read gives, and `[]` is correct — worldwide means no region clause at all.
153
+ // The resolvers earn their place on the two cases above, not on that one.
154
+ //
155
+ // The whole scope object is handed over rather than two fields, so the ladder sees the stamp it reads.
156
+ const job = { ...scope, jobKey: "shape-read" };
157
+ return {
158
+ manifest,
159
+ job: { ...job, classes: inScopeClassList(job, profile), jurisdictions: registerJurisdictions(job, profile) },
160
+ addedClasses: frameIdentifiedClassRows(runDir),
161
+ form,
162
+ houseElement,
163
+ __houseConfirmation: houseConfirmation,
164
+ __absent: absent,
165
+ capabilities: registerCapabilities(),
166
+ unavailableOffices: registerUnavailableOffices(),
167
+ };
168
+ }
169
+
170
+ function main() {
171
+ const runDir = process.argv[2];
172
+ if (!runDir) {
173
+ console.error("usage: node scripts/register-plan-shape.mjs <run-directory>");
174
+ console.error(" Prints how many register questions run unasked and how many wait. Counts only —");
175
+ console.error(" no marks, no terms, no identifiers. Writes nothing into the run directory.");
176
+ process.exit(2);
177
+ }
178
+ const dir = resolve(runDir);
179
+ if (!existsSync(dir)) { console.error(`register-plan-shape: no such run directory: ${dir}`); process.exit(2); }
180
+
181
+ const inputs = inputsFor(dir);
182
+ const plan = compileRegisterPlan(inputs);
183
+ // THE ONE QUESTION THE EXCLUDED ELEMENT STILL OWES, appended after the compile exactly as the run
184
+ // appends it: the element leaves the conflict analysis, and in its place the plan asks once whether
185
+ // the client's own registrations are there. It is not derived from the manifest, so it cannot come
186
+ // out of the compile — and a recompile that dropped it would under-count by one on every run that
187
+ // excluded an element.
188
+ if (inputs.__houseConfirmation && plan?.entries) {
189
+ const used = new Set(plan.entries.map((e) => e.qid));
190
+ plan.entries.push({ ...inputs.__houseConfirmation,
191
+ qid: mintSupplementalQid({ prefix: "house", term: inputs.__houseConfirmation.term, used }),
192
+ nice_classes: plan.nice_classes ?? [], regions: plan.regions ?? [] });
193
+ }
194
+ const s = planShape(plan);
195
+
196
+ // WHICH REGISTER THIS BOX IS CONFIGURED FOR, because the shape depends on it — a provider that cannot
197
+ // express a predicate stamps the entry unsupported, and unsupported entries are counted here. A
198
+ // number read against the wrong provider is not this run's shape. The vendor name is vocabulary this
199
+ // repository already publishes; nothing about the matter is.
200
+ // ── AND WHAT THIS RUN ACTUALLY FROZE, BESIDE IT ────────────────────────────────────────────────
201
+ //
202
+ // The recompile answers "what would today's engine ask". On its own that number is uninterpretable:
203
+ // the first real run this tool saw compiled 44 questions against 129 in the frozen plan, and nobody
204
+ // could say from one number whether that was a deliberate removal, a defect, or a difference in the
205
+ // inputs. Printed side by side and split by axis, the same two numbers say WHERE the population
206
+ // moved, which is the question a reader actually has. Still counts only, and the frozen plan is read,
207
+ // never rewritten.
208
+ // THE CATCH IS NARROWED TO THE FILE BEING ABSENT, deliberately. A blanket catch here swallowed a
209
+ // ReferenceError on the first cut — the path was read off a variable that is not in scope in this
210
+ // function — and the comparison silently never printed, reading exactly like a run with no frozen
211
+ // plan. An absent artifact is a fact about the run; anything else is a fault in this script and must
212
+ // say so rather than look like one.
213
+ let frozen = null;
214
+ const frozenPath = paths(dir).registerPlan;
215
+ if (existsSync(frozenPath)) frozen = planShape(JSON.parse(readFileSync(frozenPath, "utf8")));
216
+
217
+ if (inputs.__absent?.length) {
218
+ console.log(" INPUTS THIS RUN DIRECTORY DOES NOT CARRY — each one lowers the counts below:");
219
+ for (const a of inputs.__absent) console.log(` · ${a}`);
220
+ console.log("");
221
+ }
222
+ console.log(` register configured here ${inputs.capabilities?.id ?? "none — CLEAROTRON_DATABASE is unset"}`);
223
+ console.log(` questions compiled ${s.total}`);
224
+ console.log(` run without being asked for ${s.open}`);
225
+ for (const [kind, n] of Object.entries(s.byKind).sort()) console.log(` ${String(n).padStart(4)} ${kind}`);
226
+ console.log(` waiting for the reading turn ${s.waiting}`);
227
+ for (const [axis, n] of Object.entries(s.byAxis).sort()) console.log(` ${String(n).padStart(4)} ${axis}`);
228
+ if (s.parentGated) console.log(` waiting on a parent question ${s.parentGated} (the crowd-gated fringe)`);
229
+ if (plan.added_classes?.length) console.log(` classes the frame added ${plan.added_classes.length}`);
230
+
231
+ if (frozen) {
232
+ console.log(`\n THIS RUN'S OWN FROZEN PLAN, for comparison — what it asked, not what today would ask:`);
233
+ console.log(` questions frozen ${frozen.total} (recompiled now: ${s.total})`);
234
+ const axes = [...new Set([...Object.keys(frozen.byAxis), ...Object.keys(s.byAxis),
235
+ ...(frozen.byAxisAll ? Object.keys(frozen.byAxisAll) : []), ...Object.keys(s.byAxisAll ?? {})])].sort();
236
+ for (const a of axes) {
237
+ const was = frozen.byAxisAll?.[a] ?? 0, now = s.byAxisAll?.[a] ?? 0;
238
+ if (was || now) console.log(` ${String(was).padStart(4)} -> ${String(now).padStart(4)} ${a}`);
239
+ }
240
+ if (frozen.total !== s.total) {
241
+ console.log(`\n The two populations differ by ${Math.abs(frozen.total - s.total)}. Gating marks an entry WAITING,`);
242
+ console.log(" it never removes one, so a difference here is the compiler's, not the gate's — the axis");
243
+ console.log(" rows above say which lane moved. Expected where a lane was deliberately retired.");
244
+ }
245
+ }
246
+
247
+ // THE PROPERTY, STATED AS A VERDICT the reader does not have to derive from the numbers above.
248
+ // `unexpected` is the whole of it: an entry that neither waits nor is one of the three sanctioned
249
+ // kinds is a question this plan would ask in the same breath as the identical mark.
250
+ if (s.unexpected.length) {
251
+ console.log(`\n NOT HELD — ${s.unexpected.length} question(s) run unasked that are none of the three kinds:`);
252
+ for (const u of [...new Set(s.unexpected)].sort()) console.log(` ${u}`);
253
+ process.exitCode = 1;
254
+ return;
255
+ }
256
+ console.log("\n HELD — every question outside the identical-mark, saturation and goods-narrowed set waits.");
257
+ }
258
+
259
+ if (isEntrypoint(import.meta.url)) main();