clearotron 0.3.2-beta.7 → 0.3.2-beta.9
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/.env.example +58 -26
- package/CONTRIBUTING.md +8 -8
- package/INSTALL.md +148 -81
- package/README.md +3 -3
- package/SECURITY.md +3 -3
- package/bin/brandowner.mjs +3 -3
- package/bin/framework-preflight.mjs +1 -1
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +151 -27
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +2 -1
- package/docs/INTAKE.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/architecture/03-run-lifecycle.md +6 -6
- package/docs/architecture/04-configuration-reference.md +32 -14
- package/docs/architecture/05-config-governance.md +23 -8
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/08-development-guide.md +6 -6
- package/docs/configuration.md +5 -5
- package/docs/decisions/0003-credential-model.md +1 -1
- package/docs/writing-standard.md +4 -0
- package/driver/CHANGELOG.md +124 -0
- package/driver/README.md +3 -3
- package/driver/band-size.mjs +59 -0
- package/driver/binding-layers.mjs +1 -1
- package/driver/citation-census.json +3 -3
- package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/company-bundle.mjs +3 -3
- package/driver/compose-read.mjs +8 -14
- package/driver/config-inventory.mjs +112 -9
- package/driver/consumption-ledger.mjs +2 -2
- package/driver/contract-arm2-baseline.json +2 -5
- package/driver/contract-dictation-registry.mjs +19 -19
- package/driver/contract-e3-backlog.mjs +43 -43
- package/driver/contract-e3-baseline.json +14 -14
- package/driver/contract-vocabulary.mjs +68 -27
- package/driver/deliver-trigger.sh +16 -16
- package/driver/demo-container.mjs +3 -3
- package/driver/dev-portal.mjs +3 -3
- package/driver/disposition-call.mjs +1 -1
- package/driver/door-gates.mjs +41 -7
- package/driver/doubt-ledger.mjs +2 -2
- package/driver/drainer-identity.mjs +34 -8
- package/driver/driver.config.mjs +367 -104
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/README.md +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +3 -3
- package/driver/engine/mcp/gather-config.mjs +9 -9
- package/driver/engine/mcp/perplexity-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +18 -5
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/enqueue-schema.mjs +6 -2
- package/driver/findings-model.mjs +6 -3
- package/driver/flag-snapshot.mjs +34 -8
- package/driver/form-neighbourhood.mjs +54 -7
- package/driver/framework.mjs +4 -4
- package/driver/gateway.mjs +36 -24
- package/driver/jx-lanes.mjs +23 -4
- package/driver/jx-units.mjs +7 -4
- package/driver/jx.mjs +34 -4
- package/driver/knockout-review-record.mjs +56 -4
- package/driver/known-conflicts.mjs +1 -1
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +1 -1
- package/driver/ordinary-words.mjs +51 -0
- package/driver/outbox-backoff.mjs +31 -16
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +2 -2
- package/driver/phase0.mjs +3 -3
- package/driver/pipeline-knockout.mjs +5 -5
- package/driver/pipeline.mjs +396 -81
- package/driver/placement-form.mjs +77 -1
- package/driver/placement-model.mjs +1 -1
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +107 -6
- package/driver/portal-service.mjs +80 -14
- package/driver/portal-upstream.mjs +1 -1
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/preserve-merge.mjs +3 -3
- package/driver/product-rows.mjs +2 -2
- package/driver/products.mjs +1 -1
- package/driver/profiles/README.md +3 -3
- package/driver/profiles/demo-brand-owner.json +2 -2
- package/driver/profiles.mjs +55 -17
- package/driver/progress.mjs +18 -8
- package/driver/provider-usage.mjs +8 -8
- package/driver/publish/index.mjs +154 -8
- package/driver/publish/knockout.mjs +39 -5
- package/driver/publish/pool-admin.mjs +1 -1
- package/driver/publish/publish-inputs.mjs +18 -2
- package/driver/publish/render-knockout.mjs +184 -31
- package/driver/publish/render.mjs +323 -93
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/search-depth.mjs +133 -4
- package/driver/publish/templates/report.css +78 -4
- package/driver/publish/xlsx.mjs +20 -1
- package/driver/queue-order.mjs +2 -2
- package/driver/recording-agreement.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-count.mjs +50 -5
- package/driver/register-coverage.mjs +161 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-grant-vocabulary.mjs +1 -1
- package/driver/register-plan.mjs +189 -2
- package/driver/registry-fidelity.mjs +3 -3
- package/driver/repair-composers.mjs +1 -1
- package/driver/repair-contract.mjs +1 -1
- package/driver/replay-archive.mjs +6 -6
- package/driver/report-overview-record.mjs +2 -2
- package/driver/result-noun-fields.mjs +2 -2
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +5 -5
- package/driver/scope-facts.mjs +20 -5
- package/driver/scope-ledger.mjs +5 -5
- package/driver/search-policy.mjs +22 -12
- package/driver/skills/README.md +15 -15
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/case-law-citation/SKILL.md +4 -4
- package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
- package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
- package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
- package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
- package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
- package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
- package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
- package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
- package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
- package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
- package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
- package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
- package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
- package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
- package/driver/skills/frame-diff/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +10 -7
- package/driver/skills/matter-frame/SKILL.md +3 -3
- package/driver/skills/narrative-refutation/SKILL.md +9 -9
- package/driver/skills/placement-inquiry/SKILL.md +5 -5
- package/driver/stage-context.mjs +1 -1
- package/driver/stages-knockout.mjs +4 -4
- package/driver/stages.mjs +65 -61
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +340 -136
- package/driver/surface-exit-verdict.mjs +58 -0
- package/driver/systemd/README.md +9 -6
- package/driver/systemd/clearotron-worker.service +1 -1
- package/driver/terminal-clamp.mjs +109 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/usage-ledger.mjs +1 -1
- package/driver/variant-manifest-model.mjs +4 -4
- package/driver/verify-knockout.mjs +27 -0
- package/driver/verify.mjs +94 -6
- package/driver/whatif-queue.mjs +1 -1
- package/driver/wordlists/en.txt +63906 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/README.md +1 -1
- package/mcp-server/lib/options.mjs +8 -7
- package/mcp-server/lib/plan.mjs +18 -2
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/lib/usage.mjs +3 -3
- package/mcp-server/lib/whatif.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +18 -1
- package/package.json +12 -11
- package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/README.md +1 -1
- package/providers/_shared/enumerate.mjs +6 -6
- package/providers/_shared/execute-plan.mjs +3 -3
- package/providers/_shared/ledger.mjs +119 -5
- package/providers/_shared/provider-text.mjs +2 -2
- package/providers/_shared/screen.mjs +2 -2
- package/providers/_shared/script-form.mjs +3 -3
- package/providers/_shared/territory-codes.mjs +23 -3
- package/providers/clarivate/README.md +1 -1
- package/providers/clarivate/src/capabilities.js +12 -12
- package/providers/clarivate/src/core.js +37 -43
- package/providers/corsearch/README.md +1 -1
- package/providers/corsearch/src/capabilities.js +5 -5
- package/providers/corsearch/src/core.js +3 -3
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/src/core.js +1 -1
- package/providers/signa/README.md +1 -1
- package/providers/signa/src/capabilities.js +42 -49
- package/providers/signa/src/core.js +106 -29
- package/providers/uspto-local/README.md +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +1 -0
- package/scripts/ask-ai-render-check.mjs +127 -1
- package/scripts/authority-boundary-probe.mjs +8 -6
- package/scripts/backfill-started-at.mjs +2 -2
- package/scripts/census-merge-driver.mjs +33 -2
- package/scripts/citation-anchor-report.mjs +181 -0
- package/scripts/dead-names.mjs +1 -1
- package/scripts/deprecate-below.mjs +66 -8
- package/scripts/drain-preflight.mjs +1 -1
- package/scripts/e2e.mjs +174 -0
- package/scripts/env-audit.mjs +39 -6
- package/scripts/env-classify.mjs +20 -2
- package/scripts/freeze-example-run.mjs +61 -18
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/live-surface-check.mjs +124 -41
- package/scripts/markdown-link-check.mjs +1 -1
- package/scripts/merge-shape-check.mjs +242 -0
- package/scripts/mint-names-in-force.mjs +5 -5
- package/scripts/mint-offered-territories.mjs +72 -0
- package/scripts/mint-public-residue.mjs +2 -2
- package/scripts/mint-reference-strip-backlog.mjs +2 -2
- package/scripts/mint-suite-census.mjs +75 -2
- package/scripts/mint-writing-standard-backlog.mjs +2 -2
- package/scripts/purge-runs.mjs +7 -7
- package/scripts/reconcile-runs.mjs +2 -2
- package/scripts/release-approve-parked.mjs +20 -2
- package/scripts/release-await-cut.mjs +120 -1
- package/scripts/release-note-required.mjs +76 -8
- package/scripts/report-header-render-check.mjs +164 -0
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/brand.mjs +27 -0
- package/shared/connect-clients.mjs +39 -11
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/identifier-scan.mjs +65 -9
- package/shared/identifier-sentinels.mjs +22 -0
- package/shared/names-in-force.mjs +4 -2
- package/shared/offered-territories.json +738 -0
- package/shared/pre-rename-spellings.mjs +53 -0
- package/shared/reference-guard-classes.mjs +40 -2
- package/shared/stdio-connect.mjs +39 -4
- package/shared/tree-commit.mjs +48 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
|
@@ -103,7 +103,9 @@ import { readDrainerStamp, drainerVerdict, defaultPpidOf } from "../driver/drain
|
|
|
103
103
|
import { readUpdaterStamp, updaterVerdict, resolveUpdaterStampPath, updaterAbsentHere, UPDATER_STAMP_BASENAME } from "../driver/updater-identity.mjs"; // — the mechanism that PLACES commits
|
|
104
104
|
import { claimerIsAlive } from "../driver/claim-liveness.mjs"; // the shared liveness test, same polarity as the queue's
|
|
105
105
|
import { processTable } from "../shared/process-table.mjs"; // — /proc is not the only box
|
|
106
|
-
import { envFrom } from "../shared/env-aliases.mjs";
|
|
106
|
+
import { envFrom } from "../shared/env-aliases.mjs";
|
|
107
|
+
import { gitTry, treeOf } from "../shared/tree-commit.mjs"; // — a packaged install has no git, and says its commit in build-info.json
|
|
108
|
+
import { exitFor } from "../driver/surface-exit-verdict.mjs"; // — a 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
|
|
107
109
|
|
|
108
110
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
109
111
|
const asJson = process.argv.includes("--json");
|
|
@@ -128,12 +130,44 @@ if (!POOL_ROOT) {
|
|
|
128
130
|
// fell through to a hardcoded default while the pool root correctly pointed at test. A cross-instance
|
|
129
131
|
// probe that reports "ok" is worse than no probe at all.
|
|
130
132
|
const at = (host, port) => `http://${host || "127.0.0.1"}:${port}`;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
|
|
134
|
+
// AND THE THREE DOORS BELOW USED TO CONTRADICT THAT PARAGRAPH. Each fell back to a literal — 18792,
|
|
135
|
+
// 18802, 18811 — and all three of those are PRODUCTION's ports. So the note above described the defect
|
|
136
|
+
// rather than the behaviour: an instance that does not name its own ports was checked against
|
|
137
|
+
// production's doors and told the result was its own. The reports directory two blocks up had already
|
|
138
|
+
// been given the cure, for the identical reason, and the doors were left behind.
|
|
139
|
+
//
|
|
140
|
+
// The failure is worst where it looks best. On a box with nothing on those ports the arms fail with
|
|
141
|
+
// ECONNREFUSED, which is loud and gets read. On a box that HAS something listening there — this one
|
|
142
|
+
// runs several instances — the probe succeeds and the check reports another instance's health as this
|
|
143
|
+
// instance's, in a PASS. A cross-instance probe that reports "ok" is worse than no probe at all.
|
|
144
|
+
//
|
|
145
|
+
// So: no literal, and unset refuses by name rather than guessing. Every missing variable is named in
|
|
146
|
+
// one refusal, because a reader fixing these one exit code at a time runs the check four times.
|
|
147
|
+
const doorUrl = (explicit, portName, hostName) => {
|
|
148
|
+
const direct = (process.env[explicit] ?? "").trim();
|
|
149
|
+
if (direct) return { url: direct, missing: null };
|
|
150
|
+
const port = (process.env[portName] ?? "").trim();
|
|
151
|
+
if (port) return { url: at(process.env[hostName], port), missing: null };
|
|
152
|
+
return { url: null, missing: `${explicit} or ${portName}` };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const DOORS = {
|
|
156
|
+
"ops-MCP": doorUrl("TRADEMARK_MCP_URL", "TRADEMARK_MCP_HTTP_PORT", "TRADEMARK_MCP_HTTP_HOST"),
|
|
157
|
+
portal: doorUrl("PORTAL_URL", "PORTAL_SERVICE_PORT", "PORTAL_SERVICE_HOST"),
|
|
158
|
+
"client door": doorUrl("CLIENT_MCP_URL", "CLIENT_MCP_HTTP_PORT", "CLIENT_MCP_HTTP_HOST"),
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// An unconfigured door is NOT PROBED and says so by name. It is deliberately NOT a refusal: this check
|
|
162
|
+
// is driven against boxes that legitimately do not run every door, and an arrival check that aborts
|
|
163
|
+
// because one door is unnamed reports nothing about the seven surfaces it could have read. The
|
|
164
|
+
// `trigger lane reachable` arm below has answered exactly this way since it was written — an unset
|
|
165
|
+
// origin is skipped, never passed and never failed — and this is that rule applied to the other three.
|
|
166
|
+
const DOORS_UNSET = Object.entries(DOORS).filter(([, d]) => d.missing);
|
|
167
|
+
|
|
168
|
+
const MCP_URL = DOORS["ops-MCP"].url;
|
|
169
|
+
const PORTAL_URL = DOORS.portal.url;
|
|
170
|
+
const CLIENT_MCP_URL = DOORS["client door"].url;
|
|
137
171
|
|
|
138
172
|
// The bundled demo roster, as `list_profiles` REPORTS it. A door that resolves exactly this set is a
|
|
139
173
|
// door with no CLEAROTRON_CUSTOMERS_DIR — #83. Compared as a set, not a count: a deployment may legitimately
|
|
@@ -158,11 +192,37 @@ const OPS_TOKEN = (() => {
|
|
|
158
192
|
})();
|
|
159
193
|
|
|
160
194
|
const results = [];
|
|
161
|
-
const record = (name, state, detail) => { results.push({ name, state, detail }); return state === "pass"; };
|
|
195
|
+
const record = (name, state, detail, blocked = false) => { results.push({ name, state, detail, blocked }); return state === "pass"; };
|
|
162
196
|
const pass = (n, d) => record(n, "pass", d);
|
|
163
197
|
const fail = (n, d) => record(n, "fail", d);
|
|
164
198
|
const skip = (n, d) => record(n, "skip", d); // could not be reached — never counted as a pass
|
|
165
199
|
|
|
200
|
+
/**
|
|
201
|
+
* COULD NOT LOOK — a skip, and one that moves the exit code.
|
|
202
|
+
*
|
|
203
|
+
* Two arms used to call `fail` while their own message said "This is a failure to look, never a pass".
|
|
204
|
+
* The text was honest and the verdict was not: FAIL is what a genuine drift also produces, so a reader
|
|
205
|
+
* could not tell "this box has drifted" from "I was unable to look" without reading to the end of the
|
|
206
|
+
* message. A drift is fixed by redeploying; a could-not-look is fixed by pointing the check at something
|
|
207
|
+
* it can read. Until somebody does, nothing is known either way.
|
|
208
|
+
*
|
|
209
|
+
* IT IS NOT THE SAME AS AN ORDINARY SKIP, which is why this exists rather than reusing `skip`. Several
|
|
210
|
+
* surfaces are deliberately not probed — the client door answers behind an access proxy, and a door this
|
|
211
|
+
* instance does not name has no address to dial. Those are by design and are the resting state of a
|
|
212
|
+
* healthy box. If every skip moved the exit code, the new code would fire on every good run and be
|
|
213
|
+
* ignored inside a week, which is the failure this whole issue is about repeated one level up.
|
|
214
|
+
*/
|
|
215
|
+
const blocked = (n, d) => record(n, "skip", d, true);
|
|
216
|
+
|
|
217
|
+
// Named here rather than where the URLs are resolved, because `skip` does not exist yet up there. Each
|
|
218
|
+
// unconfigured door is on the report as its own line, so a reader sees WHICH surface went unread instead
|
|
219
|
+
// of inferring it from arms that are quietly missing.
|
|
220
|
+
for (const [name, d] of DOORS_UNSET) {
|
|
221
|
+
skip(`${name} configured`, `neither ${d.missing} is set, so this instance does not say where its ${name} is `
|
|
222
|
+
+ "— NOT PROBED. It used to fall through to production's port and report that door's answer as this "
|
|
223
|
+
+ "instance's own.");
|
|
224
|
+
}
|
|
225
|
+
|
|
166
226
|
// ── helpers ──────────────────────────────────────────────────────────────────────────────────────────
|
|
167
227
|
|
|
168
228
|
function getJson(url, timeoutMs = 5000) {
|
|
@@ -196,13 +256,8 @@ function tcpAlive(url, timeoutMs = 3000) {
|
|
|
196
256
|
// just written explaining itself, leaving `null` to mean "no clone here" and "could not ask" equally.
|
|
197
257
|
//
|
|
198
258
|
// `gitTry` keeps the reason; `git` stays exactly as it was for the callers that only want the value.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return { ok: true, out: execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(), err: null };
|
|
202
|
-
} catch (e) {
|
|
203
|
-
return { ok: false, out: null, err: String(e?.stderr || e?.message || e).replace(/\s+/g, " ").trim().slice(0, 120) };
|
|
204
|
-
}
|
|
205
|
-
};
|
|
259
|
+
// It now lives in shared/tree-commit.mjs beside `treeOf`, which asks it first and falls back to a
|
|
260
|
+
// packaged install's build-info.json.
|
|
206
261
|
const git = (repo, ...args) => gitTry(repo, ...args).out;
|
|
207
262
|
|
|
208
263
|
// — `systemctl --user` NEEDS A USER BUS, and a caller without one gets an error, not an empty list.
|
|
@@ -260,7 +315,7 @@ function declaredTimers() {
|
|
|
260
315
|
function serviceClones() {
|
|
261
316
|
// — the list is DECLARED, not written here. It used to be eight names inline, which put a unit
|
|
262
317
|
// inside the drift guarantee or outside it by omission: `client-access` was live on production and in
|
|
263
|
-
// no list at all, and `
|
|
318
|
+
// no list at all, and `clearance-outbox` was tracked, live on production, and equally invisible. Both are
|
|
264
319
|
// in the inventory now, and so is the reason each untracked unit is untracked.
|
|
265
320
|
const units = [...CHECKED_UNITS];
|
|
266
321
|
const env = userBusEnv();
|
|
@@ -307,9 +362,9 @@ function serviceClones() {
|
|
|
307
362
|
const parsed = unitWorkingDirectory(wd);
|
|
308
363
|
if (!parsed.path) declaredWhy = parsed.why;
|
|
309
364
|
else {
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
-
else declaredWhy =
|
|
365
|
+
const t = treeOf(parsed.path);
|
|
366
|
+
if (t.root) declaredTree = t.root;
|
|
367
|
+
else declaredWhy = t.why;
|
|
313
368
|
}
|
|
314
369
|
}
|
|
315
370
|
|
|
@@ -338,9 +393,9 @@ function serviceClones() {
|
|
|
338
393
|
const { tree, why } = treeOfRunning(cmdline, rel);
|
|
339
394
|
if (!tree) runningWhy = why;
|
|
340
395
|
else {
|
|
341
|
-
const
|
|
342
|
-
if (
|
|
343
|
-
else runningWhy = `
|
|
396
|
+
const t = treeOf(tree);
|
|
397
|
+
if (t.root) runningTree = t.root;
|
|
398
|
+
else runningWhy = `pid ${pid} is running from ${tree}, and ${t.why}`;
|
|
344
399
|
}
|
|
345
400
|
}
|
|
346
401
|
}
|
|
@@ -357,11 +412,11 @@ function serviceClones() {
|
|
|
357
412
|
unreadable: idle ? null : chosen.why });
|
|
358
413
|
continue;
|
|
359
414
|
}
|
|
360
|
-
const head =
|
|
415
|
+
const head = treeOf(chosen.clone);
|
|
361
416
|
out.push({ unit: u, active, load, type, since, clone: chosen.clone, source: chosen.source,
|
|
362
417
|
disagreement: chosen.disagreement,
|
|
363
|
-
head: head.
|
|
364
|
-
unreadable: head.
|
|
418
|
+
head: head.head,
|
|
419
|
+
unreadable: head.head ? null : head.why });
|
|
365
420
|
}
|
|
366
421
|
const probe = reached > 0
|
|
367
422
|
? { ok: true, why: null }
|
|
@@ -487,6 +542,7 @@ else skip("register wired", "the snapshot carries no register block");
|
|
|
487
542
|
// it from what the door itself resolved is what keeps a customer key out of this source file.
|
|
488
543
|
let probeProfileKey = null;
|
|
489
544
|
try {
|
|
545
|
+
if (!MCP_URL) throw new Error("__door_unset__");
|
|
490
546
|
const listed = await mcpToolCall({ url: MCP_URL, token: OPS_TOKEN, tool: "list_profiles", args: {}, timeoutMs: 15000 });
|
|
491
547
|
const keys = (Array.isArray(listed?.clients) ? listed.clients : []).map((p) => p.key ?? p.profileKey).filter(Boolean).sort();
|
|
492
548
|
probeProfileKey = keys[0] ?? null;
|
|
@@ -547,7 +603,8 @@ try {
|
|
|
547
603
|
}
|
|
548
604
|
}
|
|
549
605
|
} catch (e) {
|
|
550
|
-
|
|
606
|
+
if (e?.message === "__door_unset__") skip("roster resolves", "this instance does not say where its ops-MCP is, so the roster was NOT PROBED");
|
|
607
|
+
else fail("roster resolves", `list_profiles failed: ${e.message}`);
|
|
551
608
|
}
|
|
552
609
|
|
|
553
610
|
// 3. THE LOAD-BEARING CHECK — every door's availability answer vs the engine's own, recomputed here
|
|
@@ -555,6 +612,7 @@ try {
|
|
|
555
612
|
// from its own process environment instead of from this file.
|
|
556
613
|
let mcpOptions = null;
|
|
557
614
|
try {
|
|
615
|
+
if (!MCP_URL) throw new Error("__door_unset__");
|
|
558
616
|
mcpOptions = await mcpToolCall({ url: MCP_URL, token: OPS_TOKEN, tool: "describe_options", args: {}, timeoutMs: 20000 });
|
|
559
617
|
pass("ops-MCP reachable", `${MCP_URL} answered describe_options`);
|
|
560
618
|
// ── — REACHABLE IS NOT THE SAME AS DELIBERATE ─────────────────────────────
|
|
@@ -584,7 +642,8 @@ try {
|
|
|
584
642
|
record("the ops door's auth mode was chosen for it", posture.state, posture.message);
|
|
585
643
|
}
|
|
586
644
|
} catch (e) {
|
|
587
|
-
|
|
645
|
+
if (e?.message === "__door_unset__") skip("ops-MCP reachable", "this instance does not say where its ops-MCP is — NOT PROBED");
|
|
646
|
+
else fail("ops-MCP reachable", `${MCP_URL}: ${e.message}`);
|
|
588
647
|
}
|
|
589
648
|
|
|
590
649
|
// ── 3b. THE TRIGGER LANE, AS ITS OWN SURFACE ─────────────────────────────────────────────────────────
|
|
@@ -702,15 +761,17 @@ if (mcpOptions) {
|
|
|
702
761
|
|
|
703
762
|
// 6. The portal. Only /portal/health is reachable without a Cloudflare Access JWT — everything else is
|
|
704
763
|
// NOT PROBED, and says so rather than passing by omission.
|
|
705
|
-
const health = await getJson(`${PORTAL_URL}/portal/health`);
|
|
706
|
-
if (
|
|
764
|
+
const health = PORTAL_URL ? await getJson(`${PORTAL_URL}/portal/health`) : null;
|
|
765
|
+
if (!PORTAL_URL) skip("portal health", "this instance does not say where its portal is — NOT PROBED");
|
|
766
|
+
else if (health.status !== 200 || !health.json) fail("portal health", `${PORTAL_URL}/portal/health → ${health.status || health.error}`);
|
|
707
767
|
else if (health.json.ui !== "built") fail("portal health", `ui="${health.json.ui}" — the portal is serving a stale or missing bundle (never add --omit=dev to the deploy)`);
|
|
708
768
|
else pass("portal health", `ok=${health.json.ok} ui="${health.json.ui}"`);
|
|
709
769
|
skip("portal gates agree", "every portal route but /portal/health is behind Cloudflare Access — not callable from a script, so NOT probed");
|
|
710
770
|
|
|
711
771
|
// 7. client-MCP liveness only, for the same reason. Its API-key door's secret is a crown jewel and is
|
|
712
772
|
// never read, let alone logged, by this script.
|
|
713
|
-
skip("client-MCP",
|
|
773
|
+
skip("client-MCP", !CLIENT_MCP_URL ? "this instance does not say where its client door is — NOT PROBED"
|
|
774
|
+
: (await tcpAlive(CLIENT_MCP_URL)) ? "listening; behind CF Access so its answers are NOT probed" : `not listening on ${CLIENT_MCP_URL}`);
|
|
714
775
|
|
|
715
776
|
// 8. Every service on the commit you think it is. This is the straddle check.
|
|
716
777
|
const { clones, probe: unitProbe } = serviceClones();
|
|
@@ -733,7 +794,10 @@ const heads = [...new Set(running.map((c) => c.head))];
|
|
|
733
794
|
// demanded commit agreement from services this deploy must not move, and reddened on every prod deploy
|
|
734
795
|
// for units behaving correctly. The discriminator is the CLONE, which `serviceClones()` has read all
|
|
735
796
|
// along; see `serviceCommitVerdict` for why it is not `tracked`.
|
|
736
|
-
|
|
797
|
+
// A PACKAGED INSTALL NAMES ITS OWN COMMIT IN build-info.json, and this script runs from inside one on
|
|
798
|
+
// production and pre-prod. Asking git alone reported both as unreadable on every deploy.
|
|
799
|
+
const deployTree = treeOf(HERE);
|
|
800
|
+
const deployClone = deployTree.root;
|
|
737
801
|
if (!unitProbe.ok)
|
|
738
802
|
skip("services share one commit", `could not enumerate systemd --user units, so the commit was NOT compared — ${unitProbe.why}. This is a failure to look, not a finding about the deployment`);
|
|
739
803
|
// — THE SAME DEFECT ONE BRANCH UP, found reviewing this change rather than in the issue. This
|
|
@@ -744,7 +808,7 @@ if (!unitProbe.ok)
|
|
|
744
808
|
// unreadable disclosure, so the decision belongs there and not in a pre-filter.
|
|
745
809
|
else {
|
|
746
810
|
const ownedClone = running.find((c) => String(c.clone ?? "").replace(/\/+$/, "") === String(deployClone ?? "").replace(/\/+$/, ""));
|
|
747
|
-
const ahead = ownedClone ? (git(ownedClone.clone, "status", "-sb")?.includes("ahead") ?? false) : false;
|
|
811
|
+
const ahead = ownedClone && deployTree.source === "git" ? (git(ownedClone.clone, "status", "-sb")?.includes("ahead") ?? false) : false;
|
|
748
812
|
// — ALL the clones, not just the ones with a head. The verdict scopes to owned units itself;
|
|
749
813
|
// passing it the pre-filtered list is what made the units it could not read invisible to the sentence
|
|
750
814
|
// it prints.
|
|
@@ -774,9 +838,10 @@ else {
|
|
|
774
838
|
catch (e) { resolveError = String(e?.message ?? e).slice(0, 160); }
|
|
775
839
|
|
|
776
840
|
if (!workspaceRoot) {
|
|
777
|
-
|
|
841
|
+
blocked("the process that executes runs is on the deployed commit",
|
|
778
842
|
`the workspace root could not be resolved (${resolveError ?? "no value"}), so the drainer's stamp could not be found. `
|
|
779
|
-
+ "This is a failure to look, never a pass
|
|
843
|
+
+ "This is a failure to look, never a pass — and it is not a drift either: nothing is known about "
|
|
844
|
+
+ "this surface until the check can be pointed at something it can read.");
|
|
780
845
|
} else {
|
|
781
846
|
// `null` from processTable is UNREADABLE, not empty — the verdict distinguishes them, because
|
|
782
847
|
// "no drainer is running" and "I could not see the process table" are the same empty array and only
|
|
@@ -785,7 +850,7 @@ else {
|
|
|
785
850
|
try { processes = processTable(); } catch { processes = null; }
|
|
786
851
|
const v = drainerVerdict({
|
|
787
852
|
stamp: readDrainerStamp(workspaceRoot),
|
|
788
|
-
headCommit:
|
|
853
|
+
headCommit: deployTree.head,
|
|
789
854
|
isAlive: claimerIsAlive,
|
|
790
855
|
processes,
|
|
791
856
|
// second criterion — an orphaned drainer in a closed login session is a
|
|
@@ -798,7 +863,10 @@ else {
|
|
|
798
863
|
// to an unprobed posture — a failure to look, never a pass.
|
|
799
864
|
posture: { worker: probeWorker(), timer: probeTimer() },
|
|
800
865
|
});
|
|
801
|
-
|
|
866
|
+
// — AND THE VERDICT'S OWN could-not-look MARKER, carried through. The verdict distinguishes a
|
|
867
|
+
// process table it could not read from one that answered; dropping that here would put the
|
|
868
|
+
// distinction back where it was, one layer down.
|
|
869
|
+
record("the process that executes runs is on the deployed commit", v.state, v.message, v.blocked === true);
|
|
802
870
|
}
|
|
803
871
|
}
|
|
804
872
|
|
|
@@ -857,9 +925,10 @@ else {
|
|
|
857
925
|
}
|
|
858
926
|
|
|
859
927
|
if (!resolveUpdaterStampPath(deployDir)) {
|
|
860
|
-
|
|
928
|
+
blocked("the updater that deploys this box is the current one",
|
|
861
929
|
`where the updater stamps itself could not be resolved (${resolveWhy ?? "no value"}), so `
|
|
862
|
-
+ `${UPDATER_STAMP_BASENAME} was not looked for. This is a failure to look, never a pass
|
|
930
|
+
+ `${UPDATER_STAMP_BASENAME} was not looked for. This is a failure to look, never a pass — and not `
|
|
931
|
+
+ "a drift: redeploying would change nothing, because nothing was compared.");
|
|
863
932
|
} else {
|
|
864
933
|
const v = updaterVerdict({
|
|
865
934
|
stamp: readUpdaterStamp(deployDir),
|
|
@@ -951,7 +1020,7 @@ else {
|
|
|
951
1020
|
//
|
|
952
1021
|
// The check above compares a unit against its tracked file. It can only do that for units it was told
|
|
953
1022
|
// to look at, and the list was eight names written inline — so `client-access` ran on production, in no
|
|
954
|
-
// list, compared against nothing, and reported by nothing. `
|
|
1023
|
+
// list, compared against nothing, and reported by nothing. `clearance-outbox` was the mirror: tracked and
|
|
955
1024
|
// live on production, and equally absent from the list, so its drift was never checked either.
|
|
956
1025
|
//
|
|
957
1026
|
// This arm asks the question one level up. It is deliberately NOT the drift comparison: a unit can be
|
|
@@ -1054,6 +1123,11 @@ else {
|
|
|
1054
1123
|
|
|
1055
1124
|
const failed = results.filter((r) => r.state === "fail");
|
|
1056
1125
|
const skipped = results.filter((r) => r.state === "skip");
|
|
1126
|
+
const couldNotLook = results.filter((r) => r.blocked);
|
|
1127
|
+
|
|
1128
|
+
// The three answers this check can give, and which outranks which, are in driver/surface-exit-verdict.mjs
|
|
1129
|
+
// — extracted for the reason roster-verdict and unit-state-verdict were: this file is a program, and a
|
|
1130
|
+
// decision that can only be reached by running it is a decision nobody can drive.
|
|
1057
1131
|
|
|
1058
1132
|
if (asJson) {
|
|
1059
1133
|
console.log(JSON.stringify({ ok: failed.length === 0, poolRoot: POOL_ROOT, register: wiredRegister, results }, null, 2));
|
|
@@ -1061,8 +1135,17 @@ if (asJson) {
|
|
|
1061
1135
|
const mark = { pass: " ok ", fail: " FAIL ", warn: " warn ", skip: " skip " };
|
|
1062
1136
|
console.log(`\n== live surface check — ${POOL_ROOT} ==\n`);
|
|
1063
1137
|
for (const r of results) console.log(`[${mark[r.state]}] ${r.name}\n ${r.detail}`);
|
|
1064
|
-
|
|
1138
|
+
const headline = failed.length ? `FAIL — ${failed.length} disagreement(s)`
|
|
1139
|
+
: couldNotLook.length ? `COULD NOT LOOK — ${couldNotLook.length} surface(s) unreadable, and none of the rest disagreed`
|
|
1140
|
+
: "PASS";
|
|
1141
|
+
console.log(`\n${headline}`
|
|
1065
1142
|
+ `${skipped.length ? ` · ${skipped.length} surface(s) NOT probed (see above — not probed is not passed)` : ""}\n`);
|
|
1143
|
+
// Named, not left to the exit code alone: whoever reads this on a terminal never sees `$?`.
|
|
1144
|
+
if (couldNotLook.length) {
|
|
1145
|
+
console.log(" could not be read — nothing is known about these either way, and redeploying changes nothing:");
|
|
1146
|
+
for (const r of couldNotLook) console.log(` ${r.name}`);
|
|
1147
|
+
console.log("");
|
|
1148
|
+
}
|
|
1066
1149
|
}
|
|
1067
1150
|
|
|
1068
|
-
process.exit(failed.length
|
|
1151
|
+
process.exit(exitFor({ failed: failed.length, couldNotLook: couldNotLook.length }));
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// subtly wrong makes this loud in the one direction that trains people to override it.
|
|
32
32
|
// · Destinations containing `<` or `>`. The skills corpus writes prose templates with
|
|
33
33
|
// angle-bracket placeholders — `[<register> · <id>](<composed record URL>)` in
|
|
34
|
-
// driver/skills/
|
|
34
|
+
// driver/skills/clearance-search/delivery-contract.md is an instruction to a model, not a path.
|
|
35
35
|
// No real path contains those characters, so the exclusion costs nothing.
|
|
36
36
|
//
|
|
37
37
|
// ── THE ALLOWLIST IS SELF-INVALIDATING ───────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright 2026 Cordillera Sarl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
|
+
//
|
|
5
|
+
// WHAT A MERGE DID TO THE SHAPE OF THE TREE, which no other check here asks.
|
|
6
|
+
//
|
|
7
|
+
// Every gate in this directory reads CONTENT: is this citation resolvable, does this commit owe a release
|
|
8
|
+
// note, does the census match the tree, is this variable declared. A merge's damage is to the SHAPE — what
|
|
9
|
+
// it deleted, what it brought back, what there are now two of — and a tree can be damaged in all three ways
|
|
10
|
+
// while every content gate passes, because each of them asks only about files that are there.
|
|
11
|
+
//
|
|
12
|
+
// Measured on 2026-09-17, on a tree carrying 26 duplicated release notes: `citation-line-check`,
|
|
13
|
+
// `release-note-required`, `env-audit` and `mint-suite-census --check` all exited 0. The arm that caught it
|
|
14
|
+
// ran in CI, after the push.
|
|
15
|
+
//
|
|
16
|
+
// ── THE INCIDENT THIS IS BUILT FROM ──────────────────────────────────────────────────────────────────
|
|
17
|
+
//
|
|
18
|
+
// A branch was rebased locally while the remote still carried its original commit, so the published tip was
|
|
19
|
+
// merged back rather than force-pushed — right for the history and wrong for the tree. That tip's lineage
|
|
20
|
+
// predated a release, so the merge restored the release notes the cut had consumed. Each then existed in
|
|
21
|
+
// both `.changeset/` and `.changeset/pre/`, and the next cut would have republished text a reader had
|
|
22
|
+
// already been shown.
|
|
23
|
+
//
|
|
24
|
+
// The merge was checked at the time for the things a merge is usually checked for: no work lost, no change
|
|
25
|
+
// applied twice, every test name still present. Nobody asked what else it brought back.
|
|
26
|
+
//
|
|
27
|
+
// AND IT WEAKENED A TEST IN ANOTHER FILE. A guard being written that week failed to fail when its author
|
|
28
|
+
// planted a defect, because the duplication meant the code under test found what it needed at the first
|
|
29
|
+
// path and never reached the branch being driven. A shape defect does not only break its own guard; it
|
|
30
|
+
// silently changes what other tests reach. That is why this asks about the tree rather than about a file.
|
|
31
|
+
//
|
|
32
|
+
// ── WHAT IT REFUSES, AND THE ONE IT DELIBERATELY DOES NOT ────────────────────────────────────────────
|
|
33
|
+
//
|
|
34
|
+
// RESURRECTED — refused, and ONLY under the paths named in RESURRECTION_SCOPE. A file the merge added to
|
|
35
|
+
// the target that the target's own history had deleted.
|
|
36
|
+
//
|
|
37
|
+
// THE SCOPE IS THE WHOLE DESIGN, not a first cut somebody forgot to widen. Judged over the whole tree this
|
|
38
|
+
// check refuses `git revert`: reverting a commit that deleted a file re-adds it, and the target's history
|
|
39
|
+
// carries the deletion, which is this condition word for word. A revert is an honest act and one of the
|
|
40
|
+
// most valuable ones on this repository — so a guard that reds on it is a guard somebody switches off, and
|
|
41
|
+
// then it is not there for the case it was built for.
|
|
42
|
+
//
|
|
43
|
+
// So it runs where resurrection has no honest explanation. A release note consumed by a cut has been shown
|
|
44
|
+
// to readers already; nothing legitimate brings it back. That is a property of `.changeset/`, not of files
|
|
45
|
+
// in general, and the list says so rather than pretending to a generality it does not have.
|
|
46
|
+
//
|
|
47
|
+
// WIDENING IT NEEDS THE REVERT QUESTION ANSWERED FIRST. The durable discriminator is that a revert's diff
|
|
48
|
+
// is the inverse of the deleting commit's, not anything in its message, which can say whatever was typed.
|
|
49
|
+
// Until something computes that, more paths means more honest reds.
|
|
50
|
+
//
|
|
51
|
+
// TWO OF ONE NAME — refused. `.changeset/` and `.changeset/pre/` holding the same note. ONE pair, named
|
|
52
|
+
// here, not a framework for pairs: this is the only one anybody has been bitten by, and a general
|
|
53
|
+
// mechanism for a population of one is a mechanism whose other members are imaginary.
|
|
54
|
+
//
|
|
55
|
+
// REMOVED — reported, never refused. A merge that deletes from the branch it is merging into is worth a
|
|
56
|
+
// look and is often correct: the removal that FIXED the incident above was exactly this shape. A guard
|
|
57
|
+
// that reds on an honest deletion is a guard somebody switches off, so this one prints and exits 0.
|
|
58
|
+
|
|
59
|
+
import { execFileSync } from "node:child_process";
|
|
60
|
+
import { dirname, basename } from "node:path";
|
|
61
|
+
import { fileURLToPath } from "node:url";
|
|
62
|
+
|
|
63
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Run git and hand back stdout, or null when it fails.
|
|
67
|
+
*
|
|
68
|
+
* HOOKS OFF, ALWAYS. This runs git inside repositories the arms below create, and a `core.hooksPath` set
|
|
69
|
+
* globally reaches into every one of them — which is how a suite ends up depending on whoever ran it last
|
|
70
|
+
* having the right git config. Measured the hard way on this box, 2026-09-17: a global hooks path broke two
|
|
71
|
+
* arms because the commit-message hook treats a repository with no origin as public, and a throwaway
|
|
72
|
+
* repository has no origin either.
|
|
73
|
+
*/
|
|
74
|
+
export const gitIn = (repo) => (...args) => {
|
|
75
|
+
try {
|
|
76
|
+
return execFileSync("git", ["-c", "core.hooksPath=", "-C", repo, ...args],
|
|
77
|
+
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
78
|
+
} catch { return null; }
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** The lines of a `--name-only` listing, empties dropped. A null read is an empty list, never a throw. */
|
|
82
|
+
const lines = (out) => String(out ?? "").split("\n").map((s) => s.trim()).filter(Boolean);
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* THE PAIRS THAT MAY NOT HOLD THE SAME NAME. A note in both places has its text republished by the next
|
|
86
|
+
* cut — the reader is shown an announcement they were shown in the last release.
|
|
87
|
+
*
|
|
88
|
+
* Deliberately a list of one. See the header.
|
|
89
|
+
*/
|
|
90
|
+
export const UNIQUE_PAIRS = [
|
|
91
|
+
{ a: ".changeset", b: ".changeset/pre", why: "a note in both would have its text republished by the next cut" },
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* WHERE A FILE COMING BACK HAS NO HONEST EXPLANATION. See the header: judged over the whole tree, the
|
|
96
|
+
* resurrection question refuses `git revert`, which is why this is a list and not a wildcard.
|
|
97
|
+
*
|
|
98
|
+
* A release note the cut consumed has been shown to readers. There is no act that legitimately returns one,
|
|
99
|
+
* so here — and only here — a file arriving back through a merge is a finding rather than a question.
|
|
100
|
+
*/
|
|
101
|
+
export const RESURRECTION_SCOPE = [".changeset/"];
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Is this commit a merge, and what are its parents?
|
|
105
|
+
*
|
|
106
|
+
* The FIRST parent is the branch being merged INTO, which is the thing every question below is asked
|
|
107
|
+
* about: "what did this do to the target" only means something relative to what the target had.
|
|
108
|
+
*/
|
|
109
|
+
export function parentsOf(git, commit) {
|
|
110
|
+
const out = git("rev-list", "--parents", "-n", "1", commit);
|
|
111
|
+
if (!out) return { ok: false, parents: [], why: `could not read ${commit}` };
|
|
112
|
+
const ids = out.split(/\s+/).filter(Boolean).slice(1);
|
|
113
|
+
return { ok: true, parents: ids, why: null };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The files a merge added to its first parent, and the ones it removed.
|
|
118
|
+
*
|
|
119
|
+
* `--diff-filter` on the two-dot form: what the target gained and lost by taking this merge. NOT the
|
|
120
|
+
* three-dot form, which answers a question about the merge base and would report the branch's own additions
|
|
121
|
+
* as though the merge had made them.
|
|
122
|
+
*/
|
|
123
|
+
export function addedAndRemoved(git, firstParent, commit) {
|
|
124
|
+
return {
|
|
125
|
+
added: lines(git("diff", "--name-only", "--diff-filter=A", firstParent, commit)),
|
|
126
|
+
removed: lines(git("diff", "--name-only", "--diff-filter=D", firstParent, commit)),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Of the files this merge ADDED, the ones the target had previously DELETED — and the commit that deleted
|
|
132
|
+
* each, so the finding is judgeable without a second command.
|
|
133
|
+
*
|
|
134
|
+
* A file the target never had is an ordinary addition and is not reported: a branch adding files is what a
|
|
135
|
+
* branch is for. What has no innocent explanation is a file the target decided to remove arriving back
|
|
136
|
+
* through a merge, because the branch did not delete it — its lineage was older than the deletion.
|
|
137
|
+
*/
|
|
138
|
+
export function resurrectedBy(git, firstParent, added, scope = RESURRECTION_SCOPE) {
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const path of added) {
|
|
141
|
+
if (!scope.some((p) => path.startsWith(p))) continue;
|
|
142
|
+
const deletedIn = git("log", "--diff-filter=D", "--format=%h %s", "-1", firstParent, "--", path);
|
|
143
|
+
if (deletedIn) out.push({ path, deletedIn });
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Names present under BOTH halves of a pair. Compared by basename, which is what a cut reads. */
|
|
149
|
+
export function duplicatedAcross(git, commit, pairs = UNIQUE_PAIRS) {
|
|
150
|
+
const at = (dir) => {
|
|
151
|
+
const all = lines(git("ls-tree", "-r", "--name-only", commit, "--", `${dir}/`));
|
|
152
|
+
return all.filter((f) => f.endsWith(".md"));
|
|
153
|
+
};
|
|
154
|
+
const out = [];
|
|
155
|
+
for (const pair of pairs) {
|
|
156
|
+
const inB = new Set(at(pair.b).map((f) => basename(f)));
|
|
157
|
+
// The `a` half must EXCLUDE the `b` half, or every file under the nested directory counts as its own
|
|
158
|
+
// duplicate and the check reports the whole of `pre/` against itself.
|
|
159
|
+
const inA = at(pair.a).filter((f) => !f.startsWith(`${pair.b}/`)).map((f) => basename(f));
|
|
160
|
+
const both = [...new Set(inA.filter((n) => inB.has(n)))].sort();
|
|
161
|
+
if (both.length) out.push({ ...pair, names: both });
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The whole reading for one commit. Never throws: an unreadable commit is a stated could-not-look, because
|
|
168
|
+
* a shape check that dies on a bad argument and exits non-zero is indistinguishable from one that found
|
|
169
|
+
* something.
|
|
170
|
+
*/
|
|
171
|
+
export function mergeShape({ repo = HERE, commit = "HEAD", git = gitIn(repo) } = {}) {
|
|
172
|
+
const { ok, parents, why } = parentsOf(git, commit);
|
|
173
|
+
if (!ok) return { readable: false, why, isMerge: false, added: [], removed: [], resurrected: [], duplicated: [] };
|
|
174
|
+
if (parents.length < 2) {
|
|
175
|
+
return { readable: true, why: null, isMerge: false, added: [], removed: [], resurrected: [], duplicated: [] };
|
|
176
|
+
}
|
|
177
|
+
const { added, removed } = addedAndRemoved(git, parents[0], commit);
|
|
178
|
+
return {
|
|
179
|
+
readable: true, why: null, isMerge: true, firstParent: parents[0], added, removed,
|
|
180
|
+
resurrected: resurrectedBy(git, parents[0], added),
|
|
181
|
+
duplicated: duplicatedAcross(git, commit),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The exit code a reading earns. Resurrection and a duplicated name refuse; a removal is reported. */
|
|
186
|
+
export function verdictOf(shape) {
|
|
187
|
+
if (!shape.readable) return 2;
|
|
188
|
+
return shape.resurrected.length || shape.duplicated.length ? 1 : 0;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The repository and the commit a command line names. PURE, so an arm can hold it.
|
|
193
|
+
*
|
|
194
|
+
* The first version filtered out "the argument after --repo" as `i !== repoAt + 1`. With no --repo,
|
|
195
|
+
* `repoAt` is -1 and that filter drops position 0 — the commit the reader named — so the check examined
|
|
196
|
+
* HEAD instead, found no merge, and exited 0 saying nothing was examined: a clean answer about a commit
|
|
197
|
+
* nobody asked about.
|
|
198
|
+
*/
|
|
199
|
+
export function parseArgs(args, here = HERE) {
|
|
200
|
+
const repoAt = args.indexOf("--repo");
|
|
201
|
+
const repo = repoAt >= 0 ? args[repoAt + 1] : here;
|
|
202
|
+
const commit = args.filter((a, i) => !a.startsWith("--") && (repoAt < 0 || i !== repoAt + 1))[0] ?? "HEAD";
|
|
203
|
+
return { repo, commit };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
207
|
+
const { repo, commit } = parseArgs(process.argv.slice(2));
|
|
208
|
+
|
|
209
|
+
const shape = mergeShape({ repo, commit });
|
|
210
|
+
const code = verdictOf(shape);
|
|
211
|
+
|
|
212
|
+
if (!shape.readable) {
|
|
213
|
+
console.error(`merge-shape-check: ${shape.why} — nothing was examined, which is not the same as nothing being wrong.`);
|
|
214
|
+
process.exit(code);
|
|
215
|
+
}
|
|
216
|
+
if (!shape.isMerge) {
|
|
217
|
+
console.log(`merge-shape-check: ${commit} is not a merge, so there is no merge to judge. Nothing examined.`);
|
|
218
|
+
process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
console.log(`merge-shape-check: ${commit} against its first parent ${shape.firstParent.slice(0, 8)}`);
|
|
222
|
+
console.log(` ${shape.added.length} file(s) added to the target, ${shape.removed.length} removed from it`);
|
|
223
|
+
|
|
224
|
+
for (const r of shape.resurrected) {
|
|
225
|
+
console.error(`\n RESURRECTED ${r.path}`);
|
|
226
|
+
console.error(` the target deleted this in ${r.deletedIn}, and this merge brings it back. The branch did not`);
|
|
227
|
+
console.error(` re-add it — its lineage is older than the deletion, so the file rode in on the merge.`);
|
|
228
|
+
}
|
|
229
|
+
for (const d of shape.duplicated) {
|
|
230
|
+
console.error(`\n TWO OF ${d.names.length} NAME(S) ${d.a} and ${d.b}`);
|
|
231
|
+
console.error(` ${d.why}`);
|
|
232
|
+
for (const n of d.names) console.error(` ${n}`);
|
|
233
|
+
}
|
|
234
|
+
// Printed last and never fatal: this is the shape a correct fix also has.
|
|
235
|
+
if (shape.removed.length) {
|
|
236
|
+
console.log(`\n removed from the target by this merge — read them, they are often correct:`);
|
|
237
|
+
for (const p of shape.removed.slice(0, 40)) console.log(` ${p}`);
|
|
238
|
+
if (shape.removed.length > 40) console.log(` … and ${shape.removed.length - 40} more`);
|
|
239
|
+
}
|
|
240
|
+
if (code === 0) console.log("\n nothing resurrected and nothing duplicated.");
|
|
241
|
+
process.exit(code);
|
|
242
|
+
}
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
// ── WHY THIS IS DERIVED AND NOT WRITTEN BY HAND ──────────────────────────────────────────────────
|
|
8
8
|
//
|
|
9
9
|
// It is the replacement half of the retired-spelling check. The DETECTION half
|
|
10
|
-
// needs nothing from this file: no product code reads a `
|
|
10
|
+
// needs nothing from this file: no product code reads a `CLEARANCE_*` name any more, so a `CLEARANCE_*` that
|
|
11
11
|
// is set is dead whatever this list says. What the list decides is the second half of the sentence —
|
|
12
12
|
// which name to tell the operator to use instead — and that is where a hand list does damage. A hand
|
|
13
13
|
// list stops at whoever was looking, and sending an operator to a variable nothing reads replaces one
|
|
14
14
|
// silent failure with another.
|
|
15
15
|
//
|
|
16
|
-
// The rule is the rename's own: the sweep moved `
|
|
17
|
-
// untouched. So every `CLEAROTRON_<SUFFIX>` this build reads implies `
|
|
18
|
-
// spelling, and a `
|
|
16
|
+
// The rule is the rename's own: the sweep moved `CLEARANCE_<SUFFIX>` to `CLEAROTRON_<SUFFIX>`, suffix
|
|
17
|
+
// untouched. So every `CLEAROTRON_<SUFFIX>` this build reads implies `CLEARANCE_<SUFFIX>` was its old
|
|
18
|
+
// spelling, and a `CLEARANCE_*` name with no `CLEAROTRON_` partner is a setting that no longer exists at
|
|
19
19
|
// all rather than one that moved. Both are worth saying and they are different sentences.
|
|
20
20
|
//
|
|
21
21
|
// ── THE ORACLE THIS DERIVATION IS CHECKED AGAINST ────────────────────────────────────────────────
|
|
@@ -110,7 +110,7 @@ function render(names) {
|
|
|
110
110
|
// names-in-force.mjs — GENERATED by scripts/mint-names-in-force.mjs. Do not edit by hand.
|
|
111
111
|
//
|
|
112
112
|
// Every \`CLEAROTRON_*\` name this build reads. It exists so the retired-spelling check can name the
|
|
113
|
-
// replacement for an old \`
|
|
113
|
+
// replacement for an old \`CLEARANCE_*\` line rather than only saying the old one is dead — see
|
|
114
114
|
// shared/env-aliases.mjs. Re-mint with:
|
|
115
115
|
//
|
|
116
116
|
// node scripts/mint-names-in-force.mjs
|