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
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
// once per snapshot, by a writer that is already async. So that import is DYNAMIC, and search-policy
|
|
52
52
|
// never pulls the plan layer in to answer a question about a product menu.
|
|
53
53
|
import { PROMPT_TERRITORIES } from "./compose-read.mjs";
|
|
54
|
-
import { territoryTier } from "./territory-tiers.mjs";
|
|
54
|
+
import { territoryTier, territoryKey } from "./territory-tiers.mjs";
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* The composer display names this register can actually search.
|
|
@@ -157,3 +157,163 @@ export function registerCoverageCause(geography, territories, all = PROMPT_TERRI
|
|
|
157
157
|
if (rule(territories, all)) return null;
|
|
158
158
|
return geography === "worldwide, and nothing else" ? "register-not-worldwide" : "register-coverage";
|
|
159
159
|
}
|
|
160
|
+
|
|
161
|
+
// ── the REQUEST half ────────────────────────────────────────────────────────────────────────────────
|
|
162
|
+
//
|
|
163
|
+
// The product half above asks whether a PRODUCT can be ordered at all. This asks whether THIS request's
|
|
164
|
+
// territories can be searched. Two questions, two rulings, deliberately not folded together:
|
|
165
|
+
//
|
|
166
|
+
// · the PRODUCT-level refusal was retired on 2026-08-31 and stays retired — a worldwide search is
|
|
167
|
+
// orderable on a partial register and the gap is DISCLOSED in the report.
|
|
168
|
+
// `coverage-is-disclosed-never-refused.test.mjs` pins that, and this function must never make it red.
|
|
169
|
+
// · the REQUEST-level refusal was ruled on 2026-09-17: a requester who NAMES a territory the wired
|
|
170
|
+
// register cannot search is told so BEFORE the run. That gap is not disclosable — there is no result
|
|
171
|
+
// to caveat, only a territory the client asked about and would never hear another word on.
|
|
172
|
+
//
|
|
173
|
+
// AN EMPTY LIST IS THE "NAMES NO TERRITORY" STATE, BY CONSTRUCTION, and that is why no mode string is
|
|
174
|
+
// read here. `effective-scope.mjs`'s ladder answers `[]` for a worldwide stamp — worldwide accepts no
|
|
175
|
+
// narrowing, so the account's defaults are not consulted — and `[]` again when no layer set a territory.
|
|
176
|
+
// So "a search that names no territory always runs" falls out of the empty set passing. A saved account
|
|
177
|
+
// default that DID reach the ladder is in the list and is judged with the rest, which is the ruling of
|
|
178
|
+
// 2026-09-17 on that question: a default counts as named once it reaches the engine.
|
|
179
|
+
//
|
|
180
|
+
// AND A NAME THE SNAPSHOT CANNOT SPEAK TO FAILS OPEN. `registerTerritories` is scoped to the composer's
|
|
181
|
+
// 37 display names. The other doors are not: `normalizeTerritory` passes any two-letter token through, so
|
|
182
|
+
// the CLI and start_run can name `VN`, which is inside clarivate's own 186-office enum and outside those
|
|
183
|
+
// 37. A bare membership test would refuse a search this engine runs today. Only a territory the
|
|
184
|
+
// snapshot's vocabulary can SPEAK TO is judged, and everything else is the pipeline's to defer as now.
|
|
185
|
+
|
|
186
|
+
/** Composer display name for a canonical key, so a refusal says "China" whether the requester wrote
|
|
187
|
+
* "China", "cn" or "CN". Built per call from the same list the covered set is scoped to. */
|
|
188
|
+
function displayByKey(all) {
|
|
189
|
+
const out = new Map();
|
|
190
|
+
for (const name of all) out.set(territoryKey(name), name);
|
|
191
|
+
return out;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The territories this request NAMES that the wired register cannot search, as composer display names.
|
|
196
|
+
*
|
|
197
|
+
* @param territories the resolved territories — `effective-scope.mjs`'s ladder answer
|
|
198
|
+
* @param registerTerritories what `coveredTerritoryNames` returned: `[...]`, `null` (no declared
|
|
199
|
+
* restriction) or `undefined` (the snapshot does not say)
|
|
200
|
+
* @returns `[]` — nothing to refuse, INCLUDING on `null` and `undefined`, which fail open exactly as
|
|
201
|
+
* they do at every other layer. Never treat an empty return as "the check did not run".
|
|
202
|
+
*
|
|
203
|
+
* Both sides are compared through `territoryKey`, which is the identity function this tree already has
|
|
204
|
+
* for "what makes two spellings the same place" — it folds EM/EUTM/EUIPO to EU and UK to GB. That fold
|
|
205
|
+
* is the whole of requirement 2: an EU-covering register refusing a request that names "European Union"
|
|
206
|
+
* was the defect measured on 2026-09-17, and it cannot recur while both sides go through one authority.
|
|
207
|
+
*/
|
|
208
|
+
export function uncoveredTerritories(territories, registerTerritories, all = PROMPT_TERRITORIES) {
|
|
209
|
+
if (registerTerritories === null || registerTerritories === undefined) return [];
|
|
210
|
+
const covered = new Set(registerTerritories.map(territoryKey));
|
|
211
|
+
const display = displayByKey(all);
|
|
212
|
+
const out = [];
|
|
213
|
+
const seen = new Set();
|
|
214
|
+
for (const t of territories ?? []) {
|
|
215
|
+
const key = territoryKey(t);
|
|
216
|
+
// Outside the snapshot's vocabulary ⇒ this register's covered list says nothing about it. Fail open.
|
|
217
|
+
if (!key || !display.has(key) || covered.has(key) || seen.has(key)) continue;
|
|
218
|
+
seen.add(key);
|
|
219
|
+
out.push(display.get(key));
|
|
220
|
+
}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// One-line join rather than an import: this module's header makes its static graph part of the design
|
|
225
|
+
// ("search-policy.mjs imports this file … a cycle is one careless import away"), and the only other
|
|
226
|
+
// spelling of this lives behind scope-facts.mjs, which is not a leaf.
|
|
227
|
+
const joinAnd = (parts) => (parts.length <= 1 ? parts.join("") : `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`);
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The door's sentence for a request naming territories the wired register cannot search — or null.
|
|
231
|
+
*
|
|
232
|
+
* WRITTEN ONCE, FOR EVERY DOOR. Wording approved by the owner on 2026-09-17, including naming the wired
|
|
233
|
+
* register, which the composer's own design note otherwise forbids on screen. The remedy clause is his
|
|
234
|
+
* addition of the same day: a caller with no screen — start_run, the CLI — has to be able to act on this
|
|
235
|
+
* sentence alone, on the next call.
|
|
236
|
+
*
|
|
237
|
+
* IT POINTS BACK RATHER THAN REPEATING. The first draft named the territories twice ("remove China and
|
|
238
|
+
* Japan"); the owner ruled on the plural the same day — "remove them", no need to repeat the countries —
|
|
239
|
+
* and the singular follows the same reason, since it repeated its one country for no better cause. The
|
|
240
|
+
* sentence already names them, so a screenless caller still has every territory it must drop.
|
|
241
|
+
*
|
|
242
|
+
* @param registerLabel the register's display label. Absent on a snapshot written before it was carried,
|
|
243
|
+
* and the sentence simply does not name it then rather than naming a key.
|
|
244
|
+
*/
|
|
245
|
+
export function registerReachRefusal(uncovered, registerLabel = null) {
|
|
246
|
+
if (!uncovered?.length) return null;
|
|
247
|
+
const names = joinAnd(uncovered);
|
|
248
|
+
const register = String(registerLabel ?? "").trim();
|
|
249
|
+
const where = register ? `${register}, the register configured here` : "the register configured here";
|
|
250
|
+
return `${names} ${uncovered.length === 1 ? "is" : "are"} not available with ${where}`
|
|
251
|
+
+ ` — remove ${uncovered.length === 1 ? "it" : "them"} to run this search.`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── WHAT THE FORM SHOULD OFFER: EVERY PLACE A REGISTER THIS PRODUCT SUPPORTS CAN SEARCH ────────────────
|
|
255
|
+
//
|
|
256
|
+
// The form offered 37 places with no rule behind them: Bulgaria and Greece, but not Denmark, Portugal,
|
|
257
|
+
// Vietnam or Colombia, while a worldwide search on the wider register already swept every one of its 186
|
|
258
|
+
// offices. The rule is the registers' own reach: a place is offered when at least one supported register
|
|
259
|
+
// provider can search it, decided by the SAME resolution the plan compiler runs (`resolveRegions`, one code
|
|
260
|
+
// at a time), so the offer cannot promise a place the compiler would defer everywhere. What THIS install's
|
|
261
|
+
// register does not reach is marked per deployment by `coveredTerritoryNames` and the door's refusal — the
|
|
262
|
+
// offer is the product's; the marking is the install's.
|
|
263
|
+
//
|
|
264
|
+
// A provider with no enumerable coverage (a global aggregator declaring `covered: null`) widens nothing:
|
|
265
|
+
// "everywhere" is not a list, and letting it in would offer every code the engine holds, including places
|
|
266
|
+
// no register answers for.
|
|
267
|
+
//
|
|
268
|
+
// NAMES ARE NEVER WRITTEN HERE. A place the form already offers keeps its shipped label; any other is named
|
|
269
|
+
// by the runtime's standard English region names (CLDR), the same data the territory vocabulary already
|
|
270
|
+
// resolves typed names against. A code with neither — the regional systems no label exists for — is left
|
|
271
|
+
// out and listed, never given a name composed in code. Provider extension codes (X-, ZZ) are not places.
|
|
272
|
+
//
|
|
273
|
+
// @returns {Promise<{ offered: {code: string, name: string}[], unnamed: string[] }>} regions first, then
|
|
274
|
+
// countries by name.
|
|
275
|
+
export async function searchableTerritories() {
|
|
276
|
+
const [{ KNOWN_JURISDICTION_CODES, canonicalJurisdictionCode }, { normalizeTerritory }, { PROVIDER_CAPABILITIES }, { resolveRegions }]
|
|
277
|
+
= await Promise.all([import("./jurisdiction-codes.mjs"), import("../providers/_shared/territory-codes.mjs"),
|
|
278
|
+
import("./register-capabilities.mjs"), import("./register-plan.mjs")]);
|
|
279
|
+
const enumerable = Object.values(PROVIDER_CAPABILITIES).filter((caps) => Array.isArray(caps?.offices?.covered));
|
|
280
|
+
const shippedName = new Map(LABELS_OFFERED_BEFORE_THE_RULE.map((n) => [canonicalJurisdictionCode(normalizeTerritory(n) ?? ""), n]));
|
|
281
|
+
let cldr = null;
|
|
282
|
+
try { cldr = new Intl.DisplayNames(["en"], { type: "region" }); } catch { cldr = null; }
|
|
283
|
+
const offered = [], unnamed = [];
|
|
284
|
+
const seen = new Set();
|
|
285
|
+
for (const raw of KNOWN_JURISDICTION_CODES) {
|
|
286
|
+
const code = canonicalJurisdictionCode(raw);
|
|
287
|
+
if (!code || seen.has(code) || /^(X.|ZZ)$/.test(code)) continue;
|
|
288
|
+
seen.add(code);
|
|
289
|
+
const searched = enumerable.some((caps) => {
|
|
290
|
+
const { regions, deferred } = resolveRegions([code], caps);
|
|
291
|
+
return deferred.length === 0 && regions.length > 0;
|
|
292
|
+
});
|
|
293
|
+
if (!searched) continue;
|
|
294
|
+
let name = shippedName.get(code) ?? null;
|
|
295
|
+
if (!name && cldr) { try { const n = cldr.of(code); if (n && n !== code && !/^unknown/i.test(n)) name = n; } catch { /* no name */ } }
|
|
296
|
+
if (!name) { unnamed.push(code); continue; }
|
|
297
|
+
offered.push({ code, name });
|
|
298
|
+
}
|
|
299
|
+
// Regions first, in the order the form already showed them; then countries by name.
|
|
300
|
+
const regional = new Set(["EU", "BX", "AP", "OA", "EA", "WO"]);
|
|
301
|
+
const shippedAt = (t) => { const i = LABELS_OFFERED_BEFORE_THE_RULE.indexOf(t.name); return i < 0 ? Infinity : i; };
|
|
302
|
+
offered.sort((a, b) => (regional.has(b.code) - regional.has(a.code))
|
|
303
|
+
|| (regional.has(a.code) ? shippedAt(a) - shippedAt(b) : 0) || a.name.localeCompare(b.name, "en"));
|
|
304
|
+
return { offered, unnamed: unnamed.sort() };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// THE 37 LABELS THE FORM CARRIED BEFORE THIS RULE, frozen. They are shipped strings, so each place keeps
|
|
308
|
+
// the label a client already reads — "Hong Kong", "Macau", "Turkey", where the runtime's standard names
|
|
309
|
+
// differ — and the two regional systems whose only names are these. Frozen HERE rather than read from the
|
|
310
|
+
// form's live list, because that list is now minted FROM this function, and a rule that read its own
|
|
311
|
+
// output would lose its labels the first time the file was regenerated from scratch.
|
|
312
|
+
export const LABELS_OFFERED_BEFORE_THE_RULE = Object.freeze([
|
|
313
|
+
"European Union", "Benelux", "African Regional (ARIPO)",
|
|
314
|
+
"United States", "United Kingdom", "Ireland", "France", "Germany", "Spain", "Italy", "Netherlands",
|
|
315
|
+
"Switzerland", "Austria", "Sweden", "Norway", "Poland", "Bulgaria", "Greece", "Turkey", "Canada",
|
|
316
|
+
"Mexico", "Brazil", "Argentina", "China", "Hong Kong", "Taiwan", "Macau", "Japan", "South Korea",
|
|
317
|
+
"Singapore", "India", "Thailand", "Australia", "New Zealand", "United Arab Emirates", "Saudi Arabia",
|
|
318
|
+
"South Africa",
|
|
319
|
+
]);
|
|
@@ -88,6 +88,153 @@ export function accountingArmed(runDir) {
|
|
|
88
88
|
return existsSync(driverDir(String(runDir ?? ""), ACCOUNTING_STAMP));
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// ── THE BAND REACHES THE SEAT IN BATCHES, AND A BATCH IS THE UNIT OF ACCOUNTING ───────────────────
|
|
92
|
+
//
|
|
93
|
+
// A dense matter carried 1,161 records into one digest turn (2026-09-16). The seat ran out of turn
|
|
94
|
+
// before it had accounted for them all, the call was refused with 902 outstanding, and the ladder
|
|
95
|
+
// re-sent the same shape: 35 minutes and 172,900 output tokens for a document that was never written.
|
|
96
|
+
// Nothing was wrong with the judgment. The stage was handed more records than one turn holds.
|
|
97
|
+
//
|
|
98
|
+
// SO THE DRIVER SPLITS THE OWED SET AND THE SEAT RECORDS ONE CALL PER BATCH. What made that impossible
|
|
99
|
+
// was not the absence of a split — the transport has taken several calls since the patch path was
|
|
100
|
+
// built — but WHERE THE REFUSAL LOOKED. It looked at the whole owed set on every call, and a refused
|
|
101
|
+
// call stores no model, so the accumulator could never start: batch 1 was refused for not being
|
|
102
|
+
// batches 2 to 12, and the work in it was discarded. Driven before this change, three calls of one
|
|
103
|
+
// record each against an owed set of three: every call refused "2 of 3", no model stored, no document
|
|
104
|
+
// written, and the same refusal on call 3 as on call 1.
|
|
105
|
+
//
|
|
106
|
+
// A CALL NAMING A BATCH IS JUDGED ON THAT BATCH. A call naming none is judged on the whole owed set,
|
|
107
|
+
// exactly as before, which is what keeps the existing rungs working unchanged: the re-classify rung
|
|
108
|
+
// sends the COMPLETE set of rows and the recall-reconciliation flush sends a patch of the rows it is
|
|
109
|
+
// ending, and neither names a batch.
|
|
110
|
+
export const DIGEST_BATCH_RECORDS = 100;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The owed set split into batches, in order. PURE, and deterministic across a resume: `owed` is derived
|
|
114
|
+
* from placements.json, not from anything the run accumulates, so batch 7 holds the same records on the
|
|
115
|
+
* retry as it did on the attempt that was killed.
|
|
116
|
+
*/
|
|
117
|
+
export function batchesOf(owed, size = DIGEST_BATCH_RECORDS) {
|
|
118
|
+
const list = [...new Set((Array.isArray(owed) ? owed : []).map(joinKey).filter(Boolean))];
|
|
119
|
+
const n = Math.max(1, Number(size) || DIGEST_BATCH_RECORDS);
|
|
120
|
+
const out = [];
|
|
121
|
+
for (let i = 0; i < list.length; i += n) out.push(list.slice(i, i + n));
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The records a model accounts for, by the three exits this seam recognises. PURE.
|
|
127
|
+
*
|
|
128
|
+
* ONE READER FOR TWO GATES, and that is the point of exporting it. The call-time refusal and the
|
|
129
|
+
* stage's exit gate have to be counting the same thing; two implementations of "accounted" would drift
|
|
130
|
+
* and the drift would show up as a stage that passed with records ended nowhere.
|
|
131
|
+
*/
|
|
132
|
+
export function accountedUris(model, owed = []) {
|
|
133
|
+
const accounted = new Set();
|
|
134
|
+
for (const r of [...(model?.findings_rows ?? []), ...(model?.incumbent_rows ?? []), ...(model?.negative_rows ?? [])])
|
|
135
|
+
accounted.add(joinKey(r?.uri));
|
|
136
|
+
// A disagreement resolution accounts for a record when its subject names that record's uri — the
|
|
137
|
+
// third exit, and the one a reader is least likely to expect, so it is joined rather than assumed.
|
|
138
|
+
for (const d of model?.disagreement_resolutions ?? [])
|
|
139
|
+
for (const k of (Array.isArray(owed) ? owed : [])) if (k && lc(d?.subject).includes(k)) accounted.add(k);
|
|
140
|
+
accounted.delete("");
|
|
141
|
+
return accounted;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Every record uri a raw call carries, across the three row lists. PURE. */
|
|
145
|
+
export function callUris(call) {
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const r of [...(call?.findings_rows ?? []), ...(call?.incumbent_rows ?? []), ...(call?.negative_rows ?? [])]) {
|
|
148
|
+
const k = joinKey(r?.uri);
|
|
149
|
+
if (k) out.push(k);
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Which batch accounted each record, after this call. PURE. Returns `{ batchOf, doubled }`.
|
|
156
|
+
*
|
|
157
|
+
* DOUBLE-COUNTING IS A BATCH COLLISION, NEVER A REPEATED URI, and the distinction is the whole reason
|
|
158
|
+
* this is keyed rather than a membership test. `mergeDigestPatch` replaces a row by uri on purpose —
|
|
159
|
+
* "refreshing an ending is idempotent" — and the recall-reconciliation flush rung depends on it,
|
|
160
|
+
* telling the seat to re-send rows it is changing. So a uri arriving again UNDER ITS OWN BATCH is that
|
|
161
|
+
* legitimate refresh and is kept; the same uri arriving under a DIFFERENT batch is a record ended
|
|
162
|
+
* twice, which is what inflates a count nobody can reconcile, and it is refused naming both batches.
|
|
163
|
+
*/
|
|
164
|
+
export function batchLedger(storedBatchOf, call) {
|
|
165
|
+
const batchOf = { ...(storedBatchOf ?? {}) };
|
|
166
|
+
const doubled = [];
|
|
167
|
+
const batch = call?.batch;
|
|
168
|
+
if (!Number.isInteger(batch) || batch < 1) return { batchOf, doubled };
|
|
169
|
+
for (const k of callUris(call)) {
|
|
170
|
+
const was = batchOf[k];
|
|
171
|
+
if (was !== undefined && was !== batch) { doubled.push({ uri: k, was, now: batch }); continue; }
|
|
172
|
+
batchOf[k] = batch;
|
|
173
|
+
}
|
|
174
|
+
return { batchOf, doubled };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The batch block the digest dispatch carries: how the driver split this run's band, which batches are
|
|
179
|
+
* outstanding, and the one rule that makes a batch call different from a whole-document one. `null` when
|
|
180
|
+
* the run has no owed population, because a brief that enumerates nothing reads as a rule with no work.
|
|
181
|
+
*
|
|
182
|
+
* ON A RESUME IT IS THE RESUME INSTRUCTION, and that is why it is computed rather than fixed: the
|
|
183
|
+
* batches already accounted for are named as done, so the seat re-reads none of them. A retry that
|
|
184
|
+
* starts at batch 1 is how the stage burned 35 minutes twice on the same matter.
|
|
185
|
+
*/
|
|
186
|
+
export function digestBatchBrief(gap) {
|
|
187
|
+
if (!gap?.armed || !Array.isArray(gap.owed) || !gap.owed.length) return null;
|
|
188
|
+
const plan = batchesOf(gap.owed);
|
|
189
|
+
const outstanding = new Set(gap.unaccounted ?? []);
|
|
190
|
+
const todo = [];
|
|
191
|
+
for (let i = 0; i < plan.length; i++) if (plan[i].some((k) => outstanding.has(k))) todo.push(i + 1);
|
|
192
|
+
const done = plan.length - todo.length;
|
|
193
|
+
const lines = [
|
|
194
|
+
`## Your records, in ${plan.length} batch${plan.length === 1 ? "" : "es"}`,
|
|
195
|
+
"",
|
|
196
|
+
`This run carried ${gap.owed.length} record${gap.owed.length === 1 ? "" : "s"} into the digest. The driver has split them into `
|
|
197
|
+
+ `${plan.length} batch${plan.length === 1 ? "" : "es"} of up to ${DIGEST_BATCH_RECORDS}. Record ONE `
|
|
198
|
+
+ "`record_register_digest` call per batch, carrying `batch: <the number>` — not one call for the whole band.",
|
|
199
|
+
"",
|
|
200
|
+
"Every record in the batch you name must end in that same call: a findings row, an incumbent row, a "
|
|
201
|
+
+ "Negative-results drop with its ground token, or a Disagreement resolution. The call is refused if "
|
|
202
|
+
+ "one of them ends nowhere, and the refusal lists exactly which — send only those; everything you "
|
|
203
|
+
+ "have already recorded is kept. A record ends in ONE batch: ending it again under a different "
|
|
204
|
+
+ "batch number is refused.",
|
|
205
|
+
"",
|
|
206
|
+
`Your prose sections (opposition, merch_sweep, cross_checks, open_flags) ride any batch call and are `
|
|
207
|
+
+ "kept when a later call omits them.",
|
|
208
|
+
"",
|
|
209
|
+
];
|
|
210
|
+
if (done) lines.push(`${done} of these batches ${done === 1 ? "is" : "are"} already recorded and complete. `
|
|
211
|
+
+ `Outstanding: batch ${todo.join(", ")}. Do not re-read or re-send the batches that are done.`, "");
|
|
212
|
+
else lines.push(`Outstanding: every batch, 1 to ${plan.length}.`, "");
|
|
213
|
+
return lines.join("\n");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* What this run's digest still owes, read off the stored model. `{ armed, owed, accounted, unaccounted }`.
|
|
218
|
+
*
|
|
219
|
+
* THE EXIT GATE'S READ. Before batching, a digest that ended nothing failed because no document was
|
|
220
|
+
* ever written — the refusal on the last call was the gate. Once a batch call is accepted, the document
|
|
221
|
+
* EXISTS from batch 1 onwards, so that failure stops firing, and a seat that stopped after batch 6 would
|
|
222
|
+
* ship a document missing half the band with nothing refusing it. That inversion is what this closes,
|
|
223
|
+
* and it is armed by the same era stamp as the call-time refusal so archived runs are judged as they
|
|
224
|
+
* always were.
|
|
225
|
+
*/
|
|
226
|
+
export function digestAccountingGap(runDir) {
|
|
227
|
+
const armed = accountingArmed(runDir);
|
|
228
|
+
if (!armed) return { armed: false, owed: [], accounted: [], unaccounted: [] };
|
|
229
|
+
const facts = readDigestFacts(runDir);
|
|
230
|
+
if (!Array.isArray(facts.owed)) return { armed: true, owed: null, accounted: [], unaccounted: null };
|
|
231
|
+
const model = lastAcceptedModel(runDir);
|
|
232
|
+
if (!model) return { armed: true, owed: facts.owed, accounted: [], unaccounted: facts.owed, no_model: true };
|
|
233
|
+
const accounted = accountedUris(model, facts.owed);
|
|
234
|
+
return { armed: true, owed: facts.owed, accounted: [...accounted],
|
|
235
|
+
unaccounted: facts.owed.filter((k) => !accounted.has(k)) };
|
|
236
|
+
}
|
|
237
|
+
|
|
91
238
|
/**
|
|
92
239
|
* The document's section headings, EXPORTED because three separate readers key on them and a heading
|
|
93
240
|
* changed here without changing them is the failure this constant exists to make impossible.
|
|
@@ -597,6 +744,10 @@ export function acceptRegisterDigest(params, facts = emptyFacts()) {
|
|
|
597
744
|
open_flags: str(params?.open_flags),
|
|
598
745
|
instructed_checks,
|
|
599
746
|
disagreement_resolutions,
|
|
747
|
+
// WRITTEN ONLY WHEN THERE IS ONE, so a run that never batched stores the model it always stored and
|
|
748
|
+
// an archived model replays byte-identical. It is the accumulator's own bookkeeping and no renderer
|
|
749
|
+
// reads it: `renderRegisterFindings` takes the keys it names and ignores the rest.
|
|
750
|
+
...(params?.batch_of && Object.keys(params.batch_of).length ? { batch_of: params.batch_of } : {}),
|
|
600
751
|
};
|
|
601
752
|
|
|
602
753
|
// ASK WHAT THE ZERO MEANS. A digest that surfaced nothing AND dropped nothing has not judged the
|
|
@@ -610,16 +761,39 @@ export function acceptRegisterDigest(params, facts = emptyFacts()) {
|
|
|
610
761
|
// Disagreement-resolutions row. Nothing new is invented here — the join is over lists this call
|
|
611
762
|
// already carries.
|
|
612
763
|
if (facts.armed) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
764
|
+
// A RECORD ENDED TWICE IS ANSWERED BEFORE A RECORD ENDED NOWHERE. The ledger is the driver's
|
|
765
|
+
// (batchLedger, from the raw call), so this reads a decision rather than making one.
|
|
766
|
+
const doubled = Array.isArray(params?.batch_doubled) ? params.batch_doubled : [];
|
|
767
|
+
if (doubled.length) {
|
|
768
|
+
const show = doubled.slice(0, 5).map((d) => `${d.uri} (batch ${d.was}, again in batch ${d.now})`).join("; ");
|
|
769
|
+
return { ok: false, reason: `registerdigest_double_counted:${doubled.length} record(s) this call ends were already ended by another batch: ${show}${doubled.length > 5 ? ` (+${doubled.length - 5} more)` : ""}. Each record ends exactly once, in its own batch — re-send the batch that owns it if the ending was wrong, and drop it from this one` };
|
|
770
|
+
}
|
|
771
|
+
const accounted = accountedUris(model, facts.owed);
|
|
772
|
+
// ── THE SCOPE IS THE BATCH WHEN THE CALL NAMES ONE, AND THE WHOLE OWED SET WHEN IT DOES NOT ─────
|
|
773
|
+
//
|
|
774
|
+
// Both arms use the same accounted set and the same three exits; only the population moves. A call
|
|
775
|
+
// naming no batch is judged exactly as it was before batching existed, which is what every rung
|
|
776
|
+
// that re-sends a complete document depends on.
|
|
777
|
+
const batch = Number.isInteger(params?.batch) && params.batch >= 1 ? params.batch : null;
|
|
778
|
+
let scope = facts.owed, where = "this run carried into the digest";
|
|
779
|
+
if (batch !== null) {
|
|
780
|
+
const plan = batchesOf(facts.owed);
|
|
781
|
+
const slice = plan[batch - 1];
|
|
782
|
+
if (!slice) {
|
|
783
|
+
return { ok: false, reason: `registerdigest_batch_unknown:batch ${batch} — this run's band splits into ${plan.length} batch(es) of up to ${DIGEST_BATCH_RECORDS} records, so there is no batch ${batch} to record. The dispatch names the batch count; send the batches it lists` };
|
|
784
|
+
}
|
|
785
|
+
scope = slice;
|
|
786
|
+
where = `batch ${batch} of ${plan.length} carried`;
|
|
787
|
+
}
|
|
788
|
+
const unaccounted = scope.filter((k) => !accounted.has(k));
|
|
620
789
|
if (unaccounted.length) {
|
|
621
|
-
|
|
622
|
-
|
|
790
|
+
// THE REFUSAL IS THE WORK LIST. It used to show five of them and leave the seat to infer the rest,
|
|
791
|
+
// which is what made "retry the digest" the only move it could read off the refusal. A batch is at
|
|
792
|
+
// most DIGEST_BATCH_RECORDS records, so its outstanding set is quotable in full and the seat can
|
|
793
|
+
// act on THIS refusal without re-reading the band.
|
|
794
|
+
const cap = batch !== null ? DIGEST_BATCH_RECORDS : 5;
|
|
795
|
+
const show = unaccounted.slice(0, cap).join(", ");
|
|
796
|
+
return { ok: false, reason: `registerdigest_unaccounted_records:${unaccounted.length} of ${scope.length} record(s) ${where} into the digest end nowhere — neither a findings row, nor a Negative-results drop, nor a Disagreement resolution: ${show}${unaccounted.length > cap ? ` (+${unaccounted.length - cap} more)` : ""}. Each needs one of the three, and a drop needs its ground token. Send ONLY these — everything you have already recorded is kept` };
|
|
623
797
|
}
|
|
624
798
|
}
|
|
625
799
|
|
|
@@ -652,7 +826,20 @@ export function recordRegisterDigest(runDir, received, { facts = null, now = ()
|
|
|
652
826
|
|
|
653
827
|
// A PATCH call merges onto the stored model BEFORE acceptance, so the whole document is validated
|
|
654
828
|
// as one thing every time — a patch cannot slip a row past a check by arriving alone.
|
|
655
|
-
|
|
829
|
+
//
|
|
830
|
+
// A BATCH CALL IS A PATCH, AND IT IS NOT OPTIONAL THAT IT IS. Batches accumulate by definition: if
|
|
831
|
+
// batch 2 replaced the stored model rather than merging onto it, batch 1's rows would leave the
|
|
832
|
+
// document the moment batch 2 was accepted, and the run would reach delivery a batch short with every
|
|
833
|
+
// call reading as accepted. The batch ledger travels the same way — read off the stored model, so a
|
|
834
|
+
// resume that re-enters the stage carries what the killed attempt had already accounted for.
|
|
835
|
+
const batching = Number.isInteger(received?.batch) && received.batch >= 1;
|
|
836
|
+
const accumulates = received?.patch === true || batching;
|
|
837
|
+
const stored = accumulates ? lastAcceptedModel(runDir) : null;
|
|
838
|
+
const merged = accumulates ? mergeDigestPatch(stored, received) : received;
|
|
839
|
+
const ledger = batchLedger(stored?.batch_of, received);
|
|
840
|
+
const params = batching
|
|
841
|
+
? { ...merged, batch: received.batch, batch_of: ledger.batchOf, batch_doubled: ledger.doubled }
|
|
842
|
+
: merged;
|
|
656
843
|
const verdict = acceptRegisterDigest(params, facts ?? readDigestFacts(runDir));
|
|
657
844
|
if (!verdict.ok) {
|
|
658
845
|
try { writeFileSync(paths.refusals, `${JSON.stringify({ at: now(), reason: verdict.reason })}\n`, { flag: "a" }); }
|
|
@@ -682,8 +869,46 @@ export function recordRegisterDigest(runDir, received, { facts = null, now = ()
|
|
|
682
869
|
const at = join(String(runDir ?? ""), FINDINGS_FILE);
|
|
683
870
|
// The model lands BEFORE the document: a later patch merges onto what was accepted, so a write that
|
|
684
871
|
// fails must not leave a stored model describing a document nobody has.
|
|
872
|
+
//
|
|
873
|
+
// ── AND UNDER BATCHING THE MODEL IS THE ACCUMULATOR, SO LOSING IT IS FATAL HERE ──────────────────
|
|
874
|
+
//
|
|
875
|
+
// Best-effort was right while one call carried the whole document: a lost model cost the next patch
|
|
876
|
+
// its base and said so by refusing. It is wrong once the batches ARE the document. Lose it after
|
|
877
|
+
// batch 3 and batch 4 merges onto nothing, batch 3's records stop being accounted, the union never
|
|
878
|
+
// closes, and the run ends refusing over records the seat accounted for correctly twenty minutes
|
|
879
|
+
// earlier — a loop with a true-looking refusal at the end of it.
|
|
880
|
+
let modelWriteFailed = null;
|
|
685
881
|
try { writeFileSync(paths.model, JSON.stringify(verdict.model, null, 2) + "\n"); }
|
|
686
|
-
catch {
|
|
882
|
+
catch (e) { modelWriteFailed = String(e?.message ?? e).slice(0, 200); }
|
|
883
|
+
if (modelWriteFailed && Number.isInteger(received?.batch)) {
|
|
884
|
+
const reason = `registerdigest_model_write_failed:this run accounts for its records in batches and the driver could not store what this batch accepted (${modelWriteFailed}), so the next batch would merge onto a base missing these rows and the run would refuse over records you have already accounted for (driver-written: this is a bug, not a model defect, and re-stating the call cannot fix it)`;
|
|
885
|
+
try { writeFileSync(paths.refusals, `${JSON.stringify({ at: now(), reason })}\n`, { flag: "a" }); }
|
|
886
|
+
catch { /* the refusal record is best-effort; the refusal itself is returned regardless */ }
|
|
887
|
+
return { written: null, refused: reason, captured: closeCapture({ ok: false, refused: reason }), capture_failed: captureFailed };
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// ── THE CLIENT'S DOCUMENT STAYS ALL-OR-NOTHING WHILE BATCHES ARE OUTSTANDING ─────────────────────
|
|
891
|
+
//
|
|
892
|
+
// `register-findings.md` is read by nine parsers, by the gateway through `toolWrittenArtifact` to
|
|
893
|
+
// decide whether this stage produced anything at all, and by a lawyer. Writing it on each accepted
|
|
894
|
+
// batch puts a page on disk carrying the title, every heading and a third of the records — complete
|
|
895
|
+
// to every one of those readers. The exit gate in `validators.registerFindings` refuses such a
|
|
896
|
+
// document, which catches a seat that stops half way; it catches it AFTER the partial page exists.
|
|
897
|
+
//
|
|
898
|
+
// So while any owed record is still outstanding the batch accumulates into the MODEL, which is the
|
|
899
|
+
// driver's own state, and the document is rendered on the call that closes the union. A run that dies
|
|
900
|
+
// at batch 6 then leaves no document at all — exactly what an unfinished digest has always left, so
|
|
901
|
+
// the older "no document" stage failure keeps working unchanged and the exit gate becomes the second
|
|
902
|
+
// lock rather than the only one.
|
|
903
|
+
const gap = digestAccountingGap(runDir);
|
|
904
|
+
if (Array.isArray(gap.unaccounted) && gap.unaccounted.length) {
|
|
905
|
+
return {
|
|
906
|
+
written: null, refused: null, batch_accepted: received?.batch ?? null, remaining: gap.unaccounted.length,
|
|
907
|
+
accounted: gap.accounted.length,
|
|
908
|
+
captured: closeCapture({ ok: true, batch_accepted: received?.batch ?? null, remaining: gap.unaccounted.length }),
|
|
909
|
+
capture_failed: captureFailed,
|
|
910
|
+
};
|
|
911
|
+
}
|
|
687
912
|
try { writeFileSync(at, verdict.content); }
|
|
688
913
|
catch (e) {
|
|
689
914
|
return { written: null, refused: null, write_failed: String(e?.message ?? e).slice(0, 200),
|
|
@@ -69,5 +69,5 @@ export function grantVocabularySentence(provider = null) {
|
|
|
69
69
|
+ `whole, and the frozen plan's entries are fetched by the executor, not by you. So reaching for one `
|
|
70
70
|
+ `of these means the query is wrong, not that a capability is missing: go back to the sweep and fix `
|
|
71
71
|
+ `its scope. This list is the tools THIS provider serves; another deployment's differs, and `
|
|
72
|
-
+ `\`skills/
|
|
72
|
+
+ `\`skills/clearance-register/providers/<name>.md\` is where the provider-specific vocabulary lives.`;
|
|
73
73
|
}
|