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
|
@@ -29,9 +29,11 @@ import { join } from "node:path";
|
|
|
29
29
|
import { runStreamingChild, absolutizeSkillRefs, WRITE_DISCIPLINE, buildEnvelope, resolveSpawnCwd } from "./common.mjs";
|
|
30
30
|
import { renderCodexConfigToml } from "./mcp/codex-config.mjs";
|
|
31
31
|
import { resolveAuthMode } from "./auth.mjs";
|
|
32
|
-
import {
|
|
32
|
+
import { resolveEngineProgram } from "../driver.config.mjs"; // — the one place that finds the program; it reads every spelling of the setting
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
// The same one resolver as the claude adapter (driver.config.mjs resolveEngineProgram), for the same reason:
|
|
35
|
+
// the absolute path it found, or what was asked for when it found nothing.
|
|
36
|
+
const codexBin = () => { const r = resolveEngineProgram("openai-agent"); return r.resolved ?? r.bin; };
|
|
35
37
|
|
|
36
38
|
// tier/alias → codex `-m` model id. opus/sonnet/haiku are the driver's abstract tiers (CONTRACT §3). The
|
|
37
39
|
// GPT ids are ENV-OVERRIDABLE and default to three DISTINCT rungs of the codex ladder — `gpt-5.6-sol`,
|
package/driver/engine/probe.mjs
CHANGED
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
// The one place the real adapter is exercised, it is pointed at `driver/test/mock-claude.mjs` through
|
|
55
55
|
// `CLEAROTRON_CLAUDE_PATH` — the same offline fixture the engine tests already spawn.
|
|
56
56
|
|
|
57
|
-
import { ENGINE_BINARIES, DEFAULT_ENGINE_ID, engineAdapterSpecifier } from "../driver.config.mjs";
|
|
58
|
-
import { resolveAuthMode } from "./auth.mjs";
|
|
57
|
+
import { ENGINE_BINARIES, DEFAULT_ENGINE_ID, engineAdapterSpecifier, resolveEngineProgram } from "../driver.config.mjs";
|
|
58
|
+
import { resolveAuthMode, CLOUD_SETTINGS, CLOUD_CREDENTIAL_CHECK } from "./auth.mjs";
|
|
59
59
|
|
|
60
60
|
/** Six words. Short enough to be free in practice, and it still requires a real completed turn. */
|
|
61
61
|
export const PROBE_PROMPT = "Reply with the single word: ok.";
|
|
@@ -90,44 +90,97 @@ const tail = (s) => {
|
|
|
90
90
|
return t.length > DETAIL_CHARS ? `…${t.slice(-DETAIL_CHARS)}` : t;
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* An engine instruction that names its program (`run \`claude\` once…`, `claude setup-token`), rewritten to
|
|
95
|
+
* name the copy that will actually run when that copy is the one Clearotron installed. That copy is not on
|
|
96
|
+
* PATH, so for it the bare word is a command the reader's shell cannot
|
|
97
|
+
* find, at the one step nobody can do for them. Any other copy is on PATH or named by path already, and
|
|
98
|
+
* the text is returned unchanged.
|
|
99
|
+
*
|
|
100
|
+
* HERE, AND RE-EXPORTED BY SETUP. Setup's own screens used it and the probe's sign-in advice, which doctor
|
|
101
|
+
* and the run door print, did not, so after setup's install doctor told the reader to run a `claude` their
|
|
102
|
+
* shell does not have. The probe cannot import the wizard, so the one copy lives on this side.
|
|
103
|
+
*/
|
|
104
|
+
export function namingProgram(text, eng, bin) {
|
|
105
|
+
if (!text || bin?.source !== "installed" || !bin.path) return text;
|
|
106
|
+
const program = /\s/.test(bin.path) ? `"${bin.path}"` : bin.path;
|
|
107
|
+
return String(text).replace(new RegExp(`(^|\`)${eng.fallback}(?=[\\s\`]|$)`, "g"), (_, before) => `${before}${program}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
// THE HEADLESS ROUTE, WHERE THE ENGINE HAS ONE. The interactive sign-in is the one thing a server with no
|
|
94
111
|
// browser cannot do, and it was the only remedy this offered — including to a box that had configured the
|
|
95
112
|
// route built for servers. The engine table already carries that route; this reads it rather than a copy.
|
|
96
|
-
|
|
113
|
+
//
|
|
114
|
+
// `program` is the copy that ran ({ source, path }, or null), and every command run ON THIS MACHINE is named
|
|
115
|
+
// through `namingProgram`. The token route's command can run on any machine, so it keeps the bare word,
|
|
116
|
+
// with this machine's copy named beside it, as setup names it.
|
|
117
|
+
const signInLine = (engine, program = null) => {
|
|
97
118
|
const spec = ENGINE_BINARIES[engine];
|
|
98
|
-
const base = spec?.signIn
|
|
119
|
+
const base = spec?.signIn ? namingProgram(spec.signIn, spec, program) : "sign the CLI in";
|
|
99
120
|
const h = spec?.headless;
|
|
100
121
|
// Both forms the wizard already offers, read off the same table: a TOKEN route is run elsewhere and
|
|
101
|
-
// carried here by variable; a DEVICE route is run on this
|
|
122
|
+
// carried here by variable; a DEVICE route is run on this machine and signs it in directly. The route is
|
|
123
|
+
// named by where a sign-in can be completed, as INSTALL.md's sign-in table names it.
|
|
102
124
|
if (!h?.cmd) return base;
|
|
125
|
+
const here = namingProgram(h.cmd, spec, program);
|
|
103
126
|
return h.tokenEnv
|
|
104
|
-
? `${base} — or, on a
|
|
105
|
-
: `${base} — or, on a
|
|
127
|
+
? `${base} — or, on a machine you cannot complete a sign-in on, run \`${h.cmd}\` on any machine you can sign in on${here !== h.cmd ? ` (on this one, \`${here}\`)` : ""} and set the token it prints as ${h.tokenEnv} in this install's environment file`
|
|
128
|
+
: `${base} — or, on a machine you cannot complete a sign-in on, run \`${here}\` here`;
|
|
106
129
|
};
|
|
107
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Who refused the credentials and what to check, for a turn paid through an API key or a cloud account:
|
|
133
|
+
* `{ who, what, check }`, or null on a subscription, whose remedy is the sign-in above. `auth` is the
|
|
134
|
+
* resolver's answer for the turn ({ mode, cloud }). Names only, never a value.
|
|
135
|
+
*
|
|
136
|
+
* THE SIGN-IN IS A SUBSCRIPTION'S REMEDY AND NO OTHER'S. A cloud that refuses the credentials, or a key the
|
|
137
|
+
* vendor refuses, answers 401 or 403 like a signed-out program, and the advice was to run the program once
|
|
138
|
+
* and sign in: nothing to sign in to on a cloud, and a sign-in the adapter would not use under a key.
|
|
139
|
+
*/
|
|
140
|
+
export function credentialCheck(engine, auth) {
|
|
141
|
+
if (auth?.mode === "cloud") {
|
|
142
|
+
const c = CLOUD_CREDENTIAL_CHECK[auth.cloud];
|
|
143
|
+
return c ? { who: c.who, what: "the credentials", check: c.check } : null;
|
|
144
|
+
}
|
|
145
|
+
const spec = ENGINE_BINARIES[engine];
|
|
146
|
+
if (auth?.mode === "api-key" && spec?.apiKeyEnv) return { who: spec.vendor, what: "the API key", check: spec.apiKeyEnv };
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const capitalised = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
150
|
+
|
|
108
151
|
/**
|
|
109
152
|
* One verdict from one turn. PURE — no clock, no filesystem, no process.
|
|
110
153
|
*
|
|
111
154
|
* `tuple` is the engine's normalized return (engine/CONTRACT.md §1); `error` is a THROW, which is a
|
|
112
155
|
* distinct class and not a returned failure: `openai-agent.runTurn` throws for both of its auth shapes
|
|
113
156
|
* (resolveAuthMode on api-key-without-key, and the auth.json refusal) rather than settling a tuple.
|
|
157
|
+
*
|
|
158
|
+
* `auth` is how the turn was paid for (resolveAuthMode's `{ mode, cloud }`) and `program` the copy that ran
|
|
159
|
+
* (`{ source, path }`); both only shape the advice, never the mode, so the run door refuses exactly what it
|
|
160
|
+
* refused before. Absent, the advice is the subscription's, naming the bare program word.
|
|
114
161
|
*/
|
|
115
|
-
export function classifyProbe({ engine, tuple = null, error = null, timeoutSec = PROBE_TIMEOUT_SEC } = {}) {
|
|
162
|
+
export function classifyProbe({ engine, tuple = null, error = null, timeoutSec = PROBE_TIMEOUT_SEC, auth = null, program = null } = {}) {
|
|
116
163
|
const id = String(engine ?? "").trim().toLowerCase();
|
|
117
164
|
const v = (mode, basis, headline, fix, extra = {}) =>
|
|
118
165
|
({ ok: false, engine: id, mode, basis, headline, fix, detail: null, ...extra });
|
|
119
166
|
|
|
120
167
|
// ── a THROW ────────────────────────────────────────────────────────────────────────────────────────
|
|
121
|
-
// The thrown text is relayed
|
|
122
|
-
//
|
|
123
|
-
//
|
|
168
|
+
// The thrown text is relayed as the fix wherever the thrower already says what to do. Those messages
|
|
169
|
+
// were written by the module that owns the decision (auth.mjs owns the billing refusal, the codex
|
|
170
|
+
// adapter owns `codex login`); paraphrasing them here creates a second wording that drifts.
|
|
171
|
+
//
|
|
172
|
+
// ONE CHANGE ONLY, AND ONLY TO A SIGN-IN: the program's name. The codex adapter refuses a subscription
|
|
173
|
+
// with no sign-in by throwing "run `codex login`" before it starts anything, which is the commonest way a
|
|
174
|
+
// signed-out Codex reaches this line. Relayed as thrown, that told a reader whose only copy is the one
|
|
175
|
+
// setup installed, which is not on PATH, to run a command their shell does not have. `namingProgram`
|
|
176
|
+
// rewrites the bare word for that copy and leaves every other copy's text as it was thrown.
|
|
124
177
|
if (error) {
|
|
125
178
|
const msg = String(error?.message ?? error);
|
|
126
|
-
if (/=api-key but/i.test(msg))
|
|
179
|
+
if (error?.billingRefusal === true || /=api-key but/i.test(msg)) // auth.mjs marks every billing refusal
|
|
127
180
|
return v("auth-misconfigured", "config",
|
|
128
|
-
`${id} cannot start: the billing mode this box declares has no key`, msg, { detail: null });
|
|
181
|
+
`${id} cannot start: ${/=api-key but/i.test(msg) ? "the billing mode this box declares has no key" : "the billing setting this box declares is refused"}`, msg, { detail: null });
|
|
129
182
|
if (SIGNED_OUT_RE.test(msg))
|
|
130
|
-
return v("signed-out", "config", `${id} is not signed in`, msg);
|
|
183
|
+
return v("signed-out", "config", `${id} is not signed in`, ENGINE_BINARIES[id] ? namingProgram(msg, ENGINE_BINARIES[id], program) : msg);
|
|
131
184
|
if (TIER_RE.test(msg))
|
|
132
185
|
return v("tier-unavailable", "config", `${id} cannot reach the model it was asked for`, tierFix(id, msg));
|
|
133
186
|
return v("failed", "throw", `${id} could not run a turn`, msg);
|
|
@@ -144,7 +197,11 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
|
|
|
144
197
|
// which pipe this happened to look at.
|
|
145
198
|
const detail = tail(tuple.stderr) ?? tail(tuple.stdout);
|
|
146
199
|
|
|
147
|
-
|
|
200
|
+
// A completed turn names what served it, the model and the provider as the program reported them, and
|
|
201
|
+
// null where it named neither, so a proof says which model and whose account it proved.
|
|
202
|
+
if (tuple.code === 0) return { ok: true, engine: id, mode: "ok", basis: "completed-turn", headline: `${id} completed a turn`, fix: null, detail: null,
|
|
203
|
+
served: typeof tuple.modelWire === "string" && tuple.modelWire ? tuple.modelWire : null,
|
|
204
|
+
provider: typeof tuple.providerWire === "string" && tuple.providerWire ? tuple.providerWire : null };
|
|
148
205
|
|
|
149
206
|
// spawn itself failed. The filesystem preflight normally catches this first; when it does not, say so
|
|
150
207
|
// in the binary's own vocabulary rather than as a mysterious engine fault.
|
|
@@ -163,9 +220,15 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
|
|
|
163
220
|
{ resetsAt: s.resetsAt ?? null, detail });
|
|
164
221
|
}
|
|
165
222
|
|
|
223
|
+
// THE MODE STAYS `signed-out` UNDER EVERY WAY OF PAYING, because the run door refuses on the mode and a
|
|
224
|
+
// refused credential is as much this machine's to fix as a signed-out program. Only the words follow how
|
|
225
|
+
// the turn is paid for.
|
|
226
|
+
const refused = credentialCheck(id, auth);
|
|
166
227
|
if (SIGNED_OUT_RE.test(text))
|
|
167
|
-
return
|
|
168
|
-
|
|
228
|
+
return refused
|
|
229
|
+
? v("signed-out", "text-match", `${capitalised(refused.who)} refused ${refused.what}`, `check ${refused.check}, then run this again.`, { detail })
|
|
230
|
+
: v("signed-out", "text-match", `${id} is not signed in`,
|
|
231
|
+
`Sign in: ${signInLine(id, program)}, then run this again. Setup does not do it for you — the CLI owns its own login.`, { detail });
|
|
169
232
|
|
|
170
233
|
if (TIER_RE.test(text))
|
|
171
234
|
return v("tier-unavailable", "text-match", `${id} cannot reach the model it was asked for`, tierFix(id, text), { detail });
|
|
@@ -179,7 +242,9 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
|
|
|
179
242
|
// from the shape rather than read from a message.
|
|
180
243
|
if (s.noStreamEvents)
|
|
181
244
|
return v("signed-out", "startup-class", `${id} exited before it produced anything`,
|
|
182
|
-
|
|
245
|
+
refused
|
|
246
|
+
? `That is what refused credentials look like, so start there: check ${refused.check}, then run this again. The engine's stderr below is the diagnosis if it is something else.`
|
|
247
|
+
: `That is the signed-out shape, so start there: ${signInLine(id, program)}, then run this again. The engine's stderr below is the diagnosis if it is something else.`, { detail });
|
|
183
248
|
|
|
184
249
|
return v("failed", "nonzero-exit", `${id} ran but the turn failed (exit ${tuple.code})`,
|
|
185
250
|
"The engine's stderr below is the whole story; a turn that starts and fails is not a configuration this check can name.", { detail });
|
|
@@ -292,11 +357,15 @@ export function probeWeatherWarning(verdict) {
|
|
|
292
357
|
* and a caller building the environment it hands this module fills from it. `doctor` kept a second copy
|
|
293
358
|
* that had dropped both credentials, so it filled a probe environment without the very token it then
|
|
294
359
|
* reported missing. Exported so there is nothing left to copy.
|
|
360
|
+
*
|
|
361
|
+
* The cloud a Claude turn is sent to and paid through is on it too (`CLOUD_SETTINGS`, from auth.mjs).
|
|
362
|
+
* Without those names a cloud answered in setup would pass the resolver, which reads the caller's
|
|
363
|
+
* environment, and never reach the turn, which reads this process's: a proof of an account no run bills.
|
|
295
364
|
*/
|
|
296
365
|
export function engineEnvKeys() {
|
|
297
366
|
return [...new Set(["CLEAROTRON_AI", ...Object.values(ENGINE_BINARIES)
|
|
298
367
|
.flatMap((s) => [s.env, s.authEnv, s.apiKeyEnv, s.headless?.tokenEnv])
|
|
299
|
-
.filter(Boolean)])];
|
|
368
|
+
.filter(Boolean), ...CLOUD_SETTINGS])];
|
|
300
369
|
}
|
|
301
370
|
|
|
302
371
|
function applyEngineEnv(env) {
|
|
@@ -346,6 +415,12 @@ async function defaultLoadAdapter(engine) {
|
|
|
346
415
|
/**
|
|
347
416
|
* Run the probe and return a verdict. Never throws for a configuration fault — a caller that wants a
|
|
348
417
|
* refusal calls `preflightEngineTurn`, and a caller that wants to report calls this.
|
|
418
|
+
*
|
|
419
|
+
* `program` is the copy the caller has already found and is running (`{ source, path }`), for a caller
|
|
420
|
+
* that knows more about it than its setting says. Setup pins the program setting to the absolute path of
|
|
421
|
+
* the copy it proves, so the turn runs exactly that copy; resolved from that setting alone, the copy setup
|
|
422
|
+
* installed reads as a path the reader set, and the advice then named the bare word that copy does not
|
|
423
|
+
* answer to. Without it, the probe resolves the copy itself.
|
|
349
424
|
*/
|
|
350
425
|
export async function probeEngineTurn({
|
|
351
426
|
env = process.env,
|
|
@@ -354,6 +429,7 @@ export async function probeEngineTurn({
|
|
|
354
429
|
loadAdapter = defaultLoadAdapter,
|
|
355
430
|
timeoutSec = PROBE_TIMEOUT_SEC,
|
|
356
431
|
stallSec = PROBE_STALL_SEC,
|
|
432
|
+
program: knownProgram = null,
|
|
357
433
|
} = {}) {
|
|
358
434
|
const id = String(env.CLEAROTRON_AI || DEFAULT_ENGINE_ID).trim().toLowerCase();
|
|
359
435
|
if (!ENGINE_BINARIES[id]) {
|
|
@@ -366,8 +442,10 @@ export async function probeEngineTurn({
|
|
|
366
442
|
};
|
|
367
443
|
}
|
|
368
444
|
|
|
369
|
-
// The billing-mode door, before anything spawns — a fail-loud config error must not cost a turn.
|
|
370
|
-
|
|
445
|
+
// The billing-mode door, before anything spawns — a fail-loud config error must not cost a turn. Its
|
|
446
|
+
// answer is kept: a refusal from a cloud or of a key is advised on differently from a signed-out program.
|
|
447
|
+
let auth;
|
|
448
|
+
try { auth = resolveAuthMode({ engineName: id, env }); }
|
|
371
449
|
catch (e) { return classifyProbe({ engine: id, error: e, timeoutSec }); }
|
|
372
450
|
|
|
373
451
|
let turn = injectedRunTurn;
|
|
@@ -377,11 +455,20 @@ export async function probeEngineTurn({
|
|
|
377
455
|
}
|
|
378
456
|
|
|
379
457
|
const restore = applyEngineEnv(env);
|
|
458
|
+
let program = null;
|
|
380
459
|
try {
|
|
460
|
+
// THE COPY THAT RUNS, resolved the way the adapter resolves it: by the one resolver, inside the
|
|
461
|
+
// environment the turn runs in. The sign-in advice names it when it is the copy Clearotron installed,
|
|
462
|
+
// which is not on PATH. AFTER applyEngineEnv, NEVER BEFORE: the resolver reads this process's
|
|
463
|
+
// environment, and the caller's program setting is only there from that line until `restore()`, so above
|
|
464
|
+
// it this would name whatever copy the shell happened to point at. Inside the try, so the `finally` puts
|
|
465
|
+
// the environment back whatever it does. A resolver that cannot answer leaves the bare word.
|
|
466
|
+
if (knownProgram?.path) program = { source: knownProgram.source ?? null, path: knownProgram.path };
|
|
467
|
+
else try { const r = resolveEngineProgram(id); program = r.resolved ? { source: r.source, path: r.resolved } : null; } catch { /* the bare word */ }
|
|
381
468
|
const tuple = await turn({ message: PROBE_PROMPT, model: PROBE_MODEL, thinking: PROBE_THINKING, timeoutSec, stallSec });
|
|
382
|
-
return classifyProbe({ engine: id, tuple, timeoutSec });
|
|
469
|
+
return classifyProbe({ engine: id, tuple, timeoutSec, auth, program });
|
|
383
470
|
} catch (e) {
|
|
384
|
-
return classifyProbe({ engine: id, error: e, timeoutSec });
|
|
471
|
+
return classifyProbe({ engine: id, error: e, timeoutSec, auth, program });
|
|
385
472
|
} finally {
|
|
386
473
|
restore();
|
|
387
474
|
}
|
|
@@ -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
|
-
// Job-file shape for studio/
|
|
3
|
+
// Job-file shape for studio/clearance-search/queue/<id>.json (written by email-loop on a clearance-search request).
|
|
4
4
|
// The job id = sanitized email message-id so a re-delivered webhook overwrites the same file (no duplicate run).
|
|
5
5
|
//
|
|
6
6
|
// Blocking semantics follow change-spec v3 §B2: the ONLY content reason a search may not start is the
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// with a `noref<hash>` slug (see phase0.mjs deriveSlug). The intake confirmation brief (email-loop §6)
|
|
11
11
|
// resolves ambiguity BEFORE enqueue; this validator is the runner-side mechanical backstop.
|
|
12
12
|
|
|
13
|
-
import { loadProfiles, loadProjects, resolveProfile, applicantMatchesProfile, recipeProseGuard, platformEntryErrors } from "./profiles.mjs";
|
|
13
|
+
import { loadProfiles, loadProjects, resolveProfile, applicantMatchesProfile, recipeProseGuard, platformEntryErrors , unreadableProfiles } from "./profiles.mjs";
|
|
14
14
|
import { demoRunShape } from "./demo-run-agreement.mjs";
|
|
15
15
|
import { ORDERABLE_PRODUCTS, policyFor, checkMarkBudget, checkScopeAgainstPolicy, loadRecipes, kebabCollisions, resolveSearchPolicy } from "./search-policy.mjs";
|
|
16
16
|
// — the §B2 gate resolves the subject through the SAME ladder the run uses. See the gate itself.
|
|
@@ -776,6 +776,10 @@ export function validateJob(job, { atClaim = false } = {}) {
|
|
|
776
776
|
profiles = loadProfiles({ force: true });
|
|
777
777
|
known = profiles.has(key);
|
|
778
778
|
}
|
|
779
|
+
// A company whose own file would not load is a known company that could not be READ — the same
|
|
780
|
+
// failure to look as a store that cannot be read, and handled the same way below, never "no such
|
|
781
|
+
// customer". The file's reason travels with the run's own refusal (resolveProfile).
|
|
782
|
+
if (!known) known = new Set(unreadableProfiles(profiles).map((u) => u.key)).has(key);
|
|
779
783
|
roster = [...profiles.keys()].sort();
|
|
780
784
|
} catch { known = true; }
|
|
781
785
|
// NAME THE ROSTER THIS PROCESS CAN SEE, always.
|
|
@@ -590,8 +590,11 @@ export function riskStatement({ tier, verdict, reasons, basis, clauses } = {}) {
|
|
|
590
590
|
const basisNote = registerOnly ? " Register findings only — no common-law or marketplace search was run." : "";
|
|
591
591
|
if (v === "BLOCKING") return `On hold — the reviewing lawyer's open questions must be resolved before any recommendation.${basisNote}`;
|
|
592
592
|
if (v === "CONDITIONAL") {
|
|
593
|
-
|
|
594
|
-
|
|
593
|
+
// A clause stored as explicit null is a condition ruled to the run record alone: it is not the lede,
|
|
594
|
+
// it is not counted, and its reason is never the fallback text. `undefined` (legacy/short) is not null.
|
|
595
|
+
const cs = Array.isArray(clauses) ? clauses : [];
|
|
596
|
+
const conds = (Array.isArray(reasons) ? reasons : []).filter((r, i) => cs[i] !== null).map((r) => String(r ?? "").trim()).filter(Boolean);
|
|
597
|
+
const cls = cs.map((c) => String(c ?? "").trim()).filter(Boolean);
|
|
595
598
|
const lede = cls[0] ?? conds[0] ?? "the open conditions carried in the report";
|
|
596
599
|
const first = clipClause(sentenceCaseLead(lede), STATEMENT_CLAUSE_MAX);
|
|
597
600
|
const n = conds.length || cls.length;
|
|
@@ -1732,7 +1735,7 @@ function validateNet(f, ord, mode) {
|
|
|
1732
1735
|
// This file's header says `findings_` = a top-level shape defect, `finding_` = a specific finding/field,
|
|
1733
1736
|
// and by that rule this token would be `finding_net_chained` alongside finding_net_missing /
|
|
1734
1737
|
// finding_net_invalid / finding_net_prescriptive. It is `findings_net_chained` instead, because the
|
|
1735
|
-
// convention is about ROUTING and routing disagrees. pipeline.mjs
|
|
1738
|
+
// convention is about ROUTING and routing disagrees. pipeline.mjs reads:
|
|
1736
1739
|
//
|
|
1737
1740
|
// const eligible = /^invalid_file:/.test(fail) && /:finding_[a-z]/.test(fail) && !/:findings_/.test(fail);
|
|
1738
1741
|
//
|
package/driver/flag-snapshot.mjs
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
// than being unable to ask. Failing closed here would mean a file-read error takes the whole portal
|
|
42
42
|
// down — trading a rare wrong-greyed-out option for a total outage.
|
|
43
43
|
|
|
44
|
-
import { writeFileSync, readFileSync, mkdirSync } from "node:fs";
|
|
44
|
+
import { writeFileSync, readFileSync, mkdirSync, existsSync } from "node:fs";
|
|
45
45
|
import { dirname, join } from "node:path";
|
|
46
46
|
import { BUILT } from "./search-policy.mjs";
|
|
47
47
|
import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — one entry-point test, all spellings
|
|
@@ -129,7 +129,7 @@ const truthy = (v) => ["1", "true", "yes", "on"].includes(String(v ?? "").trim()
|
|
|
129
129
|
* `capturedAt` is supplied rather than read from the clock so this stays testable and so a caller can
|
|
130
130
|
* stamp it from the same instant it stamps everything else.
|
|
131
131
|
*/
|
|
132
|
-
export function buildFlagSnapshot(env, { capturedAt, registerProvider = null, registerCanCount = null, registerTerritories = undefined, engine = undefined, providers = undefined }) {
|
|
132
|
+
export function buildFlagSnapshot(env, { capturedAt, registerProvider = null, registerLabel = null, registerCanCount = null, registerTerritories = undefined, engine = undefined, providers = undefined }) {
|
|
133
133
|
const flags = {};
|
|
134
134
|
// Written unconditionally, true or false to a count: a reader must be able to tell "this snapshot
|
|
135
135
|
// tracks no flags" from "this snapshot lost its flags", and `flags: {}` alone cannot say which.
|
|
@@ -181,6 +181,12 @@ export function buildFlagSnapshot(env, { capturedAt, registerProvider = null, re
|
|
|
181
181
|
register: registerProvider
|
|
182
182
|
? {
|
|
183
183
|
provider: registerProvider,
|
|
184
|
+
// The register's own DISPLAY label ("Signa"), beside the key the engine switches on ("signa").
|
|
185
|
+
// A door that names the register to a client must not print the key, and capabilities — where
|
|
186
|
+
// the label lives — is a provider module this process may not be able to import. Same split as
|
|
187
|
+
// `territories` directly below: the writer runs in the engine environment and resolves it once.
|
|
188
|
+
// Omitted rather than guessed when the writer had none, and every reader falls back to the key.
|
|
189
|
+
...(registerLabel ? { label: registerLabel } : {}),
|
|
184
190
|
canCount: registerCanCount,
|
|
185
191
|
...(registerTerritories === undefined ? {} : { territories: registerTerritories }),
|
|
186
192
|
}
|
|
@@ -335,9 +341,9 @@ export function postureDisagreement(snapshot, live) {
|
|
|
335
341
|
// the better answer for a reader: "found" against "not found" says it without a legend.
|
|
336
342
|
const found = (v) => (v === true ? "found" : v === false ? "not found" : null);
|
|
337
343
|
differ("engine program", found(snapshot.engine?.binaryPresent), found(live.engine?.binaryPresent),
|
|
338
|
-
"whether a NEW search can start — the
|
|
339
|
-
+ "engine program can be found, so one screen offers a search the other refuses. Restart the
|
|
340
|
-
+ "
|
|
344
|
+
"whether a NEW search can start — the services, when they last started, and this deployment do not agree "
|
|
345
|
+
+ "that the engine program can be found, so one screen offers a search the other refuses. Restart the "
|
|
346
|
+
+ "services so they look again; if they still disagree, `clearotron doctor` says which side to fix and how");
|
|
341
347
|
|
|
342
348
|
// Flags: compare only names BOTH sides declare, for the same reason `differ` skips absent values —
|
|
343
349
|
// a build that adds a flag must not read as every older capture disagreeing with it.
|
|
@@ -363,7 +369,7 @@ export function postureDisagreement(snapshot, live) {
|
|
|
363
369
|
|
|
364
370
|
/** Where the snapshot lives. Beside the pool, so it shares the pool's lifecycle and backup. */
|
|
365
371
|
export function snapshotPath(poolRoot) {
|
|
366
|
-
return join(poolRoot, "_state", "
|
|
372
|
+
return join(poolRoot, "_state", "clearance-flag-snapshot.json");
|
|
367
373
|
}
|
|
368
374
|
|
|
369
375
|
/**
|
|
@@ -377,7 +383,10 @@ export function readFlagSnapshot(poolRoot) {
|
|
|
377
383
|
// reads as intentional.
|
|
378
384
|
if (!poolRoot) return null;
|
|
379
385
|
try {
|
|
380
|
-
|
|
386
|
+
// The file's pre-rename name is read when the new one is not there yet, so an upgraded install does
|
|
387
|
+
// not read as "no snapshot" until its first write under the new name.
|
|
388
|
+
const legacy = join(poolRoot, "_state", "prelim-flag-snapshot.json");
|
|
389
|
+
const raw = JSON.parse(readFileSync(existsSync(snapshotPath(poolRoot)) || !existsSync(legacy) ? snapshotPath(poolRoot) : legacy, "utf8"));
|
|
381
390
|
if (!raw || typeof raw !== "object" || typeof raw.flags !== "object") return null;
|
|
382
391
|
return raw;
|
|
383
392
|
} catch {
|
|
@@ -425,6 +434,22 @@ export function registerTerritoriesFor(snapshot) {
|
|
|
425
434
|
return Array.isArray(v) ? v.filter((n) => typeof n === "string") : undefined;
|
|
426
435
|
}
|
|
427
436
|
|
|
437
|
+
/**
|
|
438
|
+
* The wired register's DISPLAY label, for a sentence a client reads — `null` when the snapshot does not
|
|
439
|
+
* carry one, which every snapshot written before this shipped does not.
|
|
440
|
+
*
|
|
441
|
+
* FALLS BACK TO THE PROVIDER KEY rather than to nothing: a door that names the register is better off
|
|
442
|
+
* saying "signa" than saying nothing at all, and the caller decides whether a key is good enough to
|
|
443
|
+
* print. It is deliberately NOT title-cased on the way out — "uspto-local" title-cased is worse prose
|
|
444
|
+
* than the key, and inventing a display name is the provider module's job, not this reader's.
|
|
445
|
+
*/
|
|
446
|
+
export function registerLabelFor(snapshot) {
|
|
447
|
+
const l = snapshot?.register?.label;
|
|
448
|
+
if (typeof l === "string" && l.trim()) return l.trim();
|
|
449
|
+
const p = snapshot?.register?.provider;
|
|
450
|
+
return typeof p === "string" && p.trim() ? p.trim() : null;
|
|
451
|
+
}
|
|
452
|
+
|
|
428
453
|
/**
|
|
429
454
|
* What this instance searches. THREE answers, exactly as `registerTerritoriesFor` above:
|
|
430
455
|
*
|
|
@@ -535,11 +560,12 @@ export async function livePosture({ env = process.env } = {}) {
|
|
|
535
560
|
const { REGISTER_PROVIDER } = await import("./driver.config.mjs");
|
|
536
561
|
const { capabilitiesFor } = await import("./register-capabilities.mjs");
|
|
537
562
|
const canCount = (() => { try { return capabilitiesFor(REGISTER_PROVIDER).countProbe !== "none"; } catch { return null; } })();
|
|
563
|
+
const label = (() => { try { return capabilitiesFor(REGISTER_PROVIDER).label ?? null; } catch { return null; } })();
|
|
538
564
|
const { coveredTerritoryNames } = await import("./register-coverage.mjs");
|
|
539
565
|
const territories = await (async () => { try { return await coveredTerritoryNames(capabilitiesFor(REGISTER_PROVIDER)); } catch { return undefined; } })();
|
|
540
566
|
const { engineInventory, providerInventory } = await import("./config-inventory.mjs");
|
|
541
567
|
return buildFlagSnapshot(env, {
|
|
542
|
-
capturedAt: new Date().toISOString(), registerProvider: REGISTER_PROVIDER, registerCanCount: canCount,
|
|
568
|
+
capturedAt: new Date().toISOString(), registerProvider: REGISTER_PROVIDER, registerLabel: label, registerCanCount: canCount,
|
|
543
569
|
registerTerritories: territories,
|
|
544
570
|
engine: engineInventory(env), providers: providerInventory(env),
|
|
545
571
|
});
|
|
@@ -84,6 +84,36 @@ export function editNeighbourhood(element) {
|
|
|
84
84
|
return [...out].sort();
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// ── A one-letter neighbour that is an ordinary word with a DIFFERENT SOUND is not searched ────────────
|
|
88
|
+
//
|
|
89
|
+
// THE REVIEWING LAWYER'S TEST IS CONFUSING SIMILARITY, and edit-1 over a short ordinary word is mostly other
|
|
90
|
+
// ordinary words: CARE, CODE, CORD, BORE, MORE beside CORE, each a common mark with its own crowd. Measured
|
|
91
|
+
// on a delivered four-letter run, 2026-09-18: 1,037 of 2,098 records (49%) were reached only by the one-letter
|
|
92
|
+
// lists; on the dense production matter of 2026-09-16, 1,154 of 2,146 (54%). Conceptually different words are
|
|
93
|
+
// not confused by consumers, so fetching them is cost and not coverage. A respelling that sounds the same —
|
|
94
|
+
// KORE for CORE — is exactly what a search must find, and so is every neighbour of a made-up word, because two
|
|
95
|
+
// unknown words that sound alike have nothing conceptual to keep them apart (MALENA beside VALENA).
|
|
96
|
+
//
|
|
97
|
+
// So a neighbour is dropped only when BOTH hold: it is in the ordinary-word list, AND no Double-Metaphone key
|
|
98
|
+
// of it matches any key of the element. Everything else is dispatched exactly as before. There is no judgment
|
|
99
|
+
// of the MARK here — coined or ordinary — only of each neighbour, so a coined element keeps its whole list
|
|
100
|
+
// except where a neighbour is a different-sounding real word. The list only ever removes queries, and a word
|
|
101
|
+
// missing from it is searched: over-search is the safe direction.
|
|
102
|
+
//
|
|
103
|
+
// DOUBLE METAPHONE KEEPS VOWELS ONLY AT THE START, so a vowel change inside the word does not change the key:
|
|
104
|
+
// CARE, CURE and GORE all key KR, as CORE does, and are KEPT. That is the rule as specified erring towards the
|
|
105
|
+
// search, and it is recorded here so nobody reads those three as a defect of the list.
|
|
106
|
+
//
|
|
107
|
+
// `ordinaryWords` is a Set of lowercase words, or null. Null drops nothing — the behaviour before this rule —
|
|
108
|
+
// so a caller that does not load the list (every test that predates it) is unchanged. PURE.
|
|
109
|
+
export function ordinaryWordDifferentSound(element, ordinaryWords) {
|
|
110
|
+
const el = normalizeElement(element);
|
|
111
|
+
if (!el || !(ordinaryWords instanceof Set) || !ordinaryWords.size) return [];
|
|
112
|
+
const own = new Set(doubleMetaphone(el).filter(Boolean));
|
|
113
|
+
return editNeighbourhood(el).filter((t) => ordinaryWords.has(t)
|
|
114
|
+
&& !doubleMetaphone(t).filter(Boolean).some((k) => own.has(k)));
|
|
115
|
+
}
|
|
116
|
+
|
|
87
117
|
// ── Consonant skeleton + wildcard patterns — retrieve the phonetic VOWEL family in one bounded vendor query ──
|
|
88
118
|
// VALENA → consonants V,L,N → skeleton "VLN"; vowel-slot wildcard "V?L?N?"-style patterns the vendor's Lucene
|
|
89
119
|
// `?`/`*` supports (live-confirmed). This is the tractable, complete way to reach VYLONA/VILINA/VILENA without
|
|
@@ -229,13 +259,18 @@ export function transliterations(element, { scripts = SUPPORTED_SCRIPTS } = {})
|
|
|
229
259
|
// droppedVariantFamilies). The floor stays exhaustive WITHIN the families judgment kept — this is the funnel
|
|
230
260
|
// honouring a scope decision that was already written and, until 2026-07-18, ignored. edit-1 is never
|
|
231
261
|
// droppable: it is the doctrine floor (radiusFor), not a family.
|
|
232
|
-
export function formNeighbourhood(element, { markets = [], scripts = SUPPORTED_SCRIPTS, droppedAxes = [] } = {}) {
|
|
262
|
+
export function formNeighbourhood(element, { markets = [], scripts = SUPPORTED_SCRIPTS, droppedAxes = [], ordinaryWords = null } = {}) {
|
|
233
263
|
const radius = radiusFor(element);
|
|
234
264
|
const el = radius.element;
|
|
235
|
-
if (!el) return { element: "", radius, exactQueries: [], wildcardPatterns: [], phoneticKeys: [], confusables: [], transliterations: [], ledger: { disclosed: radius.note, axes: [] } };
|
|
265
|
+
if (!el) return { element: "", radius, exactQueries: [], wildcardPatterns: [], phoneticKeys: [], confusables: [], transliterations: [], ordinaryWordDifferentSound: [], ledger: { disclosed: radius.note, axes: [] } };
|
|
236
266
|
|
|
237
267
|
const drop = new Set(droppedAxes ?? []);
|
|
238
|
-
const
|
|
268
|
+
const generatedEdits = editNeighbourhood(el);
|
|
269
|
+
// Only edit-1 is filtered. A term another generator also produces (a confusable, a transliteration) is
|
|
270
|
+
// still dispatched as that generator's — those families are unchanged by this rule.
|
|
271
|
+
const notSearched = ordinaryWordDifferentSound(el, ordinaryWords);
|
|
272
|
+
const skip = new Set(notSearched);
|
|
273
|
+
const edits = generatedEdits.filter((t) => !skip.has(t));
|
|
239
274
|
const confs = drop.has("visual-confusable") ? [] : visualConfusables(el);
|
|
240
275
|
const trans = drop.has("transliteration") ? [] : transliterations(el, { scripts });
|
|
241
276
|
const wildcards = drop.has("phonetic-family") ? [] : skeletonPatterns(el);
|
|
@@ -261,11 +296,15 @@ export function formNeighbourhood(element, { markets = [], scripts = SUPPORTED_S
|
|
|
261
296
|
phoneticKeys: keys,
|
|
262
297
|
confusables: confs,
|
|
263
298
|
transliterations: trans,
|
|
299
|
+
ordinaryWordDifferentSound: notSearched,
|
|
264
300
|
ledger: {
|
|
265
301
|
disclosed: radius.note,
|
|
266
302
|
dropped_axes: [...drop].sort(),
|
|
267
303
|
axes: [
|
|
268
|
-
{ axis: "edit-1", count: edits.length,
|
|
304
|
+
{ axis: "edit-1", count: edits.length, generated: generatedEdits.length, not_searched: notSearched.length,
|
|
305
|
+
mechanism: notSearched.length
|
|
306
|
+
? "Damerau-Levenshtein edit-1, exhaustive, less the neighbours that are ordinary words with a different sound"
|
|
307
|
+
: "Damerau-Levenshtein edit-1, exhaustive" },
|
|
269
308
|
// NO PATTERN is a THIRD state, and it is disclosed in the same voice as a judgment drop. An
|
|
270
309
|
// element with too few consonants to anchor a skeleton wildcard (X, and anything normalizing to
|
|
271
310
|
// one character) yields no retrieval pattern at all — see skeletonPatterns. Silence here would
|
|
@@ -349,7 +388,7 @@ export function coverageGaps(band, { dispatched = [], explained = [] } = {}) {
|
|
|
349
388
|
// exhaustive edit-1 neighbourhood: 1,736 junk exact queries on AquaPlus 2026-07-17, 4,524 on the 07-16 run.
|
|
350
389
|
// Measured 2026-07-18: 10 of 20 recent runs carried one of these. The JSON field is a validated scalar and
|
|
351
390
|
// cannot swallow a sentence.
|
|
352
|
-
export function renderFormNeighbourhoodJson(manifestMd, { markets = [], scripts = SUPPORTED_SCRIPTS, droppedAxes = [], model = null, mark = "" } = {}) {
|
|
391
|
+
export function renderFormNeighbourhoodJson(manifestMd, { markets = [], scripts = SUPPORTED_SCRIPTS, droppedAxes = [], model = null, mark = "", ordinaryWords = null } = {}) {
|
|
353
392
|
const { seeds, seededFrom, rejected } = floorSeeds(manifestMd, { model, mark });
|
|
354
393
|
// The reason travels WITH the throw. It used to say only "the job states no mark", which is one
|
|
355
394
|
// of two causes and not the one that actually fires: a non-Latin mark states a mark perfectly well and
|
|
@@ -361,7 +400,7 @@ export function renderFormNeighbourhoodJson(manifestMd, { markets = [], scripts
|
|
|
361
400
|
|| "manifest names no Dominant element / Formative root, and the job states no mark";
|
|
362
401
|
throw new Error(`form_neighbourhood_no_element: ${why} — nothing to seed the mechanical form band`);
|
|
363
402
|
}
|
|
364
|
-
const elements = seeds.map(({ element, role }) => ({ element, role, band: formNeighbourhood(element, { markets, scripts, droppedAxes }) }));
|
|
403
|
+
const elements = seeds.map(({ element, role }) => ({ element, role, band: formNeighbourhood(element, { markets, scripts, droppedAxes, ordinaryWords }) }));
|
|
365
404
|
const families = variantFloorFamilies(elements, { mark, droppedAxes });
|
|
366
405
|
return JSON.stringify({
|
|
367
406
|
schema_version: 2,
|
|
@@ -596,10 +635,11 @@ export function spacingPunctuationForms(mark) {
|
|
|
596
635
|
export function variantFloorFamilies(elements, { mark = "", droppedAxes = [] } = {}) {
|
|
597
636
|
const drop = new Set(droppedAxes ?? []);
|
|
598
637
|
const edit = new Set(), visual = new Set(), translit = new Set(), other = new Set();
|
|
599
|
-
const wildcards = new Set(), keys = new Set();
|
|
638
|
+
const wildcards = new Set(), keys = new Set(), notSearched = new Set();
|
|
600
639
|
for (const el of elements ?? []) {
|
|
601
640
|
const band = el?.band;
|
|
602
641
|
if (!band) continue;
|
|
642
|
+
for (const t of band.ordinaryWordDifferentSound ?? []) notSearched.add(t);
|
|
603
643
|
const edits = new Set(editNeighbourhood(el.element));
|
|
604
644
|
const confs = new Set(band.confusables ?? []);
|
|
605
645
|
const trans = new Set(band.transliterations ?? []);
|
|
@@ -639,6 +679,12 @@ export function variantFloorFamilies(elements, { mark = "", droppedAxes = [] } =
|
|
|
639
679
|
dropped: drop.has("phonetic-family"), terms: sorted(wildcards), phonetic_keys: sorted(keys) },
|
|
640
680
|
...(other.size ? [{ family: "other", category: "other", generator: "formNeighbourhood (dedupeOnFold residue)",
|
|
641
681
|
enumeration: "a dispatched term no single generator claims — recorded rather than hidden", dropped: false, terms: sorted(other) }] : []),
|
|
682
|
+
// NOT SEARCHED, and listed in full so the plan says what it left out and why. Not a crowd and not a
|
|
683
|
+
// gap: a deliberate rule (ordinaryWordDifferentSound). `searched: false` keeps it out of the floor the
|
|
684
|
+
// merge below counts, so a model that proposes one of these words is recorded as its own addition.
|
|
685
|
+
...(notSearched.size ? [{ family: "ordinary-word-different-sound", category: "phonetic", generator: "editNeighbourhood",
|
|
686
|
+
enumeration: "edit-1 neighbours that are ordinary English words and share no Double-Metaphone key with the element",
|
|
687
|
+
dispatch: "not searched — ordinary word, different sound", searched: false, dropped: true, terms: sorted(notSearched) }] : []),
|
|
642
688
|
];
|
|
643
689
|
}
|
|
644
690
|
|
|
@@ -674,6 +720,7 @@ export function mergeVariantFloor(floorFamilies, modelVariants, { rejectedSeeds
|
|
|
674
720
|
const families = Array.isArray(floorFamilies) ? floorFamilies : [];
|
|
675
721
|
const floorByKey = new Map();
|
|
676
722
|
for (const f of families) {
|
|
723
|
+
if (f?.searched === false) continue; // listed for disclosure, never searched — not floor
|
|
677
724
|
for (const t of f?.terms ?? []) {
|
|
678
725
|
const k = formKey(t);
|
|
679
726
|
if (!k || floorByKey.has(k)) continue;
|
package/driver/framework.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
3
|
// framework.mjs — the risk-framework MANIFEST layer (doc 50: the framework in force rates the matter).
|
|
4
4
|
//
|
|
5
|
-
// The framework itself is a PROSE deck (skills/
|
|
5
|
+
// The framework itself is a PROSE deck (skills/clearance-search/risk-framework*.md) — the customer's own legal
|
|
6
6
|
// judgment written down, which synthesis reads and reasons WITH. This module carries the small
|
|
7
7
|
// machine-readable sidecar (<framework>.manifest.json) that lets validators, the renderer, the archive index
|
|
8
8
|
// and the profile UI consume the framework's VOCABULARY — band words, severity order, entity label, source —
|
|
@@ -18,12 +18,12 @@ import { readFileSync } from "node:fs";
|
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
|
|
20
20
|
// ── selection (the ?? fallback IS the "Generic default rates the matter" rule) ────────────────────────────
|
|
21
|
-
export const DEFAULT_FRAMEWORK = "skills/
|
|
22
|
-
export const DEFAULT_WORKED_EXAMPLES = "skills/
|
|
21
|
+
export const DEFAULT_FRAMEWORK = "skills/clearance-search/risk-framework.md";
|
|
22
|
+
export const DEFAULT_WORKED_EXAMPLES = "skills/clearance-search/worked-examples.md";
|
|
23
23
|
export const frameworkFor = (profile) => profile?.frameworkPath ?? DEFAULT_FRAMEWORK;
|
|
24
24
|
export const workedExamplesFor = (profile) => profile?.workedExamplesPath ?? DEFAULT_WORKED_EXAMPLES;
|
|
25
25
|
|
|
26
|
-
/** skills/
|
|
26
|
+
/** skills/clearance-search/risk-framework-x.md → skills/clearance-search/risk-framework-x.manifest.json.
|
|
27
27
|
* The manifest path is DERIVED, never a profile knob — the profile names only the .md. */
|
|
28
28
|
export const manifestPathFor = (fwPath) => String(fwPath).replace(/\.md$/, ".manifest.json");
|
|
29
29
|
|