clearotron 0.3.2 → 0.3.3-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/CONTRIBUTING.md +12 -0
- package/INSTALL.md +8 -0
- package/bin/onboard.mjs +109 -13
- package/bin/start.mjs +1 -1
- package/build-info.json +2 -2
- package/demo/MANIFEST.json +27 -0
- package/docs/INTAKE.md +8 -0
- package/docs/architecture/04-configuration-reference.md +29 -11
- package/driver/CHANGELOG.md +49 -0
- package/driver/citation-census.json +3 -3
- package/driver/clearance-variants-record.mjs +12 -1
- package/driver/common-law-coverage-status.mjs +113 -0
- package/driver/config-inventory.mjs +1 -1
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +37 -37
- package/driver/contract-vocabulary.mjs +8 -8
- package/driver/coverage-form-io.mjs +3 -1
- package/driver/coverage-form.mjs +38 -11
- package/driver/coverage-ledger.mjs +37 -7
- package/driver/coverage-union.mjs +2 -2
- package/driver/crowd-context.mjs +19 -6
- package/driver/dev-portal.mjs +3 -3
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +80 -9
- package/driver/engine/CONTRACT.md +3 -2
- package/driver/engine/anthropic-agent.mjs +34 -7
- package/driver/engine/mcp/clarivate-server.mjs +4 -2
- package/driver/engine/mcp/corsearch-server.mjs +3 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +47 -5
- package/driver/engine/mcp/euipo-server.mjs +2 -0
- package/driver/engine/mcp/free-tier-server.mjs +2 -0
- package/driver/engine/mcp/gather-config.mjs +8 -2
- package/driver/engine/mcp/probe-server.mjs +37 -0
- package/driver/engine/mcp/proposal-fields.mjs +45 -0
- package/driver/engine/mcp/recording-server.mjs +30 -0
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +89 -12
- package/driver/engine/mcp/unit-note-server.mjs +50 -0
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/engine/openai-agent.mjs +7 -0
- package/driver/engine/probe.mjs +67 -14
- package/driver/engine/tool-refusal.mjs +16 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/envelope-settle.mjs +82 -13
- package/driver/findings-model.mjs +4 -4
- package/driver/gateway.mjs +18 -2
- package/driver/manager-groups-verdict.mjs +1 -1
- package/driver/matter-frame-record.mjs +24 -7
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +12 -3
- package/driver/pipeline-knockout.mjs +3 -3
- package/driver/pipeline.mjs +154 -50
- package/driver/plan-run-agreement-verdict.mjs +49 -0
- package/driver/portal-service.mjs +8 -4
- package/driver/progress.mjs +14 -3
- package/driver/publish/index.mjs +41 -26
- package/driver/publish/report-data.mjs +4 -3
- package/driver/publish/xlsx.mjs +26 -4
- package/driver/queue-markers.mjs +44 -0
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/reference-score.mjs +10 -2
- package/driver/register-availability.mjs +2 -2
- package/driver/register-plan.mjs +313 -21
- package/driver/roster-verdict.mjs +1 -1
- package/driver/runner.mjs +26 -2
- package/driver/settle-stamp.mjs +10 -3
- package/driver/skills/clearance-common-law/SKILL.md +2 -0
- package/driver/skills/clearance-register/SKILL.md +44 -3
- package/driver/skills/clearance-register/digest.md +5 -5
- package/driver/skills/clearance-register/providers/clarivate.md +1 -1
- package/driver/skills/clearance-register/unit.md +39 -0
- package/driver/skills/clearance-variants/SKILL.md +1 -1
- package/driver/skills/matter-frame/SKILL.md +4 -2
- package/driver/stages.mjs +12 -5
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +293 -29
- package/driver/synthesis-record.mjs +80 -2
- package/driver/unit-file-drift.mjs +3 -3
- package/driver/unit-inventory.mjs +2 -2
- package/driver/unit-state-verdict.mjs +1 -1
- package/driver/updater-identity.mjs +2 -3
- package/driver/variant-manifest-model.mjs +11 -1
- package/driver/verify.mjs +5 -5
- package/driver/withheld-families.mjs +104 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/brief.mjs +16 -12
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +3 -2
- package/package.json +2 -2
- package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +2 -2
- package/providers/_shared/enumerate.mjs +15 -2
- package/providers/_shared/execute-plan.mjs +19 -1
- package/providers/_shared/plan-guards.mjs +40 -0
- package/providers/clarivate/src/capabilities.js +15 -5
- package/providers/clarivate/src/core.js +41 -5
- package/providers/corsearch/src/capabilities.js +4 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +22 -8
- package/providers/signa/src/core.js +12 -1
- package/scripts/demo-evidence.mjs +114 -0
- package/scripts/e2e.mjs +1 -1
- package/scripts/engine-probe.mjs +6 -5
- package/scripts/env-audit.mjs +1 -1
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/live-surface-check.mjs +32 -33
- package/scripts/mint-suite-census.mjs +66 -0
- package/scripts/package-size-budget.mjs +117 -0
- package/scripts/register-plan-shape.mjs +259 -0
- package/scripts/release-note-required.mjs +38 -1
- package/scripts/repo-writes.mjs +1 -1
- package/scripts/report-sections-render-check.mjs +7 -3
- package/scripts/score.mjs +7 -1
- package/scripts/settings-render-check.mjs +36 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/identifier-scan.mjs +22 -5
- package/shared/scroll-settle.mjs +67 -0
package/driver/runner.mjs
CHANGED
|
@@ -26,7 +26,7 @@ import { driverDir, ensureDriverDir } from "../shared/driver-dir.mjs"; // —
|
|
|
26
26
|
// The queue's filename vocabulary, in ONE place — the rule, extended by to the prose-sidecar and
|
|
27
27
|
// claim-sidecar names, because a harness check retyped four of them from memory and false-alarmed on the
|
|
28
28
|
// other nine. Behaviour here is unchanged: the same object and the same three suffixes, sourced.
|
|
29
|
-
import { isLiveQueueMarker, PROSE_PARTS, CLAIM_SIDECAR_SUFFIXES, TERMINAL_QUEUE_SUFFIXES } from "./queue-markers.mjs";
|
|
29
|
+
import { isLiveQueueMarker, PROSE_PARTS, CLAIM_SIDECAR_SUFFIXES, TERMINAL_QUEUE_SUFFIXES, withFoldedGoods } from "./queue-markers.mjs";
|
|
30
30
|
import { matterLedgerPath, DEFAULT_CLIENT_DAILY_RUNS } from "./usage-ledger.mjs"; // ONE ledger-path calculation, shared with the portal pre-check
|
|
31
31
|
import { orderTimeRefusal, START_ENV_FILE_FLAG, startEnvFileOf } from "./run-requirements.mjs"; // one authority for what a run needs, and when it is asked for
|
|
32
32
|
import { unitEnvPath, envFileRead } from "../shared/env-local.mjs"; // the file the units read, named by its one author
|
|
@@ -79,7 +79,12 @@ function assembleJob(procPath, qdir, base) {
|
|
|
79
79
|
if (v.trim()) job[field] = v;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
|
|
82
|
+
// AFTER the sidecars, because a goods description can arrive as one and folding before them would
|
|
83
|
+
// read a field the job did not have yet. Intake accepts the description under either spelling and
|
|
84
|
+
// every reader in a run asks for the current field, so it is folded once here rather than at each of
|
|
85
|
+
// the ten places that ask — the seam that let a job on the older spelling scope no goods at all. The
|
|
86
|
+
// queue file is not touched: what was filed stays as filed.
|
|
87
|
+
return withFoldedGoods(job);
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
// Best-effort sweep of a job's prose sidecars once it reaches a terminal state, so a drained queue leaves no
|
|
@@ -2360,6 +2365,20 @@ export async function watch({
|
|
|
2360
2365
|
// — injectable for the same reason every other dependency here is: the loop's arms drive it with a
|
|
2361
2366
|
// fake clock and must not write to a real install.
|
|
2362
2367
|
heartbeat = () => beat(config.runLockDir),
|
|
2368
|
+
// ── AND IT KEEPS BEATING WHILE THE TICK RUNS ────────────────────────────────────────────────────
|
|
2369
|
+
//
|
|
2370
|
+
// The beat above is written once per tick and the tick then AWAITS a whole run. A clearance takes
|
|
2371
|
+
// hours, so on a busy install the heartbeat was stale for the entire time the worker was healthiest
|
|
2372
|
+
// — measured forty minutes stale on a live run that was writing records throughout. Anything
|
|
2373
|
+
// reading beat age as liveness would call that run dead, and the obvious next act is to restart or
|
|
2374
|
+
// kill it. The file was fresh when the box was IDLE, which is the reading that makes it dangerous:
|
|
2375
|
+
// it looks like a liveness signal precisely when nothing is happening.
|
|
2376
|
+
//
|
|
2377
|
+
// So the beat continues on its own timer for as long as the run is in flight. Injectable because
|
|
2378
|
+
// the arms must drive it on a fake clock; unref'd in the default so a pending beat can never be the
|
|
2379
|
+
// thing keeping this process alive.
|
|
2380
|
+
beatEveryMs = WATCH_TICK_MS,
|
|
2381
|
+
startBeating = (fn, everyMs) => { const t = setInterval(fn, everyMs); t.unref?.(); return () => clearInterval(t); },
|
|
2363
2382
|
} = {}) {
|
|
2364
2383
|
note(`[runner] watch: polling every ${Math.round(tickMs / 1000)}s (no systemd here); a due park or an unclaimed job is picked up on the next tick. Ctrl-C to stop.`);
|
|
2365
2384
|
for (let n = 0; ; n++) {
|
|
@@ -2371,8 +2390,13 @@ export async function watch({
|
|
|
2371
2390
|
// leave a freshly-started worker looking absent for its whole first tick — which is exactly the window
|
|
2372
2391
|
// a user watches after pressing Start.
|
|
2373
2392
|
heartbeat();
|
|
2393
|
+
const stopBeating = startBeating(heartbeat, beatEveryMs);
|
|
2374
2394
|
try { await run({ once: true }); }
|
|
2375
2395
|
catch (e) { note(`[runner] watch tick FAILED (isolated — retrying next tick): ${e?.message ?? e}`); }
|
|
2396
|
+
// Reached on every path out of the tick, including the one where the catch above swallows a
|
|
2397
|
+
// failure. A timer left running would go on beating for a worker that had stopped — the one
|
|
2398
|
+
// direction this file's fail-safe rule forbids, because it is the reading nobody checks twice.
|
|
2399
|
+
finally { stopBeating(); }
|
|
2376
2400
|
if (n + 1 >= ticks) break;
|
|
2377
2401
|
const due = now() + tickMs;
|
|
2378
2402
|
for (;;) {
|
package/driver/settle-stamp.mjs
CHANGED
|
@@ -55,12 +55,15 @@ export const SETTLE_SCHEMA_VERSION = 1;
|
|
|
55
55
|
* reads a delivery date (turnaround, an SLA, a client-facing "delivered on") would have inherited it.
|
|
56
56
|
* A backfiller must not compose this value at all: call `backfillSettleStamp`, which reads it.
|
|
57
57
|
*/
|
|
58
|
-
|
|
58
|
+
// The outcome words are named for what they are: the full-search lane's reviewer `signoff` (CLEAR /
|
|
59
|
+
// CONDITIONAL / BLOCKING) and the quick-search lane's rating `tier`. Both once shared one `verdict` field,
|
|
60
|
+
// which read as the clearance's answer and held a different vocabulary on each lane.
|
|
61
|
+
export function writeSettleStamp(poolRunDir, { state, signoff = null, tier = null, deliveredAt = null, runId = null, lane = null } = {}) {
|
|
59
62
|
if (!poolRunDir) return { written: false, reason: "no pool run directory — the run published nowhere" };
|
|
60
63
|
if (!state) return { written: false, reason: "no terminal state given — a stamp with no state is the absence it would be mistaken for" };
|
|
61
64
|
const path = join(poolRunDir, SETTLE_FILE);
|
|
62
65
|
try {
|
|
63
|
-
writeFileSync(path, `${JSON.stringify({ schema_version: SETTLE_SCHEMA_VERSION, state,
|
|
66
|
+
writeFileSync(path, `${JSON.stringify({ schema_version: SETTLE_SCHEMA_VERSION, state, ...(signoff ? { signoff } : {}), ...(tier ? { tier } : {}), deliveredAt, runId, lane, stampedAt: new Date().toISOString() }, null, 2)}\n`);
|
|
64
67
|
// Group-read like every other pool file. Best-effort on its own: a stamp nobody can chmod is still
|
|
65
68
|
// a stamp, and the set-GID pool already grants the group.
|
|
66
69
|
try { chmodSync(path, 0o640); } catch { /* best-effort, exactly as publish's writeRO does */ }
|
|
@@ -116,7 +119,11 @@ export function backfillSettleStamp(poolRunDir, runDir, { readStatus = defaultRe
|
|
|
116
119
|
return { written: false, reason: "status.json says delivered but carries no deliveredAt — the delivery time this backfill exists to preserve is not recorded" };
|
|
117
120
|
return writeSettleStamp(poolRunDir, {
|
|
118
121
|
state: status.state,
|
|
119
|
-
|
|
122
|
+
// A status.json written before the sign-off moved carries it as `verdict`, and on the quick-search
|
|
123
|
+
// lane that field held the rating; each is read back into the field it always meant.
|
|
124
|
+
...((status.lane ?? (status.marks ? "knockout" : "clearance")) === "knockout"
|
|
125
|
+
? { tier: status.tier ?? status.verdict ?? null }
|
|
126
|
+
: { signoff: status.review?.signoff ?? status.verdict ?? null }),
|
|
120
127
|
deliveredAt: status.deliveredAt ?? null,
|
|
121
128
|
runId: status.runId ?? null,
|
|
122
129
|
lane: status.lane ?? (status.marks ? "knockout" : "clearance"),
|
|
@@ -364,6 +364,8 @@ One row per planned coverage unit (each mandatory platform; the field-scoped gen
|
|
|
364
364
|
| field-scoped general search (collab / non-gaming goods) | confirmed-clean | run per matter scope |
|
|
365
365
|
| non-Latin platform reach (translit variants) | coverage-limited | marketplace data thin for non-Latin scripts; absence not confirmed clean |
|
|
366
366
|
|
|
367
|
+
Record the same statuses by calling `record_coverage_status`, passing `grid_spec_path` (the same driver-written spec path the grid tool was given) and one entry per ledger row: its coverage unit, and its status, exactly `confirmed-clean`, `coverage-limited` or `deferred`.
|
|
368
|
+
|
|
367
369
|
### Cross-checks suggested (handed to orchestrator for clearance-register dispatch)
|
|
368
370
|
|
|
369
371
|
| Trigger | Suggested cross-check |
|
|
@@ -110,6 +110,45 @@ funnel never converts a resource limit into a clean negative and never re-adds a
|
|
|
110
110
|
says "searched N, ship clean". (Phoneme at 5 and image at 10 are observed budgets — exceeding them usually
|
|
111
111
|
indicates a worker repeating itself, not finding new content.)
|
|
112
112
|
|
|
113
|
+
### At every step: look at what you have before you work on it
|
|
114
|
+
|
|
115
|
+
Fifty? Read them all. A few hundred? Sort by the client's goods and markets, read the near ones first, list
|
|
116
|
+
the rest. A thousand or more? The field is crowded: read the identical and live ones for the client's
|
|
117
|
+
goods, tell the client it is crowded, and do not write up the rest. Carry forward only what a lawyer would
|
|
118
|
+
raise with the client. Write down what you set aside and why. This applies to the register's answer, to the
|
|
119
|
+
list of records, to the placements, to the off-register sweep and to the write-up alike.
|
|
120
|
+
|
|
121
|
+
### Read the identical mark first. Look at the count before you read anything
|
|
122
|
+
|
|
123
|
+
On every matter the identical mark, in the instructed classes, is the first thing you read, and nothing
|
|
124
|
+
wider is asked until you have read it. The register tells you how many filings answer a question before it
|
|
125
|
+
hands you any. Take a look: how bad is it? If the list is one you can read record by record, read it in
|
|
126
|
+
full. If it is not, do not read it and do not leave it: narrow the same question, in this order, until it
|
|
127
|
+
is, then read that list in full and decide.
|
|
128
|
+
|
|
129
|
+
1. Ask again for the identical mark, in the instructed classes, limited to the client's goods words. Read
|
|
130
|
+
that list.
|
|
131
|
+
2. Still a crowd? Ask again limited to the client's main markets, one question per market. Read those lists.
|
|
132
|
+
3. Still a crowd in a market? Ask again with the dominant goods word alone. Read it.
|
|
133
|
+
4. Still a crowd? Ask one class at a time. Read each list.
|
|
134
|
+
5. When the readable list already holds conflicts in the client's field, stop widening. Do not open scripts,
|
|
135
|
+
neighbours, compounds or guessed owners for this mark. Write which questions you did not ask and why.
|
|
136
|
+
6. When the readable list is thin, widen one step at a time: close variants first, then the owner families
|
|
137
|
+
of what you found.
|
|
138
|
+
|
|
139
|
+
Every question you ask is recorded with its count. A narrowing replaces nothing silently: the crowd it
|
|
140
|
+
replaces stays on the record with its count.
|
|
141
|
+
|
|
142
|
+
When you stop widening under step 5, the families you did not open are recorded `withheld-by-judgment`
|
|
143
|
+
with your reason — not `confirmed-clean`, which would claim a search nobody ran, and not
|
|
144
|
+
`coverage-limited`, which says the engine tried and could not finish. You chose where the work was best
|
|
145
|
+
spent; say so, and say which questions you did not ask.
|
|
146
|
+
|
|
147
|
+
When you narrow a crowded question, name the crowd it replaces: put its qid on the proposal as
|
|
148
|
+
`narrows`. The record then shows the crowd and the question that answered it side by side, each with
|
|
149
|
+
its own count. A narrowing that does not name what it replaced leaves the crowd looking unanswered.
|
|
150
|
+
|
|
151
|
+
|
|
113
152
|
**The per-major and per-jurisdiction named queries are breadth, not a budgeted sufficiency allowance.** Each
|
|
114
153
|
in-scope major (US/EU/UK/CN/JP) and each material jurisdiction the matter-frame declared is a **named slice
|
|
115
154
|
the funnel COVERS** — and when the region-scoped in-scope sweep (Recipe 1 Step 2) returns `enumerated`
|
|
@@ -193,11 +232,11 @@ one hard stop — the driver fails fan-in on it, so a clean can never ship over
|
|
|
193
232
|
writes no `coverage_judgment` and no clean verdict** — it hands up the complete band + honest `incomplete`s and
|
|
194
233
|
lets judgment decide.
|
|
195
234
|
|
|
196
|
-
### Coverage ledger — which document you owe, and what the
|
|
235
|
+
### Coverage ledger — which document you owe, and what the four status tokens mean
|
|
197
236
|
|
|
198
237
|
**The status vocabulary is CLOSED: EXACTLY one bare token of: `confirmed-clean` / `coverage-limited` /
|
|
199
|
-
`deferred`.** Qualifiers never go in a status cell; they go in the reason. The distinction between
|
|
200
|
-
|
|
238
|
+
`deferred` / `withheld-by-judgment`.** Qualifiers never go in a status cell; they go in the reason. The distinction between
|
|
239
|
+
`coverage-limited` and `deferred` is doctrine, not wording, and the driver relabels a row that gets it wrong:
|
|
201
240
|
|
|
202
241
|
- **`coverage-limited`** — the search RAN and could not be exhausted: a crowd over the provider window, a
|
|
203
242
|
count-only saturation descriptor, a volume/pagination ceiling. A re-run cannot close it, so the escalation
|
|
@@ -207,6 +246,8 @@ two is doctrine, not wording, and the driver relabels a row that gets it wrong:
|
|
|
207
246
|
disclose — and it clamps the verdict CLEAR→CONDITIONAL. Mislabel one of these `coverage-limited` and a
|
|
208
247
|
fixable hole disappears into an accepted limit.
|
|
209
248
|
- **`confirmed-clean`** — the slice ran to completion and judgment cleared it.
|
|
249
|
+
- **`withheld-by-judgment`** — a waiting family the reading turn chose not to ask, so it was never searched.
|
|
250
|
+
Only a `family` row takes it, and its reason goes into the audit workbook, not the report.
|
|
210
251
|
|
|
211
252
|
**HOW those statuses are recorded is not yours to assume — the dispatch states it, and it is one route,
|
|
212
253
|
the `record_coverage` tool.** The driver computes the form before every digest dispatch and enumerates
|
|
@@ -207,14 +207,14 @@ ledger` table and its JSON mirror from what you record afterwards. A run that ca
|
|
|
207
207
|
gets a form — one declaring that, and naming its cause from a closed vocabulary — so "there is no form"
|
|
208
208
|
is not a state you will meet.
|
|
209
209
|
|
|
210
|
-
The dispatch ENUMERATES the form's rows — one per axis, one per unaccounted crowd block
|
|
211
|
-
deferred slice, each with its `row_id` — and every identifier is computed by the driver from the frozen
|
|
210
|
+
The dispatch ENUMERATES the form's rows — one per axis, one per unaccounted crowd block, one per
|
|
211
|
+
deferred slice and one per waiting family the reading turn did not ask, each with its `row_id` — and every identifier is computed by the driver from the frozen
|
|
212
212
|
register plan and the plan-execution receipt: the coverage unit, the query id, the hit count, the
|
|
213
213
|
unaccounted classes and terms, and each deferred slice's own receipt reason. Rule **every** row by
|
|
214
214
|
calling the **`record_coverage` tool**, one entry per row, with two values of yours:
|
|
215
215
|
|
|
216
|
-
- `status` — EXACTLY one bare token: `confirmed-clean` / `coverage-limited` / `deferred`.
|
|
217
|
-
never go in the status; they go in the reason.
|
|
216
|
+
- `status` — EXACTLY one bare token: `confirmed-clean` / `coverage-limited` / `deferred` / `withheld-by-judgment`.
|
|
217
|
+
Qualifiers never go in the status; they go in the reason. A waiting family's row takes only `withheld-by-judgment`, and most arrive already settled with the reading turn's reason.
|
|
218
218
|
- `reason` — the sentence the lawyer reads.
|
|
219
219
|
|
|
220
220
|
The driver validates each row as it arrives — a refused row names what to change, and the rest of the
|
|
@@ -359,7 +359,7 @@ For example: subject `PHINIA — placed at watchlist-annex, class-match said hea
|
|
|
359
359
|
decision `ADOPTED`, reason `the cl.12 overlap is auto-parts vs the applicant's software; off-field`.
|
|
360
360
|
|
|
361
361
|
**Coverage ledger → the `coverage_judgment` contract.** You rule every row of the driver's coverage form
|
|
362
|
-
through `record_coverage` (a `confirmed-clean` / `coverage-limited` / `deferred` status and a reason on
|
|
362
|
+
through `record_coverage` (a `confirmed-clean` / `coverage-limited` / `deferred` / `withheld-by-judgment` status and a reason on
|
|
363
363
|
every row — the driver renders both the `## Coverage ledger` table and `register-coverage-ledger.json`
|
|
364
364
|
from what the tool records), plus the rolled-up sufficiency line. The **synthesis stage** lifts the rolled-up line into `findings.json`
|
|
365
365
|
as the top-level `coverage_judgment` field: `{ "sufficient": <bool>, "reason": "<why>" }`. The FORM is the
|
|
@@ -82,7 +82,7 @@ An operator word *inside* a phrase is handled for you: "BLACK AND DECKER" goes o
|
|
|
82
82
|
`*BLACK ADJ A?D ADJ DECKER*` (the `?` stops the parser reading AND as an operator). The one term that
|
|
83
83
|
still defers is a bare two-letter operator word — `OR` alone has no interior character to wildcard.
|
|
84
84
|
|
|
85
|
-
`names[]` (an OR-stack) becomes ONE value joined with explicit ` OR `. The safe width is **
|
|
85
|
+
`names[]` (an OR-stack) becomes ONE value joined with explicit ` OR `. The safe width is **496 terms** —
|
|
86
86
|
the bound is the JSON parser's document-nesting cap, which the register names in the refusal it answers a
|
|
87
87
|
wider stack with. `register_enumerate` chunks wider stacks for you at that bound.
|
|
88
88
|
|
|
@@ -45,6 +45,45 @@ You were spawned to run exactly ONE axis named in your task. Read the manifest +
|
|
|
45
45
|
3. **ENUMERATE the dangerous NAMED band with `register_enumerate` — class-scoped, never manual paginate-then-sample.** For each named query (the exact mark + each specific variant × in-scope class × material/major jurisdiction), call **`register_enumerate`** with the query (`name`/`names`/`match_mode`/`nice_classes`/`regions`/`owner_country`/`in_scope_classes` + the usual `register_search` fields). **MANDATORY: every `register_enumerate` call MUST carry `nice_classes`=<the matter's in-scope Nice set> AND `in_scope_classes`=<the same set>.** An enumerate with `nice_classes` OMITTED runs an all-45-class crowd (the `default` / `starts_with` / `ends_with` / `phonetic` / `fuzzy` modes are unbounded unscoped) — it floods the band and TIMES OUT the stage, and it is **FORBIDDEN**: scope-by-class is breadth the matter instructed, not a "good enough" call. The **only** all-class exception is the exact-IDENTICAL cross-class merch check (`match_mode:exact`, `nice_classes:[25]`). `fuzzy` is **never** an enumerate mode. **The tool owns the page loop and CANNOT return a partial list** — you cannot get a partial result and call it done, and there is no top-N mode. It returns exactly **one of two states**:
|
|
46
46
|
- **`{state:"enumerated", total_hits, count, records:[…]}`** — it paged to `has_more:false`; every named record is carried forward, already batch-screened (each record carries `record_id`, `mark_text`, `classes`, `status`, `owner_name`, `owner_country`, `application_date`, `registration_date`, `expiry_date`, `jurisdictions`, `screen_verdict`). Write this verbatim as an `enumerated` band block.
|
|
47
47
|
- **`{state:"incomplete", total_hits, fetched, sample, reason}`** — it could **not** page to completion (the band is a genuine CROWD over the resource ceiling, the provider 5000-record window was hit, or a provider error occurred). Write this verbatim as an `incomplete` band block. **This is a SIGNAL to judgment, never a clean negative and never something you self-accept.** You do **not** "narrow to tractable and call it clean" — an incomplete result crosses the firewall as an incomplete block; judgment decides whether to command a narrower enumeration or halt.
|
|
48
|
+
|
|
49
|
+
### At every step: look at what you have before you work on it
|
|
50
|
+
|
|
51
|
+
Fifty? Read them all. A few hundred? Sort by the client's goods and markets, read the near ones first, list
|
|
52
|
+
the rest. A thousand or more? The field is crowded: read the identical and live ones for the client's
|
|
53
|
+
goods, tell the client it is crowded, and do not write up the rest. Carry forward only what a lawyer would
|
|
54
|
+
raise with the client. Write down what you set aside and why. This applies to the register's answer, to the
|
|
55
|
+
list of records, to the placements, to the off-register sweep and to the write-up alike.
|
|
56
|
+
|
|
57
|
+
### Read the identical mark first. Look at the count before you read anything
|
|
58
|
+
|
|
59
|
+
On every matter the identical mark, in the instructed classes, is the first thing you read, and nothing
|
|
60
|
+
wider is asked until you have read it. The register tells you how many filings answer a question before it
|
|
61
|
+
hands you any. Take a look: how bad is it? If the list is one you can read record by record, read it in
|
|
62
|
+
full. If it is not, do not read it and do not leave it: narrow the same question, in this order, until it
|
|
63
|
+
is, then read that list in full and decide.
|
|
64
|
+
|
|
65
|
+
1. Ask again for the identical mark, in the instructed classes, limited to the client's goods words. Read
|
|
66
|
+
that list.
|
|
67
|
+
2. Still a crowd? Ask again limited to the client's main markets, one question per market. Read those lists.
|
|
68
|
+
3. Still a crowd in a market? Ask again with the dominant goods word alone. Read it.
|
|
69
|
+
4. Still a crowd? Ask one class at a time. Read each list.
|
|
70
|
+
5. When the readable list already holds conflicts in the client's field, stop widening. Do not open scripts,
|
|
71
|
+
neighbours, compounds or guessed owners for this mark. Write which questions you did not ask and why.
|
|
72
|
+
6. When the readable list is thin, widen one step at a time: close variants first, then the owner families
|
|
73
|
+
of what you found.
|
|
74
|
+
|
|
75
|
+
Every question you ask is recorded with its count. A narrowing replaces nothing silently: the crowd it
|
|
76
|
+
replaces stays on the record with its count.
|
|
77
|
+
|
|
78
|
+
When you stop widening under step 5, the families you did not open are recorded `withheld-by-judgment`
|
|
79
|
+
with your reason — not `confirmed-clean`, which would claim a search nobody ran, and not
|
|
80
|
+
`coverage-limited`, which says the engine tried and could not finish. You chose where the work was best
|
|
81
|
+
spent; say so, and say which questions you did not ask.
|
|
82
|
+
|
|
83
|
+
When you narrow a crowded question, name the crowd it replaces: put its qid on the proposal as
|
|
84
|
+
`narrows`. The record then shows the crowd and the question that answered it side by side, each with
|
|
85
|
+
its own count. A narrowing that does not name what it replaced leaves the crowd looking unanswered.
|
|
86
|
+
|
|
48
87
|
- **Write each call's result as one block in `register-units/<axis>-band.json`** (the named-band array — see *Named-band artifact* below). One block per `register_enumerate` / count-probe call. An `enumerated` block carries its records; an `incomplete` block is carried forward **verbatim — never dropped, never "accepted", never silently re-narrowed-then-cleaned**.
|
|
49
88
|
- **The completeness contract is UNIFORM — it applies to the named band too.** "The exact mark, in-class, every major+material jurisdiction" is *usually* small and `enumerated` — but that is **not load-bearing**. If it ever runs to a few hundred and `register_enumerate` returns `incomplete`, it stays `incomplete` and crosses to judgment, exactly like the wider crowd. There is exactly **one** way to be done with a search: `enumerated`. Everything else is `incomplete` and the lawyer reads it. **Never self-accept "good enough" anywhere — not even the named band.**
|
|
50
89
|
4. **For a SATURATION CROWD (a genuinely unbounded `contains` / character-indexed pile), write a COUNT-ONLY descriptor — do NOT enumerate it.** A saturated element solo (e.g. the everyday-word meaning token alone, the bare common element) is not a named band — it is noise the lawyer needs *described*, not *piled in*. Run `register_search` with `limit:1` (count-only, capturing `total_hits`) and write the result as an `{state:"incomplete", query, total_hits, fetched:0, sample:[], reason:"crowd descriptor — …"}` block. **Do NOT call `register_enumerate` on a saturation crowd, and do NOT call it clean.** The descriptor (count + why) is what crosses the firewall; the raw character-noise pile does not. Judgment reads the count and decides whether to command a narrower named enumeration inside it.
|
|
@@ -429,7 +429,7 @@ Step 5 names none.
|
|
|
429
429
|
1. Start with the client's goods and services wording: the order's wording if it has one, otherwise the product description in the company profile.
|
|
430
430
|
2. Add the words other filings use for the same goods that the client's wording does not already contain.
|
|
431
431
|
|
|
432
|
-
Single words or short phrases as a specification would write them, no wildcards, at most 24. Each word is matched against the goods and services description of registered marks, so use words a specification would contain. A word broader than the goods widens the search instead of narrowing it. The register cannot read the words and, or, not, adj or near inside an item. An item containing one is searched without it. Omit the key when the matter
|
|
432
|
+
Single words or short phrases as a specification would write them, no wildcards, at most 24. Each word is matched against the goods and services description of registered marks, so use words a specification would contain. A word broader than the goods widens the search instead of narrowing it. The register cannot read the words and, or, not, adj or near inside an item. An item containing one is searched without it. Send an empty list when you have considered the goods and no word is worth narrowing by. Omit the key only when the matter states no goods at all — the two are different answers and the run records which one you gave.
|
|
433
433
|
|
|
434
434
|
### Step 6 — Cross-mark themes (once, after all marks processed)
|
|
435
435
|
|
|
@@ -69,8 +69,10 @@ You do not write a file. Hand the frame back by calling `record_matter_frame`; t
|
|
|
69
69
|
- Online platforms / retail / direct sales / partner programmes / developer portals
|
|
70
70
|
- Geographic distribution patterns
|
|
71
71
|
|
|
72
|
-
###
|
|
73
|
-
|
|
72
|
+
### Which classes to search
|
|
73
|
+
|
|
74
|
+
Start from the classes the order names. Then look at the client's own goods and business as the order and the company profile describe them. If they plainly reach a class the order did not name, add it, and write one sentence saying why (for example: the franchise sells video game accessories, so class 28). Add a class only for the client's own goods, never for what a competitor might hold. Never remove a class the order named. The added class is searched like every other: the identical mark first, and the count looked at before anything is read.
|
|
75
|
+
|
|
74
76
|
- Name the classes scoped IN (with a one-line reason each) and any deliberately scoped OUT. Wrong class scope produces a false-clean, so be deliberate — this is a recall lever, not a formality.
|
|
75
77
|
|
|
76
78
|
### Scope jurisdictions — search wide, cite narrow (instructed scope honored; worldwide / brand-signalled scope leans wide + discloses)
|
package/driver/stages.mjs
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { join, basename } from "node:path";
|
|
18
18
|
import { driverRel } from "../shared/driver-dir.mjs"; //
|
|
19
|
+
import { awaitsReadingTurn } from "../providers/_shared/plan-guards.mjs"; // — a guard the dictation misreads is a question the model never knows it may ask
|
|
19
20
|
import { validators } from "./verify.mjs";
|
|
20
21
|
import { REGISTER_PROVIDER, PROVIDERS } from "./driver.config.mjs";
|
|
21
22
|
import { REGISTER_AXES, decideAxes } from "./coverage-ledger.mjs";
|
|
@@ -1359,7 +1360,7 @@ export const STAGES = {
|
|
|
1359
1360
|
// EACH FIELD CARRIES ITS OWN IMPERATIVE IN ITS OWN SENTENCE (: a field phrased outside one was
|
|
1360
1361
|
// written 0 of 9 times against 74 of 74 when imperative-carried).
|
|
1361
1362
|
`Hand the frame back by calling the \`record_matter_frame\` tool. Send \`prose_body\` — the commercial read of the matter in full prose: client, sector, product description, customer base, channels of trade, off-field sectors, sector-convergence flags, watchlist-owner seeds, your scope reasoning, the class scope and adjacency call with a one-line reason per class, the applicant's own and affiliated marks, and the campaign shape where you are inferring one (label an inference as an inference).`,
|
|
1362
|
-
`Send \`scope_basis\` as "instructed" or "derived", with \`scope_jurisdictions\` and \`excluded_jurisdictions\` as arrays of territories. Send \`identified_classes\` — the Nice classes you judge NECESSARY that the request did NOT instruct, each as {class, reason}, the class a whole number 1-45 and the reason one line.
|
|
1363
|
+
`Send \`scope_basis\` as "instructed" or "derived", with \`scope_jurisdictions\` and \`excluded_jurisdictions\` as arrays of territories. Send \`identified_classes\` — the Nice classes you judge NECESSARY that the request did NOT instruct, each as {class, reason}, the class a whole number 1-45 and the reason one line. Add a class only for the client's own goods, never for what a competitor might hold, and give one sentence saying why — a class sent without a reason is not searched. The added class is searched like every other: the identical mark first, and the count looked at before anything is read. Omit the field or send an empty array where the instructed classes are the whole scope, which is the ordinary answer and adds nothing.`,
|
|
1363
1364
|
// The driver STAMPS the instructed-scope section from _driver/instructed-scope.json, so the seat is
|
|
1364
1365
|
// not asked to quote back values the driver wrote at intake. That retyping was the stage's
|
|
1365
1366
|
// `frame_scope_missing` loop and it is gone; see matter-frame-record.mjs.
|
|
@@ -1410,7 +1411,7 @@ export const STAGES = {
|
|
|
1410
1411
|
contractElements: {
|
|
1411
1412
|
"The prose manifest's Request / Elements / Variants tables and Watchlists section — the same terms already in variant-manifest.json": {
|
|
1412
1413
|
class: "mechanical:code-rendered", tokens: ["too_short", "missing"],
|
|
1413
|
-
why: "variant-manifest.json holds mark, dominant_element, elements[], variants[], incumbent_classes[], watchlist_owners[]; the prose tables restate exactly those. Both tokens police the PROSE copy (needs /variant/i + /\\|/ = a markdown table exists)",
|
|
1414
|
+
why: "variant-manifest.json holds mark, dominant_element, elements[], variants[], incumbent_classes[], watchlist_owners[], goods_words[]; the prose tables restate exactly those. Both tokens police the PROSE copy (needs /variant/i + /\\|/ = a markdown table exists)",
|
|
1414
1415
|
},
|
|
1415
1416
|
"mark — \"<the mark verbatim>\"": {
|
|
1416
1417
|
class: "mechanical:pre-bound", tokens: ["variantmodel_mark_missing"],
|
|
@@ -1588,6 +1589,12 @@ export const STAGES = {
|
|
|
1588
1589
|
`Hand the manifest back by calling the \`record_clearance_variants\` tool. Send \`mark\` verbatim, \`dominant_element\`, and \`elements\` — one \`{value, kind}\` per token, kind from the closed set distinctive | common | saturated-common.`,
|
|
1589
1590
|
`Send \`variants\` — one \`{value, category, rationale, romanization}\` per search term, category from the closed set the skill names, and \`romanization\` on every non-Latin value and only on those.`,
|
|
1590
1591
|
`Send \`incumbent_classes\` and \`watchlist_owners\` where Step 5 names them, as arrays; omit or send empty where it does not.`,
|
|
1592
|
+
// ITS OWN SENTENCE, BECAUSE A FIELD WITH NO IMPERATIVE IS A FIELD NOBODY FILLS. A production run
|
|
1593
|
+
// proved it: the manual carried the instruction, the order's goods wording was in the dispatch,
|
|
1594
|
+
// the model discussed the goods eighteen times in its prose — and handed back no goods words at
|
|
1595
|
+
// all, because nothing in what it hands back had a slot for them. The manual asked; the contract
|
|
1596
|
+
// did not.
|
|
1597
|
+
`Send \`goods_words\` — the words the register search is narrowed to, from the matter's own goods and services wording plus the words other filings use for the same goods. Single words or short phrases as a specification would write them, no wildcards, at most 24. Send an empty list when you have considered the goods and no word is worth narrowing by. Omit it only when the matter states no goods at all: this is what stops a crowded sweep returning more filings than anyone can read, so leaving it out costs the search its narrowing.`,
|
|
1591
1598
|
// THE SCOPE LEDGER STOPS BEING A TABLE THE DRIVER RE-READS. It used to be dictated as markdown in
|
|
1592
1599
|
// the skill doc and recovered by parsing those columns back out of the prose (renderScopeLedgerJson
|
|
1593
1600
|
// over variant-manifest.md). The rows arrive typed now and the driver renders the table AND
|
|
@@ -2188,7 +2195,7 @@ export const STAGES = {
|
|
|
2188
2195
|
// message for transparency and so judgment knows what is already covered.
|
|
2189
2196
|
`EXECUTE THE FROZEN PLAN VIA THE TOOL: call register_execute_plan ONCE with {"plan_path": "${P.registerPlan}", "axis": "${axis}", "output_path": "${P.registerBand(axis)}"}. The tool runs every dictated entry below ITSELF (paged enumerates; count-only crowd descriptors; a "when"-guarded fringe only if its parent enumerated) and WRITES the band file itself with each block's qid stamped. Do NOT run these dictated entries manually and do NOT write their blocks yourself.`,
|
|
2190
2197
|
`For your audit context, the dictated entries the tool will run:`,
|
|
2191
|
-
...planEntries.map((e) => `- qid "${e.qid}": ${e.predicate} ${e.terms ? `names ${JSON.stringify(e.terms)} (one OR-stacked call)` : JSON.stringify(e.term)}${e.owner ? ` · owner ${JSON.stringify(e.owner)}` : ""} · nice_classes ${JSON.stringify(e.nice_classes)}${e.regions?.length ? ` · regions ${JSON.stringify(e.regions)}` : ""}${e.when ? ` · when: "${e.when.runs_if_enumerated}" enumerated` : ""} · expected: ${e.expected_kind}${Array.isArray(e.covered_by) && e.covered_by.length ? ` · crowd context — coverage is ${e.covered_by.join(", ")}` : ""}`),
|
|
2198
|
+
...planEntries.map((e) => `- qid "${e.qid}": ${e.predicate} ${e.terms ? `names ${JSON.stringify(e.terms)} (one OR-stacked call)` : JSON.stringify(e.term)}${e.owner ? ` · owner ${JSON.stringify(e.owner)}` : ""} · nice_classes ${JSON.stringify(e.nice_classes)}${e.regions?.length ? ` · regions ${JSON.stringify(e.regions)}` : ""}${e.when ? (awaitsReadingTurn(e.when) ? ` · WAITING FOR YOU: not asked unless you ask for it after reading the identical mark's list` : ` · when: "${e.when.runs_if_enumerated}" enumerated`) : ""} · expected: ${e.expected_kind}${Array.isArray(e.covered_by) && e.covered_by.length ? ` · crowd context — coverage is ${e.covered_by.join(", ")}` : ""}`),
|
|
2192
2199
|
// copper-lattice re-route (supplemental_lane contract): judgment additions stay the model's
|
|
2193
2200
|
// CALL — which queries the manifest/frame warrant beyond the dictated set — but their
|
|
2194
2201
|
// EXECUTION and their band blocks are code's (register_propose_supplemental mints qid'd
|
|
@@ -2263,7 +2270,7 @@ export const STAGES = {
|
|
|
2263
2270
|
// is the quiet one: the note says nine enumerated queries, the band holds eight, and nothing compares
|
|
2264
2271
|
// them. Deriving removes the disagreement rather than detecting it.
|
|
2265
2272
|
`FILE THIS AXIS'S AUDIT NOTE WITH \`record_unit_note\`. THE DISPATCH NAMES NO PATH FOR IT, deliberately — the driver writes this axis's note from what you send and you never open it, so there is no path here for you to hold. THE COUNTS ARE NOT YOURS TO TYPE: queries enumerated, incomplete blocks and records carried forward are taken from the band, so the note and the band cannot disagree. Send only what the band cannot say — \`null_result\` if this axis genuinely found nothing (refused against a band that carries records), and \`note\`, ONE short observation an auditor would want, in a lawyer's words. Still NO coverage-limited/confirmed-clean/deferred rows and NO clearance verdict: those are judgment's, Layer B. Call it AFTER the band exists — a note over a band that has not been written is refused by name, because an account of a sweep that has not happened is not a short note, it is a wrong one.`,
|
|
2266
|
-
CROSS_CHECK_HANDOFF,
|
|
2273
|
+
CROSS_CHECK_HANDOFF, "WHERE THE PLAN ABOVE HAS WAITING FAMILIES, RECORD EVERY ONE YOU DO NOT ASK with `record_withheld_families`: its qids, as listed above, and why it was not asked. A family you leave unasked was never searched, so it is recorded withheld-by-judgment; one nobody judged holds up delivery. The reason goes into the audit workbook, not the report.", // withheld-families.mjs
|
|
2267
2274
|
// THE CLOSING LINE SPLITS WITH THE LANE, because what the seat owes splits with it. Under the
|
|
2268
2275
|
// supplemental-lane contract the seat writes NOTHING — the band is the tools' and the note is the
|
|
2269
2276
|
// driver's — so the dispatch names no file and ends the way conversion 9's reviewer does. With the
|
|
@@ -2446,7 +2453,7 @@ export const STAGES = {
|
|
|
2446
2453
|
// E1 — what this stage asks a model for, and what discharges each element. See THE STAGE-
|
|
2447
2454
|
// CONTRACT DECLARATION above STAGES for the enum and the rules; contract-audit.mjs enforces them.
|
|
2448
2455
|
contractElements: {
|
|
2449
|
-
"coverage form `status` per row — EXACTLY one bare token of confirmed-clean / coverage-limited / deferred": {
|
|
2456
|
+
"coverage form `status` per row — EXACTLY one bare token of confirmed-clean / coverage-limited / deferred / withheld-by-judgment": {
|
|
2450
2457
|
class: "judgment", tokens: ["coverage_no_status", "coverage_clean_unexecuted", "coverage_clean_skipped", "coverage_clean_tainted"],
|
|
2451
2458
|
why: "'Does this un-enumerated slice matter to whether I can sign' — the sufficiency call the funnel is forbidden to make (clearance-register SKILL.md, `## Coverage = the band blocks`). The obligations are driver-computed with every identifier; the status is a VALUE the seat sends through record_coverage (typed transport), validated per row at call time. #850 keeps it J.",
|
|
2452
2459
|
},
|
|
@@ -80,7 +80,7 @@ const slimRun = (r) => ({
|
|
|
80
80
|
markName: r.markName, ref: r.ref, classes: r.classes,
|
|
81
81
|
state: r.state, stepN: r.stepN, stepTotal: r.stepTotal, stepLabel: r.stepLabel,
|
|
82
82
|
lastStage: r.lastStage, // spec 64 C — the RAW stage key ("register-unit:primary-sweep"): what the run is actually doing
|
|
83
|
-
|
|
83
|
+
tier: r.tier, statement: r.statement, caption: r.caption, // the rating, never the reviewer's sign-off; spec 64 — THE one risk statement (absent on legacy runs); caption — the report's own conclusion
|
|
84
84
|
url: r.url, failedStage: r.failedStage, reason: r.reason,
|
|
85
85
|
resetsAt: r.resetsAt, // rate-limit POSTPONE ONLY: when the cap window clears + the run auto-resumes (ISO)
|
|
86
86
|
recoveryResumesAt: r.recoveryResumesAt ?? null, // recovery park's backoff clock (A4 split — never conflated with a provider cap)
|
|
@@ -100,7 +100,7 @@ export function buildRecentActivity({ reports = [], limit = 12 } = {}) {
|
|
|
100
100
|
// spec 64 — a delivered run's outcome is THE one risk statement when the run carries it (band +
|
|
101
101
|
// stance in one sentence), never a bare disposition word beside a severity word on another page.
|
|
102
102
|
out.push({ kind: "report", ok, when: r.deliveredAt || r.updatedAt || r.startedAt || "", label: r.markName || r.slug || r.runId,
|
|
103
|
-
outcome: ok ? (r.statement || r.
|
|
103
|
+
outcome: ok ? (r.caption || r.statement || r.tier || "delivered") : `failed${r.failedStage ? " · " + r.failedStage : ""}`,
|
|
104
104
|
url: r.url || (ok && r.runId ? `${r.runId}/report.html` : "") });
|
|
105
105
|
}
|
|
106
106
|
// ONE explicit time key per row: a report's `when` can come from any of three stamps, and they all sort on
|