clearotron 0.2.4 → 0.3.0-beta.1
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 +13 -2
- package/CONTRIBUTING.md +1 -1
- package/INSTALL.md +62 -38
- package/bin/brandowner.mjs +18 -169
- package/bin/clearotron.mjs +3 -1
- package/bin/connect.mjs +28 -19
- package/bin/disconnect.mjs +3 -3
- package/bin/example.mjs +7 -7
- package/bin/framework-preflight.mjs +49 -0
- package/bin/grant.mjs +151 -93
- package/bin/onboard.mjs +220 -138
- package/bin/start.mjs +146 -137
- package/bin/stop.mjs +2 -2
- package/bin/update.mjs +1 -1
- package/build-info.json +2 -2
- package/docs/CLIENT-MCP.md +2 -2
- package/docs/E2E.md +12 -2
- package/docs/ONBOARDING.md +1 -1
- package/docs/PORTAL.md +14 -13
- package/docs/SECURITY.md +23 -24
- package/docs/architecture/04-configuration-reference.md +12 -5
- package/docs/architecture/05-config-governance.md +7 -7
- package/docs/architecture/07-quality-and-audit.md +1 -1
- package/docs/architecture/08-development-guide.md +5 -0
- package/docs/configuration.md +118 -0
- package/docs/decisions/0004-documentation-structure.md +2 -2
- package/docs/decisions/0006-what-the-public-repository-carries.md +2 -2
- package/driver/CHANGELOG.md +70 -0
- package/driver/ask-ledger.mjs +2 -2
- package/driver/cancel.mjs +27 -0
- package/driver/case-law-sources.mjs +3 -3
- package/driver/company-bundle.mjs +261 -0
- package/driver/compare.mjs +1 -1
- package/driver/compose-read.mjs +2 -2
- package/driver/config-inventory.mjs +2 -2
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/contract-vocabulary.mjs +1 -1
- package/driver/declination-call.mjs +1 -1
- package/driver/deliver-trigger.sh +3 -3
- package/driver/dev-portal.mjs +3 -1
- package/driver/digest-queue.mjs +1 -1
- package/driver/disposition-tool.mjs +1 -1
- package/driver/doc-constants.mjs +1 -1
- package/driver/drain-posture.mjs +2 -2
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +44 -9
- package/driver/effective-scope.mjs +30 -1
- package/driver/effort-model.mjs +6 -6
- package/driver/engine/CONTRACT.md +2 -2
- package/driver/engine/anthropic-agent.mjs +11 -11
- package/driver/engine/jx-turn.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +29 -4
- package/driver/engine/mcp/recording-server.mjs +73 -1
- package/driver/engine/openai-agent.mjs +1 -1
- package/driver/engine/probe.mjs +28 -4
- package/driver/enqueue-schema.mjs +23 -3
- package/driver/findings-model.mjs +2 -2
- package/driver/flag-snapshot.mjs +2 -2
- package/driver/floor-duty.mjs +2 -2
- package/driver/frame-diff-model.mjs +1 -1
- package/driver/framework-preflight.mjs +143 -0
- package/driver/gateway.mjs +9 -1
- package/driver/hit-list.mjs +1 -1
- package/driver/jx-lanes.mjs +1 -1
- package/driver/jx.mjs +1 -1
- package/driver/knockout-assess-record.mjs +1 -1
- package/driver/knockout-review-record.mjs +435 -0
- package/driver/order-probe.mjs +1 -1
- package/driver/outbox-backoff.mjs +2 -2
- package/driver/owner-use-check.mjs +2 -2
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +105 -8
- package/driver/pipeline.mjs +81 -30
- package/driver/plain-register.mjs +77 -3
- package/driver/portal-access.mjs +141 -74
- package/driver/portal-config-view.mjs +59 -70
- package/driver/portal-report.mjs +4 -4
- package/driver/portal-service.mjs +348 -141
- package/driver/portal-upstream.mjs +105 -17
- package/driver/predelivery-lint.mjs +43 -18
- package/driver/product-rows.mjs +1 -1
- package/driver/products.mjs +1 -1
- package/driver/profile-page.html +30 -5
- package/driver/profile-service.mjs +197 -26
- package/driver/profiles.mjs +48 -1
- package/driver/publish/index.mjs +31 -13
- package/driver/publish/knockout.mjs +9 -5
- package/driver/publish/office-record-links.mjs +189 -0
- package/driver/publish/parse.mjs +3 -3
- package/driver/publish/publish-inputs.mjs +26 -0
- package/driver/publish/render-knockout.mjs +42 -42
- package/driver/publish/render.mjs +29 -4
- package/driver/publish/report-data.mjs +2 -2
- package/driver/publish/seed-pool.mjs +1 -1
- package/driver/publish/templates/report.css +8 -8
- package/driver/publish/xlsx.mjs +49 -7
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/recipe-service.mjs +1 -1
- package/driver/record-carry.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/reference-strip-signatures.mjs +1 -1
- package/driver/register-availability.mjs +4 -3
- package/driver/register-count.mjs +3 -3
- package/driver/register-records.mjs +1 -1
- package/driver/repair-composers.mjs +1 -1
- package/driver/repairs.mjs +3 -3
- package/driver/replay-archive.mjs +1 -1
- package/driver/report-card-record.mjs +1 -1
- package/driver/result-noun-fields.mjs +5 -0
- package/driver/roster-verdict.mjs +48 -5
- package/driver/run-activity.mjs +1 -1
- package/driver/run-requirements.mjs +18 -5
- package/driver/runner.mjs +24 -15
- package/driver/search-policy.mjs +8 -8
- package/driver/senior-rights.mjs +1 -1
- package/driver/skills/prelim-search/delivery-contract.md +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.md +10 -7
- package/driver/stages-knockout.mjs +72 -6
- package/driver/stages.mjs +10 -10
- package/driver/suite-census.json +238 -70
- package/driver/synthesis-record.mjs +2 -2
- package/driver/systemd/clearotron-client-mcp.service +3 -3
- package/driver/systemd/clearotron-deploy.service +2 -2
- package/driver/systemd/clearotron-mcp-face.service +1 -1
- package/driver/systemd/clearotron-portal.service +3 -3
- package/driver/systemd/clearotron-worker.service +5 -5
- package/driver/systemd/install-census.mjs +1 -1
- package/driver/systemd/render-units.mjs +9 -9
- package/driver/terminal-clamp.mjs +1 -1
- package/driver/trigger-cap.mjs +18 -2
- package/driver/unit-inventory.mjs +8 -8
- package/driver/usage-ledger.mjs +5 -3
- package/driver/verify-knockout.mjs +7 -7
- package/driver/verify.mjs +5 -5
- package/driver/whatif-memo-run.mjs +1 -1
- package/driver/whatif-queue.mjs +3 -3
- package/driver/whatif-worker.mjs +2 -2
- package/examples/README.md +1 -1
- package/examples/grants.example.json +25 -24
- package/mcp-server/CHANGELOG.md +10 -0
- package/mcp-server/http-server.mjs +3 -3
- package/mcp-server/key-socket.mjs +1 -1
- package/mcp-server/lib/audit-view.mjs +3 -3
- package/mcp-server/lib/brief.mjs +3 -3
- package/mcp-server/lib/driver.mjs +1 -1
- package/mcp-server/lib/events.mjs +1 -1
- package/mcp-server/lib/http-handler.mjs +2 -2
- package/mcp-server/lib/instructions.mjs +2 -2
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/lib/ops.mjs +6 -3
- package/mcp-server/lib/options.mjs +15 -4
- package/mcp-server/lib/plan.mjs +7 -6
- package/mcp-server/lib/runs.mjs +10 -0
- package/mcp-server/lib/whatif.mjs +5 -5
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/README.md +1 -1
- package/mcp-server/remote/client-mcp-apikey.service +1 -1
- package/mcp-server/remote/client-mcp.service +2 -2
- package/mcp-server/remote/trademark-artifacts-http.service +1 -1
- package/mcp-server/server.mjs +38 -24
- package/package.json +2 -2
- package/portal-ui/dist/assets/{index-KFAHMgdT.js → index-CWTHP0sH.js} +3471 -1901
- package/portal-ui/dist/assets/{index-1ziUJX1E.css → index-KpytsmNH.css} +79 -26
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/lane-probe.mjs +9 -3
- package/providers/jx-subclass/lookup.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
- package/scripts/citation-drift-report.mjs +1 -1
- package/scripts/citation-line-check.mjs +2 -2
- package/scripts/drive-env-check.mjs +1 -1
- package/scripts/e2e.mjs +5 -5
- package/scripts/env-audit.mjs +13 -1
- package/scripts/headless-page.mjs +5 -5
- package/scripts/live-surface-check.mjs +26 -2
- package/scripts/mint-names-in-force.mjs +19 -5
- package/scripts/mint-reference-strip-backlog.mjs +1 -1
- package/scripts/mint-suite-census.mjs +37 -10
- package/scripts/pack-publishable.mjs +1 -1
- package/scripts/preinstall-node-check.mjs +1 -1
- package/scripts/release-await-cut.mjs +3 -3
- package/scripts/release-cut-decision.mjs +1 -1
- package/scripts/release-dist-tag.mjs +1 -1
- package/scripts/release-install-check.mjs +1 -1
- package/scripts/release-notes-lint.mjs +1 -1
- package/scripts/release-publish-guard.mjs +1 -1
- package/scripts/release-version-pr-checks.mjs +2 -2
- package/scripts/release-version.mjs +61 -5
- package/scripts/render-brand-banner.mjs +1 -1
- package/scripts/render-check.mjs +2 -2
- package/scripts/repo-writes.mjs +1 -1
- package/scripts/report-frame-check.mjs +1 -1
- package/scripts/report-screenshot.mjs +2 -2
- package/scripts/retire-bare-refs.mjs +1 -1
- package/scripts/revisit-render-check.mjs +1 -1
- package/scripts/score.mjs +1 -1
- package/scripts/strip-titles-and-attributions.mjs +389 -0
- package/scripts/strip-tracker-citations.mjs +122 -5
- package/scripts/test-run.mjs +4 -4
- package/scripts/third-party-notices.mjs +1 -1
- package/scripts/verify-publishable.mjs +1 -1
- package/shared/access-audience.mjs +2 -2
- package/shared/anon-overlay.mjs +1 -1
- package/shared/brand.mjs +15 -1
- package/shared/bundle-freshness.mjs +1 -1
- package/shared/bundle-rebuild.mjs +1 -1
- package/shared/checkout-move.mjs +2 -2
- package/shared/client-door.mjs +8 -8
- package/shared/connect-clients.mjs +7 -7
- package/shared/connector-signin-probe.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/env-local.mjs +5 -5
- package/shared/grants-edit.mjs +76 -0
- package/shared/install-auth.mjs +1 -1
- package/shared/listen.mjs +3 -3
- package/shared/mcp-challenge.mjs +1 -1
- package/shared/names-in-force.mjs +2 -1
- package/shared/onboarding-store.mjs +19 -2
- package/shared/reference-guard-classes.mjs +44 -2
- package/shared/register-selection.mjs +1 -1
- package/shared/scope.mjs +223 -56
- package/shared/secret-file.mjs +1 -1
- package/shared/server-units.mjs +1 -1
- package/shared/staff-domain.mjs +45 -78
- package/shared/summary-blocks.mjs +2 -2
- package/shared/systemd-failure.mjs +3 -3
- package/shared/tracked-files.mjs +1 -1
- package/shared/trigger-lane.mjs +1 -1
- package/shared/tty-style.mjs +1 -1
- package/shared/usage-block.mjs +1 -1
- package/shared/vacuous-pass.mjs +1 -1
- package/shared/verb-shim.mjs +1 -1
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
//
|
|
58
58
|
// ── AND WHEN EACH ONE IS ASKED FOR, WHICH IS A SECOND AXIS AND NOT THE SAME ONE ─────────────────────
|
|
59
59
|
//
|
|
60
|
-
//
|
|
60
|
+
// Ruling 2026-09-06, in session: "someone can install and select key later so it should still
|
|
61
61
|
// start." So a hosted install comes up with no register configured — the doors answer, the portal
|
|
62
62
|
// answers, and the box is a working install waiting for one value.
|
|
63
63
|
//
|
|
@@ -186,13 +186,26 @@ export function missingRequirements(env = {}, tables = {}) {
|
|
|
186
186
|
*
|
|
187
187
|
* Returns null when nothing blocks, so a caller cannot mistake "configured" for "could not look".
|
|
188
188
|
*/
|
|
189
|
-
export function orderTimeRefusal(env = {}, tables = {}, { envFile = null } = {}) {
|
|
189
|
+
export function orderTimeRefusal(env = {}, tables = {}, { envFile = null, readFile = null } = {}) {
|
|
190
190
|
const missing = missingRequirements(env, tables).atOrder;
|
|
191
191
|
if (!missing.length) return null;
|
|
192
192
|
const names = missing.map((r) => r.name);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
193
|
+
// EVERY FILE THAT REACHES THIS RUN, EACH NAMED BY WHAT READS IT — the rule `bin/start.mjs` already
|
|
194
|
+
// follows, for the reason it gives: two copies of this sentence is how one of them comes to name a file
|
|
195
|
+
// the reader cannot use. This copy named only `envFile`, the file background units read. A runner
|
|
196
|
+
// started from a terminal on a box with no units reads the install's own file instead, so its operator
|
|
197
|
+
// was told to create a file nothing on that box reads — and the one actually read went unnamed.
|
|
198
|
+
// `readFile` is the file THIS process loaded at start; it is null under a unit, where the unit's own
|
|
199
|
+
// EnvironmentFile is the only configuration and `envFile` alone is the true answer.
|
|
200
|
+
const cmd = "`clearotron install` in a terminal, which writes them for you";
|
|
201
|
+
const both = readFile && envFile && readFile !== envFile;
|
|
202
|
+
const one = readFile || envFile;
|
|
203
|
+
const where = both
|
|
204
|
+
? ` Set them in either of these — both reach a run:\n ${readFile} — the file this runner read when it started\n`
|
|
205
|
+
+ ` ${envFile} — the file background units read\n then restart, or run ${cmd}.`
|
|
206
|
+
: one
|
|
207
|
+
? ` Set them in ${one} and restart, or run ${cmd}.`
|
|
208
|
+
: " Run `clearotron install` in a terminal to configure them, or set them in the file this install's units read.";
|
|
196
209
|
return {
|
|
197
210
|
names,
|
|
198
211
|
operator: `this installation cannot run a search yet — it is installed but not configured:\n`
|
package/driver/runner.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import { driverDir, ensureDriverDir } from "../shared/driver-dir.mjs"; // —
|
|
|
29
29
|
import { isLiveQueueMarker, PROSE_PARTS, CLAIM_SIDECAR_SUFFIXES, TERMINAL_QUEUE_SUFFIXES } from "./queue-markers.mjs";
|
|
30
30
|
import { matterLedgerPath } from "./usage-ledger.mjs"; // ONE ledger-path calculation, shared with the portal pre-check
|
|
31
31
|
import { orderTimeRefusal } from "./run-requirements.mjs"; // one authority for what a run needs, and when it is asked for
|
|
32
|
-
import { unitEnvPath } from "../shared/env-local.mjs"; // the file the units read, named by its one author
|
|
32
|
+
import { unitEnvPath, envFileRead } from "../shared/env-local.mjs"; // the file the units read, named by its one author
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
34
|
import { config, preflightDeploymentUrls } from "./driver.config.mjs";
|
|
35
35
|
import { deriveSlug, todayISO, mintFreshCodename } from "./phase0.mjs";
|
|
@@ -237,10 +237,15 @@ function recordMatter(qdir, entry) {
|
|
|
237
237
|
// any account that needs a different number says so in its own runCaps block.
|
|
238
238
|
export const DEFAULT_CLIENT_DAILY_RUNS = 20;
|
|
239
239
|
|
|
240
|
-
export function checkRunCaps({ account, caps, queueDirs, inHandTagged = true, now = Date.now(), clientRun = false }) {
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
|
|
240
|
+
export function checkRunCaps({ account, caps, queueDirs, inHandTagged = true, now = Date.now(), clientRun = false, organisation = null }) {
|
|
241
|
+
// GENERIC IS CAPPED LIKE ANY COMPANY, one lane per organisation (ruling 2026-09-10: "every
|
|
242
|
+
// organisation's Generic lane carries the daily cap; the exemption goes"). A Generic run counts in the
|
|
243
|
+
// lane of the organisation it was filed under and nowhere else, so one organisation's day cannot use up
|
|
244
|
+
// another's. What stays uncapped is what was never client-attributed — a job with no `clientRun` stamp:
|
|
245
|
+
// a person with access to everything, the email door, the CLI — exactly as for a company.
|
|
246
|
+
if (!account) return null;
|
|
247
|
+
const inLane = (key, org) => (key ?? null) === account && (account !== "generic" || (org ?? null) === organisation);
|
|
248
|
+
const lane = account === "generic" ? (organisation ? `Generic for ${organisation}` : "Generic") : account;
|
|
244
249
|
// The default applies to the DAILY allowance only. maxQueued/monthlyRuns stay opt-in per profile.
|
|
245
250
|
const eff = Number.isInteger(caps?.dailyRuns) ? caps : { ...(caps ?? {}), dailyRuns: DEFAULT_CLIENT_DAILY_RUNS };
|
|
246
251
|
caps = eff;
|
|
@@ -252,7 +257,7 @@ export function checkRunCaps({ account, caps, queueDirs, inHandTagged = true, no
|
|
|
252
257
|
let files = [];
|
|
253
258
|
try { files = readdirSync(qd).filter(isLiveQueueMarker); } catch { continue; }
|
|
254
259
|
for (const f of files) {
|
|
255
|
-
try {
|
|
260
|
+
try { const j = JSON.parse(readFileSync(join(qd, f), "utf8")); if (inLane(j.profileKey, j.tenant)) queued++; }
|
|
256
261
|
catch { /* mid-rename / prose sidecar — skip */ }
|
|
257
262
|
}
|
|
258
263
|
}
|
|
@@ -261,7 +266,7 @@ export function checkRunCaps({ account, caps, queueDirs, inHandTagged = true, no
|
|
|
261
266
|
// the uniform +1 admitted untagged jobs one over the cap).
|
|
262
267
|
const inFlight = queued + (inHandTagged ? 0 : 1);
|
|
263
268
|
if (inFlight > caps.maxQueued) {
|
|
264
|
-
return `admission cap: ${
|
|
269
|
+
return `admission cap: ${lane} already has ${inFlight - 1} run(s) queued/in-flight (runCaps.maxQueued=${caps.maxQueued}) — wait for the backlog to drain or cancel one (stop_run), then re-send`;
|
|
265
270
|
}
|
|
266
271
|
}
|
|
267
272
|
// dailyRuns — the beta allowance, and the ONLY cap that counts a subset of runs rather than all of
|
|
@@ -292,23 +297,23 @@ export function checkRunCaps({ account, caps, queueDirs, inHandTagged = true, no
|
|
|
292
297
|
// So a client whose runs keep failing is bounded by the MONTHLY cap, not the daily one — which
|
|
293
298
|
// is why a monthlyRuns value is worth setting on any account this matters for.
|
|
294
299
|
if (e.failed === true) continue;
|
|
295
|
-
if (e.profileKey
|
|
300
|
+
if (inLane(e.profileKey, e.organisation) && e.clientPrincipal === true
|
|
296
301
|
&& typeof e.ts === "number" && new Date(e.ts).toISOString().slice(0, 10) === dayKey) today++;
|
|
297
302
|
}
|
|
298
303
|
}
|
|
299
304
|
if (today + 1 > caps.dailyRuns) {
|
|
300
|
-
return `daily allowance: ${
|
|
305
|
+
return `daily allowance: ${lane} has started ${today} search(es) today (runCaps.dailyRuns=${caps.dailyRuns}) — the allowance resets at midnight UTC, or ask your ${BRAND.name} contact to run this one for you`;
|
|
301
306
|
}
|
|
302
307
|
}
|
|
303
308
|
if (Number.isInteger(caps.monthlyRuns)) {
|
|
304
309
|
let month = 0;
|
|
305
310
|
for (const qd of queueDirs) {
|
|
306
311
|
for (const e of readMatterLedger(qd)) {
|
|
307
|
-
if (e.profileKey
|
|
312
|
+
if (inLane(e.profileKey, e.organisation) && typeof e.ts === "number" && new Date(e.ts).toISOString().slice(0, 7) === monthKey) month++;
|
|
308
313
|
}
|
|
309
314
|
}
|
|
310
315
|
if (month + 1 > caps.monthlyRuns) {
|
|
311
|
-
return `admission cap: ${
|
|
316
|
+
return `admission cap: ${lane} has started ${month} run(s) this month (runCaps.monthlyRuns=${caps.monthlyRuns}) — the cap resets at month end; raise it in the customer profile if this is intended growth`;
|
|
312
317
|
}
|
|
313
318
|
}
|
|
314
319
|
return null;
|
|
@@ -742,7 +747,7 @@ async function backstopFailureNotice({ res, job, agentId, base, codename, studio
|
|
|
742
747
|
// parked/failed-at-intake, or the claim was lost.
|
|
743
748
|
// ── THE ORDER-TIME REFUSAL, AND WHY IT IS HERE ──────────────────────────────────
|
|
744
749
|
//
|
|
745
|
-
// A hosted install now STARTS with no register configured (
|
|
750
|
+
// A hosted install now STARTS with no register configured (ruling 2026-09-06: "someone can install
|
|
746
751
|
// and select key later so it should still start"). The protection that used to live in
|
|
747
752
|
// `clearotron start --background` moves here — it does not go away.
|
|
748
753
|
//
|
|
@@ -851,7 +856,7 @@ async function claimAndPrep(jsonFile, qdir, agentId) {
|
|
|
851
856
|
}
|
|
852
857
|
// ── IS THIS BOX CONFIGURED TO SEARCH AT ALL? See the header above claimAndPrep.
|
|
853
858
|
{
|
|
854
|
-
const refusal = orderTimeRefusal(process.env, await runTables(), { envFile: unitEnvPath() });
|
|
859
|
+
const refusal = orderTimeRefusal(process.env, await runTables(), { envFile: unitEnvPath(), readFile: envFileRead() });
|
|
855
860
|
if (refusal) {
|
|
856
861
|
note(`[runner] ${base} REFUSED at order time — this install is not configured to run a search: ${refusal.names.join(", ")}`);
|
|
857
862
|
await failAtIntake(procPath, qdir, base, agentId, job,
|
|
@@ -977,7 +982,8 @@ async function claimAndPrep(jsonFile, qdir, agentId) {
|
|
|
977
982
|
// clarifies before any spend and before the matter is recorded.
|
|
978
983
|
if (!effProfile) note(`[runner] ${base} run-caps skipped — profile resolution errored, caps not evaluable for this admission (fail-open by doctrine, logged for visibility)`);
|
|
979
984
|
const capMsg = checkRunCaps({ account: effProfile?.key ?? null, caps: effProfile?.runCaps, queueDirs: config.queueDirs,
|
|
980
|
-
inHandTagged: Boolean(job.profileKey), clientRun: job.clientPrincipal === true
|
|
985
|
+
inHandTagged: Boolean(job.profileKey), clientRun: job.clientPrincipal === true,
|
|
986
|
+
organisation: typeof job.tenant === "string" && job.tenant ? job.tenant : null });
|
|
981
987
|
if (capMsg) {
|
|
982
988
|
note(`[runner] ${base} ${capMsg}`);
|
|
983
989
|
await failAtIntake(procPath, qdir, base, agentId, job, { classify: "clarify", errors: [capMsg] }, [capMsg]);
|
|
@@ -1005,7 +1011,10 @@ async function claimAndPrep(jsonFile, qdir, agentId) {
|
|
|
1005
1011
|
}
|
|
1006
1012
|
}
|
|
1007
1013
|
recordMatter(qdir, { sig, conversationId, msgId: job.msgId, id: job.id, ts: dupNow,
|
|
1008
|
-
|
|
1014
|
+
// Generic is tagged too, with the organisation whose lane it counts in. An untagged Generic row
|
|
1015
|
+
// counted towards nothing, which was the exemption the owner's 2026-09-10 ruling removed.
|
|
1016
|
+
...(effProfile?.key ? { profileKey: effProfile.key } : {}),
|
|
1017
|
+
...(effProfile?.key === "generic" && typeof job.tenant === "string" && job.tenant ? { organisation: job.tenant } : {}),
|
|
1009
1018
|
// positive-only, and it is what tomorrow's daily count reads. A run that omits it is a staff,
|
|
1010
1019
|
// email or CLI run and never consumes a client's allowance.
|
|
1011
1020
|
...(job.clientPrincipal === true ? { clientPrincipal: true } : {}),
|
package/driver/search-policy.mjs
CHANGED
|
@@ -263,8 +263,8 @@ export const PRODUCT_POLICIES = {
|
|
|
263
263
|
// not run is a search that did not happen, and this row was the shortest of the three.
|
|
264
264
|
//
|
|
265
265
|
// WORLDWIDE (global-preliminary-search) IS DELIBERATELY LEFT AT 1 and that is now an inversion: the
|
|
266
|
-
// broader product gets the weaker recall follow-up. The ruling named this product, and widening
|
|
267
|
-
//
|
|
266
|
+
// broader product gets the weaker recall follow-up. The ruling named this product, and widening a
|
|
267
|
+
// ruling to the most expensive product is not mine to do. Raised as a question rather than
|
|
268
268
|
// assumed —.
|
|
269
269
|
recallFollowupMax: 2,
|
|
270
270
|
envelopeRounds: "as-today",
|
|
@@ -584,7 +584,7 @@ export function gateResolvedPolicy(resolved, { built = BUILT, registerTerritorie
|
|
|
584
584
|
*/
|
|
585
585
|
export function productAvailability(policy, { built = BUILT, registerCanCount = null, registerTerritories = undefined, geography = null, demo = false } = {}) {
|
|
586
586
|
if (!policy) return "unbuilt";
|
|
587
|
-
// ── — A DEMO NO LONGER REFUSES A PRODUCT (
|
|
587
|
+
// ── — A DEMO NO LONGER REFUSES A PRODUCT (ruling, 2026-08-31) ──────
|
|
588
588
|
//
|
|
589
589
|
// `demo` used to return FIRST, before every other cause, and greyed all four products with a sentence
|
|
590
590
|
// about credentials. That implemented the ruling of 14:44 that day, which the owner superseded at
|
|
@@ -615,7 +615,7 @@ export function productAvailability(policy, { built = BUILT, registerCanCount =
|
|
|
615
615
|
// or unrestricted coverage returns null there — this arm cannot fire on a snapshot that says nothing.
|
|
616
616
|
if (geography) {
|
|
617
617
|
const cause = registerCoverageCause(geography, registerTerritories);
|
|
618
|
-
// ── — COVERAGE IS DISCLOSED, NEVER REFUSED (
|
|
618
|
+
// ── — COVERAGE IS DISCLOSED, NEVER REFUSED (ruling, 2026-08-31) ──
|
|
619
619
|
//
|
|
620
620
|
// "A user could still run global and just be aware of the limitations — I prefer that than switch
|
|
621
621
|
// it off." The gate removed the product instead, and it was the ONE place in this system that
|
|
@@ -632,7 +632,7 @@ export function productAvailability(policy, { built = BUILT, registerCanCount =
|
|
|
632
632
|
//
|
|
633
633
|
// `register-cannot-count` is ruled explicitly out of scope on that issue: a register that cannot
|
|
634
634
|
// return counts cannot produce the search's core output, which is a capability gap.
|
|
635
|
-
// ── D6 (
|
|
635
|
+
// ── D6 (ruling, 2026-09-02) — AND THE OTHER COVERAGE CAUSE TOO ────
|
|
636
636
|
//
|
|
637
637
|
// "Disclosure yes, in line with the picker." The picker offers a territory the register cannot
|
|
638
638
|
// reach and says so at the control; a product refused for the same fact was the last place the two
|
|
@@ -678,7 +678,7 @@ export function productAvailability(policy, { built = BUILT, registerCanCount =
|
|
|
678
678
|
*/
|
|
679
679
|
export const UNAVAILABLE_NOTE = {
|
|
680
680
|
unbuilt: "Not part of the current release.",
|
|
681
|
-
// `demo` IS DELETED FROM THIS MAP (
|
|
681
|
+
// `demo` IS DELETED FROM THIS MAP (ruling 2026-08-31 14:47), and
|
|
682
682
|
// deleting it is what makes the reversal structural rather than a filter somebody can undo by
|
|
683
683
|
// accident. Every client-facing surface renders a product's refusal as `UNAVAILABLE_NOTE[cause]`;
|
|
684
684
|
// while a sentence sat here for a demo, the greyed control was one `return "demo"` away from coming
|
|
@@ -692,7 +692,7 @@ export const UNAVAILABLE_NOTE = {
|
|
|
692
692
|
// a baked-in provider name, no CLEAROTRON_* string in scope here, and it says the one thing a reader can
|
|
693
693
|
// act on — this is a coverage limit, so a newer version will not fix it.
|
|
694
694
|
//
|
|
695
|
-
// `register-not-worldwide` IS DELETED FROM THIS MAP (
|
|
695
|
+
// `register-not-worldwide` IS DELETED FROM THIS MAP (ruling
|
|
696
696
|
// 2026-08-31), and deleting it is what makes the ruling structural rather than a filter somebody can
|
|
697
697
|
// reverse by accident. Every client-facing surface renders a product's refusal as
|
|
698
698
|
// `UNAVAILABLE_NOTE[cause]`; while a sentence sat here for that cause, the ruling held only as long as
|
|
@@ -701,7 +701,7 @@ export const UNAVAILABLE_NOTE = {
|
|
|
701
701
|
// visible edit to a map with this paragraph above it. The cause itself still exists and is still
|
|
702
702
|
// computed: `coverageDisclosure` below keys on it, and says what the register DOES reach.
|
|
703
703
|
//
|
|
704
|
-
// `register-coverage` IS DELETED TOO (
|
|
704
|
+
// `register-coverage` IS DELETED TOO (ruling D6, 2026-09-02: "disclosure yes, in line with the
|
|
705
705
|
// picker"). It was the last place a coverage fact could still refuse a product, which is the
|
|
706
706
|
// disagreement between the two controls that the ruling on this issue forbids. Same structural move
|
|
707
707
|
// as its sibling and for the same reason: with no sentence here there is nothing for a door to
|
package/driver/senior-rights.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
-
// senior-rights.mjs — "verify the right that matters" (WP-receipts, 2026-07-05
|
|
3
|
+
// senior-rights.mjs — "verify the right that matters" (WP-receipts, 2026-07-05 steer).
|
|
4
4
|
//
|
|
5
5
|
// The VENZY reference case: the lead conflict was ONE finding backed by NINE registrations of the same
|
|
6
6
|
// mark (Türkiye, UAE, Saudi, …). The run fetched exactly one leg — the top-scoring UAE 2015 filing —
|
|
@@ -44,7 +44,7 @@ never even given. The card's FRAME (the head, `- ord:`, `- group:`, `- source:`,
|
|
|
44
44
|
already the driver's from the 2026-08-16 frame conversion, and the entries above described it for a
|
|
45
45
|
reader rather than ordering anyone to type it.
|
|
46
46
|
|
|
47
|
-
**What did NOT move, and must not be finished by a later reader:** the card's PROSE.
|
|
47
|
+
**What did NOT move, and must not be finished by a later reader:** the card's PROSE. Ruling S2
|
|
48
48
|
(2026-08-13) re-scoped the proposal rather than adopting it — the mechanical fields move now, and whether the
|
|
49
49
|
analysis itself should be rendered is decided by EVIDENCE, one matter built both ways and read side by
|
|
50
50
|
side. The transport carries the seat's sentences; it does not write them.
|
|
@@ -10,21 +10,24 @@
|
|
|
10
10
|
## Band meanings (triage grain)
|
|
11
11
|
|
|
12
12
|
### Very High
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
- **Triage read.** Obvious major blocker.
|
|
14
15
|
|
|
15
16
|
### High
|
|
16
|
-
|
|
17
|
-
known-to-enforce rights holders.
|
|
17
|
+
|
|
18
|
+
- **Triage read.** Significant conflicts — same format as Medium, more urgent. Reserved for dominant, well-resourced, known-to-enforce rights holders.
|
|
18
19
|
|
|
19
20
|
### Medium
|
|
20
|
-
|
|
21
|
+
|
|
22
|
+
- **Triage read.** Material findings that need attention. List adverse parties with dispute context.
|
|
21
23
|
|
|
22
24
|
### Manageable
|
|
23
|
-
|
|
24
|
-
common English phrase.
|
|
25
|
+
|
|
26
|
+
- **Triage read.** Minor findings, no strong claims. Flag major competitors/brand owners if relevant. The floor for any common English phrase.
|
|
25
27
|
|
|
26
28
|
### Low
|
|
27
|
-
|
|
29
|
+
|
|
30
|
+
- **Triage read.** No issues found. Available only for coined/fanciful terms (calibration rule 3).
|
|
28
31
|
|
|
29
32
|
## Calibration rules (they override gut instinct — reviewer-derived)
|
|
30
33
|
|
|
@@ -14,13 +14,14 @@ import { kebab } from "./search-policy.mjs";
|
|
|
14
14
|
// re-deriving it. A third copy is what put the knockout frame a class behind the intake.
|
|
15
15
|
import { requestNamesClasses } from "./enqueue-schema.mjs";
|
|
16
16
|
import { knockoutAssessChunkFile } from "./knockout-assess-record.mjs";
|
|
17
|
+
import { knockoutReviewFile, validateKnockoutReviewFile } from "./knockout-review-record.mjs";
|
|
17
18
|
export { kebab }; // one definition (search-policy) — re-exported for the lane's existing imports
|
|
18
19
|
|
|
19
20
|
const lines = (...xs) => xs.filter(Boolean).join("\n");
|
|
20
21
|
const reads = (skillReads) => `First, read and follow exactly: ${skillReads.join(", ")}.`;
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
|
-
* One line per owner the driver looked up, for the assess dispatch
|
|
24
|
+
* One line per owner the driver looked up, for the assess dispatch.
|
|
24
25
|
*
|
|
25
26
|
* BOTH OUTCOMES ARE NAMED, and that is the point. A row whose search answered gets its payload path; a
|
|
26
27
|
* row whose search did NOT answer says so in the dispatch itself. Listing only the answered ones would
|
|
@@ -51,6 +52,9 @@ export function koPaths(runDir) {
|
|
|
51
52
|
plan: join(runDir, "knockout-plan.json"),
|
|
52
53
|
findings: join(runDir, "knockout-findings.json"),
|
|
53
54
|
assessment: join(runDir, "knockout-assessment.md"),
|
|
55
|
+
// The reviewing pass's record of what it rewrote and what it let stand. At the RUN ROOT, like the
|
|
56
|
+
// assess chunks and for the same reason: it is a model output, and `_driver/` is behind a deny hook.
|
|
57
|
+
review: knockoutReviewFile(runDir),
|
|
54
58
|
// Depth 2: the code-authoritative count sidecar + its per-call receipts. Both live under
|
|
55
59
|
// _driver/ — they are the driver's own measurements, never a model's output.
|
|
56
60
|
registerCounts: driverDir(runDir, "register-counts.json"),
|
|
@@ -63,8 +67,7 @@ export function koPaths(runDir) {
|
|
|
63
67
|
// rows were left exactly as they were rather than gaining a matching marker: their shape is pinned
|
|
64
68
|
// byte-for-byte by register-count.test.mjs, and a discriminator only one side needs is enough.
|
|
65
69
|
registerRecords: driverDir(runDir, "register-records.json"),
|
|
66
|
-
// The scoped owner lookups this run owes its promoted filings, and their receipts
|
|
67
|
-
// 276). Both under `_driver/` because both are the DRIVER's own measurement of a call it made — the
|
|
70
|
+
// The scoped owner lookups this run owes its promoted filings, and their receipts. Both under `_driver/` because both are the DRIVER's own measurement of a call it made — the
|
|
68
71
|
// rule stated fourteen lines above. The seat reads the payloads under research/, never these.
|
|
69
72
|
ownerChecks: driverDir(runDir, "owner-checks.json"),
|
|
70
73
|
ownerCheckLedger: driverDir(runDir, "owner-check.jsonl"),
|
|
@@ -98,6 +101,9 @@ export function koPaths(runDir) {
|
|
|
98
101
|
// back to this and nothing ever writes it again. Delete once no resumable run predates the move.
|
|
99
102
|
assessChunkLegacy: (n) => driverDir(runDir, `knockout-assess-${n}.json`),
|
|
100
103
|
instructedScope: driverDir(runDir, "instructed-scope.json"),
|
|
104
|
+
// as on the clearance lane, and apart from instructed-scope.json for the same reason: the frame
|
|
105
|
+
// quotes that file's values verbatim. Written at intake, read by no stage.
|
|
106
|
+
defaultTerritories: driverDir(runDir, "default-territories.json"),
|
|
101
107
|
emailBody: join(runDir, "email-body.md"),
|
|
102
108
|
};
|
|
103
109
|
}
|
|
@@ -318,7 +324,7 @@ export const KO_STAGES = {
|
|
|
318
324
|
existsSync(K.registerRecords)
|
|
319
325
|
? `THE REGISTER FILINGS THIS RUN ALREADY FETCHED — ${K.registerRecords}. Real records, retrieved before you started: owner, jurisdiction, status, classes, dates. Read them and WEIGH them for the marks you are rating. A registration on this list is evidence about the name it names — treat its owner, its scope and its vulnerability the way the firm-wide reasoning tells you to (revocability above the lowest band; an enforcer's portfolio profile; a crowd as a mitigant under its gating precondition). What you may NOT do is describe the lane that fetched them — see the coverage rule below.`
|
|
320
326
|
: "",
|
|
321
|
-
// ── WHAT THE OWNER ACTUALLY SELLS, ALREADY SEARCHED
|
|
327
|
+
// ── WHAT THE OWNER ACTUALLY SELLS, ALREADY SEARCHED ────────────────────────────────────────────
|
|
322
328
|
//
|
|
323
329
|
// The defect this closes is not that the seat lacked an instruction — it is that the seat had no
|
|
324
330
|
// way to know. It was handed an owner's NAME and its class numbers and nothing else, so a read
|
|
@@ -386,7 +392,7 @@ export const KO_STAGES = {
|
|
|
386
392
|
// source, never a regex over rendered prose.
|
|
387
393
|
// ── THE PER-MARK OPENING ASSESSMENT ──────────────────────────────────────
|
|
388
394
|
//
|
|
389
|
-
//
|
|
395
|
+
// Ruling, 2026-08-26: a batch client gets the same kind of opening paragraph a single-mark
|
|
390
396
|
// client gets, MODEL-AUTHORED and at full length — "the most useful piece of pre-triage; I don't
|
|
391
397
|
// see why we constrain it; 4× longer like the single mark seems fine."
|
|
392
398
|
//
|
|
@@ -397,7 +403,7 @@ export const KO_STAGES = {
|
|
|
397
403
|
// ADDITIONAL TO `chunkSummary`, NOT A REPLACEMENT. The cross-mark paragraph still exists and still
|
|
398
404
|
// owns the grouped page — the two are different documents for different readers, and collapsing
|
|
399
405
|
// them is what produced the blank.
|
|
400
|
-
// ── STRUCTURE, AND A SCALE THAT HOLDS
|
|
406
|
+
// ── STRUCTURE, AND A SCALE THAT HOLDS ─────────────────────────────────────────────────────────
|
|
401
407
|
//
|
|
402
408
|
// Owner, 2026-08-31, reading a delivered run: the per-mark opening was "now HUGE and formatted
|
|
403
409
|
// even worse — no newlines, borderline not a summary". Measured on that run: 2,875 characters,
|
|
@@ -485,4 +491,64 @@ export const KO_STAGES = {
|
|
|
485
491
|
: `As a CONTINUATION chunk you send \`marks\` and \`chunkSummary\` only — batch and framework came from chunk 1.`,
|
|
486
492
|
),
|
|
487
493
|
},
|
|
494
|
+
|
|
495
|
+
// ── THE REVIEWING PASS ──────────────────────────────────────────────────────
|
|
496
|
+
//
|
|
497
|
+
// The third stage, after assess. It rewrites the lines a reader meets before opening anything, in the
|
|
498
|
+
// words that reader already owns, and it does nothing else.
|
|
499
|
+
//
|
|
500
|
+
// SMALLER THAN THE STAGE IT COPIES, DELIBERATELY. The clearance lane's refutation stage re-derives the
|
|
501
|
+
// headline, audits the plan and grades corrections; this one is handed a list of lines and returns
|
|
502
|
+
// replacements for the ones it agrees are wrong. It has no verdict, no band, no evidence and no
|
|
503
|
+
// decision. Copying that stage's SHAPE was the design; copying its size would have made a rewrite pass
|
|
504
|
+
// into a second reviewer, on a product that screens rather than clears.
|
|
505
|
+
//
|
|
506
|
+
// NOT THE PRE-DELIVERY LINT, AND THIS IS THE REASON. That lint is deterministic and its own header
|
|
507
|
+
// states what happens to a failing check: it ships as a visible flag at the top of the client's report.
|
|
508
|
+
// That is a disclosure, and putting a note about the lawyer's vocabulary on the client's own page is
|
|
509
|
+
// the opposite of the intent, on the surface the rule exists to protect. The lint is the right
|
|
510
|
+
// instrument for a mechanical defect and the wrong one for a rewrite.
|
|
511
|
+
//
|
|
512
|
+
// MODEL AND EFFORT. Opus at high, like the two stages before it. A rewrite that keeps every fact while
|
|
513
|
+
// changing the register is the same judgment the rating seats make, and the whole value of the pass is
|
|
514
|
+
// that the replacement reads better than what it replaced. A cheaper rung would produce replacements
|
|
515
|
+
// nobody would ship, which is a pass that runs and achieves nothing.
|
|
516
|
+
"knockout-review": {
|
|
517
|
+
model: "opus", thinking: "high", timeoutSec: 600, stallSec: 420,
|
|
518
|
+
// DECLARATIVE ONLY on this lane, exactly as the assess stage records: nothing in koStage reads
|
|
519
|
+
// `skillReads`, and the effective read is the `reads([...])` call in the message below. The two are
|
|
520
|
+
// edited together and the asymmetry is written down rather than left to be discovered.
|
|
521
|
+
skillReads: ["skills/prelim-search/report-prose.md"],
|
|
522
|
+
out: (K) => K.review,
|
|
523
|
+
validate: validateKnockoutReviewFile,
|
|
524
|
+
// ── THE MESSAGE PROMISES A TABLE ONLY WHEN IT HAS ONE ──────────────────────────
|
|
525
|
+
//
|
|
526
|
+
// `evidenceLines` is the driver's measurement, passed at dispatch. Two things follow, and the arms
|
|
527
|
+
// found both: the defaults must not be absent — a message that throws on a ctx without them cannot
|
|
528
|
+
// be composed by anything except the production call site, which is how a dispatch goes unexamined
|
|
529
|
+
// — and the promise has to branch. A stage message that says "the lines are below" and then lists
|
|
530
|
+
// none has handed the seat an absence dressed as a pass, which is the reading this whole rule
|
|
531
|
+
// exists to stop. The driver does not dispatch on an empty measurement; the branch is here because
|
|
532
|
+
// "it cannot happen" is not a property a message should depend on.
|
|
533
|
+
message: ({ K, evidenceLines = [], exclusionNote = "" }) => lines(
|
|
534
|
+
reads(["skills/prelim-search/report-prose.md"]),
|
|
535
|
+
`You are the REVIEWING PASS on a knockout screen. The batch is already rated and nothing about that rating is yours to change. Your job is the words.`,
|
|
536
|
+
`THE RULE. A line a reader meets before opening anything carries no legal or engine vocabulary and no sentence longer than 25 words. Inside a fold the lawyer's words are allowed where a plain one would lose precision; on a default-visible line they are not. The reader here is the client of the lawyer who ordered this screen, and they read the same page.`,
|
|
537
|
+
`The rated record: ${K.findings}. Read it — the lines named below are quoted from it and you are rewriting them in place.`,
|
|
538
|
+
evidenceLines.length
|
|
539
|
+
? `THE DRIVER MEASURED THESE LINES. Each row is the line, the ADDRESS that names it, and what a deterministic read found. THIS IS EVIDENCE, NOT A VERDICT: judge each in context and pass over any where the word is the subject rather than the profession's shorthand. ${exclusionNote}`
|
|
540
|
+
: `THE DRIVER'S READ FLAGGED NO LINE ON THIS RECORD, so there is no table below and nothing here is evidence of a fault. Read the record yourself against the rule above; if it is already plain, send one \`declined\` row saying so rather than inventing a rewrite to fill the call.`,
|
|
541
|
+
...evidenceLines,
|
|
542
|
+
// ── THE DICTATION NAMES THE FIELD, NOT AN EFFECT ────────────────────────────────
|
|
543
|
+
//
|
|
544
|
+
// "Make the report plainer" is read and not applied, and nothing refuses — it simply does not
|
|
545
|
+
// happen. The address IS the field, so every rewrite this stage returns names the exact line it
|
|
546
|
+
// replaces and the driver writes it there.
|
|
547
|
+
`HAND YOUR REWRITES BACK BY CALLING \`record_knockout_review\`. THERE IS NO FILE FOR YOU TO WRITE and this dispatch names none for you to write to. Send \`rewrites\`: one row per line you are replacing, each carrying the \`at\` address COPIED VERBATIM from the row above and \`text\`, the replacement prose. The driver writes your text into that field exactly as you send it.`,
|
|
548
|
+
`A REWRITE KEEPS EVERY FACT. You are changing the register, never the content: the same band, the same names, the same reasons, the same conclusion, in the words the reader already owns. Splitting one long sentence into two short ones is the commonest right answer. Dropping the reason to make a line shorter is the commonest wrong one.`,
|
|
549
|
+
`SEND \`declined\` FOR A LINE YOU ARE LEAVING ALONE, with \`why\`. A word that is a mark being screened, a client's own product name, or a term with no plainer form that keeps the meaning — say so and leave it. A line you neither rewrite nor decline is indistinguishable afterwards from a line nobody read.`,
|
|
550
|
+
`YOU MOVE NOTHING ELSE. No band, no finding, no ordinal, no count, no piece of evidence and no caveat the engine wrote. There is no field for any of them and a payload naming one is refused.`,
|
|
551
|
+
`IF YOU CALL AGAIN, SEND ONLY WHAT YOU ARE CORRECTING. The driver merges BY ADDRESS onto what it already accepted, so a row you omit keeps what you sent for it before.`,
|
|
552
|
+
),
|
|
553
|
+
},
|
|
488
554
|
};
|
package/driver/stages.mjs
CHANGED
|
@@ -279,7 +279,7 @@ export function paths(runDir) {
|
|
|
279
279
|
// What was DECIDED about the receipt's deferrals, and when — kept apart from plan-execution.json,
|
|
280
280
|
// which is an execution-FACTS receipt several validators fail-closed on. See envelope-settle.mjs.
|
|
281
281
|
envelopeDecision: p(driverRel("envelope-decision.json")),
|
|
282
|
-
instructedScope: p(driverRel("instructed-scope.json")),
|
|
282
|
+
instructedScope: p(driverRel("instructed-scope.json")), defaultTerritories: p(driverRel("default-territories.json")), // the stored account defaults the engine cannot search — written at intake, read by no stage (pipeline.mjs writes it)
|
|
283
283
|
// compute-don't-author — the per-class scope truth (scope-facts.mjs): instructed × frozen plan ×
|
|
284
284
|
// band states × coverage ledger. The masthead classes/coverage_line are stamped FROM this sidecar.
|
|
285
285
|
scopeFacts: p(driverRel("scope-facts.json")),
|
|
@@ -991,7 +991,7 @@ export const pharmaMatter = (job) =>
|
|
|
991
991
|
// The reach is pinned by report-prose-standard.test.mjs, because nothing FAILS if these rules never
|
|
992
992
|
// arrive: the stage simply writes to model defaults and every other test still passes.
|
|
993
993
|
const REPORT_PROSE = "skills/prelim-search/report-prose.md";
|
|
994
|
-
// THE SPINE RIDES WITH synthesis-rules.md EVERYWHERE IT IS READ
|
|
994
|
+
// THE SPINE RIDES WITH synthesis-rules.md EVERYWHERE IT IS READ. 412 lines MOVED
|
|
995
995
|
// out of that file into `firm-wide-reasoning.md` so the knockout lane can read the same copy instead of
|
|
996
996
|
// carrying its own retired transcription. A move, not a copy — which means every stage that read
|
|
997
997
|
// synthesis-rules.md for that material must now read both, or it silently LOSES doctrine it has had all
|
|
@@ -2093,7 +2093,7 @@ export const STAGES = {
|
|
|
2093
2093
|
},
|
|
2094
2094
|
"per-axis prose digest — the SHORT AUDIT NOTE at register-units/<axis>.md": {
|
|
2095
2095
|
class: "judgment", tokens: ["too_short"],
|
|
2096
|
-
why: "#850 rules the per-axis prose digest J (keep) and I do not re-litigate. The only arm that speaks about it is nonEmpty at 40 or 80 chars, in validators.registerUnit — the branch reading `return /not applicable|n\\/a|no .*(hits|results)/i.test(c) ? nonEmpty(c, 40) : nonEmpty(c, 80)`. CONVERTED
|
|
2096
|
+
why: "#850 rules the per-axis prose digest J (keep) and I do not re-litigate. The only arm that speaks about it is nonEmpty at 40 or 80 chars, in validators.registerUnit — the branch reading `return /not applicable|n\\/a|no .*(hits|results)/i.test(c) ? nonEmpty(c, 40) : nonEmpty(c, 80)`. CONVERTED, and the ruling STANDS: the judgment half — is this axis a null result, and the one observation the counts cannot carry — is still the seat's, sent as values through `record_unit_note`. What left is the part that was never judgment: the three COUNTS (queries enumerated, incomplete blocks, records carried forward) are aggregates over the tool-written band, so the driver derives them and the note cannot disagree with the material it describes. The floor is checked at the call now as well as by the validator, so a short note surfaces as a refusal the seat can act on rather than as a stage failure.",
|
|
2097
2097
|
},
|
|
2098
2098
|
"escalation judgement — the CROWD BOUND: attempt each dangerous-category slice once class-scoped, gate on the RESULT, stop terminal on a crowd, and write a block only for the distinctive anchor": {
|
|
2099
2099
|
class: "judgment", tokens: [],
|
|
@@ -2474,7 +2474,7 @@ export const STAGES = {
|
|
|
2474
2474
|
},
|
|
2475
2475
|
"the Sheet-1 findings row's identifier cells — URI, Mark, Owner, Country, Classes, Status, Filed, Expiry": {
|
|
2476
2476
|
class: "mechanical:tool-written", tokens: ["registerdigest_uri_missing", "registerdigest_uri_unknown"],
|
|
2477
|
-
why: "CONVERTED (conversion 11
|
|
2477
|
+
why: "CONVERTED (conversion 11): the seat sends the position's `uri` and the driver renders every cell from the band record it names — record_id, mark_text, classes, status, owner_name, owner_country, application_date, registration_date, expiry_date. The join is now the check: a uri no band record carries is refused AT THE CALL, where restating it costs nothing, instead of producing a plausible row of retyped cells that fails downstream or nowhere. The DECISION that a position earns a row stays judgment (element above); the cells were never anything but transcription.",
|
|
2478
2478
|
},
|
|
2479
2479
|
"the full clickable record URL, composed from providers/<name>.md 'Record base host' plus the record `uri`": {
|
|
2480
2480
|
class: "mechanical:tool-written", tokens: [],
|
|
@@ -2618,8 +2618,8 @@ export const STAGES = {
|
|
|
2618
2618
|
`YOU DO NOT SEND — AND MUST NOT RETYPE — any record's Mark, Owner, Country, Classes, Status, Filed or Expiry, the clickable record URL, the register name, the summary counts, the audit trail or the record ids you read. Every one of those is rendered from the band record your \`uri\` names or from this run's own receipts. The uri IS the join: a uri no band record carries is refused on the call, naming it, rather than rendered as a row of blank cells.`,
|
|
2619
2619
|
// ── — THE SEAT MARKS THE LIST, AND THE RULE NAMES THE FIELD IT SENDS ─────────
|
|
2620
2620
|
//
|
|
2621
|
-
// Stated as a FIELD ON THE CALL, deliberately. The failure this avoids is measured:
|
|
2622
|
-
//
|
|
2621
|
+
// Stated as a FIELD ON THE CALL, deliberately. The failure this avoids is measured: an earlier
|
|
2622
|
+
// ruling covered a rule that described a downstream effect — "the string you sweep" — the seat opened
|
|
2623
2623
|
// the file, and it reached one variant of thirty-six, because the seat authors rows and had no
|
|
2624
2624
|
// field to put the effect in. A rule the seat cannot map onto something it sends produces nothing
|
|
2625
2625
|
// and no refusal.
|
|
@@ -3222,7 +3222,7 @@ export const STAGES = {
|
|
|
3222
3222
|
// lane's own prose contract. Two of the four longest sentences in the delivered report were
|
|
3223
3223
|
// coverage/gap prose, and NOTHING governed it: the code-stamped `coverage_line:` front-matter
|
|
3224
3224
|
// (scope-facts.mjs,) is EXCLUDED from predelivery-lint's prose scan by design
|
|
3225
|
-
// (stripFrontMatterBlock
|
|
3225
|
+
// (stripFrontMatterBlock in predelivery-lint.mjs), so a coverage number re-typed into prose
|
|
3226
3226
|
// beside it is caught only if the prose form itself trips SCOPE_NUMBER_RE — which the narrative's
|
|
3227
3227
|
// own phrasings routinely dodge. Prompt-only per §7; the fix is to stop authoring the duplicate.
|
|
3228
3228
|
`COVERAGE PROSE (the lane that runs longest — hold it to the house budgets): the register coverage line a reader sees is COMPUTED from this run's own record and STAMPED BY CODE as front-matter (the proportion, the class states, the searched registers). Do NOT re-type its numbers anywhere in prose — not in a coverage[] note, not in the coverage_judgment reason, not in the narrative. Nothing catches the duplicate for you (the code-stamped line sits outside the prose checks precisely because ITS numbers are the authoritative ones), so a re-typed count does not disagree with the record — it silently drifts from it a redelivery later. Carry the SUBSTANCE and drop the number: "the remaining forms are non-Latin script" says the useful half; the code says how many. State each coverage fact ONCE, in ONE place — an area's state belongs in its coverage[] row, the sufficiency read belongs in coverage_judgment.reason, and neither is re-narrated in the other or in the findings. And say WHICH KIND of negative you hold every time: a source this run actually queried and got nothing from reads "searched — none found"; a source it did not reach reads "not searched this run" or "could not be searched — <the reason>". The same source must never wear both readings in one report.`,
|
|
@@ -3870,7 +3870,7 @@ export const STAGES = {
|
|
|
3870
3870
|
reads(reportCardSkillReads({ profile })),
|
|
3871
3871
|
// ── CONVERSION 5. THE FIRST FAN-OUT TRANSPORT ────────────────────────────────────
|
|
3872
3872
|
//
|
|
3873
|
-
// What moves is the SHAPE, not the analysis.
|
|
3873
|
+
// What moves is the SHAPE, not the analysis. Ruling S2 (2026-08-13) keeps the prose the
|
|
3874
3874
|
// model's pending a side-by-side reading of one matter built both ways — so this dispatch still
|
|
3875
3875
|
// asks for the seat's judgment, bullet by bullet, and stops asking it to hit line templates a
|
|
3876
3876
|
// parser then re-reads.
|
|
@@ -4561,11 +4561,11 @@ export function assertTierSanity() {
|
|
|
4561
4561
|
return true;
|
|
4562
4562
|
}
|
|
4563
4563
|
|
|
4564
|
-
// ── WHO THE COMPLETION NOTICE IS FOR
|
|
4564
|
+
// ── WHO THE COMPLETION NOTICE IS FOR ─────────────────────────────────────────────────────────────────
|
|
4565
4565
|
//
|
|
4566
4566
|
// It went to whoever runs the agent, never to whoever asked. `AGENT_WHATSAPP` is keyed by AGENT ID, and
|
|
4567
4567
|
// every user of this deployment shares the one agent, so on every run the operator was paged for work
|
|
4568
|
-
// somebody else ordered and the person who ordered it was told nothing.
|
|
4568
|
+
// somebody else ordered and the person who ordered it was told nothing. Ruling, 2026-09-07: route
|
|
4569
4569
|
// to the requester where a number is held for them, and keep the operator's number as a copy the
|
|
4570
4570
|
// operator can switch off.
|
|
4571
4571
|
//
|