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
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
|
+
//
|
|
5
|
+
// mint-offered-territories.mjs — write the places the New Clearance form offers, derived by rule.
|
|
6
|
+
//
|
|
7
|
+
// The list is `searchableTerritories()` (driver/register-coverage.mjs): every place at least one supported
|
|
8
|
+
// register can search, named by its shipped label or the runtime's standard English name, never by hand.
|
|
9
|
+
// It is written ONCE, to shared/offered-territories.json, and read from there by both halves that must
|
|
10
|
+
// agree — the form (portal-ui composerProduct.ts, at build time) and the engine's mirror of it
|
|
11
|
+
// (driver/compose-read.mjs PROMPT_TERRITORIES). Two lists kept equal by a test drift in the gap between
|
|
12
|
+
// runs; one file read twice cannot.
|
|
13
|
+
//
|
|
14
|
+
// A COMMITTED file rather than a runtime call, for two reasons. The portal is a browser bundle and cannot
|
|
15
|
+
// run the engine's resolver; and what the form offers should change when somebody re-mints it and reads
|
|
16
|
+
// the diff, not silently when a provider's coverage table is edited. `generated-files-are-current.mjs`
|
|
17
|
+
// runs this with --check, so a coverage change that moves the list reds until it is minted.
|
|
18
|
+
//
|
|
19
|
+
// Usage: node scripts/mint-offered-territories.mjs [--check]
|
|
20
|
+
// --check exit 1 if the committed file disagrees with a fresh derivation, printing the difference
|
|
21
|
+
|
|
22
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { dirname, join } from "node:path";
|
|
24
|
+
import { fileURLToPath } from "node:url";
|
|
25
|
+
|
|
26
|
+
const REPO = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
27
|
+
const OUT = join(REPO, "shared", "offered-territories.json");
|
|
28
|
+
|
|
29
|
+
/** The file's content, derived fresh. Regions first, then countries, each as `{code, name}`. */
|
|
30
|
+
export async function deriveOfferedTerritories() {
|
|
31
|
+
const { searchableTerritories } = await import("../driver/register-coverage.mjs");
|
|
32
|
+
const { territoryTier } = await import("../driver/territory-tiers.mjs");
|
|
33
|
+
const { offered, unnamed } = await searchableTerritories();
|
|
34
|
+
const regions = [], countries = [], untiered = [];
|
|
35
|
+
for (const t of offered) {
|
|
36
|
+
const tier = territoryTier(t.name);
|
|
37
|
+
if (tier === "region") regions.push(t);
|
|
38
|
+
else if (tier === "country") countries.push(t);
|
|
39
|
+
else untiered.push(t.code);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
_generated: "by scripts/mint-offered-territories.mjs from searchableTerritories() — do not edit; re-mint",
|
|
43
|
+
regions, countries,
|
|
44
|
+
// Recorded, not dropped: a searchable place the list could not name, or could not place in a tier.
|
|
45
|
+
left_out: { unnamed, untiered },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const render = (doc) => JSON.stringify(doc, null, 2) + "\n";
|
|
50
|
+
|
|
51
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
52
|
+
const next = render(await deriveOfferedTerritories());
|
|
53
|
+
if (process.argv.includes("--check")) {
|
|
54
|
+
let current;
|
|
55
|
+
try { current = readFileSync(OUT, "utf8"); }
|
|
56
|
+
catch { console.error(`mint-offered-territories: ${OUT} is missing — run without --check to mint it.`); process.exit(1); }
|
|
57
|
+
if (current === next) {
|
|
58
|
+
const doc = JSON.parse(current);
|
|
59
|
+
console.log(`offered-territories: current, ${doc.regions.length} region(s) and ${doc.countries.length} countr(ies).`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
const names = (s) => { const d = JSON.parse(s); return new Set([...d.regions, ...d.countries].map((t) => `${t.code} ${t.name}`)); };
|
|
63
|
+
const [a, b] = [names(current), names(next)];
|
|
64
|
+
for (const x of b) if (!a.has(x)) console.error(` + ${x}`);
|
|
65
|
+
for (const x of a) if (!b.has(x)) console.error(` - ${x}`);
|
|
66
|
+
console.error("offered-territories: the committed list disagrees with the rule — re-mint: node scripts/mint-offered-territories.mjs");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
writeFileSync(OUT, next);
|
|
70
|
+
const doc = JSON.parse(next);
|
|
71
|
+
console.log(`offered-territories: wrote ${doc.regions.length} region(s) and ${doc.countries.length} countr(ies) to ${OUT}`);
|
|
72
|
+
}
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { join, dirname } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { CLASSES, censusOf, publishedOf } from "../shared/reference-guard-classes.mjs";
|
|
20
|
+
import { CLASSES, censusOf, publishedOf, publishedReader } from "../shared/reference-guard-classes.mjs";
|
|
21
21
|
import { trackedFiles, skipReason } from "../shared/tracked-files.mjs";
|
|
22
22
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
23
23
|
|
|
@@ -35,7 +35,7 @@ export function mint() {
|
|
|
35
35
|
const p = publishedOf(tracked, ROOT);
|
|
36
36
|
if (p.error) { console.error(`mint-public-residue: ${p.error}`); process.exit(2); }
|
|
37
37
|
if (p.laid) console.log(`mint-public-residue: ${p.laid} tracked path(s) are not in HEAD — laid over this checkout, not published in it, and not counted`);
|
|
38
|
-
const c = censusOf(p.files, (f) => readFileSync(join(ROOT, f), "utf8"));
|
|
38
|
+
const c = censusOf(p.files, publishedReader(ROOT, (f) => readFileSync(join(ROOT, f), "utf8")));
|
|
39
39
|
return { classes: CLASSES.map((x) => x.id), total: c.total, files: c.files };
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// CI runs. Without it, the table is rewritten.
|
|
15
15
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { execFileSync } from "node:child_process";
|
|
17
|
-
import { publishedOf } from "../shared/reference-guard-classes.mjs";
|
|
17
|
+
import { publishedOf, publishedReader } from "../shared/reference-guard-classes.mjs";
|
|
18
18
|
import { join, dirname } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
import { SIGNATURES, censusOf } from "../driver/reference-strip-signatures.mjs";
|
|
@@ -34,7 +34,7 @@ const p = publishedOf(all, ROOT);
|
|
|
34
34
|
if (p.error) { console.error(`mint-reference-strip-backlog: ${p.error}`); process.exit(2); }
|
|
35
35
|
if (p.laid) console.log(`mint-reference-strip-backlog: ${p.laid} tracked path(s) are not in HEAD — laid over this checkout, not published in it, and not counted`);
|
|
36
36
|
const tracked = p.files;
|
|
37
|
-
const minted = censusOf(ROOT, tracked, (f) => readFileSync(join(ROOT, f), "utf8"));
|
|
37
|
+
const minted = censusOf(ROOT, tracked, publishedReader(ROOT, (f) => readFileSync(join(ROOT, f), "utf8")));
|
|
38
38
|
|
|
39
39
|
if (process.argv.includes("--check")) {
|
|
40
40
|
const have = JSON.parse(readFileSync(TABLE, "utf8"));
|
|
@@ -39,7 +39,7 @@ import { CENSUS_WORKSPACES, CENSUS_ROOT_SCRIPTS, countTestSites, collectionFromM
|
|
|
39
39
|
// THE MEASUREMENT SIDE of the root-script check: the census states the globs it expects and this
|
|
40
40
|
// supplies what the corpus actually resolves to, so the two can disagree.
|
|
41
41
|
import { providerTestFiles } from "./test-full.mjs";
|
|
42
|
-
import { withheldEntryFor, announceWithheldMode } from "../shared/withheld-paths-access.mjs"; // — withheld is a stated absence, not a loss.: the record does not ship, and without it every absence is a loss
|
|
42
|
+
import { withheldEntryFor, announceWithheldMode, CUT_RECORD_PRESENT } from "../shared/withheld-paths-access.mjs"; // — withheld is a stated absence, not a loss.: the record does not ship, and without it every absence is a loss
|
|
43
43
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — realpath both sides, or a symlinked invocation exits 0 silently
|
|
44
44
|
import { publishedOf } from "../shared/reference-guard-classes.mjs";
|
|
45
45
|
|
|
@@ -73,6 +73,60 @@ const README = [
|
|
|
73
73
|
"RE-STAMP DELIBERATELY: node scripts/mint-suite-census.mjs --apply",
|
|
74
74
|
];
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* WHAT A LAID PATH MEANS DEPENDS ON THE TREE, and this census used to answer the same way for both.
|
|
78
|
+
*
|
|
79
|
+
* It printed that some tracked paths were not in HEAD and not counted, and then reported the population
|
|
80
|
+
* as "unchanged" — a summary of a population it had just said it did not read. Measured twice on
|
|
81
|
+
* 2026-09-17, from opposite directions: mid-merge it dropped every path arriving from the other parent,
|
|
82
|
+
* and with an ordinary new test file staged it dropped that, because `git add` puts a file in the index
|
|
83
|
+
* and only a commit puts it in HEAD. Both printed the note; both reported unchanged; one shipped a census
|
|
84
|
+
* six tests and twenty-three assertions short and was caught by `--check` afterwards rather than here.
|
|
85
|
+
*
|
|
86
|
+
* THE DISCRIMINATOR ALREADY EXISTS and is the cut record. Under the private overlay a laid path is the
|
|
87
|
+
* withheld corpus staged over a clone — expected, and excluding it is the whole reason HEAD is consulted
|
|
88
|
+
* rather than the index. With no cut record nothing is withheld from this tree, so a tracked path missing
|
|
89
|
+
* from HEAD is not a withheld file: it is work the author has staged and is about to leave out of the
|
|
90
|
+
* stamp. That is the strict mode this repository already declares for every other absence.
|
|
91
|
+
*
|
|
92
|
+
* So it refuses there, and names the paths. It does NOT refuse under the overlay, because that would break
|
|
93
|
+
* the case HEAD-filtering was introduced for — and a guard that fires on the legitimate use is one
|
|
94
|
+
* somebody switches off.
|
|
95
|
+
*
|
|
96
|
+
* Pure, and takes the cut-record flag rather than reading it, so both branches can be driven. The flag is
|
|
97
|
+
* fixed when its module is imported and a test cannot flip it without writing into the tree.
|
|
98
|
+
*/
|
|
99
|
+
export function laidPathVerdict({ laid = 0, laidPaths = [], cutRecordPresent = false, overlayLaid = null } = {}) {
|
|
100
|
+
if (!laid) return { refuse: false, message: null };
|
|
101
|
+
if (cutRecordPresent) {
|
|
102
|
+
return { refuse: false,
|
|
103
|
+
message: `mint-suite-census: ${laid} tracked path(s) are not in HEAD — laid over this checkout, not published in it, and not counted` };
|
|
104
|
+
}
|
|
105
|
+
// THE OVERLAY SAYS WHAT IT LAID, because the cut record this leaned on is carried by no tree, the
|
|
106
|
+
// overlaid one included (shared/withheld-paths-access.mjs says so of itself). So under the private
|
|
107
|
+
// overlay this refused every run, and the arm that runs every minter over the overlaid tree went red on
|
|
108
|
+
// a census that was current. The overlay now writes the paths it laid into the clone's own `.git`, where
|
|
109
|
+
// nothing tracks or ships them. A staged path on that list is laid; one that is not is still staged work
|
|
110
|
+
// and still refuses — the list excuses exactly what the overlay put there and nothing beside it.
|
|
111
|
+
const unlisted = overlayLaid ? laidPaths.filter((f) => !overlayLaid.has(f)) : laidPaths;
|
|
112
|
+
if (overlayLaid && !unlisted.length) {
|
|
113
|
+
return { refuse: false,
|
|
114
|
+
message: `mint-suite-census: ${laid} tracked path(s) are not in HEAD — laid by the private overlay, which lists every one, and not counted` };
|
|
115
|
+
}
|
|
116
|
+
laidPaths = unlisted;
|
|
117
|
+
laid = unlisted.length;
|
|
118
|
+
const named = laidPaths.slice(0, 20).map((f) => ` ${f}`).join("\n");
|
|
119
|
+
const more = laidPaths.length > 20 ? `\n … and ${laidPaths.length - 20} more` : "";
|
|
120
|
+
return { refuse: true,
|
|
121
|
+
message: `mint-suite-census: ${laid} tracked path(s) are in the index and NOT in HEAD, and this tree carries no\n`
|
|
122
|
+
+ " cut record — so nothing here is withheld and these are not laid-over files. They are staged work,\n"
|
|
123
|
+
+ " and a census minted now would be stamped over a population it did not read:\n"
|
|
124
|
+
+ `${named}${more}\n`
|
|
125
|
+
+ " Commit them first, then mint. `git add` puts a file in the index; only a commit puts it in HEAD,\n"
|
|
126
|
+
+ " and this reads HEAD so the private overlay's withheld corpus is not counted as an addition.\n"
|
|
127
|
+
+ " Mid-merge this is the same fault wearing different clothes: commit the merge, then mint." };
|
|
128
|
+
}
|
|
129
|
+
|
|
76
130
|
/**
|
|
77
131
|
* The tracked files matching `globs` that HEAD actually carries.
|
|
78
132
|
*
|
|
@@ -94,6 +148,22 @@ const README = [
|
|
|
94
148
|
* A tree with no HEAD cannot say what it published. That is a failure to look, and it exits rather
|
|
95
149
|
* than reading as "nothing is laid here", which is the permissive answer and the one that passes.
|
|
96
150
|
*/
|
|
151
|
+
/**
|
|
152
|
+
* The paths the private overlay says it laid over this clone, or null where it laid nothing.
|
|
153
|
+
*
|
|
154
|
+
* Read from the clone's own git directory, `info/overlay-laid`, one path per line. Inside `.git` on
|
|
155
|
+
* purpose: nothing there is tracked, so it can never reach the published tree or be counted by a guard,
|
|
156
|
+
* and a developer's checkout has no such file unless an overlay wrote it.
|
|
157
|
+
*/
|
|
158
|
+
export function overlayLaidOf(root) {
|
|
159
|
+
try {
|
|
160
|
+
const gitDir = execFileSync("git", ["-C", root, "rev-parse", "--absolute-git-dir"], { encoding: "utf8" }).trim();
|
|
161
|
+
const list = join(gitDir, "info", "overlay-laid");
|
|
162
|
+
if (!existsSync(list)) return null;
|
|
163
|
+
return new Set(readFileSync(list, "utf8").split("\n").map((x) => x.trim()).filter(Boolean));
|
|
164
|
+
} catch { return null; }
|
|
165
|
+
}
|
|
166
|
+
|
|
97
167
|
function publishedFiles(root, globs) {
|
|
98
168
|
let all = [];
|
|
99
169
|
try {
|
|
@@ -103,7 +173,10 @@ function publishedFiles(root, globs) {
|
|
|
103
173
|
if (!all.length) return [];
|
|
104
174
|
const p = publishedOf(all, root);
|
|
105
175
|
if (p.error) { console.error(`mint-suite-census: ${p.error}`); process.exit(2); }
|
|
106
|
-
|
|
176
|
+
const verdict = laidPathVerdict({ laid: p.laid, laidPaths: p.laidPaths ?? [], cutRecordPresent: CUT_RECORD_PRESENT,
|
|
177
|
+
overlayLaid: overlayLaidOf(root) });
|
|
178
|
+
if (verdict.refuse) { console.error(verdict.message); process.exit(2); }
|
|
179
|
+
if (verdict.message) console.log(verdict.message);
|
|
107
180
|
return p.files.slice().sort();
|
|
108
181
|
}
|
|
109
182
|
|
|
@@ -25,7 +25,7 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
25
25
|
import { join, dirname } from "node:path";
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
27
|
import { CLASSES, censusOf } from "../shared/writing-standard-classes.mjs";
|
|
28
|
-
import { publishedOf } from "../shared/reference-guard-classes.mjs";
|
|
28
|
+
import { publishedOf, publishedReader } from "../shared/reference-guard-classes.mjs";
|
|
29
29
|
import { trackedFiles, skipReason } from "../shared/tracked-files.mjs";
|
|
30
30
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
31
31
|
|
|
@@ -43,7 +43,7 @@ export function mint() {
|
|
|
43
43
|
const p = publishedOf(tracked, ROOT);
|
|
44
44
|
if (p.error) { console.error(`mint-writing-standard-backlog: ${p.error}`); process.exit(2); }
|
|
45
45
|
if (p.laid) console.log(`mint-writing-standard-backlog: ${p.laid} tracked path(s) are not in HEAD — laid over this checkout, not published in it, and not counted`);
|
|
46
|
-
const c = censusOf(p.files, (f) => readFileSync(join(ROOT, f), "utf8"));
|
|
46
|
+
const c = censusOf(p.files, publishedReader(ROOT, (f) => readFileSync(join(ROOT, f), "utf8")));
|
|
47
47
|
return { classes: CLASSES.map((x) => x.id), total: c.total, files: c.files };
|
|
48
48
|
}
|
|
49
49
|
|
package/scripts/purge-runs.mjs
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
// republishable from the workspace archive, which does not satisfy "gone":
|
|
14
14
|
//
|
|
15
15
|
// 1. pool <poolRoot>/<runId>/ — published, Caddy-served
|
|
16
|
-
// 2. archive <ws>/studio/
|
|
17
|
-
// 3. live <ws>/studio/
|
|
16
|
+
// 2. archive <ws>/studio/clearance-search/archive/<YYYY-MM>/<matter>/<codename>/ — republish source
|
|
17
|
+
// 3. live <ws>/studio/clearance-search/<matter>/<codename>/ — running/failed, never published
|
|
18
18
|
//
|
|
19
19
|
// Dry-run is the default and --apply is required to remove anything. The guards below exist because
|
|
20
20
|
// the failure that matters is not "deleted too little" — it is deleting the one run someone wanted
|
|
@@ -24,7 +24,7 @@ import "../shared/env-local.mjs"; // — FIRST: the CLEAROTRON_* translation m
|
|
|
24
24
|
// module-top capture below it evaluates. A call in this file's BODY
|
|
25
25
|
// would run too late — that was the repair that left this open.
|
|
26
26
|
import { readdirSync, statSync, readFileSync, rmSync, existsSync, appendFileSync } from "node:fs";
|
|
27
|
-
import { join, basename } from "node:path";
|
|
27
|
+
import { join, basename } from "node:path"; import { studioDirFor } from "../shared/pre-rename-spellings.mjs";
|
|
28
28
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
29
29
|
import { pendingRunIds, noticeOwed } from "../shared/notice-owed.mjs"; // one reading of "still owes a notice", shared with the delivery path
|
|
30
30
|
// The ONE exception to this script's no-driver-imports posture, and it is the reason that posture exists:
|
|
@@ -94,10 +94,10 @@ const WORKSPACE_ROOT = requireRoot("CLEAROTRON_WORK_DIR", "workspace");
|
|
|
94
94
|
// same answer to a careless reader, and the careless answer is DELETE.
|
|
95
95
|
// THE SAME RESOLUTION THE DELIVERY PATH USES, including its default. Reading only the environment
|
|
96
96
|
// variable would be a SECOND resolution, and a wrong one: `driver.config.mjs` falls back to
|
|
97
|
-
// `<workspaceRoot>/
|
|
97
|
+
// `<workspaceRoot>/clearance-outbox`, so on every box that does not set the variable — which is most of
|
|
98
98
|
// them — an env-only read answers "no outbox" for an outbox that exists and has markers in it. That is
|
|
99
99
|
// the same class of defect as not looking at all.
|
|
100
|
-
const OUTBOX_DIR = String(process.env.CLEAROTRON_OUTBOX_DIR ?? "").trim() || join(WORKSPACE_ROOT, "
|
|
100
|
+
const OUTBOX_DIR = String(process.env.CLEAROTRON_OUTBOX_DIR ?? "").trim() || join(WORKSPACE_ROOT, "clearance-outbox");
|
|
101
101
|
const OUTBOX_NAMES = (() => {
|
|
102
102
|
try { return readdirSync(OUTBOX_DIR); } catch (e) {
|
|
103
103
|
// AN OUTBOX THAT WAS NEVER CREATED IS EMPTY; ONE THAT WILL NOT OPEN IS UNKNOWN. Collapsing the two
|
|
@@ -172,7 +172,7 @@ const POOL_NON_RUN = new Set(["assets", "customer", "_state"]);
|
|
|
172
172
|
// system's own state is not.
|
|
173
173
|
const RUN_ARTIFACTS = ["meta.json", "report.html", "report.md", "findings.json", "status.json"];
|
|
174
174
|
const looksLikeRun = (dir) => RUN_ARTIFACTS.some((f) => existsSync(join(dir, f)));
|
|
175
|
-
// Not matter directories inside a workspace
|
|
175
|
+
// Not matter directories inside a workspace clearance-search root.
|
|
176
176
|
const WS_NON_MATTER = new Set(["STATUS.md", "archive", "queue", "_known-conflicts"]);
|
|
177
177
|
|
|
178
178
|
const readJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
|
|
@@ -210,7 +210,7 @@ function collect() {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
for (const agent of AGENTS) {
|
|
213
|
-
const base = join(WORKSPACE_ROOT, `workspace-${agent}
|
|
213
|
+
const base = studioDirFor(join(WORKSPACE_ROOT, `workspace-${agent}`));
|
|
214
214
|
if (!isDir(base)) continue;
|
|
215
215
|
|
|
216
216
|
// 2. Workspace archive — <YYYY-MM>/<matter>/<codename>. runId is matter+codename, matching the
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
import "../shared/env-local.mjs"; // — FIRST: applies the CLEAROTRON_* translation before any module-top
|
|
36
36
|
// capture evaluates. Reads no `.env` here — that load is gated on isCliEntry(argv[1]).
|
|
37
37
|
import { readdirSync, readFileSync, writeFileSync, renameSync, existsSync } from "node:fs";
|
|
38
|
-
import { join } from "node:path";
|
|
38
|
+
import { join } from "node:path"; import { studioDirFor } from "../shared/pre-rename-spellings.mjs";
|
|
39
39
|
import { DRIVER_DIR } from "../shared/driver-dir.mjs"; //
|
|
40
40
|
import { classifyRun, terminalPatch, DEFAULT_QUIET_MS } from "../driver/reconcile-runs.mjs";
|
|
41
41
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — realpath both sides, or a symlinked invocation exits 0 silently
|
|
@@ -120,7 +120,7 @@ function main() {
|
|
|
120
120
|
const now = Date.now();
|
|
121
121
|
const rows = [];
|
|
122
122
|
for (const agent of AGENTS) {
|
|
123
|
-
const studio = join(WORKSPACE_ROOT, `workspace-${agent}
|
|
123
|
+
const studio = studioDirFor(join(WORKSPACE_ROOT, `workspace-${agent}`));
|
|
124
124
|
for (const dir of findRunDirs(studio)) rows.push({ agent, ...reconcileRunDir(dir, { apply: APPLY, now, quietMs: QUIET_MS }) });
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -99,7 +99,25 @@ async function branchHead() {
|
|
|
99
99
|
return (await br.json())?.commit?.sha ?? null;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
/**
|
|
103
|
+
* ONE PASS: read the version branch's head, and approve any run parked on exactly that head.
|
|
104
|
+
*
|
|
105
|
+
* EXPORTED BECAUSE ONCE IS NOT ENOUGH. The step that calls this runs before a wait that then lasts
|
|
106
|
+
* minutes, and the version branch is force-pushed whenever the version step runs — so main moving
|
|
107
|
+
* during the wait leaves a fresh parked run on a fresh head with nobody left to approve it. Measured on
|
|
108
|
+
* the 0.3.2-beta.8 cut: two heads, one approval, cleared by hand. `awaitCut` calls this once per poll
|
|
109
|
+
* so the second head gets the same look the first did.
|
|
110
|
+
*
|
|
111
|
+
* IT NEVER THROWS AND NEVER RETURNS NON-ZERO. A pass that cannot look says so and reports nothing
|
|
112
|
+
* approved; the cut's own wait ends the way it always did. Approving is a convenience, and a
|
|
113
|
+
* convenience that can fail a release is not one.
|
|
114
|
+
*/
|
|
115
|
+
export async function approvePass() {
|
|
116
|
+
try { return await onePass(); }
|
|
117
|
+
catch (e) { say(`could not run (${e?.message ?? e}) — nothing approved.`); return 0; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function onePass() {
|
|
103
121
|
// THE ABSENT SECRET IS THE ORDINARY CASE UNTIL THE TOKEN IS MINTED, and it must read as ordinary.
|
|
104
122
|
if (!TOKEN) {
|
|
105
123
|
say("no ACTIONS_APPROVE_TOKEN — nothing approved; the version run waits for a person, as before.");
|
|
@@ -148,4 +166,4 @@ async function main() {
|
|
|
148
166
|
// RUN WHEN INVOKED, whatever the file is called. The old guard compared argv[1] to this file's NAME, so
|
|
149
167
|
// a rename made the step print nothing and exit 0 — a script that never ran, wearing the face of a
|
|
150
168
|
// successful no-op. Comparing the resolved URLs asks the real question instead.
|
|
151
|
-
if (import.meta.url === pathToFileURL(argv[1] ?? "").href)
|
|
169
|
+
if (import.meta.url === pathToFileURL(argv[1] ?? "").href) approvePass().then((c) => exit(c));
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
// one question is how a pipeline comes to publish something nobody merged.
|
|
49
49
|
import { appendFileSync } from "node:fs";
|
|
50
50
|
import { execFileSync } from "node:child_process";
|
|
51
|
+
import { approvePass } from "./release-approve-parked.mjs";
|
|
51
52
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs";
|
|
52
53
|
import { cutDecision, versionAtHead, tagsHere } from "./release-cut-decision.mjs";
|
|
53
54
|
|
|
@@ -117,10 +118,23 @@ export const STEP_MS = 30 * 1000;
|
|
|
117
118
|
*
|
|
118
119
|
* Returns `{ cut, version, waitedMs, gaveUp }`. `gaveUp` is not a failure — see the header.
|
|
119
120
|
*/
|
|
120
|
-
export async function awaitCut({ refresh, read, sleep, waitMs = WAIT_MS, stepMs = STEP_MS, now = () => 0 } = {}) {
|
|
121
|
+
export async function awaitCut({ refresh, read, sleep, tend = null, waitMs = WAIT_MS, stepMs = STEP_MS, now = () => 0 } = {}) {
|
|
121
122
|
const started = now();
|
|
122
123
|
let waitedMs = 0;
|
|
123
124
|
for (;;) {
|
|
125
|
+
// ── THE SECOND LOOK, EVERY PASS ────────────────────────────────────────────────────────────────
|
|
126
|
+
//
|
|
127
|
+
// `tend` is the parked-run approval, and it is here rather than once before the loop because the
|
|
128
|
+
// version branch is force-pushed whenever the version step runs. If main moves while this waits,
|
|
129
|
+
// the version pull request is refreshed onto a new head, that head parks a run of its own, and the
|
|
130
|
+
// approval already spent was spent on a head that no longer exists.
|
|
131
|
+
//
|
|
132
|
+
// IT CANNOT BREAK THE WAIT. A tend that throws is reported and the pass continues: this loop's job
|
|
133
|
+
// is to notice a merge, and it must keep doing that whether or not a convenience beside it worked.
|
|
134
|
+
if (tend) {
|
|
135
|
+
try { await tend(); }
|
|
136
|
+
catch (e) { console.log(`release-await-cut: the parked-run look failed (${String(e?.message ?? e).slice(0, 120)}) — the wait continues.`); }
|
|
137
|
+
}
|
|
124
138
|
await refresh();
|
|
125
139
|
const d = read();
|
|
126
140
|
// ASKED BEFORE THE FIRST SLEEP, so a merge that landed while CI was finishing costs no wait at all —
|
|
@@ -135,6 +149,91 @@ export async function awaitCut({ refresh, read, sleep, waitMs = WAIT_MS, stepMs
|
|
|
135
149
|
|
|
136
150
|
const git = (args) => execFileSync("git", args, { encoding: "utf8" });
|
|
137
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Was a cut ASKED FOR by this run, and which pull request is it waiting on.
|
|
154
|
+
*
|
|
155
|
+
* A push and the schedule set nothing in motion — they ask main a question and the cron floor sits
|
|
156
|
+
* underneath either answer. A dispatch is different: the same run opened the version pull request and
|
|
157
|
+
* told it to merge itself, so the wait expiring means the thing this run was for did not happen.
|
|
158
|
+
*/
|
|
159
|
+
export function cutRequest(env = process.env) {
|
|
160
|
+
const asked = String(env.CLEAROTRON_CUT_REQUESTED ?? "").trim() === "true";
|
|
161
|
+
const pr = String(env.CLEAROTRON_CUT_PR ?? "").trim();
|
|
162
|
+
return { asked, pr: /^[0-9]+$/.test(pr) ? Number(pr) : null };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* What to say when the budget expires, and whether it is a failure. PURE.
|
|
167
|
+
*
|
|
168
|
+
* THE DISTINCTION THIS EXISTS FOR. Three ways to publish nothing look identical from outside:
|
|
169
|
+
* nothing was set in motion, this run set something in motion and it did not land, and the loop could
|
|
170
|
+
* not look at all. The third is exit 2 and is decided before this is reached. The first two are what
|
|
171
|
+
* this separates, and until it did, a dispatched cut that released nothing reported success — measured
|
|
172
|
+
* twice on 2026-09-17, on runs 35202212206 and 35260883981.
|
|
173
|
+
*
|
|
174
|
+
* NAMING WHICH IS HALF THE POINT. A red that says only "did not merge" sends the next reader to the
|
|
175
|
+
* same log this was written from. The reason is read off the pull request rather than guessed.
|
|
176
|
+
*/
|
|
177
|
+
export function expiryVerdict({ cut, requested, pr = null }) {
|
|
178
|
+
if (cut) return { red: false, reason: null };
|
|
179
|
+
if (!requested) return { red: false, reason: null };
|
|
180
|
+
if (!pr) return { red: true, reason: "a cut was dispatched, and no version pull request was found to wait on" };
|
|
181
|
+
// A PULL REQUEST THAT COULD NOT BE READ IS NOT ONE THAT WAS NOT FOUND. Both used to arrive here as
|
|
182
|
+
// `null`, so a failed read reported "no version pull request was found" about a pull request that was
|
|
183
|
+
// standing open — a confident wrong reason, on the one line this function exists to get right.
|
|
184
|
+
if (pr.unread) {
|
|
185
|
+
return { red: true, reason: `the version pull request (${pr.number}) could not be read, so why it did not merge is unknown — read it by hand (${pr.why})` };
|
|
186
|
+
}
|
|
187
|
+
if (pr.merged) return { red: true, reason: "the version pull request merged, yet main carries no untagged version — the publish this run was for has gone missing" };
|
|
188
|
+
const bad = (pr.checks ?? []).filter((c) => ["failure", "error", "timed_out", "cancelled", "action_required"].includes(c.conclusion));
|
|
189
|
+
if (bad.length) {
|
|
190
|
+
const named = bad.map((c) => `${c.context} (${c.conclusion})`).join(", ");
|
|
191
|
+
return { red: true, reason: `the version pull request did not merge: ${named}` };
|
|
192
|
+
}
|
|
193
|
+
if (!(pr.checks ?? []).some((c) => c.conclusion)) {
|
|
194
|
+
return { red: true, reason: "the version pull request did not merge: no check on it had concluded when the budget expired — it is most likely waiting for approval before its run will start" };
|
|
195
|
+
}
|
|
196
|
+
if (pr.mergeable === false) return { red: true, reason: "the version pull request did not merge: it is not mergeable" };
|
|
197
|
+
return { red: true, reason: "the version pull request did not merge, and nothing on it says why — read it by hand" };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* One entry of the rollup, as a conclusion or `null` while it has none.
|
|
202
|
+
*
|
|
203
|
+
* THE ROLLUP HOLDS TWO SHAPES. A check run carries `conclusion`; a commit status carries `state` and no
|
|
204
|
+
* `conclusion` at all, so reading only the first counts every red status as not-yet-concluded.
|
|
205
|
+
*/
|
|
206
|
+
function conclusionOf(c) {
|
|
207
|
+
if (c.conclusion) return String(c.conclusion).toLowerCase();
|
|
208
|
+
if (c.state && !["PENDING", "EXPECTED"].includes(String(c.state).toUpperCase())) return String(c.state).toLowerCase();
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The version pull request's state and its checks' conclusions. Injected so an arm can drive it.
|
|
214
|
+
*
|
|
215
|
+
* `null` means there was no number to read. A read that FAILED returns `{ unread: true }` instead, so the
|
|
216
|
+
* verdict can tell "no pull request" from "could not look at it".
|
|
217
|
+
*/
|
|
218
|
+
export async function readVersionPr(number, { run = ((args) => execFileSync("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] })) } = {}) {
|
|
219
|
+
if (!number) return null;
|
|
220
|
+
try {
|
|
221
|
+
const j = JSON.parse(run(["pr", "view", String(number), "--repo", "CordilleraSarl/clearotron",
|
|
222
|
+
"--json", "merged,mergeable,statusCheckRollup"]));
|
|
223
|
+
return {
|
|
224
|
+
merged: Boolean(j.merged),
|
|
225
|
+
mergeable: j.mergeable === "MERGEABLE" ? true : j.mergeable === "CONFLICTING" ? false : null,
|
|
226
|
+
checks: (j.statusCheckRollup ?? []).map((c) => ({
|
|
227
|
+
context: String(c.name ?? c.context ?? "a check"),
|
|
228
|
+
conclusion: conclusionOf(c),
|
|
229
|
+
})),
|
|
230
|
+
};
|
|
231
|
+
} catch (e) {
|
|
232
|
+
const why = String(e?.stderr || e?.message || e).trim().split("\n")[0].slice(0, 160);
|
|
233
|
+
return { unread: true, number, why };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
138
237
|
/**
|
|
139
238
|
* One read of `main`: the version it carries, whether that version is tagged, and WHICH COMMIT said so.
|
|
140
239
|
*
|
|
@@ -209,6 +308,8 @@ function main() {
|
|
|
209
308
|
// answer this pipeline cannot afford, so it is refreshed on every pass rather than once at checkout.
|
|
210
309
|
refresh: async () => { git(["fetch", "--no-tags", "--prune", "origin", "+refs/heads/main:refs/remotes/origin/main"]); git(["fetch", "--tags", "--force", "origin"]); },
|
|
211
310
|
read: () => readMain(),
|
|
311
|
+
// THE SAME SCRIPT THE STEP ABOVE RUNS, not a second implementation of the same decision.
|
|
312
|
+
tend: () => approvePass(),
|
|
212
313
|
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
213
314
|
now: () => Date.now() - started,
|
|
214
315
|
}).catch((e) => {
|
|
@@ -229,7 +330,10 @@ function main() {
|
|
|
229
330
|
}).then((r) => {
|
|
230
331
|
if (!r) return;
|
|
231
332
|
const secs = Math.round(r.waitedMs / 1000);
|
|
333
|
+
// A DISPATCHED CUT DOES NOT GET THE REASSURANCE. The error below says what happened, and a line
|
|
334
|
+
// calling it ordinary directly above that error is the one a reader skimming the log stops at.
|
|
232
335
|
if (r.cut) console.log(`release-await-cut: main carries ${r.version} with no tag, after ${secs}s. Publishing.`);
|
|
336
|
+
else if (cutRequest().asked) console.log(`release-await-cut: nothing to publish after ${secs}s — main carries ${r.version} and it is already tagged.`);
|
|
233
337
|
else console.log(`release-await-cut: nothing to publish after ${secs}s — main carries ${r.version} and it is `
|
|
234
338
|
+ "already tagged, or the version branch did not merge. This is the ordinary outcome and not a fault; "
|
|
235
339
|
+ "the scheduled check is still underneath it.");
|
|
@@ -239,6 +343,21 @@ function main() {
|
|
|
239
343
|
// `sha` IS WRITTEN ON BOTH ANSWERS, not only on a cut. It records which commit this loop's verdict is
|
|
240
344
|
// about, so a run that published nothing can still be read back against the tree it looked at.
|
|
241
345
|
if (out) appendFileSync(out, `cut=${r.cut ? "true" : "false"}\nversion=${r.version}\nsha=${r.sha ?? ""}\nlooked=true\n`);
|
|
346
|
+
return r;
|
|
347
|
+
}).then(async (r) => {
|
|
348
|
+
// ── A DISPATCHED CUT THAT PUBLISHED NOTHING IS A FAILED RUN ─────────────────────────────────────
|
|
349
|
+
//
|
|
350
|
+
// Everything above stays exit 0, and on a push or the schedule that is right: nothing was asked
|
|
351
|
+
// for, and the cron floor sits underneath. A dispatch is a request, and THIS run is what tries to
|
|
352
|
+
// satisfy it — it opened the version pull request and armed it. So the budget expiring means the
|
|
353
|
+
// one thing the run existed to do did not happen, and reporting success for that is how a release
|
|
354
|
+
// goes missing with every light green.
|
|
355
|
+
if (!r) return; // could-not-look already exited 2 above
|
|
356
|
+
const { asked, pr } = cutRequest();
|
|
357
|
+
const v = expiryVerdict({ cut: r.cut, requested: asked, pr: asked && !r.cut ? await readVersionPr(pr) : null });
|
|
358
|
+
if (!v.red) return;
|
|
359
|
+
console.error(`::error::release-await-cut: this run dispatched a cut and released nothing. ${v.reason}`);
|
|
360
|
+
process.exitCode = 1;
|
|
242
361
|
});
|
|
243
362
|
}
|
|
244
363
|
|