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
package/driver/products.mjs
CHANGED
|
@@ -405,7 +405,7 @@ export function nativeLanguageMode(product) {
|
|
|
405
405
|
*
|
|
406
406
|
* Exported because two surfaces outside this module have to say it and both used to say something else:
|
|
407
407
|
* the resolution-time recommendation (jx-lanes.mjs zhScopeDepthNotes) and the DELIVERED REPORT's own
|
|
408
|
-
* coverage row (pipeline.mjs scriptScopeDisclosure). Both named `
|
|
408
|
+
* coverage row (pipeline.mjs scriptScopeDisclosure). Both named `clearance-jx` and `Depth 5` — an internal
|
|
409
409
|
* product key and a rung on a ladder — so the coverage row named a remedy that was a product
|
|
410
410
|
* deleted, quoted at a number that no longer exists, and could not have ordered either.
|
|
411
411
|
*
|
|
@@ -53,8 +53,8 @@ consumes it; a unit test greps each symbol, so a field with no live consumer fai
|
|
|
53
53
|
| `delivery` | delivery | `{ email: "summary", privileged: bool }` — the "Privileged & Confidential" header. Two further optional sub-keys are accepted: `style` (a prose string, guarded by the same anti-rule check as a context pack, dictated into the `report-overview` and `report-card` stages as presentation tone and never to `synthesis`, the rating stage) and `template` (a report-template name; `"standard"` is the only one that exists, and it is the default). **Absent ⇒ neutral default** (`{email:"summary"}` — deliberately SILENT on `privileged`, which is three-state on every surface that reads it (`confPosture`): `true` extends the marking to "Attorney Work Product", `false` is a deliberate OFF, and absent is no opinion, which gets the plain "Privileged & Confidential" every legal deliverable carries. Saying `false` in the neutral overlay read as an instruction to strip the marking, and a Generic-default clearance shipped with no line at all). `email` no longer selects anything: every run's mail is a COVER NOTE pointing at the one report. The old `"table"` value — a full review table inlined into the mail body — is **retired**; it is still accepted at load so stored profiles keep validating, and folded to `"summary"` by `normalizeDelivery`. A customer who wants their own house format gets it drafted by the assistant from the run's `report-data.json`, where a person reads it before it goes |
|
|
54
54
|
| `riskAppetite` | context | a PROSE-POSTURE string (optional) that flavours **emphasis + recommended follow-up** in delivery curation — the two stages that are dictated it, `report-overview` and `report-card` — **never the Level/Composite**. A load-time anti-threshold guard rejects numeric/threshold phrasing (`>50%`, `Level C or above`, `threshold`); the "never decides" invariance itself is gated by review |
|
|
55
55
|
| `marketplaceDensity` | structural | `"sparse"` (default) \| `"dense"` — selects the per-profile grid cell budget so a byte-heavy marketplace's verbatim stdout fits the worker output channel (sparse ⇒ 98-cell budget; dense ⇒ 16). Dense fits long retail listings (beverages and supplements); gaming stores stay sparse |
|
|
56
|
-
| `frameworkPath` | rating authority | optional path to the customer's OWN risk framework (`skills/
|
|
57
|
-
| `workedExamplesPath` | context | optional path to a per-customer worked-examples set (`skills/
|
|
56
|
+
| `frameworkPath` | rating authority | optional path to the customer's OWN risk framework (`skills/clearance-search/<file>.md`, path-escape-blocked). **The framework in force RATES the matter** — the customer's own if on file, else the Generic default `risk-framework.md`; nothing in between. Each framework is a prose deck (the client's own rubric, reasoned WITH) plus a `.manifest.json` sidecar carrying its band vocabulary (`test/framework-lint.test.mjs` guards the pair). Git + legal-team gated; the config UI shows which is in force, read-only |
|
|
57
|
+
| `workedExamplesPath` | context | optional path to a per-customer worked-examples set (`skills/clearance-search/<file>.md`). The analysis DEPTH TARGET in `synthesis`, calibrated under that customer's framework; **absent ⇒ the Generic default `worked-examples.md`** |
|
|
58
58
|
| `defaultProduct` | entitlement | which of the four searches runs when a request names none (`search-policy.mjs`). May be left unset, and that is not a gap — a clearance that names no product is then named by its own resolved territories |
|
|
59
59
|
| `allowedRecipes[]` | entitlement | the closed menu of searches this account may trigger; non-empty when present, and **absent ⇒ everything allowed** |
|
|
60
60
|
| `jxPolicy` | entitlement | the native-language deepening POSTURE — declared lanes / escalation / provider stance. Policy, never capability; frozen into the run sidecar so a resume keeps the posture the run started under |
|
|
@@ -90,7 +90,7 @@ value into a profile file is a load-time error.
|
|
|
90
90
|
the write door: the same profile saved through the editor is a validated auto-commit with no PR in
|
|
91
91
|
it, and the shared load-time validators are what both paths cannot get past.
|
|
92
92
|
3. **Rewrite check:** the skills are platform-agnostic (they follow the dictated list), but read
|
|
93
|
-
`skills/
|
|
93
|
+
`skills/clearance-common-law/SKILL.md` + `perplexity-prompts.md`, `skills/clearance-search/SKILL.md` +
|
|
94
94
|
`phase2-execution.md`, and the driver message prose in `stages.mjs` once against the new
|
|
95
95
|
industry — worked examples are gaming-flavored by history.
|
|
96
96
|
4. ONE validation run against a known/synthetic matter: confirm the grid swept exactly the
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"delivery": { "email": "summary", "privileged": false },
|
|
18
18
|
"riskAppetite": "Evidence-first. Lead with the register position, then say plainly what the common-law picture adds or fails to settle. Name coverage limits in the body rather than in a footnote — an unread source is a fact about the search, not a caveat about the report. Keep crowded-field noise brief.",
|
|
19
19
|
"defaultProduct": "multi-country-focus-search",
|
|
20
|
-
"frameworkPath": "skills/
|
|
21
|
-
"workedExamplesPath": "skills/
|
|
20
|
+
"frameworkPath": "skills/clearance-search/risk-framework-demo.md",
|
|
21
|
+
"workedExamplesPath": "skills/clearance-search/worked-examples-demo.md",
|
|
22
22
|
"jxPolicy": { "laneDepth": { "ja": "full", "ko": "candidates" } },
|
|
23
23
|
"runCaps": { "dailyRuns": 12, "maxQueued": 2 }
|
|
24
24
|
}
|
package/driver/profiles.mjs
CHANGED
|
@@ -346,9 +346,12 @@ export const FIELD_CONSUMERS = {
|
|
|
346
346
|
|
|
347
347
|
// Per-customer reasoning-skill selection. frameworkPath is the customer's OWN risk framework — under doc 50
|
|
348
348
|
// it RATES the matter; absent ⇒ the Generic default rates it (DEFAULT_FRAMEWORK in framework.mjs). Constrained to
|
|
349
|
-
// the
|
|
349
|
+
// the clearance-search skill dir + a .md suffix so a profile cannot point the synthesis read at an arbitrary
|
|
350
350
|
// path. The SHARED doctrine lives identically across the per-customer frameworks; only the examples diverge.
|
|
351
|
-
|
|
351
|
+
// The folder's pre-rename spelling is accepted as the same folder: a store written before the rename names it,
|
|
352
|
+
// and one unmigrated profile must not take a company, or the whole roster, off the list. The file is read
|
|
353
|
+
// from whichever spelling the store holds (config.resolveSkillPath).
|
|
354
|
+
const SKILL_PATH_RE = /^skills\/(?:clearance|prelim)-search\/[A-Za-z0-9._-]+\.md$/;
|
|
352
355
|
|
|
353
356
|
// The delivery overlay every run gets. `email` is no longer a choice: every run's mail is a COVER NOTE
|
|
354
357
|
// pointing at the report (one report, one shape, per-lawyer client mail drafted by the assistant).
|
|
@@ -566,11 +569,11 @@ function validateProfileShape(key, p, { sparse = false } = {}) {
|
|
|
566
569
|
die(`marketplaceDensity must be ${MARKETPLACE_DENSITIES.map((v) => `"${v}"`).join(" or ")} `
|
|
567
570
|
+ `(got "${p.marketplaceDensity}")`);
|
|
568
571
|
// frameworkPath / workedExamplesPath (optional, Phase 2): a per-customer reasoning-skill file. Constrained
|
|
569
|
-
// to skills/
|
|
572
|
+
// to skills/clearance-search/*.md (no path escape) — a profile selects a SHIPPED skill, never an arbitrary path.
|
|
570
573
|
for (const k of ["frameworkPath", "workedExamplesPath"]) {
|
|
571
574
|
if (p[k] == null) continue;
|
|
572
575
|
if (typeof p[k] !== "string" || !SKILL_PATH_RE.test(p[k]) || p[k].includes(".."))
|
|
573
|
-
die(`${k} must be a path of the form "skills/
|
|
576
|
+
die(`${k} must be a path of the form "skills/clearance-search/<file>.md" (got ${JSON.stringify(p[k])})`);
|
|
574
577
|
}
|
|
575
578
|
// defaultProduct (optional): one of the four in the offering, by id. A typo hard-fails at load rather
|
|
576
579
|
// than silently running the wrong-priced product on every job.
|
|
@@ -679,25 +682,42 @@ let cache = null;
|
|
|
679
682
|
/** Read one directory of profiles/<key>.json → Map(key → profile). No generic requirement and no
|
|
680
683
|
* matchDomains check here: both are properties of the MERGED roster, not of one layer, and asserting
|
|
681
684
|
* them per-layer would refuse a base+overlay pair that is perfectly valid once combined. */
|
|
682
|
-
|
|
685
|
+
// ONE COMPANY THAT CANNOT BE READ MUST NOT TAKE THE OTHERS WITH IT — on the deployment's own store. Strict,
|
|
686
|
+
// every file or none, is right for an explicit directory: that is how this loader's rules are checked. On the
|
|
687
|
+
// configured store it refused every company over one file, and the portal then offered `generic` alone with
|
|
688
|
+
// no error, so a client's clearances looked deleted. So `tolerant` isolates each file: a company that fails is
|
|
689
|
+
// left out, its reason is kept on the returned map as `unreadable`, and asking for it by key refuses with that
|
|
690
|
+
// reason (resolveProfile). `generic` is never tolerated: it is the fallback every unprofiled job rates under,
|
|
691
|
+
// and replacing a deployment's own with the bundled one would change every such answer without a word.
|
|
692
|
+
function readProfilesLayer(dir, { tolerant = false } = {}) {
|
|
683
693
|
const profiles = new Map();
|
|
694
|
+
const unreadable = [];
|
|
684
695
|
for (const f of readdirSync(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
685
696
|
const key = f.replace(/\.json$/, "");
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
+
try {
|
|
698
|
+
let p;
|
|
699
|
+
try { p = JSON.parse(readFileSync(join(dir, f), "utf8")); }
|
|
700
|
+
catch (e) { throw new Error(`profiles/${f}: unparseable JSON (${e.message})`); }
|
|
701
|
+
validateProfileShape(key, p);
|
|
702
|
+
// Context pack (Phase 1): a sibling `<key>.context.md`, attached AFTER validateProfileShape so the
|
|
703
|
+
// deny-unknown-key gate (which governs the JSON shape) never sees it. Optional — absent ⇒ no pack.
|
|
704
|
+
// Validated at load (rule-shape + size budget) so a bad pack fails loudly here, like riskAppetite (F8).
|
|
705
|
+
const packPath = join(dir, CONTEXT_PACK_FILE(key));
|
|
706
|
+
const contextPack = existsSync(packPath) ? readFileSync(packPath, "utf8").trim() : "";
|
|
707
|
+
if (contextPack) assertContextPackShape(contextPack, `profiles/${CONTEXT_PACK_FILE(key)}`);
|
|
708
|
+
profiles.set(key, { key, ...p, ...(contextPack ? { contextPack } : {}) });
|
|
709
|
+
} catch (e) {
|
|
710
|
+
if (!tolerant || key === "generic") throw e;
|
|
711
|
+
unreadable.push({ key, file: f, reason: String(e?.message ?? e) });
|
|
712
|
+
}
|
|
697
713
|
}
|
|
714
|
+
Object.defineProperty(profiles, "unreadable", { value: unreadable, enumerable: false });
|
|
698
715
|
return profiles;
|
|
699
716
|
}
|
|
700
717
|
|
|
718
|
+
/** The companies a roster left out because their file would not load: `[{ key, file, reason }]`. */
|
|
719
|
+
export const unreadableProfiles = (profiles) => (Array.isArray(profiles?.unreadable) ? profiles.unreadable : []);
|
|
720
|
+
|
|
701
721
|
/** Load every profiles/<key>.json → Map(key → profile), OVERLAY OVER BASE. Hard-fails loudly
|
|
702
722
|
* on a generic.json missing from BOTH layers (the universal fallback — its absence would mis-profile
|
|
703
723
|
* EVERY job), on a configured-but-unreadable overlay, and on any matchDomains overlap in the merged
|
|
@@ -746,7 +766,7 @@ export function loadProfiles({ dir, force = false, includeTestFixtures, includeD
|
|
|
746
766
|
// the universal fallback the module REQUIRES by name, the thing every unprofiled job resolves to. That
|
|
747
767
|
// is the one file whose absence makes an empty store a refusal, so that is the only one that falls
|
|
748
768
|
// through. Everything else in a deployment's roster is the deployment's own.
|
|
749
|
-
const profiles = overlay ? readProfilesLayer(overlay) : readProfilesLayer(baseDir);
|
|
769
|
+
const profiles = overlay ? readProfilesLayer(overlay, { tolerant: true }) : readProfilesLayer(baseDir);
|
|
750
770
|
|
|
751
771
|
// ── A TEST FIXTURE IS PRESENT AND NEVER OFFERED ─────────────────────────────────────────────────
|
|
752
772
|
//
|
|
@@ -855,6 +875,15 @@ export function loadProfiles({ dir, force = false, includeTestFixtures, includeD
|
|
|
855
875
|
export function resolveProfile(job, { profiles = loadProfiles() } = {}) {
|
|
856
876
|
const key = String(job?.profileKey ?? "").trim();
|
|
857
877
|
if (key && profiles.has(key)) return profiles.get(key);
|
|
878
|
+
// A company whose file would not load is not an unknown company: say which, and why.
|
|
879
|
+
const unread = unreadableProfiles(profiles);
|
|
880
|
+
const failed = key ? unread.find((u) => u.key === key) : null;
|
|
881
|
+
if (failed) {
|
|
882
|
+
const err = new Error(`profile_unreadable:${key} — ${failed.reason}. The company exists but its profile could not be read, `
|
|
883
|
+
+ `so this run is refused rather than rated under another company's settings.`);
|
|
884
|
+
err.code = "profile_unreadable";
|
|
885
|
+
throw err;
|
|
886
|
+
}
|
|
858
887
|
// A NAMED-but-unknown key is not a graceful-degradation case, it is a roster mismatch — the two
|
|
859
888
|
// sides disagree about which config store is real, and falling back to `generic` silently strips
|
|
860
889
|
// the client's platforms, their self-exclusion seed and the framework that RATES the matter. That
|
|
@@ -875,6 +904,15 @@ export function resolveProfile(job, { profiles = loadProfiles() } = {}) {
|
|
|
875
904
|
if (dom === dl || dom.endsWith(`.${dl}`)) return p;
|
|
876
905
|
}
|
|
877
906
|
}
|
|
907
|
+
// WITH A COMPANY UNREAD, "no company matched" is not known: the unread one's domains are not in hand, and
|
|
908
|
+
// this job may be its. Falling to `generic` here is the silent wrong-company deliverable, so refuse.
|
|
909
|
+
if (unread.length) {
|
|
910
|
+
const err = new Error(`profile_roster_incomplete — ${unread.length} company profile(s) could not be read `
|
|
911
|
+
+ `(${unread.map((u) => u.file).join(", ")}), so whether this job belongs to one of them cannot be decided. `
|
|
912
|
+
+ `Name the company on the job, or fix the file.`);
|
|
913
|
+
err.code = "profile_roster_incomplete";
|
|
914
|
+
throw err;
|
|
915
|
+
}
|
|
878
916
|
return profiles.get("generic");
|
|
879
917
|
}
|
|
880
918
|
|
package/driver/progress.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
-
// progress.mjs — live run status for the
|
|
3
|
+
// progress.mjs — live run status for the clearance-search driver.
|
|
4
4
|
//
|
|
5
5
|
// Two artifacts, both written by the driver into the FORWARDING agent's own workspace
|
|
6
6
|
// (so the agent's sandboxed read tool can see them — agents can't exec, so on-demand status is a
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// blindly incremented — so the resumable pipeline can re-drive a run without corrupting either file.
|
|
15
15
|
|
|
16
16
|
import { readFileSync, writeFileSync, renameSync, unlinkSync, readdirSync, statSync, existsSync } from "node:fs";
|
|
17
|
-
import { join, dirname } from "node:path";
|
|
17
|
+
import { join, dirname } from "node:path"; import { STUDIO_SEGMENT_RE } from "../shared/pre-rename-spellings.mjs";
|
|
18
18
|
import { DRIVER_DIR } from "../shared/driver-dir.mjs"; //
|
|
19
19
|
import { config } from "./driver.config.mjs";
|
|
20
20
|
import { batchMarkName } from "./mark-name.mjs";
|
|
@@ -29,7 +29,7 @@ import { engineCommit, engineCommitSource } from "./engine-build.mjs"; // —
|
|
|
29
29
|
// driver's execution units (fan-out register axes, skeptic-escalation re-runs, corrective re-synthesis,
|
|
30
30
|
// two refutation passes) onto a clean forward-only sequence, so the displayed step never jumps backward.
|
|
31
31
|
export const DISPLAY_STEPS = [
|
|
32
|
-
"Framing the matter", // 1 matter-frame,
|
|
32
|
+
"Framing the matter", // 1 matter-frame, clearance-variants
|
|
33
33
|
"Register sweeps", // 2 common-law + register-unit:* (fan-out + escalation re-runs collapse here)
|
|
34
34
|
"Placement & digest", // 3 placement-inquiry, register-digest (+ re-digest)
|
|
35
35
|
"Skeptic review", // 4 skeptic
|
|
@@ -48,7 +48,7 @@ export const DISPLAY_STEPS = [
|
|
|
48
48
|
// UNLABELLED GAP on the stepper the client watches — the run looks stalled while it is working. Three
|
|
49
49
|
// stages were sitting in that state (blind-frame, frame-diff, doubt-closure); each now says so by name.
|
|
50
50
|
export const STAGE_TO_STEP = {
|
|
51
|
-
"matter-frame": 0, "
|
|
51
|
+
"matter-frame": 0, "clearance-variants": 0,
|
|
52
52
|
"common-law": 1, "common-law-half": 1, "register-unit": 1,
|
|
53
53
|
"placement-inquiry": 2, "register-digest": 2,
|
|
54
54
|
skeptic: 3,
|
|
@@ -398,11 +398,21 @@ export function seedRunStatus(ctx, { resume = false } = {}) {
|
|
|
398
398
|
}, null, { critical: true }); // door B — the clearance lane's identity seed, same rule
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
//
|
|
401
|
+
// Record that the run is at `rawStageKey`: advance the displayed step where the stage has one, and say
|
|
402
|
+
// what the run is doing either way, then refresh STATUS.md.
|
|
403
|
+
//
|
|
404
|
+
// AN UNMAPPED STAGE MOVES NO STEP AND IS STILL SOMETHING THE RUN IS DOING. This returned early for one,
|
|
405
|
+
// so it wrote no `lastStage` at all — and `lastStage` is what status-snapshot publishes as "what the run
|
|
406
|
+
// is actually doing". Three stages have no display step ON PURPOSE (STAGE_NO_STEP names each and why),
|
|
407
|
+
// so for the whole of any of them every surface went on naming the PREVIOUS stage, which is the same
|
|
408
|
+
// defect as a stale step wearing a different field. The step fields are still withheld — that part of
|
|
409
|
+
// the early return was right, and an unmapped stage must never touch the displayed step.
|
|
402
410
|
export function recordTransition(ctx, rawStageKey) {
|
|
403
411
|
const step = stepForStage(rawStageKey);
|
|
404
|
-
|
|
405
|
-
|
|
412
|
+
writeRunStatus(ctx, {
|
|
413
|
+
...(step ? { stepIndex: step.index, stepLabel: step.label, stepN: step.n, stepTotal: step.total } : {}),
|
|
414
|
+
lastStage: rawStageKey,
|
|
415
|
+
});
|
|
406
416
|
rollupStatus(ctx?.run?.studioRoot);
|
|
407
417
|
}
|
|
408
418
|
|
|
@@ -424,7 +434,7 @@ function findStatusFiles(root, depth, acc) {
|
|
|
424
434
|
}
|
|
425
435
|
|
|
426
436
|
function agentFromStudioRoot(studioRoot) {
|
|
427
|
-
const m = new RegExp(`${config.workspacePrefixRe}([^/]+)/studio
|
|
437
|
+
const m = new RegExp(`${config.workspacePrefixRe}([^/]+)/studio/${STUDIO_SEGMENT_RE}/?$`).exec(studioRoot ?? "");
|
|
428
438
|
return m ? m[1] : "";
|
|
429
439
|
}
|
|
430
440
|
|
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
//
|
|
15
15
|
// This module is read-only and defensive: a missing/unreadable ledger or a torn last line (two gateway
|
|
16
16
|
// turns appending concurrently) returns zeros / skips that line — it never throws. The driver calls it at
|
|
17
|
-
// publish time to attribute THIS run's calls by the session-key prefix `
|
|
17
|
+
// publish time to attribute THIS run's calls by the session-key prefix `clearance-<slug>-<codename>-`.
|
|
18
18
|
|
|
19
|
-
import { readFileSync, existsSync } from "node:fs";
|
|
19
|
+
import { readFileSync, existsSync } from "node:fs"; import { runPrefixSpellings } from "../shared/pre-rename-spellings.mjs";
|
|
20
20
|
// Aliased: three functions below take a parameter literally named `ledgerPath`, and an unaliased
|
|
21
21
|
// import would be shadowed by it inside its own default-value expression (a TDZ ReferenceError
|
|
22
22
|
// at the first call, not at load).
|
|
@@ -46,7 +46,7 @@ export const DEFAULT_LEDGER_PATH = resolveLedgerPath("call");
|
|
|
46
46
|
export const KINDS = ["search", "record_fetch", "image", "phoneme", "batch_screen", "enumerate", "execute_plan", "propose_supplemental"];
|
|
47
47
|
|
|
48
48
|
// The gateway namespaces the driver's --session-key as `agent:<agentId>:<key>` before it reaches the
|
|
49
|
-
// plugin (confirmed on the first live run: sessionKey = `agent:clawdi:
|
|
49
|
+
// plugin (confirmed on the first live run: sessionKey = `agent:clawdi:clearance-<slug>-<codename>-…`). Strip
|
|
50
50
|
// that leading `agent:<id>:` namespace so the run prefix anchors at the real start of the caller's key —
|
|
51
51
|
// otherwise a bare startsWith("clearotron-…") matches nothing.
|
|
52
52
|
function stripGatewayNs(s) {
|
|
@@ -54,11 +54,11 @@ function stripGatewayNs(s) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
// Does this ledger row belong to the run identified by `runPrefix`? The driver's --session-key is
|
|
57
|
-
// `
|
|
57
|
+
// `clearance-<slug>-<codename>-<stage><axis>` (+ optional `-rerunN`), so a prefix match catches every stage +
|
|
58
58
|
// axis + retry of the run. We check sessionKey (carries the key) and, defensively, sessionId.
|
|
59
59
|
function rowMatchesRun(row, runPrefix) {
|
|
60
|
-
return stripGatewayNs(row.sessionKey).startsWith(
|
|
61
|
-
|| stripGatewayNs(row.sessionId).startsWith(
|
|
60
|
+
return runPrefixSpellings(runPrefix).some((rp) => stripGatewayNs(row.sessionKey).startsWith(rp)
|
|
61
|
+
|| stripGatewayNs(row.sessionId).startsWith(rp)); // either spelling: a run resumed across the rename
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
// ── band-truth gate (2026-07-14, teal-foundry): count the ledger rows attributed to ONE unit lane ──────
|
|
@@ -146,7 +146,7 @@ function emptyTally() {
|
|
|
146
146
|
/**
|
|
147
147
|
* Tally every ledger line whose gateway id starts with `runPrefix`.
|
|
148
148
|
* @param {string} ledgerPath path to the JSONL ledger
|
|
149
|
-
* @param {string} runPrefix e.g. `
|
|
149
|
+
* @param {string} runPrefix e.g. `clearance-acme-bluejay-` (note the trailing hyphen)
|
|
150
150
|
* @returns {object} the tally (see emptyTally) — never throws
|
|
151
151
|
*/
|
|
152
152
|
export function tallyRegisterCalls(ledgerPath = DEFAULT_LEDGER_PATH, runPrefix) {
|
|
@@ -208,7 +208,7 @@ export function tallyRegisterCalls(ledgerPath = DEFAULT_LEDGER_PATH, runPrefix)
|
|
|
208
208
|
* (missing/unreadable ledger or a torn last line ⇒ skips that line, never throws). A cache_hit record_fetch
|
|
209
209
|
* still counts — the URI WAS fetched this run, which is exactly what the gate asks.
|
|
210
210
|
* @param {string} ledgerPath path to the JSONL ledger
|
|
211
|
-
* @param {string} runPrefix e.g. `
|
|
211
|
+
* @param {string} runPrefix e.g. `clearance-acme-bluejay-` (note the trailing hyphen)
|
|
212
212
|
* @returns {Set<string>} the set of fetched record URIs
|
|
213
213
|
*/
|
|
214
214
|
export function fetchedRecordUris(ledgerPath = DEFAULT_LEDGER_PATH, runPrefix) {
|
package/driver/publish/index.mjs
CHANGED
|
@@ -16,12 +16,14 @@ import { buildAudit } from './xlsx.mjs';
|
|
|
16
16
|
import { parseFindingsJson, parseFindingsJsonLenient, deriveDisplayVerdict, joinFindingToBlock, CLIENT_TIER_BY_COMPOSITE, projectCoverageJudgment } from '../findings-model.mjs';
|
|
17
17
|
import { readStore, requiredAbsent, nonClosingAbsences } from './publish-inputs.mjs'; // — and why an absence did not close
|
|
18
18
|
import { clearanceReportData } from './report-data.mjs';
|
|
19
|
-
import { searchDepthRecord } from './search-depth.mjs'; // how much was read to reach the answer, as counts and tokens
|
|
19
|
+
import { searchDepthRecord, planTerritoriesOf } from './search-depth.mjs'; import { bandRecords } from '../named-band.mjs'; // how much was read to reach the answer, as counts and tokens
|
|
20
20
|
import { parseFrameworkManifest } from '../framework.mjs';
|
|
21
|
-
import { rollupTokens } from '../tokens.mjs';
|
|
21
|
+
import { rollupTokens, servedModels } from '../tokens.mjs';
|
|
22
22
|
import { reportIdentityFor, productCoverageNote, isRegisterOnly } from '../search-policy.mjs';
|
|
23
23
|
import { readRecordArtifacts, bindFindingsToRecords, joinEvidenceStatus } from '../registry-fidelity.mjs';
|
|
24
24
|
import { deliveryFlagLines } from '../predelivery-lint.mjs';
|
|
25
|
+
import { unrenderableConditions } from '../terminal-clamp.mjs'; // — the conditions that reach no client surface
|
|
26
|
+
import { runLog } from '../log.mjs'; // — one line in the run record when one does
|
|
25
27
|
import { PROVIDERS, config } from '../driver.config.mjs';
|
|
26
28
|
import { declaredRecordOrigins } from '../record-origins.mjs';
|
|
27
29
|
import { NEUTRAL_DELIVERY, loadProfiles } from '../profiles.mjs';
|
|
@@ -791,6 +793,85 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
791
793
|
// the narrative, never in a status line). Best-effort; legacy runs have no sidecar.
|
|
792
794
|
let verdictInfo = null;
|
|
793
795
|
try { verdictInfo = JSON.parse(readFileSync(driverDir(runDir, 'verdict.json'), 'utf8')); } catch { /* legacy */ }
|
|
796
|
+
// ── THE CONDITIONS THAT REACH NO PAGE, RECORDED RATHER THAN LOST ────────────────────────────────
|
|
797
|
+
//
|
|
798
|
+
// A condition whose reader-facing sentence was never stored and cannot be composed from the run
|
|
799
|
+
// record's own counts is dropped instead of printed in the engine's words. That is right for the
|
|
800
|
+
// reader and it is a silent loss for everyone else, so the drop reports itself twice: an ordinary
|
|
801
|
+
// row on the workbook's gaps sheet, and one line in the run record here.
|
|
802
|
+
//
|
|
803
|
+
// THE WORDS ARE THE RUN RECORD'S OWN. The reason is what the clamp site wrote; the state word and the
|
|
804
|
+
// column headings are the gaps sheet's. Nothing on this path composes a sentence.
|
|
805
|
+
//
|
|
806
|
+
// A FRESH RUN NEVER REACHES THIS. Every clamp site stores its clause, so the list is empty and the
|
|
807
|
+
// sheet grows no row and the log gains no line. It bites on a republished archive, which is exactly
|
|
808
|
+
// where the operator has no other way to learn the page is short of a point.
|
|
809
|
+
const droppedConditions = unrenderableConditions(verdictInfo || {}).map((reason) => ({
|
|
810
|
+
area: 'Conditions', state: 'open', note: reason,
|
|
811
|
+
}));
|
|
812
|
+
// Best-effort, in the house pattern: a publish never fails for want of a log line, and a republish
|
|
813
|
+
// from a directory this process cannot write is still a publish.
|
|
814
|
+
if (droppedConditions.length && runDir) {
|
|
815
|
+
try {
|
|
816
|
+
runLog(runDir, { event: 'client-condition-dropped', n: droppedConditions.length,
|
|
817
|
+
reasons: droppedConditions.map((c) => c.note) });
|
|
818
|
+
} catch { /* the workbook row is the record that matters; this line is the second copy */ }
|
|
819
|
+
}
|
|
820
|
+
// ── THE CHECKS THE RUN DECIDED ON AND DID NOT MAKE ─────────────────────────────────────────────
|
|
821
|
+
//
|
|
822
|
+
// The recall net mints a probe per remembered conflict and a probe per owner behind one, then
|
|
823
|
+
// dispatches at most five owner probes. The excess is recorded in the run's own receipt and nothing
|
|
824
|
+
// downstream carried it to a reader, so a search that decided on nineteen ownership checks, made five
|
|
825
|
+
// and said nothing about the other fourteen read as a search that made the checks it wanted.
|
|
826
|
+
//
|
|
827
|
+
// The cap is not the defect: it is deliberate and the rows it drops are ranked material-first, so the
|
|
828
|
+
// five that run are the five that matter most. What was missing is the disclosure, and it lands where
|
|
829
|
+
// the 2026-09-17 ruling put the same class of fact — an ordinary row on the workbook's gaps sheet and
|
|
830
|
+
// one line in the run record. Nothing reaches the report page: the approved boards draw a forward
|
|
831
|
+
// decision and an open question back to the client, and a check nobody made is neither.
|
|
832
|
+
//
|
|
833
|
+
// THE WORDS ARE THE RECEIPT'S OWN. The party and the probe id are read from it verbatim; "over the
|
|
834
|
+
// cap" and "never dispatched" are the vocabulary the ask ledger already ships for these same rows.
|
|
835
|
+
// Nothing here composes a sentence, and the note carries no seam, so the gaps sheet's own splitter
|
|
836
|
+
// leaves "What was done" empty — which is the fact: nothing was done.
|
|
837
|
+
// READ THROUGH THE DECLARED HELPER, three states and not two. An absent receipt is a run whose recall
|
|
838
|
+
// net minted nothing — env-gated off, or a matter with no remembered conflict — and there is nothing
|
|
839
|
+
// to disclose. A DAMAGED one is a different fact: the probes may have overflowed and this publish
|
|
840
|
+
// cannot tell, so it says so in the run record instead of shipping the same empty sheet an
|
|
841
|
+
// everything-dispatched run ships. Collapsing those two is the defect publish-inputs.mjs exists for.
|
|
842
|
+
//
|
|
843
|
+
// NOT PUSHED ONTO `inputsAbsent`. That list rides meta.json's clientGate record, whose own note two
|
|
844
|
+
// hundred lines down is that it stays undefined when every store was found so a complete run's meta
|
|
845
|
+
// is byte-identical to one written before the list existed. Most runs have no recall receipt, so
|
|
846
|
+
// adding this store to it would change the meta of every archived run on re-render for a store that
|
|
847
|
+
// feeds a workbook row and not the gate -- the same reason the other presentation-only stores are
|
|
848
|
+
// not on it either.
|
|
849
|
+
// `runDir ?? dirname(reportMd)`, the same base its neighbours take and not a defensive flourish:
|
|
850
|
+
// publishReport's `runDir` is OPTIONAL, and a bare `readStore(runDir, …)` throws TypeError on an
|
|
851
|
+
// undefined base rather than reporting an absent store. Caught in CI by the graceful-stop runner arm,
|
|
852
|
+
// which publishes without one: the throw left publishReport at `published → null` and the runner
|
|
853
|
+
// exited 1. Reading from the report's own directory is also the right answer for a republish.
|
|
854
|
+
const recallStore = readStore(runDir ?? dirname(reportMd), '_driver/register-recall.json');
|
|
855
|
+
const undispatchedProbes = (recallStore.value?.overflow ?? [])
|
|
856
|
+
.filter((o) => o && (o.term || o.qid))
|
|
857
|
+
.map((o) => ({
|
|
858
|
+
area: String(o.term ?? o.qid),
|
|
859
|
+
state: 'not-searched',
|
|
860
|
+
note: `over the cap, never dispatched (${String(o.qid ?? 'no probe id recorded')})`,
|
|
861
|
+
}));
|
|
862
|
+
if (recallStore.state === 'damaged' && runDir) {
|
|
863
|
+
try {
|
|
864
|
+
runLog(runDir, { event: 'probe-over-cap-unreadable', store: recallStore.name, error: recallStore.error });
|
|
865
|
+
} catch { /* best-effort, as below */ }
|
|
866
|
+
}
|
|
867
|
+
// Best-effort, as above: a publish never fails for want of a log line.
|
|
868
|
+
if (undispatchedProbes.length && runDir) {
|
|
869
|
+
try {
|
|
870
|
+
runLog(runDir, { event: 'probe-over-cap-undispatched', n: undispatchedProbes.length,
|
|
871
|
+
parties: undispatchedProbes.map((p) => p.area) });
|
|
872
|
+
} catch { /* the workbook row is the record that matters; this line is the second copy */ }
|
|
873
|
+
}
|
|
874
|
+
|
|
794
875
|
// doc 50 — the run's FROZEN framework manifest (band vocabulary). Present on band-doctrine runs;
|
|
795
876
|
// absent on every archived run (they render byte-identically on the legacy paths).
|
|
796
877
|
let framework = null;
|
|
@@ -819,11 +900,25 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
819
900
|
// two must not render the same way. Null on archived runs ⇒ render keeps the prose fallback.
|
|
820
901
|
let scopeBasis = null;
|
|
821
902
|
let searchedJurisdictions = [];
|
|
903
|
+
// WHAT WAS SEARCHED IS THE PLAN'S ANSWER, NOT THE ARCHIVE'S, and it is carried separately from the
|
|
904
|
+
// header's list below because the two are used for different things and one of them is deliberately
|
|
905
|
+
// emptied. The coverage presentation asks which territories the run searched and which it could not
|
|
906
|
+
// reach; the plan compiles both — entries carry the regions it will query, `deferred_coverage` carries
|
|
907
|
+
// the ones this provider does not cover with the reason each. A register that archives no records
|
|
908
|
+
// still has both, which is the whole point: keyed off the record store, a provider that keeps nothing
|
|
909
|
+
// looked like a provider nobody asked.
|
|
910
|
+
//
|
|
911
|
+
// THREE-VALUED like the record listing above it: null means there is no plan to read, so the run
|
|
912
|
+
// cannot say — an archived or legacy run — and it must not be confused with a plan that named nothing.
|
|
913
|
+
let planTerritories = null;
|
|
822
914
|
try {
|
|
823
915
|
const plan = JSON.parse(readFileSync(driverDir(runDir ?? dirname(reportMd), 'register-plan.json'), 'utf8'));
|
|
824
916
|
if (plan?.scope_basis === 'worldwide') scopeBasis = 'worldwide';
|
|
825
917
|
searchedJurisdictions = [...new Set((plan?.entries ?? []).flatMap((e) => Array.isArray(e.regions) ? e.regions : []))];
|
|
826
918
|
if (!searchedJurisdictions.length && Array.isArray(plan?.regions)) searchedJurisdictions = [...new Set(plan.regions)];
|
|
919
|
+
// Derived here, BEFORE the worldwide clearing below — that clearing exists for the header and would
|
|
920
|
+
// otherwise empty this on exactly the runs that cover the most ground.
|
|
921
|
+
planTerritories = planTerritoriesOf(plan);
|
|
827
922
|
} catch { /* no plan sidecar — try the instructed scope */ }
|
|
828
923
|
if (!searchedJurisdictions.length) {
|
|
829
924
|
try {
|
|
@@ -902,7 +997,14 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
902
997
|
const reviewReceipts = {
|
|
903
998
|
// `family` rides along for deliveryFlagLines' fallback: a check whose id is not in the projection
|
|
904
999
|
// table still projects to its family's sentence rather than to anything the engine wrote.
|
|
905
|
-
|
|
1000
|
+
// ONE CHECK IS DELIBERATELY NOT PROJECTED. `client-condition-dropped` has no sentence of its own
|
|
1001
|
+
// in the projection table, so it fell through to its family's — "the delivered outcome and the
|
|
1002
|
+
// report's own text do not agree" — which describes a contradiction. This is an omission: the
|
|
1003
|
+
// outcome and the text agree perfectly and both are short of a point. It says what happened on the
|
|
1004
|
+
// gaps sheet and in the run record now, in the run record's own words, so the borrowed sentence
|
|
1005
|
+
// comes off rather than being replaced by a better one (ruled 2026-09-17).
|
|
1006
|
+
lint: (lintSink?.checks ?? []).filter(c => !c.pass && c.id !== 'client-condition-dropped')
|
|
1007
|
+
.map(c => ({ id: c.id ?? '', family: c.family ?? '', detail: c.detail ?? '' })),
|
|
906
1008
|
engagement: integritySink?.engagement ?? null,
|
|
907
1009
|
registryCorrections: Array.isArray(integritySink?.registryCorrections)
|
|
908
1010
|
? integritySink.registryCorrections.filter((c) => c && c.from != null && c.to != null) : [],
|
|
@@ -1032,7 +1134,7 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1032
1134
|
// the same rule (the workbook's own BANNED gate had already started firing on the raw detail —
|
|
1033
1135
|
// advisory, so CI stayed green). reviewReceipts.lint keeps its raw detail for the internal
|
|
1034
1136
|
// readers above (fetchState reads registry-record-coverage's URIs out of it).
|
|
1035
|
-
counts = await buildAudit({ findings, coverage, contextNotes, coverageJudgment, markAssessment, corrections: correctionsDoc, fetchState, verdict: verdictInfo, jurisdiction, commonLawJoinedTerms, registerOnly, clientGate, lintFailures: deliveryFlagLines(reviewReceipts.lint), productName, registerPublishesRecordPages: runOrigins == null ? null : runOrigins.length > 0, recordLinks: officeLinks?.byUri ?? null }, auditParsed, join(poolRunDir, auditFile), fm.title, fm);
|
|
1137
|
+
counts = await buildAudit({ droppedConditions, undispatchedProbes, findings, coverage, contextNotes, coverageJudgment, markAssessment, corrections: correctionsDoc, fetchState, verdict: verdictInfo, jurisdiction, commonLawJoinedTerms, registerOnly, clientGate, lintFailures: deliveryFlagLines(reviewReceipts.lint), productName, registerPublishesRecordPages: runOrigins == null ? null : runOrigins.length > 0, recordLinks: officeLinks?.byUri ?? null }, auditParsed, join(poolRunDir, auditFile), fm.title, fm);
|
|
1036
1138
|
grpRead(join(poolRunDir, auditFile), 0o640);
|
|
1037
1139
|
if (counts?.gateViolations?.length) console.warn(`[audit-workbook] advisory: ${counts.gateViolations.join(' | ')}`);
|
|
1038
1140
|
} catch (e) {
|
|
@@ -1053,6 +1155,11 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1053
1155
|
// rather than wire the overlay in: client names saturate privileged report prose — a partial blur is a
|
|
1054
1156
|
// worse demo than no toggle.
|
|
1055
1157
|
const reportNav = siteNav(poolRoot, 'report', null, '../', { anon: false });
|
|
1158
|
+
// The models that served this run, named as a client may read them (tokens.mjs servedModels). The closing
|
|
1159
|
+
// line of the report's scope section, the data file and the meta below all take this one read; a read
|
|
1160
|
+
// that throws records nothing rather than failing the publish.
|
|
1161
|
+
let served = null;
|
|
1162
|
+
try { served = runDir ? servedModels(runDir) : null; } catch { served = null; }
|
|
1056
1163
|
// `demoData` is resolved above the report.md write — one answer, every surface.
|
|
1057
1164
|
// ── HOW MUCH WAS READ TO REACH THE ANSWER ──────────────────────────────────────────────────────
|
|
1058
1165
|
// Derived here rather than in a stage so a REPUBLISH of an archived run picks the fields up with no
|
|
@@ -1060,6 +1167,35 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1060
1167
|
// pattern — a run with no grid or no case-law layer still gets its register counts, and the absent
|
|
1061
1168
|
// ones report themselves as zero or `not-in-scope` rather than as a gap nobody can see.
|
|
1062
1169
|
let searchDepth = null;
|
|
1170
|
+
// HOW DEEP THE LOCAL-LANGUAGE INVESTIGATION WENT, and it is derived by the one author of that
|
|
1171
|
+
// question rather than re-read here. The import is LAZY and gated on the run's own sidecar existing,
|
|
1172
|
+
// which keeps the property the pipeline's own fold keeps: a plain clearance never loads the jx
|
|
1173
|
+
// machinery at all. No sidecar means the component never ran, which the fold reports as not-in-scope.
|
|
1174
|
+
let laneDepthVerdicts = null;
|
|
1175
|
+
try {
|
|
1176
|
+
const jxSidecar = driverDir(runDir ?? dirname(reportMd), 'jx-lanes.json');
|
|
1177
|
+
if (existsSync(jxSidecar)) {
|
|
1178
|
+
const sidecar = JSON.parse(readFileSync(jxSidecar, 'utf8'));
|
|
1179
|
+
const { laneDepthOfRun } = await import('../jx.mjs');
|
|
1180
|
+
let units = null;
|
|
1181
|
+
try { units = JSON.parse(readFileSync(driverDir(runDir ?? dirname(reportMd), 'jx/units.json'), 'utf8')); } catch { /* the statement handles an absent units file */ }
|
|
1182
|
+
// THE RUN ALREADY ANSWERED THIS, AND THE STAMPED ANSWER IS THE ONE THAT COUNTS.
|
|
1183
|
+
//
|
|
1184
|
+
// `deriveJxSliceStatement` writes `fold.depth` at delivery, from the run's OWN environment, for the
|
|
1185
|
+
// reason the seam exists at all: the arms are environment, so a verdict derived later can only speak
|
|
1186
|
+
// for the box it is derived on. Publishing re-derived it here — a second author for a fact the run
|
|
1187
|
+
// had already stated — and the two disagreed on a delivered client report.
|
|
1188
|
+
//
|
|
1189
|
+
// The disagreement was not subtle. This call passed `deriveJxSliceStatement`'s whole return,
|
|
1190
|
+
// `{executes, slices}`, where the function reads `slices.candidates` — so the lookup found nothing,
|
|
1191
|
+
// every lane's `ran` came back null whatever it had really done, and a full-country JP run that
|
|
1192
|
+
// searched in Japanese printed "Local-language investigation · Not run this run". The stamped
|
|
1193
|
+
// verdict on the same run's sidecar said `ran: "candidates"`, correctly.
|
|
1194
|
+
//
|
|
1195
|
+
// So: read what the run stated, and derive only for a run delivered before the stamp existed.
|
|
1196
|
+
laneDepthVerdicts = laneDepthOfRun({ sidecar, units });
|
|
1197
|
+
}
|
|
1198
|
+
} catch { /* an unreadable sidecar reports as not-in-scope rather than failing a publish */ }
|
|
1063
1199
|
try {
|
|
1064
1200
|
const runBase = runDir ?? dirname(reportMd);
|
|
1065
1201
|
const recDir = join(runBase, '_records');
|
|
@@ -1068,15 +1204,22 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1068
1204
|
searchDepth = searchDepthRecord({
|
|
1069
1205
|
auditMd: (auditMd && existsSync(auditMd)) ? rdText(auditMd) : '',
|
|
1070
1206
|
recordIndex: recordsByUri ?? {},
|
|
1071
|
-
|
|
1207
|
+
// null, NOT []: a run whose provider archives no records has no `_records/` at all, and an empty
|
|
1208
|
+
// array is a register that was searched and returned nothing. They are different facts and the
|
|
1209
|
+
// page says different things about them, so the distinction this line already computes is kept
|
|
1210
|
+
// rather than thrown away one character later.
|
|
1211
|
+
recordFileNames: existsSync(recDir) ? readdirSync(recDir) : null,
|
|
1212
|
+
// …and where there is no archive, what the register returned: the band's own record ids.
|
|
1213
|
+
bandRecordIds: existsSync(recDir) ? null : (() => { try { const b = rdJson(join(runBase, 'register-named-band.json')); return b ? bandRecords(b).map((r) => r?.record_id).filter(Boolean) : null; } catch { return null; } })(),
|
|
1072
1214
|
commonLawGrid: rdJson(join(runBase, 'common-law-grid.json')),
|
|
1073
1215
|
caseLawText: rdText(join(dirname(reportMd), 'case-law-findings.md')),
|
|
1074
1216
|
registerPlan: rdJson(driverDir(runBase, 'register-plan.json')),
|
|
1217
|
+
laneDepthVerdicts,
|
|
1075
1218
|
});
|
|
1076
1219
|
writeRO('search-depth.json', JSON.stringify(searchDepth, null, 2));
|
|
1077
1220
|
} catch { /* the depth record is additive — a publish never fails for want of it */ }
|
|
1078
1221
|
|
|
1079
|
-
writeRO('report.html', renderHtml(parsed, findings, coverage, { demoData, productName, depthNote, scopeBasis, auditFile: auditFile || undefined, runId, delivery: deliv, recordsByUri, contextNotes, coverageJudgment: coverageJudgmentDisplay, markAssessment, fourAnswers, homeHref: '../index.html', nav: reportNav, chromeHref: '../assets/chrome.css', issued, asOf, verdictInfo, framework, searchedJurisdictions, caseLawByOrdinal, caseLawNotice, enforcerSignals, recordOrigin, recordOrigins: runOrigins, recordCitation: runProviderConf?.recordCitation ?? null, recordLinks: officeLinks?.byUri ?? null, providerLabel, seniorRights, findingsSchemaVersion, searchDepth }));
|
|
1222
|
+
writeRO('report.html', renderHtml(parsed, findings, coverage, { demoData, servedModels: served, productName, depthNote, scopeBasis, auditFile: auditFile || undefined, runId, delivery: deliv, recordsByUri, contextNotes, coverageJudgment: coverageJudgmentDisplay, markAssessment, fourAnswers, homeHref: '../index.html', nav: reportNav, chromeHref: '../assets/chrome.css', issued, asOf, verdictInfo, framework, searchedJurisdictions, planTerritories, caseLawByOrdinal, caseLawNotice, enforcerSignals, recordOrigin, recordOrigins: runOrigins, recordCitation: runProviderConf?.recordCitation ?? null, recordLinks: officeLinks?.byUri ?? null, providerLabel, seniorRights, findingsSchemaVersion, searchDepth }));
|
|
1080
1223
|
// ONE report (spec 2026-07-30 §5): report.client.html is no longer written. The knockout lane's own
|
|
1081
1224
|
// collapse note is the precedent: "two renderings of one run is how the wrong link gets sent". The
|
|
1082
1225
|
// client host serves the same report.html through the portal's readReport() (cleaning built in) — its
|
|
@@ -1150,7 +1293,7 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1150
1293
|
// is the LAST resort (a level today's registry no longer knows). meta.json below keeps the frozen
|
|
1151
1294
|
// stamp BY DESIGN — its readers re-derive.
|
|
1152
1295
|
auditFile, searchLevel: searchPolicy?.level ?? null, stageLabel: stageLabel ?? searchPolicy?.stageLabel ?? null,
|
|
1153
|
-
engineCommit: engineCommit(),
|
|
1296
|
+
engineCommit: engineCommit(), servedModels: served,
|
|
1154
1297
|
framework, verdictInfo, findings, coverage, contextNotes, markAssessment, fourAnswers, askAnswers,
|
|
1155
1298
|
actions: actionsRegister, jurisdiction, searchedJurisdictions, scopeBasis, caption: fm.overall_caption ?? null,
|
|
1156
1299
|
});
|
|
@@ -1175,6 +1318,9 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1175
1318
|
issuedAt,
|
|
1176
1319
|
// WHICH BUILD produced this. null off a git checkout — a provenance stamp never fails a publish.
|
|
1177
1320
|
engineCommit: engineCommit(),
|
|
1321
|
+
// The models that served the run, named as a client may read them (tokens.mjs servedModels). Absent when
|
|
1322
|
+
// nothing was read, so a meta from before the record keeps its shape; [] when turns ran and named none a client may read.
|
|
1323
|
+
servedModels: served ?? undefined,
|
|
1178
1324
|
kind: 'clearance', recordLinks: officeLinks?.tally ?? undefined, // per office: linked, or cited by number and why; only where the register has no record pages
|
|
1179
1325
|
searchLevel: searchPolicy?.level ?? undefined,
|
|
1180
1326
|
// Display-only face of the level ("Depth 4"), frozen alongside it so the list can show which reads
|
|
@@ -1445,7 +1591,7 @@ export function composeEmailBody(reportMdPath, url, auditFile, productName = nul
|
|
|
1445
1591
|
const FONT = "font-family:Calibri,'Segoe UI',Arial,sans-serif";
|
|
1446
1592
|
|
|
1447
1593
|
// 5-tier reporting-template risk scheme — the whole left "NAME / RISK RATING" cell takes the bg colour.
|
|
1448
|
-
// Matches skills/
|
|
1594
|
+
// Matches skills/clearance-search/templates/search-request-form.html + knockout-searches/template-formatting.md.
|
|
1449
1595
|
// `txt` = a readable text-colour version of the tier (for the EXECUTIVE SUMMARY risk phrase, where a bright
|
|
1450
1596
|
// fill like yellow/red is unreadable as text). Order matters: VERY HIGH before HIGH.
|
|
1451
1597
|
export const RISK_TIERS = [
|