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
|
@@ -178,7 +178,83 @@ export function omittedFromRender(rows) {
|
|
|
178
178
|
const list = Array.isArray(rows) ? rows : (rows?.rows ?? []);
|
|
179
179
|
return list.filter((r) => !rowIsSettled(r, r))
|
|
180
180
|
.map((r) => ({ row_id: r?.row_id ?? null, kind: r?.kind ?? null, select: selectionOf(r), mark: r?.mark ?? null,
|
|
181
|
-
missing: [!seatFields(r).tier ? "tier" : null, !seatFields(r).reason ? "reason" : null].filter(Boolean)
|
|
181
|
+
missing: [!seatFields(r).tier ? "tier" : null, !seatFields(r).reason ? "reason" : null].filter(Boolean),
|
|
182
|
+
// WHY, in the parser's own token. `missing` names only the two judgement fields, so a row refused on a
|
|
183
|
+
// fact the DRIVER copies — an owner the register never supplied — read as an omission with no cause
|
|
184
|
+
// at all, `missing: []`. That is how 141 tiered rows sat outstanding for a day with nobody able to say
|
|
185
|
+
// why: the answer was one parser call away and nothing made it.
|
|
186
|
+
cause: seatRefusal(r) ?? registerFactRefusal(r),
|
|
187
|
+
owed_by: seatRefusal(r) ? "seat" : "register" }));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// The parser tokens that speak about a fact the driver COPIES onto a register row — mark, owner,
|
|
191
|
+
// jurisdiction, records all come from the canonical row (renderEntry), never from the seat. On a SEAT row the
|
|
192
|
+
// same four are the seat's own writing.
|
|
193
|
+
const REGISTER_FACT_TOKENS = new Set(["placement_mark_missing", "placement_owner_missing",
|
|
194
|
+
"placement_jurisdiction_invalid", "placement_records_invalid"]);
|
|
195
|
+
const tokenOf = (e) => String(e?.message ?? e).split(/[:=\s(]/)[0] || "placement_invalid";
|
|
196
|
+
// Stand-ins that every driver-fact check accepts, so a check of the JUDGEMENT alone cannot be refused on a
|
|
197
|
+
// driver fact. They never reach a rendered file: this is a probe of the parser, not an entry.
|
|
198
|
+
const FACTS_PRESENT = Object.freeze({ mark: "-", owner: "-", jurisdiction: "", records: [] });
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The seat's part of a row, refused — or null when the seat has done its part. On a register row that is
|
|
202
|
+
* the judgement alone (tier, reason, borderline), probed with the driver's facts stood in; on a seat row it
|
|
203
|
+
* is the whole row, because the seat wrote all of it. PURE; never throws.
|
|
204
|
+
*/
|
|
205
|
+
export function seatRefusal(row) {
|
|
206
|
+
try {
|
|
207
|
+
validatePlacement(renderEntry(row?.kind === "register" ? FACTS_PRESENT : row, row), 0);
|
|
208
|
+
return null;
|
|
209
|
+
} catch (e) { return tokenOf(e); }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The register's part of a register row, refused — or null. A fact the register did not supply (no owner on
|
|
214
|
+
* the record) is not something a corrective turn can repair: the seat is told not to type these fields and
|
|
215
|
+
* anything it types is ignored. Null on a seat row. PURE; never throws.
|
|
216
|
+
*/
|
|
217
|
+
export function registerFactRefusal(row) {
|
|
218
|
+
if (row?.kind !== "register") return null;
|
|
219
|
+
try {
|
|
220
|
+
validatePlacement(renderEntry(row, { tier: PLACEMENT_TIERS[0], reason: "register fact probe" }), 0);
|
|
221
|
+
return null;
|
|
222
|
+
} catch (e) { const t = tokenOf(e); return REGISTER_FACT_TOKENS.has(t) ? t : null; }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* What a placement pass recorded, split by who owes each gap. The gate reads this; so does the run record.
|
|
227
|
+
*
|
|
228
|
+
* unjudged rows whose seat part the parser refuses — the seat owes them, a corrective turn can
|
|
229
|
+
* repair them, and a pass that leaves any is not a completed pass.
|
|
230
|
+
* register_selected register candidates the seat selected.
|
|
231
|
+
* register_rendered of those, how many reach placements.json.
|
|
232
|
+
* register_facts rows judged by the seat and still refused on a fact the register did not supply,
|
|
233
|
+
* counted by cause. A register that publishes no owner for a record is a real and
|
|
234
|
+
* ordinary case (measured: single records at offices that do not publish one), so a
|
|
235
|
+
* handful are recorded, not refused.
|
|
236
|
+
*
|
|
237
|
+
* `register_selected > 0 && register_rendered === 0` is the other refusal: a pass that tiered register
|
|
238
|
+
* candidates and delivered none of them has not made a judgement about the register, it has lost one. That
|
|
239
|
+
* is the shape of a register whose rows reach the fold without a field every placement needs. PURE.
|
|
240
|
+
*/
|
|
241
|
+
export function placementRenderAccount(rows) {
|
|
242
|
+
const list = Array.isArray(rows) ? rows : (rows?.rows ?? []);
|
|
243
|
+
const unjudged = [];
|
|
244
|
+
const registerFacts = {};
|
|
245
|
+
let registerSelected = 0, registerRendered = 0;
|
|
246
|
+
for (const r of list) {
|
|
247
|
+
if (!r || typeof r !== "object") continue;
|
|
248
|
+
const isRegister = r.kind === "register";
|
|
249
|
+
if (isRegister) registerSelected += 1;
|
|
250
|
+
const seat = seatRefusal(r);
|
|
251
|
+
if (seat) { unjudged.push({ row_id: r.row_id ?? null, kind: r.kind ?? null, select: selectionOf(r), cause: seat }); continue; }
|
|
252
|
+
if (!isRegister) continue;
|
|
253
|
+
if (rowIsSettled(r, r)) { registerRendered += 1; continue; }
|
|
254
|
+
const cause = registerFactRefusal(r) ?? "placement_invalid";
|
|
255
|
+
registerFacts[cause] = (registerFacts[cause] ?? 0) + 1;
|
|
256
|
+
}
|
|
257
|
+
return { unjudged, register_selected: registerSelected, register_rendered: registerRendered, register_facts: registerFacts };
|
|
182
258
|
}
|
|
183
259
|
|
|
184
260
|
/**
|
|
@@ -76,7 +76,7 @@ const ENTRY_KEYS = ["mark", "owner", "jurisdiction", "records", "tier", "reason"
|
|
|
76
76
|
// decides it — a fluent, specific, entirely wrong reason passes this test, and should, because it is
|
|
77
77
|
// arguable and arguing with it is the digest's job, not the parser's. B2 puts that judgment where it
|
|
78
78
|
// belongs: the digest is instructed to adopt-or-counter each tier BY ENGAGING ITS STATED REASON
|
|
79
|
-
// (stages.mjs +
|
|
79
|
+
// (stages.mjs + clearance-register/digest.md + synthesis-rules.md §1/§2), and narrative-refutation joins
|
|
80
80
|
// on the same JSON. This function only guarantees there is a reason to engage WITH. It is a floor
|
|
81
81
|
// against a degenerate write, not a measure of quality, and it is deliberately the only test here that
|
|
82
82
|
// looks at the reason's words at all.
|
package/driver/portal-report.mjs
CHANGED
|
@@ -554,8 +554,24 @@ const EMBED_JS = `
|
|
|
554
554
|
parent.postMessage({source:TAG,type:'controls',commands:have},'*');
|
|
555
555
|
}catch(e){}
|
|
556
556
|
}
|
|
557
|
+
// AND WHICH SECTIONS IT HOLDS. The renderer's own breadcrumb rides inside the report's sticky header,
|
|
558
|
+
// which this file strips, so the list arrives as a value (window.__CORD_SECTIONS, written above this
|
|
559
|
+
// script) and the shell draws it in its own header. Only the ids that are really in the document are
|
|
560
|
+
// announced: a report whose renderer named a section it did not draw would otherwise offer the reader
|
|
561
|
+
// a breadcrumb entry that jumps nowhere.
|
|
562
|
+
function sections(){
|
|
563
|
+
try{
|
|
564
|
+
var list=window.__CORD_SECTIONS;
|
|
565
|
+
if(!list||!list.length)return;
|
|
566
|
+
var live=[];
|
|
567
|
+
for(var i=0;i<list.length;i++) if(document.getElementById(list[i].id)) live.push(list[i]);
|
|
568
|
+
parent.postMessage({source:TAG,type:'sections',sections:live},'*');
|
|
569
|
+
}catch(e){}
|
|
570
|
+
}
|
|
557
571
|
window.addEventListener('load',controls);
|
|
558
572
|
document.addEventListener('DOMContentLoaded',controls);
|
|
573
|
+
window.addEventListener('load',sections);
|
|
574
|
+
document.addEventListener('DOMContentLoaded',sections);
|
|
559
575
|
window.addEventListener('load',schedule);
|
|
560
576
|
document.addEventListener('DOMContentLoaded',schedule);
|
|
561
577
|
document.addEventListener('toggle',schedule,true);
|
|
@@ -570,6 +586,17 @@ const EMBED_JS = `
|
|
|
570
586
|
// A FAILED COMMAND SAYS SO. This used to be catch(err){} — a button that did nothing, told nobody,
|
|
571
587
|
// and left no console trace, which is how "Export does not work" went unnoticed through a whole
|
|
572
588
|
// round of testing. The portal now hears about it and can say so.
|
|
589
|
+
// A BREADCRUMB PRESS IN THE SHELL'S HEADER. Not a verb the document defines — it is the anchor jump
|
|
590
|
+
// below, asked for from outside, and it answers on the same channel: reveal any collapsed ancestor,
|
|
591
|
+
// then post the target's document-relative top for the PARENT to scroll to, because this frame has
|
|
592
|
+
// no scrollport of its own. An id that is not in the document is ignored rather than failed: the
|
|
593
|
+
// shell only ever draws ids the document announced.
|
|
594
|
+
if(d.command==='section'){
|
|
595
|
+
var sec=document.getElementById(String(d.value||''));
|
|
596
|
+
if(sec){ revealTarget(sec); var t=Math.max(0,Math.ceil(sec.getBoundingClientRect().top+window.scrollY));
|
|
597
|
+
try{parent.postMessage({source:TAG,type:'scrollTo',top:t},'*');}catch(e){} schedule(); }
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
573
600
|
var fn = d.command==='exportPDF' ? (typeof exportPDF==='function'?exportPDF:null)
|
|
574
601
|
: d.command==='pickAll' ? (typeof pickAll==='function'?pickAll:null)
|
|
575
602
|
: d.command==='openAll' ? (typeof openAll==='function'?openAll:null)
|
|
@@ -595,7 +622,7 @@ const EMBED_JS = `
|
|
|
595
622
|
// Registered AFTER the document's own script by construction (this block is appended at the end of
|
|
596
623
|
// <body>), so by the time it runs for an '#c<ordinal>' link the ancestors are already open and the
|
|
597
624
|
// measured offset is the revealed position. preventDefault stops the frame's dead-end fragment
|
|
598
|
-
// navigation for the links the document's handler does not claim (#common-law, #
|
|
625
|
+
// navigation for the links the document's handler does not claim (#common-law, #next).
|
|
599
626
|
// AND WHEN THERE IS NO PARENT, DO NOT CANCEL THE CLICK.
|
|
600
627
|
//
|
|
601
628
|
// Everything above is right INSIDE the portal's Result screen. Un-framed it is a dead link: parent is
|
|
@@ -630,6 +657,28 @@ const EMBED_JS = `
|
|
|
630
657
|
try{parent.postMessage({source:TAG,type:'scrollTo',top:top},'*');}catch(e){}
|
|
631
658
|
schedule();
|
|
632
659
|
});
|
|
660
|
+
// A FINDING'S OWN ASK AI. The document draws the button on every finding card and the portal holds the
|
|
661
|
+
// control, so the press crosses to the parent like the anchor jump above, carrying which finding it was.
|
|
662
|
+
// A clearance numbers its findings once, in the card's id; a knockout restarts its numbers for each name,
|
|
663
|
+
// so the name's position rides beside it. The "Also considered" cards carry the button and no number —
|
|
664
|
+
// a record that was ruled out was never given one — and still post, with the number null: the parent
|
|
665
|
+
// opens the control about the report rather than leaving a button that does nothing. Un-framed there is
|
|
666
|
+
// nobody to answer, so nothing is posted.
|
|
667
|
+
document.addEventListener('click',function(e){
|
|
668
|
+
var b=e.target.closest?e.target.closest('.ask-fi'):null;
|
|
669
|
+
if(!b||!framed)return;
|
|
670
|
+
var card=b.closest('.card');
|
|
671
|
+
var whole=function(v){var n=parseInt(v,10);return String(n)===String(v)&&n>=0?n:null;};
|
|
672
|
+
var koOrd=card?card.getAttribute('data-ko-ord'):null;
|
|
673
|
+
var idOrd=card&&(card.id||'').charAt(0)==='c'?card.id.slice(1):null;
|
|
674
|
+
var ord=koOrd!==null?whole(koOrd):(idOrd!==null?whole(idOrd):null);
|
|
675
|
+
var koMark=card?card.getAttribute('data-ko-mark'):null;
|
|
676
|
+
var mi=koMark!==null?whole(koMark):null;
|
|
677
|
+
// A press that cannot reach the page must not vanish: the button would look pressed and do nothing,
|
|
678
|
+
// the same silence a broken Export once hid behind. The console is the one place left to say so.
|
|
679
|
+
try{parent.postMessage({source:TAG,type:'askAi',ordinal:ord,markIndex:mi},'*');}
|
|
680
|
+
catch(err){try{console.warn('Ask AI could not reach the page: '+(err&&err.message||err));}catch(e){}}
|
|
681
|
+
});
|
|
633
682
|
schedule();
|
|
634
683
|
})();
|
|
635
684
|
`;
|
|
@@ -746,8 +795,42 @@ const FEEDBACK_JS = `
|
|
|
746
795
|
// Appended at the very end of <body>: the stylesheet is inlined into <head>, so a rule placed here wins
|
|
747
796
|
// the cascade on equal specificity without needing !important, and the script runs with the document's
|
|
748
797
|
// own functions already defined.
|
|
749
|
-
|
|
750
|
-
|
|
798
|
+
/**
|
|
799
|
+
* THE SECTION BREADCRUMB, LIFTED OUT BEFORE THE HEADER GOES (owner, 2026-09-18).
|
|
800
|
+
*
|
|
801
|
+
* The renderers emit the breadcrumb INSIDE `.rep-stickyhead` — one header, one sticky surface — and this
|
|
802
|
+
* function strips that header, because the portal draws its own. So the breadcrumb has to leave the
|
|
803
|
+
* document and arrive as data: the portal draws it in `.report-head`, beside READS, where it stays on
|
|
804
|
+
* top of a six-thousand-pixel read.
|
|
805
|
+
*
|
|
806
|
+
* It could not stay in the frame whatever this stripped. The frame is sized to its content
|
|
807
|
+
* (Result.tsx), so the framed document has no scrollport of its own and `position:sticky` inside it
|
|
808
|
+
* pins to nothing — which is exactly what a reader met: a bar sitting over the confidentiality line
|
|
809
|
+
* that scrolled away with the page.
|
|
810
|
+
*
|
|
811
|
+
* READ OFF THE MARKUP THE RENDERER WROTE, never recomposed from a list of section names here. A second
|
|
812
|
+
* copy of the vocabulary is how a renderer comes to disagree with the shell about what a report holds.
|
|
813
|
+
* Returns [] for a document with no strip — `sectionStrip()` emits none under two live sections — and
|
|
814
|
+
* the shell then draws no breadcrumb, which is the right answer rather than an empty row.
|
|
815
|
+
*/
|
|
816
|
+
const STRIP_NAV_RE = /<nav class="[^"]*\bstrip\b[^"]*"[^>]*>([\s\S]*?)<\/nav>/i;
|
|
817
|
+
const STRIP_LINK_RE = /<a\b[^>]*\bdata-sec="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
818
|
+
export function sectionsOf(html) {
|
|
819
|
+
const nav = STRIP_NAV_RE.exec(String(html ?? ""));
|
|
820
|
+
if (!nav) return [];
|
|
821
|
+
const out = [];
|
|
822
|
+
for (const m of nav[1].matchAll(STRIP_LINK_RE)) {
|
|
823
|
+
const label = m[2].replace(/<[^>]*>/g, "").replace(/\s+/g, " ").trim();
|
|
824
|
+
if (m[1] && label) out.push({ id: m[1], label });
|
|
825
|
+
}
|
|
826
|
+
return out;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function injectEmbedLayer(html, { feedback = false, sections = [] } = {}) {
|
|
830
|
+
// The breadcrumb list is a VALUE the injected script reads, not markup: `<` is escaped so a label can
|
|
831
|
+
// never close the script element it rides inside.
|
|
832
|
+
const secs = JSON.stringify(sections ?? []).replace(/</g, "\\u003c");
|
|
833
|
+
const block = `<style data-embed="portal">${EMBED_CSS}</style><script data-embed="portal">window.__CORD_SECTIONS=${secs};${EMBED_JS}</script>`
|
|
751
834
|
+ (feedback ? `<style data-embed="feedback">${FEEDBACK_CSS}</style><script data-embed="feedback">${FEEDBACK_JS}</script>` : "");
|
|
752
835
|
const i = html.lastIndexOf("</body>");
|
|
753
836
|
// No </body> means markup we do not recognise; appending is still correct and still parses.
|
|
@@ -787,6 +870,10 @@ export function prepareReportForEmbed(html, { staff = false, poolRoot = null, fe
|
|
|
787
870
|
const note = (tag) => unbalanced.push(tag);
|
|
788
871
|
const missingCss = [];
|
|
789
872
|
|
|
873
|
+
// READ BEFORE ANYTHING IS STRIPPED. The breadcrumb rides inside `.rep-stickyhead`, which the chrome
|
|
874
|
+
// strip below removes, so this is the last moment it exists.
|
|
875
|
+
const sections = sectionsOf(html);
|
|
876
|
+
|
|
790
877
|
const nav = stripBalanced(html, NAV_RE, "nav", note);
|
|
791
878
|
let out = nav.html;
|
|
792
879
|
const strippedNav = nav.removed;
|
|
@@ -865,11 +952,11 @@ export function prepareReportForEmbed(html, { staff = false, poolRoot = null, fe
|
|
|
865
952
|
|
|
866
953
|
// After the stylesheet, so the injected rule sits later in the cascade than the sheet it overrides —
|
|
867
954
|
// and after every strip, so nothing above can match the injected markup and eat it.
|
|
868
|
-
out = injectEmbedLayer(out, { feedback });
|
|
955
|
+
out = injectEmbedLayer(out, { feedback, sections });
|
|
869
956
|
|
|
870
957
|
return {
|
|
871
958
|
html: out, strippedNav, neutralised, mcpLeaks, tokensDropped, ratedUnderDropped, runUnderProjectDropped,
|
|
872
|
-
internalTailsDropped, reviewerCodesDropped, unbalanced, missingCss,
|
|
959
|
+
internalTailsDropped, reviewerCodesDropped, unbalanced, missingCss, sections,
|
|
873
960
|
};
|
|
874
961
|
}
|
|
875
962
|
|
|
@@ -129,7 +129,7 @@ export const allowanceExhaustedLine = (cap, who) =>
|
|
|
129
129
|
const INSTALL_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
130
130
|
|
|
131
131
|
const READ_OFF_NOTE = "Reading a brief is not available on this instance — set the search up below.";
|
|
132
|
-
import { basename, dirname, join, resolve as pathResolve } from "node:path";
|
|
132
|
+
import { basename, dirname, join, resolve as pathResolve } from "node:path"; import { studioDirFor } from "../shared/pre-rename-spellings.mjs";
|
|
133
133
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
134
134
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
135
135
|
import { makePrincipal, assertPrincipal, genericOrgOf, mayReadRun, reachCovers, principalView, seesEverything, mayRun,
|
|
@@ -677,7 +677,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
677
677
|
};
|
|
678
678
|
// THE CANONICAL INTAKE FIRST, and it is why this parameter exists. `CLEAROTRON_QUEUE_DIR` is where the
|
|
679
679
|
// enqueue CLI and ops-MCP `start_run` write — which is where the PORTAL's own submissions land, since
|
|
680
|
-
// its `trigger` is an ops-MCP hop. The walk below finds only `workspace-*/studio/
|
|
680
|
+
// its `trigger` is an ops-MCP hop. The walk below finds only `workspace-*/studio/clearance-search/queue`,
|
|
681
681
|
// and a documented headless install has no workspaces at all: measured on the test box, the only queue
|
|
682
682
|
// under the whole tree is the configured one, and it holds portal-prefixed jobs. So this scan ran ZERO
|
|
683
683
|
// times there, and a submitted search was invisible on the dashboard from submit until claim — a
|
|
@@ -687,7 +687,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
687
687
|
}
|
|
688
688
|
try {
|
|
689
689
|
for (const ws of readdirSync(workspaceRoot).filter((n) => n.startsWith("workspace-"))) {
|
|
690
|
-
const studio = join(workspaceRoot, ws
|
|
690
|
+
const studio = studioDirFor(join(workspaceRoot, ws));
|
|
691
691
|
let slugs = []; try { slugs = readdirSync(studio); } catch { continue; }
|
|
692
692
|
for (const slug of slugs) {
|
|
693
693
|
// Still walked, so a deployment whose queue is not in `queueDirs` keeps working. The union is
|
|
@@ -1332,7 +1332,24 @@ export function makePortalService({
|
|
|
1332
1332
|
// explains a control that cannot be used, `coverageNote` qualifies one that can. Folding this
|
|
1333
1333
|
// into the first would make every caller of `productAvailability` — the portal, the MCP door,
|
|
1334
1334
|
// the dev cockpit — read a disclosure as a refusal, which is the behaviour the ruling removes.
|
|
1335
|
-
|
|
1335
|
+
// ── THE PRODUCT ROWS CARRY NO COVERAGE SENTENCE ───────────────────────────────────────────
|
|
1336
|
+
//
|
|
1337
|
+
// This row printed a four-line paragraph — what the wired register reaches, of how many places
|
|
1338
|
+
// this search can name, and that the rest would be disclosed in the report as deferred coverage
|
|
1339
|
+
// rather than reported as clear. The owner met it on a running install and ruled it out on
|
|
1340
|
+
// 2026-09-18: it is on no board, and it describes the behaviour that naming an unreachable
|
|
1341
|
+
// territory no longer has. A named territory the register cannot reach is now refused at the
|
|
1342
|
+
// door, in one sentence naming the territory — so a paragraph promising to defer it instead
|
|
1343
|
+
// tells the reader the opposite of what the engine will do.
|
|
1344
|
+
//
|
|
1345
|
+
// NOTHING REPLACES IT, AND NO OTHER ROW GETS ONE. What a worldwide search on a partial register
|
|
1346
|
+
// actually reached belongs in the report that describes the search that ran, not in the form
|
|
1347
|
+
// that orders it.
|
|
1348
|
+
//
|
|
1349
|
+
// THE SENTENCE STILL EXISTS, one surface later, and deliberately: the review step before the
|
|
1350
|
+
// spend still states what is being bought. That is a different question asked at a different
|
|
1351
|
+
// moment — this row is "which search", that screen is "this is what you are committing to" —
|
|
1352
|
+
// and it was a separate ruling. `coverageDisclosure` keeps composing it for that caller.
|
|
1336
1353
|
// ── — THE PRODUCT DECLARES WHAT IT NEEDS, so the row can say so ─────
|
|
1337
1354
|
//
|
|
1338
1355
|
// The owner ordered the one product carrying `caseLaw: true` and first heard of the lane in the
|
|
@@ -1349,7 +1366,7 @@ export function makePortalService({
|
|
|
1349
1366
|
// a warning on every deployment whose writer has not run since is worse than the silence it
|
|
1350
1367
|
// replaces.
|
|
1351
1368
|
return { ...l, available: cause === null, unavailableNote: cause ? UNAVAILABLE_NOTE[cause] : null,
|
|
1352
|
-
coverageNote:
|
|
1369
|
+
coverageNote: null,
|
|
1353
1370
|
capabilityNote: l.caseLaw && caseLawReady === false ? CASE_LAW_DARK_NOTE : null };
|
|
1354
1371
|
}),
|
|
1355
1372
|
// ── the TERRITORY affordance ─────────────────────────────────────────────────────────────
|
|
@@ -1742,7 +1759,7 @@ export function makePortalService({
|
|
|
1742
1759
|
// The recipeKey arm that used to sit here (a 422 when saved searches were "not switched on") went
|
|
1743
1760
|
// with CLEAROTRON_RECIPES_MODE on 2026-07-27: a saved search is now honoured wherever it resolves.
|
|
1744
1761
|
// Asked of the RESOLVED product, not of the body: a request that names none resolves through
|
|
1745
|
-
// the account's default and its own territories, and the old read (`body.searchLevel || "
|
|
1762
|
+
// the account's default and its own territories, and the old read (`body.searchLevel || "clearance"`)
|
|
1746
1763
|
// answered about a product nobody had chosen. `resolveFor` fails open to a null resolution, and a
|
|
1747
1764
|
// null one is not judged here — validateJob and the scope rules below still run, and the runner
|
|
1748
1765
|
// is the wall.
|
|
@@ -2503,8 +2520,14 @@ async function connectorDoorKind(url) {
|
|
|
2503
2520
|
//
|
|
2504
2521
|
// COMPOSED IN ONE PLACE and handed over as a string. The browser cannot know this install's
|
|
2505
2522
|
// path, so the three surfaces stating this route cannot drift apart even if someone tries.
|
|
2523
|
+
//
|
|
2524
|
+
// AND IT NAMES THE DISTRIBUTION, like every other surface that states this route. Without the
|
|
2525
|
+
// target this one composer answers as it does for an install that is not under WSL at all, so on
|
|
2526
|
+
// a WSL box this field alone carried the inside-WSL line while the rows beside it led with the
|
|
2527
|
+
// Windows-side one. Nothing draws this field today; it is on the wire, and a field that answers
|
|
2528
|
+
// differently from the rows is a trap for whoever draws it next.
|
|
2506
2529
|
const stdio = seesEverything(principal)
|
|
2507
|
-
? stdioConnectOffer({ workDir: process.env.CLEAROTRON_WORK_DIR || null, reportsDir: process.env.CLEAROTRON_REPORTS_DIR || null })
|
|
2530
|
+
? stdioConnectOffer({ workDir: process.env.CLEAROTRON_WORK_DIR || null, reportsDir: process.env.CLEAROTRON_REPORTS_DIR || null, wsl: wslTarget() })
|
|
2508
2531
|
: null;
|
|
2509
2532
|
|
|
2510
2533
|
// ── THE PAGE IS HANDED ANSWERS, NOT FACTS TO REASON FROM ─────────────
|
|
@@ -3051,7 +3074,10 @@ async function connectorDoorKind(url) {
|
|
|
3051
3074
|
// who signed in. Dynamic import deliberately: `profiles.mjs` captures the store directory at
|
|
3052
3075
|
// MODULE LOAD (see this file's note above), so it is never pulled in at our own load time.
|
|
3053
3076
|
const { companyFactsOf } = await import("./profiles.mjs");
|
|
3054
|
-
|
|
3077
|
+
// A company whose file would not load is named with its reason (staff-only route), never left out
|
|
3078
|
+
// in silence: a switcher with one company where there were twenty-five reads as deleted work.
|
|
3079
|
+
const unreadable = Array.isArray(profiles?.unreadable) ? profiles.unreadable.map((u) => ({ key: u.key, reason: u.reason })) : [];
|
|
3080
|
+
return { status: 200, json: { customers: [...profiles.values()].map((p) => ({ key: p.key, name: p.name, ...companyFactsOf(p) })), ...(unreadable.length ? { unreadable } : {}) } };
|
|
3055
3081
|
}
|
|
3056
3082
|
// /portal/admin/config — what this deployment actually has switched on. Read from the SNAPSHOT,
|
|
3057
3083
|
// never from process.env: this process has no engine environment, so asking its own env would
|
|
@@ -98,7 +98,7 @@ export const PATH_FIELDS = ["frameworkPath", "workedExamplesPath"];
|
|
|
98
98
|
/**
|
|
99
99
|
* The code-owned values, READ-ONLY, for display. The page shows them badged; it cannot send them.
|
|
100
100
|
*
|
|
101
|
-
* Two of them are PATHS INSIDE THE ENGINE — `skills/
|
|
101
|
+
* Two of them are PATHS INSIDE THE ENGINE — `skills/clearance-search/risk-framework-<customer>.md` — and they
|
|
102
102
|
* are withheld from a client here, on the server, where the role is already in hand. The React page has
|
|
103
103
|
* filtered them out of its own render since the rebuild, but a filter in the browser is a display
|
|
104
104
|
* convenience and not a wall: the value still crossed the wire and was one devtools tab away.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// the fifteen return-path transports were measured with that hole:
|
|
10
10
|
//
|
|
11
11
|
// record_report_overview actions, methodology, handling_note (a section of the client's report)
|
|
12
|
-
//
|
|
12
|
+
// record_clearance_variants incumbent_classes, watchlist_owners, search_floor
|
|
13
13
|
// record_blind_frame sources
|
|
14
14
|
// record_matter_frame scope_jurisdictions, excluded_jurisdictions
|
|
15
15
|
// record_unit_note null_result, note
|
|
@@ -65,8 +65,8 @@ export function refuseUndeclared(params, declared, token, path = "") {
|
|
|
65
65
|
// does not declare it: `narrative.corrections`, accepted and dropped. That is what this refuses.
|
|
66
66
|
//
|
|
67
67
|
// Refusing unknown TOP-LEVEL keys as well was the first cut, and it was wrong. Real traffic carries
|
|
68
|
-
// envelope fields the tool schema does not declare — the
|
|
69
|
-
// `schema_version`, which `
|
|
68
|
+
// envelope fields the tool schema does not declare — the clearance-variants mock sends
|
|
69
|
+
// `schema_version`, which `acceptClearanceVariants` ignores because it writes its OWN
|
|
70
70
|
// `schema_version: SCHEMA_VERSION` into the model. Inert for as long as it has existed, and the
|
|
71
71
|
// strict version made it FATAL: the whole stage refused, the run dead, for a key nobody reads.
|
|
72
72
|
//
|
package/driver/product-rows.mjs
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
//
|
|
26
26
|
// So: a third module that contaminates neither, and one place to pin.
|
|
27
27
|
|
|
28
|
-
import { ORDERABLE_PRODUCTS, PRODUCT_POLICIES, RETIRED_POLICIES, policyFor } from "./search-policy.mjs";
|
|
28
|
+
import { ORDERABLE_PRODUCTS, PRODUCT_POLICIES, RETIRED_POLICIES, policyFor, productKeyAsRenamed } from "./search-policy.mjs";
|
|
29
29
|
import { maxNamesFor, productSpec } from "./products.mjs";
|
|
30
30
|
import { leversFromResolved, turnaround, turnaroundHours } from "./effort-model.mjs";
|
|
31
31
|
|
|
@@ -96,7 +96,7 @@ export function baseTurnaroundFor(policy) {
|
|
|
96
96
|
* exactly what shipped before.
|
|
97
97
|
*/
|
|
98
98
|
export function productRow(key) {
|
|
99
|
-
const k =
|
|
99
|
+
const k = productKeyAsRenamed(key); // a pre-rename key names the same product
|
|
100
100
|
const p = PRODUCT_POLICIES[k] ?? RETIRED_POLICIES[k];
|
|
101
101
|
if (!p) return null;
|
|
102
102
|
const base = baseTurnaroundFor(p);
|
package/driver/products.mjs
CHANGED
|
@@ -405,7 +405,7 @@ export function nativeLanguageMode(product) {
|
|
|
405
405
|
*
|
|
406
406
|
* Exported because two surfaces outside this module have to say it and both used to say something else:
|
|
407
407
|
* the resolution-time recommendation (jx-lanes.mjs zhScopeDepthNotes) and the DELIVERED REPORT's own
|
|
408
|
-
* coverage row (pipeline.mjs scriptScopeDisclosure). Both named `
|
|
408
|
+
* coverage row (pipeline.mjs scriptScopeDisclosure). Both named `clearance-jx` and `Depth 5` — an internal
|
|
409
409
|
* product key and a rung on a ladder — so the coverage row named a remedy that was a product
|
|
410
410
|
* deleted, quoted at a number that no longer exists, and could not have ordered either.
|
|
411
411
|
*
|
|
@@ -53,8 +53,8 @@ consumes it; a unit test greps each symbol, so a field with no live consumer fai
|
|
|
53
53
|
| `delivery` | delivery | `{ email: "summary", privileged: bool }` — the "Privileged & Confidential" header. Two further optional sub-keys are accepted: `style` (a prose string, guarded by the same anti-rule check as a context pack, dictated into the `report-overview` and `report-card` stages as presentation tone and never to `synthesis`, the rating stage) and `template` (a report-template name; `"standard"` is the only one that exists, and it is the default). **Absent ⇒ neutral default** (`{email:"summary"}` — deliberately SILENT on `privileged`, which is three-state on every surface that reads it (`confPosture`): `true` extends the marking to "Attorney Work Product", `false` is a deliberate OFF, and absent is no opinion, which gets the plain "Privileged & Confidential" every legal deliverable carries. Saying `false` in the neutral overlay read as an instruction to strip the marking, and a Generic-default clearance shipped with no line at all). `email` no longer selects anything: every run's mail is a COVER NOTE pointing at the one report. The old `"table"` value — a full review table inlined into the mail body — is **retired**; it is still accepted at load so stored profiles keep validating, and folded to `"summary"` by `normalizeDelivery`. A customer who wants their own house format gets it drafted by the assistant from the run's `report-data.json`, where a person reads it before it goes |
|
|
54
54
|
| `riskAppetite` | context | a PROSE-POSTURE string (optional) that flavours **emphasis + recommended follow-up** in delivery curation — the two stages that are dictated it, `report-overview` and `report-card` — **never the Level/Composite**. A load-time anti-threshold guard rejects numeric/threshold phrasing (`>50%`, `Level C or above`, `threshold`); the "never decides" invariance itself is gated by review |
|
|
55
55
|
| `marketplaceDensity` | structural | `"sparse"` (default) \| `"dense"` — selects the per-profile grid cell budget so a byte-heavy marketplace's verbatim stdout fits the worker output channel (sparse ⇒ 98-cell budget; dense ⇒ 16). Dense fits long retail listings (beverages and supplements); gaming stores stay sparse |
|
|
56
|
-
| `frameworkPath` | rating authority | optional path to the customer's OWN risk framework (`skills/
|
|
57
|
-
| `workedExamplesPath` | context | optional path to a per-customer worked-examples set (`skills/
|
|
56
|
+
| `frameworkPath` | rating authority | optional path to the customer's OWN risk framework (`skills/clearance-search/<file>.md`, path-escape-blocked). **The framework in force RATES the matter** — the customer's own if on file, else the Generic default `risk-framework.md`; nothing in between. Each framework is a prose deck (the client's own rubric, reasoned WITH) plus a `.manifest.json` sidecar carrying its band vocabulary (`test/framework-lint.test.mjs` guards the pair). Git + legal-team gated; the config UI shows which is in force, read-only |
|
|
57
|
+
| `workedExamplesPath` | context | optional path to a per-customer worked-examples set (`skills/clearance-search/<file>.md`). The analysis DEPTH TARGET in `synthesis`, calibrated under that customer's framework; **absent ⇒ the Generic default `worked-examples.md`** |
|
|
58
58
|
| `defaultProduct` | entitlement | which of the four searches runs when a request names none (`search-policy.mjs`). May be left unset, and that is not a gap — a clearance that names no product is then named by its own resolved territories |
|
|
59
59
|
| `allowedRecipes[]` | entitlement | the closed menu of searches this account may trigger; non-empty when present, and **absent ⇒ everything allowed** |
|
|
60
60
|
| `jxPolicy` | entitlement | the native-language deepening POSTURE — declared lanes / escalation / provider stance. Policy, never capability; frozen into the run sidecar so a resume keeps the posture the run started under |
|
|
@@ -90,7 +90,7 @@ value into a profile file is a load-time error.
|
|
|
90
90
|
the write door: the same profile saved through the editor is a validated auto-commit with no PR in
|
|
91
91
|
it, and the shared load-time validators are what both paths cannot get past.
|
|
92
92
|
3. **Rewrite check:** the skills are platform-agnostic (they follow the dictated list), but read
|
|
93
|
-
`skills/
|
|
93
|
+
`skills/clearance-common-law/SKILL.md` + `perplexity-prompts.md`, `skills/clearance-search/SKILL.md` +
|
|
94
94
|
`phase2-execution.md`, and the driver message prose in `stages.mjs` once against the new
|
|
95
95
|
industry — worked examples are gaming-flavored by history.
|
|
96
96
|
4. ONE validation run against a known/synthetic matter: confirm the grid swept exactly the
|
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
"delivery": { "email": "summary", "privileged": false },
|
|
18
18
|
"riskAppetite": "Evidence-first. Lead with the register position, then say plainly what the common-law picture adds or fails to settle. Name coverage limits in the body rather than in a footnote — an unread source is a fact about the search, not a caveat about the report. Keep crowded-field noise brief.",
|
|
19
19
|
"defaultProduct": "multi-country-focus-search",
|
|
20
|
-
"frameworkPath": "skills/
|
|
21
|
-
"workedExamplesPath": "skills/
|
|
20
|
+
"frameworkPath": "skills/clearance-search/risk-framework-demo.md",
|
|
21
|
+
"workedExamplesPath": "skills/clearance-search/worked-examples-demo.md",
|
|
22
22
|
"jxPolicy": { "laneDepth": { "ja": "full", "ko": "candidates" } },
|
|
23
23
|
"runCaps": { "dailyRuns": 12, "maxQueued": 2 }
|
|
24
24
|
}
|
package/driver/profiles.mjs
CHANGED
|
@@ -346,9 +346,12 @@ export const FIELD_CONSUMERS = {
|
|
|
346
346
|
|
|
347
347
|
// Per-customer reasoning-skill selection. frameworkPath is the customer's OWN risk framework — under doc 50
|
|
348
348
|
// it RATES the matter; absent ⇒ the Generic default rates it (DEFAULT_FRAMEWORK in framework.mjs). Constrained to
|
|
349
|
-
// the
|
|
349
|
+
// the clearance-search skill dir + a .md suffix so a profile cannot point the synthesis read at an arbitrary
|
|
350
350
|
// path. The SHARED doctrine lives identically across the per-customer frameworks; only the examples diverge.
|
|
351
|
-
|
|
351
|
+
// The folder's pre-rename spelling is accepted as the same folder: a store written before the rename names it,
|
|
352
|
+
// and one unmigrated profile must not take a company, or the whole roster, off the list. The file is read
|
|
353
|
+
// from whichever spelling the store holds (config.resolveSkillPath).
|
|
354
|
+
const SKILL_PATH_RE = /^skills\/(?:clearance|prelim)-search\/[A-Za-z0-9._-]+\.md$/;
|
|
352
355
|
|
|
353
356
|
// The delivery overlay every run gets. `email` is no longer a choice: every run's mail is a COVER NOTE
|
|
354
357
|
// pointing at the report (one report, one shape, per-lawyer client mail drafted by the assistant).
|
|
@@ -566,11 +569,11 @@ function validateProfileShape(key, p, { sparse = false } = {}) {
|
|
|
566
569
|
die(`marketplaceDensity must be ${MARKETPLACE_DENSITIES.map((v) => `"${v}"`).join(" or ")} `
|
|
567
570
|
+ `(got "${p.marketplaceDensity}")`);
|
|
568
571
|
// frameworkPath / workedExamplesPath (optional, Phase 2): a per-customer reasoning-skill file. Constrained
|
|
569
|
-
// to skills/
|
|
572
|
+
// to skills/clearance-search/*.md (no path escape) — a profile selects a SHIPPED skill, never an arbitrary path.
|
|
570
573
|
for (const k of ["frameworkPath", "workedExamplesPath"]) {
|
|
571
574
|
if (p[k] == null) continue;
|
|
572
575
|
if (typeof p[k] !== "string" || !SKILL_PATH_RE.test(p[k]) || p[k].includes(".."))
|
|
573
|
-
die(`${k} must be a path of the form "skills/
|
|
576
|
+
die(`${k} must be a path of the form "skills/clearance-search/<file>.md" (got ${JSON.stringify(p[k])})`);
|
|
574
577
|
}
|
|
575
578
|
// defaultProduct (optional): one of the four in the offering, by id. A typo hard-fails at load rather
|
|
576
579
|
// than silently running the wrong-priced product on every job.
|
|
@@ -679,25 +682,42 @@ let cache = null;
|
|
|
679
682
|
/** Read one directory of profiles/<key>.json → Map(key → profile). No generic requirement and no
|
|
680
683
|
* matchDomains check here: both are properties of the MERGED roster, not of one layer, and asserting
|
|
681
684
|
* them per-layer would refuse a base+overlay pair that is perfectly valid once combined. */
|
|
682
|
-
|
|
685
|
+
// ONE COMPANY THAT CANNOT BE READ MUST NOT TAKE THE OTHERS WITH IT — on the deployment's own store. Strict,
|
|
686
|
+
// every file or none, is right for an explicit directory: that is how this loader's rules are checked. On the
|
|
687
|
+
// configured store it refused every company over one file, and the portal then offered `generic` alone with
|
|
688
|
+
// no error, so a client's clearances looked deleted. So `tolerant` isolates each file: a company that fails is
|
|
689
|
+
// left out, its reason is kept on the returned map as `unreadable`, and asking for it by key refuses with that
|
|
690
|
+
// reason (resolveProfile). `generic` is never tolerated: it is the fallback every unprofiled job rates under,
|
|
691
|
+
// and replacing a deployment's own with the bundled one would change every such answer without a word.
|
|
692
|
+
function readProfilesLayer(dir, { tolerant = false } = {}) {
|
|
683
693
|
const profiles = new Map();
|
|
694
|
+
const unreadable = [];
|
|
684
695
|
for (const f of readdirSync(dir).filter((n) => n.endsWith(".json")).sort()) {
|
|
685
696
|
const key = f.replace(/\.json$/, "");
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
+
try {
|
|
698
|
+
let p;
|
|
699
|
+
try { p = JSON.parse(readFileSync(join(dir, f), "utf8")); }
|
|
700
|
+
catch (e) { throw new Error(`profiles/${f}: unparseable JSON (${e.message})`); }
|
|
701
|
+
validateProfileShape(key, p);
|
|
702
|
+
// Context pack (Phase 1): a sibling `<key>.context.md`, attached AFTER validateProfileShape so the
|
|
703
|
+
// deny-unknown-key gate (which governs the JSON shape) never sees it. Optional — absent ⇒ no pack.
|
|
704
|
+
// Validated at load (rule-shape + size budget) so a bad pack fails loudly here, like riskAppetite (F8).
|
|
705
|
+
const packPath = join(dir, CONTEXT_PACK_FILE(key));
|
|
706
|
+
const contextPack = existsSync(packPath) ? readFileSync(packPath, "utf8").trim() : "";
|
|
707
|
+
if (contextPack) assertContextPackShape(contextPack, `profiles/${CONTEXT_PACK_FILE(key)}`);
|
|
708
|
+
profiles.set(key, { key, ...p, ...(contextPack ? { contextPack } : {}) });
|
|
709
|
+
} catch (e) {
|
|
710
|
+
if (!tolerant || key === "generic") throw e;
|
|
711
|
+
unreadable.push({ key, file: f, reason: String(e?.message ?? e) });
|
|
712
|
+
}
|
|
697
713
|
}
|
|
714
|
+
Object.defineProperty(profiles, "unreadable", { value: unreadable, enumerable: false });
|
|
698
715
|
return profiles;
|
|
699
716
|
}
|
|
700
717
|
|
|
718
|
+
/** The companies a roster left out because their file would not load: `[{ key, file, reason }]`. */
|
|
719
|
+
export const unreadableProfiles = (profiles) => (Array.isArray(profiles?.unreadable) ? profiles.unreadable : []);
|
|
720
|
+
|
|
701
721
|
/** Load every profiles/<key>.json → Map(key → profile), OVERLAY OVER BASE. Hard-fails loudly
|
|
702
722
|
* on a generic.json missing from BOTH layers (the universal fallback — its absence would mis-profile
|
|
703
723
|
* EVERY job), on a configured-but-unreadable overlay, and on any matchDomains overlap in the merged
|
|
@@ -746,7 +766,7 @@ export function loadProfiles({ dir, force = false, includeTestFixtures, includeD
|
|
|
746
766
|
// the universal fallback the module REQUIRES by name, the thing every unprofiled job resolves to. That
|
|
747
767
|
// is the one file whose absence makes an empty store a refusal, so that is the only one that falls
|
|
748
768
|
// through. Everything else in a deployment's roster is the deployment's own.
|
|
749
|
-
const profiles = overlay ? readProfilesLayer(overlay) : readProfilesLayer(baseDir);
|
|
769
|
+
const profiles = overlay ? readProfilesLayer(overlay, { tolerant: true }) : readProfilesLayer(baseDir);
|
|
750
770
|
|
|
751
771
|
// ── A TEST FIXTURE IS PRESENT AND NEVER OFFERED ─────────────────────────────────────────────────
|
|
752
772
|
//
|
|
@@ -855,6 +875,15 @@ export function loadProfiles({ dir, force = false, includeTestFixtures, includeD
|
|
|
855
875
|
export function resolveProfile(job, { profiles = loadProfiles() } = {}) {
|
|
856
876
|
const key = String(job?.profileKey ?? "").trim();
|
|
857
877
|
if (key && profiles.has(key)) return profiles.get(key);
|
|
878
|
+
// A company whose file would not load is not an unknown company: say which, and why.
|
|
879
|
+
const unread = unreadableProfiles(profiles);
|
|
880
|
+
const failed = key ? unread.find((u) => u.key === key) : null;
|
|
881
|
+
if (failed) {
|
|
882
|
+
const err = new Error(`profile_unreadable:${key} — ${failed.reason}. The company exists but its profile could not be read, `
|
|
883
|
+
+ `so this run is refused rather than rated under another company's settings.`);
|
|
884
|
+
err.code = "profile_unreadable";
|
|
885
|
+
throw err;
|
|
886
|
+
}
|
|
858
887
|
// A NAMED-but-unknown key is not a graceful-degradation case, it is a roster mismatch — the two
|
|
859
888
|
// sides disagree about which config store is real, and falling back to `generic` silently strips
|
|
860
889
|
// the client's platforms, their self-exclusion seed and the framework that RATES the matter. That
|
|
@@ -875,6 +904,15 @@ export function resolveProfile(job, { profiles = loadProfiles() } = {}) {
|
|
|
875
904
|
if (dom === dl || dom.endsWith(`.${dl}`)) return p;
|
|
876
905
|
}
|
|
877
906
|
}
|
|
907
|
+
// WITH A COMPANY UNREAD, "no company matched" is not known: the unread one's domains are not in hand, and
|
|
908
|
+
// this job may be its. Falling to `generic` here is the silent wrong-company deliverable, so refuse.
|
|
909
|
+
if (unread.length) {
|
|
910
|
+
const err = new Error(`profile_roster_incomplete — ${unread.length} company profile(s) could not be read `
|
|
911
|
+
+ `(${unread.map((u) => u.file).join(", ")}), so whether this job belongs to one of them cannot be decided. `
|
|
912
|
+
+ `Name the company on the job, or fix the file.`);
|
|
913
|
+
err.code = "profile_roster_incomplete";
|
|
914
|
+
throw err;
|
|
915
|
+
}
|
|
878
916
|
return profiles.get("generic");
|
|
879
917
|
}
|
|
880
918
|
|