clearotron 0.4.0 → 0.4.1-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/THIRD-PARTY-NOTICES.md +2 -2
- package/bin/README.md +1 -1
- package/bin/clearotron.mjs +2 -0
- package/bin/mcp.mjs +80 -0
- package/bin/start.mjs +14 -1
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/docs/SECURITY-OWASP.md +21 -13
- package/docs/SECURITY.md +20 -14
- package/docs/architecture/04-configuration-reference.md +3 -3
- package/docs/architecture/05-config-governance.md +1 -1
- package/docs/architecture/06-operations-runbook.md +5 -4
- package/docs/architecture/08-development-guide.md +3 -1
- package/docs/architecture/09-security-and-data.md +16 -11
- package/driver/CHANGELOG.md +29 -0
- package/driver/binding-layers.mjs +28 -6
- package/driver/citation-census.json +3 -3
- package/driver/common-law-coverage-status.mjs +52 -0
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/compare.mjs +1 -1
- package/driver/connotation-search.mjs +5 -5
- package/driver/contract-audit.mjs +6 -6
- package/driver/contract-dictation-registry.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +18 -17
- package/driver/contract-vocabulary.mjs +145 -132
- package/driver/declination-call.mjs +5 -1
- package/driver/degraded-parts.mjs +10 -3
- package/driver/dev-portal.mjs +2 -1
- package/driver/dispatch-record.mjs +19 -0
- package/driver/doubt-closure-tool.mjs +2 -2
- package/driver/driver.config.mjs +39 -6
- package/driver/engagement-receipt.mjs +1 -1
- package/driver/engine/CONTRACT.md +41 -1
- package/driver/engine/anthropic-agent.mjs +56 -4
- package/driver/engine/mcp/codex-config.mjs +8 -3
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +32 -5
- package/driver/engine/mcp/stdio-server.mjs +45 -7
- package/driver/engine/mcp/supplemental.mjs +26 -1
- package/driver/engine/openai-agent.mjs +113 -20
- package/driver/engine/session-record.mjs +223 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/findings-model.mjs +4 -4
- package/driver/flag-snapshot.mjs +2 -1
- package/driver/form-neighbourhood.mjs +7 -7
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/framework.mjs +22 -0
- package/driver/gateway.mjs +96 -14
- package/driver/jx.mjs +1 -1
- package/driver/knockout-review-record.mjs +55 -1
- package/driver/named-band.mjs +4 -0
- package/driver/owner-screen.mjs +7 -4
- package/driver/package.json +1 -1
- package/driver/phase0.mjs +13 -2
- package/driver/pipeline-knockout.mjs +15 -13
- package/driver/pipeline.mjs +132 -48
- package/driver/portal-families.mjs +3 -3
- package/driver/portal-service.mjs +6 -5
- package/driver/predelivery-lint.mjs +19 -19
- package/driver/products.mjs +1 -1
- package/driver/profiles.mjs +4 -3
- package/driver/publish/index.mjs +1 -1
- package/driver/publish/knockout.mjs +5 -4
- package/driver/publish/office-record-links.mjs +17 -2
- package/driver/publish/publish-inputs.mjs +3 -3
- package/driver/publish/render-knockout.mjs +17 -5
- package/driver/publish/render.mjs +4 -2
- package/driver/publish/xlsx.mjs +1 -1
- package/driver/record-fold.mjs +177 -0
- package/driver/register-count.mjs +20 -3
- package/driver/register-coverage.mjs +1 -1
- package/driver/register-plan.mjs +83 -18
- package/driver/register-records.mjs +7 -0
- package/driver/registry-fidelity.mjs +2 -2
- package/driver/repairs.mjs +1 -1
- package/driver/report-card-record.mjs +4 -4
- package/driver/result-noun-fields.mjs +3 -3
- package/driver/scope-facts.mjs +11 -3
- package/driver/scope-rules.mjs +2 -2
- package/driver/search-policy.mjs +20 -1
- package/driver/skills/clearance-common-law/SKILL.md +22 -22
- package/driver/skills/clearance-register/digest.md +3 -3
- package/driver/skills/clearance-register/register-recipes.md +11 -11
- package/driver/skills/clearance-register/status-rules.md +1 -1
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
- package/driver/skills/clearance-search/phase2-execution.md +6 -5
- package/driver/skills/clearance-search/synthesis-rules.md +4 -4
- package/driver/skills/clearance-search/worked-examples.md +39 -39
- package/driver/skills/clearance-variants/SKILL.md +16 -16
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/stage-context.mjs +36 -1
- package/driver/stages-knockout.mjs +6 -5
- package/driver/stages.mjs +30 -30
- package/driver/suite-census.json +259 -55
- package/driver/synthesis-record.mjs +5 -0
- package/driver/unit-inventory.mjs +17 -28
- package/driver/verify.mjs +7 -7
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/CONNECT.md +9 -3
- package/mcp-server/demo-mode.mjs +10 -0
- package/mcp-server/lib/evidence.mjs +8 -8
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/lib/runs.mjs +7 -1
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/package.json +2 -2
- package/mcp-server/packs/README.md +1 -1
- package/mcp-server/server.mjs +16 -2
- package/package.json +4 -3
- package/portal-ui/package.json +3 -3
- package/providers/_shared/count.mjs +9 -2
- package/providers/_shared/enumerate.mjs +125 -41
- package/providers/_shared/execute-plan.mjs +15 -4
- package/providers/corsearch/src/capabilities.js +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +2 -2
- package/providers/signa/src/capabilities.js +32 -2
- package/providers/signa/src/core.js +262 -36
- package/scripts/band-shape-probe.mjs +2 -2
- package/scripts/deprecate-below.mjs +28 -4
- package/scripts/e2e-first-time.mjs +17 -2
- package/scripts/env-audit.mjs +3 -2
- package/scripts/freeze-example-run.mjs +10 -10
- package/scripts/mcp-registry-listing.mjs +199 -0
- package/scripts/mint-plugin-folder.mjs +65 -0
- package/scripts/release-version.mjs +44 -0
- package/scripts/score.mjs +1 -1
- package/shared/register-floor.mjs +19 -0
- package/shared/scope.mjs +18 -2
- package/shared/trigger-lane.mjs +1 -1
- package/skills/clearotron-ops/SKILL.md +81 -24
|
@@ -52,6 +52,7 @@ import { findCoverageRecommendations } from "./verify.mjs";
|
|
|
52
52
|
import { declinationCallPaths, readDeclinations } from "./declination-tool.mjs"; // — the seat's own declines, read by the driver never asserted by the seat
|
|
53
53
|
import { reconcileDeclinationDuty, declinationDutyRefusal } from "./declination-duty.mjs";
|
|
54
54
|
import { findingUris } from "./record-carry.mjs"; // one derivation of "which records did the findings name", called not copied
|
|
55
|
+
import { foldAfterSave } from "./record-fold.mjs"; // once the delivery seam is passed, the saved record is folded again
|
|
55
56
|
|
|
56
57
|
export const NARRATIVE_FILE = "narrative.md";
|
|
57
58
|
export const FINDINGS_FILE = "findings.json";
|
|
@@ -670,6 +671,10 @@ export function recordSynthesis(runDir, received, opts = {}) {
|
|
|
670
671
|
captured: closeCapture({ ok: true }), capture_failed: captureFailed,
|
|
671
672
|
};
|
|
672
673
|
}
|
|
674
|
+
// ONCE THE DELIVERY SEAM IS PASSED, THE RECORD IS FOLDED AGAIN ON ACCEPTANCE (design, 2026-10-03). The
|
|
675
|
+
// call records above stay the model's own, the base for its next patch; findings.json becomes the folded
|
|
676
|
+
// record the reviewer, the cards and the delivery read. Before the seam this leaves the record as written.
|
|
677
|
+
foldAfterSave(dir0, findingsAt);
|
|
673
678
|
|
|
674
679
|
return {
|
|
675
680
|
written: [findingsAt, narrativeAt], refused: null,
|
|
@@ -159,7 +159,7 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
159
159
|
// stamped and says where its verdict lives, and driver/updater-identity.mjs makes that verdict from
|
|
160
160
|
// the digests the updater records about itself.
|
|
161
161
|
attribution: "stamp",
|
|
162
|
-
measured: "2026-09-08
|
|
162
|
+
measured: "2026-09-08. The service's own state says nothing about the timer: it is `inactive` between runs and `inactive` when the timer has been stopped. The timer's ActiveState says the timer itself is still up. It is NOT a next elapse and does not promise one: `active` means UP, never SCHEDULED. Proving something will still fire takes a real next elapse, which `systemctl list-timers --all` supplies and `show -p ActiveState` cannot. Measured in testing 2026-09-09: a timer reading `ActiveState=active` whose `OnCalendar` names a date already past, so `list-timers` reports its next fire as null and it will never run again.",
|
|
163
163
|
tracked: ["clearotron-deploy.service", "clearotron-deploy.timer"],
|
|
164
164
|
note: "the install's own updater.",
|
|
165
165
|
// IT WAS AN ORPHAN, AND THE CONDITION IT NAMED HAS BEEN MET. This entry carried an `orphanReason`
|
|
@@ -212,8 +212,7 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
212
212
|
// prose references in driver/systemd/render-units.mjs also describe this file as though it were in
|
|
213
213
|
// the tree; they are stale today either way.
|
|
214
214
|
unit: "profile-service", runsOn: [],
|
|
215
|
-
measured: "2026-09-08,
|
|
216
|
-
+ "no unit contents were read.",
|
|
215
|
+
measured: "2026-09-08",
|
|
217
216
|
tracked: null,
|
|
218
217
|
untrackedReason: "the repository has never carried this file — `git log --all` on the path is empty. "
|
|
219
218
|
+ "It was described as deploying from a copy held on the deployment itself, in the same "
|
|
@@ -243,11 +242,8 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
243
242
|
// REPORTED, never a fault, for exactly this case. It becomes `["prod"]` in the change that DEPLOYS
|
|
244
243
|
// it, against a fresh enumeration, which is the ratchet working rather than being edited around.
|
|
245
244
|
unit: "clearotron-portal", runsOn: ["test"], tracked: ["clearotron-portal.service"],
|
|
246
|
-
measured: "2026-09-07
|
|
247
|
-
note: "
|
|
248
|
-
+ "and the MCP face are the two children bin/start.mjs supervises, and neither ran under systemd, "
|
|
249
|
-
+ "so a hosted deployment kept them alive with a hand launcher and `clearotron start` had nothing "
|
|
250
|
-
+ "to refuse in favour of.",
|
|
245
|
+
measured: "2026-09-07",
|
|
246
|
+
note: "The portal had NO unit at all — it and the MCP face are the two children bin/start.mjs supervises, and neither ran under systemd.",
|
|
251
247
|
orphanReason: "THE WEB PORTAL, SHIPPED WITH NO BOX CARRYING IT YET — the same KIND of orphan as "
|
|
252
248
|
+ "clearotron-deploy (never installed), a different unit and a different install path. It is not "
|
|
253
249
|
+ "courtlistener-mcp, which runs on production under another name, and not feedback-mint, which "
|
|
@@ -260,9 +256,8 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
260
256
|
// Same as clearotron-portal above: shipped, running nowhere, declared as an orphan rather than
|
|
261
257
|
// asserting a production state no enumeration has ever shown.
|
|
262
258
|
unit: "clearotron-mcp-face", runsOn: ["test"], tracked: ["clearotron-mcp-face.service"],
|
|
263
|
-
measured: "2026-09-07
|
|
264
|
-
note: "
|
|
265
|
-
+ "MCP; the other of the two processes that had no unit.",
|
|
259
|
+
measured: "2026-09-07",
|
|
260
|
+
note: "The engine door the portal calls over MCP; the other of the two processes that had no unit.",
|
|
266
261
|
orphanReason: "THE ENGINE DOOR THE PORTAL CALLS, shipped with no box carrying it yet. Same kind as "
|
|
267
262
|
+ "clearotron-portal beside it and as clearotron-deploy — never installed — and distinct from "
|
|
268
263
|
+ "both other kinds in this list. It arrives with the PRODUCTION REBUILD and then `systemctl "
|
|
@@ -289,7 +284,7 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
289
284
|
// door beside it: it is in SERVER_INSTALL_SET, and it leaves this list on the day a measured
|
|
290
285
|
// enumeration of a box shows it running.
|
|
291
286
|
unit: "clearotron-client-mcp", runsOn: ["test"], tracked: ["clearotron-client-mcp.service"],
|
|
292
|
-
measured: "2026-09-07
|
|
287
|
+
measured: "2026-09-07",
|
|
293
288
|
note: "INSTALLED WITH THE PRODUCT since 2026-09-03. The client door "
|
|
294
289
|
+ "— the one surface that accepts an account-scoped key, and a separate process from the engine "
|
|
295
290
|
+ "door, which refuses one outright.",
|
|
@@ -307,9 +302,8 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
307
302
|
// Written as part of retiring the path-watcher posture, because retiring `prelim-driver.*` without
|
|
308
303
|
// it leaves a hosted box healthy-looking and draining nothing.
|
|
309
304
|
unit: "clearotron-worker", runsOn: ["test"], tracked: ["clearotron-worker.service"],
|
|
310
|
-
measured: "2026-09-07
|
|
311
|
-
note: "
|
|
312
|
-
+ "directly (owner ruling 2026-08-31). Ships tracked and runs nowhere yet.",
|
|
305
|
+
measured: "2026-09-07",
|
|
306
|
+
note: "The drain, as one plain service unit invoking the entrypoint directly.",
|
|
313
307
|
orphanReason: "AWAITING A BOX, not an orphan by design. It is the hosted posture's drainer, and the "
|
|
314
308
|
+ "hosted posture is an opt-in step no install performs. It must NOT be "
|
|
315
309
|
+ "started beside `clearotron start`, which supervises its own worker — the two postures are "
|
|
@@ -320,7 +314,7 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
320
314
|
orphanReason: "THE PRE-RENAME PORTAL. Production runs `clearotron-portal`; this name is "
|
|
321
315
|
+ "what that service used to be called, and the 2026-09-08 reading finds no unit of this name under "
|
|
322
316
|
+ "any account. It ran on production until the rename and the entry was never re-measured after it.",
|
|
323
|
-
measured: "2026-09-08
|
|
317
|
+
measured: "2026-09-08", tracked: null,
|
|
324
318
|
untrackedReason: "the deployed copy carries real Cloudflare Access team/AUD/domain values inline. "
|
|
325
319
|
+ "A tracked file would be the placeholder TEMPLATE, merged by hand after a "
|
|
326
320
|
+ "diff — writing one carelessly replaces working auth with placeholders that look configured.",
|
|
@@ -335,7 +329,7 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
335
329
|
orphanReason: "THE PRE-RENAME OPERATOR DOOR. Production runs `clearotron-mcp-face`; the "
|
|
336
330
|
+ "2026-09-08 reading finds no unit of this name under any account. Same rename as the portal it "
|
|
337
331
|
+ "serves, and the same claim carried across it unmeasured.",
|
|
338
|
-
measured: "2026-09-08
|
|
332
|
+
measured: "2026-09-08", tracked: null,
|
|
339
333
|
untrackedReason: "same CF Access inline-values shape as the portal it serves.",
|
|
340
334
|
},
|
|
341
335
|
{
|
|
@@ -343,7 +337,7 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
343
337
|
orphanReason: "THE PRE-RENAME CLIENT DOOR. `clearotron-client-mcp` runs on both the production and "
|
|
344
338
|
+ "the test deployments; the 2026-09-08 reading finds no unit of THIS name under any account. The "
|
|
345
339
|
+ "tracked template stays because the repository still ships it — see the note.",
|
|
346
|
-
measured: "2026-09-08
|
|
340
|
+
measured: "2026-09-08", tracked: ["client-mcp.service"],
|
|
347
341
|
note: "TRACKED, in mcp-server/remote/ — this entry said it had no file, and the file was there. It is "
|
|
348
342
|
+ "a banner-marked TEMPLATE, so the live copy differing from it is the arrangement, not drift. "
|
|
349
343
|
+ "It was reported as shipping no unit; a unit did ship, in mcp-server/remote/ rather than "
|
|
@@ -353,10 +347,7 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
353
347
|
},
|
|
354
348
|
{
|
|
355
349
|
unit: "client-mcp-apikey", runsOn: ["test"],
|
|
356
|
-
measured: "2026-09-08,
|
|
357
|
-
+ "`clearotron-client-mcp`, and INACTIVE — enabled with no running process. Absent from "
|
|
358
|
-
+ "production. The entry declared production and the reading puts it on the other deployment "
|
|
359
|
-
+ "— the claim was not stale, it named the wrong machine.",
|
|
350
|
+
measured: "2026-09-08",
|
|
360
351
|
tracked: ["client-mcp-apikey.service"],
|
|
361
352
|
// THE SEPARATE QUESTION, ANSWERED. The line above used to end by saying that why it runs beside the
|
|
362
353
|
// renamed door was a question for somebody else. It is a survivor of the rename, not a posture
|
|
@@ -396,7 +387,7 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
396
387
|
orphanReason: "A PRE-RENAME NAME OF THE RETIRED DEPLOYMENT, and the one of them whose tracked file "
|
|
397
388
|
+ "is GENERIC rather than a template — so the file is ready to be compared against a live copy the "
|
|
398
389
|
+ "day something runs it, and nothing does. Read as absent under every account on 2026-09-08.",
|
|
399
|
-
measured: "2026-09-08
|
|
390
|
+
measured: "2026-09-08", tracked: ["trademark-artifacts-http.service"],
|
|
400
391
|
note: "TRACKED, in mcp-server/remote/. GENERIC, not a template: it carries no banner and no "
|
|
401
392
|
+ "placeholder, and defers its CF Access values to the EnvironmentFile — so its live copy is "
|
|
402
393
|
+ "expected to MATCH the tracked file, and a difference is real drift.",
|
|
@@ -453,8 +444,7 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
453
444
|
orphanReason: "NOT THIS REPOSITORY'S SERVICE, and no longer running under this name: production holds it "
|
|
454
445
|
+ "only under a retired-suffixed filename, a suffix systemd never reads. "
|
|
455
446
|
+ "Declared here so a unit sharing a machine with ours is not invisible, and now declared as gone.",
|
|
456
|
-
measured: "2026-09-08,
|
|
457
|
-
+ "does not load. No loadable unit of this name.", tracked: null,
|
|
447
|
+
measured: "2026-09-08", tracked: null,
|
|
458
448
|
untrackedReason: "it is not this repo's service. Its ExecStart runs a script from a different "
|
|
459
449
|
+ "product's checkout on the same box, not under this clone — so there is no code here to "
|
|
460
450
|
+ "template, and the old reason (CF Access values inline) would send the next reader hunting for a "
|
|
@@ -519,8 +509,7 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
519
509
|
+ "flag was ever captured, so stopping it dropped nothing. The decision owed, if any, is whether the "
|
|
520
510
|
+ "feedback machinery is removed rather than parked; driver/run-activity.mjs still carries a note "
|
|
521
511
|
+ "addressed to whoever picks that up, and it is still unclaimed.",
|
|
522
|
-
note: "STOPPED against an input nothing writes to any more
|
|
523
|
-
+ "and it was disabled the same evening.",
|
|
512
|
+
note: "STOPPED against an input nothing writes to any more.",
|
|
524
513
|
},
|
|
525
514
|
// ── ANOTHER PRODUCT'S UNITS ARE NOT DECLARED HERE, AND THAT IS THE RULE (ruling 594, 2026-09-27) ──
|
|
526
515
|
//
|
|
@@ -544,7 +533,7 @@ export const UNIT_INVENTORY = Object.freeze([
|
|
|
544
533
|
// deployments. Not here. This file ships.
|
|
545
534
|
{
|
|
546
535
|
unit: "trademark-test-deploy", runsOn: [],
|
|
547
|
-
measured: "2026-09-07
|
|
536
|
+
measured: "2026-09-07. The hourly deploy runs as clearotron-deploy.timer, which has its own entry; this name is what that one used to be called.", tracked: null,
|
|
548
537
|
// AN EMPTY runsOn OWES A REASON, and this one is a supersession rather than an orphan: the hourly
|
|
549
538
|
// deploy still runs on the test deployment, under the name `clearotron-deploy`, which has its own entry
|
|
550
539
|
// its own measurement. This name is what that unit used to be called. It is kept rather than deleted
|
package/driver/verify.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { matterFrameWasRecorded, frameRatifiedForms } from "./matter-frame-recor
|
|
|
16
16
|
import { findConnotationViolations, parsePrRiskResults, prRiskPopulation,
|
|
17
17
|
CONNOTATION_UNMATCHED_MARK, CONNOTATION_NO_RESEMBLANCE_MARK, MEANING_ANGLES_RE,
|
|
18
18
|
parseDispositionForm, CONNOTATION_UNRULED_REASONS, queryKey } from "./connotation-search.mjs";
|
|
19
|
-
import { formSidecarName, formSidecarPath } from "./disposition-union.mjs";
|
|
19
|
+
import { formSidecarName, formSidecarPath } from "./disposition-union.mjs";
|
|
20
20
|
// B — the transport's own four failure states. The audit reads the run's records; this file locates them.
|
|
21
21
|
import { auditDispositionCalls, CALL_FAILURE_REASONS } from "./disposition-call-audit.mjs";
|
|
22
22
|
import { callRecordPaths } from "./disposition-tool.mjs";
|
|
@@ -37,7 +37,7 @@ import { registerPlanCallKilled } from "./tool-calls.mjs"; // — did the dict
|
|
|
37
37
|
import { parseFindingsJson, parseFindingsJsonLenient, CLIENT_TIER_BY_COMPOSITE, isUnconditionalProceed, joinFindingToBlock, parseBlockOrd } from "./findings-model.mjs";
|
|
38
38
|
import { parsePlacementsJson } from "./placement-model.mjs"; import { placementFormSidecarName, parsePlacementForm, placementRenderAccount } from "./placement-form.mjs";
|
|
39
39
|
import { parseCaseLawLedger, findCaseLawLedgerViolations, caseLawLedgerFail } from "./case-law-ledger.mjs";
|
|
40
|
-
import { parseFrameworkManifest,
|
|
40
|
+
import { parseFrameworkManifest, materialBand, normalizeBand } from "./framework.mjs"; import { readFrozenMethod, FROZEN_METHOD_FILE } from "./framework-method.mjs";
|
|
41
41
|
import { parseNamedBand, findCollapsedBands } from "./named-band.mjs";
|
|
42
42
|
import { parseBlindFrameModel } from "./blind-frame-model.mjs";
|
|
43
43
|
import { parseFrameDiff } from "./frame-diff-model.mjs";
|
|
@@ -158,7 +158,7 @@ function nonEmpty(content, min = MIN) {
|
|
|
158
158
|
* true for only one lane, and put back. Appending keeps every existing matcher matching — they all test
|
|
159
159
|
* substrings — and gives the seat the one word it was missing.
|
|
160
160
|
*
|
|
161
|
-
* It also sharpens `noChange` (
|
|
161
|
+
* It also sharpens `noChange` (in runStageLadder() in gateway.mjs, "this attempt's token equals the previous attempt's"):
|
|
162
162
|
* a file that starts failing a DIFFERENT member now reads as changed instead of as a stalled retry.
|
|
163
163
|
*
|
|
164
164
|
* `names` is positional against `markers` and optional; a call site that omits it emits exactly the token
|
|
@@ -681,7 +681,7 @@ function commonLawStructural(c, p = null) {
|
|
|
681
681
|
needsSection(c, "coverage-ledger", [/coverage[\s-]ledger/i], "coverage-ledger"),
|
|
682
682
|
needsSection(c, "audit-trail", [/audit[\s-]trail/i], "audit-trail"),
|
|
683
683
|
needs(c, [/\|/], "platform matrix"),
|
|
684
|
-
|
|
684
|
+
// no status check here (ruling 2026-10-01): a ledger with no status word and no record passes, and code records what it states — recordStatusesFromFindings() in common-law-coverage-status.mjs
|
|
685
685
|
);
|
|
686
686
|
}
|
|
687
687
|
|
|
@@ -1203,9 +1203,9 @@ function checkFindingsSibling(p, c) {
|
|
|
1203
1203
|
const v4 = (parsed.schemaVersion ?? 1) >= 4;
|
|
1204
1204
|
if (v4 && !fw.manifest) return fail("framework_manifest_missing_for_v4: schema_version 4 findings need the frozen _driver/framework.json to judge band words");
|
|
1205
1205
|
const v3 = (parsed.schemaVersion ?? 1) >= 3; // gates key on schema_version — v2/archived runs and replay never flip
|
|
1206
|
-
// "material finding" — the line the receipt gates key on: composite>=3 on the legacy scale; on v4,
|
|
1207
|
-
//
|
|
1208
|
-
const material = (f) => v4 ? (f.band != null &&
|
|
1206
|
+
// "material finding" — the line the receipt gates key on: composite>=3 on the legacy scale; on v4, banded on
|
|
1207
|
+
// a rung whose own tone is medium risk or higher (materialBand — by tone, never by position on the ladder).
|
|
1208
|
+
const material = (f) => v4 ? (f.band != null && materialBand(fw.manifest, f.band)) : f.composite >= 3;
|
|
1209
1209
|
for (const f of parsed.findings) {
|
|
1210
1210
|
if (material(f) && f.use_check != null && !String(f.use_check.source ?? "").trim())
|
|
1211
1211
|
return fail(`finding_use_check_source_missing:${f.ordinal}`);
|
package/mcp-server/CHANGELOG.md
CHANGED
package/mcp-server/CONNECT.md
CHANGED
|
@@ -21,9 +21,9 @@ It reads run directories off your disk. So:
|
|
|
21
21
|
|
|
22
22
|
- **It shows you runs you have already produced.** `CLEAROTRON_WORK_DIR` is where the engine writes
|
|
23
23
|
them, and that is the one variable the server needs.
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
- **With no install, it shows you the demo.** `npx clearotron mcp` starts this server from the npm package.
|
|
25
|
+
On a machine with no install it answers from the four sample runs `npx clearotron demo` replays, and every
|
|
26
|
+
row says it is the demo. On a machine with an install it reads that install's runs.
|
|
27
27
|
- **Nothing here spends money on its own,** with two exceptions that are gated and named: `start_run`
|
|
28
28
|
enqueues a real clearance, and `what_if_run` re-runs one pipeline stage. Both are refused by default on
|
|
29
29
|
a sensible allowlist — see "Hold two tools back" below.
|
|
@@ -51,6 +51,12 @@ claude mcp add trademark-artifacts --scope user \
|
|
|
51
51
|
|
|
52
52
|
Check it: `claude mcp list` prints `trademark-artifacts: … - √ Connected`.
|
|
53
53
|
|
|
54
|
+
With no clone, start it from the npm package instead, naming the version:
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
claude mcp add trademark-artifacts --scope user -- npx -y clearotron@<version> mcp
|
|
58
|
+
```
|
|
59
|
+
|
|
54
60
|
`--scope user` makes it available in every project, not only the directory you ran it in. In Windows
|
|
55
61
|
PowerShell 5.1, write the separator as `"--"`: that shell drops a bare `--`. Remove it with
|
|
56
62
|
`claude mcp remove trademark-artifacts`.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// demo-mode.mjs — whether this server is serving the demo's sample runs.
|
|
5
|
+
//
|
|
6
|
+
// `clearotron mcp` decides it and the server reads it, in one process: bin/mcp.mjs sets it before it
|
|
7
|
+
// loads the server, and the server reads it as it answers. It is a module and not an environment
|
|
8
|
+
// variable because nothing outside this process sets it or reads it, and every environment variable the
|
|
9
|
+
// product reads is a setting an operator is told about.
|
|
10
|
+
export const demoMode = { on: false };
|
|
@@ -294,7 +294,7 @@ const MATCH_SHAPES = [
|
|
|
294
294
|
// `*` wildcard, and the term extractor must see the word, not the markup.
|
|
295
295
|
const deMarkup = (s) => String(s ?? "").replace(/\*\*|__/g, "");
|
|
296
296
|
|
|
297
|
-
// Record COUNTS ride along in parentheses — `
|
|
297
|
+
// Record COUNTS ride along in parentheses — `HAVEN (424)`, `ヘイヴン (34) / 헤이븐 (20)`. They are
|
|
298
298
|
// process exhaust rather than the query, and dropping them first is what lets the rest of the string be
|
|
299
299
|
// recognised as the bare term it is.
|
|
300
300
|
const deCount = (s) => String(s ?? "").replace(/\s*\(\s*~?\s*[\d,]+\s*\)/g, "");
|
|
@@ -302,26 +302,26 @@ const deCount = (s) => String(s ?? "").replace(/\s*\(\s*~?\s*[\d,]+\s*\)/g, "");
|
|
|
302
302
|
// A searched term is recognised three ways, in order, and NOT recognised otherwise:
|
|
303
303
|
// 1. explicitly quoted — `exact "Drivers Haven" (default)`
|
|
304
304
|
// 2. the whole string, when it carries no query scaffolding at all. This is how the common-law sweep
|
|
305
|
-
// writes them and it is the bulk of the corpus:
|
|
306
|
-
// `
|
|
305
|
+
// writes them and it is the bulk of the corpus: `드라이버스 헤이븐`, `车手港湾`, `DRIVERZ`,
|
|
306
|
+
// `HAVEN WORLD`. Those scripts are caseless, so a capitals rule cannot see them at all.
|
|
307
307
|
// "No scaffolding" is checked positively: no match-shape keyword, no residual parenthetical, and
|
|
308
308
|
// none of the structural punctuation the audit uses for anything OTHER than a query (a record path,
|
|
309
309
|
// a bracketed class list, an em-dash clause, a key: value tail).
|
|
310
|
-
// 3. an ALL-CAPS token — `exact
|
|
310
|
+
// 3. an ALL-CAPS token — `exact HAVEN`, `*HAVEN / fuzzy`. Two-letter tokens (ZH, JP, DE) are
|
|
311
311
|
// language/jurisdiction codes, not marks, and the register offices above are offices, not marks.
|
|
312
312
|
const SCAFFOLD = /[[\]`:—]|\/mark\//;
|
|
313
313
|
|
|
314
314
|
// "Does this read as a NAME?" — the property that separates a searched mark from a description of a
|
|
315
315
|
// search. Every alphabetic token must start upper-case or be in a caseless script (CJK, Hangul, Kana,
|
|
316
|
-
// Arabic); a token that is entirely lower-case Latin is prose. So `
|
|
317
|
-
// `Вензи` and
|
|
316
|
+
// Arabic); a token that is entirely lower-case Latin is prose. So `Driver's Haven`, `HAVEN WORLD`,
|
|
317
|
+
// `Вензи` and `드라이버스 헤이븐` qualify and `national register`, `formative phrases` and
|
|
318
318
|
// `one-swap lead-noun neighbours` do not. A property, not a list of banned words — a description nobody
|
|
319
319
|
// anticipated still fails it.
|
|
320
320
|
function looksLikeName(s) {
|
|
321
321
|
const tokens = String(s).split(/\s+/).filter((t) => /\p{L}/u.test(t));
|
|
322
322
|
if (!tokens.length || tokens.length > 5) return false;
|
|
323
323
|
// A lone two-letter capital is a language or jurisdiction code (ZH, DE, JP), never a mark. Caseless
|
|
324
|
-
// scripts are exempt from the length floor —
|
|
324
|
+
// scripts are exempt from the length floor — 港湾 is a whole word in two characters.
|
|
325
325
|
const cased = tokens.filter((t) => /\p{Lu}|\p{Ll}/u.test(t));
|
|
326
326
|
if (cased.length && cased.every((t) => t.replace(/[^\p{L}]/gu, "").length <= 2)) return false;
|
|
327
327
|
return tokens.every((t) => !/^\p{Ll}/u.test(t.replace(/^[^\p{L}\p{N}]+/u, "")));
|
|
@@ -361,7 +361,7 @@ function extractTerm(raw) {
|
|
|
361
361
|
const asClasses = (nums) => [...new Set(nums.map(Number).filter((n) => Number.isInteger(n) && n >= 1 && n <= 45))];
|
|
362
362
|
|
|
363
363
|
// In a SEARCH TERM, classes are read only where they are LABELLED ("cl 9/28/41/42", "cl.25", "class 35") —
|
|
364
|
-
// never from a bare parenthesised number, which in this corpus is a record COUNT ("
|
|
364
|
+
// never from a bare parenthesised number, which in this corpus is a record COUNT ("HAVEN (424)").
|
|
365
365
|
function extractClasses(s) {
|
|
366
366
|
const m = String(s ?? "").match(/\bcl(?:ass(?:es)?)?\.?\s*([\d]{1,2}(?:\s*[/,]\s*\d{1,2})*)/i);
|
|
367
367
|
return m ? asClasses(m[1].split(/[/,]/)) : [];
|
|
@@ -388,7 +388,7 @@ export function traceKnockout(run, target, events = []) {
|
|
|
388
388
|
kind: "verdict",
|
|
389
389
|
verdict: doc?.batch?.overall ?? runVerdictFromEvents(events) ?? null,
|
|
390
390
|
marks: marksOf(doc).map((m) => ({ mark: m.name, band: m.rating ?? null, basis: m.basis ?? null })),
|
|
391
|
-
note: "A Knockout verdict is the
|
|
391
|
+
note: "A Knockout verdict is the highest rating across the batch's marks; each mark's own rating and the "
|
|
392
392
|
+ "one-sentence ground for it are listed here.",
|
|
393
393
|
});
|
|
394
394
|
|
package/mcp-server/lib/runs.mjs
CHANGED
|
@@ -58,7 +58,13 @@ export function tierFromRecord(runDir) {
|
|
|
58
58
|
} catch { return null; }
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export function unreadableRunsReason({ workSet, workRoot, workExists, poolSet }) {
|
|
61
|
+
export function unreadableRunsReason({ workSet, workRoot, workExists, poolSet, workReadError = null }) {
|
|
62
|
+
// A work folder that was named and is not there, or cannot be read, is said, never listed as empty.
|
|
63
|
+
if (workSet && workReadError) {
|
|
64
|
+
return `no searches can be read here: CLEAROTRON_WORK_DIR is ${workRoot}, which `
|
|
65
|
+
+ (workReadError === "ENOENT" ? "does not exist. " : `cannot be read (${workReadError}). `)
|
|
66
|
+
+ "`npx clearotron doctor` prints where an install keeps them.";
|
|
67
|
+
}
|
|
62
68
|
if (workSet || workExists || poolSet) return null;
|
|
63
69
|
return `no searches can be read here: CLEAROTRON_WORK_DIR is unset and ${workRoot} does not exist, `
|
|
64
70
|
+ "and CLEAROTRON_REPORTS_DIR is unset. Set them to the install's directories — `npx clearotron doctor` "
|
package/mcp-server/lib/scrub.mjs
CHANGED
|
@@ -158,7 +158,7 @@ export function scrubMarkdown(text) {
|
|
|
158
158
|
// at least announced itself as internal.
|
|
159
159
|
//
|
|
160
160
|
// The ruling needed no new product decision, because the product had already made it:
|
|
161
|
-
//
|
|
161
|
+
// clearanceReportData() in report-data.mjs (`const live`) filters to live findings, "a withdrawn finding renders nowhere — it does
|
|
162
162
|
// not exist here either". Two client surfaces, one question, two answers. So the block is DROPPED, and
|
|
163
163
|
// the two agree by construction rather than key by key — which is the thing asked not to repeat.
|
|
164
164
|
//
|
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1-beta.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"test:full": "npm run test"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
16
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
17
17
|
"jose": "^6.2.3"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
@@ -37,7 +37,7 @@ saying the same thing.
|
|
|
37
37
|
|
|
38
38
|
The `SKILL.md` (and for ops, `COURIER.md`) is portable prompt text: paste it into Claude project
|
|
39
39
|
instructions, a ChatGPT GPT/system prompt, or any agent's system context — or, in Claude Code, install
|
|
40
|
-
the whole thing at once
|
|
40
|
+
the whole thing at once as the plugin in `plugin/`, which carries copies of the three skills.
|
|
41
41
|
`CONNECT.md` is for the human doing the wiring. `manifest.json` names the pack version, the files that
|
|
42
42
|
stayed here, and the `skill.directory` its prompt text moved to, so an integrator can pin + diff
|
|
43
43
|
upgrades.
|
package/mcp-server/server.mjs
CHANGED
|
@@ -26,13 +26,14 @@ import {
|
|
|
26
26
|
ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema,
|
|
27
27
|
ListPromptsRequestSchema, GetPromptRequestSchema,
|
|
28
28
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
29
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
29
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
30
30
|
import { join, basename } from "node:path";
|
|
31
31
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
32
32
|
import { fileURLToPath } from "node:url";
|
|
33
33
|
|
|
34
34
|
import { enumerateRuns, resolveRun, runAccountKey, runOrganisation, runProfileFacts, productIdentityFor, unreadableRunsReason } from "./lib/runs.mjs";
|
|
35
35
|
import { ORDERABLE_PRODUCTS } from "../driver/search-policy.mjs";
|
|
36
|
+
import { demoMode } from "./demo-mode.mjs";
|
|
36
37
|
import { PRODUCTS } from "../driver/products.mjs";
|
|
37
38
|
|
|
38
39
|
// The offering writes its own schema prose. A hand-typed "up to 20 names" beside a wall that refuses at
|
|
@@ -178,6 +179,17 @@ function getStages(runDir) {
|
|
|
178
179
|
return { stages, failover };
|
|
179
180
|
}
|
|
180
181
|
|
|
182
|
+
// THE DEMO SAYS IT IS THE DEMO, on every row, in the demo's own two sentences (bin/example.mjs prints both
|
|
183
|
+
// before anything else). `clearotron mcp` serves the demo's sample runs where a machine has no install,
|
|
184
|
+
// and an assistant reading a row must not take a sample for one of the person's own searches.
|
|
185
|
+
export const DEMO_LABEL = "Real engine output for the fictional mark VENQORI, captured against Clarivate Compumark. "
|
|
186
|
+
+ "It is an example, not advice.";
|
|
187
|
+
|
|
188
|
+
/** The code a folder's read fails with, or null where it reads. */
|
|
189
|
+
function readError(dir) {
|
|
190
|
+
try { readdirSync(dir); return null; } catch (e) { return e?.code ?? "unreadable"; }
|
|
191
|
+
}
|
|
192
|
+
|
|
181
193
|
function runSummary(run) {
|
|
182
194
|
const s = run.status ?? {};
|
|
183
195
|
// WHO THE SEARCH WAS FOR, on every row. A session that holds several clients was handed eight rows
|
|
@@ -188,6 +200,7 @@ function runSummary(run) {
|
|
|
188
200
|
// field is what made the list unanswerable in the first place.
|
|
189
201
|
const facts = runProfileFacts(run);
|
|
190
202
|
return {
|
|
203
|
+
...(demoMode.on ? { demo: DEMO_LABEL } : {}),
|
|
191
204
|
runId: run.runId, slug: run.slug, codename: run.codename, date: run.date, agent: run.agent,
|
|
192
205
|
client: facts.known
|
|
193
206
|
? { key: facts.account, name: facts.clientName }
|
|
@@ -272,6 +285,7 @@ const tools = {
|
|
|
272
285
|
const unreadable = unreadableRunsReason({
|
|
273
286
|
workSet: !!config.envValue("CLEAROTRON_WORK_DIR"), workRoot: config.workspaceRoot,
|
|
274
287
|
workExists: existsSync(config.workspaceRoot), poolSet: !!config.poolRootOrNull,
|
|
288
|
+
workReadError: readError(config.workspaceRoot),
|
|
275
289
|
});
|
|
276
290
|
if (unreadable) throw new Error(unreadable);
|
|
277
291
|
let out = enumerateRuns({ agent, state, slug, mark }).map(runSummary);
|
|
@@ -1102,7 +1116,7 @@ export function makeServer({ scope = null, local = null, readonly = false } = {}
|
|
|
1102
1116
|
// pack was previously a file no connecting client ever saw. Guidance only; never a substitute for the
|
|
1103
1117
|
// authorize()/scrub gates. Staff and ops get no instructions (see lib/instructions.mjs).
|
|
1104
1118
|
const server = new Server(
|
|
1105
|
-
{ name: NS, version:
|
|
1119
|
+
{ name: NS, version: productIdentity().version ?? "unknown" }, // the package's own version, the one server_info reports
|
|
1106
1120
|
// `prompts` is declared for every kind; promptsFor() returns [] for the audiences that get none, so
|
|
1107
1121
|
// an ops client sees an empty list rather than a protocol error on a capability we did not announce.
|
|
1108
1122
|
{ capabilities: { tools: {}, resources: {}, prompts: {} }, instructions: instructionsFor(eff) },
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clearotron",
|
|
3
|
+
"mcpName": "io.github.CordilleraSarl/clearotron",
|
|
3
4
|
"type": "module",
|
|
4
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.1-beta.1",
|
|
5
6
|
"license": "AGPL-3.0-only",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
"sync:uspto": "node bin/uspto-sync.mjs",
|
|
59
60
|
"bench:uspto": "node providers/uspto-local/bench/bench.mjs",
|
|
60
61
|
"verify:uspto": "node providers/uspto-local/bin/verify-index.mjs",
|
|
61
|
-
"lint:driver": "eslint --no-error-on-unmatched-pattern --max-warnings 0 --config eslint.driver.config.mjs \"driver/**/*.mjs\" \"bin/**/*.mjs\" \"shared/**/*.mjs\" \"mcp-server/**/*.mjs\" \"scripts/**/*.mjs\" \"cut/**/*.mjs\"",
|
|
62
|
+
"lint:driver": "eslint --no-error-on-unmatched-pattern --max-warnings 0 --config eslint.driver.config.mjs \"driver/**/*.mjs\" \"bin/**/*.mjs\" \"shared/**/*.mjs\" \"mcp-server/**/*.mjs\" \"scripts/**/*.mjs\" \"cut/**/*.mjs\" \"plugin/**/*.mjs\"",
|
|
62
63
|
"doctrine-report": "node scripts/doctrine-report.mjs",
|
|
63
64
|
"notices": "node scripts/third-party-notices.mjs",
|
|
64
65
|
"notices:check": "node scripts/third-party-notices.mjs --check",
|
|
@@ -73,7 +74,7 @@
|
|
|
73
74
|
"globals": "^17.11.0"
|
|
74
75
|
},
|
|
75
76
|
"dependencies": {
|
|
76
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
77
|
+
"@modelcontextprotocol/sdk": "^1.31.0",
|
|
77
78
|
"buffers": "file:vendor/buffers",
|
|
78
79
|
"exceljs": "^4.4.0",
|
|
79
80
|
"jose": "^6.2.3",
|
package/portal-ui/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "portal-ui",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.1-beta.1",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
|
|
8
8
|
"engines": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@types/react": "^19.3.0",
|
|
24
24
|
"@types/react-dom": "^19.3.0",
|
|
25
|
-
"@vitejs/plugin-react": "^6.1.
|
|
25
|
+
"@vitejs/plugin-react": "^6.1.2",
|
|
26
26
|
"typescript": "^7.0.2",
|
|
27
|
-
"vite": "^8.3.
|
|
27
|
+
"vite": "^8.3.3"
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
import { guardCountCall, guardToolCall } from "./transport-guard.mjs";
|
|
49
49
|
import { clipProviderText } from "./provider-text.mjs"; // — keep the discriminator
|
|
50
|
+
import { floorOf } from "../../shared/register-floor.mjs";
|
|
50
51
|
|
|
51
52
|
export const parseToolText = (r) => { try { return JSON.parse(r?.text ?? ""); } catch { return null; } };
|
|
52
53
|
export const isToolError = (r) => !!(r?.isError) || (typeof r?.text === "string" && r.text.startsWith("ERROR"));
|
|
@@ -109,9 +110,15 @@ export function makeCountProbe(deps) {
|
|
|
109
110
|
// ("a provider that cannot count must never read as zero") broken from the inside, by its own
|
|
110
111
|
// comment. The number now has to BE a number; nothing else is an answer.
|
|
111
112
|
if (!Number.isFinite(parsed.total_hits)) {
|
|
112
|
-
|
|
113
|
+
// A register's floor ("at least 10,000") is still no count, so the probe stays not-ok; the figure
|
|
114
|
+
// rides beside it for a caller that can carry one (enumerate.mjs's rescues: a crowd, never an error).
|
|
115
|
+
const floor = floorOf(parsed);
|
|
116
|
+
return { ok: false, total: null, probe: "cheap", ...(floor !== null ? { floor } : {}),
|
|
113
117
|
reason: "the search response carried no usable total_hits, so nothing was counted — the number is UNKNOWN, which is not the same as none" };
|
|
114
118
|
}
|
|
115
|
-
|
|
119
|
+
// What the total counts rides with it when the register said (a provider's `total_counts`, see
|
|
120
|
+
// enumerate.mjs): a per-class or per-spelling count keeps the register's number and says so.
|
|
121
|
+
return { ok: true, total: parsed.total_hits, probe: "cheap", reason: null,
|
|
122
|
+
...(typeof parsed.total_counts === "string" ? { total_counts: parsed.total_counts } : {}) };
|
|
116
123
|
};
|
|
117
124
|
}
|