clearotron 0.4.0-beta.1 → 0.4.0-beta.2
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/INSTALL.md +4 -31
- package/README.md +5 -5
- package/bin/brandowner.mjs +1 -1
- package/bin/onboard.mjs +18 -74
- package/bin/start.mjs +58 -14
- package/bin/update.mjs +4 -1
- package/build-info.json +2 -2
- package/demo/full-country-search/run/_driver/search-policy.json +1 -1
- package/demo/full-country-search/run/status.json +1 -1
- package/demo/global-preliminary-search/run/_driver/search-policy.json +1 -1
- package/demo/global-preliminary-search/run/status.json +1 -1
- package/demo/knockout-search/run/_driver/search-policy.json +1 -1
- package/demo/knockout-search/run/status.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/search-policy.json +1 -1
- package/demo/multi-country-focus-search/run/status.json +1 -1
- package/docs/README.md +1 -0
- package/docs/SECURITY-OWASP.md +298 -0
- package/docs/SECURITY.md +18 -13
- package/docs/architecture/02-architecture.md +5 -5
- package/docs/architecture/03-run-lifecycle.md +28 -29
- package/docs/architecture/04-configuration-reference.md +3 -4
- package/docs/architecture/05-config-governance.md +5 -7
- package/docs/architecture/06-operations-runbook.md +0 -12
- package/docs/architecture/07-quality-and-audit.md +12 -13
- package/docs/architecture/09-security-and-data.md +9 -4
- package/driver/CHANGELOG.md +60 -0
- package/driver/ask-ledger.mjs +2 -0
- package/driver/authority-trees.mjs +90 -8
- package/driver/band-shape.mjs +5 -5
- package/driver/case-law-ledger.mjs +19 -3
- package/driver/claim-liveness.mjs +30 -1
- package/driver/clearance-variants-record.mjs +10 -3
- package/driver/common-law-receipts.mjs +62 -12
- package/driver/commonlaw-carry.mjs +1 -1
- package/driver/compare.mjs +2 -2
- package/driver/connotation-search.mjs +30 -101
- package/driver/contract-arm2-baseline.json +6 -5
- package/driver/contract-e3-backlog.mjs +60 -60
- package/driver/contract-vocabulary.mjs +38 -31
- package/driver/coverage-form-io.mjs +13 -1
- package/driver/coverage-form.mjs +42 -4
- package/driver/coverage-ledger.mjs +3 -5
- package/driver/cross-check-wait.mjs +43 -0
- package/driver/declination-call.mjs +33 -9
- package/driver/declination-tool.mjs +27 -15
- package/driver/deferral-row.mjs +139 -0
- package/driver/degraded-parts.mjs +377 -0
- package/driver/deliver-trigger.sh +1 -1
- package/driver/demo-container.mjs +4 -1
- package/driver/dev-portal.mjs +4 -4
- package/driver/dispatch-record.mjs +2 -2
- package/driver/driver.config.mjs +177 -87
- package/driver/effective-scope.mjs +58 -2
- package/driver/engine/anthropic-agent.mjs +103 -27
- package/driver/engine/auth.mjs +2 -2
- package/driver/engine/child-record.mjs +11 -0
- package/driver/engine/cli-version.mjs +4 -3
- package/driver/engine/common.mjs +17 -2
- package/driver/engine/deny-authority-write.mjs +3 -1
- package/driver/engine/engine-env.mjs +174 -0
- package/driver/engine/engine-spawn.mjs +179 -0
- package/driver/engine/mcp/band-server.mjs +1 -1
- package/driver/engine/mcp/codex-config.mjs +112 -3
- package/driver/engine/mcp/declination-server.mjs +9 -8
- package/driver/engine/mcp/dispositions-server.mjs +2 -1
- package/driver/engine/mcp/fetch-server.mjs +14 -11
- package/driver/engine/mcp/gather-config.mjs +22 -10
- package/driver/engine/mcp/perplexity-server.mjs +96 -16
- package/driver/engine/mcp/probe-server.mjs +43 -21
- package/driver/engine/mcp/public-fetch.mjs +195 -0
- package/driver/engine/mcp/recording-server.mjs +78 -13
- package/driver/engine/mcp/supplemental.mjs +13 -13
- package/driver/engine/mcp/unit-note-server.mjs +50 -1
- package/driver/engine/openai-agent.mjs +109 -28
- package/driver/engine/probe.mjs +152 -19
- package/driver/enqueue-schema.mjs +3 -3
- package/driver/findings-model.mjs +64 -13
- package/driver/floor-duty.mjs +21 -5
- package/driver/form-neighbourhood.mjs +5 -2
- package/driver/framework-method.mjs +304 -0
- package/driver/gateway.mjs +44 -16
- package/driver/hand-off-exits.mjs +129 -0
- package/driver/jx.mjs +8 -5
- package/driver/knockout-assess-record.mjs +7 -1
- package/driver/knockout-frame-record.mjs +96 -34
- package/driver/knockout-review-record.mjs +7 -2
- package/driver/matter-frame-record.mjs +31 -2
- package/driver/methodology-witness.mjs +6 -3
- package/driver/named-band.mjs +1 -1
- package/driver/owner-use-check.mjs +45 -10
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +4 -0
- package/driver/phase0.mjs +2 -2
- package/driver/pipeline-knockout.mjs +128 -35
- package/driver/pipeline.mjs +373 -598
- package/driver/placement-carry.mjs +20 -1
- package/driver/placement-form-io.mjs +18 -4
- package/driver/placement-form.mjs +52 -3
- package/driver/placement-union.mjs +32 -5
- package/driver/portal-report.mjs +3 -3
- package/driver/portal-service.mjs +4 -3
- package/driver/portal-static.mjs +11 -18
- package/driver/profile-page.html +5 -5
- package/driver/profiles.mjs +5 -5
- package/driver/progress.mjs +5 -3
- package/driver/provider-usage.mjs +1 -1
- package/driver/publish/index.mjs +94 -22
- package/driver/publish/knockout.mjs +133 -12
- package/driver/publish/profiles-page.mjs +2 -0
- package/driver/publish/publish-inputs.mjs +10 -4
- package/driver/publish/render-knockout.mjs +47 -20
- package/driver/publish/render.mjs +12 -7
- package/driver/publish/report-data.mjs +2 -0
- package/driver/publish/search-depth.mjs +77 -4
- package/driver/publish/templates/report.css +1 -1
- package/driver/publish/xlsx.mjs +34 -15
- package/driver/reasoning-tripwires.mjs +2 -160
- package/driver/recall-receipt.mjs +27 -0
- package/driver/record-carry.mjs +8 -8
- package/driver/reference-score.mjs +142 -20
- package/driver/register-count.mjs +5 -3
- package/driver/register-digest-record.mjs +13 -0
- package/driver/register-plan.mjs +156 -75
- package/driver/register-records.mjs +27 -3
- package/driver/registration-scripts.mjs +22 -0
- package/driver/repair-composers.mjs +19 -3
- package/driver/repair-contract.mjs +1 -1
- package/driver/repairs.mjs +5 -2
- package/driver/reviewer-open-points.mjs +33 -0
- package/driver/rule-shape.mjs +8 -8
- package/driver/run-economics.mjs +2 -2
- package/driver/run-integrity.mjs +2 -2
- package/driver/runner.mjs +17 -5
- package/driver/scope-facts.mjs +89 -12
- package/driver/score-redaction.mjs +247 -0
- package/driver/search-policy.mjs +17 -0
- package/driver/skills/README.md +2 -2
- package/driver/skills/clearance-common-law/SKILL.md +29 -36
- package/driver/skills/clearance-common-law/perplexity-prompts.md +5 -27
- package/driver/skills/clearance-register/SKILL.md +15 -16
- package/driver/skills/clearance-register/digest.md +14 -30
- package/driver/skills/clearance-register/register-recipes.md +14 -56
- package/driver/skills/clearance-register/unit.md +36 -47
- package/driver/skills/clearance-search/SKILL.md +6 -8
- package/driver/skills/clearance-search/delivery-contract.md +0 -7
- package/driver/skills/clearance-search/firm-wide-reasoning.md +5 -4
- package/driver/skills/clearance-search/phase2-execution.md +5 -6
- package/driver/skills/clearance-search/risk-framework-triage.md +2 -2
- package/driver/skills/clearance-search/synthesis-rules.md +5 -5
- package/driver/skills/clearance-search/template-formatting.md +2 -2
- package/driver/skills/clearance-variants/SKILL.md +6 -6
- package/driver/skills/clearance-variants/transliteration-scripts.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +31 -1
- package/driver/skills/knockout-frame/SKILL.md +42 -10
- package/driver/skills/matter-frame/SKILL.md +12 -5
- package/driver/skills/narrative-refutation/SKILL.md +3 -1
- package/driver/skills/placement-inquiry/SKILL.md +4 -0
- package/driver/stage-context.mjs +23 -2
- package/driver/stages-knockout.mjs +92 -79
- package/driver/stages.mjs +88 -53
- package/driver/stray-artifacts.mjs +9 -5
- package/driver/suite-census.json +620 -140
- package/driver/synthesis-record.mjs +26 -4
- package/driver/systemd/README.md +5 -6
- package/driver/systemd/clearotron-worker.service +6 -1
- package/driver/tokens.mjs +2 -2
- package/driver/unit-inventory.mjs +21 -40
- package/driver/variant-manifest-model.mjs +21 -2
- package/driver/verify-knockout.mjs +163 -4
- package/driver/verify.mjs +40 -14
- package/driver/web-grid.mjs +150 -0
- package/driver/withheld-families.mjs +113 -4
- package/driver/worker-heartbeat.mjs +13 -2
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/lib/ops.mjs +10 -10
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/trace.mjs +3 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +2 -2
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-D_O_55vK.js → index-BPAUjcI0.js} +1 -1
- package/portal-ui/dist/assets/{index-5CCwiJG7.css → index-D2wrw9cH.css} +18 -2
- package/portal-ui/dist/assets/plus-jakarta-sans-BUCHxqJ-.woff2 +0 -0
- package/portal-ui/dist/index.html +2 -15
- package/portal-ui/package.json +1 -1
- package/providers/_shared/enumerate.mjs +155 -50
- package/providers/_shared/execute-plan.mjs +62 -16
- package/providers/_shared/ledger.mjs +1 -1
- package/providers/_shared/plan-guards.mjs +7 -0
- package/providers/clarivate/src/core.js +54 -6
- package/providers/corsearch/README.md +1 -2
- package/providers/corsearch/src/core.js +2 -2
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/README.md +8 -7
- package/providers/oauth-mcp-bridge/bridge.mjs +21 -5
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
- package/providers/oauth-mcp-bridge/warm-server.mjs +16 -5
- package/providers/perplexity/src/core.js +246 -21
- package/providers/signa/src/core.js +27 -25
- package/scripts/README.md +2 -1
- package/scripts/added-reference-check.mjs +2 -1
- package/scripts/backup-recall-stores.mjs +5 -5
- package/scripts/demo-evidence.mjs +2 -1
- package/scripts/drive-env-check.mjs +2 -1
- package/scripts/e2e-first-time.mjs +469 -0
- package/scripts/e2e-scenario-ops.mjs +441 -0
- package/scripts/e2e.mjs +104 -16
- package/scripts/env-audit.mjs +8 -0
- package/scripts/freeze-example-run.mjs +7 -7
- package/scripts/generated-files-are-current.mjs +2 -1
- package/scripts/hand-off-exits-probe.mjs +75 -0
- package/scripts/import-cycle-check.mjs +2 -1
- package/scripts/merge-shape-check.mjs +2 -1
- package/scripts/package-size-budget.mjs +2 -1
- package/scripts/record-carry-probe.mjs +1 -1
- package/scripts/release-install-check.mjs +4 -1
- package/scripts/release-note-required.mjs +44 -10
- package/scripts/render-check.mjs +5 -2
- package/scripts/report-offline-render-check.mjs +169 -0
- package/scripts/score.mjs +76 -5
- package/scripts/strip-titles-and-attributions.mjs +2 -1
- package/scripts/strip-tracker-citations.mjs +2 -1
- package/scripts/test-full.mjs +2 -1
- package/scripts/test-run.mjs +10 -3
- package/scripts/third-party-notices.mjs +3 -1
- package/scripts/travelling-predicates.mjs +1 -1
- package/scripts/writing-standard-check.mjs +2 -1
- package/shared/brand-fonts.mjs +59 -0
- package/shared/brand.mjs +5 -5
- package/shared/browser-temp-root.mjs +3 -2
- package/shared/doctrine-overlay.mjs +1 -1
- package/shared/driver-dir.mjs +27 -2
- package/shared/fonts/OFL-fira-code.txt +93 -0
- package/shared/fonts/OFL-plus-jakarta-sans.txt +93 -0
- package/shared/fonts/README.md +37 -0
- package/shared/fonts/fira-code.woff2 +0 -0
- package/shared/fonts/plus-jakarta-sans.woff2 +0 -0
- package/shared/npm-cli.mjs +23 -0
- package/shared/os-advice.mjs +14 -2
- package/shared/path-seps.mjs +42 -0
- package/shared/process-table.mjs +71 -3
- package/shared/reference-guard-classes.mjs +30 -6
- package/shared/root-doc-commands.mjs +6 -2
- package/shared/running-start.mjs +24 -7
- package/shared/scope.mjs +2 -2
- package/shared/wsl.mjs +1 -1
- package/driver/known-conflicts.mjs +0 -345
|
@@ -110,18 +110,6 @@ and the environment file holding the secrets.
|
|
|
110
110
|
- **A scheduled gateway restart** (chat-channel session and plugin-loader hygiene) should *defer*
|
|
111
111
|
off live work rather than skip: wait while a run is live, up to a bound, then restart anyway.
|
|
112
112
|
`XDG_RUNTIME_DIR` must be set for `systemctl --user` to work from cron.
|
|
113
|
-
- **Pin the agent id before upgrading an install made before 0.2.2.** The default agent id changed
|
|
114
|
-
from `clawdi` to `localagent`, and that id is a path segment: runs live under
|
|
115
|
-
`<workspaceRoot>/workspace-<agent>/studio/clearance-search/`. An install that never set one starts
|
|
116
|
-
reading an empty workspace, and empty reads as "no runs" rather than as an error. Set **both**
|
|
117
|
-
variables in the environment file — the gather servers read their own:
|
|
118
|
-
|
|
119
|
-
```
|
|
120
|
-
CLEAROTRON_DEFAULT_AGENT=clawdi
|
|
121
|
-
CLEAROTRON_GATHER_AGENT=clawdi
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
An install that already sets them is unaffected, and a fresh install needs neither.
|
|
125
113
|
- **Integrator-platform upgrades** go through that platform's own guarded upgrade path — dry-run,
|
|
126
114
|
exact-version confirm, stop → update → start. Never `npm i -g` / `pnpm add -g` directly.
|
|
127
115
|
|
|
@@ -92,9 +92,15 @@ under-grading ships a wrong CLEAR. The reviewer's context includes code-computed
|
|
|
92
92
|
and a mandatory plan-execution check. CONDITIONAL/BLOCKING triggers corrective re-synthesis, a
|
|
93
93
|
freshness gate proving the named corrections actually reached `findings.json`, and a warm verdict
|
|
94
94
|
re-check that can only fall back to the *entry* verdict. A terminal BLOCKING first gets the
|
|
95
|
-
degenerate-artifact refusal (a "BLOCKING" with zero cited defects is re-asked fresh, once),
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
degenerate-artifact refusal (a "BLOCKING" with zero cited defects is re-asked fresh, once), and
|
|
96
|
+
then the run **delivers**. Ruling 2026-08-26, verbatim: "Deliver always, with open points printed.
|
|
97
|
+
The refusal on a blocking review goes." The corrective ladder is still the fix arm and still runs
|
|
98
|
+
first; what changed is only what happens once it is exhausted. "Never ships" and "never ships
|
|
99
|
+
*looking finished*" were treated as one requirement and are two — only the second was ever
|
|
100
|
+
load-bearing, and it is met by the open-points section being code-built from the review. Ruling
|
|
101
|
+
2026-09-24 settled where that section goes: reviewer notes never reach the client page, and a
|
|
102
|
+
report the reviewer still refuses ships with its rating and nothing added, the points recorded
|
|
103
|
+
beside the review for the reviewing lawyer. The reviewer's verdict is treated as documented-noisy;
|
|
98
104
|
that is precisely why every *honesty* signal below derives from ledgers and receipts, not review
|
|
99
105
|
prose.
|
|
100
106
|
|
|
@@ -138,16 +144,10 @@ What makes the coverage statement trustworthy:
|
|
|
138
144
|
- **The clamps** (`applyCoverageFloor`) only ever raise CLEAR → CONDITIONAL, never lower, never
|
|
139
145
|
manufacture BLOCKING: live condition actions, the lawyer's own `coverage_judgment.sufficient:
|
|
140
146
|
false` (sufficiency is judgment's call — code only carries it), frame residuals, register gaps
|
|
141
|
-
(deferred rows ∪ taint axes
|
|
142
|
-
self-report), and in-window deadlines delivered without their dates. The verdict sidecar
|
|
147
|
+
(deferred rows ∪ taint axes — independent of the model's self-report). The verdict sidecar
|
|
143
148
|
(`_driver/verdict.json`) is then the single label authority for every surface; one shared
|
|
144
149
|
predicate keeps the validator and the auto-correct judging "unconditional proceed" with the same
|
|
145
150
|
eyes.
|
|
146
|
-
- **Recall memory**: confirmed conflicts persist per mark in a workspace store
|
|
147
|
-
(`_known-conflicts/`, rows are only added — never deleted or rewritten, save the machine-provenance
|
|
148
|
-
`terminal` a delivered run may upgrade — human edits always win, non-Latin marks handled); the next run
|
|
149
|
-
folds them in as deterministic plan probes, and a prior-confirmed conflict that neither resurfaces
|
|
150
|
-
nor gets a recorded justification is a *material regression* that clamps.
|
|
151
151
|
|
|
152
152
|
Register budgets deserve one honest sentence: the per-worker call budgets in the skill prose
|
|
153
153
|
(enumerates, phoneme ≤ 5, image ≤ 10) are **observed, prompt-level budgets, not code-enforced
|
|
@@ -194,9 +194,8 @@ pass over inherited artifacts is visible as such.
|
|
|
194
194
|
`reasoning-tripwires.mjs` is the mechanical net under principles held holistically in the skills:
|
|
195
195
|
recall floor (a live identical registration dropped in the noise rows but not carried), seed
|
|
196
196
|
neutrality (upstream artifacts must state facts, never grades), probative grading, matrix ceilings
|
|
197
|
-
(legacy scale only), status honesty (clean headline over material gaps), deadline urgency
|
|
198
|
-
|
|
199
|
-
recall regression. All of it lands in `_driver/reasoning-integrity.json` and surfaces on the audit
|
|
197
|
+
(legacy scale only), status honesty (clean headline over material gaps), deadline urgency,
|
|
198
|
+
unresolved disagreements, orphan register findings, uncross-checked demotions. All of it lands in `_driver/reasoning-integrity.json` and surfaces on the audit
|
|
200
199
|
workbook and quality pages — never as a banner on the rendered report.
|
|
201
200
|
Two of these pure functions *also* feed the verdict clamps (register gap, deadline gap) — but via
|
|
202
201
|
the clamp path on ledger facts, never as a tripwire "failure". `rule-shape.mjs` (rating decided by
|
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
> Part of the architecture pack (`docs/architecture/`). The driver's module tree and the headless
|
|
4
4
|
> integrator contract are in [`driver/README.md`](../../driver/README.md).
|
|
5
5
|
|
|
6
|
-
The security posture in
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
The security posture in brief: **nothing listens on the network except loopback services behind an
|
|
7
|
+
authenticated tunnel. A stage on Claude is offered no tool that runs a command. A stage on Codex runs its
|
|
8
|
+
commands inside Codex's own sandbox, or with the permissions of the account Clearotron runs as where a
|
|
9
|
+
machine sets the bypass. The process that runs everything else is ordinary code, not an agent. Each
|
|
10
|
+
stage's AI program starts with a named list of settings, and the key that signs access keys is handed
|
|
11
|
+
only to the portal and the two doors. On Claude, a stage's file tools can read nothing outside its run folder, its
|
|
12
|
+
instruction folders and any folder the machine's own Claude settings add. On Codex with its sandbox on,
|
|
13
|
+
a stage's commands can read nothing outside its run folder, its instruction folders, the temporary
|
|
14
|
+
folders, and the system and program files a command needs to run.**
|
|
10
15
|
|
|
11
16
|
## Trust boundaries
|
|
12
17
|
|
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.4.0-beta.2
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- New: A knockout search now also asks whether each name is already in use in the client's field, such as a game character or a drink.
|
|
8
|
+
- New: Searching by judgment.
|
|
9
|
+
|
|
10
|
+
This release changes how a clearance decides what to search. Until now the engine followed fixed rules: a set number of spellings, a fixed list of stores, a stop after the first identical mark. Fixed rules are fast and cheap, but they miss things. On a recent matter, a lawyer's review found marks the engine had counted but never read, and others it had raised that did not matter.
|
|
11
|
+
|
|
12
|
+
So the approach changes. Wherever the engine holds a pile of results, it now looks at what is there and decides what a lawyer would raise. It carries that forward and writes down what it set aside, and why. Nothing is dropped silently. Every wider search, every spelling set aside and every store left out appears in the audit workbook with its reason.
|
|
13
|
+
|
|
14
|
+
What you will notice:
|
|
15
|
+
- Searches widen where it could change the advice, and narrow where it cannot.
|
|
16
|
+
- Near spellings no buyer would confuse with the mark are set aside, with the reason recorded.
|
|
17
|
+
- Searches in other scripts run only in markets that file marks in that script.
|
|
18
|
+
- A large company's register is read for the marks that share the searched name. The rest is counted, not fetched.
|
|
19
|
+
- Marketplace searches cover the stores that sell the client's goods, and say which stores were left out and why.
|
|
20
|
+
- The report no longer opens with the internal reviewer's notes. They reach the reviewing lawyer separately.
|
|
21
|
+
- A large marketplace search no longer fails because its results were too big to return in one piece.
|
|
22
|
+
- Where a company's framework rates through named inputs, the report shows them beside each band.
|
|
23
|
+
|
|
24
|
+
A word on regressions. While testing this line we found two. A crowded search failed before delivering its report, and a knockout's ratings moved one step away from the lawyer's. Both were caught by running the same matters against a lawyer's answers, and both are fixed here. Every build is now tested that way before it ships. We are tuning for three things at once: a report that is right the first time, delivered fast, at a sensible cost. They pull against each other, and each release is our best current balance.
|
|
25
|
+
|
|
26
|
+
### Patch Changes
|
|
27
|
+
|
|
28
|
+
- For operators: on the Anthropic engine, a search can no longer read the install's settings or other private files.
|
|
29
|
+
- For operators: stages on the Anthropic engine can no longer run commands on the server; every tool that runs a command is removed from them.
|
|
30
|
+
- Fixed: In a crowded field, the search now reads the spellings a lawyer would raise, rather than the ones that fit under a record limit.
|
|
31
|
+
- Fixed: When a knockout's search for filings fails, the report and the audit workbook no longer show the raw error.
|
|
32
|
+
- Fixed: On Windows, a failed finding card no longer shows as covered in the audit workbook.
|
|
33
|
+
- Fixed: When a search could not be made, the report says so instead of giving a wrong reason.
|
|
34
|
+
- Fixed: A register search limited to the client's goods now searches every other office when one office cannot filter by goods.
|
|
35
|
+
- Fixed: a right the report keeps is no longer dropped because other registrations of the same right were ruled out.
|
|
36
|
+
- Fixed: A knockout search no longer drops a store listing or fan wiki page its web search found; the rating now reads every result.
|
|
37
|
+
- Fixed: A knockout's audit workbook also lists a part that failed as a whole, such as its filings listing or plain-language review. A filing shown without its link now says why.
|
|
38
|
+
- Fixed: on a Mac, `clearotron start` no longer offers to run in the background, which only works on Linux.
|
|
39
|
+
- Fixed: on a Mac, a search step could write into Clearotron's own protected folders by naming them in a different letter case. Those writes are now refused.
|
|
40
|
+
- Fixed: A register search that times out across many countries is asked again in smaller parts; one that still times out is reported as incomplete.
|
|
41
|
+
- Fixed: on the OpenAI engine, Codex's own sandbox no longer refuses the register searches a clearance runs.
|
|
42
|
+
- For operators: on the OpenAI engine with Codex's sandbox on, the commands a search runs can no longer read the install's settings or other private files.
|
|
43
|
+
- Fixed: the audit workbook's What was searched tab no longer shows a web or marketplace search that found similar listings as clean.
|
|
44
|
+
- Fixed: Worldwide searches are no longer narrowed to an account's default territories, and results too crowded to read in full are no longer reported clean.
|
|
45
|
+
- Fixed: A report no longer offers a native-language investigation to a client who ordered one, and says when that investigation was not completed.
|
|
46
|
+
- Fixed: When a knockout's lookup of a filing owner fails, its read now says the lookup did not answer, instead of saying it found nothing.
|
|
47
|
+
- New: Clearotron runs natively on Windows, from PowerShell: no WSL2, no Git and no administrator rights.
|
|
48
|
+
- Fixed: a full country search's report no longer says court decisions were searched when that search failed or its source was down.
|
|
49
|
+
- New: Global preliminary, multi-country and full country searches now search the web in more depth and keep more results from each search.
|
|
50
|
+
- Fixed: opening a report no longer contacts any font service; the report carries its typeface, now Plus Jakarta Sans, inside itself.
|
|
51
|
+
- Fixed: searches no longer reuse earlier results.
|
|
52
|
+
- Fixed: skipped searches are no longer shown as unfinished in the report.
|
|
53
|
+
- For operators: each stage's AI program now receives only the settings it needs, and never the key that signs access keys.
|
|
54
|
+
- Fixed: The audit workbook now lists, on Coverage & gaps, any part of a report that could not be completed on the run. A knockout's Audit Trail also lists an owner lookup that got no answer.
|
|
55
|
+
- Fixed: on the OpenAI engine, a Full country search now reads case law from CourtListener and Legal Data Hunter, not EUR-Lex alone.
|
|
56
|
+
- Fixed: setup, `clearotron doctor` and the start of every search now check that the engine can write a file where a search writes its results.
|
|
57
|
+
- New: The meaning and reputation search now asks the questions the matter's framing names, in the languages whose markets matter. Where the framing names none, the report says no meaning search ran.
|
|
58
|
+
- For operators: the old default agent name is gone from the product. An install upgrading from a version before 0.2.2 must set its own agent name to keep seeing its earlier runs.
|
|
59
|
+
- Fixed: The operator tool no longer accepts instructions, which never reached the run; a call sending them is refused.
|
|
60
|
+
- Fixed: on the OpenAI engine, the tool that reads web pages now refuses addresses on the server's own network, such as cloud metadata addresses.
|
|
61
|
+
- For operators: the background worker no longer receives the key that signs access keys. Run `clearotron start --background` once to update an installed worker.
|
|
62
|
+
|
|
3
63
|
## 0.4.0-beta.1
|
|
4
64
|
|
|
5
65
|
### Patch Changes
|
package/driver/ask-ledger.mjs
CHANGED
|
@@ -428,6 +428,8 @@ export function deriveAsks({
|
|
|
428
428
|
}
|
|
429
429
|
|
|
430
430
|
// 9 ── cross-checks: the common-law→register recovery nets (xcheck + recall) — directives carry
|
|
431
|
+
// (the recall receipt exists only on runs from before the recall store's removal, 2026-09-24;
|
|
432
|
+
// on a newer run `recall` is null and this family lists the cross-checks alone)
|
|
431
433
|
// qids (computed executed); over-cap rows are real unanswered questions (OPEN → closure/lawyer).
|
|
432
434
|
for (const [name, doc] of [["xcheck", xcheck], ["recall", recall]]) {
|
|
433
435
|
for (const d of doc?.directives ?? []) {
|
|
@@ -44,17 +44,97 @@
|
|
|
44
44
|
// set-GID scar on the pool root). The existing detect-and-journal sweep STAYS as corroboration; its
|
|
45
45
|
// never-kill-a-run property is sound and survives.
|
|
46
46
|
|
|
47
|
-
import {
|
|
47
|
+
import { realpathSync, statSync } from "node:fs";
|
|
48
|
+
import { resolve, win32, posix } from "node:path";
|
|
48
49
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
49
50
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
/**
|
|
52
|
+
* Is `child` inside `root` — by path segment, never by string prefix? PURE.
|
|
53
|
+
*
|
|
54
|
+
* ON WINDOWS BY WINDOWS RULES. There a path may use either separator and a name matches in any case, so
|
|
55
|
+
* `c:/users/x/SKILLS/a.md` is a file inside `C:\Users\x\skills`. Compared as Linux compares, the
|
|
56
|
+
* write lands in the protected tree and the boundary lets it through. `platform` is a parameter so that
|
|
57
|
+
* branch runs on a Linux CI. `foldCase` says whether to ignore letter case: always on Windows, and
|
|
58
|
+
* elsewhere as foldsCaseAt finds the protected folder's disk, which denyReason asks for each tree.
|
|
59
|
+
*/
|
|
60
|
+
export function isInside(root, child, { platform = process.platform, foldCase = platform === "win32" } = {}) {
|
|
61
|
+
const P = platform === "win32" ? win32 : posix;
|
|
62
|
+
const fold = foldCase ? (s) => s.toLowerCase() : (s) => s;
|
|
63
|
+
const r = fold(P.resolve(String(root ?? "")));
|
|
64
|
+
const c = fold(P.resolve(String(child ?? "")));
|
|
54
65
|
if (!r || r === "." || !c) return false;
|
|
55
66
|
// `/a/skills-backup` is NOT inside `/a/skills`. A `startsWith` without the separator says it is, and
|
|
56
67
|
// that is the classic form of this check being wrong in the direction that blocks real work.
|
|
57
|
-
return c === r || c.startsWith(r.endsWith(sep) ? r : r + sep);
|
|
68
|
+
return c === r || c.startsWith(r.endsWith(P.sep) ? r : r + P.sep);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The one spelling of a Windows path: no device prefix, and the long names Windows itself reports.
|
|
73
|
+
*
|
|
74
|
+
* WINDOWS REACHES ONE FILE BY SEVERAL NAMES, and the boundary compared the one it was handed. Measured on
|
|
75
|
+
* a Windows runner, 2026-09-23: a write spelled `\\?\C:\…\skills\x.md`, and a write spelled with the
|
|
76
|
+
* long name of a folder the policy had recorded by its 8.3 short name (`RUNNER~1` against
|
|
77
|
+
* `runneradmin`), both reached the protected folder and were allowed. The device prefix (`\\?\`,
|
|
78
|
+
* `\\.\`) is taken off, and the longest part of the path that exists is asked of Windows itself, which
|
|
79
|
+
* answers with the long names and follows a junction, so a junction pointing into a protected folder is
|
|
80
|
+
* inside it too. The part that does not exist yet is joined back as written. `realpath` is injected so
|
|
81
|
+
* this runs on Linux.
|
|
82
|
+
*/
|
|
83
|
+
export function canonicalWindowsPath(p, { realpath = realpathSync.native } = {}) {
|
|
84
|
+
const bare = String(p ?? "").replace(/^[\\/]{2}[?.][\\/]UNC[\\/]/i, "\\\\").replace(/^[\\/]{2}[?.][\\/]/, "");
|
|
85
|
+
const abs = win32.resolve(bare);
|
|
86
|
+
const rest = [];
|
|
87
|
+
for (let cur = abs; ;) {
|
|
88
|
+
try { return win32.join(realpath(cur), ...rest); }
|
|
89
|
+
catch {
|
|
90
|
+
const parent = win32.dirname(cur);
|
|
91
|
+
if (parent === cur) return abs;
|
|
92
|
+
rest.unshift(win32.basename(cur));
|
|
93
|
+
cur = parent;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const swapCase = (s) => [...s].map((ch) => (ch === ch.toLowerCase() ? ch.toUpperCase() : ch.toLowerCase())).join("");
|
|
99
|
+
const foldMemo = new Map();
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Does the disk holding `root` ignore letter case? Asked of THAT folder, never of the platform.
|
|
103
|
+
*
|
|
104
|
+
* A Mac's default volume ignores case, so `P/SKILLS/x.md` is a file inside `P/skills`, and a boundary
|
|
105
|
+
* comparing letter for letter let the write through: measured on the macOS runner, 2026-09-23. But a Mac
|
|
106
|
+
* volume can be formatted to mind case, and a Linux folder can be set to ignore it, so the platform does
|
|
107
|
+
* not answer. The folder is stat'ed under its own name and under that name with every letter's case
|
|
108
|
+
* swapped: the same file under both means this disk ignores case. It writes nothing, so it adds nothing to
|
|
109
|
+
* a protected tree.
|
|
110
|
+
*
|
|
111
|
+
* WINDOWS ALWAYS FOLDS, as it did before this was asked, because NTFS ignores case unless a folder is set
|
|
112
|
+
* otherwise. Where the question cannot be put (the folder does not exist yet, or its name has no letters)
|
|
113
|
+
* the answer is the platform's default disk, Windows and macOS folding, so a Mac errs toward refusing. The
|
|
114
|
+
* answer is kept per folder for the life of the process. `stat` is injected so every branch runs on Linux.
|
|
115
|
+
*/
|
|
116
|
+
export function foldsCaseAt(root, { platform = process.platform, stat = statSync } = {}) {
|
|
117
|
+
if (platform === "win32") return true;
|
|
118
|
+
const abs = posix.resolve(String(root ?? ""));
|
|
119
|
+
const key = `${platform}|${abs}`;
|
|
120
|
+
if (foldMemo.has(key) && stat === statSync) return foldMemo.get(key);
|
|
121
|
+
const byDefault = platform === "darwin";
|
|
122
|
+
const name = posix.basename(abs);
|
|
123
|
+
let answer = byDefault;
|
|
124
|
+
const other = swapCase(name);
|
|
125
|
+
if (name && other !== name) {
|
|
126
|
+
try {
|
|
127
|
+
const own = stat(abs);
|
|
128
|
+
try {
|
|
129
|
+
const swapped = stat(posix.join(posix.dirname(abs), other));
|
|
130
|
+
answer = own.dev === swapped.dev && own.ino === swapped.ino;
|
|
131
|
+
} catch (e) {
|
|
132
|
+
answer = e?.code === "ENOENT" || e?.code === "ENOTDIR" ? false : byDefault;
|
|
133
|
+
}
|
|
134
|
+
} catch { answer = byDefault; }
|
|
135
|
+
}
|
|
136
|
+
if (stat === statSync) foldMemo.set(key, answer);
|
|
137
|
+
return answer;
|
|
58
138
|
}
|
|
59
139
|
|
|
60
140
|
/**
|
|
@@ -87,11 +167,13 @@ export function authorityTrees({ skillsRoots = [], profilesDir = null, runDir =
|
|
|
87
167
|
* why it is refused, that a retry gets the same answer, and where stage output lives. No second person, no
|
|
88
168
|
* imperative. The seat decides what to do with a fact, which is the whole difference.
|
|
89
169
|
*/
|
|
90
|
-
export function denyReason(targetPath, trees) {
|
|
170
|
+
export function denyReason(targetPath, trees, { platform = process.platform, foldsCase = (root) => foldsCaseAt(root, { platform }),
|
|
171
|
+
canonical = platform === "win32" ? (p) => canonicalWindowsPath(p) : (p) => p } = {}) {
|
|
91
172
|
const t = String(targetPath ?? "").trim();
|
|
92
173
|
if (!t) return null;
|
|
174
|
+
const target = canonical(t);
|
|
93
175
|
for (const tree of trees ?? []) {
|
|
94
|
-
if (!isInside(tree.path,
|
|
176
|
+
if (!isInside(canonical(tree.path), target, { platform, foldCase: foldsCase(tree.path) })) continue;
|
|
95
177
|
return `REFUSED by the driver's write boundary: ${t} is inside ${tree.why} (${tree.path}). `
|
|
96
178
|
+ `That tree is authored at deploy time and never by a running stage, so the refusal is configuration `
|
|
97
179
|
+ `rather than a permission prompt, and a retry of the same path returns this same answer. Stage `
|
package/driver/band-shape.mjs
CHANGED
|
@@ -130,7 +130,7 @@ export function classifyRecord(markText, targets) {
|
|
|
130
130
|
if (!norm) {
|
|
131
131
|
// ── — A MARK WITH NO LATIN SKELETON CAN STILL BE IDENTICAL, AND IT WAS (2026-08-14) ──────────
|
|
132
132
|
//
|
|
133
|
-
// R1 retrieved TEN exact-match
|
|
133
|
+
// R1 retrieved TEN exact-match 澜珀 registrations — four REGISTERED, three in class 9, all in the
|
|
134
134
|
// matter's own classes — and the delivered report did not mention the token once. This line is
|
|
135
135
|
// where they went. `normalizeElement` keeps only Latin characters, so a Han mark normalises to the
|
|
136
136
|
// empty string, fell straight to `unclassifiable`, and `unclassifiable` is not one of the two tiers
|
|
@@ -139,7 +139,7 @@ export function classifyRecord(markText, targets) {
|
|
|
139
139
|
// weighed. Retrieved, banded, positioned, and never an obligation.
|
|
140
140
|
//
|
|
141
141
|
// The classifier could not COMPARE them; that is true, and it is not the same as could not MATCH
|
|
142
|
-
// them.
|
|
142
|
+
// them. 澜珀 equals 澜珀 by inspection. The precedent is in this repo already: reference-score's
|
|
143
143
|
// matchesReference makes RAW comparison its rule 1 for exactly this case, because normalizeElement
|
|
144
144
|
// keeps only [a-z0-9] and a CJK mark folds to the empty string that every Latin rule would match
|
|
145
145
|
// against everything. The scorer has had that rule since R1's Chinese registration was specced. The
|
|
@@ -147,13 +147,13 @@ export function classifyRecord(markText, targets) {
|
|
|
147
147
|
//
|
|
148
148
|
// EQUALITY ONLY, DELIBERATELY. The floors are mechanical obligations a lawyer must answer row by
|
|
149
149
|
// row, so every member has to be defensible without judgment: raw-equal after NFKC (which folds
|
|
150
|
-
// full-width and compatibility forms of the same characters) is that. Containment is not —
|
|
151
|
-
// contains
|
|
150
|
+
// full-width and compatibility forms of the same characters) is that. Containment is not — 澜珀計
|
|
151
|
+
// contains 澜珀 and is a different mark — and whether it matters is judgment's call, reached
|
|
152
152
|
// through the same-family tiers and the crowd descriptors, not by widening a floor.
|
|
153
153
|
//
|
|
154
154
|
// THIS PARAGRAPH USED TO SAY "narrower than the scorer's rule", and it was right when it was
|
|
155
155
|
// written: matchesReference's script rule was unowned containment in both directions until,
|
|
156
|
-
// which scored gold
|
|
156
|
+
// which scored gold 澜珀 as retrieved off a different proprietor's longer mark that contains it. The scorer now takes
|
|
157
157
|
// equality too, with one owner-gated containment escape the floors deliberately do not have. The
|
|
158
158
|
// two agree; neither is the looser one any more.
|
|
159
159
|
const raw = text.normalize("NFKC").trim();
|
|
@@ -36,7 +36,18 @@
|
|
|
36
36
|
// "no node imports → tests offline" invariant, exactly as connotation-search.mjs already does.
|
|
37
37
|
import { abbrev } from "./repair-contract.mjs";
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
// Version 2 is the one whose instruction tells the model to write `null` for a query it could not send,
|
|
40
|
+
// and never 0. At version 1 a 0 could mean either, so a reader may not count one as a search that ran.
|
|
41
|
+
export const CASE_LAW_LEDGER_SCHEMA_VERSION = 2;
|
|
42
|
+
|
|
43
|
+
// WHAT A RECORD THAT NAMES NO VERSION IS. The oldest, never the newest. Defaulting an unversioned record
|
|
44
|
+
// to whatever this build happens to be at hands it every promise the current instruction makes, which is
|
|
45
|
+
// exactly backwards: the records that name no version are the ones written before the field was asked
|
|
46
|
+
// for, and their zeros are the ambiguous kind. Read low, and a record only gains a promise by claiming it.
|
|
47
|
+
export const CASE_LAW_LEDGER_OLDEST_SCHEMA = 1;
|
|
48
|
+
|
|
49
|
+
/** Version 2 and up separate "sent and returned nothing" (0) from "never sent" (null). */
|
|
50
|
+
export const ledgerSeparatesUnsentQueries = (ledger) => Number(ledger?.schema_version) >= 2;
|
|
40
51
|
|
|
41
52
|
/**
|
|
42
53
|
* How far a proceeding got. `read` is the only state that discharges a depth dive, and the distinction
|
|
@@ -91,11 +102,16 @@ export function parseCaseLawLedger(raw) {
|
|
|
91
102
|
return {
|
|
92
103
|
error: null,
|
|
93
104
|
ledger: {
|
|
94
|
-
schema_version: Number(parsed.schema_version) ||
|
|
105
|
+
schema_version: Number(parsed.schema_version) || CASE_LAW_LEDGER_OLDEST_SCHEMA,
|
|
95
106
|
queries: queries.filter((q) => q && typeof q === "object").map((q) => ({
|
|
96
107
|
query: str(q.query),
|
|
97
108
|
jurisdiction: str(q.jurisdiction),
|
|
98
|
-
|
|
109
|
+
// NULL STAYS NULL, and this is load-bearing from version 2 on. `Number(null)` is 0 and
|
|
110
|
+
// `Number("")` is 0, both finite, so the obvious coercion turns the record's "I could not send
|
|
111
|
+
// this query" into "I sent it and got nothing" — the very two the version exists to separate,
|
|
112
|
+
// collapsed by the parser before any reader sees them. Only a value that genuinely is a number
|
|
113
|
+
// becomes one; everything else is null, which is the record declining to give a count.
|
|
114
|
+
results: q.results == null || q.results === "" || Number.isNaN(Number(q.results)) ? null : Number(q.results),
|
|
99
115
|
})),
|
|
100
116
|
citations: citations.filter((c) => c && typeof c === "object").map((c) => ({
|
|
101
117
|
proceeding: str(c.proceeding),
|
|
@@ -35,6 +35,7 @@ import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
|
35
35
|
import { spawnSync } from "node:child_process"; // — the birth stamp where there is no /proc
|
|
36
36
|
import { join } from "node:path";
|
|
37
37
|
import { config } from "./driver.config.mjs";
|
|
38
|
+
import { windowsProcessScript, parseWindowsRows, defaultRunPowerShell } from "../shared/process-table.mjs"; // the birth stamp on Windows
|
|
38
39
|
|
|
39
40
|
// ── B2 — fail-safe claim liveness ────────────────────────────────────────────────────────────────────
|
|
40
41
|
// The .pid sidecar records "<pid>:<starttime>" (starttime = field 22 of /proc/<pid>/stat, the kernel's
|
|
@@ -42,7 +43,7 @@ import { config } from "./driver.config.mjs";
|
|
|
42
43
|
// impersonate a dead claimer and rot its `.processing` forever. Legacy bare-pid sidecars still parse
|
|
43
44
|
// (starttime null → pid-aliveness is all we have, today's behavior). All exported for unit tests.
|
|
44
45
|
export function procStarttime(pid, readStat = undefined,
|
|
45
|
-
{ platform = process.platform, readPsStart = defaultReadPsStart } = {}) {
|
|
46
|
+
{ platform = process.platform, readPsStart = defaultReadPsStart, runPowerShell = defaultRunPowerShell } = {}) {
|
|
46
47
|
// AN INJECTED READER IS THE CALLER'S STATEMENT ABOUT HOW TO READ, and it outranks the platform.
|
|
47
48
|
//
|
|
48
49
|
// Measured on macOS (the verification run): the arm that pins the field-22
|
|
@@ -61,6 +62,7 @@ export function procStarttime(pid, readStat = undefined,
|
|
|
61
62
|
return stat.slice(stat.lastIndexOf(")") + 2).trim().split(/\s+/)[19] ?? null;
|
|
62
63
|
} catch { return null; }
|
|
63
64
|
}
|
|
65
|
+
if (platform === "win32") return windowsStarttime(pid, runPowerShell);
|
|
64
66
|
// ── — THE SAME DEFENCE WHERE THERE IS NO /proc ─────────────────────────
|
|
65
67
|
//
|
|
66
68
|
// Returning null here was not a neutral degradation. `claimToken` announces it and falls back to a
|
|
@@ -82,6 +84,33 @@ export function procStarttime(pid, readStat = undefined,
|
|
|
82
84
|
} catch { return null; }
|
|
83
85
|
}
|
|
84
86
|
|
|
87
|
+
// ── WINDOWS: NEITHER /proc NOR ps ────────────────────────────────────────────────────────────────────
|
|
88
|
+
//
|
|
89
|
+
// Without a stamp on Windows the portal said nothing was draining the queue while the worker ran, which
|
|
90
|
+
// is the false alarm described above, and a claim held by a dead runner stayed held until the max-claim-age
|
|
91
|
+
// ceiling. That second cost is larger on Windows than anywhere else, because Windows hands a freed pid to
|
|
92
|
+
// the next process soon after. So the stamp is read from the system's process list, through PowerShell:
|
|
93
|
+
// the creation time as epoch milliseconds, the same reader `processTable` uses, so a stamp written from
|
|
94
|
+
// one is compared with a stamp read by the same code.
|
|
95
|
+
//
|
|
96
|
+
// ONE PROCESS IS REMEMBERED: this one. Its start cannot change while it runs, and the heartbeat and the
|
|
97
|
+
// status stamp ask for it on every tick, each a PowerShell start. Every other pid is read afresh, for the
|
|
98
|
+
// reason above: a remembered stamp is the dead process's, handed to whatever now wears its number.
|
|
99
|
+
let ownWindowsStamp;
|
|
100
|
+
function windowsStarttime(pid, runPowerShell) {
|
|
101
|
+
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
102
|
+
const own = pid === process.pid && runPowerShell === defaultRunPowerShell;
|
|
103
|
+
if (own && ownWindowsStamp) return ownWindowsStamp;
|
|
104
|
+
let row = null;
|
|
105
|
+
try {
|
|
106
|
+
const r = runPowerShell(windowsProcessScript(pid));
|
|
107
|
+
if (r && r.status === 0) row = parseWindowsRows(r.stdout).find((p) => p.pid === pid) ?? null;
|
|
108
|
+
} catch { return null; }
|
|
109
|
+
const stamp = row && Number.isFinite(row.startedAt) ? String(row.startedAt) : null;
|
|
110
|
+
if (own && stamp) ownWindowsStamp = stamp;
|
|
111
|
+
return stamp;
|
|
112
|
+
}
|
|
113
|
+
|
|
85
114
|
/** `ps` is POSIX and is on macOS. Empty stdout (no such pid) parses to NaN above, which is `null`. */
|
|
86
115
|
function defaultReadPsStart(pid) {
|
|
87
116
|
const r = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8" });
|
|
@@ -130,8 +130,15 @@ export function renderClearanceVariants(model, scopeRows) {
|
|
|
130
130
|
const out = ["# Variant manifest", ""];
|
|
131
131
|
out.push(`## Mark: ${model.mark}`, "", `Dominant element: ${model.dominant_element}`, "");
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
// The famous-mark column appears only when an element carries the flag, so a manifest with none reads
|
|
134
|
+
// exactly as it did before the flag existed.
|
|
135
|
+
if (model.elements.some((e) => e.famous_mark_flag)) {
|
|
136
|
+
out.push("### Elements", "", "| Value | Kind | Famous-mark check |", "|---|---|---|");
|
|
137
|
+
for (const e of model.elements) out.push(`| ${e.value} | ${e.kind} | ${e.famous_mark_flag ? "yes" : ""} |`);
|
|
138
|
+
} else {
|
|
139
|
+
out.push("### Elements", "", "| Value | Kind |", "|---|---|");
|
|
140
|
+
for (const e of model.elements) out.push(`| ${e.value} | ${e.kind} |`);
|
|
141
|
+
}
|
|
135
142
|
out.push("");
|
|
136
143
|
|
|
137
144
|
out.push(`### Variants (${model.variants.length})`, "", "| Value | Category | Romanisation | Rationale |", "|---|---|---|---|");
|
|
@@ -172,7 +179,7 @@ export function renderClearanceVariants(model, scopeRows) {
|
|
|
172
179
|
/** The shape this tool declares, at every depth — what the ACCEPTOR enforces. */
|
|
173
180
|
const DECLARED = Object.freeze({
|
|
174
181
|
"": ["mark", "dominant_element", "elements", "variants", "incumbent_classes", "search_floor", "watchlist_owners", "goods_words", "scope_ledger"],
|
|
175
|
-
elements: ["value", "kind"],
|
|
182
|
+
elements: ["value", "kind", "famous_mark_flag"],
|
|
176
183
|
variants: ["value", "category", "rationale", "romanization"],
|
|
177
184
|
scope_ledger: ["layer", "item", "status", "reason", "reopen_trigger"],
|
|
178
185
|
});
|
|
@@ -30,6 +30,7 @@ export const MIN_CELLS_PER_VARIANT = 7;
|
|
|
30
30
|
// It composes `queryKey`, which is the one author of what "the same text" means across a provider
|
|
31
31
|
// boundary, and keeps its own wrapping-quote strip because a term arrives quoted here and does not there.
|
|
32
32
|
import { queryKey } from "./connotation-search.mjs";
|
|
33
|
+
import { dictatedCells } from "../providers/perplexity/src/core.js"; // what the spec asks, product or blocks
|
|
33
34
|
|
|
34
35
|
const norm = (s) => queryKey(String(s || "").trim().replace(/^["'`\u2018\u2019\u201c\u201d]+|["'`\u2018\u2019\u201c\u201d]+$/g, ""));
|
|
35
36
|
|
|
@@ -285,6 +286,30 @@ export function findGridLedgerViolations(manifestOrTerms, ledgerRaw, { minCellsP
|
|
|
285
286
|
return shortVariants(variants, (term) => platforms.get(norm(term))?.size || 0, minCellsPerVariant);
|
|
286
287
|
}
|
|
287
288
|
|
|
289
|
+
/**
|
|
290
|
+
* — THE RECEIPTS JOIN FOR A GRID IN BLOCKS: every cell the spec dictated ran — a cell or a recorded gap in
|
|
291
|
+
* the ledger — judged cell by cell. The count and identity joins below assume every term owes every
|
|
292
|
+
* platform, which a grid the matter frame decided does not ask. Same {variant, cells, expected} shape as
|
|
293
|
+
* findGridLedgerViolations, plus the platforms still owed, so the `grid_join_missing` token is unchanged.
|
|
294
|
+
* Throws on an unparseable ledger.
|
|
295
|
+
*
|
|
296
|
+
* @returns {Array<{variant:string, cells:number, expected:number, missing:string[]}>}
|
|
297
|
+
*/
|
|
298
|
+
export function findUnranDictatedCells(spec, ledgerRaw) {
|
|
299
|
+
const map = parseGridLedger(ledgerRaw);
|
|
300
|
+
const owed = new Map(); // term → [platform, ran?]
|
|
301
|
+
for (const [term, platform] of dictatedCells(spec)) {
|
|
302
|
+
if (!owed.has(term)) owed.set(term, []);
|
|
303
|
+
owed.get(term).push([platform, map.get(norm(term))?.has(norm(platform)) === true]);
|
|
304
|
+
}
|
|
305
|
+
const out = [];
|
|
306
|
+
for (const [variant, rows] of owed) {
|
|
307
|
+
const missing = rows.filter(([, ran]) => !ran).map(([p]) => p);
|
|
308
|
+
if (missing.length) out.push({ variant, cells: rows.length - missing.length, expected: rows.length, missing });
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
|
|
288
313
|
// ── WS-B: platform-identity join (per-customer profiles) ────────────────────────────────────────────────
|
|
289
314
|
// The count floor proves HOW MANY distinct platforms each variant accounted for — never WHICH. With a
|
|
290
315
|
// dictated per-customer platform list, a worker on gaming habit could sweep the wrong marketplaces and
|
|
@@ -674,6 +699,10 @@ export function splitGridSpec(spec, { outputPaths = {}, dispositionsPaths = {} }
|
|
|
674
699
|
// The meaning seat sweeps no cells. `terms: []` is what makes its dispatch the meaning work and
|
|
675
700
|
// nothing else, and it is why the grid halves keep an untouched 50/50 parity split.
|
|
676
701
|
terms: isMeaning ? [] : terms[h],
|
|
702
|
+
// A grid in blocks keeps its blocks, each cut to this half's terms (a term owns all its cells in one half).
|
|
703
|
+
...(Array.isArray(spec.grids) ? { grids: spec.grids.map((g) => ({ terms: g.terms.filter((t) => !isMeaning && terms[h].includes(t)), platforms: g.platforms })).filter((g) => g.terms.length) } : {}),
|
|
704
|
+
// The famous-mark record names only the cells this seat runs.
|
|
705
|
+
...(Array.isArray(spec.famous) ? { famous: spec.famous.filter((t) => !isMeaning && terms[h].includes(t)) } : {}),
|
|
677
706
|
output_path: outputPaths[h] ?? spec.output_path,
|
|
678
707
|
// The disposition_required stamp still rides BOTH halves: it is the receipt-presence arm, and a
|
|
679
708
|
// stray pr_risk block in the non-owning half must still be judged rather than waved through.
|
|
@@ -714,7 +743,8 @@ export function splitGridSpec(spec, { outputPaths = {}, dispositionsPaths = {} }
|
|
|
714
743
|
* batch has since closed is dropped). This is the never-thinner guarantee: every dictated cell is
|
|
715
744
|
* accounted as a real cell or an HONEST gap, so a half failure can never yield a ledger that is
|
|
716
745
|
* thinner but still valid. A half's honestly-recorded gap for a cell OUTSIDE the spec (a re-keyed
|
|
717
|
-
* variant / an extra swept platform) is UNIONED in too — the spec-only recompute would erase it
|
|
746
|
+
* variant / an extra swept platform) is UNIONED in too — the spec-only recompute would erase it,
|
|
747
|
+
* except a meaning query's gap row when the merged receipts answer that query.
|
|
718
748
|
* - extras.pr_risk[]: concatenated (a then b), deduped by trimmed query — restores the full dictated
|
|
719
749
|
* meaning-receipt set from the per-half partitions. Other extras keys: arrays concatenate,
|
|
720
750
|
* anything else first-defined wins.
|
|
@@ -773,23 +803,32 @@ export function mergeGrids(a, b, { spec, halfErrors = {} } = {}) {
|
|
|
773
803
|
// gaps: recomputed against the FULL spec
|
|
774
804
|
const halfTerms = splitGridTerms(spec?.terms ?? []);
|
|
775
805
|
const gaps = [];
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
}
|
|
806
|
+
// Over the cells the spec DICTATES, never terms × platforms: a grid the frame decided is blocks, and the
|
|
807
|
+
// product of its unions would make a gap of every cell nobody asked for, which the closure pass would run.
|
|
808
|
+
for (const [term, platform] of spec ? dictatedCells(spec) : []) {
|
|
809
|
+
if (seen.has(key(term, platform))) continue;
|
|
810
|
+
const owner = halfOfTerm(halfTerms, term);
|
|
811
|
+
const error = gapErr.get(key(term, platform))?.error
|
|
812
|
+
|| (owner && halfErrors[owner]) || "cell not accounted by either half-grid";
|
|
813
|
+
gaps.push({ term, platform, error });
|
|
784
814
|
}
|
|
785
815
|
// UNION honestly-recorded NON-SPEC gap rows: a half may record a gap for a cell OUTSIDE the canonical
|
|
786
816
|
// spec (a legitimately re-keyed variant / an extra platform a half swept). The spec-only recompute above
|
|
787
817
|
// never emits those, silently erasing an honestly-recorded coverage gap — union them so an honest gap
|
|
788
818
|
// survives the merge. A cell present in cells[] still wins (a stale gap a supplementary batch has since
|
|
789
819
|
// closed is dropped exactly as for spec cells), and a gap already emitted by the spec loop is not doubled.
|
|
820
|
+
//
|
|
821
|
+
// A MEANING QUERY THE MERGED RECEIPTS ANSWER KEEPS NO GAP ROW. The grid tool records one for each dictated
|
|
822
|
+
// query its program did not return, and the engine's re-issue can answer that query later in the same
|
|
823
|
+
// ledger. Kept beside the answer, the row says a search did not complete when it did, and the closure pass
|
|
824
|
+
// takes every gap row for a store cell, so it searches the query again on a site called "connotation".
|
|
825
|
+
// Queries are matched as the receipts are (norm).
|
|
826
|
+
const answered = new Set((Array.isArray(extras.pr_risk) ? extras.pr_risk : [])
|
|
827
|
+
.map((e) => (typeof e?.query === "string" ? norm(e.query) : "")).filter(Boolean));
|
|
790
828
|
const gappedKeys = new Set(gaps.map((g) => key(g.term, g.platform)));
|
|
791
829
|
for (const [k, g] of gapErr) {
|
|
792
830
|
if (seen.has(k) || gappedKeys.has(k)) continue;
|
|
831
|
+
if (String(g.platform).toLowerCase() === "connotation" && answered.has(norm(g.term))) continue;
|
|
793
832
|
gaps.push({ term: g.term, platform: g.platform, error: g.error || "cell not accounted by either half-grid" });
|
|
794
833
|
}
|
|
795
834
|
return { cells, extras, gaps };
|
|
@@ -926,7 +965,16 @@ export function findSimilarListingSignals(findingsContent) {
|
|
|
926
965
|
const signals = [];
|
|
927
966
|
const OWNER_RE = /(?:developer|publisher|seller|owner)(?:\s+|_)of(?:\s+|_)record\W{0,5}([^\n|]+)/i;
|
|
928
967
|
const OWNER_SKIP_RE = /not extracted|unknown|n\/a|none|^[\s—–-]*$/i;
|
|
968
|
+
// A table whose header lists the of-record fields matches OWNER_RE on its first label and captures the
|
|
969
|
+
// next one, so a label reached the register as an owner's name ("publisherofrecord", once its
|
|
970
|
+
// underscores were stripped). A label is never a name.
|
|
971
|
+
const OWNER_LABEL_RE = /^(?:developer|publisher|seller|owner)[\s_]*of[\s_]*record\b/i;
|
|
929
972
|
const URL_RE = /https?:\/\/[^\s)|\]">]+/;
|
|
973
|
+
// A TABLE'S HEADER ROW NAMES COLUMNS, NEVER AN OWNER, whatever column follows an of-record label ("URL",
|
|
974
|
+
// "Store"). It is the row a separator row follows, and both are table rows: a bare "---" under an owner's
|
|
975
|
+
// line is a rule, not a separator, and the owner stands.
|
|
976
|
+
const SEPARATOR_RE = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)*\|?\s*$/;
|
|
977
|
+
const headerRow = (ln, next) => ln.includes("|") && String(next ?? "").includes("|") && SEPARATOR_RE.test(next);
|
|
930
978
|
let inMatrix = false, inFindings = false;
|
|
931
979
|
let block = null; // { markText, owner, url }
|
|
932
980
|
const flush = () => {
|
|
@@ -935,7 +983,9 @@ export function findSimilarListingSignals(findingsContent) {
|
|
|
935
983
|
}
|
|
936
984
|
block = null;
|
|
937
985
|
};
|
|
938
|
-
|
|
986
|
+
const lines = (findingsContent || "").split("\n");
|
|
987
|
+
for (let i = 0; i < lines.length; i++) {
|
|
988
|
+
const ln = lines[i];
|
|
939
989
|
const h = ln.match(/^(#{1,6})\s+(.*)/);
|
|
940
990
|
if (h) {
|
|
941
991
|
const title = h[2];
|
|
@@ -958,10 +1008,10 @@ export function findSimilarListingSignals(findingsContent) {
|
|
|
958
1008
|
continue;
|
|
959
1009
|
}
|
|
960
1010
|
if (inFindings && block) {
|
|
961
|
-
const om = ln.match(OWNER_RE);
|
|
1011
|
+
const om = headerRow(ln, lines[i + 1]) ? null : ln.match(OWNER_RE);
|
|
962
1012
|
if (om && !block.owner) {
|
|
963
1013
|
const owner = om[1].replace(/[*_`]/g, "").trim();
|
|
964
|
-
if (owner && !OWNER_SKIP_RE.test(owner)) block.owner = owner.slice(0, 120);
|
|
1014
|
+
if (owner && !OWNER_SKIP_RE.test(owner) && !OWNER_LABEL_RE.test(om[1].trim())) block.owner = owner.slice(0, 120);
|
|
965
1015
|
}
|
|
966
1016
|
const um = ln.match(URL_RE);
|
|
967
1017
|
if (um && !block.url) block.url = um[0];
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
// ── THE NON-LATIN TRAP IS ANSWERED, NOT AVOIDED ──────────────────────────────────────────────────────
|
|
40
40
|
//
|
|
41
41
|
// `normalizeJoinText` folds every non-[A-Za-z0-9] run to a space, so it returns the EMPTY STRING for
|
|
42
|
-
//
|
|
42
|
+
// 澜珀, 冰沙, 瓦波冰沙, ワボスラッシュ and 와보 슬러시. A mark-keyed trace is structurally blind to
|
|
43
43
|
// every one of them.
|
|
44
44
|
//
|
|
45
45
|
// NOTHING HERE TOUCHES IT. A Han-script candidate joins on its ASCII/percent-encoded URL. A Han-script
|