clearotron 0.4.0-beta.0 → 0.4.0-beta.2
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 +9 -0
- package/INSTALL.md +4 -31
- package/README.md +5 -5
- package/bin/brandowner.mjs +1 -1
- package/bin/onboard.mjs +18 -74
- package/bin/start.mjs +58 -14
- package/bin/update.mjs +10 -2
- package/build-info.json +2 -2
- package/demo/full-country-search/run/_driver/search-policy.json +1 -1
- package/demo/full-country-search/run/status.json +1 -1
- package/demo/global-preliminary-search/run/_driver/search-policy.json +1 -1
- package/demo/global-preliminary-search/run/status.json +1 -1
- package/demo/knockout-search/run/_driver/search-policy.json +1 -1
- package/demo/knockout-search/run/status.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/search-policy.json +1 -1
- package/demo/multi-country-focus-search/run/status.json +1 -1
- package/docs/README.md +1 -0
- package/docs/SECURITY-OWASP.md +298 -0
- package/docs/SECURITY.md +18 -13
- package/docs/architecture/02-architecture.md +5 -5
- package/docs/architecture/03-run-lifecycle.md +28 -29
- package/docs/architecture/04-configuration-reference.md +5 -10
- package/docs/architecture/05-config-governance.md +5 -7
- package/docs/architecture/06-operations-runbook.md +0 -12
- package/docs/architecture/07-quality-and-audit.md +12 -13
- package/docs/architecture/09-security-and-data.md +9 -4
- package/driver/CHANGELOG.md +70 -0
- package/driver/ask-ledger.mjs +4 -2
- package/driver/authority-trees.mjs +90 -8
- package/driver/band-shape.mjs +13 -13
- package/driver/blind-frame-model.mjs +1 -1
- package/driver/case-law-ledger.mjs +19 -3
- package/driver/claim-liveness.mjs +30 -1
- package/driver/clearance-variants-record.mjs +10 -3
- package/driver/common-law-receipts.mjs +64 -14
- package/driver/commonlaw-carry.mjs +2 -2
- package/driver/compare.mjs +2 -2
- package/driver/connotation-search.mjs +33 -104
- package/driver/contract-arm2-baseline.json +6 -5
- package/driver/contract-e3-backlog.mjs +62 -62
- package/driver/contract-vocabulary.mjs +38 -31
- package/driver/coverage-form-io.mjs +13 -1
- package/driver/coverage-form.mjs +42 -4
- package/driver/coverage-ledger.mjs +3 -5
- package/driver/cross-check-wait.mjs +43 -0
- package/driver/declination-call.mjs +34 -10
- package/driver/declination-tool.mjs +27 -15
- package/driver/deferral-row.mjs +139 -0
- package/driver/degraded-parts.mjs +377 -0
- package/driver/deliver-trigger.sh +1 -1
- package/driver/demo-container.mjs +4 -1
- package/driver/dev-portal.mjs +5 -5
- package/driver/dispatch-record.mjs +2 -2
- package/driver/driver.config.mjs +193 -96
- package/driver/e2e/README.md +1 -1
- package/driver/effective-scope.mjs +58 -2
- package/driver/engine/anthropic-agent.mjs +103 -27
- package/driver/engine/auth.mjs +2 -2
- package/driver/engine/child-record.mjs +11 -0
- package/driver/engine/cli-version.mjs +4 -3
- package/driver/engine/common.mjs +17 -2
- package/driver/engine/deny-authority-write.mjs +3 -1
- package/driver/engine/engine-env.mjs +174 -0
- package/driver/engine/engine-spawn.mjs +179 -0
- package/driver/engine/mcp/band-server.mjs +1 -1
- package/driver/engine/mcp/clarivate-server.mjs +2 -1
- package/driver/engine/mcp/codex-config.mjs +112 -3
- package/driver/engine/mcp/corsearch-server.mjs +1 -0
- package/driver/engine/mcp/declination-server.mjs +9 -8
- package/driver/engine/mcp/dispositions-server.mjs +2 -1
- package/driver/engine/mcp/euipo-server.mjs +1 -0
- package/driver/engine/mcp/fetch-server.mjs +14 -11
- package/driver/engine/mcp/free-tier-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +23 -11
- package/driver/engine/mcp/perplexity-server.mjs +97 -17
- package/driver/engine/mcp/probe-server.mjs +43 -21
- package/driver/engine/mcp/public-fetch.mjs +195 -0
- package/driver/engine/mcp/recording-server.mjs +79 -14
- package/driver/engine/mcp/signa-server.mjs +1 -0
- package/driver/engine/mcp/supplemental.mjs +14 -14
- package/driver/engine/mcp/unit-note-server.mjs +50 -1
- package/driver/engine/mcp/uspto-local-server.mjs +1 -0
- package/driver/engine/openai-agent.mjs +109 -28
- package/driver/engine/probe.mjs +152 -19
- package/driver/enqueue-schema.mjs +5 -5
- package/driver/feedback-issues.mjs +1 -1
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +67 -16
- package/driver/flag-snapshot.mjs +1 -1
- package/driver/floor-duty.mjs +23 -7
- package/driver/form-neighbourhood.mjs +52 -17
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/framework-method.mjs +304 -0
- package/driver/gateway.mjs +49 -21
- package/driver/hand-off-exits.mjs +129 -0
- package/driver/jx-lanes.mjs +1 -1
- package/driver/jx.mjs +8 -5
- package/driver/knockout-assess-record.mjs +7 -1
- package/driver/knockout-frame-record.mjs +96 -34
- package/driver/knockout-review-record.mjs +7 -2
- package/driver/log.mjs +2 -2
- package/driver/matter-frame-record.mjs +31 -2
- package/driver/methodology-witness.mjs +6 -3
- package/driver/named-band.mjs +1 -1
- package/driver/owner-use-check.mjs +45 -10
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +4 -0
- package/driver/phase0.mjs +2 -2
- package/driver/pipeline-knockout.mjs +256 -129
- package/driver/pipeline.mjs +417 -614
- package/driver/placement-carry.mjs +22 -3
- package/driver/placement-form-io.mjs +18 -4
- package/driver/placement-form.mjs +53 -4
- package/driver/placement-union.mjs +32 -5
- package/driver/portal-report.mjs +3 -3
- package/driver/portal-service.mjs +6 -5
- package/driver/portal-static.mjs +11 -18
- package/driver/predelivery-lint.mjs +10 -10
- package/driver/profile-page.html +5 -5
- package/driver/profiles.mjs +5 -5
- package/driver/progress.mjs +6 -4
- package/driver/provider-usage.mjs +25 -2
- package/driver/publish/index.mjs +97 -25
- package/driver/publish/knockout.mjs +136 -14
- package/driver/publish/profiles-page.mjs +2 -0
- package/driver/publish/publish-inputs.mjs +10 -4
- package/driver/publish/render-knockout.mjs +48 -21
- package/driver/publish/render.mjs +13 -8
- package/driver/publish/report-data.mjs +2 -0
- package/driver/publish/search-depth.mjs +77 -4
- package/driver/publish/templates/report.css +1 -1
- package/driver/publish/xlsx.mjs +34 -15
- package/driver/reasoning-tripwires.mjs +2 -160
- package/driver/recall-receipt.mjs +27 -0
- package/driver/recall-reconciliation.mjs +1 -1
- package/driver/record-carry.mjs +14 -14
- package/driver/recording-agreement.mjs +2 -2
- package/driver/reference-score.mjs +169 -47
- package/driver/register-count.mjs +61 -4
- package/driver/register-digest-record.mjs +14 -1
- package/driver/register-plan.mjs +161 -80
- package/driver/register-records.mjs +37 -4
- package/driver/registration-scripts.mjs +22 -0
- package/driver/registry-fidelity.mjs +4 -4
- package/driver/repair-composers.mjs +23 -7
- package/driver/repair-contract.mjs +1 -1
- package/driver/repairs.mjs +5 -2
- package/driver/reviewer-open-points.mjs +33 -0
- package/driver/rule-shape.mjs +8 -8
- package/driver/run-economics.mjs +3 -3
- package/driver/run-integrity.mjs +2 -2
- package/driver/runner.mjs +17 -5
- package/driver/scope-facts.mjs +89 -12
- package/driver/score-redaction.mjs +247 -0
- package/driver/screen-gate.mjs +1 -1
- package/driver/search-policy.mjs +17 -0
- package/driver/skills/README.md +2 -2
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/clearance-common-law/SKILL.md +29 -36
- package/driver/skills/clearance-common-law/perplexity-prompts.md +8 -30
- package/driver/skills/clearance-register/SKILL.md +15 -16
- package/driver/skills/clearance-register/digest.md +15 -31
- package/driver/skills/clearance-register/register-recipes.md +14 -56
- package/driver/skills/clearance-register/unit.md +37 -48
- package/driver/skills/clearance-search/SKILL.md +6 -8
- package/driver/skills/clearance-search/delivery-contract.md +0 -7
- package/driver/skills/clearance-search/firm-wide-reasoning.md +5 -4
- package/driver/skills/clearance-search/phase2-execution.md +5 -6
- package/driver/skills/clearance-search/report-prose.md +5 -5
- package/driver/skills/clearance-search/risk-framework-triage.md +2 -2
- package/driver/skills/clearance-search/synthesis-rules.md +9 -9
- package/driver/skills/clearance-search/template-formatting.md +2 -2
- package/driver/skills/clearance-variants/SKILL.md +8 -8
- package/driver/skills/clearance-variants/transliteration-scripts.md +2 -2
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/skills/knockout-assess/SKILL.md +40 -10
- package/driver/skills/knockout-frame/SKILL.md +42 -10
- package/driver/skills/matter-frame/SKILL.md +12 -5
- package/driver/skills/matter-frame/watchlist-reference.md +1 -1
- package/driver/skills/narrative-refutation/SKILL.md +5 -3
- package/driver/skills/placement-inquiry/SKILL.md +5 -1
- package/driver/stage-context.mjs +27 -6
- package/driver/stages-knockout.mjs +92 -79
- package/driver/stages.mjs +111 -68
- package/driver/stray-artifacts.mjs +9 -5
- package/driver/suite-census.json +696 -150
- package/driver/synthesis-record.mjs +26 -4
- package/driver/systemd/README.md +5 -6
- package/driver/systemd/clearotron-client-mcp.service +24 -0
- package/driver/systemd/clearotron-mcp-face.service +24 -0
- package/driver/systemd/clearotron-portal.service +24 -0
- package/driver/systemd/clearotron-worker.service +29 -0
- package/driver/tokens.mjs +10 -9
- package/driver/turnaround-bands.mjs +1 -1
- package/driver/unit-inventory.mjs +24 -43
- package/driver/variant-manifest-model.mjs +22 -3
- package/driver/verify-knockout.mjs +163 -4
- package/driver/verify.mjs +42 -16
- package/driver/web-grid.mjs +150 -0
- package/driver/whatif-memo-run.mjs +1 -1
- package/driver/withheld-families.mjs +113 -4
- package/driver/worker-heartbeat.mjs +13 -2
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/lib/ops.mjs +10 -10
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/trace.mjs +3 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +3 -3
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-DVtz44vH.js → index-BPAUjcI0.js} +3 -2
- package/portal-ui/dist/assets/{index-5CCwiJG7.css → index-D2wrw9cH.css} +18 -2
- package/portal-ui/dist/assets/plus-jakarta-sans-BUCHxqJ-.woff2 +0 -0
- package/portal-ui/dist/index.html +2 -15
- package/portal-ui/package.json +1 -1
- package/providers/_shared/README.md +1 -1
- package/providers/_shared/answer-memory.mjs +199 -0
- package/providers/_shared/enumerate.mjs +155 -50
- package/providers/_shared/execute-plan.mjs +62 -16
- package/providers/_shared/ledger-path.mjs +1 -1
- package/providers/_shared/ledger.mjs +48 -6
- package/providers/_shared/plan-guards.mjs +7 -0
- package/providers/_shared/script-form.mjs +24 -5
- package/providers/_shared/term-shape.mjs +5 -5
- package/providers/clarivate/src/capabilities.js +11 -0
- package/providers/clarivate/src/core.js +193 -18
- package/providers/corsearch/README.md +1 -2
- package/providers/corsearch/src/core.js +2 -2
- package/providers/jx-subclass/lookup.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/README.md +8 -7
- package/providers/oauth-mcp-bridge/bridge.mjs +21 -5
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
- package/providers/oauth-mcp-bridge/warm-server.mjs +16 -5
- package/providers/perplexity/src/core.js +246 -21
- package/providers/signa/src/capabilities.js +24 -0
- package/providers/signa/src/core.js +93 -25
- package/scripts/README.md +2 -1
- package/scripts/added-reference-check.mjs +2 -1
- package/scripts/backup-recall-stores.mjs +5 -5
- package/scripts/demo-evidence.mjs +2 -1
- package/scripts/deprecate-below.mjs +114 -2
- package/scripts/drive-env-check.mjs +2 -1
- package/scripts/e2e-first-time.mjs +469 -0
- package/scripts/e2e-scenario-ops.mjs +441 -0
- package/scripts/e2e.mjs +104 -16
- package/scripts/env-audit.mjs +8 -0
- package/scripts/freeze-example-run.mjs +7 -7
- package/scripts/generated-files-are-current.mjs +2 -1
- package/scripts/hand-off-exits-probe.mjs +75 -0
- package/scripts/import-cycle-check.mjs +2 -1
- package/scripts/merge-shape-check.mjs +2 -1
- package/scripts/package-size-budget.mjs +2 -1
- package/scripts/record-carry-probe.mjs +1 -1
- package/scripts/release-entry-catch-up.mjs +77 -8
- package/scripts/release-install-check.mjs +4 -1
- package/scripts/release-note-required.mjs +139 -9
- package/scripts/release-rehearsal-version.mjs +60 -0
- package/scripts/release-sbom.mjs +104 -0
- package/scripts/release-visible-check.mjs +7 -5
- package/scripts/render-check.mjs +5 -2
- package/scripts/report-offline-render-check.mjs +169 -0
- package/scripts/score.mjs +79 -8
- package/scripts/strip-titles-and-attributions.mjs +2 -1
- package/scripts/strip-tracker-citations.mjs +2 -1
- package/scripts/test-full.mjs +2 -1
- package/scripts/test-run.mjs +10 -3
- package/scripts/third-party-notices.mjs +3 -1
- package/scripts/travelling-predicates.mjs +1 -1
- package/scripts/writing-standard-check.mjs +2 -1
- package/shared/brand-fonts.mjs +59 -0
- package/shared/brand.mjs +6 -6
- package/shared/browser-temp-root.mjs +3 -2
- package/shared/doctrine-overlay.mjs +1 -1
- package/shared/driver-dir.mjs +47 -11
- package/shared/fonts/OFL-fira-code.txt +93 -0
- package/shared/fonts/OFL-plus-jakarta-sans.txt +93 -0
- package/shared/fonts/README.md +37 -0
- package/shared/fonts/fira-code.woff2 +0 -0
- package/shared/fonts/plus-jakarta-sans.woff2 +0 -0
- package/shared/names-in-force.mjs +2 -0
- package/shared/npm-cli.mjs +23 -0
- package/shared/os-advice.mjs +14 -2
- package/shared/path-seps.mjs +42 -0
- package/shared/process-table.mjs +71 -3
- package/shared/reference-guard-classes.mjs +30 -6
- package/shared/root-doc-commands.mjs +6 -2
- package/shared/running-start.mjs +24 -7
- package/shared/scope.mjs +2 -2
- package/shared/wsl.mjs +1 -1
- package/driver/known-conflicts.mjs +0 -327
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// deferral-row.mjs — the row a reader gets for anything left open this run, and the words its reasons
|
|
5
|
+
// are given in. Moved out of pipeline.mjs unchanged so that publishing and the audit workbook can reuse the
|
|
6
|
+
// shipped words without importing the pipeline. A pure leaf: no imports at all.
|
|
7
|
+
|
|
8
|
+
// wp50/wi9 — machine directives reworded at the authoring point: "frame-reopen / source:who inn list…"
|
|
9
|
+
// with note "deferred this run — mechanical-fail:timeout" is machinery-speak on a lawyer's report.
|
|
10
|
+
export const plainDirective = (s) => String(s ?? "").replace(/\b(source|variant|field|axis):\s*/gi, "").replace(/\s+/g, " ").trim();
|
|
11
|
+
// ── — TRANSLATE AT THE BOUNDARY, NEVER FILTER ──────────────────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// This switch is a CODE-MINTED, CODE-ENUMERATED set of reason prefixes, not a dictionary. Every token
|
|
14
|
+
// it matches is minted by this same file (`regDeferReason`) and written to _driver/frame-reopen.json;
|
|
15
|
+
// none of it is ever authored by a model, and none of it comes from client prose or the register. That is the whole distinction drew — the `mechanical-fail:*` arms below
|
|
16
|
+
// have sat here since wp50/wi9 and nobody called them a ban list, because a switch over your own
|
|
17
|
+
// output is not a find-and-replace over somebody else's.
|
|
18
|
+
//
|
|
19
|
+
// DROPPING THE SENTENCE IS NOT AN OPTION. Adding `no-code-remedy:` to parse.mjs's ENGINE_INTERNAL_RE
|
|
20
|
+
// would delete the whole sentence (that filter drops per sentence) and with it the disclosure the row
|
|
21
|
+
// exists to make — injectDeferralCoverage's own contract is that "the reader-visible disclosure is the
|
|
22
|
+
// whole point of 'can't close → disclose'". A reader who is told nothing about an unsearched slice is
|
|
23
|
+
// worse off than one told about it in engine words. So: translate, never filter.
|
|
24
|
+
//
|
|
25
|
+
// The engine wording is KEPT on the internal record — regTermRows' `dispatch_reason` and the
|
|
26
|
+
// frame-reopen.json `reason` are unchanged, so remedy-accounting and the audit still read the token.
|
|
27
|
+
export const plainDeferralReason = (r) => {
|
|
28
|
+
const t = String(r ?? "unclosed").trim();
|
|
29
|
+
if (/mechanical-fail:timeout/i.test(t)) return "the source timed out this run";
|
|
30
|
+
if (/mechanical-fail:(.+)/i.test(t)) return `a tool failed this run (${t.replace(/^mechanical-fail:/i, "")})`;
|
|
31
|
+
if (/^no-code-remedy:/i.test(t)) {
|
|
32
|
+
// The two forms regDeferReason mints, in the reader's words. Both must still say the slice was NOT
|
|
33
|
+
// searched and why — a translation that loses that has filtered the disclosure by other means.
|
|
34
|
+
if (/class-gap/i.test(t))
|
|
35
|
+
return "no search could be built for it: the gap names classes but no searchable name, and re-running the matter's own classes would only repeat the main sweep, so it is left open here rather than reported as clean";
|
|
36
|
+
if (/\blabel\b/i.test(t))
|
|
37
|
+
return "no search could be built for it: the item names a category rather than a name a register can be searched for, so searching the wording literally would return nothing and read as clean, and it is left open here";
|
|
38
|
+
return "no search could be built for it this run, so it is left open here rather than reported as clean";
|
|
39
|
+
}
|
|
40
|
+
// ── — THE ARMS DID NOT REACH, AND A FALLBACK THAT CANNOT LEAK ────────────────────────
|
|
41
|
+
//
|
|
42
|
+
// built this switch for the two families it had met and left `return t` under them, so every
|
|
43
|
+
// OTHER token `regDeferReason` and `partitionFiring` mint reached the coverage row verbatim. The
|
|
44
|
+
// worst shape is a hash-bearing one — `slice-not-landed:supp:<axis>:<predicate>:<term>:<hash>` — which
|
|
45
|
+
// is not a sentence anybody can act on, and the row it lands in is one a reader relies on to tell a
|
|
46
|
+
// searched slice from an unsearched one.
|
|
47
|
+
//
|
|
48
|
+
// THIS IS THE ONLY PLACE IT CAN BE REPAIRED. The portal's embed strip passes coverage rows through
|
|
49
|
+
// untouched, so whatever this function returns is what the served page carries — there is no second
|
|
50
|
+
// surface downstream that could catch a token this one let past.
|
|
51
|
+
if (/^no-resumable-session\b/i.test(t))
|
|
52
|
+
return "the earlier search session could not be resumed to finish it, so the slice was left open rather than reported as clean";
|
|
53
|
+
if (/^unchanged-after-resume\b/i.test(t))
|
|
54
|
+
return "it was re-run and the result did not change, so it stands as it was — open, not cleared";
|
|
55
|
+
if (/^not-verified-closed\b/i.test(t))
|
|
56
|
+
return "nothing in the run's own record confirms it was searched, so it is left open rather than assumed done";
|
|
57
|
+
if (/^source-not-swept\b/i.test(t))
|
|
58
|
+
return "the channel it needed was never swept this run, so nothing is known about it either way";
|
|
59
|
+
if (/^digest-locked-resume\b/i.test(t))
|
|
60
|
+
return "the earlier pass was still holding the record when the re-run was attempted, so it could not be completed this run";
|
|
61
|
+
if (/^resume-arm-unverifiable\b/i.test(t))
|
|
62
|
+
return "the re-run left no evidence of what it actually searched, so it cannot be treated as having closed this — disclosed rather than assumed clean";
|
|
63
|
+
if (/^proposals-rejected\b/i.test(t))
|
|
64
|
+
return "the searches proposed for it could not be put to this register, so it was never searched";
|
|
65
|
+
if (/^slice-not-landed\b/i.test(t))
|
|
66
|
+
return "the search for it was planned and never reached the register, so nothing is known about it either way";
|
|
67
|
+
if (/^redigest-fail\b/i.test(t))
|
|
68
|
+
return "the re-read of the results failed this run, so what it holds was never established";
|
|
69
|
+
// THE FALLBACK IS THE FIX, and the arms above are the courtesy.
|
|
70
|
+
//
|
|
71
|
+
// `return t` was the defect: it made every UNKNOWN token a rendered string, so the leak was not
|
|
72
|
+
// the nine arms above — it was that the set is open and the default published whatever it did not
|
|
73
|
+
// recognise. A tenth token minted tomorrow would have leaked on the day it was minted. This says the
|
|
74
|
+
// one thing every branch here must say — the slice was not completed and is not clean — and says it
|
|
75
|
+
// without the token.
|
|
76
|
+
//
|
|
77
|
+
// AND IT DROPS NOTHING PROTECTED. Its ruling is "translate, never filter", because a reader told
|
|
78
|
+
// nothing about an unsearched slice is worse off than one told in engine words. The row still renders,
|
|
79
|
+
// still says it was not completed, and still reads as open. The engine wording is untouched on the
|
|
80
|
+
// internal record — `regTermRows`' `dispatch_reason` and `frame-reopen.json`'s `reason` — exactly as
|
|
81
|
+
// that ruling stipulates, so remedy-accounting and the audit still read the token.
|
|
82
|
+
return "it could not be completed this run, so it is left open here rather than reported as clean";
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A RAW CAUSE, IN THE READER'S WORDS. A lookup or a listing that failed carries the provider's own text —
|
|
87
|
+
* an exception's message, a status line — and that text belongs to the run's record, never to a client's
|
|
88
|
+
* cell. A timeout reads as the timeout line and anything else as the line for a search left open; the
|
|
89
|
+
* cause itself stays where it was written.
|
|
90
|
+
*/
|
|
91
|
+
export const plainCause = (cause) =>
|
|
92
|
+
plainDeferralReason(/timeout|timed out/i.test(String(cause ?? "")) ? "mechanical-fail:timeout" : "unfinished");
|
|
93
|
+
|
|
94
|
+
// ── — A HEADING IS NOT THE PLACE FOR A DIRECTIVE ───────────────────────────────────────────────
|
|
95
|
+
//
|
|
96
|
+
// What shipped: `Follow-up / ${plainDirective(directive).slice(0, 60)}` — a hard character cut with no
|
|
97
|
+
// word boundary and no ellipsis. Two delivered specimens in demo:
|
|
98
|
+
// "Follow-up / consumer and prosumer water testing (pool, spa, aquarium, ho" (mid-"home")
|
|
99
|
+
// "Follow-up / agri-tech, irrigation, aquaculture and hydrology monitoring " (mid-"software", and
|
|
100
|
+
// a trailing space)
|
|
101
|
+
// That string is a CLIENT HEADING — render.mjs's coverageGrid prints it under "What we covered — and
|
|
102
|
+
// what's open", and it rides verbatim into report-data.json.
|
|
103
|
+
//
|
|
104
|
+
// THE FIX IS HERE, NOT IN THE RENDERER, deliberately. coverageGrid carries the ruling that
|
|
105
|
+
// "NOTHING IS SUBSTITUTED HERE ANY MORE — `area` and `note` ride verbatim". Clipping in the renderer
|
|
106
|
+
// would re-open substitution over a client string at exactly the seam that ruling closed. The heading
|
|
107
|
+
// is wrong where it is MINTED, so it is fixed where it is minted.
|
|
108
|
+
//
|
|
109
|
+
// A smarter truncation would still be a truncation. The real defect is that a whole search directive
|
|
110
|
+
// was being asked to serve as a heading. So the heading gets a short, word-boundary-cut label with an
|
|
111
|
+
// explicit "…" saying it was shortened, and the FULL directive moves into `note`, which has room for
|
|
112
|
+
// it. Nothing is lost from the reader's view; it moves to where it fits.
|
|
113
|
+
//
|
|
114
|
+
// "Follow-up / " STAYS LITERAL. coverage-ledger.coverageUnitLabel keys label suppression off that exact
|
|
115
|
+
// shape ("a driver-authored plain-English area … must not be touched"), so only what follows it moves.
|
|
116
|
+
const DEFERRAL_AREA_ITEM_BUDGET = 48; // characters after "Follow-up / " — a heading, not a sentence
|
|
117
|
+
|
|
118
|
+
/** Cut `s` to at most `max` characters at a WORD boundary, marking the cut with an explicit "…".
|
|
119
|
+
* Trailing punctuation and whitespace are trimmed before the ellipsis so no heading ends "monitoring …"
|
|
120
|
+
* or with a dangling bracket. Returns `s` unchanged when it already fits. PURE. */
|
|
121
|
+
export function clipToWord(s, max) {
|
|
122
|
+
const t = String(s ?? "").trim();
|
|
123
|
+
if (t.length <= max) return t;
|
|
124
|
+
const cut = t.slice(0, max);
|
|
125
|
+
const sp = cut.lastIndexOf(" ");
|
|
126
|
+
const head = (sp > 0 ? cut.slice(0, sp) : cut).replace(/[\s,;:.\-–—(/[]+$/, "");
|
|
127
|
+
return head ? `${head}…` : `${cut.trim()}…`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** — ONE deferral's reader-visible coverage row. Exported so the shape a client reads is testable
|
|
131
|
+
* without a run directory: injectDeferralCoverage below is the file-I/O wrapper around this. PURE. */
|
|
132
|
+
export function deferralCoverageRow(directive, reason) {
|
|
133
|
+
const full = plainDirective(directive);
|
|
134
|
+
return {
|
|
135
|
+
area: `Follow-up / ${clipToWord(full, DEFERRAL_AREA_ITEM_BUDGET)}`,
|
|
136
|
+
state: "open",
|
|
137
|
+
note: `${full} — not completed this run — ${plainDeferralReason(reason)}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// degraded-parts.mjs — the parts of a report that failed, degraded or were skipped while the report still
|
|
5
|
+
// shipped, read from the run's record at delivery so the audit workbook can say so.
|
|
6
|
+
//
|
|
7
|
+
// THE RULE (ruled 2026-09-25): run clean first time; if not, retry and recover; and anything that still
|
|
8
|
+
// failed, short of a failure that makes the report meaningless, is delivered and flagged in the audit. The
|
|
9
|
+
// defect this module exists for is the part that fails silently: the report ships without it and nothing
|
|
10
|
+
// in the delivery says so.
|
|
11
|
+
//
|
|
12
|
+
// READ FROM THE STATE AT DELIVERY, NEVER FROM HISTORY. The run log is append-only across resumes, so a
|
|
13
|
+
// failure that a retry or a resume later recovered is still in it. Each check reads what the delivery can
|
|
14
|
+
// see: the stage's own last attempt, the artifact the part is built from, or the last event its step
|
|
15
|
+
// wrote. A part that recovered writes no row.
|
|
16
|
+
//
|
|
17
|
+
// THE WORDS ARE THE WORKBOOK'S OWN. A check names its part with a label the report or the workbook already
|
|
18
|
+
// prints (PART_NAMES, each held to its source by a test) and gives a reason token that the shipped
|
|
19
|
+
// deferral row translates into its reader's words. The row is that builder's, so this module writes no
|
|
20
|
+
// sentence, and no raw cause reaches a reader's cell: the cause stays on the run's record.
|
|
21
|
+
//
|
|
22
|
+
// A pure leaf: node:fs, node:path, shared/driver-dir.mjs, the row's own module and the publisher's table of
|
|
23
|
+
// stores (itself node:fs and node:path only), so it cannot drag driver.config.mjs, whose unset-env defaults
|
|
24
|
+
// are PRODUCTION, into anything that imports it.
|
|
25
|
+
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { driverDir, labelOfDriverFile } from "../shared/driver-dir.mjs"; // a card's label carries a colon, which Windows writes %3A
|
|
28
|
+
import { deferralCoverageRow } from "./deferral-row.mjs";
|
|
29
|
+
import { PUBLISH_INPUTS, readStore } from "./publish/publish-inputs.mjs";
|
|
30
|
+
|
|
31
|
+
// The name each part is given: a label the report or the workbook already prints. Never a new phrase.
|
|
32
|
+
export const PART_NAMES = {
|
|
33
|
+
courtDecisions: "Court decisions", // the report's search-coverage row (publish/render.mjs)
|
|
34
|
+
searchLog: "What was searched", // the workbook's tab (publish/xlsx.mjs)
|
|
35
|
+
register: "Register", // the report's search-coverage row (publish/render.mjs)
|
|
36
|
+
localLanguage: "Local-language investigation", // the report's search-coverage row (publish/render.mjs)
|
|
37
|
+
findings: "Findings", // the workbook's tab (publish/xlsx.mjs)
|
|
38
|
+
machineQc: "Machine QC", // the workbook's Summary row for the checks (publish/xlsx.mjs)
|
|
39
|
+
conditions: "Conditions", // the gaps sheet's area for a condition that reaches no page (publish/index.mjs)
|
|
40
|
+
coverage: "Coverage & gaps", // the workbook's tab (publish/xlsx.mjs)
|
|
41
|
+
marketplaceWeb: "Marketplace and web", // the report's search-coverage row (publish/render.mjs)
|
|
42
|
+
machineChecks: "Machine Checks", // the knockout workbook's sheet (publish/knockout.mjs)
|
|
43
|
+
aboutThisRequest: "About this request", // the knockout report's heading (publish/render-knockout.mjs)
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// The words the owner ruled where no shipped label or line fitted (537 and 539, 2026-09-25), each held to
|
|
47
|
+
// the ruling verbatim by a test. They are his; nothing here composes around them.
|
|
48
|
+
export const RULED_WORDS = {
|
|
49
|
+
officialRecords: "Official records", // the register records behind a clearance's cards
|
|
50
|
+
plainLanguageReview: "Plain-language review", // the knockout pass that rewrites the rater's wording
|
|
51
|
+
// A knockout web search that ran, whose answer reached the report, and whose trail entry was never written.
|
|
52
|
+
recordNotKept: "The record of this search was not kept this run; its answer was used",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** A filing's Note when its link was removed because the address is not on the register's own site (537). */
|
|
56
|
+
export const linkNotOnRegisterSite = (register) => `Address not on this register's own site (${register}); cited by number`;
|
|
57
|
+
|
|
58
|
+
// Reason tokens the shipped deferral row translates (deferral-row.mjs, plainDeferralReason). Only these two
|
|
59
|
+
// reach it from here, so the reader's line is one of the row's own and never the raw cause.
|
|
60
|
+
export const TIMED_OUT = "mechanical-fail:timeout"; // → "the source timed out this run"
|
|
61
|
+
export const NOT_COMPLETED = "unfinished"; // → the row's fallback: "it could not be completed this run, …"
|
|
62
|
+
|
|
63
|
+
export const DEGRADED_PARTS_FILE = "degraded-parts.json";
|
|
64
|
+
|
|
65
|
+
const readJsonl = (p) => {
|
|
66
|
+
try {
|
|
67
|
+
return readFileSync(p, "utf8").split("\n").filter((l) => l.trim())
|
|
68
|
+
.map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
69
|
+
} catch { return []; }
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** The stage's own last attempt row, or null when the stage wrote none. */
|
|
73
|
+
export function lastAttempt(runDir, stage) {
|
|
74
|
+
const rows = readJsonl(driverDir(runDir, `${stage}.jsonl`))
|
|
75
|
+
.filter((r) => Number.isFinite(Number(r?.attempt)) && typeof r.ok === "boolean");
|
|
76
|
+
return rows.length ? rows[rows.length - 1] : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The last run-log event carrying one of `names`, or null. */
|
|
80
|
+
const lastEvent = (log, names) => {
|
|
81
|
+
for (let i = log.length - 1; i >= 0; i--) if (names.includes(log[i]?.event)) return log[i];
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const reasonFor = (fail) => (/timeout|timed out/i.test(String(fail ?? "")) ? TIMED_OUT : NOT_COMPLETED);
|
|
86
|
+
|
|
87
|
+
// ── The checks. Each returns null, or {part, name, reason, cause}: `cause` is the raw fact for the run's
|
|
88
|
+
// record and never reaches a cell.
|
|
89
|
+
|
|
90
|
+
// Court decisions, on a run that was due them: the stage ended failed, or its file is absent or empty. An
|
|
91
|
+
// empty file reads "not in scope" on the report (publish/search-depth.mjs), so nothing else says it.
|
|
92
|
+
function courtDecisions(runDir, log) {
|
|
93
|
+
if (lastEvent(log, ["case-law-decision"])?.run !== true) return null;
|
|
94
|
+
const last = lastAttempt(runDir, "case-law");
|
|
95
|
+
const part = { part: "court-decisions", name: PART_NAMES.courtDecisions };
|
|
96
|
+
if (last?.ok === false) return { ...part, reason: reasonFor(last.fail), cause: `the case-law stage ended failed: ${last.fail ?? "no cause recorded"}` };
|
|
97
|
+
let text;
|
|
98
|
+
try { text = readFileSync(join(runDir, "case-law-findings.md"), "utf8"); } catch { text = null; }
|
|
99
|
+
if (text === null) return { ...part, reason: NOT_COMPLETED, cause: "case-law-findings.md is absent" };
|
|
100
|
+
if (!text.trim()) return { ...part, reason: NOT_COMPLETED, cause: "case-law-findings.md is empty" };
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The search log the workbook's "What was searched" tab is built from. Without it the tab lists no
|
|
105
|
+
// register search at all.
|
|
106
|
+
function searchLog(runDir) {
|
|
107
|
+
let text;
|
|
108
|
+
try { text = readFileSync(join(runDir, "audit.md"), "utf8"); } catch { text = null; }
|
|
109
|
+
if (text !== null && text.trim()) return null;
|
|
110
|
+
return { part: "search-log", name: PART_NAMES.searchLog, reason: NOT_COMPLETED,
|
|
111
|
+
cause: text === null ? "audit.md is absent" : "audit.md is empty" };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A step whose last word in the run log was its failure: `ok` names the event it writes when it succeeds,
|
|
115
|
+
// so a later success on a resume clears it.
|
|
116
|
+
const failedStep = (log, ok, failed) => {
|
|
117
|
+
const last = lastEvent(log, [ok, failed]);
|
|
118
|
+
return last?.event === failed ? last : null;
|
|
119
|
+
};
|
|
120
|
+
const stageEnded = (runDir, stage) => {
|
|
121
|
+
const last = lastAttempt(runDir, stage);
|
|
122
|
+
return last?.ok === false ? last : null;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// The register's own steps, each of which the run survives without: the band's shape (the floors and the
|
|
126
|
+
// full cards read it), the mechanical form floor, the owner screen, and the digest's folding in of re-runs.
|
|
127
|
+
function register(runDir, log) {
|
|
128
|
+
const part = { part: "register", name: PART_NAMES.register, reason: NOT_COMPLETED };
|
|
129
|
+
const steps = [
|
|
130
|
+
["band-shape-derived", "band-shape-failed", "the band shape"],
|
|
131
|
+
["form-neighbourhood-derived", "form-neighbourhood-skipped", "the form floor"],
|
|
132
|
+
["owner-screen-derived", "owner-screen-failed", "the owner screen"],
|
|
133
|
+
["digest-flush", "digest-flush-failed", "the digest flush"],
|
|
134
|
+
];
|
|
135
|
+
const failed = steps.map(([ok, bad, what]) => [failedStep(log, ok, bad), what]).filter(([e]) => e);
|
|
136
|
+
if (!failed.length) return null;
|
|
137
|
+
return { ...part, cause: failed.map(([e, what]) => `${what}: ${e.fail ?? e.reason ?? e.event}`).join("; ") };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// A finding's written card: its stage ended failed, so the card shipped with its structured rows only.
|
|
141
|
+
//
|
|
142
|
+
// MATCHED ON THE LABEL, NEVER ON THE FILE NAME. A card's stage label carries a colon — `report-card:3` —
|
|
143
|
+
// and Windows cannot hold one, so `driverFileName` writes it `%3A` there and this filter's raw colon
|
|
144
|
+
// matched nothing: `degradedParts` returned an empty list, and the audit workbook then reported coverage
|
|
145
|
+
// CLOSED over a card that failed. On Linux the two spellings are the same string, which is why every arm
|
|
146
|
+
// was green and only the scheduled Windows run showed it.
|
|
147
|
+
//
|
|
148
|
+
// `labelOfDriverFile` decodes `%3A` on every platform and strips the extension, so the label is the same
|
|
149
|
+
// on both and the predicate reads it rather than the bytes on disk. The label is also what `stageEnded`
|
|
150
|
+
// wants: it resolves its path through `driverDir`, which re-encodes the colon for the platform it is on,
|
|
151
|
+
// so handing it the raw name would send Windows looking for a file it never wrote.
|
|
152
|
+
function findingCards(runDir) {
|
|
153
|
+
let files = [];
|
|
154
|
+
try { files = readdirSync(driverDir(runDir)).filter((f) => f.endsWith(".jsonl")); } catch { return null; }
|
|
155
|
+
const failed = files.map((f) => labelOfDriverFile(f)).filter((stage) => /^report-card:.+/.test(stage))
|
|
156
|
+
.filter((stage) => stageEnded(runDir, stage));
|
|
157
|
+
if (!failed.length) return null;
|
|
158
|
+
return { part: "finding-cards", name: PART_NAMES.findings, reason: NOT_COMPLETED, cause: `card stages ended failed: ${failed.join(", ")}` };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The checks that stand behind the report without appearing in it: the frame's omission check (the blind
|
|
162
|
+
// frame and the frame diff), the reviewer's pass and its re-check, a corrective pass that rolled back, and
|
|
163
|
+
// the crowd counts the judgment reads. Each shares the one name, so they are one reader's line.
|
|
164
|
+
function checks(runDir, log) {
|
|
165
|
+
const failed = [];
|
|
166
|
+
const blind = stageEnded(runDir, "blind-frame");
|
|
167
|
+
if (blind) failed.push(["the blind frame", blind.fail]);
|
|
168
|
+
const diff = failedStep(log, "frame-diff", "frame-diff-skipped");
|
|
169
|
+
if (diff && !blind) failed.push(["the frame diff", diff.reason]);
|
|
170
|
+
const skeptic = stageEnded(runDir, "skeptic");
|
|
171
|
+
if (skeptic) failed.push(["the reviewer's pass", skeptic.fail]);
|
|
172
|
+
const recheck = stageEnded(runDir, "narrative-refutation");
|
|
173
|
+
if (recheck) failed.push(["the review's re-check", recheck.fail]);
|
|
174
|
+
const rollback = failedStep(log, "corrective-cycle-receipt", "corrective-rollback");
|
|
175
|
+
if (rollback) failed.push(["the corrective pass, rolled back", rollback.reason]);
|
|
176
|
+
const crowd = failedStep(log, "crowd-context", "crowd-context-failed");
|
|
177
|
+
if (crowd) failed.push(["the crowd counts", crowd.fail]);
|
|
178
|
+
if (!failed.length) return null;
|
|
179
|
+
return { part: "checks", name: PART_NAMES.machineQc, reason: reasonFor(failed.map(([, why]) => why).join("; ")),
|
|
180
|
+
cause: failed.map(([what, why]) => `${what}: ${why ?? "no cause recorded"}`).join("; ") };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The native-language investigation, where lanes were asked and none of them ran. The caller hands in the
|
|
184
|
+
// state publishing prints (publish/search-depth.mjs, localLanguageDepth): the lanes' own record is read
|
|
185
|
+
// through jx.mjs, which this leaf may not import. A lane that ran short of its depth is not read here, because
|
|
186
|
+
// the run cannot yet tell a shortfall from a limit of the product.
|
|
187
|
+
function localLanguage(runDir, log, { localLanguage: state = null } = {}) {
|
|
188
|
+
if (state !== "not-run") return null;
|
|
189
|
+
return { part: "local-language", name: PART_NAMES.localLanguage, reason: NOT_COMPLETED, cause: "native-language lanes were asked and none of them ran" };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The fetched official records, `_records/*.json`: "absent" with no file, "damaged" when files are there
|
|
194
|
+
* and none of them parses, "read" otherwise. It asks what registry-fidelity.mjs readRecordArtifacts asks
|
|
195
|
+
* of the same folder, which skips a file it cannot parse, only whether anything survived.
|
|
196
|
+
*/
|
|
197
|
+
export function recordsState(runDir) {
|
|
198
|
+
let files;
|
|
199
|
+
try { files = readdirSync(join(runDir, "_records")).filter((f) => f.endsWith(".json")); } catch { return "absent"; }
|
|
200
|
+
if (!files.length) return "absent";
|
|
201
|
+
for (const f of files) {
|
|
202
|
+
try {
|
|
203
|
+
const body = JSON.parse(readFileSync(join(runDir, "_records", f), "utf8"));
|
|
204
|
+
if (body && typeof body === "object" && !Array.isArray(body)) return "read";
|
|
205
|
+
} catch { /* the next file */ }
|
|
206
|
+
}
|
|
207
|
+
return "damaged";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The official records behind the cards, where the run's record set was built from a fetch ledger it could
|
|
211
|
+
// not read: the set is empty for that reason, and the cards print what the model wrote with nothing to check
|
|
212
|
+
// it against and no mark that it went unchecked. Two cases are left out on purpose. A merely empty set is the
|
|
213
|
+
// product's design on a register whose report cites no record address, which never fetches one. And the
|
|
214
|
+
// count of fetches whose bodies went missing is taken before the closure fetch, which can still close it.
|
|
215
|
+
function officialRecords(runDir, log) {
|
|
216
|
+
const last = lastEvent(log, ["record-artifacts"]);
|
|
217
|
+
if (!last?.ledgerError) return null;
|
|
218
|
+
return { part: "official-records", name: RULED_WORDS.officialRecords, reason: NOT_COMPLETED,
|
|
219
|
+
cause: `the fetch ledger could not be read: ${last.ledgerError}` };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const CHECKS = [courtDecisions, searchLog, register, localLanguage, findingCards, checks, officialRecords];
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Every part of this clearance run that failed and still ships, read from the run directory as it stands
|
|
226
|
+
* at delivery. `opts.localLanguage` is the native-language state publishing prints, when the run has lanes. A check that cannot read its input reads nothing and reports nothing; the run's own record
|
|
227
|
+
* already carries that input's failure. PURE apart from reading `runDir`.
|
|
228
|
+
*/
|
|
229
|
+
export function degradedParts(runDir, opts = {}) {
|
|
230
|
+
const log = readJsonl(driverDir(runDir, "run.jsonl"));
|
|
231
|
+
return CHECKS.map((check) => check(runDir, log, opts)).filter(Boolean);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The rows the audit workbook prints for `parts`: the shipped deferral row. One row per name: two parts
|
|
236
|
+
* that share a name are one reader's line, and both causes stay on the record.
|
|
237
|
+
*/
|
|
238
|
+
export function degradedPartRows(parts) {
|
|
239
|
+
const seen = new Set();
|
|
240
|
+
const rows = [];
|
|
241
|
+
for (const p of parts) {
|
|
242
|
+
if (seen.has(p.name)) continue;
|
|
243
|
+
seen.add(p.name);
|
|
244
|
+
rows.push(deferralCoverageRow(p.name, p.reason));
|
|
245
|
+
}
|
|
246
|
+
return rows;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Write the parts and their rows to the run's record, where publishing and a later republish read them,
|
|
251
|
+
* with the state of every store the publisher reads as delivery found it: the evidence a republish needs
|
|
252
|
+
* to tell a store this run lost from one it never had.
|
|
253
|
+
*/
|
|
254
|
+
export function writeDegradedParts(runDir, parts) {
|
|
255
|
+
const record = { parts, rows: degradedPartRows(parts), stores: storeStates(runDir) };
|
|
256
|
+
writeFileSync(driverDir(runDir, DEGRADED_PARTS_FILE), JSON.stringify(record, null, 2) + "\n");
|
|
257
|
+
return record;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The rows written at delivery, or [] for a run that wrote none (every run before this change). */
|
|
261
|
+
export function readDegradedPartRows(runDir) {
|
|
262
|
+
try {
|
|
263
|
+
const rows = JSON.parse(readFileSync(driverDir(runDir, DEGRADED_PARTS_FILE), "utf8"))?.rows;
|
|
264
|
+
return Array.isArray(rows) ? rows.filter((r) => r && typeof r.area === "string" && typeof r.note === "string") : [];
|
|
265
|
+
} catch { return []; }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── AT PUBLISH: THE STORES A REPUBLISH OR A DISK FAULT CAN TAKE ──────────────────────────────────────
|
|
269
|
+
//
|
|
270
|
+
// Two things only the publisher sees. A store that is PRESENT AND UNREADABLE when it is read: an archived
|
|
271
|
+
// run is never in that state, only a missing one, so it cannot fire on older work. And a store that
|
|
272
|
+
// delivery read and a later republish finds MISSING: the delivery's own record of what it read is the
|
|
273
|
+
// evidence, so a run delivered before that record existed writes nothing, as before.
|
|
274
|
+
//
|
|
275
|
+
// Each store the publisher reads is the part it feeds, named with a label the report or workbook prints,
|
|
276
|
+
// or it is declared out with the reason, so a new store cannot be added without deciding which.
|
|
277
|
+
export const STORE_PARTS = {
|
|
278
|
+
"findings.json": "findings",
|
|
279
|
+
"_driver/receipts.json": "officialRecords",
|
|
280
|
+
"_driver/senior-rights.json": "officialRecords",
|
|
281
|
+
"_driver/verdict.json": "conditions",
|
|
282
|
+
"_driver/framework.json": "findings",
|
|
283
|
+
"_driver/register-recall.json": "coverage",
|
|
284
|
+
"_driver/register-plan.json": "register",
|
|
285
|
+
"register-named-band.json": "register",
|
|
286
|
+
"_driver/instructed-scope.json": "register",
|
|
287
|
+
"_driver/jx-lanes.json": "localLanguage",
|
|
288
|
+
"_driver/jx/units.json": "localLanguage",
|
|
289
|
+
"case-law-findings.md": "courtDecisions",
|
|
290
|
+
"case-law-citations.json": "courtDecisions",
|
|
291
|
+
"_driver/predelivery-lint.json": "machineQc",
|
|
292
|
+
"_driver/escalation-state.json": "machineQc",
|
|
293
|
+
"_driver/reasoning-integrity.json": "machineQc",
|
|
294
|
+
"_driver/corrections-state.json": "machineQc",
|
|
295
|
+
"common-law-grid.json": "marketplaceWeb",
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/** The stores no row describes honestly, each with its reason. */
|
|
299
|
+
export const STORES_WITHOUT_A_PART = {
|
|
300
|
+
"_driver/enforcer-signals.json": "presentation-only lines about enforcement, and no shipped name describes them",
|
|
301
|
+
"status.json": "the pool's mark name and a machine note, neither of them a part of the report",
|
|
302
|
+
"_driver/search-policy.json": "the product's identity, which no shipped name describes",
|
|
303
|
+
"_driver/profile.json": "the pool's profile stamp, not a part of the report",
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// The record set is a folder, not a named store (publish-inputs.mjs NOT_READ_BY_NAME), and is read as one.
|
|
307
|
+
const RECORDS = "_records/";
|
|
308
|
+
|
|
309
|
+
const partName = (key) => PART_NAMES[key] ?? RULED_WORDS[key];
|
|
310
|
+
|
|
311
|
+
/** The state of every store the publisher reads, as `base` holds it now: read, damaged or absent. */
|
|
312
|
+
export function storeStates(base) {
|
|
313
|
+
const states = {};
|
|
314
|
+
for (const name of Object.keys(PUBLISH_INPUTS)) {
|
|
315
|
+
try { states[name] = readStore(base, name).state; } catch { states[name] = "damaged"; }
|
|
316
|
+
}
|
|
317
|
+
states[RECORDS] = recordsState(base);
|
|
318
|
+
return states;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** The store states delivery recorded, or null for a run delivered before it recorded them. */
|
|
322
|
+
export function readDeliveredStores(runDir) {
|
|
323
|
+
try {
|
|
324
|
+
const stores = JSON.parse(readFileSync(driverDir(runDir, DEGRADED_PARTS_FILE), "utf8"))?.stores;
|
|
325
|
+
return stores && typeof stores === "object" && !Array.isArray(stores) ? stores : null;
|
|
326
|
+
} catch { return null; }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The parts publishing finds degraded, from the store states now and, where delivery recorded them, then.
|
|
331
|
+
* PURE over its inputs. `now` is `storeStates(base)`; `delivered` is `readDeliveredStores(runDir)`.
|
|
332
|
+
*/
|
|
333
|
+
export function degradedAtPublish(now = {}, delivered = null) {
|
|
334
|
+
const parts = [];
|
|
335
|
+
const mapped = { ...STORE_PARTS, [RECORDS]: "officialRecords" };
|
|
336
|
+
for (const [store, key] of Object.entries(mapped)) {
|
|
337
|
+
const state = now[store];
|
|
338
|
+
const cause = state === "damaged" ? `${store} is present and cannot be read`
|
|
339
|
+
: state === "absent" && delivered?.[store] === "read" ? `${store} was read at delivery and is missing now`
|
|
340
|
+
: null;
|
|
341
|
+
if (cause) parts.push({ part: `store:${store}`, name: partName(key), reason: NOT_COMPLETED, cause });
|
|
342
|
+
}
|
|
343
|
+
return parts;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** One row per area across lists, the first kept: delivery's rows, then publishing's. */
|
|
347
|
+
export function mergeDegradedRows(...lists) {
|
|
348
|
+
const seen = new Set();
|
|
349
|
+
const rows = [];
|
|
350
|
+
for (const r of lists.flat()) {
|
|
351
|
+
if (!r || typeof r.area !== "string" || seen.has(r.area)) continue;
|
|
352
|
+
seen.add(r.area);
|
|
353
|
+
rows.push(r);
|
|
354
|
+
}
|
|
355
|
+
return rows;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ── THE KNOCKOUT: A STEP THAT FAILED AS A WHOLE ───────────────────────────────────────────────────────
|
|
359
|
+
//
|
|
360
|
+
// The listing of filings and the owner lookups each end in one run-log event, a success or a failure, and
|
|
361
|
+
// the last of the pair is the step's state at delivery, so a resume that succeeded clears it. The
|
|
362
|
+
// plain-language review logs its outcome; the three that ship the rater's wording unreviewed are failures.
|
|
363
|
+
const REVIEW_FAILED = ["stage-failed", "artifact-unreadable", "rewrite-refused-by-the-merged-gate"];
|
|
364
|
+
|
|
365
|
+
/** Which of the knockout's whole steps ended failed, each with its raw cause for the run's record. */
|
|
366
|
+
export function knockoutStepFailures(runDir) {
|
|
367
|
+
const log = readJsonl(driverDir(runDir, "run.jsonl"));
|
|
368
|
+
const listing = lastEvent(log, ["knockout-register-records", "knockout-register-records-refused", "knockout-register-records-failed"]);
|
|
369
|
+
const owners = failedStep(log, "knockout-owner-checks", "knockout-owner-checks-failed");
|
|
370
|
+
const review = lastEvent(log, ["knockout-review"]);
|
|
371
|
+
const cause = (e) => String(e?.cause ?? e?.reason ?? e?.outcome ?? e?.event ?? "no cause recorded");
|
|
372
|
+
return {
|
|
373
|
+
listing: listing?.event === "knockout-register-records-failed" ? { reason: reasonFor(cause(listing)), cause: cause(listing) } : null,
|
|
374
|
+
ownerChecks: owners ? { reason: reasonFor(cause(owners)), cause: cause(owners) } : null,
|
|
375
|
+
review: REVIEW_FAILED.includes(review?.outcome) ? { reason: reasonFor(cause(review)), cause: `${review.outcome}: ${cause({ reason: review.reason })}` } : null,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
@@ -43,7 +43,7 @@ MSG="The trademark engine's outbox has pending events. Run the clearotron-delive
|
|
|
43
43
|
# slim container. Without it `timeout …` is simply a command that is not there: bash answers 127, and
|
|
44
44
|
# because this script runs under `set -u` and NOT `set -e`, it sails straight on. outbox-backoff.mjs
|
|
45
45
|
# then judges 127 the way it judges any nonzero exit and records `nonzero_exit_127` — a WAKE FAILURE.
|
|
46
|
-
# So the operator reads "wake for
|
|
46
|
+
# So the operator reads "wake for <agent> failed" in the journal, goes looking at the CLI, at auth, at
|
|
47
47
|
# the agent, and the answer is that a package is missing on the box. Deliveries stop, backoff climbs,
|
|
48
48
|
# every diagnosis points somewhere real and wrong. That is the silent class this repo keeps paying
|
|
49
49
|
# for, so it is turned into one sentence that names the cause.
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
27
27
|
import { tmpdir } from "node:os";
|
|
28
28
|
import { basename, dirname, join, resolve, sep } from "node:path"; import { studioDirFor } from "../shared/pre-rename-spellings.mjs";
|
|
29
|
+
import { config } from "./driver.config.mjs"; // — the install's own default agent, never a literal
|
|
29
30
|
|
|
30
31
|
/** The entry file each lane's publisher reads as its source, in the order a child is probed for one. */
|
|
31
32
|
export const ENTRY_FILES = Object.freeze(["report.md", "knockout-findings.json"]);
|
|
@@ -200,7 +201,9 @@ export function seedDemoRuns({ workspace, examplesDir, portalOrigin = null }) {
|
|
|
200
201
|
let s;
|
|
201
202
|
try { s = JSON.parse(readFileSync(join(run, "status.json"), "utf8")); } catch { continue; }
|
|
202
203
|
if (!s?.slug || !s?.codename || !s?.date) continue;
|
|
203
|
-
|
|
204
|
+
// THE FALLBACK IS THE INSTALL'S OWN DEFAULT AGENT, not a literal (ruling 563). It fires only for a
|
|
205
|
+
// sample whose status.json carries no agent; all four shipped samples carry one.
|
|
206
|
+
const dir = join(studioDirFor(join(workspace, `workspace-${s.agent || config.defaultAgent}`)), s.slug, `${s.date}-${s.codename}`);
|
|
204
207
|
if (existsSync(join(dir, "status.json"))) already.push(s.runId);
|
|
205
208
|
else {
|
|
206
209
|
// ONE SAMPLE'S UNREADABLE FILE COSTS THAT SAMPLE ONLY, and is named, never a throw out of the loop.
|
package/driver/dev-portal.mjs
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
import { envFileRead } from "../shared/env-local.mjs"; // side effect: apply this install's .env when THIS file is the CLI entry (never on library import)
|
|
33
33
|
import { createServer, request as httpRequest } from "node:http";
|
|
34
34
|
import { readFileSync, existsSync, statSync, readdirSync, writeFileSync, renameSync, mkdirSync } from "node:fs";
|
|
35
|
-
import { join, resolve, extname, dirname } from "node:path"; import { studioDirFor } from "../shared/pre-rename-spellings.mjs";
|
|
35
|
+
import { join, resolve, extname, dirname, sep } from "node:path"; import { studioDirFor } from "../shared/pre-rename-spellings.mjs";
|
|
36
36
|
import { fileURLToPath } from "node:url";
|
|
37
37
|
import { config } from "./driver.config.mjs";
|
|
38
38
|
import { BRAND } from "../shared/brand.mjs";
|
|
@@ -78,7 +78,7 @@ export const DEV_COCKPIT_JOB_FIELDS = Object.freeze({
|
|
|
78
78
|
+ "is a form for composing REAL dev runs; a fixture run is declared by the job file that wants one, so "
|
|
79
79
|
+ "the fact travels with the run rather than with whoever filled in a form.",
|
|
80
80
|
promptParts: "the requester's declaration that the prose rides as SIDECAR files. The cockpit composes a job "
|
|
81
|
-
+ "from its own form and writes no sidecars, so it may not claim that shape
|
|
81
|
+
+ "from its own form and writes no sidecars, so it may not claim that shape.",
|
|
82
82
|
forwarderDomain: "the cockpit's assembler defaults it; a dev form has no forwarding domain to state.",
|
|
83
83
|
provider: "which register vendor answers is the dev instance's own configuration, not a form field.",
|
|
84
84
|
name: "the pre-markName spelling of the search subject. The form posts `mark`.",
|
|
@@ -253,7 +253,7 @@ function cockpitHtml() {
|
|
|
253
253
|
<label>Extra marketplaces (comma — ADDED to the account's own)</label><input name="platforms" placeholder="gnc.com">
|
|
254
254
|
<label><input type="checkbox" name="worldwide" style="width:auto"> worldwide (search everywhere; NOT the same as leaving countries blank)</label>
|
|
255
255
|
<label><input type="checkbox" name="nativeLanguage" style="width:auto"> native-language investigation (multi-country focus search only)</label>
|
|
256
|
-
<label>Saved search (recipe key — leave the product blank)</label><input name="recipeKey" id="f_recipe" placeholder="
|
|
256
|
+
<label>Saved search (recipe key — leave the product blank)</label><input name="recipeKey" id="f_recipe" placeholder="standard-clearance">
|
|
257
257
|
<label>Batch marks (knockout — one per line, optional "NAME [9, 42]")</label><textarea name="marks" placeholder="IRONWHISK [8, 21] CLUVENDRA SUNDAY ROAST CLUB [21, 35]"></textarea>
|
|
258
258
|
<label>Classes (comma)</label><input name="classes" placeholder="9,42">
|
|
259
259
|
<label>Goods / services</label><textarea name="goods"></textarea>
|
|
@@ -271,7 +271,7 @@ function cockpitHtml() {
|
|
|
271
271
|
<div id="searches" style="margin-top:8px">loading…</div></section>
|
|
272
272
|
<section><h2>Compose a saved search</h2>
|
|
273
273
|
<form id="sf">
|
|
274
|
-
<label>Slug (filename + trigger key) *</label><input name="slug" placeholder="
|
|
274
|
+
<label>Slug (filename + trigger key) *</label><input name="slug" placeholder="standard-clearance" required>
|
|
275
275
|
<label>Label (display name) *</label><input name="label" placeholder="Quarterly product-name screen" required>
|
|
276
276
|
<label>Base search (the product this saved search runs) *</label><select name="base" id="sf_base"></select>
|
|
277
277
|
<label><input type="checkbox" name="registerProbe" style="width:auto"> + register filing counts (knockout bases only)</label>
|
|
@@ -470,7 +470,7 @@ export function startPortal({ poolRoot = null, port = 18899, host = "127.0.0.1",
|
|
|
470
470
|
// Static pool files. decodeURIComponent + resolve + prefix check = traversal-guarded.
|
|
471
471
|
let p;
|
|
472
472
|
try { p = resolve(root, "." + decodeURIComponent(url.pathname)); } catch { res.writeHead(400); return res.end(); }
|
|
473
|
-
if (p !== root && !p.startsWith(root +
|
|
473
|
+
if (p !== root && !p.startsWith(root + sep)) { res.writeHead(400); return res.end("bad path"); } // resolve() gives this machine's separator
|
|
474
474
|
if (existsSync(p) && statSync(p).isDirectory()) p = join(p, "index.html");
|
|
475
475
|
if (!existsSync(p)) { res.writeHead(404, { "content-type": "text/plain" }); return res.end("not found in the dev pool"); }
|
|
476
476
|
sendFile(res, p);
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
import { createHash } from "node:crypto";
|
|
38
38
|
import { mkdirSync, readFileSync, renameSync, writeFileSync, existsSync } from "node:fs";
|
|
39
39
|
import { join } from "node:path";
|
|
40
|
-
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
40
|
+
import { driverDir, driverFileName } from "../shared/driver-dir.mjs"; //
|
|
41
41
|
|
|
42
42
|
export const DISPATCH_SUFFIX = "dispatch.txt";
|
|
43
43
|
|
|
@@ -115,7 +115,7 @@ export function dispatchGrantState(dispatch, ranTools) {
|
|
|
115
115
|
*/
|
|
116
116
|
export function recordDispatch(runDir, stage, { attempt, repair = 0, kind = "fresh", message, grant } = {}) {
|
|
117
117
|
if (!runDir) return null;
|
|
118
|
-
const name = dispatchFileName(stage, attempt, repair);
|
|
118
|
+
const name = driverFileName(dispatchFileName(stage, attempt, repair)); // on Windows the label's colon is written %3A
|
|
119
119
|
try {
|
|
120
120
|
const text = String(message ?? "");
|
|
121
121
|
const buf = Buffer.from(text, "utf8");
|