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.
- package/CONTRIBUTING.md +12 -0
- package/INSTALL.md +8 -0
- package/bin/onboard.mjs +109 -13
- package/bin/start.mjs +1 -1
- package/build-info.json +2 -2
- package/demo/MANIFEST.json +27 -0
- package/docs/INTAKE.md +8 -0
- package/docs/architecture/04-configuration-reference.md +29 -11
- package/driver/CHANGELOG.md +49 -0
- package/driver/citation-census.json +3 -3
- package/driver/clearance-variants-record.mjs +12 -1
- package/driver/common-law-coverage-status.mjs +113 -0
- package/driver/config-inventory.mjs +1 -1
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +37 -37
- package/driver/contract-vocabulary.mjs +8 -8
- package/driver/coverage-form-io.mjs +3 -1
- package/driver/coverage-form.mjs +38 -11
- package/driver/coverage-ledger.mjs +37 -7
- package/driver/coverage-union.mjs +2 -2
- package/driver/crowd-context.mjs +19 -6
- package/driver/dev-portal.mjs +3 -3
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +80 -9
- package/driver/engine/CONTRACT.md +3 -2
- package/driver/engine/anthropic-agent.mjs +34 -7
- package/driver/engine/mcp/clarivate-server.mjs +4 -2
- package/driver/engine/mcp/corsearch-server.mjs +3 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +47 -5
- package/driver/engine/mcp/euipo-server.mjs +2 -0
- package/driver/engine/mcp/free-tier-server.mjs +2 -0
- package/driver/engine/mcp/gather-config.mjs +8 -2
- package/driver/engine/mcp/probe-server.mjs +37 -0
- package/driver/engine/mcp/proposal-fields.mjs +45 -0
- package/driver/engine/mcp/recording-server.mjs +30 -0
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +89 -12
- package/driver/engine/mcp/unit-note-server.mjs +50 -0
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/engine/openai-agent.mjs +7 -0
- package/driver/engine/probe.mjs +67 -14
- package/driver/engine/tool-refusal.mjs +16 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/envelope-settle.mjs +82 -13
- package/driver/findings-model.mjs +4 -4
- package/driver/gateway.mjs +18 -2
- package/driver/manager-groups-verdict.mjs +1 -1
- package/driver/matter-frame-record.mjs +24 -7
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +12 -3
- package/driver/pipeline-knockout.mjs +3 -3
- package/driver/pipeline.mjs +154 -50
- package/driver/plan-run-agreement-verdict.mjs +49 -0
- package/driver/portal-service.mjs +8 -4
- package/driver/progress.mjs +14 -3
- package/driver/publish/index.mjs +41 -26
- package/driver/publish/report-data.mjs +4 -3
- package/driver/publish/xlsx.mjs +26 -4
- package/driver/queue-markers.mjs +44 -0
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/reference-score.mjs +10 -2
- package/driver/register-availability.mjs +2 -2
- package/driver/register-plan.mjs +313 -21
- package/driver/roster-verdict.mjs +1 -1
- package/driver/runner.mjs +26 -2
- package/driver/settle-stamp.mjs +10 -3
- package/driver/skills/clearance-common-law/SKILL.md +2 -0
- package/driver/skills/clearance-register/SKILL.md +44 -3
- package/driver/skills/clearance-register/digest.md +5 -5
- package/driver/skills/clearance-register/providers/clarivate.md +1 -1
- package/driver/skills/clearance-register/unit.md +39 -0
- package/driver/skills/clearance-variants/SKILL.md +1 -1
- package/driver/skills/matter-frame/SKILL.md +4 -2
- package/driver/stages.mjs +12 -5
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +293 -29
- package/driver/synthesis-record.mjs +80 -2
- package/driver/unit-file-drift.mjs +3 -3
- package/driver/unit-inventory.mjs +2 -2
- package/driver/unit-state-verdict.mjs +1 -1
- package/driver/updater-identity.mjs +2 -3
- package/driver/variant-manifest-model.mjs +11 -1
- package/driver/verify.mjs +5 -5
- package/driver/withheld-families.mjs +104 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/brief.mjs +16 -12
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +3 -2
- package/package.json +2 -2
- package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +2 -2
- package/providers/_shared/enumerate.mjs +15 -2
- package/providers/_shared/execute-plan.mjs +19 -1
- package/providers/_shared/plan-guards.mjs +40 -0
- package/providers/clarivate/src/capabilities.js +15 -5
- package/providers/clarivate/src/core.js +41 -5
- package/providers/corsearch/src/capabilities.js +4 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +22 -8
- package/providers/signa/src/core.js +12 -1
- package/scripts/demo-evidence.mjs +114 -0
- package/scripts/e2e.mjs +1 -1
- package/scripts/engine-probe.mjs +6 -5
- package/scripts/env-audit.mjs +1 -1
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/live-surface-check.mjs +32 -33
- package/scripts/mint-suite-census.mjs +66 -0
- package/scripts/package-size-budget.mjs +117 -0
- package/scripts/register-plan-shape.mjs +259 -0
- package/scripts/release-note-required.mjs +38 -1
- package/scripts/repo-writes.mjs +1 -1
- package/scripts/report-sections-render-check.mjs +7 -3
- package/scripts/score.mjs +7 -1
- package/scripts/settings-render-check.mjs +36 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/identifier-scan.mjs +22 -5
- 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
|
|
504
|
-
(
|
|
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
|
|
586
|
+
const v = rosterVerdict({ keys, onDisk, bundledDemos, caller,
|
|
586
587
|
expectDemos: process.env.CLEAROTRON_E2E_EXPECT_DEMO_ROSTER === "1" });
|
|
587
|
-
(
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
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
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
// report a build live until this arm has looked
|
|
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
|
|
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:
|
|
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(
|
|
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();
|