clearotron 0.3.2-beta.8 → 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 +34 -3
- package/CONTRIBUTING.md +8 -8
- package/INSTALL.md +6 -6
- package/SECURITY.md +3 -3
- package/bin/brandowner.mjs +3 -3
- package/bin/framework-preflight.mjs +1 -1
- package/bin/start.mjs +18 -4
- 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 +6 -3
- package/docs/architecture/05-config-governance.md +6 -1
- 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 +48 -0
- package/driver/README.md +3 -3
- 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/consumption-ledger.mjs +2 -2
- package/driver/contract-arm2-baseline.json +2 -3
- package/driver/contract-dictation-registry.mjs +19 -19
- package/driver/contract-e3-backlog.mjs +19 -19
- package/driver/contract-e3-baseline.json +14 -14
- package/driver/contract-vocabulary.mjs +24 -17
- 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/doubt-ledger.mjs +2 -2
- package/driver/drainer-identity.mjs +34 -8
- package/driver/driver.config.mjs +95 -45
- 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 +5 -5
- package/driver/enqueue-schema.mjs +6 -2
- package/driver/findings-model.mjs +5 -2
- package/driver/flag-snapshot.mjs +6 -3
- package/driver/form-neighbourhood.mjs +54 -7
- package/driver/framework.mjs +4 -4
- package/driver/gateway.mjs +12 -6
- package/driver/jx-lanes.mjs +2 -2
- package/driver/jx-units.mjs +1 -1
- package/driver/jx.mjs +30 -2
- package/driver/knockout-review-record.mjs +56 -4
- package/driver/known-conflicts.mjs +1 -1
- package/driver/named-band.mjs +1 -33
- 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 +206 -68
- package/driver/placement-form.mjs +77 -1
- package/driver/placement-model.mjs +1 -1
- package/driver/portal-report.mjs +92 -5
- package/driver/portal-service.mjs +34 -8
- package/driver/portal-upstream.mjs +1 -1
- 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 +110 -5
- package/driver/publish/knockout.mjs +29 -4
- package/driver/publish/pool-admin.mjs +1 -1
- package/driver/publish/publish-inputs.mjs +18 -2
- package/driver/publish/render-knockout.mjs +115 -24
- package/driver/publish/render.mjs +153 -34
- package/driver/publish/search-depth.mjs +133 -4
- package/driver/publish/templates/report.css +60 -3
- package/driver/publish/xlsx.mjs +8 -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-count.mjs +50 -5
- package/driver/register-coverage.mjs +67 -0
- package/driver/register-grant-vocabulary.mjs +1 -1
- package/driver/register-plan.mjs +19 -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/run-requirements.mjs +3 -3
- package/driver/runner.mjs +2 -2
- 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 +53 -53
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +218 -92
- package/driver/surface-exit-verdict.mjs +58 -0
- package/driver/systemd/README.md +2 -2
- package/driver/systemd/clearotron-worker.service +1 -1
- package/driver/terminal-clamp.mjs +2 -0
- 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 +67 -6
- package/driver/whatif-queue.mjs +1 -1
- package/driver/wordlists/en.txt +63906 -0
- package/mcp-server/CHANGELOG.md +4 -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 +3 -0
- package/package.json +12 -11
- package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
- package/portal-ui/dist/assets/{index-6jzO9HiX.js → index-EVaSo5-g.js} +1459 -482
- 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/oauth-mcp-bridge/CHANGELOG.md +4 -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/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 +4 -4
- 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 +27 -0
- package/scripts/env-classify.mjs +20 -2
- package/scripts/freeze-example-run.mjs +20 -10
- 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/shared/brand.mjs +27 -0
- package/shared/connect-clients.mjs +39 -11
- 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 +3 -1
- 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
|
@@ -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
|
|
package/driver/gateway.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import { MODEL_FILE as BLIND_FRAME_MODEL_FILE } from "./blind-frame-record.mjs";
|
|
|
34
34
|
import { FLAGS_FILE as SKEPTIC_FLAGS_FILE } from "./skeptic-record.mjs";
|
|
35
35
|
import { MODEL_FILE as FRAME_DIFF_MODEL_FILE, PROSE_FILE as FRAME_DIFF_PROSE_FILE } from "./frame-diff-record.mjs";
|
|
36
36
|
import { MATTER_CONTEXT_FILE } from "./matter-frame-record.mjs";
|
|
37
|
-
import { MODEL_FILE as VARIANT_MODEL_FILE, PROSE_FILE as VARIANT_PROSE_FILE } from "./
|
|
37
|
+
import { MODEL_FILE as VARIANT_MODEL_FILE, PROSE_FILE as VARIANT_PROSE_FILE } from "./clearance-variants-record.mjs";
|
|
38
38
|
import { PROSE_FILE as REPORT_OVERVIEW_FILE } from "./report-overview-record.mjs";
|
|
39
39
|
import { NARRATIVE_FILE, FINDINGS_FILE, refusalsFor } from "./synthesis-record.mjs";
|
|
40
40
|
import { FINDINGS_FILE as REGISTER_FINDINGS_FILE, refusalsFor as registerDigestRefusalsFor } from "./register-digest-record.mjs";
|
|
@@ -96,8 +96,8 @@ export const TOOL_WRITTEN_ARTIFACTS = new Map([
|
|
|
96
96
|
// scope-ledger.json is deliberately ABSENT: it was already driver-written before this conversion (the
|
|
97
97
|
// driver derived it), so it is outside this conversion's claim. What changed is where its values come
|
|
98
98
|
// from, not who writes it.
|
|
99
|
-
[VARIANT_MODEL_FILE, { tool: "
|
|
100
|
-
[VARIANT_PROSE_FILE, { tool: "
|
|
99
|
+
[VARIANT_MODEL_FILE, { tool: "record_clearance_variants", what: "the variant manifest" }],
|
|
100
|
+
[VARIANT_PROSE_FILE, { tool: "record_clearance_variants", what: "the variant manifest" }],
|
|
101
101
|
// Conversion 4 — ONE basename, and the first row whose artifact a CLIENT reads. report-overview.md is
|
|
102
102
|
// the delivered report's front-matter and its Actions section; assembleReportMd splices the code-built
|
|
103
103
|
// sections into it and publishes the result. So a repair that arrives naming this file and gets handed
|
|
@@ -261,6 +261,7 @@ import { unionDispositionForm, formSidecarPath } from "./disposition-union.mjs";
|
|
|
261
261
|
import { unionCoverageForm } from "./coverage-union.mjs";
|
|
262
262
|
import { coverageFormStamp, readCoverageForm, readCoverageFormInput, writeCoverageForm } from "./coverage-form-io.mjs";
|
|
263
263
|
import { unionPlacementForm } from "./placement-union.mjs";
|
|
264
|
+
import { placementRenderAccount } from "./placement-form.mjs";
|
|
264
265
|
import { placementFormStamp, readPlacementForm, readPlacementFormInput, readSubmittedPlacementForm, writePlacementForm, renderPlacementsFile } from "./placement-form-io.mjs";
|
|
265
266
|
// The register-axis vocabulary, quoted verbatim into the coverage-form axis hint. ONE source: the same
|
|
266
267
|
// constant `rowIsSettled` refuses against, so the hint can never name a set the gate does not accept.
|
|
@@ -741,7 +742,12 @@ export function syncPlacementForm(files) {
|
|
|
741
742
|
// never put an unparseable deliverable on disk, and on a throw the previous file is left alone.
|
|
742
743
|
const r = renderPlacementsFile(runDir, u.form.rows);
|
|
743
744
|
if (!r.ok) note(`[placement-form] placements.json NOT re-rendered: ${r.error} — the previous file stands and the omission is on the form`);
|
|
744
|
-
|
|
745
|
+
// WHO OWES EACH ROW THE RENDER LEFT OUT — the same account validators.placement judges with, carried onto
|
|
746
|
+
// the attempt row so an omission is never read without its cause.
|
|
747
|
+
const a = placementRenderAccount(u.form.rows);
|
|
748
|
+
const account = { unjudged: a.unjudged.length, registerSelected: a.register_selected,
|
|
749
|
+
registerRendered: a.register_rendered, registerFacts: a.register_facts };
|
|
750
|
+
return { ...u, rendered: r.ok ? r.placements : null, render_error: r.error, account };
|
|
745
751
|
}
|
|
746
752
|
return null;
|
|
747
753
|
}
|
|
@@ -1699,7 +1705,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1699
1705
|
// destroyed. `unresolved` counts selections the fold does not hold. RECORDING ONLY.
|
|
1700
1706
|
placements: lastPlacementUnion ? { settled: lastPlacementUnion.settled, outstanding: lastPlacementUnion.outstanding,
|
|
1701
1707
|
carried: lastPlacementUnion.carried, total: lastPlacementUnion.total, seatRows: lastPlacementUnion.seat_rows,
|
|
1702
|
-
unresolved: lastPlacementUnion.unresolved, rendered: lastPlacementUnion.rendered } : undefined,
|
|
1708
|
+
unresolved: lastPlacementUnion.unresolved, rendered: lastPlacementUnion.rendered, account: lastPlacementUnion.account } : undefined,
|
|
1703
1709
|
//: how many in-dispatch form repairs THIS attempt bought (absent = none). Its own rows
|
|
1704
1710
|
// sit immediately above with the defect each one was dispatched to fix.
|
|
1705
1711
|
formRepairs: formRepairsThisAttempt || undefined,
|
|
@@ -2088,7 +2094,7 @@ function refusalsInWindow(files, runDir, from, to) {
|
|
|
2088
2094
|
}
|
|
2089
2095
|
|
|
2090
2096
|
function rel(p) {
|
|
2091
|
-
const i = p.indexOf("/prelim-search/");
|
|
2097
|
+
const i = Math.max(p.indexOf("/clearance-search/"), p.indexOf("/prelim-search/")); // either spelling of the studio segment
|
|
2092
2098
|
return i >= 0 ? p.slice(i + 1) : p;
|
|
2093
2099
|
}
|
|
2094
2100
|
|
package/driver/jx-lanes.mjs
CHANGED
|
@@ -183,10 +183,10 @@ export function decideJxLanes({ job, profile, searchPolicy } = {}) {
|
|
|
183
183
|
*
|
|
184
184
|
* ── THE GATE THAT WAS DECIDING, AND DECIDING OFF ────────────────────────────────────────────────────
|
|
185
185
|
*
|
|
186
|
-
* This function opened with `resolvedPolicy.level !== "
|
|
186
|
+
* This function opened with `resolvedPolicy.level !== "clearance"`. `resolveSearchPolicy` now returns
|
|
187
187
|
* `level` = THE PRODUCT ID for all four searches, so that leg was false on every live run and the whole
|
|
188
188
|
* recommendation was dead — silently, with both callers (pipeline.mjs and runner.mjs) live and 3,754
|
|
189
|
-
* driver tests green, because the one test that covered it hand-fed `{ level: "
|
|
189
|
+
* driver tests green, because the one test that covered it hand-fed `{ level: "clearance" }`, a value
|
|
190
190
|
* nothing produces any more. A retired slug does not stop deciding when it is retired; it starts
|
|
191
191
|
* deciding one way.
|
|
192
192
|
*
|
package/driver/jx-units.mjs
CHANGED
|
@@ -144,7 +144,7 @@ function ledgerRow(runDir, row) {
|
|
|
144
144
|
try { appendFileSync(driverDir(runDir, "jx-completions.jsonl"), JSON.stringify(row) + "\n"); } catch { /* receipts best-effort */ }
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
const runPrefix = (run) => `
|
|
147
|
+
const runPrefix = (run) => `clearance-${run?.slug ?? "run"}-${run?.codename ?? "local"}-`;
|
|
148
148
|
|
|
149
149
|
// ── Executor chains (the resolveJxExecutor idiom: injected → CLEAROTRON_JX_FIXTURES → live) ─────────────
|
|
150
150
|
export function resolveSerpExecutor(opts, { mark, lane }) {
|
package/driver/jx.mjs
CHANGED
|
@@ -186,6 +186,13 @@ export function deriveJxSliceStatement({ sidecar, units = null, env = process.en
|
|
|
186
186
|
//
|
|
187
187
|
// PURE. Takes the parsed sidecar and the slice statement `deriveJxSliceStatement` just produced.
|
|
188
188
|
export function deriveLaneDepthVerdicts({ sidecar, slices } = {}) {
|
|
189
|
+
// EITHER SHAPE, AND THAT IS NOT POLITENESS. This reads `slices.candidates`, and the publish path handed
|
|
190
|
+
// it `deriveJxSliceStatement`'s whole return — `{executes, slices}` — so the lookup found nothing and
|
|
191
|
+
// every lane reported `ran: null` whatever it had done. Nothing threw and nothing was logged: the
|
|
192
|
+
// failure arrived as a delivered client report saying a search had not run when it had. The two shapes
|
|
193
|
+
// are told apart with certainty rather than guessed at — a statement carries a `slices` object and a
|
|
194
|
+
// slice map carries slice names — so a caller cannot pass the wrong one and be quietly misread.
|
|
195
|
+
const bySlice = slices?.slices && typeof slices.slices === "object" ? slices.slices : slices;
|
|
189
196
|
const declared = sidecar?.lanes ?? {};
|
|
190
197
|
const out = {};
|
|
191
198
|
for (const lane of Object.keys(declared)) {
|
|
@@ -194,10 +201,10 @@ export function deriveLaneDepthVerdicts({ sidecar, slices } = {}) {
|
|
|
194
201
|
// lane that gains a slice later is covered without this function being edited — and so a lane that
|
|
195
202
|
// has none is a measured fact rather than a hardcoded assumption about ja/ko.
|
|
196
203
|
const deep = JX_SLICES.filter((s) => s.slice > 1 && s.lane === lane);
|
|
197
|
-
const deepStates = deep.map((s) => ({ name: s.name, state:
|
|
204
|
+
const deepStates = deep.map((s) => ({ name: s.name, state: bySlice?.[s.name]?.state ?? null, why: bySlice?.[s.name]?.why ?? null }));
|
|
198
205
|
const deepRan = deepStates.filter((d) => d.state === "ran").map((d) => d.name);
|
|
199
206
|
// Slice 1 is the multi-lane one: its per-lane state lives in `.lanes`, never in a scalar `.state`.
|
|
200
|
-
const candidatesState =
|
|
207
|
+
const candidatesState = bySlice?.candidates?.lanes?.[lane] ?? null;
|
|
201
208
|
|
|
202
209
|
const ran = deepRan.length ? "full" : candidatesState === "ran" ? "candidates" : null;
|
|
203
210
|
const shortfall = asked === "full" && ran !== "full";
|
|
@@ -225,6 +232,27 @@ export function deriveLaneDepthVerdicts({ sidecar, slices } = {}) {
|
|
|
225
232
|
return out;
|
|
226
233
|
}
|
|
227
234
|
|
|
235
|
+
/**
|
|
236
|
+
* THE VERDICT A PUBLISHED REPORT SHOULD CARRY: the one the RUN stated, not a fresh one. PURE.
|
|
237
|
+
*
|
|
238
|
+
* `stateJxFold` writes `fold.depth` at delivery, derived from this run's own environment — which is the
|
|
239
|
+
* reason the seam exists: the arms are environment, so a verdict derived on some later box speaks for
|
|
240
|
+
* that box and not for the run. Publishing derived its own, and the two disagreed on a delivered client
|
|
241
|
+
* report: the run said the ja lane ran as a candidate lane, the publisher said no lane ran, and the page
|
|
242
|
+
* printed "Not run this run" over a search that had happened.
|
|
243
|
+
*
|
|
244
|
+
* Deriving stays, for a run delivered before the stamp existed — an old artifact still gets the best
|
|
245
|
+
* answer its record supports, rather than none.
|
|
246
|
+
*
|
|
247
|
+
* @param {object|null} sidecar parsed _driver/jx-lanes.json
|
|
248
|
+
* @param {object|null} units parsed _driver/jx/units.json, or null
|
|
249
|
+
*/
|
|
250
|
+
export function laneDepthOfRun({ sidecar, units = null, env = process.env } = {}) {
|
|
251
|
+
const stamped = sidecar?.fold?.depth;
|
|
252
|
+
if (stamped && typeof stamped === "object" && !Array.isArray(stamped)) return stamped;
|
|
253
|
+
return deriveLaneDepthVerdicts({ sidecar, slices: deriveJxSliceStatement({ sidecar, units, env }) });
|
|
254
|
+
}
|
|
255
|
+
|
|
228
256
|
const flag = (name) => ["1", "true", "yes", "on"].includes(String(process.env[name] ?? "").trim().toLowerCase());
|
|
229
257
|
// — the private copy is gone; laneArmed (driver.config.mjs) is the one reader.
|
|
230
258
|
|
|
@@ -43,6 +43,9 @@ import { captureCall, stampVerdict } from "./call-capture.mjs";
|
|
|
43
43
|
import { refuseUndeclared as refuseUndeclaredShared, lastAccepted, acceptedEnvelope } from "./preserve-merge.mjs";
|
|
44
44
|
import { knockoutVisibleProse } from "./predelivery-lint.mjs"; // the walk is the authority on what a reader sees
|
|
45
45
|
import { plainRegisterFlags } from "./plain-register.mjs"; // the pinned rule, one copy
|
|
46
|
+
// THE HINT'S fold, and it is imported rather than rebuilt — see nearestAddress. It is a comparison key
|
|
47
|
+
// for saying WHICH line was probably meant; it never decides whether an address resolves.
|
|
48
|
+
import { queryKey } from "./connotation-search.mjs";
|
|
46
49
|
|
|
47
50
|
const SCHEMA_VERSION = 1;
|
|
48
51
|
|
|
@@ -142,6 +145,42 @@ export function addressKey(at) {
|
|
|
142
145
|
return [a.field, a.mark ?? "", a.index ?? "", a.ordinal ?? ""].join(ADDRESS_SEP);
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
/**
|
|
149
|
+
* THE ADDRESS A SEAT PROBABLY MEANT, FOR THE REFUSAL TO NAME — and for nothing else.
|
|
150
|
+
*
|
|
151
|
+
* The lookup above stays EXACT and that is a ruling, not an oversight (owner, 2026-09-16). Punctuation
|
|
152
|
+
* can be part of a mark's identity, so two marks differing by an apostrophe form are not assumed to be
|
|
153
|
+
* one mark and a rewrite is never applied by analogy. What was wrong was the refusal, not the matching:
|
|
154
|
+
* a seat that re-typed a name with a right single quotation mark where the record has an apostrophe was
|
|
155
|
+
* told its rewrite "names no line on this record", which describes the record rather than the difference,
|
|
156
|
+
* and the rewrite was dropped with nobody able to see why. One character, invisible in most fonts.
|
|
157
|
+
*
|
|
158
|
+
* So this names the nearest and changes nothing. It folds ONLY typographic form — `queryKey`, the same
|
|
159
|
+
* key the meaning gate was folded onto after a production clearance stopped on exactly this character —
|
|
160
|
+
* and it is imported rather than copied, because two private folds are how the two gates drift apart.
|
|
161
|
+
* That key deliberately does not fold accents, so it cannot quietly propose that Café and Cafe are the
|
|
162
|
+
* same mark.
|
|
163
|
+
*
|
|
164
|
+
* It requires an EXACT match on every other part of the address — field, index, ordinal — so the hint is
|
|
165
|
+
* always the same line of the same record, differing only in how the mark's name was typed. A candidate
|
|
166
|
+
* whose mark is byte-identical is not a hint: the caller only asks when the exact lookup already failed,
|
|
167
|
+
* and pointing at the address that was just refused would say nothing.
|
|
168
|
+
*
|
|
169
|
+
* @returns {object|null} the address on this record, or null when nothing differs by form alone
|
|
170
|
+
*/
|
|
171
|
+
export function nearestAddress(at, knownAts) {
|
|
172
|
+
const want = at ?? {};
|
|
173
|
+
if (want.mark == null) return null; // no name to have mistyped
|
|
174
|
+
const same = (a, b) => (a ?? "") === (b ?? "");
|
|
175
|
+
for (const k of knownAts ?? []) {
|
|
176
|
+
if (!k || k.field !== want.field) continue;
|
|
177
|
+
if (!same(k.index, want.index) || !same(k.ordinal, want.ordinal)) continue;
|
|
178
|
+
const kn = String(k.mark ?? ""), wn = String(want.mark ?? "");
|
|
179
|
+
if (kn !== wn && queryKey(kn) === queryKey(wn)) return k;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
145
184
|
/** Is this a well-formed address for a field the pass may reach? The reason, or null. */
|
|
146
185
|
export function addressFault(at) {
|
|
147
186
|
if (!at || typeof at !== "object" || Array.isArray(at)) return "an address must be an object";
|
|
@@ -296,15 +335,21 @@ export function validateKnockoutReviewFile(file, text) {
|
|
|
296
335
|
try { merged = JSON.parse(readFileSync(findingsFile, "utf8")); }
|
|
297
336
|
catch (e) { return { ok: false, reason: `the merged record will not parse, so no address could be checked: ${e.message}` }; }
|
|
298
337
|
|
|
299
|
-
const
|
|
338
|
+
const knownAts = knockoutVisibleProse(merged).map((v) => v.at);
|
|
339
|
+
const known = new Set(knownAts.map((a) => addressKey(a)));
|
|
300
340
|
const owned = new Set(knockoutVisibleProse(merged).filter((v) => v.at?.engineOwned).map((v) => addressKey(v.at)));
|
|
341
|
+
// Appended to a refusal, never consulted before one: the address still has to match exactly.
|
|
342
|
+
const hint = (at) => {
|
|
343
|
+
const n = nearestAddress(at, knownAts);
|
|
344
|
+
return n ? ` — this record carries "${n.mark}" at that line, which differs from "${at.mark}" only in how it is typed; the two are not assumed to be the same mark, so re-send the rewrite naming the record's spelling` : "";
|
|
345
|
+
};
|
|
301
346
|
for (const r of doc.rewrites ?? []) {
|
|
302
347
|
const key = addressKey(r.at);
|
|
303
348
|
if (owned.has(key)) return { ok: false, reason: `the rewrite for ${r.at.field} names a caveat the engine wrote, not a seat — it is not offered and cannot be changed` };
|
|
304
|
-
if (!known.has(key)) return { ok: false, reason: `the rewrite for ${r.at.field}${r.at.mark ? ` on "${r.at.mark}"` : ""} names no line on this record` };
|
|
349
|
+
if (!known.has(key)) return { ok: false, reason: `the rewrite for ${r.at.field}${r.at.mark ? ` on "${r.at.mark}"` : ""} names no line on this record${hint(r.at)}` };
|
|
305
350
|
}
|
|
306
351
|
for (const d of doc.declined ?? []) {
|
|
307
|
-
if (!known.has(addressKey(d.at))) return { ok: false, reason: `the declined row for ${d.at.field}${d.at.mark ? ` on "${d.at.mark}"` : ""} names no line on this record` };
|
|
352
|
+
if (!known.has(addressKey(d.at))) return { ok: false, reason: `the declined row for ${d.at.field}${d.at.mark ? ` on "${d.at.mark}"` : ""} names no line on this record${hint(d.at)}` };
|
|
308
353
|
}
|
|
309
354
|
return { ok: true };
|
|
310
355
|
}
|
|
@@ -340,7 +385,14 @@ export function applyKnockoutReview(merged, review) {
|
|
|
340
385
|
for (const r of review?.rewrites ?? []) {
|
|
341
386
|
const key = addressKey(r.at);
|
|
342
387
|
const seen = byKey.get(key);
|
|
343
|
-
if (!seen) {
|
|
388
|
+
if (!seen) {
|
|
389
|
+
// Same hint as the validator's, for the same reason: the apply path is reached on a record this
|
|
390
|
+
// pass did not validate, and an unresolved row that cannot say WHY is the state that cost a
|
|
391
|
+
// rewrite silently. It still changes nothing — `unresolved` is a report.
|
|
392
|
+
const near = nearestAddress(r.at, [...byKey.values()].map((v) => v.at));
|
|
393
|
+
unresolved.push({ at: r.at, why: near ? `no such line on this record — it carries "${near.mark}" there, differing from "${r.at.mark}" only in how it is typed` : "no such line on this record" });
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
344
396
|
if (seen.at?.engineOwned) { refused.push({ at: r.at, why: "the engine wrote this caveat, not a seat" }); continue; }
|
|
345
397
|
const at = r.at, text = String(r.text);
|
|
346
398
|
// THE WALK IS THE BOUNDS CHECK. `seen` came from this same record, so a resolved address names a
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// (copper-causeway could not see teal-conduit's VENERET: different noref slugs for the same VENZY).
|
|
9
9
|
// The store therefore lives one level up, keyed by the MARK:
|
|
10
10
|
//
|
|
11
|
-
// <studioRoot>/_known-conflicts/<kebab(mark)>.json (studioRoot = workspace-<agent>/studio/
|
|
11
|
+
// <studioRoot>/_known-conflicts/<kebab(mark)>.json (studioRoot = workspace-<agent>/studio/clearance-search)
|
|
12
12
|
//
|
|
13
13
|
// One file per searched mark name; workspace-per-agent keeps customers separated. Each file keeps the
|
|
14
14
|
// EXACT inner shape the tripwire already reads ({schema_version, marks:{"<mark key>":[rows]}}), so
|
package/driver/named-band.mjs
CHANGED
|
@@ -29,23 +29,6 @@ import { abbrev } from "./repair-contract.mjs";
|
|
|
29
29
|
|
|
30
30
|
export const BAND_STATES = ["enumerated", "incomplete"];
|
|
31
31
|
|
|
32
|
-
// ── WHAT ONE QUERY MAY PUT INTO THE BAND ─────────────────────────────────────────────────────────
|
|
33
|
-
//
|
|
34
|
-
// THE DEFECT (production run, 2026-09-16). A 2,146-record band, of which 1,154 — 54% — were reachable
|
|
35
|
-
// from two queries and nothing else. Both were machine-built forms of an ordinary short word, so they
|
|
36
|
-
// matched every mark containing that word across four registers. Measured on the preserved band: the
|
|
37
|
-
// three biggest queries returned 589, 583 and 271 records; the fourth returned 185. A ceiling at 200
|
|
38
|
-
// therefore bites exactly those three and leaves the rest of that plan untouched, which is why it is
|
|
39
|
-
// the number rather than a rounder one.
|
|
40
|
-
//
|
|
41
|
-
// AN OVER-CAP QUERY IS NOT TRUNCATED, IT IS RECLASSIFIED. The band already has a word for "this query
|
|
42
|
-
// matched more than we carried": `incomplete`, which produces a crowd descriptor carrying the full
|
|
43
|
-
// count. So the excess is DISCLOSED with its number rather than dropped — judgment reads the crowd and
|
|
44
|
-
// can say the ground was too broad to enumerate, which is a true statement about the search. Silently
|
|
45
|
-
// keeping the first two hundred would be the one outcome worse than the flood: a narrower band that
|
|
46
|
-
// reads as complete.
|
|
47
|
-
export const BAND_QUERY_CAP = 200;
|
|
48
|
-
|
|
49
32
|
/**
|
|
50
33
|
* Parse + lightly validate the named-band artifact. Returns { enumerated:[…records], crowds:[…descriptors] }.
|
|
51
34
|
* Throws `named_band_*` tokens (token FIRST) so the stage validator + corrective-retry can key on the defect,
|
|
@@ -112,22 +95,7 @@ export function parseNamedBand(raw) {
|
|
|
112
95
|
// at band-shape.mjs's `record_id` filter — the same loss, one step further from anything that
|
|
113
96
|
// could name it.
|
|
114
97
|
const recs = Array.isArray(b.records) ? b.records : [];
|
|
115
|
-
const
|
|
116
|
-
for (const r of recs) { if (r && typeof r === "object" && !Array.isArray(r)) kept.push({ ...r, ...prov }); }
|
|
117
|
-
if (kept.length > BAND_QUERY_CAP) {
|
|
118
|
-
// The count the provider reported is the truth about the ground; `kept.length` is only what this
|
|
119
|
-
// block carried. Prefer the reported total and fall back to what we hold, so the descriptor never
|
|
120
|
-
// claims a smaller crowd than it can prove.
|
|
121
|
-
const total = countOrNull(b.total_hits) ?? kept.length;
|
|
122
|
-
for (const r of kept.slice(0, BAND_QUERY_CAP)) enumerated.push(r);
|
|
123
|
-
crowds.push({
|
|
124
|
-
query, total_hits: total, fetched: BAND_QUERY_CAP, sample: [],
|
|
125
|
-
reason: `one query returned ${kept.length} record(s), over the ${BAND_QUERY_CAP}-record ceiling any single query may add to this band; the first ${BAND_QUERY_CAP} are carried and the rest are disclosed here as a crowd rather than enumerated`,
|
|
126
|
-
...(typeof b.qid === "string" && b.qid ? { qid: b.qid } : {}),
|
|
127
|
-
});
|
|
128
|
-
} else {
|
|
129
|
-
for (const r of kept) enumerated.push(r);
|
|
130
|
-
}
|
|
98
|
+
for (const r of recs) { if (r && typeof r === "object" && !Array.isArray(r)) enumerated.push({ ...r, ...prov }); }
|
|
131
99
|
} else {
|
|
132
100
|
// count-first rescue (2026-07-10, copper-lattice): a crowd descriptor may carry per-term truth —
|
|
133
101
|
// `term_counts` (each term's tool-derived count + disposition) and the fully-enumerated tractable
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
// ordinary-words.mjs — the impure edge of the one-letter neighbourhood rule: it reads the word list off disk.
|
|
4
|
+
//
|
|
5
|
+
// form-neighbourhood.mjs is PURE and takes the list as a Set; this is the one place that turns the file into
|
|
6
|
+
// that Set, so there is one reading of it and not one per caller. The file's own header states its source,
|
|
7
|
+
// its date and its licence; lines starting with `#` are that header and are skipped.
|
|
8
|
+
//
|
|
9
|
+
// A LIST THAT WILL NOT LOAD REMOVES NOTHING. The rule only ever takes queries away, so the safe failure is an
|
|
10
|
+
// empty Set — every neighbour searched, exactly as before the rule existed — and the caller is told, so the
|
|
11
|
+
// run record can say the rule did not apply rather than read as a mark with no ordinary-word neighbours.
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
/** Where each language's list lives. English only; other markets add a file and a key. */
|
|
20
|
+
export const WORD_LIST_PATHS = Object.freeze({ en: join(HERE, "wordlists", "en.txt") });
|
|
21
|
+
|
|
22
|
+
/** Parse a list file's text into a Set of lowercase words. PURE. */
|
|
23
|
+
export function parseWordList(text) {
|
|
24
|
+
const out = new Set();
|
|
25
|
+
for (const line of String(text ?? "").split("\n")) {
|
|
26
|
+
const w = line.trim();
|
|
27
|
+
if (!w || w.startsWith("#")) continue;
|
|
28
|
+
out.add(w.toLowerCase());
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const cache = new Map();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The ordinary-word Set for a language, read once per process.
|
|
37
|
+
* @returns {{ words: Set<string>, error: string|null }}
|
|
38
|
+
*/
|
|
39
|
+
export function loadOrdinaryWords(lang = "en") {
|
|
40
|
+
if (cache.has(lang)) return cache.get(lang);
|
|
41
|
+
const p = WORD_LIST_PATHS[lang];
|
|
42
|
+
let r;
|
|
43
|
+
if (!p) r = { words: new Set(), error: `no word list for language "${lang}"` };
|
|
44
|
+
else {
|
|
45
|
+
try { r = { words: parseWordList(readFileSync(p, "utf8")), error: null }; }
|
|
46
|
+
catch (e) { r = { words: new Set(), error: `word list ${lang} unreadable: ${String(e?.message ?? e).slice(0, 120)}` }; }
|
|
47
|
+
if (!r.error && !r.words.size) r = { words: r.words, error: `word list ${lang} holds no words` };
|
|
48
|
+
}
|
|
49
|
+
cache.set(lang, r);
|
|
50
|
+
return r;
|
|
51
|
+
}
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// sendPending with no .sent, so a lost/given-up marker (or an overnight finish) is retried
|
|
16
16
|
// on the prelim-outbox.timer cadence independent of heartbeat activeHours.
|
|
17
17
|
//
|
|
18
|
-
// TIGHT-LOOP INVARIANT (load-bearing): prelim-outbox.path is PathExistsGlob=…/
|
|
18
|
+
// TIGHT-LOOP INVARIANT (load-bearing): prelim-outbox.path is PathExistsGlob=…/clearance-outbox/*.pending —
|
|
19
19
|
// level-triggered, so a RETAINED marker re-triggers the service the moment it deactivates. The backoff
|
|
20
20
|
// sidecars therefore live in <outbox>/backoff/ — a subdirectory the glob can never match (`*` does not
|
|
21
21
|
// cross `/`, and "backoff" has no .pending suffix; inotify on the outbox dir doesn't recurse either) —
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
// ("due" / "retry 300") — a broken helper must degrade to retried wakes, never to lost deliveries.
|
|
27
27
|
|
|
28
28
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
29
|
-
import { join, basename } from "node:path";
|
|
29
|
+
import { join, basename } from "node:path"; import { studioDirFor } from "../shared/pre-rename-spellings.mjs";
|
|
30
30
|
import { fileURLToPath } from "node:url";
|
|
31
31
|
import { config } from "./driver.config.mjs";
|
|
32
32
|
import { atomicWrite } from "./progress.mjs";
|
|
@@ -130,20 +130,35 @@ export function settleWake(agent, { code, stdout }, now = Date.now()) {
|
|
|
130
130
|
// Which *.pending markers currently route to <agent>? Mirrors deliver-trigger.sh's grouping exactly: a
|
|
131
131
|
// JSON packet carries an "agent" field; a legacy delivered marker's first-line body IS the agent id.
|
|
132
132
|
export function markersForAgent(agent) {
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
// BOTH DIRECTORIES, AND THE OLD ONE IS NOT OPTIONAL. The default outbox was renamed with the `clearance`
|
|
134
|
+
// identifier; a box that never pinned `CLEAROTRON_OUTBOX_DIR` still has markers under the old name.
|
|
135
|
+
// Reading only the new directory would leave them there for ever, and each one is a report a client
|
|
136
|
+
// is owed — with nothing to see, because an outbox nobody reads looks exactly like an empty one.
|
|
137
|
+
// Each marker keeps the path it was found at, so an ack removes the file that actually exists.
|
|
135
138
|
const out = [];
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
139
|
+
// THE SECOND DIRECTORY IS SKIPPED WHEN IT IS THE FIRST ONE. The two accessors answered the same path
|
|
140
|
+
// once, and this loop then listed every marker twice — the same file handed out as two pieces of work,
|
|
141
|
+
// with the strike count that quarantines a stuck marker counting each wake twice. A guard rather than a
|
|
142
|
+
// dedupe at the end: two directories that are the same directory is a configuration fact worth costing
|
|
143
|
+
// nothing, and a dedupe would also hide a genuine duplicate filename across two real directories.
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
for (const dir of [config.outboxDir, config.legacyOutboxDir]) {
|
|
146
|
+
if (!dir || seen.has(dir)) continue;
|
|
147
|
+
seen.add(dir);
|
|
148
|
+
let names = [];
|
|
149
|
+
try { names = readdirSync(dir).filter((f) => f.endsWith(".pending")); } catch { continue; }
|
|
150
|
+
for (const file of names.sort()) {
|
|
151
|
+
let raw;
|
|
152
|
+
try { raw = readFileSync(join(dir, file), "utf8"); } catch { continue; } // raced an ack — skip
|
|
153
|
+
let who = null, kind = "delivered";
|
|
154
|
+
if (raw.trimStart().startsWith("{")) {
|
|
155
|
+
try { const j = JSON.parse(raw); who = j.agent != null ? String(j.agent) : null; kind = j.kind != null ? String(j.kind) : "delivered"; }
|
|
156
|
+
catch { who = null; }
|
|
157
|
+
} else {
|
|
158
|
+
who = raw.split("\n")[0].trim();
|
|
159
|
+
}
|
|
160
|
+
if (who === agent) out.push({ file, path: join(dir, file), kind });
|
|
145
161
|
}
|
|
146
|
-
if (who === agent) out.push({ file, path: join(config.outboxDir, file), kind });
|
|
147
162
|
}
|
|
148
163
|
return out;
|
|
149
164
|
}
|
|
@@ -188,7 +203,7 @@ function* eachRunDir() {
|
|
|
188
203
|
let workspaces = [];
|
|
189
204
|
try { workspaces = readdirSync(config.workspaceRoot).filter((n) => n.startsWith("workspace-")); } catch { return; }
|
|
190
205
|
for (const ws of workspaces) {
|
|
191
|
-
const studio = join(config.workspaceRoot, ws
|
|
206
|
+
const studio = studioDirFor(join(config.workspaceRoot, ws));
|
|
192
207
|
let slugs = [];
|
|
193
208
|
try { slugs = readdirSync(studio); } catch { continue; }
|
|
194
209
|
for (const slug of slugs) {
|
|
@@ -311,7 +326,7 @@ export function rescanOwedRuns() {
|
|
|
311
326
|
let workspaces = [];
|
|
312
327
|
try { workspaces = readdirSync(config.workspaceRoot).filter((n) => n.startsWith("workspace-")); } catch { return dropped; }
|
|
313
328
|
for (const ws of workspaces) {
|
|
314
|
-
const studio = join(config.workspaceRoot, ws
|
|
329
|
+
const studio = studioDirFor(join(config.workspaceRoot, ws));
|
|
315
330
|
let slugs = [];
|
|
316
331
|
try { slugs = readdirSync(studio); } catch { continue; }
|
|
317
332
|
for (const slug of slugs) {
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.2-beta.
|
|
5
|
+
"version": "0.3.2-beta.9",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
|
|
8
8
|
"engines": {
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"why": "a caption-only call renders 149B, clears the 120-char floor, and ships a SECTION OF THE CLIENT'S ONE REPORT without the 'Checks we ran' bullets, the methodology or the handling note. Traced: report-overview.md is the head of report.md (assembleReportMd() in pipeline.mjs) which renders to the HTML (publish/index.mjs:649 publishReport). The 'Only you can close these' register is NOT lost — it is code-built from findings.json under spec 64's 'code wins' ruling."
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
|
-
"tool": "
|
|
62
|
+
"tool": "record_clearance_variants",
|
|
63
63
|
"fields": [
|
|
64
64
|
"incumbent_classes",
|
|
65
65
|
"watchlist_owners",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"why": "the schema the seat is handed declares `fields` REQUIRED and acceptBlindFrame takes a call without it — the goods/on-field half of the cold threat model"
|
|
85
85
|
},
|
|
86
86
|
{
|
|
87
|
-
"tool": "
|
|
87
|
+
"tool": "record_clearance_variants",
|
|
88
88
|
"fields": [
|
|
89
89
|
"elements",
|
|
90
90
|
"scope_ledger"
|