clearotron 0.3.2-beta.7 → 0.3.2-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +58 -26
- package/CONTRIBUTING.md +8 -8
- package/INSTALL.md +148 -81
- package/README.md +3 -3
- package/SECURITY.md +3 -3
- package/bin/brandowner.mjs +3 -3
- package/bin/framework-preflight.mjs +1 -1
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +151 -27
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +2 -1
- package/docs/INTAKE.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/architecture/03-run-lifecycle.md +6 -6
- package/docs/architecture/04-configuration-reference.md +32 -14
- package/docs/architecture/05-config-governance.md +23 -8
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/08-development-guide.md +6 -6
- package/docs/configuration.md +5 -5
- package/docs/decisions/0003-credential-model.md +1 -1
- package/docs/writing-standard.md +4 -0
- package/driver/CHANGELOG.md +124 -0
- package/driver/README.md +3 -3
- package/driver/band-size.mjs +59 -0
- package/driver/binding-layers.mjs +1 -1
- package/driver/citation-census.json +3 -3
- package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/company-bundle.mjs +3 -3
- package/driver/compose-read.mjs +8 -14
- package/driver/config-inventory.mjs +112 -9
- package/driver/consumption-ledger.mjs +2 -2
- package/driver/contract-arm2-baseline.json +2 -5
- package/driver/contract-dictation-registry.mjs +19 -19
- package/driver/contract-e3-backlog.mjs +43 -43
- package/driver/contract-e3-baseline.json +14 -14
- package/driver/contract-vocabulary.mjs +68 -27
- package/driver/deliver-trigger.sh +16 -16
- package/driver/demo-container.mjs +3 -3
- package/driver/dev-portal.mjs +3 -3
- package/driver/disposition-call.mjs +1 -1
- package/driver/door-gates.mjs +41 -7
- package/driver/doubt-ledger.mjs +2 -2
- package/driver/drainer-identity.mjs +34 -8
- package/driver/driver.config.mjs +367 -104
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/README.md +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +3 -3
- package/driver/engine/mcp/gather-config.mjs +9 -9
- package/driver/engine/mcp/perplexity-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +18 -5
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/enqueue-schema.mjs +6 -2
- package/driver/findings-model.mjs +6 -3
- package/driver/flag-snapshot.mjs +34 -8
- package/driver/form-neighbourhood.mjs +54 -7
- package/driver/framework.mjs +4 -4
- package/driver/gateway.mjs +36 -24
- package/driver/jx-lanes.mjs +23 -4
- package/driver/jx-units.mjs +7 -4
- package/driver/jx.mjs +34 -4
- package/driver/knockout-review-record.mjs +56 -4
- package/driver/known-conflicts.mjs +1 -1
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +1 -1
- package/driver/ordinary-words.mjs +51 -0
- package/driver/outbox-backoff.mjs +31 -16
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +2 -2
- package/driver/phase0.mjs +3 -3
- package/driver/pipeline-knockout.mjs +5 -5
- package/driver/pipeline.mjs +396 -81
- package/driver/placement-form.mjs +77 -1
- package/driver/placement-model.mjs +1 -1
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +107 -6
- package/driver/portal-service.mjs +80 -14
- package/driver/portal-upstream.mjs +1 -1
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/preserve-merge.mjs +3 -3
- package/driver/product-rows.mjs +2 -2
- package/driver/products.mjs +1 -1
- package/driver/profiles/README.md +3 -3
- package/driver/profiles/demo-brand-owner.json +2 -2
- package/driver/profiles.mjs +55 -17
- package/driver/progress.mjs +18 -8
- package/driver/provider-usage.mjs +8 -8
- package/driver/publish/index.mjs +154 -8
- package/driver/publish/knockout.mjs +39 -5
- package/driver/publish/pool-admin.mjs +1 -1
- package/driver/publish/publish-inputs.mjs +18 -2
- package/driver/publish/render-knockout.mjs +184 -31
- package/driver/publish/render.mjs +323 -93
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/search-depth.mjs +133 -4
- package/driver/publish/templates/report.css +78 -4
- package/driver/publish/xlsx.mjs +20 -1
- package/driver/queue-order.mjs +2 -2
- package/driver/recording-agreement.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-count.mjs +50 -5
- package/driver/register-coverage.mjs +161 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-grant-vocabulary.mjs +1 -1
- package/driver/register-plan.mjs +189 -2
- package/driver/registry-fidelity.mjs +3 -3
- package/driver/repair-composers.mjs +1 -1
- package/driver/repair-contract.mjs +1 -1
- package/driver/replay-archive.mjs +6 -6
- package/driver/report-overview-record.mjs +2 -2
- package/driver/result-noun-fields.mjs +2 -2
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +5 -5
- package/driver/scope-facts.mjs +20 -5
- package/driver/scope-ledger.mjs +5 -5
- package/driver/search-policy.mjs +22 -12
- package/driver/skills/README.md +15 -15
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/case-law-citation/SKILL.md +4 -4
- package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
- package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
- package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
- package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
- package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
- package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
- package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
- package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
- package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
- package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
- package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
- package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
- package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
- package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
- package/driver/skills/frame-diff/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +10 -7
- package/driver/skills/matter-frame/SKILL.md +3 -3
- package/driver/skills/narrative-refutation/SKILL.md +9 -9
- package/driver/skills/placement-inquiry/SKILL.md +5 -5
- package/driver/stage-context.mjs +1 -1
- package/driver/stages-knockout.mjs +4 -4
- package/driver/stages.mjs +65 -61
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +340 -136
- package/driver/surface-exit-verdict.mjs +58 -0
- package/driver/systemd/README.md +9 -6
- package/driver/systemd/clearotron-worker.service +1 -1
- package/driver/terminal-clamp.mjs +109 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/usage-ledger.mjs +1 -1
- package/driver/variant-manifest-model.mjs +4 -4
- package/driver/verify-knockout.mjs +27 -0
- package/driver/verify.mjs +94 -6
- package/driver/whatif-queue.mjs +1 -1
- package/driver/wordlists/en.txt +63906 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/README.md +1 -1
- package/mcp-server/lib/options.mjs +8 -7
- package/mcp-server/lib/plan.mjs +18 -2
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/lib/usage.mjs +3 -3
- package/mcp-server/lib/whatif.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +18 -1
- package/package.json +12 -11
- package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/README.md +1 -1
- package/providers/_shared/enumerate.mjs +6 -6
- package/providers/_shared/execute-plan.mjs +3 -3
- package/providers/_shared/ledger.mjs +119 -5
- package/providers/_shared/provider-text.mjs +2 -2
- package/providers/_shared/screen.mjs +2 -2
- package/providers/_shared/script-form.mjs +3 -3
- package/providers/_shared/territory-codes.mjs +23 -3
- package/providers/clarivate/README.md +1 -1
- package/providers/clarivate/src/capabilities.js +12 -12
- package/providers/clarivate/src/core.js +37 -43
- package/providers/corsearch/README.md +1 -1
- package/providers/corsearch/src/capabilities.js +5 -5
- package/providers/corsearch/src/core.js +3 -3
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/src/core.js +1 -1
- package/providers/signa/README.md +1 -1
- package/providers/signa/src/capabilities.js +42 -49
- package/providers/signa/src/core.js +106 -29
- package/providers/uspto-local/README.md +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +1 -0
- package/scripts/ask-ai-render-check.mjs +127 -1
- package/scripts/authority-boundary-probe.mjs +8 -6
- package/scripts/backfill-started-at.mjs +2 -2
- package/scripts/census-merge-driver.mjs +33 -2
- package/scripts/citation-anchor-report.mjs +181 -0
- package/scripts/dead-names.mjs +1 -1
- package/scripts/deprecate-below.mjs +66 -8
- package/scripts/drain-preflight.mjs +1 -1
- package/scripts/e2e.mjs +174 -0
- package/scripts/env-audit.mjs +39 -6
- package/scripts/env-classify.mjs +20 -2
- package/scripts/freeze-example-run.mjs +61 -18
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/live-surface-check.mjs +124 -41
- package/scripts/markdown-link-check.mjs +1 -1
- package/scripts/merge-shape-check.mjs +242 -0
- package/scripts/mint-names-in-force.mjs +5 -5
- package/scripts/mint-offered-territories.mjs +72 -0
- package/scripts/mint-public-residue.mjs +2 -2
- package/scripts/mint-reference-strip-backlog.mjs +2 -2
- package/scripts/mint-suite-census.mjs +75 -2
- package/scripts/mint-writing-standard-backlog.mjs +2 -2
- package/scripts/purge-runs.mjs +7 -7
- package/scripts/reconcile-runs.mjs +2 -2
- package/scripts/release-approve-parked.mjs +20 -2
- package/scripts/release-await-cut.mjs +120 -1
- package/scripts/release-note-required.mjs +76 -8
- package/scripts/report-header-render-check.mjs +164 -0
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/brand.mjs +27 -0
- package/shared/connect-clients.mjs +39 -11
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/identifier-scan.mjs +65 -9
- package/shared/identifier-sentinels.mjs +22 -0
- package/shared/names-in-force.mjs +4 -2
- package/shared/offered-territories.json +738 -0
- package/shared/pre-rename-spellings.mjs +53 -0
- package/shared/reference-guard-classes.mjs +40 -2
- package/shared/stdio-connect.mjs +39 -4
- package/shared/tree-commit.mjs +48 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
|
@@ -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.
|
|
@@ -51,11 +51,15 @@ import { readFlagSnapshot, engineFor, providersFor, postureDisagreement } from "
|
|
|
51
51
|
// reading is, because the question it was standing in for — does this still describe the box — now has
|
|
52
52
|
// a direct answer in `lastRun.disagrees`.
|
|
53
53
|
import { engineMode } from "./config-inventory.mjs"; // — the mode is DERIVED at read time, never stored
|
|
54
|
+
import { BILLING_MODES } from "./engine/auth.mjs"; // — the three words a capture's billing mode may be shown as
|
|
54
55
|
// THE ENGINE TABLE, READ FOR TWO WORDS. A row saying an engine cannot run has to name the program it
|
|
55
56
|
// could not find and the command that installs it, or the reader is told they have a problem and not
|
|
56
57
|
// what to do about it — and this table is already where the wizard and the run-door preflight read
|
|
57
58
|
// both of those, so naming them here adds no second description of an engine.
|
|
58
59
|
import { ENGINE_BINARIES } from "./driver.config.mjs";
|
|
60
|
+
// WHICH SETUP COMMAND THIS READER CAN TYPE, as a word and never a path: the same answer `/me` sends the
|
|
61
|
+
// search screen, so the two pages name the same command for the same install.
|
|
62
|
+
import { installRoute } from "../shared/invocation.mjs";
|
|
59
63
|
|
|
60
64
|
/**
|
|
61
65
|
* The flag view.
|
|
@@ -88,6 +92,12 @@ function withProgram(engine) {
|
|
|
88
92
|
// layout and is deliberately kept out of anything a browser renders.
|
|
89
93
|
program: spec?.fallback ?? null,
|
|
90
94
|
install: spec?.install ?? null,
|
|
95
|
+
// The setting that names the program's full path, which is what an administrator sets when the
|
|
96
|
+
// services cannot find a program this machine has. A name, never its value.
|
|
97
|
+
programSetting: spec?.env ?? null,
|
|
98
|
+
// SETUP INSTALLS THE PROGRAM NOW, so a row saying it cannot be found names setup, the way this reader
|
|
99
|
+
// can run it: `packaged` or `checkout`.
|
|
100
|
+
setupRoute: installRoute(),
|
|
91
101
|
};
|
|
92
102
|
}
|
|
93
103
|
|
|
@@ -135,8 +145,27 @@ function postureView(snap) {
|
|
|
135
145
|
* The capture does not go away; it stops being the answer. It becomes "what the last run saw", and its
|
|
136
146
|
* job is to name any field on which it disagrees with the live reading.
|
|
137
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* A CAPTURE'S BILLING WORD, SHOWN ONLY WHEN IT IS ONE OF THE MODES.
|
|
150
|
+
*
|
|
151
|
+
* The inventory records a billing word that is not a mode as `unknown`, because whatever was typed into
|
|
152
|
+
* the setting that decides who pays can be a key pasted there by mistake. A capture written before it
|
|
153
|
+
* did holds the typed word itself, as its mode and inside its refusal's sentence, and this page would
|
|
154
|
+
* print it twice: in the last-run comparison, whose live side now reads `unknown`, and under a capture
|
|
155
|
+
* shown in place of a live reading. So such a capture is read as `unknown`, with no refusal sentence,
|
|
156
|
+
* which the page does not read anyway (it words the refusal from `reason`). Until the engine service
|
|
157
|
+
* restarts and writes its capture again, that is the only copy of the word the page could reach.
|
|
158
|
+
*/
|
|
159
|
+
function withKnownBillingWord(snap) {
|
|
160
|
+
const billing = snap?.engine?.billing;
|
|
161
|
+
if (!billing || typeof billing !== "object") return snap;
|
|
162
|
+
if (billing.mode == null || billing.mode === "unknown" || BILLING_MODES.includes(billing.mode)) return snap;
|
|
163
|
+
const { refusal: _typed, ...rest } = billing;
|
|
164
|
+
return { ...snap, engine: { ...snap.engine, billing: { ...rest, mode: "unknown" } } };
|
|
165
|
+
}
|
|
166
|
+
|
|
138
167
|
export function flagView(poolRoot, { live = null } = {}) {
|
|
139
|
-
const snap = readFlagSnapshot(poolRoot);
|
|
168
|
+
const snap = withKnownBillingWord(readFlagSnapshot(poolRoot));
|
|
140
169
|
|
|
141
170
|
// NO LIVE POSTURE IS A DIFFERENT PAGE, NOT A DEGRADED ONE. A caller that supplied none cannot be
|
|
142
171
|
// answered "live" at all, so this says which reading it is showing rather than presenting a capture
|
package/driver/portal-report.mjs
CHANGED
|
@@ -362,6 +362,12 @@ const MCP_HOST_RE = /https?:\/\/[A-Za-z0-9.-]+\/mcp/gi;
|
|
|
362
362
|
// introduces it and the full stop that closes it — leaving either behind puts a stray break or an orphan
|
|
363
363
|
// "." in the footer of every report.
|
|
364
364
|
const RATED_UNDER_RE = /<br\s*\/?>Rated under:\s*<span class="mono">[^<]*<\/span>\./gi;
|
|
365
|
+
// THE PROJECT LINE IS THE SAME CLASS AND WAS MISSING ITS STRIP. "Run under project: <name>" is
|
|
366
|
+
// internal provenance — an end-to-end arm reads it out of the published internal report — and it sat
|
|
367
|
+
// beside "Rated under" in the footer with nothing removing it here, so it reached every embedded
|
|
368
|
+
// reader exactly as written: the engine's phrase for the folder a job was filed in, and the folder's
|
|
369
|
+
// name. Same shape, same removal, one line apart.
|
|
370
|
+
const RUN_UNDER_PROJECT_RE = /<br\s*\/?>Run under project:\s*<span class="mono">[^<]*<\/span>\./gi;
|
|
365
371
|
|
|
366
372
|
const CONNECTOR_TOKEN_RE = /([?&])token=v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(&)?/g;
|
|
367
373
|
|
|
@@ -548,8 +554,24 @@ const EMBED_JS = `
|
|
|
548
554
|
parent.postMessage({source:TAG,type:'controls',commands:have},'*');
|
|
549
555
|
}catch(e){}
|
|
550
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
|
+
}
|
|
551
571
|
window.addEventListener('load',controls);
|
|
552
572
|
document.addEventListener('DOMContentLoaded',controls);
|
|
573
|
+
window.addEventListener('load',sections);
|
|
574
|
+
document.addEventListener('DOMContentLoaded',sections);
|
|
553
575
|
window.addEventListener('load',schedule);
|
|
554
576
|
document.addEventListener('DOMContentLoaded',schedule);
|
|
555
577
|
document.addEventListener('toggle',schedule,true);
|
|
@@ -564,6 +586,17 @@ const EMBED_JS = `
|
|
|
564
586
|
// A FAILED COMMAND SAYS SO. This used to be catch(err){} — a button that did nothing, told nobody,
|
|
565
587
|
// and left no console trace, which is how "Export does not work" went unnoticed through a whole
|
|
566
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
|
+
}
|
|
567
600
|
var fn = d.command==='exportPDF' ? (typeof exportPDF==='function'?exportPDF:null)
|
|
568
601
|
: d.command==='pickAll' ? (typeof pickAll==='function'?pickAll:null)
|
|
569
602
|
: d.command==='openAll' ? (typeof openAll==='function'?openAll:null)
|
|
@@ -589,7 +622,7 @@ const EMBED_JS = `
|
|
|
589
622
|
// Registered AFTER the document's own script by construction (this block is appended at the end of
|
|
590
623
|
// <body>), so by the time it runs for an '#c<ordinal>' link the ancestors are already open and the
|
|
591
624
|
// measured offset is the revealed position. preventDefault stops the frame's dead-end fragment
|
|
592
|
-
// 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).
|
|
593
626
|
// AND WHEN THERE IS NO PARENT, DO NOT CANCEL THE CLICK.
|
|
594
627
|
//
|
|
595
628
|
// Everything above is right INSIDE the portal's Result screen. Un-framed it is a dead link: parent is
|
|
@@ -624,6 +657,28 @@ const EMBED_JS = `
|
|
|
624
657
|
try{parent.postMessage({source:TAG,type:'scrollTo',top:top},'*');}catch(e){}
|
|
625
658
|
schedule();
|
|
626
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
|
+
});
|
|
627
682
|
schedule();
|
|
628
683
|
})();
|
|
629
684
|
`;
|
|
@@ -740,8 +795,42 @@ const FEEDBACK_JS = `
|
|
|
740
795
|
// Appended at the very end of <body>: the stylesheet is inlined into <head>, so a rule placed here wins
|
|
741
796
|
// the cascade on equal specificity without needing !important, and the script runs with the document's
|
|
742
797
|
// own functions already defined.
|
|
743
|
-
|
|
744
|
-
|
|
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>`
|
|
745
834
|
+ (feedback ? `<style data-embed="feedback">${FEEDBACK_CSS}</style><script data-embed="feedback">${FEEDBACK_JS}</script>` : "");
|
|
746
835
|
const i = html.lastIndexOf("</body>");
|
|
747
836
|
// No </body> means markup we do not recognise; appending is still correct and still parses.
|
|
@@ -781,6 +870,10 @@ export function prepareReportForEmbed(html, { staff = false, poolRoot = null, fe
|
|
|
781
870
|
const note = (tag) => unbalanced.push(tag);
|
|
782
871
|
const missingCss = [];
|
|
783
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
|
+
|
|
784
877
|
const nav = stripBalanced(html, NAV_RE, "nav", note);
|
|
785
878
|
let out = nav.html;
|
|
786
879
|
const strippedNav = nav.removed;
|
|
@@ -796,8 +889,16 @@ export function prepareReportForEmbed(html, { staff = false, poolRoot = null, fe
|
|
|
796
889
|
|
|
797
890
|
// The footer fingerprint. Counted like the rest so a renderer change that moves it shows up as a zero
|
|
798
891
|
// rather than as a line that quietly starts shipping again.
|
|
892
|
+
//
|
|
893
|
+
// ONE COUNTER PER LINE, AND THAT IS THE WHOLE POINT OF COUNTING. These two strips shared a counter, so
|
|
894
|
+
// the check that exists to catch a renderer moving this markup could not tell WHICH line moved: the
|
|
895
|
+
// renderer could rename one of them, that strip would match nothing, the other would still fire, and
|
|
896
|
+
// the count would read 1 exactly as it does when both work. A count that cannot fall to zero for one
|
|
897
|
+
// of the two things it counts is not a measurement of either.
|
|
799
898
|
let ratedUnderDropped = 0;
|
|
899
|
+
let runUnderProjectDropped = 0;
|
|
800
900
|
out = out.replace(RATED_UNDER_RE, () => { ratedUnderDropped += 1; return ""; });
|
|
901
|
+
out = out.replace(RUN_UNDER_PROJECT_RE, () => { runUnderProjectDropped += 1; return ""; });
|
|
801
902
|
|
|
802
903
|
// ── THE ASK-AI BAND COMES OUT FOR EVERY READER ──────────────────────────────────────────────────
|
|
803
904
|
//
|
|
@@ -851,11 +952,11 @@ export function prepareReportForEmbed(html, { staff = false, poolRoot = null, fe
|
|
|
851
952
|
|
|
852
953
|
// After the stylesheet, so the injected rule sits later in the cascade than the sheet it overrides —
|
|
853
954
|
// and after every strip, so nothing above can match the injected markup and eat it.
|
|
854
|
-
out = injectEmbedLayer(out, { feedback });
|
|
955
|
+
out = injectEmbedLayer(out, { feedback, sections });
|
|
855
956
|
|
|
856
957
|
return {
|
|
857
|
-
html: out, strippedNav, neutralised, mcpLeaks, tokensDropped, ratedUnderDropped,
|
|
858
|
-
internalTailsDropped, reviewerCodesDropped, unbalanced, missingCss,
|
|
958
|
+
html: out, strippedNav, neutralised, mcpLeaks, tokensDropped, ratedUnderDropped, runUnderProjectDropped,
|
|
959
|
+
internalTailsDropped, reviewerCodesDropped, unbalanced, missingCss, sections,
|
|
859
960
|
};
|
|
860
961
|
}
|
|
861
962
|
|
|
@@ -98,6 +98,24 @@ import { fileURLToPath } from "node:url";
|
|
|
98
98
|
// re-declared: one number, one count, and every existing importer of this module keeps working.
|
|
99
99
|
export { DEFAULT_CLIENT_DAILY_RUNS, accountUsage };
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* What a requester is told when the day's allowance is gone.
|
|
103
|
+
*
|
|
104
|
+
* ONE SENTENCE, TWO PLACES. The server refuses with it and the screen warns with it, and a reader who
|
|
105
|
+
* meets both must not meet two different sentences about the same fact. The browser cannot import this
|
|
106
|
+
* module, so the portal composes its own copy and `portal-ui/test/allowanceParity.test.ts` pins the two
|
|
107
|
+
* together — the same arrangement the effort model already has.
|
|
108
|
+
*
|
|
109
|
+
* NAMED, NOT ADDRESSED. It read "ask your <name> contact", which puts a possessive and a noun around a
|
|
110
|
+
* name that is already the answer: on a deployment that has configured no brand the portal's fallback is
|
|
111
|
+
* the words "the operator", and the sentence rendered "ask your the operator contact to run this one for
|
|
112
|
+
* you". The line beside it on the same screen had always said "ask <name>", and that is the one the
|
|
113
|
+
* approved design draws.
|
|
114
|
+
*/
|
|
115
|
+
export const allowanceExhaustedLine = (cap, who) =>
|
|
116
|
+
`You have used all ${cap} of this account's searches for today. `
|
|
117
|
+
+ `The allowance resets at midnight UTC — or ask ${who} to run this one for you.`;
|
|
118
|
+
|
|
101
119
|
// One sentence, two places (the capability payload and the route that refuses). Never names the
|
|
102
120
|
// missing credential: this reaches a client screen, and our plumbing is not their business.
|
|
103
121
|
//
|
|
@@ -111,7 +129,7 @@ export { DEFAULT_CLIENT_DAILY_RUNS, accountUsage };
|
|
|
111
129
|
const INSTALL_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
112
130
|
|
|
113
131
|
const READ_OFF_NOTE = "Reading a brief is not available on this instance — set the search up below.";
|
|
114
|
-
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";
|
|
115
133
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
116
134
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
117
135
|
import { makePrincipal, assertPrincipal, genericOrgOf, mayReadRun, reachCovers, principalView, seesEverything, mayRun,
|
|
@@ -130,7 +148,7 @@ import { productRows, productRow, baseTurnaroundFor } from "./product-rows.mjs";
|
|
|
130
148
|
import { resolveForDoor, gateResolvedRequest } from "./door-gates.mjs";
|
|
131
149
|
import { resolveEffectiveScope } from "./effective-scope.mjs";
|
|
132
150
|
import { quoteForJob } from "./run-quote.mjs";
|
|
133
|
-
import { readFlagSnapshot, builtFor, registerCanCountFor, registerTerritoriesFor, caseLawReadyFor } from "./flag-snapshot.mjs";
|
|
151
|
+
import { readFlagSnapshot, builtFor, registerCanCountFor, registerTerritoriesFor, registerLabelFor, caseLawReadyFor } from "./flag-snapshot.mjs";
|
|
134
152
|
import { isDemo, demoPostureLine } from "./demo-posture.mjs";
|
|
135
153
|
import { triggerCapGap, triggerCapWarning } from "./trigger-cap.mjs"; // F51 — one answer, three surfaces
|
|
136
154
|
import { makeUpstream } from "./portal-upstream.mjs";
|
|
@@ -468,7 +486,16 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
468
486
|
// zombie face this state exists to end. pausedKind "operator" tells the UI which words to use.
|
|
469
487
|
const paused = s.state === "postponed" || s.state === "recovering" || s.state === "parked-for-human";
|
|
470
488
|
out.push({ ...(liveRetired ? { retired: true } : {}),
|
|
471
|
-
runId: s.runId, account: owner, ...(owner === "generic" ? { organisation: p.organisation ?? null } : {}), title: s.markName ?? s.slug,
|
|
489
|
+
runId: s.runId, account: owner, ...(owner === "generic" ? { organisation: p.organisation ?? null } : {}), title: s.markName ?? s.slug,
|
|
490
|
+
// THE RUN’S OWN PIPELINE, off the frozen policy sidecar this branch already reads — the same
|
|
491
|
+
// source the `product` field two lines down takes, so a row cannot contradict its own product.
|
|
492
|
+
// `s.lane` is the runner’s queue directory and it is not that: a knockout claimed from any other
|
|
493
|
+
// lane arrived here as a clearance, and Home then quoted it the clearance turnaround — hours,
|
|
494
|
+
// against a search that finishes in minutes. The queued rows below were fixed for exactly this
|
|
495
|
+
// and this row was left on the weaker field. The lane stays as the fallback for a run whose
|
|
496
|
+
// sidecar predates the stamp, and an unplaceable product stays a clearance, which is what every
|
|
497
|
+
// listing has always shown for something it could not identify.
|
|
498
|
+
kind: (policyFor(sp?.level)?.pipeline ?? (s.lane === "knockout" ? "knockout" : null)) === "knockout" ? "knockout-batch" : "clearance",
|
|
472
499
|
markName: typeof s.markName === "string" ? s.markName : null,
|
|
473
500
|
// The project, straight off the frozen sidecar this branch already reads as `p`. A LIVE run needs
|
|
474
501
|
// no publish stamp and no back-fill — the sidecar is right there, and freezeProfile has written
|
|
@@ -650,7 +677,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
650
677
|
};
|
|
651
678
|
// THE CANONICAL INTAKE FIRST, and it is why this parameter exists. `CLEAROTRON_QUEUE_DIR` is where the
|
|
652
679
|
// enqueue CLI and ops-MCP `start_run` write — which is where the PORTAL's own submissions land, since
|
|
653
|
-
// 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`,
|
|
654
681
|
// and a documented headless install has no workspaces at all: measured on the test box, the only queue
|
|
655
682
|
// under the whole tree is the configured one, and it holds portal-prefixed jobs. So this scan ran ZERO
|
|
656
683
|
// times there, and a submitted search was invisible on the dashboard from submit until claim — a
|
|
@@ -660,7 +687,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
660
687
|
}
|
|
661
688
|
try {
|
|
662
689
|
for (const ws of readdirSync(workspaceRoot).filter((n) => n.startsWith("workspace-"))) {
|
|
663
|
-
const studio = join(workspaceRoot, ws
|
|
690
|
+
const studio = studioDirFor(join(workspaceRoot, ws));
|
|
664
691
|
let slugs = []; try { slugs = readdirSync(studio); } catch { continue; }
|
|
665
692
|
for (const slug of slugs) {
|
|
666
693
|
// Still walked, so a deployment whose queue is not in `queueDirs` keeps working. The union is
|
|
@@ -684,7 +711,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
684
711
|
// A failure `reason` is NOT client-safe, and the listing must not hand it to a browser.
|
|
685
712
|
//
|
|
686
713
|
// The engine writes `reason = String(e?.stack ?? e)` truncated to 200 characters
|
|
687
|
-
// (pipeline.mjs
|
|
714
|
+
// (pipeline.mjs, pipeline-knockout.mjs). That is a raw stack trace: absolute filesystem
|
|
688
715
|
// paths, internal module names, provider error text. Useful to staff, and none of a client's business
|
|
689
716
|
// — and redacting it in the UI would be theatre, because the field still travels over the wire and
|
|
690
717
|
// sits in devtools whatever React chooses to render.
|
|
@@ -1230,6 +1257,10 @@ export function makePortalService({
|
|
|
1230
1257
|
// THREE answers survive the read: null (unrestricted), an array, or undefined (the snapshot does not
|
|
1231
1258
|
// say — fail open). registerTerritoriesFor is what keeps them apart.
|
|
1232
1259
|
readTerritories = () => registerTerritoriesFor(readFlagSnapshot(poolRoot)),
|
|
1260
|
+
// What to CALL the wired register on screen. Beside `readTerritories` and injected for the same
|
|
1261
|
+
// reason: the covered set says a territory cannot be searched, and this says by what — the screen
|
|
1262
|
+
// cannot name the register from the key without printing an engine identifier at a client.
|
|
1263
|
+
readRegisterLabel = () => registerLabelFor(readFlagSnapshot(poolRoot)),
|
|
1233
1264
|
// — whether this deployment has a case-law source enrolled at all. Same
|
|
1234
1265
|
// injection and same snapshot as the two above, and the same tri-state: null is "the snapshot does not
|
|
1235
1266
|
// say", which must not render as a warning on a working box.
|
|
@@ -1280,6 +1311,7 @@ export function makePortalService({
|
|
|
1280
1311
|
const built = readBuilt();
|
|
1281
1312
|
const canCount = readCanCount();
|
|
1282
1313
|
const territories = readTerritories();
|
|
1314
|
+
const registerLabel = readRegisterLabel();
|
|
1283
1315
|
const caseLawReady = readCaseLaw();
|
|
1284
1316
|
return {
|
|
1285
1317
|
// Every product is LISTED whatever its state. A product that vanishes when unavailable leaves a
|
|
@@ -1300,7 +1332,24 @@ export function makePortalService({
|
|
|
1300
1332
|
// explains a control that cannot be used, `coverageNote` qualifies one that can. Folding this
|
|
1301
1333
|
// into the first would make every caller of `productAvailability` — the portal, the MCP door,
|
|
1302
1334
|
// the dev cockpit — read a disclosure as a refusal, which is the behaviour the ruling removes.
|
|
1303
|
-
|
|
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.
|
|
1304
1353
|
// ── — THE PRODUCT DECLARES WHAT IT NEEDS, so the row can say so ─────
|
|
1305
1354
|
//
|
|
1306
1355
|
// The owner ordered the one product carrying `caseLaw: true` and first heard of the lane in the
|
|
@@ -1317,7 +1366,7 @@ export function makePortalService({
|
|
|
1317
1366
|
// a warning on every deployment whose writer has not run since is worse than the silence it
|
|
1318
1367
|
// replaces.
|
|
1319
1368
|
return { ...l, available: cause === null, unavailableNote: cause ? UNAVAILABLE_NOTE[cause] : null,
|
|
1320
|
-
coverageNote:
|
|
1369
|
+
coverageNote: null,
|
|
1321
1370
|
capabilityNote: l.caseLaw && caseLawReady === false ? CASE_LAW_DARK_NOTE : null };
|
|
1322
1371
|
}),
|
|
1323
1372
|
// ── the TERRITORY affordance ─────────────────────────────────────────────────────────────
|
|
@@ -1335,6 +1384,16 @@ export function makePortalService({
|
|
|
1335
1384
|
// not told me", which fails open. A `covered ?? []` anywhere on this path offers zero territories
|
|
1336
1385
|
// on a production box.
|
|
1337
1386
|
...(territories === undefined ? {} : { territories }),
|
|
1387
|
+
// THE REGISTER'S NAME, sent whenever there is one. The covered set alone lets the screen mark a
|
|
1388
|
+
// territory as unreachable; it cannot say by WHAT, and the sentence the door refuses with names
|
|
1389
|
+
// the register ("China is not available with Signa, the register configured here"). A screen that
|
|
1390
|
+
// marked a chip without naming the register would be a second, vaguer wording for one fact.
|
|
1391
|
+
//
|
|
1392
|
+
// OMITTED, never nulled, exactly as `territories` above: absent means this deployment did not say,
|
|
1393
|
+
// and the screen must then mark the territory without naming a register rather than invent one.
|
|
1394
|
+
// It is the register's own display label, never the provider key — the key is an engine
|
|
1395
|
+
// identifier and has no place on a client's screen.
|
|
1396
|
+
...(registerLabel ? { registerLabel } : {}),
|
|
1338
1397
|
// `base` and `nativeLanguage` ride the LIST row, not just the record: the composer has to say what
|
|
1339
1398
|
// geography a saved search accepts while the row is being clicked, and fetching the record per
|
|
1340
1399
|
// selection would put that answer one round trip behind the Review button. Only `true` travels for
|
|
@@ -1700,7 +1759,7 @@ export function makePortalService({
|
|
|
1700
1759
|
// The recipeKey arm that used to sit here (a 422 when saved searches were "not switched on") went
|
|
1701
1760
|
// with CLEAROTRON_RECIPES_MODE on 2026-07-27: a saved search is now honoured wherever it resolves.
|
|
1702
1761
|
// Asked of the RESOLVED product, not of the body: a request that names none resolves through
|
|
1703
|
-
// 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"`)
|
|
1704
1763
|
// answered about a product nobody had chosen. `resolveFor` fails open to a null resolution, and a
|
|
1705
1764
|
// null one is not judged here — validateJob and the scope rules below still run, and the runner
|
|
1706
1765
|
// is the wall.
|
|
@@ -1842,9 +1901,7 @@ export function makePortalService({
|
|
|
1842
1901
|
return null;
|
|
1843
1902
|
}
|
|
1844
1903
|
if (used.today + 1 <= limit) return null;
|
|
1845
|
-
return { status: 429, json: { ok: false, errors: [
|
|
1846
|
-
`You have used all ${limit} of this account's searches for today. The allowance resets at midnight UTC — or ask your ${BRAND.name} contact to run this one for you.`,
|
|
1847
|
-
] } };
|
|
1904
|
+
return { status: 429, json: { ok: false, errors: [allowanceExhaustedLine(limit, BRAND.name)] } };
|
|
1848
1905
|
};
|
|
1849
1906
|
|
|
1850
1907
|
// /portal/api/run/plan — the confirmation gate (no spend)
|
|
@@ -2463,8 +2520,14 @@ async function connectorDoorKind(url) {
|
|
|
2463
2520
|
//
|
|
2464
2521
|
// COMPOSED IN ONE PLACE and handed over as a string. The browser cannot know this install's
|
|
2465
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.
|
|
2466
2529
|
const stdio = seesEverything(principal)
|
|
2467
|
-
? 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() })
|
|
2468
2531
|
: null;
|
|
2469
2532
|
|
|
2470
2533
|
// ── THE PAGE IS HANDED ANSWERS, NOT FACTS TO REASON FROM ─────────────
|
|
@@ -3011,7 +3074,10 @@ async function connectorDoorKind(url) {
|
|
|
3011
3074
|
// who signed in. Dynamic import deliberately: `profiles.mjs` captures the store directory at
|
|
3012
3075
|
// MODULE LOAD (see this file's note above), so it is never pulled in at our own load time.
|
|
3013
3076
|
const { companyFactsOf } = await import("./profiles.mjs");
|
|
3014
|
-
|
|
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 } : {}) } };
|
|
3015
3081
|
}
|
|
3016
3082
|
// /portal/admin/config — what this deployment actually has switched on. Read from the SNAPSHOT,
|
|
3017
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.
|
|
@@ -32,7 +32,7 @@ import { writeUpViolations, writeUpMessage } from "./narrative-write-ups.mjs";
|
|
|
32
32
|
import { findRegistryArithmeticIssues, findRegistryViolations, splitBlocks } from "./registry-fidelity.mjs";
|
|
33
33
|
import { CLIENT_TIER_BY_COMPOSITE, joinFindingToBlock, parseBlockOrd, worstLiveBand, NO_RATED_CONFLICTS, deriveActionConditions, isUnconditionalProceed, verdictStance, joinAskToAnswer, projectAssessmentField, POSITION_REQUIRED_DISPOSITIONS, OFF_FIELD_GROUNDS, FINDINGS_SCHEMA_VERSION, netChainMarkers, STATEMENT_CLAUSE_MAX } from "./findings-model.mjs";
|
|
34
34
|
import { normalizeBand } from "./framework.mjs";
|
|
35
|
-
import { clientConditions, ENGINE_TOKEN_RE } from "./terminal-clamp.mjs"; // the reader's clause per condition, and the token shape it may never carry
|
|
35
|
+
import { clientConditions, unrenderableConditions, ENGINE_TOKEN_RE } from "./terminal-clamp.mjs"; // the reader's clause per condition, and the token shape it may never carry
|
|
36
36
|
import { knockoutNoteView, REQUEST_NOTE_WORDS, REQUEST_SUBJECT_WORDS } from "./findings-model.mjs"; // one reader for where a note prints
|
|
37
37
|
import { isEngineAppendedCaveat } from "./verify-knockout.mjs"; // ONE derivation for "the engine appended this caveat, not a seat"
|
|
38
38
|
|
|
@@ -1976,10 +1976,20 @@ export function clientConditionVoiceChecks({ verdictDoc }) {
|
|
|
1976
1976
|
const carrying = clientConditions(verdictDoc)
|
|
1977
1977
|
.map((c) => ({ c, m: String(c).match(ENGINE_TOKEN_RE) }))
|
|
1978
1978
|
.filter((x) => x.m);
|
|
1979
|
+
// THE SECOND CHECK IS WHAT KEEPS THE FIRST ONE HONEST. The fallback now composes the reader's
|
|
1980
|
+
// sentence from a pre-split reason's own counts, and DROPS a token-bearing reason it cannot compose
|
|
1981
|
+
// one for. Both outcomes leave the first check passing, and they mean opposite things: one is the
|
|
1982
|
+
// condition reaching the client in a lawyer's nouns, the other is the condition reaching nobody. A
|
|
1983
|
+
// single check reporting a clean page for both would be an absence read as a pass.
|
|
1984
|
+
const dropped = unrenderableConditions(verdictDoc);
|
|
1979
1985
|
return [check("client-condition-voice", "verdict", "report", carrying.length === 0,
|
|
1980
1986
|
carrying.length
|
|
1981
1987
|
? `${carrying.length} delivered condition${carrying.length === 1 ? "" : "s"} carr${carrying.length === 1 ? "ies" : "y"} an engine identifier, so the client's "conditional on:" list reads the run record rather than the reader's sentence: ${carrying.slice(0, 3).map((x) => `"${x.m[0]}"`).join(", ")}${carrying.length > 3 ? ` (+${carrying.length - 3} more)` : ""}. The clause exists at the clamp site; persist it and this list takes it. A sidecar written before clauses were persisted flags until the run is re-generated.`
|
|
1982
|
-
: "")
|
|
1988
|
+
: ""),
|
|
1989
|
+
check("client-condition-dropped", "verdict", "report", dropped.length === 0,
|
|
1990
|
+
dropped.length
|
|
1991
|
+
? `${dropped.length} condition${dropped.length === 1 ? " was" : "s were"} recorded for this run and reach no client surface: the run-record reason carries an engine identifier and no reader's sentence was stored or could be composed from it, so it is dropped rather than printed in engine voice. The report ships without it and the run record keeps it: ${dropped.slice(0, 2).map((r) => `"${r.slice(0, 80)}…"`).join(", ")}${dropped.length > 2 ? ` (+${dropped.length - 2} more)` : ""}. Add the defect's sentence to the clause authority in terminal-clamp.mjs, or persist a clause at the clamp site.`
|
|
1992
|
+
: "")];
|
|
1983
1993
|
}
|
|
1984
1994
|
|
|
1985
1995
|
export function statementCoherenceChecks({ verdictDoc }) {
|
|
@@ -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);
|