clearotron 0.2.2 → 0.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +64 -37
- package/.nvmrc +1 -1
- package/CONTRIBUTING.md +8 -4
- package/INSTALL.md +36 -14
- package/README.md +18 -22
- package/THIRD-PARTY-NOTICES.md +1 -1
- package/bin/clearotron.mjs +22 -0
- package/bin/example.mjs +10 -5
- package/bin/onboard.mjs +370 -28
- package/bin/start.mjs +220 -12
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/demo/full-country-search/run/_driver/framework.json +1 -1
- package/demo/global-preliminary-search/run/_driver/framework.json +1 -1
- package/demo/knockout-search/run/_driver/framework.json +1 -1
- package/demo/multi-country-focus-search/run/_driver/framework.json +1 -1
- package/docs/E2E.md +5 -5
- package/docs/GLOSSARY.md +85 -0
- package/docs/PORTAL.md +2 -2
- package/docs/README.md +6 -4
- package/docs/SECURITY.md +17 -0
- package/docs/architecture/01-product-overview.md +21 -9
- package/docs/architecture/04-configuration-reference.md +2 -0
- package/docs/architecture/05-config-governance.md +19 -0
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/configuration.md +4 -3
- package/docs/decisions/0006-what-the-public-repository-carries.md +30 -5
- package/driver/CHANGELOG.md +90 -0
- package/driver/README.md +25 -6
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/config-staleness.mjs +1 -1
- package/driver/connotation-search.mjs +1 -1
- package/driver/contract-audit.mjs +5 -1
- package/driver/contract-e3-baseline.json +11 -11
- package/driver/dev-portal.mjs +2 -2
- package/driver/doubt-selection.mjs +1 -1
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +1 -1
- package/driver/effort-model.mjs +2 -2
- package/driver/engine/cli-version.mjs +103 -0
- package/driver/engine/probe.mjs +45 -5
- package/driver/enqueue-schema.mjs +8 -8
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +52 -2
- package/driver/flag-snapshot.mjs +20 -0
- package/driver/framework.mjs +1 -1
- package/driver/gateway.mjs +20 -3
- package/driver/outbox-backoff.mjs +1 -1
- package/driver/package.json +3 -3
- package/driver/party-facts.mjs +2 -2
- package/driver/phase0.mjs +1 -1
- package/driver/pipeline.mjs +211 -106
- package/driver/plain-register.mjs +134 -0
- package/driver/portal-access.mjs +1 -1
- package/driver/portal-config-view.mjs +70 -2
- package/driver/portal-families.mjs +1 -1
- package/driver/portal-local-auth.mjs +5 -1
- package/driver/portal-report.mjs +1 -1
- package/driver/portal-service.mjs +79 -9
- package/driver/portal-upstream.mjs +2 -2
- package/driver/predelivery-lint.mjs +151 -2
- package/driver/profile-page.html +10 -2
- package/driver/profile-service.mjs +4 -4
- package/driver/profiles/README.md +3 -3
- package/driver/profiles.mjs +144 -12
- package/driver/publish/index.mjs +2 -2
- package/driver/publish/knockout.mjs +7 -1
- package/driver/publish/render-knockout.mjs +453 -118
- package/driver/publish/render.mjs +111 -16
- package/driver/reference-strip-signatures.mjs +20 -0
- package/driver/search-policy.mjs +3 -3
- package/driver/skills/blind-frame/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +84 -0
- package/driver/skills/matter-frame/SKILL.md +6 -6
- package/driver/skills/matter-frame/watchlist-reference.md +2 -2
- package/driver/skills/prelim-common-law/SKILL.md +1 -1
- package/driver/skills/prelim-search/SKILL.md +1 -1
- package/driver/skills/prelim-search/report-prose.md +82 -0
- package/driver/skills/prelim-search/risk-framework-demo.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework-triage.manifest.json +1 -1
- package/driver/skills/prelim-search/risk-framework.manifest.json +1 -1
- package/driver/skills/prelim-search/synthesis-rules.md +1 -1
- package/driver/skills/prelim-variants/SKILL.md +3 -3
- package/driver/stage-context.mjs +13 -0
- package/driver/stages.mjs +54 -7
- package/driver/suite-census.json +220 -58
- package/driver/systemd/clearotron-worker.service +3 -3
- package/driver/tokens.mjs +1 -1
- package/driver/unit-inventory.mjs +214 -27
- package/driver/verify.mjs +1 -1
- package/driver/whatif-memo-run.mjs +26 -4
- package/driver/whatif-memo.mjs +19 -1
- package/examples/README.md +2 -2
- package/examples/grants.example.json +6 -7
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/lib/whatif.mjs +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/packs/ops/EXAMPLES.md +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +31 -19
- package/portal-ui/dist/assets/{index-Ct3iQtZ2.css → index-1ziUJX1E.css} +43 -2
- package/portal-ui/dist/assets/{index-BUGgiYtF.js → index-KFAHMgdT.js} +289 -73
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/term-shape.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/src/index-store.js +2 -2
- package/scripts/added-reference-check.mjs +45 -89
- package/scripts/citation-line-check.mjs +121 -2
- package/scripts/e2e.mjs +2 -2
- package/scripts/env-classify.mjs +67 -3
- package/scripts/generated-files-are-current.mjs +125 -0
- package/scripts/live-surface-check.mjs +38 -1
- package/scripts/mint-names-in-force.mjs +4 -2
- package/scripts/mint-public-residue.mjs +74 -0
- package/scripts/mint-reference-strip-backlog.mjs +12 -1
- package/scripts/mint-suite-census.mjs +5 -2
- package/scripts/nightly-notice.mjs +0 -2
- package/scripts/preinstall-node-check.mjs +24 -0
- package/scripts/render-check.mjs +13 -2
- package/scripts/report-frame-check.mjs +8 -1
- package/scripts/report-screenshot.mjs +6 -3
- package/scripts/revisit-render-check.mjs +6 -3
- package/scripts/score.mjs +1 -1
- package/scripts/strip-tracker-citations.mjs +38 -7
- package/scripts/test-full.mjs +226 -0
- package/scripts/test-run.mjs +16 -0
- package/shared/README.md +2 -1
- package/shared/anon-overlay.mjs +3 -3
- package/shared/browser-temp-root.mjs +142 -0
- package/shared/checkout-move.mjs +1 -1
- package/shared/client-door.mjs +4 -3
- package/shared/customer-store.mjs +1 -1
- package/shared/identifier-scan.mjs +1 -1
- package/shared/install-auth.mjs +35 -0
- package/shared/invocation.mjs +33 -1
- package/shared/listen.mjs +55 -3
- package/shared/names-in-force.mjs +6 -1
- package/shared/node-floor.mjs +97 -0
- package/shared/os-advice.mjs +91 -0
- package/shared/reap-on-exit.mjs +42 -0
- package/shared/reference-guard-classes.mjs +351 -0
- package/shared/staff-domain.mjs +154 -0
- package/shared/store-in-repo.mjs +23 -2
- package/shared/suite-census.mjs +31 -3
- package/shared/withheld-paths-access.mjs +36 -18
- package/skills/clearotron-ops/SKILL.md +1 -1
- package/driver/recipes/README.md +0 -49
- package/driver/skills/prelim-search/risk-framework-aurora.manifest.json +0 -19
- package/driver/skills/prelim-search/risk-framework-aurora.md +0 -77
- package/driver/skills/prelim-search/risk-framework-zephyr.manifest.json +0 -14
- package/driver/skills/prelim-search/risk-framework-zephyr.md +0 -33
- package/driver/skills/prelim-search/worked-examples-aurora.md +0 -10
- package/driver/skills/prelim-search/worked-examples-zephyr.md +0 -10
- package/scripts/ai-page-render-check.mjs +0 -532
- package/scripts/clearances-render-check.mjs +0 -480
- package/scripts/composer-render-check.mjs +0 -1027
- package/scripts/home-render-check.mjs +0 -526
- package/scripts/portal-lifecycle-check.mjs +0 -639
package/driver/engine/probe.mjs
CHANGED
|
@@ -123,7 +123,13 @@ export function classifyProbe({ engine, tuple = null, error = null, timeoutSec =
|
|
|
123
123
|
if (!tuple) return v("failed", "none", `${id} returned nothing`, "The adapter settled no tuple — this is a driver bug, not a configuration one.");
|
|
124
124
|
const s = tuple.signals ?? {};
|
|
125
125
|
const text = `${tuple.stderr ?? ""}\n${tuple.stdout ?? ""}`;
|
|
126
|
-
|
|
126
|
+
// READ FROM THE SAME STREAMS THE CLASSIFICATION IS. Every branch below matches over `text`, which is
|
|
127
|
+
// stderr AND stdout, while this carried stderr alone — so a CLI that reports its failure on stdout
|
|
128
|
+
// produced a verdict with its evidence dropped, and the reader got a headline with nothing under it.
|
|
129
|
+
// stderr stays FIRST because that is where a diagnostic belongs and where these CLIs put theirs; the
|
|
130
|
+
// fallback exists so that "the engine said nothing" is a claim about the engine rather than about
|
|
131
|
+
// which pipe this happened to look at.
|
|
132
|
+
const detail = tail(tuple.stderr) ?? tail(tuple.stdout);
|
|
127
133
|
|
|
128
134
|
if (tuple.code === 0) return { ok: true, engine: id, mode: "ok", basis: "completed-turn", headline: `${id} completed a turn`, fix: null, detail: null };
|
|
129
135
|
|
|
@@ -249,13 +255,47 @@ export function probeWeatherWarning(verdict) {
|
|
|
249
255
|
* whatever the operator's shell happens to have set and report a pass for the wrong engine — the same
|
|
250
256
|
* frozen-provider trap `preflightCandidate` works around one file over.
|
|
251
257
|
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
258
|
+
* THE SPEND VARIABLES ARE APPLIED TOO, and the rule this replaces had it backwards. It said they were
|
|
259
|
+
* deliberately not copied, so that "the probe must bill exactly the way a run on this machine would" —
|
|
260
|
+
* the aim is right and withholding them defeated it. The caller supplies the lane and the credential
|
|
261
|
+
* precisely because they are not in the environment yet; not copying them left the probe billing the
|
|
262
|
+
* way the operator's shell happened to be set, while the run that followed billed the way the reader
|
|
263
|
+
* had just chosen. The probe proved a lane nobody was going to use. Measured: an API-key install failed
|
|
264
|
+
* as "not signed in" because the adapter removes the key under any mode that is not the API-key one,
|
|
265
|
+
* and the mode it read was the unset one.
|
|
266
|
+
*
|
|
267
|
+
* WHAT IS STILL NEVER DONE, which is the sentence that was worth keeping: nothing here defaults,
|
|
268
|
+
* repairs, invents or falls back. It copies what the caller passed and nothing else, so a probe cannot
|
|
269
|
+
* pass on a credential a run would not have — the caller is the wizard about to write these very values
|
|
270
|
+
* to disk, or a command reading the values already there.
|
|
271
|
+
*
|
|
272
|
+
* AND IT PUTS EVERYTHING BACK. Widening what this writes onto the process widens what it must remove
|
|
273
|
+
* again; a credential outliving the check that borrowed it would be a worse defect than the one this
|
|
274
|
+
* fixed, and a check drives the restore rather than trusting it.
|
|
255
275
|
*/
|
|
256
276
|
function applyEngineEnv(env) {
|
|
257
277
|
if (!env || env === process.env) return () => {};
|
|
258
|
-
|
|
278
|
+
// EVERY VARIABLE THAT DECIDES WHAT THE SPAWN IS, not only the ones that decide WHICH BINARY.
|
|
279
|
+
//
|
|
280
|
+
// This list used to carry CLEAROTRON_AI and the binary paths. It did not carry the billing mode or
|
|
281
|
+
// the API key — so a caller could hand this function a correct api-key environment, watch
|
|
282
|
+
// `resolveAuthMode` accept it, and then have `runTurn` read `process.env`, where neither had arrived.
|
|
283
|
+
// `anthropic-agent.spawnEnv` deletes ANTHROPIC_API_KEY under any mode that is not api-key, and the
|
|
284
|
+
// mode it read was unset, so it deleted the key the reader had just typed. The turn then failed as
|
|
285
|
+
// "not signed in" — correctly, about an environment nobody had asked for.
|
|
286
|
+
//
|
|
287
|
+
// Reported from a real WSL2 install, 2026-09-09: the setup wizard's api-key lane, a valid key, and a
|
|
288
|
+
// sign-in failure the reader could only get past by exporting the key into their own shell.
|
|
289
|
+
//
|
|
290
|
+
// A SET, AND THE DUPLICATE IS NOT HYPOTHETICAL. Both engines name the same billing variable, so this
|
|
291
|
+
// list contains CLEAROTRON_AI_BILLING twice — and the save below is `saved.set(k, process.env[k])`
|
|
292
|
+
// inside the same loop that writes. On the second visit it saved the value the FIRST visit had just
|
|
293
|
+
// written, so the restore put that back instead of deleting it, and the mode leaked into the rest of
|
|
294
|
+
// the process. Caught by the arm that exists to prove the restore, which is the only reason widening
|
|
295
|
+
// this list was safe to do at all.
|
|
296
|
+
const keys = [...new Set(["CLEAROTRON_AI", ...Object.values(ENGINE_BINARIES)
|
|
297
|
+
.flatMap((s) => [s.env, s.authEnv, s.apiKeyEnv, s.headless?.tokenEnv])
|
|
298
|
+
.filter(Boolean)])];
|
|
259
299
|
const saved = new Map();
|
|
260
300
|
for (const k of keys) {
|
|
261
301
|
saved.set(k, process.env[k]);
|
|
@@ -237,7 +237,7 @@ function niceClassErrors(list, label) {
|
|
|
237
237
|
// and reaches the report body, where it REVERSES THE DISPLAY of everything after it. The report is a
|
|
238
238
|
// legal deliverable that goes to a client.
|
|
239
239
|
//
|
|
240
|
-
// A zero-width joiner makes "
|
|
240
|
+
// A zero-width joiner makes "IRON\u200DWHISK" render as IRONWHISK and compare unequal to it, so
|
|
241
241
|
// `selfExclusionOwners` and dedup both miss a mark a human reads as the account's own.
|
|
242
242
|
//
|
|
243
243
|
// NFD "SIRÈNE" !== NFC "SIRÈNE", so one mark submitted two ways is two matters, two searches, two bills.
|
|
@@ -285,7 +285,7 @@ const markNameSites = (job) => {
|
|
|
285
285
|
for (const f of ["markName", "name"]) if (job?.[f] != null) sites.push([f, () => job[f], (v) => { job[f] = v; }]);
|
|
286
286
|
if (Array.isArray(job?.marks)) job.marks.forEach((m, i) => {
|
|
287
287
|
// A BARE STRING IS A MARK NAME, and this shape reaches validateJob unconverted. `assembleFromFlags`
|
|
288
|
-
// turns `marks: ["
|
|
288
|
+
// turns `marks: ["IRONWHISK"]` into `[{ name: "IRONWHISK" }]`, so every door that assembles is already
|
|
289
289
|
// covered — but the runner's wall calls validateJob({ atClaim: true }) on the MANIFEST AS IT SITS ON
|
|
290
290
|
// DISK, which nothing re-assembles. The kebab-collision check below reads the same shape for exactly
|
|
291
291
|
// that reason. Walking only `m.name` would leave this guard covering every door except the one it
|
|
@@ -778,16 +778,16 @@ export function validateJob(job, { atClaim = false } = {}) {
|
|
|
778
778
|
// that would drop the customer's platforms, self-exclusion seed and the framework that RATES the matter".
|
|
779
779
|
// A MISSING key returns generic in silence. Both produce the same wrong deliverable, and only one says so.
|
|
780
780
|
//
|
|
781
|
-
// It has already happened: a paid
|
|
782
|
-
// request named the customer in prose and left the account field empty. Nothing objected.
|
|
783
|
-
// noticed hours later and re-ran it.
|
|
781
|
+
// It has already happened: a paid clearance for a test account was rated on the Generic default scale
|
|
782
|
+
// because the request named the customer in prose and left the account field empty. Nothing objected.
|
|
783
|
+
// A person noticed hours later and re-ran it.
|
|
784
784
|
//
|
|
785
785
|
// WHY THIS IS NOT SIMPLY "customer present, key absent". That shape is byte-identical to a LEGITIMATE
|
|
786
786
|
// third-party search — a firm asking us to clear a mark for their own client, who is not our account —
|
|
787
787
|
// and the applicant deliberately never selects a profile, because a third-party search must never inherit
|
|
788
788
|
// a customer's exclusions. Clarifying on that shape alone would bounce real work every day.
|
|
789
789
|
//
|
|
790
|
-
// The discriminator is the ROSTER.
|
|
790
|
+
// The discriminator is the ROSTER. That request differed in one way that matters: we hold an
|
|
791
791
|
// account by that name. A named applicant we have no account for is a third-party search and runs
|
|
792
792
|
// untouched; a named applicant who IS one of our customers, with no key, is ambiguous in a way only a
|
|
793
793
|
// person can settle — did intake forget to tag it, or is this genuinely a search against a name that
|
|
@@ -965,7 +965,7 @@ export const EXAMPLE_JOB = {
|
|
|
965
965
|
rawRequest: "<the verbatim forwarded email text, untouched — archived as inbound-request.txt (§A5)>",
|
|
966
966
|
brief: "<the confirmation brief exactly as sent to the requester — archived as confirmation-brief.md>",
|
|
967
967
|
deadline: "2026-06-20T17:00:00Z", // optional; drives the §A3 deadline-envelope arithmetic
|
|
968
|
-
profileKey: "
|
|
968
|
+
profileKey: "demo-brand-owner", // D4.1: the customer ACCOUNT the intake AI resolved → selects the
|
|
969
969
|
// profile (marketplaces/classes/delivery/appetite); omit ⇒ generic
|
|
970
970
|
product: "multi-country-focus-search",// WHICH OF THE FOUR (OPTIONAL): one of products.mjs PRODUCT_IDS.
|
|
971
971
|
// Omit ⇒ the project/customer defaultProduct, else the product
|
|
@@ -987,7 +987,7 @@ export const EXAMPLE_JOB = {
|
|
|
987
987
|
projectKey: "console-ecosystem", // spec 62 (OPTIONAL): the PROJECT/engagement under the customer whose
|
|
988
988
|
// overlay (its own marketplaces/classes/sector/posture) rates this
|
|
989
989
|
// matter; omit ⇒ runs on the customer profile. Unknown key ⇒ clarify.
|
|
990
|
-
customer: "
|
|
990
|
+
customer: "Demo Brand Owner", // applicant/owner → affiliate self-exclusion set (§B3.2)
|
|
991
991
|
customerUnknown: false, // B5: true when the applicant is neither stated nor forwarder-implied —
|
|
992
992
|
// arms candidate-self classification + the late-bind watch (NEVER inferred from the mark)
|
|
993
993
|
// caseLaw — NOT A FIELD. The case-law and opposition reading is what a Full country search IS
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// come from report-data.json; the disposition comes from findings.json beside it in the same run dir,
|
|
20
20
|
// because report-data.json is the CLIENT cut and stopped serving the engine's placement key. A
|
|
21
21
|
// reader who finds the ordinal now points at `KURENA / Manageable / rebuttable` when the flag says
|
|
22
|
-
// `
|
|
22
|
+
// `BURROWELL / Medium / conceded` knows the run was republished, which is exactly the thing a bare id
|
|
23
23
|
// would have hidden.
|
|
24
24
|
|
|
25
25
|
import { randomUUID } from "node:crypto";
|
|
@@ -168,8 +168,8 @@ export const ACTION_KINDS = [...CONDITION_KINDS, ...ADVISORY_KINDS];
|
|
|
168
168
|
export const CLIENT_TIER_BY_COMPOSITE = { 1: "LOW", 2: "MANAGEABLE", 3: "MEDIUM", 4: "HIGH", 5: "VERY HIGH" };
|
|
169
169
|
|
|
170
170
|
// ── doc 50 — band mode (schema_version 4): the framework in force rates the matter ────────────────────
|
|
171
|
-
// A v4 finding carries `band` — one of the frozen framework manifest's ordered band words (
|
|
172
|
-
// "Medium", the Generic default says "Moderate",
|
|
171
|
+
// A v4 finding carries `band` — one of the frozen framework manifest's ordered band words (one ladder
|
|
172
|
+
// says "Medium", the Generic default says "Moderate", another has a "Low") — and NO composite/level/
|
|
173
173
|
// dispute_type (one rating authority; the retired scale is FORBIDDEN, not just optional). Presentation
|
|
174
174
|
// joins on the band's TONE (a closed enum in the manifest) so 4-band and 5-band ladders both land on the
|
|
175
175
|
// existing badge/gauge ramps without any per-framework code.
|
|
@@ -2441,6 +2441,56 @@ export function knockoutFindingViews(mark, { manifest = null } = {}) {
|
|
|
2441
2441
|
* Derived through knockoutFindingViews rather than re-deriving that fallback here — one projection
|
|
2442
2442
|
* decides what a finding's number is, and this cell cannot disagree with the sheet it points into.
|
|
2443
2443
|
*/
|
|
2444
|
+
// ── THE REVIEWER'S NOTES: WHAT EACH ONE IS ABOUT (tracker issues 331 A.4, 333 rule 5) ───────────────
|
|
2445
|
+
//
|
|
2446
|
+
// The knockout page prints a note about the REQUEST at the top, above the conflicts, and a note about
|
|
2447
|
+
// the NAME under that name's cards. A note saying the screen may have been scoped to the wrong market
|
|
2448
|
+
// is the most consequential line on the page and it used to sit under roughly 1,900 words.
|
|
2449
|
+
//
|
|
2450
|
+
// THE READER LIVES HERE BECAUSE TWO MODULES NEED THE SAME ANSWER. The renderer sorts the notes; the
|
|
2451
|
+
// predelivery lint warns a writer whose note will sort the way they did not intend. Two copies of this
|
|
2452
|
+
// regex would drift, and the drift would be silent — the page would file a note one way while the
|
|
2453
|
+
// reviewer told the writer it went the other.
|
|
2454
|
+
//
|
|
2455
|
+
// IT SORTS ON WHAT THE NOTE TALKS ABOUT, and the doctrine is written to match: a note about the request
|
|
2456
|
+
// NAMES the request. The word set spans both vocabularies deliberately — "dispatch" and "instructed"
|
|
2457
|
+
// are what runs written before tracker issue 333 say, "the request" and "was asked" are what runs
|
|
2458
|
+
// written after it say — so one reader serves the archive and the new doctrine at once.
|
|
2459
|
+
export const REQUEST_NOTE_WORDS = /\b(?:dispatch|the request|the requester|instructed|was asked)\b/i;
|
|
2460
|
+
|
|
2461
|
+
// Subjects that belong to the ASKING rather than to the name. A note on one of these that never names
|
|
2462
|
+
// the request will be filed under the name, which is why the lint flags it rather than the page
|
|
2463
|
+
// guessing: the fix is one clause in the note, and only its writer can add it.
|
|
2464
|
+
export const REQUEST_SUBJECT_WORDS =
|
|
2465
|
+
/\b(?:customer industry|client(?:'s)? (?:own )?(?:prior|earlier) use|prior use|intended goods|the goods we|wrong market|described as an? )/i;
|
|
2466
|
+
|
|
2467
|
+
/** One note, in either shape, as { text, about }. `about` is null when nothing has classified it. */
|
|
2468
|
+
export function knockoutNoteView(n) {
|
|
2469
|
+
if (n && typeof n === "object" && !Array.isArray(n)) {
|
|
2470
|
+
const about = String(n.about ?? "").trim().toLowerCase();
|
|
2471
|
+
return { text: String(n.text ?? n.note ?? "").trim(), about: about === "request" || about === "name" ? about : null };
|
|
2472
|
+
}
|
|
2473
|
+
return { text: String(n ?? "").trim(), about: null };
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
/**
|
|
2477
|
+
* A mark's notes, split into the two places the page prints them.
|
|
2478
|
+
*
|
|
2479
|
+
* The rater's own `about` wins outright where one is given; only an unclassified note meets the word
|
|
2480
|
+
* reader. That precedence is the whole design: it means a run written under a doctrine that no longer
|
|
2481
|
+
* uses these words is answered without them.
|
|
2482
|
+
*/
|
|
2483
|
+
export function splitKnockoutNotes(mark) {
|
|
2484
|
+
const all = (Array.isArray(mark?.purpleNotes) ? mark.purpleNotes : []).map(knockoutNoteView).filter((v) => v.text);
|
|
2485
|
+
const request = [];
|
|
2486
|
+
const name = [];
|
|
2487
|
+
for (const v of all) {
|
|
2488
|
+
const isRequest = v.about ? v.about === "request" : REQUEST_NOTE_WORDS.test(v.text);
|
|
2489
|
+
(isRequest ? request : name).push(v.text);
|
|
2490
|
+
}
|
|
2491
|
+
return { request, name, all: all.map((v) => v.text) };
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2444
2494
|
export function knockoutFindingRange(mark) {
|
|
2445
2495
|
const ords = knockoutFindingViews(mark).map((v) => v.ordinal).sort((a, b) => a - b);
|
|
2446
2496
|
const markName = String(mark?.name ?? "").trim();
|
package/driver/flag-snapshot.mjs
CHANGED
|
@@ -318,6 +318,26 @@ export function postureDisagreement(snapshot, live) {
|
|
|
318
318
|
"silent-output-change — a different engine answers differently and nobody is told");
|
|
319
319
|
differ("billing mode", snapshot.engine?.billing?.mode ?? null, live.engine?.billing?.mode ?? null,
|
|
320
320
|
"what a run costs, and who it is billed to");
|
|
321
|
+
// THE FIELD THAT DECIDES WHETHER A SEARCH CAN START, and the one this comparison could not see.
|
|
322
|
+
//
|
|
323
|
+
// The configuration page reads the LIVE posture and the New clearance screen reads this capture, so a
|
|
324
|
+
// box where they differ on this one boolean draws a green Engine row on one screen while the other
|
|
325
|
+
// replaces its start button with "no search engine is attached". That is not a hypothetical: it is
|
|
326
|
+
// what an outside user photographed, and he gave up on the product because the greener screen looked
|
|
327
|
+
// more authoritative. Both readings were correct about their own question and nothing compared them.
|
|
328
|
+
//
|
|
329
|
+
// Worse than silence, before this line: `disagrees` came back `[]` — which this page renders as "the
|
|
330
|
+
// last run ran under this same configuration". The one field they actually disagreed on was not in the
|
|
331
|
+
// comparison, so the page positively affirmed agreement while the two surfaces contradicted each other.
|
|
332
|
+
// STRINGS, NOT THE BOOLEANS THEMSELVES. Every other row here compares names and modes, so the browser
|
|
333
|
+
// contract parses `capture` and `live` with `asString` — handed `false` it yields null, and the row
|
|
334
|
+
// would reach the page with its two values blank and only the effect sentence left. The words are also
|
|
335
|
+
// the better answer for a reader: "found" against "not found" says it without a legend.
|
|
336
|
+
const found = (v) => (v === true ? "found" : v === false ? "not found" : null);
|
|
337
|
+
differ("engine program", found(snapshot.engine?.binaryPresent), found(live.engine?.binaryPresent),
|
|
338
|
+
"whether a NEW search can start — the engine that last ran and this deployment do not agree that the "
|
|
339
|
+
+ "engine program can be found, so one screen offers a search the other refuses. Restart the engine "
|
|
340
|
+
+ "service so it re-reads its PATH, or install the CLI where the service can see it");
|
|
321
341
|
|
|
322
342
|
// Flags: compare only names BOTH sides declare, for the same reason `differ` skips absent values —
|
|
323
343
|
// a build that adds a flag must not read as every older capture disagreeing with it.
|
package/driver/framework.mjs
CHANGED
|
@@ -102,7 +102,7 @@ export function parseFrameworkManifest(raw) {
|
|
|
102
102
|
// `rootDir` may be a string (legacy: join against it) OR a resolver function taking the manifest's
|
|
103
103
|
// skills-relative path and returning an absolute one — the layered overlay-over-base lookup
|
|
104
104
|
// (driver.config.resolveSkillPath). The driver MUST resolve a framework exactly as the agent does, or the
|
|
105
|
-
// two read different files: that divergence is what killed
|
|
105
|
+
// two read different files: that divergence is what killed a test account's first run (framework_manifest_missing
|
|
106
106
|
// against the bundled tree while the agent was pointed at the config store).
|
|
107
107
|
export function loadFrameworkManifest(rootDir, fwPath) {
|
|
108
108
|
const rel = manifestPathFor(fwPath);
|
package/driver/gateway.mjs
CHANGED
|
@@ -13,7 +13,8 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, wri
|
|
|
13
13
|
import { join, dirname, basename } from "node:path";
|
|
14
14
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
|
-
import { config, resolveModel, modelFamily, modelSnapshotKind, envOn, envGateOn } from "./driver.config.mjs";
|
|
16
|
+
import { config, resolveModel, modelFamily, modelSnapshotKind, envOn, envGateOn, preflightEngineBinary } from "./driver.config.mjs";
|
|
17
|
+
import { probeCliVersion } from "./engine/cli-version.mjs";
|
|
17
18
|
import { stageLog, runLog, note, outputMeta } from "./log.mjs";
|
|
18
19
|
// — the closed disposition set has ONE author; this file dictates it and must not retype it.
|
|
19
20
|
import { DISPOSITIONS, POSITION_REQUIRED_DISPOSITIONS } from "./findings-model.mjs";
|
|
@@ -908,8 +909,8 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
908
909
|
// until the wall. ONE free rung per ladder is the concession; a second zero-usage warm turn in the
|
|
909
910
|
// same ladder is charged, and the record says so rather than leaving the difference to be inferred.
|
|
910
911
|
//
|
|
911
|
-
// PROVISIONAL, and deliberately one line to change: the number below is a routed parameter (2026-08-22,
|
|
912
|
-
//
|
|
912
|
+
// PROVISIONAL, and deliberately one line to change: the number below is a routed parameter (2026-08-22),
|
|
913
|
+
// not a measured optimum. Nobody has yet counted how often a second free rung would have
|
|
913
914
|
// converted, and until someone does, 1 is the smallest concession that closes the defect.
|
|
914
915
|
//
|
|
915
916
|
// THE CHARGING BRANCH CANNOT FIRE TODAY, and that is stated rather than left to be discovered. `warm`
|
|
@@ -1147,6 +1148,20 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1147
1148
|
// may repoint, and recorded beside a dated one they read identically. null when there is nothing to
|
|
1148
1149
|
// judge, never collapsed into "alias".
|
|
1149
1150
|
const modelSnapshot = modelSnapshotKind(modelActual);
|
|
1151
|
+
// WHICH BUILD OF THE TOOL SERVED THIS TURN. `modelSnapshot` above answers "did the model move";
|
|
1152
|
+
// this answers "did the tool move", and the two have one symptom — a run whose judgment differs from
|
|
1153
|
+
// last week's. Captured at dispatch rather than read back afterwards, because the binary a reader
|
|
1154
|
+
// could probe later is not necessarily the one that ran.
|
|
1155
|
+
//
|
|
1156
|
+
// Written as an object with its own `probe` field, so an unreadable version is a RECORDED state
|
|
1157
|
+
// rather than a missing one. Omitting it on failure would collapse "asked and could not say" into
|
|
1158
|
+
// "written before anybody asked", which is the distinction the field exists for. One spawn per
|
|
1159
|
+
// binary per process; a probe never throws, because taking down a dispatch to record a version
|
|
1160
|
+
// would be a worse defect than the gap it closes.
|
|
1161
|
+
const cli = (() => {
|
|
1162
|
+
try { return probeCliVersion(preflightEngineBinary(process.env)?.resolved ?? null); }
|
|
1163
|
+
catch (e) { return { version: null, probe: "unreadable", why: String(e?.message ?? e).slice(0, 160) }; }
|
|
1164
|
+
})();
|
|
1150
1165
|
if (modelActual) lastModelWire = modelActual; // — never overwritten with null
|
|
1151
1166
|
// The comparison is by FAMILY (driver.config modelFamily), because `--model haiku` legitimately comes
|
|
1152
1167
|
// back as `claude-haiku-4-5-20251001`. THREE-VALUED: null when either side names no family this
|
|
@@ -1576,6 +1591,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1576
1591
|
// Written even on the rows where they are null, so "this engine cannot report" stays visibly
|
|
1577
1592
|
// different from "this record predates the gauge".
|
|
1578
1593
|
modelActual, modelBasis, modelSnapshot, modelMismatch,
|
|
1594
|
+
cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}),
|
|
1579
1595
|
// W3 billing telemetry: which engine ran + the RESOLVED billing mode (subscription vs api-key). This
|
|
1580
1596
|
// records INTENT (the mode the engine was configured to bill under), not independent billing evidence
|
|
1581
1597
|
// — the actual proof is the provider console (claude's stream also reports apiKeySource; codex does
|
|
@@ -1711,6 +1727,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1711
1727
|
//: the spine carries the same pair as the per-stage log, or the two disagree about what
|
|
1712
1728
|
// ran. `model` stays the requested resolution (its existing readers); `modelActual` is the wire.
|
|
1713
1729
|
model: modelRequested, modelActual, modelBasis, modelSnapshot, modelMismatch,
|
|
1730
|
+
cliVersion: cli.version, cliVersionProbe: cli.probe, ...(cli.why ? { cliVersionWhy: cli.why } : {}),
|
|
1714
1731
|
wrote, warm: warm || undefined, warmEscalated: attempt === warmEscalatedAt || undefined,
|
|
1715
1732
|
rescued: rescued ?? undefined, killed: killed || undefined,
|
|
1716
1733
|
quiescentMs: Number.isFinite(quiescentMs) ? Math.round(quiescentMs) : undefined, // — see the per-stage row
|
|
@@ -274,7 +274,7 @@ export function settleProgress(agent, now = Date.now()) {
|
|
|
274
274
|
// disposed of on the box.
|
|
275
275
|
//
|
|
276
276
|
// **Owner ruling, 2026-08-24, SUPERSEDING THIS SWEEP'S HALF OF IT: failed runs' notification packets get
|
|
277
|
-
// the same re-drop cover as delivered ones.**
|
|
277
|
+
// the same re-drop cover as delivered ones.**
|
|
278
278
|
//
|
|
279
279
|
// BOTH ARE KEPT BECAUSE BOTH ARE STILL TRUE, and reading them as a reversal is the mistake to avoid. A
|
|
280
280
|
// failed run owes the CLIENT no report — that is 2026-08-22, and `scripts/e2e.mjs`'s delivery assertion
|
package/driver/package.json
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.4",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=22.
|
|
9
|
+
"node": ">=22.13.0"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "node ../scripts/test-run.mjs node ../scripts/test-fast.mjs test/*.test.mjs",
|
|
@@ -14,6 +14,6 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"exceljs": "^4.4.0",
|
|
17
|
-
"undici": "^
|
|
17
|
+
"undici": "^7.29.1"
|
|
18
18
|
}
|
|
19
19
|
}
|
package/driver/party-facts.mjs
CHANGED
|
@@ -226,8 +226,8 @@ const corpusHasKind = (set, kind) => {
|
|
|
226
226
|
/**
|
|
227
227
|
* The form of a party's name to match ENTRIES against.
|
|
228
228
|
*
|
|
229
|
-
* Register owners carry legal forms ("
|
|
230
|
-
* web uses ("
|
|
229
|
+
* Register owners carry legal forms ("Foxglade Therapeutics SA"); web candidate titles carry whatever the
|
|
230
|
+
* web uses ("Foxglade Therapeutics"). That asymmetry is systematic — one corpus is the register, the other
|
|
231
231
|
* is search results — so requiring the full string admits SOME entries about a party and excludes others
|
|
232
232
|
* purely by name form. A half-scoped corpus is the failure this scoping was built to avoid, inverted: it
|
|
233
233
|
* still reports a contradiction, now against a source the run plainly holds.
|
package/driver/phase0.mjs
CHANGED
|
@@ -87,7 +87,7 @@ export function todayISO(now = new Date()) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// The resolved customer = the profile key (profiles.mjs resolveProfile, forwarder-domain only). This was
|
|
90
|
-
// a
|
|
90
|
+
// a customer-vs-generic HARDCODE (D4.1); the engine is already profile-driven, so resolveProfile is
|
|
91
91
|
// the single source of truth. ctx.customer is telemetry-only (the run-start log line); per-customer
|
|
92
92
|
// DELIVERY reads the FROZEN sidecar ctx.profile.profileKey (which wins on resume) — never read
|
|
93
93
|
// ctx.customer for a delivery decision, or a profiles/ edit between run and resume would diverge.
|