clearotron 0.3.2 → 0.3.3-beta.0
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 +29 -7
- 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 +1 -1
- package/driver/CHANGELOG.md +37 -0
- package/driver/clearance-variants-record.mjs +12 -1
- package/driver/common-law-coverage-status.mjs +113 -0
- 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/drainer-identity.mjs +1 -1
- 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 +28 -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.mjs +141 -37
- package/driver/publish/index.mjs +40 -25
- package/driver/publish/xlsx.mjs +26 -4
- package/driver/queue-markers.mjs +44 -0
- package/driver/queue-watch-verdict.mjs +2 -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/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 +1 -1
- package/driver/suite-census.json +236 -14
- 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 +4 -0
- package/mcp-server/lib/brief.mjs +5 -7
- 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 +1 -1
- 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 +4 -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/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 +26 -19
- 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/settings-render-check.mjs +36 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/identifier-scan.mjs +22 -5
|
@@ -90,6 +90,9 @@ export async function clarivateFetch(apiKey, base, path, { body = null, method =
|
|
|
90
90
|
attempts = i + 1;
|
|
91
91
|
resp = await fetch(url, init);
|
|
92
92
|
if (resp.ok || resp.status < 500 || i === retries) break;
|
|
93
|
+
// A 500 wrapping the search service's 400 is a refusal of the query as written, not weather: the
|
|
94
|
+
// same request is refused the same way, so it is not sent again (countFailureReason below).
|
|
95
|
+
if (WRAPPED_REFUSAL.test(await resp.clone().text().catch(() => ""))) break;
|
|
93
96
|
await new Promise((r) => setTimeout(r, 1500 * (i + 1)));
|
|
94
97
|
}
|
|
95
98
|
} catch (err) {
|
|
@@ -1113,13 +1116,45 @@ export async function doSearch(apiKey, base, params, tctx) {
|
|
|
1113
1116
|
}
|
|
1114
1117
|
|
|
1115
1118
|
// ── Count ─────────────────────────────────────────────────────────────────────────────────────────
|
|
1116
|
-
// POST /count takes the SAME SearchRequest body as /search, is cheap, works at ANY magnitude
|
|
1117
|
-
//
|
|
1119
|
+
// POST /count takes the SAME SearchRequest body as /search, is cheap, works at ANY magnitude and
|
|
1120
|
+
// returns PER-OFFICE counts in one call. This is the enumerate
|
|
1118
1121
|
// kernel's countProbe:"endpoint" dependency, so it returns the kernel's plain probe shape
|
|
1119
1122
|
// { ok, total, reason } — plus `per_office`, which is richer than corsearch can offer and rides into
|
|
1120
1123
|
// the crowd descriptor for judgment.
|
|
1121
1124
|
|
|
1122
1125
|
/** ONE /count round trip over already-resolved params. Returns the kernel's plain probe shape. */
|
|
1126
|
+
// ── A REFUSED QUERY IS NAMED AS ONE, with the size of what was sent ─────────────────────────────────
|
|
1127
|
+
//
|
|
1128
|
+
// The vendor answers a query its search service cannot take with that service's 400, delivered inside
|
|
1129
|
+
// its own 500: `INTERNAL_SERVER_ERROR - 400 on POST request for "…/search"`. Passed on as a bare HTTP 500
|
|
1130
|
+
// it reads as an outage and names nothing an operator can act on. A query that is too wide is the common
|
|
1131
|
+
// case: the service's bound is the number of OR terms in one field, which is about 496, not the query's
|
|
1132
|
+
// length.
|
|
1133
|
+
// The reason says so: the widest OR stack's term count and characters, and this provider's declared
|
|
1134
|
+
// width. It avoids the words `HTTP 400`, which providerRejectedTheQuery reads as a cue to retry an owner
|
|
1135
|
+
// stack on the caller's own term; that behaviour is unchanged.
|
|
1136
|
+
//
|
|
1137
|
+
// AND IT IS A CAPABILITY GAP. The service refused the query as written and will refuse the identical query
|
|
1138
|
+
// every time, so the reason carries CAPABILITY_GAP_MARKER: the plan executor defers the slice as a
|
|
1139
|
+
// disclosed gap at once, as it does a query buildSearchRequest refuses before sending, instead of sending
|
|
1140
|
+
// it round the recovery ladder. On production that ladder re-sent a refused 500-term query and spent a
|
|
1141
|
+
// recovery park each time. Every other HTTP failure keeps the transient reading.
|
|
1142
|
+
const WRAPPED_REFUSAL = /\b400\s+on\s+POST\s+request\b/;
|
|
1143
|
+
const widestOrStack = (body) => (body?.searchFields ?? []).reduce((best, f) => {
|
|
1144
|
+
const v = String(f?.value ?? "");
|
|
1145
|
+
const terms = v ? v.split(" OR ").length : 0;
|
|
1146
|
+
return terms > best.terms ? { terms, chars: v.length } : best;
|
|
1147
|
+
}, { terms: 0, chars: 0 });
|
|
1148
|
+
export function countFailureReason(status, words, body) {
|
|
1149
|
+
if (status !== 500 || !WRAPPED_REFUSAL.test(String(words ?? ""))) return `HTTP ${status}${words ? `: ${words}` : ""}`;
|
|
1150
|
+
const w = widestOrStack(body);
|
|
1151
|
+
// THE NUMBERS COME LAST. The executor clips a failed slice's reason to its head and tail, and its own
|
|
1152
|
+
// prefixes fill the head, so a count placed early is elided; the tail is what a run's record keeps.
|
|
1153
|
+
return `${CAPABILITY_GAP_MARKER} register_refused_query: the register refused this query as malformed (a 400 inside its `
|
|
1154
|
+
+ `HTTP 500). Provider's words: ${words}. Sent ${w.terms} OR terms (${w.chars} characters); this provider's declared `
|
|
1155
|
+
+ `width is ${CAPABILITIES.maxOrWidth} terms.`;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1123
1158
|
async function countOnce(apiKey, base, p, tctx) {
|
|
1124
1159
|
let body;
|
|
1125
1160
|
try { body = buildSearchRequest(p); }
|
|
@@ -1129,11 +1164,12 @@ async function countOnce(apiKey, base, p, tctx) {
|
|
|
1129
1164
|
// Marked as a capability gap so the plan executor DEFERS the slice — a disclosed coverage row the
|
|
1130
1165
|
// lawyer reads — instead of stamping a transient error and grinding the whole run to a fan-in
|
|
1131
1166
|
// StageFailure over an answer that will never differ. A real HTTP failure below keeps the transient
|
|
1132
|
-
// reading and rides the repair ladder, exactly as before
|
|
1167
|
+
// reading and rides the repair ladder, exactly as before, except a query the service refused as written
|
|
1168
|
+
// (countFailureReason above).
|
|
1133
1169
|
catch (e) { return { ok: false, total: null, per_office: null, reason: `${CAPABILITY_GAP_MARKER} ${e.message}` }; }
|
|
1134
1170
|
|
|
1135
1171
|
const r = await clarivateFetch(apiKey, base, "/count", { body, tctx: { ...tctx, target: String(echoOf(p)).slice(0, 200) } });
|
|
1136
|
-
if (!r.ok) return { ok: false, total: null, per_office: null, reason:
|
|
1172
|
+
if (!r.ok) return { ok: false, total: null, per_office: null, reason: countFailureReason(r.status, errText(r), body) };
|
|
1137
1173
|
// Named rather than folded into the "no counts{}" branch below: both refuse (total null, never 0),
|
|
1138
1174
|
// but someone repairing a run needs to know the response was CUT, not that the provider answered in
|
|
1139
1175
|
// a shape we did not expect.
|
|
@@ -1734,7 +1770,7 @@ export async function doEnumerate(apiKey, base, params, tctx) {
|
|
|
1734
1770
|
// dictated a count instead of an enumerate. POST /search FAILS LOUD past 30000 (tooManyResults),
|
|
1735
1771
|
// so routing the count probe through it would return an ERROR string, the executor would stamp the
|
|
1736
1772
|
// block error:true, and a SANCTIONED CROWD would be counted MISSING — doctrine 5 inverted. POST
|
|
1737
|
-
// /count never fails on magnitude
|
|
1773
|
+
// /count never fails on magnitude and carries per-office truth.
|
|
1738
1774
|
// `countParams` is therefore {}: buildSearchRequest has no `limit` knob to honour, and passing a
|
|
1739
1775
|
// dead parameter would only imply one exists.
|
|
1740
1776
|
//
|
|
@@ -133,6 +133,10 @@ export const CAPABILITIES = Object.freeze({
|
|
|
133
133
|
// Several goods terms become several `product:` clauses, and within one field this query language
|
|
134
134
|
// ORs them implicitly — the same "any of these words" the explicit OR writes elsewhere.
|
|
135
135
|
goodsTextListOr: true,
|
|
136
|
+
// The shortest term the contains form accepts: none known, so none declared. The vendor publishes no
|
|
137
|
+
// public reference for this API (none was found on 2026-09-22), and no short-term refusal has been
|
|
138
|
+
// measured here. `null` keeps the contains form at every length; see clarivate/capabilities.js.
|
|
139
|
+
containsMinLength: null,
|
|
136
140
|
// ── WHICH FORM OF A NON-LATIN MARK DOES THE INDEX HOLD? ──────────────────────────────────────────
|
|
137
141
|
// `true` = the CHARACTERS. A native-script term is a legitimate, productive query here and MUST be
|
|
138
142
|
// sent — the shared executor's script-form refusal (providers/_shared/script-form.mjs) is switched
|
|
@@ -154,6 +154,23 @@ export const CAPABILITIES = Object.freeze({
|
|
|
154
154
|
// So the compiler does not build a multi-term goods entry for this provider, and it must never fall
|
|
155
155
|
// back to a space-joined string. No coverage is lost: the broad class-wide sweep still runs.
|
|
156
156
|
goodsTextListOr: false,
|
|
157
|
+
// ── THE SHORTEST TERM THE CONTAINS FORM ACCEPTS: 3 folded characters, documented ────────────────
|
|
158
|
+
//
|
|
159
|
+
// Signa's API reference, "Search trademarks", section "Match modes" (docs.signa.so/api-reference/
|
|
160
|
+
// trademarks/search-trademarks, read 2026-09-22): "Deterministic modes accept a single-character `q`
|
|
161
|
+
// (one non-empty folded char), whereas `similar` requires at least 2 folded chars and `contains`
|
|
162
|
+
// requires at least 3." The same page's validation rules repeat it. A shorter term on `contains` is a
|
|
163
|
+
// hard 400 ("match=contains requires q to be at least 3 characters (after case/accent folding) to
|
|
164
|
+
// avoid an over-broad substring scan").
|
|
165
|
+
//
|
|
166
|
+
// The compiler reads this: a goods-narrowed question or a saturation probe whose term is shorter is
|
|
167
|
+
// asked on the exact form instead, with the same class and goods filters, and the plan entry says so.
|
|
168
|
+
// Exact goes out as the ranked `strategies: ["exact"]`, which `similar`'s 2-character floor governs,
|
|
169
|
+
// so a two-letter mark is answered; a one-letter mark is below both floors and stays refused.
|
|
170
|
+
containsMinLength: 3,
|
|
171
|
+
// The ranked shape's floor: "`similar` requires at least 2 folded chars" (same page). An exact question
|
|
172
|
+
// shorter than this goes out on the deterministic `match`, which takes one; from here up it stays ranked.
|
|
173
|
+
rankedMinLength: 2,
|
|
157
174
|
// Search rows already carry status / nice_classes / owner_name → screening is inline, zero extra calls.
|
|
158
175
|
screenSource: "search-row",
|
|
159
176
|
// No hard result ceiling, and no total to compare one against.
|
|
@@ -168,8 +185,6 @@ export const CAPABILITIES = Object.freeze({
|
|
|
168
185
|
//
|
|
169
186
|
// strategies[] exact | phonetic | fuzzy | prefix — ranked, several per call
|
|
170
187
|
// match similar | exact | starts_with | ends_with | contains — deterministic, one only
|
|
171
|
-
//
|
|
172
|
-
// Every value below was run against the live API with the resulting total recorded.
|
|
173
188
|
predicates: Object.freeze({
|
|
174
189
|
exact: "exact", // strategies[] — the deterministic shape, the ranked one for audit continuity
|
|
175
190
|
// `contains` IS the unanchored mode this contract said did not exist. The old header
|
|
@@ -177,12 +192,11 @@ export const CAPABILITIES = Object.freeze({
|
|
|
177
192
|
// contains slice is a substring sweep, and they return different sets. It never needed to be
|
|
178
193
|
// fuzzy; it needed the deterministic shape, which nothing here had.
|
|
179
194
|
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// file where the document and the wire disagree, and the wire is the one that answers queries.
|
|
195
|
+
// A LENGTH FLOOR THE VENDOR DOCUMENTS: deterministic modes take a query of 1 character or more,
|
|
196
|
+
// and `contains` demands THREE — "match=contains requires q to be at least 3 characters (after
|
|
197
|
+
// case/accent folding) to avoid an over-broad substring scan." A one- or two-character element
|
|
198
|
+
// therefore cannot ride this predicate, and it fails loud (400) rather than quietly returning a
|
|
199
|
+
// narrower set. `containsMinLength` above declares it, and the compiler asks such a term exactly.
|
|
186
200
|
default: "contains",
|
|
187
201
|
// `starts_with`, NOT the `prefix` strategy. Both exist and the executor uses this one:
|
|
188
202
|
// planPredicateParams emits `match_mode: "starts_with"` for a trailing-`*` entry and never emits
|
|
@@ -30,6 +30,9 @@ import { nonAnswerBodyError, parseJsonBody, unparsedBodyError } from "../../_sha
|
|
|
30
30
|
import { makeEnumerate, isOwnerScoped } from "../../_shared/enumerate.mjs";
|
|
31
31
|
import { makeExecutePlan } from "../../_shared/execute-plan.mjs";
|
|
32
32
|
import CAPABILITIES, { SIGNA_OFFICE_KEYS, OWNER_SCOPED_WINDOW } from "./capabilities.js";
|
|
33
|
+
// The register's own measure of a term's length: case and accents folded, spaces not counted — the same
|
|
34
|
+
// measure its documented floors are stated in (the compiler's foldedTermLength counts the same way).
|
|
35
|
+
const foldedLength = (q) => [...String(q ?? "").normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase().replace(/\s+/g, "")].length;
|
|
33
36
|
import { SIGNA_OFFICE_SNAPSHOT } from "./offices.generated.js";
|
|
34
37
|
|
|
35
38
|
export const DEFAULT_BASE = "https://api.signa.so";
|
|
@@ -660,8 +663,16 @@ export function toSignaParams(p = {}) {
|
|
|
660
663
|
// `match`. exact/phonetic stay on strategies, which is what this provider has always sent and what
|
|
661
664
|
// its fixtures were captured with; the anchored and unanchored modes can only be expressed by
|
|
662
665
|
// `match`, so they select that shape and drop strategies.
|
|
666
|
+
//
|
|
667
|
+
// EXACT BELOW THE RANKED FLOOR ONLY. The ranked shape takes `similar`'s floor of two folded
|
|
668
|
+
// characters, so a one-letter exact question is refused there. The deterministic `match: "exact"`
|
|
669
|
+
// takes one, but it asks a narrower question: "`exact` requires the whole mark to equal `q`", where
|
|
670
|
+
// the ranked `exact` strategy also returns longer marks that contain the term, which is the recall an
|
|
671
|
+
// identical question relies on. So exact stays ranked from the floor up, and only a term below it
|
|
672
|
+
// goes deterministic, where it is answered rather than refused.
|
|
663
673
|
const mode = String(p.match_mode ?? "").trim();
|
|
664
|
-
if (mode === "exact"
|
|
674
|
+
if (mode === "exact" && foldedLength(out.query) < CAPABILITIES.rankedMinLength) out.match = "exact";
|
|
675
|
+
else if (mode === "exact" || mode === "phonetic" || mode === "prefix") out.strategies = [mode];
|
|
665
676
|
else if (mode === "starts_with" || mode === "ends_with" || mode === "contains") out.match = mode;
|
|
666
677
|
// ── `default` IS THE COUNT LANE'S WORD FOR THE SAME UNANCHORED QUERY THE PLAN LANE CALLS `{}` ────
|
|
667
678
|
//
|
|
@@ -0,0 +1,114 @@
|
|
|
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
|
+
// demo-evidence.mjs — THE HAND-WRITTEN DIFF MUST BE REVIEWABLE ON ITS OWN.
|
|
5
|
+
//
|
|
6
|
+
// One integration branch carried 37 commits, 2,490 files, 221,105 additions and 410,984 deletions, and
|
|
7
|
+
// most of it was regenerated demo evidence. A reviewer cannot find the source changes inside that, so
|
|
8
|
+
// in practice nobody reads them — the review happens, and it reviews nothing. The evidence is not the
|
|
9
|
+
// problem: it is generated output, it is meant to change wholesale, and it is right that it ships.
|
|
10
|
+
// Mixing it into the same commits as hand-written code is the problem.
|
|
11
|
+
//
|
|
12
|
+
// TWO HALVES, because a convention nobody checks is a convention that lasts one beta:
|
|
13
|
+
//
|
|
14
|
+
// --check a commit that touches `demo/` touches nothing else, across a range
|
|
15
|
+
// --apply re-record `demo/MANIFEST.json` — generator, tree, and the run each product replays
|
|
16
|
+
//
|
|
17
|
+
// The manifest is what makes the regeneration commit self-explaining: without it a reviewer skipping
|
|
18
|
+
// the demo diff is taking on trust that it IS regenerated output rather than a hand edit hidden in
|
|
19
|
+
// 200,000 lines. With it, the thing they are skipping says what produced it.
|
|
20
|
+
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
import { readFileSync, writeFileSync, readdirSync, existsSync, statSync } from "node:fs";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
27
|
+
export const MANIFEST = join(ROOT, "demo", "MANIFEST.json");
|
|
28
|
+
export const GENERATOR = "scripts/freeze-example-run.mjs";
|
|
29
|
+
|
|
30
|
+
const git = (...args) => execFileSync("git", args, { cwd: ROOT, encoding: "utf8" }).trim();
|
|
31
|
+
|
|
32
|
+
/** Every product directory under `demo/`, with the run it replays. */
|
|
33
|
+
export function products({ root = ROOT } = {}) {
|
|
34
|
+
const dir = join(root, "demo");
|
|
35
|
+
if (!existsSync(dir)) return [];
|
|
36
|
+
return readdirSync(dir)
|
|
37
|
+
.filter((name) => statSync(join(dir, name)).isDirectory())
|
|
38
|
+
.sort()
|
|
39
|
+
.map((name) => {
|
|
40
|
+
let meta = {};
|
|
41
|
+
try { meta = JSON.parse(readFileSync(join(dir, name, "meta.json"), "utf8")); } catch { /* absent is an answer */ }
|
|
42
|
+
return { product: name, runId: meta.runId ?? null, template: meta.template ?? null };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Is this a commit that mixes generated evidence with hand-written source?
|
|
48
|
+
* Returns the offending paths, or an empty array. PURE given the file list.
|
|
49
|
+
*/
|
|
50
|
+
export function mixedPaths(files) {
|
|
51
|
+
const demo = files.filter((f) => f.startsWith("demo/"));
|
|
52
|
+
if (!demo.length) return []; // touches no evidence — nothing to say
|
|
53
|
+
return files.filter((f) => !f.startsWith("demo/"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
57
|
+
const apply = process.argv.includes("--apply");
|
|
58
|
+
|
|
59
|
+
if (apply) {
|
|
60
|
+
writeFileSync(MANIFEST, `${JSON.stringify({
|
|
61
|
+
note: "What produced the evidence under demo/. Re-recorded by scripts/demo-evidence.mjs --apply in the same commit that regenerates it.",
|
|
62
|
+
generator: GENERATOR,
|
|
63
|
+
recordedFromTree: git("rev-parse", "HEAD"),
|
|
64
|
+
products: products(),
|
|
65
|
+
}, null, 2)}\n`);
|
|
66
|
+
console.log(`recorded ${products().length} demo product(s) against ${git("rev-parse", "--short", "HEAD")}`);
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// --check: every commit in the range keeps its evidence to itself.
|
|
71
|
+
const base = process.argv[process.argv.indexOf("--base") + 1] || "origin/main";
|
|
72
|
+
|
|
73
|
+
// ── A BASE THIS CHECKOUT CANNOT RESOLVE IS A FAILURE TO LOOK, AND IT SAYS SO ────────────────────
|
|
74
|
+
//
|
|
75
|
+
// A shallow clone fetches one branch, so `origin/main` is not a ref in it and `rev-list base..HEAD`
|
|
76
|
+
// dies on an ambiguous argument — a stack trace naming a line of this file, which reads as a broken
|
|
77
|
+
// script rather than as a checkout that cannot answer the question. It is neither a clean range nor
|
|
78
|
+
// a mixed one: nothing was compared. Falling back to another base would be worse, because a range
|
|
79
|
+
// measured against something the caller did not ask for reports a clean result for a population it
|
|
80
|
+
// never examined.
|
|
81
|
+
try { git("rev-parse", "--verify", "--quiet", `${base}^{commit}`); }
|
|
82
|
+
catch {
|
|
83
|
+
console.error(`demo-evidence: this checkout has no "${base}", so no commit was compared.`);
|
|
84
|
+
console.error("");
|
|
85
|
+
console.error("Nothing is known about the range either way — this is not a clean result. A shallow");
|
|
86
|
+
console.error("checkout carries only the branch it fetched; fetch the base ref, or name one this");
|
|
87
|
+
console.error("clone holds with --base.");
|
|
88
|
+
process.exit(2);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const shas = git("rev-list", `${base}..HEAD`).split("\n").filter(Boolean);
|
|
92
|
+
const bad = [];
|
|
93
|
+
for (const sha of shas) {
|
|
94
|
+
const files = git("show", "--name-only", "--pretty=format:", sha).split("\n").filter(Boolean);
|
|
95
|
+
const mixed = mixedPaths(files);
|
|
96
|
+
if (mixed.length) bad.push({ sha, mixed });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!bad.length) {
|
|
100
|
+
console.log(`demo-evidence: ${shas.length} commit(s) against ${base}; none mixes generated evidence with source.`);
|
|
101
|
+
process.exit(0);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
console.error("demo-evidence: generated evidence is mixed with hand-written source.\n");
|
|
105
|
+
console.error("A reviewer cannot find the source diff inside a regeneration, so in practice nobody reads it.\n");
|
|
106
|
+
for (const { sha, mixed } of bad) {
|
|
107
|
+
console.error(` ${sha.slice(0, 9)} also touches ${mixed.length} non-demo path(s):`);
|
|
108
|
+
for (const f of mixed.slice(0, 5)) console.error(` ${f}`);
|
|
109
|
+
if (mixed.length > 5) console.error(` …and ${mixed.length - 5} more`);
|
|
110
|
+
}
|
|
111
|
+
console.error("\nSplit the regeneration into its own commit:");
|
|
112
|
+
console.error(" git reset HEAD~ && git add demo && git commit && git add -A && git commit");
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
package/scripts/engine-probe.mjs
CHANGED
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
// ── WHY THIS EXISTS ─────────────────────────────────────────────────────────────────────────────────
|
|
11
11
|
//
|
|
12
12
|
// `probeEngineTurn` is the smallest thing that genuinely exercises an engine: one Haiku-tier turn at low
|
|
13
|
-
// effort
|
|
14
|
-
// skills dir and no run dir. It answers the one question an operator asks before spending anything —
|
|
15
|
-
// CAN this box run a turn, and under the billing mode it thinks it is using.
|
|
13
|
+
// effort, through the adapter's own spawn path, asked to call the one tool on the probe's own server, with
|
|
14
|
+
// no skills dir and no run dir. It answers the one question an operator asks before spending anything —
|
|
15
|
+
// CAN this box run a turn that uses a tool, and under the billing mode it thinks it is using.
|
|
16
16
|
//
|
|
17
17
|
// It had no way to be asked. Its only callers were `bin/onboard.mjs` (the setup wizard, which asks it
|
|
18
18
|
// once, inside a flow) and `preflightEngineTurn` in `pipeline.mjs` (which asks it at the start of a run,
|
|
@@ -28,8 +28,9 @@
|
|
|
28
28
|
// ── WHAT A GREEN PROBE DOES NOT PROVE, from the module's own header ─────────────────────────────────
|
|
29
29
|
//
|
|
30
30
|
// The cheap tier only. Both tiers ride one credential, so AUTH is proven for all of them — a per-tier
|
|
31
|
-
// model entitlement or per-tier quota is not.
|
|
32
|
-
//
|
|
31
|
+
// model entitlement or per-tier quota is not. It calls one tool on its own server, so the tool path is
|
|
32
|
+
// proven and no stage's tool server is; it sets up no skills and no run dir. A probe that says `ok` says
|
|
33
|
+
// the door opens.
|
|
33
34
|
|
|
34
35
|
// FIRST IMPORT, AND IT HAS TO BE. This entry statically reaches driver.config.mjs, which captures
|
|
35
36
|
// env at module top — and every static import evaluates before this file's own body runs, so applying the
|
package/scripts/env-audit.mjs
CHANGED
|
@@ -557,7 +557,7 @@ export function auditEnv(root = ROOT) {
|
|
|
557
557
|
// 4 read RIGHT NOW through an idiom no `env`-prefixed regex can reach —
|
|
558
558
|
// envValue("CLEAROTRON_MAX_RETRIES") and
|
|
559
559
|
// envValue("CLEAROTRON_RATE_LIMIT_DEFAULT_BACKOFF_MS") (driver.config.mjs:486,
|
|
560
|
-
// :494), envOn("CLEAROTRON_DUMP_JSON") (gateway.mjs
|
|
560
|
+
// :494), envOn("CLEAROTRON_DUMP_JSON") (runStageLadder() in gateway.mjs), and
|
|
561
561
|
// `export const API_KEY_ENV = "USPTO_API_KEY"` (providers/uspto-local/src/sync.js:77).
|
|
562
562
|
// The name is a STRING LITERAL in every one; it just never sits beside an
|
|
563
563
|
// `env` token. USPTO_API_KEY is a credential.
|
|
@@ -69,8 +69,8 @@ const FROZEN_FILES = [
|
|
|
69
69
|
{ path: "_driver/senior-rights.json", why: "publish/index.mjs:787 seniorRights" },
|
|
70
70
|
{ path: "_driver/verdict.json", why: "publish/index.mjs:792 verdictInfo" },
|
|
71
71
|
{ path: "_driver/framework.json", why: "publish/index.mjs, the frozen band vocabulary the run was rated under" },
|
|
72
|
-
{ path: "_driver/register-plan.json", why: "publish/index.mjs:
|
|
73
|
-
{ path: "_driver/instructed-scope.json", why: "publish/index.mjs:
|
|
72
|
+
{ path: "_driver/register-plan.json", why: "publish/index.mjs:910 scopeBasis" },
|
|
73
|
+
{ path: "_driver/instructed-scope.json", why: "publish/index.mjs:911 searchedJurisdictions, the fallback for register-plan" },
|
|
74
74
|
{ path: "_driver/enforcer-signals.json", why: "`esPath` declared in index.mjs" },
|
|
75
75
|
{ path: "_driver/predelivery-lint.json", why: "publish/index.mjs:172 lintSink" },
|
|
76
76
|
{ path: "_driver/escalation-state.json", why: "publish/index.mjs:173 escSink" },
|
|
@@ -118,7 +118,7 @@ const KNOCKOUT_FILES = [
|
|
|
118
118
|
// knockout report render empty (publish/knockout.mjs:140-155). Named by stages-knockout.mjs:32,41.
|
|
119
119
|
{ path: "_driver/register-counts.json", why: "publish/knockout.mjs:140-155 counted figures + the Register column" },
|
|
120
120
|
{ path: "_driver/register-records.json", why: "stages-knockout.mjs:41 the terms behind the close-variation axis" },
|
|
121
|
-
{ path: "_driver/instructed-scope.json", why: "publish/index.mjs:
|
|
121
|
+
{ path: "_driver/instructed-scope.json", why: "publish/index.mjs:911 searchedJurisdictions, the fallback for register-plan" },
|
|
122
122
|
];
|
|
123
123
|
|
|
124
124
|
/** The allowlist for a template. One place, so a new template cannot half-exist. */
|
|
@@ -500,8 +500,8 @@ if (!snapshot) {
|
|
|
500
500
|
catch { return null; }
|
|
501
501
|
})();
|
|
502
502
|
const { groups: managerGroups, why } = readUserManagerGroups(uid);
|
|
503
|
-
const
|
|
504
|
-
(
|
|
503
|
+
const v = managerGroupsVerdict({ idGroups, managerGroups, user, uid, why });
|
|
504
|
+
record("user manager groups are current", v.state, v.message, v.blocked === true);
|
|
505
505
|
}
|
|
506
506
|
|
|
507
507
|
// 1b. — HOW THIS BOX DIFFERS FROM PRODUCTION on the flags that change output without saying so.
|
|
@@ -582,9 +582,9 @@ try {
|
|
|
582
582
|
try { caller = (await import("../driver/portal-service.mjs")).opsTokenPosture(OPS_TOKEN); }
|
|
583
583
|
catch { /* stays unreadable — the verdict reports what it could not compare */ }
|
|
584
584
|
if (bundledDemos) {
|
|
585
|
-
const
|
|
585
|
+
const v = rosterVerdict({ keys, onDisk, bundledDemos, caller,
|
|
586
586
|
expectDemos: process.env.CLEAROTRON_E2E_EXPECT_DEMO_ROSTER === "1" });
|
|
587
|
-
(
|
|
587
|
+
record("roster resolves", v.state, v.message, v.blocked === true);
|
|
588
588
|
}
|
|
589
589
|
// A COMPANY IN THE STORE THAT THIS KEY CANNOT START is its own finding, not a roster disagreement. A
|
|
590
590
|
// warning, not a failure: the portal re-takes its credential at the start of every call, so a Start for
|
|
@@ -639,7 +639,7 @@ try {
|
|
|
639
639
|
effectiveMode: (process.env.TRADEMARK_MCP_AUTH_MODE || "").trim().toLowerCase() === "token" ? "token" : "cf-access",
|
|
640
640
|
allowedHosts: (process.env.TRADEMARK_MCP_ALLOWED_HOSTS || "").split(",").map((h) => h.trim()).filter(Boolean),
|
|
641
641
|
});
|
|
642
|
-
record("the ops door's auth mode was chosen for it", posture.state, posture.message);
|
|
642
|
+
record("the ops door's auth mode was chosen for it", posture.state, posture.message, posture.blocked === true);
|
|
643
643
|
}
|
|
644
644
|
} catch (e) {
|
|
645
645
|
if (e?.message === "__door_unset__") skip("ops-MCP reachable", "this instance does not say where its ops-MCP is — NOT PROBED");
|
|
@@ -827,11 +827,12 @@ else {
|
|
|
827
827
|
// process that is in no unit, so this arm derives from the PROCESS TABLE and from a stamp the drainer
|
|
828
828
|
// writes about itself.
|
|
829
829
|
//
|
|
830
|
-
//
|
|
831
|
-
//
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
// report a build live until this arm has looked
|
|
830
|
+
// A COULD-NOT-LOOK HERE MOVES THE EXIT CODE, and that is deliberate. An ordinary skip does not, so
|
|
831
|
+
// recording an absent stamp as one would let the deploy report a build live having never established
|
|
832
|
+
// what the executing process holds, which is the exact state the incident's drainer was in. It is
|
|
833
|
+
// recorded through `blocked`, which exits 3 rather than the 1 a drift gives: the deploy still does not
|
|
834
|
+
// report a build live until this arm has looked, and the reader is told it could not look rather than
|
|
835
|
+
// that something drifted.
|
|
835
836
|
{
|
|
836
837
|
let workspaceRoot = null, resolveError = null;
|
|
837
838
|
try { workspaceRoot = config.workspaceRoot; }
|
|
@@ -937,7 +938,7 @@ else {
|
|
|
937
938
|
// which tree this deploy is. Reading it twice is how they would come to.
|
|
938
939
|
deployClone,
|
|
939
940
|
});
|
|
940
|
-
record("the updater that deploys this box is the current one", v.state, v.message);
|
|
941
|
+
record("the updater that deploys this box is the current one", v.state, v.message, v.blocked === true);
|
|
941
942
|
}
|
|
942
943
|
}
|
|
943
944
|
}
|
|
@@ -947,10 +948,10 @@ else {
|
|
|
947
948
|
// inline string equality over a two-word vocabulary, which is why a `Type=oneshot` doing its job read as
|
|
948
949
|
// a fault on the deploy's final gate. The decision now lives in driver/unit-state-verdict.mjs, where a
|
|
949
950
|
// test can reach it — same move made for the roster arm, for the same reason.
|
|
950
|
-
// `record`, not
|
|
951
|
+
// `record`, not a ({pass, fail, skip})[state] shorthand: `warn` is a real outcome here.
|
|
951
952
|
{
|
|
952
953
|
const v = unitsActiveVerdict({ units: clones, probe: unitProbe });
|
|
953
|
-
record("units active", v.state, v.message);
|
|
954
|
+
record("units active", v.state, v.message, v.blocked === true);
|
|
954
955
|
}
|
|
955
956
|
|
|
956
957
|
// 10. — THE UNIT A BOX RUNS versus the unit the deployed commit SHIPS. The deploy syncs code, not
|
|
@@ -1013,7 +1014,7 @@ else {
|
|
|
1013
1014
|
return { unit: fragName ?? c.unit, live, tracked, dropIns };
|
|
1014
1015
|
});
|
|
1015
1016
|
const v = unitFileDriftVerdict({ units: rows, probe: unitProbe });
|
|
1016
|
-
record("units match the deployed commit", v.state, v.message);
|
|
1017
|
+
record("units match the deployed commit", v.state, v.message, v.blocked === true);
|
|
1017
1018
|
}
|
|
1018
1019
|
|
|
1019
1020
|
// 10a. — IS EVERY UNIT THIS BOX RUNS ACCOUNTED FOR AT ALL?
|
|
@@ -1082,7 +1083,7 @@ else {
|
|
|
1082
1083
|
const box = deploymentBox(); // — the shared rule, so /portal/health cannot disagree with this
|
|
1083
1084
|
const v = unitInventoryVerdict({ live: liveUnits, files: walk.files, collisions: walk.collisions,
|
|
1084
1085
|
filesError: walk.error, box, probe, boxNames: DEPLOYMENT_BOXES });
|
|
1085
|
-
record("every live unit is declared", v.state, v.message);
|
|
1086
|
+
record("every live unit is declared", v.state, v.message, v.blocked === true);
|
|
1086
1087
|
|
|
1087
1088
|
// — AND WHETHER ANYTHING STILL STARTS THE TIMER-DRIVEN ONES. Reported separately from the line above
|
|
1088
1089
|
// because it answers a different question: that one says a declared unit exists and is not adrift,
|
|
@@ -1090,7 +1091,7 @@ else {
|
|
|
1090
1091
|
// fails the second, and reads `inactive` for both — which is why one line could not carry both.
|
|
1091
1092
|
const t = declaredTimers();
|
|
1092
1093
|
const tv = timerVerdict(t.rows, { probeFailed: t.probeFailed });
|
|
1093
|
-
record("every declared timer is still armed", tv.state, tv.message);
|
|
1094
|
+
record("every declared timer is still armed", tv.state, tv.message, tv.blocked === true);
|
|
1094
1095
|
}
|
|
1095
1096
|
|
|
1096
1097
|
// ── — EVERY QUEUE THIS DEPLOYMENT WOULD DRAIN IS WATCHED BY SOMETHING ──────────────────────────
|
|
@@ -1116,7 +1117,7 @@ else {
|
|
|
1116
1117
|
// job's life, and two readers of one unit file is how the deploy tick and a door come to different
|
|
1117
1118
|
// conclusions about the same box. The unit path now has one home.
|
|
1118
1119
|
const v = probeQueueWatch({ queueDirs, resolveError });
|
|
1119
|
-
record("every queue this deployment would drain is watched", v.state, v.message);
|
|
1120
|
+
record("every queue this deployment would drain is watched", v.state, v.message, v.blocked === true);
|
|
1120
1121
|
}
|
|
1121
1122
|
|
|
1122
1123
|
// ── report ───────────────────────────────────────────────────────────────────────────────────────────
|
|
@@ -1129,8 +1130,14 @@ const couldNotLook = results.filter((r) => r.blocked);
|
|
|
1129
1130
|
// — extracted for the reason roster-verdict and unit-state-verdict were: this file is a program, and a
|
|
1130
1131
|
// decision that can only be reached by running it is a decision nobody can drive.
|
|
1131
1132
|
|
|
1133
|
+
// ONE DECISION, READ BY BOTH SURFACES. `--json` used to print `ok: failed.length === 0`, so a run that
|
|
1134
|
+
// could not look at a surface and found nothing else wrong told a machine reader `ok: true` while the
|
|
1135
|
+
// terminal said COULD NOT LOOK and the process exited 3. The human surface was fixed and the one a script
|
|
1136
|
+
// believes silently was not. `ok` is now the exit code's own answer, and `exit` carries which of the three.
|
|
1137
|
+
const code = exitFor({ failed: failed.length, couldNotLook: couldNotLook.length });
|
|
1138
|
+
|
|
1132
1139
|
if (asJson) {
|
|
1133
|
-
console.log(JSON.stringify({ ok:
|
|
1140
|
+
console.log(JSON.stringify({ ok: code === 0, exit: code, poolRoot: POOL_ROOT, register: wiredRegister, results }, null, 2));
|
|
1134
1141
|
} else {
|
|
1135
1142
|
const mark = { pass: " ok ", fail: " FAIL ", warn: " warn ", skip: " skip " };
|
|
1136
1143
|
console.log(`\n== live surface check — ${POOL_ROOT} ==\n`);
|
|
@@ -1148,4 +1155,4 @@ if (asJson) {
|
|
|
1148
1155
|
}
|
|
1149
1156
|
}
|
|
1150
1157
|
|
|
1151
|
-
process.exit(
|
|
1158
|
+
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
|