clearotron 0.3.0-beta.2 → 0.3.0-beta.3
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 +19 -0
- package/bin/onboard.mjs +65 -6
- package/bin/start.mjs +7 -1
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +22 -0
- package/driver/driver.config.mjs +5 -0
- package/driver/engine/anthropic-agent.mjs +34 -17
- package/driver/gateway.mjs +76 -15
- package/driver/package.json +1 -1
- package/driver/portal-service.mjs +1 -1
- package/driver/profile-service.mjs +31 -3
- package/driver/publish/index.mjs +12 -9
- package/driver/publish/knockout.mjs +4 -2
- package/driver/publish/office-record-links.mjs +56 -21
- package/driver/recipe-service.mjs +14 -5
- package/driver/record-origins.mjs +14 -0
- package/driver/suite-census.json +28 -22
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/driver.mjs +2 -0
- package/mcp-server/lib/knockout.mjs +2 -2
- package/mcp-server/lib/options.mjs +9 -2
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-CcFjgM78.js → index-C1gpdkyH.js} +17 -8
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/clarivate/src/core.js +5 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/e2e-unread-terminals.mjs +1 -1
- package/scripts/e2e.mjs +114 -9
- package/scripts/revisit-render-check.mjs +22 -4
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/store-in-repo.mjs +50 -2
package/INSTALL.md
CHANGED
|
@@ -491,6 +491,25 @@ matches, the neutral Generic default applies.
|
|
|
491
491
|
- **Your real customers live outside the repo.** Point `CLEAROTRON_CUSTOMERS_DIR` at your own private
|
|
492
492
|
config store and the engine loads *those* accounts instead. **Same engine, different config path** —
|
|
493
493
|
the code carries no customer identities.
|
|
494
|
+
|
|
495
|
+
Two things go with it, and both are refusals rather than preferences:
|
|
496
|
+
|
|
497
|
+
- **`PROFILE_REPO_ROOT` moves too.** The customer directory has to sit inside the repository that
|
|
498
|
+
variable names, because editing a profile is a commit. Point one somewhere new and leave the other
|
|
499
|
+
behind and the portal and the profile service both refuse to start, naming both variables.
|
|
500
|
+
- **That repository needs a `user.name` and a `user.email` of its own.** Saves are committed under
|
|
501
|
+
the name of whoever asked for them, but git also records who *made* the commit, and it will not
|
|
502
|
+
commit at all without one. A service account usually has no global git identity, so a store created
|
|
503
|
+
by hand needs its own:
|
|
504
|
+
|
|
505
|
+
```bash
|
|
506
|
+
git init -b main /srv/clearotron-store
|
|
507
|
+
git -C /srv/clearotron-store config user.name "clearotron local install"
|
|
508
|
+
git -C /srv/clearotron-store config user.email "clearotron@example.com"
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
`clearotron start` does this for the store it creates. A store you make yourself does not get it,
|
|
512
|
+
and the symptom is the first save failing at a commit rather than anything about profiles.
|
|
494
513
|
- **Run data is external too.** Published reports, audits, and per-run state go to the archive pool at
|
|
495
514
|
`CLEAROTRON_REPORTS_DIR`. Nothing customer-specific is committed to the repository.
|
|
496
515
|
|
package/bin/onboard.mjs
CHANGED
|
@@ -97,7 +97,7 @@ import { processTable } from "../shared/process-table.mjs"; // — /proc is no
|
|
|
97
97
|
import { programsFromAnotherCheckout } from "../shared/checkout-move.mjs";
|
|
98
98
|
import { entrypointOf } from "../driver/systemd/install-census.mjs"; // one ExecStart parser
|
|
99
99
|
import { overlayReport, renderOverlayReport } from "../shared/doctrine-overlay.mjs"; // — the doctor reports the overlay
|
|
100
|
-
import { whereSavesGo } from "../shared/store-in-repo.mjs"; // — doctor says where a portal save goes once it is committed
|
|
100
|
+
import { whereSavesGo, storeCommitRefusal, storeInRepo, storeOutsideRepoMessage, resolveStoreRepoRoot } from "../shared/store-in-repo.mjs"; // — doctor says where a portal save goes once it is committed, and why saved searches are off
|
|
101
101
|
import { engineInventory, engineMode, ENGINE_MODES } from "../driver/config-inventory.mjs"; //
|
|
102
102
|
import { probeEngineTurn, probeFailureText, PROBE_MODEL, PROBE_TIMEOUT_SEC, engineEnvKeys } from "../driver/engine/probe.mjs";
|
|
103
103
|
import { runRequiredNames, missingRequirements, REGISTER_ENV, ENGINE_ENV } from "../driver/run-requirements.mjs"; // the order-time gate's own question, asked here rather than restated
|
|
@@ -1799,6 +1799,41 @@ export async function runCheck() {
|
|
|
1799
1799
|
}
|
|
1800
1800
|
}
|
|
1801
1801
|
|
|
1802
|
+
// — SAVED SEARCHES, JUDGED BY THE RULE THE PORTAL APPLIES WHEN IT STARTS. The portal switches them off,
|
|
1803
|
+
// and answers every saved-search route "not found", when the store is unset or sits outside the
|
|
1804
|
+
// repository its saves are committed in. Its boot log said so and nothing an operator runs did, so an
|
|
1805
|
+
// install whose Custom searches could never load passed this command. Same resolver, same containment
|
|
1806
|
+
// check, read from the services' environment for the reason the roster lines give; when that could not
|
|
1807
|
+
// be read, the lines above already said so and nothing is judged here. A store that is configured but
|
|
1808
|
+
// holds a file that cannot be read fails every company's saved searches, in the portal and the
|
|
1809
|
+
// connector alike, so that is read here too.
|
|
1810
|
+
if (!hosted || serviceKnown) {
|
|
1811
|
+
const recipesSet = hosted ? effectiveForService("CLEAROTRON_RECIPES_DIR") : effective("CLEAROTRON_RECIPES_DIR");
|
|
1812
|
+
const recipesDir = recipesSet?.v || null;
|
|
1813
|
+
if (!recipesDir) {
|
|
1814
|
+
info("saved searches are off: CLEAROTRON_RECIPES_DIR is not set, so Custom searches in the portal and the "
|
|
1815
|
+
+ "connector offer none. Name a directory inside a git repository to switch them on");
|
|
1816
|
+
} else {
|
|
1817
|
+
// Layered as effectiveForService layers it — this command's environment over the services' file — so
|
|
1818
|
+
// the repository root is read from the same place the store directory above was.
|
|
1819
|
+
const storeEnv = { ...(serviceFileEnv ?? {}), ...process.env };
|
|
1820
|
+
const resolved = resolveStoreRepoRoot({ names: ["RECIPE_REPO_ROOT", "PROFILE_REPO_ROOT"], fallback: REPO, env: storeEnv });
|
|
1821
|
+
const reach = storeInRepo(recipesDir, resolved.root);
|
|
1822
|
+
if (!reach.ok) {
|
|
1823
|
+
warn(`saved searches are OFF: ${storeOutsideRepoMessage({ storeVar: "CLEAROTRON_RECIPES_DIR", storeDir: reach.store, repoVar: "RECIPE_REPO_ROOT", repoRoot: reach.repo })} `
|
|
1824
|
+
+ `The repository came from ${resolved.from}. Until this is fixed the portal answers every saved-search request as not found`);
|
|
1825
|
+
} else {
|
|
1826
|
+
const { loadRecipes } = await import("../driver/search-policy.mjs");
|
|
1827
|
+
let unreadable = null;
|
|
1828
|
+
try { loadRecipes({ dir: recipesDir, force: true }); } catch (e) { unreadable = String(e?.message ?? e).split("\n")[0]; }
|
|
1829
|
+
if (unreadable) {
|
|
1830
|
+
warn(`saved searches cannot be read from ${recipesDir}: ${unreadable}. Every company's saved searches fail `
|
|
1831
|
+
+ "to load, in the portal and the connector, until that file is fixed");
|
|
1832
|
+
} else ok(`saved searches are read from ${recipesDir}, and saves are committed in ${reach.repo}`);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1802
1837
|
// — WHICH DOCTRINE FILES THIS INSTALL OVERRIDES, AND WHETHER OURS HAVE MOVED UNDER THEM.
|
|
1803
1838
|
//
|
|
1804
1839
|
// The report itself shipped in and worked, reachable only as `npm run doctrine-report` — a name
|
|
@@ -1846,7 +1881,21 @@ export async function runCheck() {
|
|
|
1846
1881
|
}
|
|
1847
1882
|
|
|
1848
1883
|
say("\n Register provider");
|
|
1849
|
-
|
|
1884
|
+
// WHAT THE SERVICES WILL SEARCH, READ AS THE ORDER-TIME CHECK BELOW READS IT. This section asked the
|
|
1885
|
+
// shell and this command's .env, so in a fresh terminal on a hosted box it told a working install that
|
|
1886
|
+
// no register was selected, one screen above "nothing a search is refused for is missing from the units'
|
|
1887
|
+
// environment": one install, two answers, and the fix it named was already set. effectiveForService is
|
|
1888
|
+
// the reader that check asks, so the two lines cannot disagree; where it cannot read the units, this
|
|
1889
|
+
// says it could not look rather than reporting the register absent.
|
|
1890
|
+
// Only a HOSTED box has a second environment to read; with no units, the services are started from
|
|
1891
|
+
// this command's own file, and `effective` has always read and labelled exactly that.
|
|
1892
|
+
const serviceValue = hosted ? effectiveForService : effective;
|
|
1893
|
+
const prov = serviceKnown ? serviceValue("CLEAROTRON_DATABASE") : null;
|
|
1894
|
+
if (!serviceKnown) {
|
|
1895
|
+
info(`the units are installed but their environment could not be read (${unitEnv?.why ?? "no reason given"}) — `
|
|
1896
|
+
+ "which register the services search, and whether its keys are set, is not judged here: a failure to look, not a finding");
|
|
1897
|
+
}
|
|
1898
|
+
else
|
|
1850
1899
|
// `blocking`, not `warn` and not `problem`. The exit status is a CONTRACT — an absence reports and
|
|
1851
1900
|
// exits 0, a misconfiguration exits 1, and onboard-wizard.test.mjs holds it — so this cannot become a
|
|
1852
1901
|
// `problem` however much it stops the reader: an install that has not chosen a register yet is
|
|
@@ -1865,7 +1914,7 @@ export async function runCheck() {
|
|
|
1865
1914
|
else {
|
|
1866
1915
|
ok(`${spec.id} — ${spec.label} (${prov.from})`);
|
|
1867
1916
|
for (const k of spec.credentials) {
|
|
1868
|
-
const c =
|
|
1917
|
+
const c = serviceValue(k);
|
|
1869
1918
|
// issue 1871 — SET, not WORKING, and the line now says which. An operator reads a tick as "this
|
|
1870
1919
|
// works"; this one is equally true of a valid key, an expired key, a key scoped to the wrong
|
|
1871
1920
|
// account and forty characters of nonsense. --probe-providers is what settles it.
|
|
@@ -1876,7 +1925,7 @@ export async function runCheck() {
|
|
|
1876
1925
|
// and never as a problem, but never silently either: the reader has to know which offices this
|
|
1877
1926
|
// box will not reach before they read a report that says nothing was found there.
|
|
1878
1927
|
for (const k of spec.optionalCredentials ?? []) {
|
|
1879
|
-
const c =
|
|
1928
|
+
const c = serviceValue(k);
|
|
1880
1929
|
if (c) ok(`${k} present (${c.from}) — presence only; add --probe-providers to prove it retrieves`);
|
|
1881
1930
|
else info(`${k} is NOT set — ${spec.id} will run without it and DISCLOSE the offices it cannot reach as deferred coverage. Set it to search them.`);
|
|
1882
1931
|
}
|
|
@@ -1884,8 +1933,10 @@ export async function runCheck() {
|
|
|
1884
1933
|
}
|
|
1885
1934
|
|
|
1886
1935
|
say("\n Research provider");
|
|
1887
|
-
|
|
1888
|
-
|
|
1936
|
+
// The same reader as the register, for the same reason: this is a key the services use.
|
|
1937
|
+
const px = serviceKnown ? serviceValue("PERPLEXITY_API_KEY") : null;
|
|
1938
|
+
if (!serviceKnown) info("the units' environment could not be read, so whether the services hold PERPLEXITY_API_KEY is not judged here — a failure to look, not a finding");
|
|
1939
|
+
else if (px) ok(`PERPLEXITY_API_KEY present (${px.from}) — presence only; add --probe-providers to prove it answers`);
|
|
1889
1940
|
else info("PERPLEXITY_API_KEY is not set — the three clearance searches carry the common-law grid and cannot switch it off, so a clearance stops before it starts, and names the missing key; a Knockout search still runs and discloses the half it skipped");
|
|
1890
1941
|
|
|
1891
1942
|
// ── — THE LANES A PRODUCT DECLARES IT NEEDS, BEFORE A REPORT NAMES THEM ──
|
|
@@ -3644,6 +3695,14 @@ try {
|
|
|
3644
3695
|
candidate["CLEAROTRON_CUSTOMERS_DIR"] = join(cfg, "profiles");
|
|
3645
3696
|
candidate.PROFILE_REPO_ROOT = cfg; // no alias row — this name is current
|
|
3646
3697
|
for (const k of ["CLEAROTRON_CUSTOMERS_DIR", "PROFILE_REPO_ROOT"]) ok(`${k}=${candidate[k]}`);
|
|
3698
|
+
// A REPOSITORY ALREADY THERE IS ASKED NOW, while the operator is still here, whether it can record a
|
|
3699
|
+
// save. `clearotron start` gives a store it creates an identity of its own; one made by hand has none
|
|
3700
|
+
// unless somebody set it, and on a machine with no global identity the first company created in the
|
|
3701
|
+
// portal is refused. Said here, with the command, rather than discovered on that first company.
|
|
3702
|
+
if (existsSync(join(cfgAbs, ".git"))) {
|
|
3703
|
+
const cannot = storeCommitRefusal(cfgAbs);
|
|
3704
|
+
if (cannot) warn(`${cannot.message}. Until then, creating a company in the portal is refused.`);
|
|
3705
|
+
}
|
|
3647
3706
|
|
|
3648
3707
|
// CLEAROTRON_INSTRUCTIONS_DIR IS DELIBERATELY NOT WRITTEN (found in review).
|
|
3649
3708
|
//
|
package/bin/start.mjs
CHANGED
|
@@ -109,7 +109,7 @@ async function runTables() {
|
|
|
109
109
|
return { registers: PROVIDERS, engines: ENGINE_BINARIES, defaultEngine: RUN_DEFAULT_ENGINE };
|
|
110
110
|
}
|
|
111
111
|
import { spawn, spawnSync, execFileSync } from "node:child_process";
|
|
112
|
-
import { storeInRepo, storeOutsideRepoMessage } from "../shared/store-in-repo.mjs"; //
|
|
112
|
+
import { storeInRepo, storeOutsideRepoMessage, storeCommitRefusal } from "../shared/store-in-repo.mjs"; //
|
|
113
113
|
import { stdioConnectOffer } from "../shared/stdio-connect.mjs";
|
|
114
114
|
import { mergeEnvFile } from "../shared/env-file-merge.mjs";
|
|
115
115
|
import { mcpOriginFor } from "../shared/lane-address.mjs"; // — one author for the origin
|
|
@@ -1252,6 +1252,12 @@ if (isMain) {
|
|
|
1252
1252
|
// search is what will fail.
|
|
1253
1253
|
err(` WARNING: could not initialise the saved-search store at ${paths.configStore} (${String(e?.message ?? e)}). Searches will list and run; SAVING one will fail until this is a git repository.`);
|
|
1254
1254
|
}
|
|
1255
|
+
} else {
|
|
1256
|
+
// AN ADOPTED STORE IS ASKED WHETHER IT CAN RECORD A SAVE, HERE, not at the first save. A repository
|
|
1257
|
+
// made by hand has no identity unless somebody gave it one, and on a machine with no global identity
|
|
1258
|
+
// the first company created in the portal is then refused. The one created above sets its own.
|
|
1259
|
+
const cannot = storeCommitRefusal(paths.configStore);
|
|
1260
|
+
if (cannot) err(` WARNING: ${cannot.message}. Until then, creating a company or saving a search is refused.`);
|
|
1255
1261
|
}
|
|
1256
1262
|
|
|
1257
1263
|
// ── THE DEMO'S OWN STORE, WITH ITS COMPANY IN IT ────────────────────────────────────────────────
|
package/build-info.json
CHANGED
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- a782aad: Fixed: Creating a company is refused, with nothing left behind, when the configuration store cannot record it. The company used to be created anyway, with no record of who made it or when, and its organisation was given access to it.
|
|
8
|
+
|
|
9
|
+
A store with no git identity is the usual cause on a new machine, and the refusal names the command that fixes it. Setup and `clearotron start` now check a store they adopt for this straight away.
|
|
10
|
+
- d1ef225: Fixed: A stage stopped at its time limit now records the output it actually produced. It used to record a small fraction, so a stage that was working read as one that had stalled.
|
|
11
|
+
|
|
12
|
+
The token totals `clearotron tokens` reports for runs with a stopped stage now include that output.
|
|
13
|
+
- 0ff42d1: Fixed: `clearotron doctor` now says when saved searches are switched off and why, and when a saved search file cannot be read.
|
|
14
|
+
|
|
15
|
+
An assistant asking for saved searches is told when they could not be read, instead of being told there are none.
|
|
16
|
+
- a782aad: Fixed: `clearotron doctor` reports the register and the research key the background services will use, read from the file they read. Run from a new terminal, it used to say no register was selected on an install whose searches were running.
|
|
17
|
+
- 0ff42d1: Fixed: Global config appears in the account menu only for people who can open it. Someone managing one organisation used to be offered it, and the page then said it was not available.
|
|
18
|
+
|
|
19
|
+
Clearances and People no longer ask for installation-wide data that a manager of one organisation cannot see.
|
|
20
|
+
- a782aad: Fixed: A clearance or knockout searched through Compumark now links each register record to the trade mark office's own page for it. Those records used to show an internal reference nobody could open.
|
|
21
|
+
|
|
22
|
+
The offices linked are the United States, the European Union, the United Kingdom, Canada, Australia, Switzerland, France, Norway, Sweden and WIPO. A record from any other office is cited by its number, and the report says why once, under the findings.
|
|
23
|
+
- a782aad: Fixed: Saved searches work on a fresh install and in the demo, for every company, Generic included. They used to fail to load for every company, with a message saying to try again shortly.
|
|
24
|
+
|
|
3
25
|
## 0.3.0-beta.2
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/driver/driver.config.mjs
CHANGED
|
@@ -1110,6 +1110,11 @@ export const PROVIDERS = {
|
|
|
1110
1110
|
office: row?.office ?? null,
|
|
1111
1111
|
application_date: row?.application_date ?? null,
|
|
1112
1112
|
registration_date: row?.registration_date ?? null,
|
|
1113
|
+
// The office's own numbers: publish addresses the filing at the office's page from these, the
|
|
1114
|
+
// way it does for this register's clearance findings. Compumark states no filing route or IR
|
|
1115
|
+
// number of its own, so a Madrid designation is addressed as the national filing it is listed as.
|
|
1116
|
+
application_number: row?.application_number ?? null,
|
|
1117
|
+
registration_number: row?.registration_number ?? null,
|
|
1113
1118
|
// hasPublicRecordUrl:false on this provider — there is no per-record page to link, and a
|
|
1114
1119
|
// fabricated one would be worse than none.
|
|
1115
1120
|
record_url: null,
|
|
@@ -467,19 +467,32 @@ export const anthropicAgentEngine = {
|
|
|
467
467
|
let rateLimitEvent = null; // a 429 session-cap rejection rides a `rate_limit_event` (status:"rejected" + resetsAt)
|
|
468
468
|
// Streamed-usage accumulator: what the turn PROVABLY moved, observed from the stream itself, so a
|
|
469
469
|
// killed turn is never journalled as usage:null when millions of tokens moved (the "137 + usage:null
|
|
470
|
-
// ⇒ mislabelled transient/lane-wedge" class).
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
470
|
+
// ⇒ mislabelled transient/lane-wedge" class).
|
|
471
|
+
//
|
|
472
|
+
// KEYED BY MESSAGE, AND EACH FIELD IS THE LARGEST READING SEEN FOR THAT MESSAGE. One API call's usage
|
|
473
|
+
// reaches the stream several times: on `message_start`, on every `assistant` event (one per content
|
|
474
|
+
// block), and on its `message_delta`, whose output count is the call's own running total. Summing the
|
|
475
|
+
// assistant events counted a call's input and cache once per block and its output at whatever it
|
|
476
|
+
// was when the block went out, and dropping the delta lost the rest: a turn killed after 75 calls
|
|
477
|
+
// journalled 810 output tokens. Which event carries the final count need not be known here, and the
|
|
478
|
+
// order is inferred rather than recorded, so every reading is kept and the largest wins. A delta names
|
|
479
|
+
// no message, so it belongs to the one its last `message_start` opened; an assistant event with no id
|
|
480
|
+
// and no open message is a call of its own, as it always was.
|
|
481
|
+
const perMessage = new Map();
|
|
482
|
+
let openMessage = null, anonMessages = 0;
|
|
483
|
+
const foldUsage = (key, u) => {
|
|
484
|
+
if (!u) return;
|
|
485
|
+
const was = perMessage.get(key);
|
|
486
|
+
perMessage.set(key, was
|
|
487
|
+
? { input: Math.max(was.input, u.input), output: Math.max(was.output, u.output),
|
|
488
|
+
cacheRead: Math.max(was.cacheRead, u.cacheRead), cacheWrite: Math.max(was.cacheWrite, u.cacheWrite) }
|
|
489
|
+
: { input: u.input, output: u.output, cacheRead: u.cacheRead, cacheWrite: u.cacheWrite });
|
|
490
|
+
};
|
|
475
491
|
const streamedUsage = () => {
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
input
|
|
479
|
-
|
|
480
|
-
cacheRead: streamTotals.cacheRead + p.cacheRead,
|
|
481
|
-
cacheWrite: streamTotals.cacheWrite + p.cacheWrite,
|
|
482
|
-
};
|
|
492
|
+
const u = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
493
|
+
for (const m of perMessage.values()) {
|
|
494
|
+
u.input += m.input; u.output += m.output; u.cacheRead += m.cacheRead; u.cacheWrite += m.cacheWrite;
|
|
495
|
+
}
|
|
483
496
|
u.total = u.input + u.output + u.cacheRead + u.cacheWrite;
|
|
484
497
|
return u.total > 0 ? u : null; // zero observed movement stays null — a never-admitted turn must keep classifying as a lane wedge
|
|
485
498
|
};
|
|
@@ -703,9 +716,9 @@ export const anthropicAgentEngine = {
|
|
|
703
716
|
periodStart = now; periodChunk = chunkSeq;
|
|
704
717
|
}
|
|
705
718
|
syncOpenAsk();
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
719
|
+
// This block's usage, folded into its own call: the largest reading wins, so a block's early count
|
|
720
|
+
// never replaces the delta's running total, and a second block of the same call adds nothing twice.
|
|
721
|
+
foldUsage(ev.message?.id ?? openMessage ?? `__assistant_${++anonMessages}`, mapUsage(ev.message?.usage));
|
|
709
722
|
progress();
|
|
710
723
|
}
|
|
711
724
|
else if (ev.type === "user") {
|
|
@@ -737,8 +750,12 @@ export const anthropicAgentEngine = {
|
|
|
737
750
|
}
|
|
738
751
|
else if (ev.type === "stream_event") {
|
|
739
752
|
const t = ev.event?.type;
|
|
740
|
-
if (t === "message_start") {
|
|
741
|
-
|
|
753
|
+
if (t === "message_start") {
|
|
754
|
+
openMessage = ev.event.message?.id ?? `__start_${++anonMessages}`;
|
|
755
|
+
foldUsage(openMessage, mapUsage(ev.event.message?.usage));
|
|
756
|
+
progress();
|
|
757
|
+
}
|
|
758
|
+
else if (t === "message_delta") { foldUsage(openMessage ?? "__delta_without_start", mapUsage(ev.event?.usage)); progress(); }
|
|
742
759
|
else if (t === "content_block_delta" || t === "content_block_start") progress();
|
|
743
760
|
// THINKING GAUGE (partials): the earliest tells. Any one is sufficient; belt-and-braces so a
|
|
744
761
|
// display-mode or CLI-version change cannot silently blind the gauge.
|
package/driver/gateway.mjs
CHANGED
|
@@ -1052,6 +1052,10 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1052
1052
|
grant: gatherAllowedTools,
|
|
1053
1053
|
})
|
|
1054
1054
|
: null;
|
|
1055
|
+
// The start of this attempt's window in the per-run refusal journals. Taken before the witness and
|
|
1056
|
+
// the snapshot, which only widens the window by their own time: an earlier attempt's refusals all
|
|
1057
|
+
// precede its own settle, so they stay outside it.
|
|
1058
|
+
const dispatchedAt = Date.now();
|
|
1055
1059
|
for (const line of describeMethodologyDrift(witnessStageMethodology(runDir, name, effMessage, engineResolveSkill)))
|
|
1056
1060
|
note(`[${name}] ${line}`);
|
|
1057
1061
|
// — the frozen judged-by set, hashed into THIS PROCESS'S MEMORY before the seat runs. Never
|
|
@@ -1368,7 +1372,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1368
1372
|
// ONE artifact judgement, shared by both rescues below (the rule: never a second, drifting copy of
|
|
1369
1373
|
// the contract). Present + written by THIS attempt (the per-attempt snapshot — an inherited file and
|
|
1370
1374
|
// equally an earlier attempt's file never rescue a failed turn) + passes the stage's own validator.
|
|
1371
|
-
const attemptWroteTruth = () => {
|
|
1375
|
+
const attemptWroteTruth = (why = null) => {
|
|
1372
1376
|
// — the rescues judge with `validate` directly rather than through judgeArtifacts, so the union
|
|
1373
1377
|
// has to run here too or a rescued turn would be refused for rows the form already holds. Idempotent,
|
|
1374
1378
|
// so the double call on the normal path costs a regeneration and changes nothing.
|
|
@@ -1381,16 +1385,21 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1381
1385
|
// answer for a killed-in-the-gap attempt is no. Idempotent, so the double call costs a regeneration.
|
|
1382
1386
|
const pu = syncPlacementForm(files);
|
|
1383
1387
|
if (pu) lastPlacementUnion = pu;
|
|
1388
|
+
// `why`, when a caller passes one, is told WHICH file failed and on what, so a refusal can name both.
|
|
1384
1389
|
return files.every((f) => {
|
|
1385
1390
|
const now = statOf(f);
|
|
1386
|
-
|
|
1387
|
-
if (
|
|
1391
|
+
const no = (cause, reason) => { if (why) Object.assign(why, { file: rel(f), cause, ...(reason ? { reason } : {}) }); return false; };
|
|
1392
|
+
if (now === null) return no("absent");
|
|
1393
|
+
if (now === preArtifact.get(f)) return no("not-written-by-this-attempt");
|
|
1394
|
+
if (validate) { const v = validate(f, readFileSync(f, "utf8")); if (!v.ok) return no("invalid", v.reason ? String(v.reason) : undefined); }
|
|
1388
1395
|
return true;
|
|
1389
1396
|
});
|
|
1390
1397
|
};
|
|
1391
1398
|
let rescued = null;
|
|
1392
1399
|
let quiescentMs = null; //: how long the artifact had been untouched when the turn settled
|
|
1393
|
-
let rescueRefused = null; //: WHICH of the rescue's
|
|
1400
|
+
let rescueRefused = null; //: WHICH of the rescue's four causes refused — on the row, not only in a note
|
|
1401
|
+
let rescueRefusedFile = null, rescueRefusedReason = null; //: and which file, and on what
|
|
1402
|
+
let attemptRefusals = null; //: this dispatch's refusals of a tool-written artifact, on a timeout
|
|
1394
1403
|
if (fail && /^nonzero_exit_/.test(fail) && files.length) {
|
|
1395
1404
|
if (killClass || killSeen) {
|
|
1396
1405
|
note(`[${name}] ${fail} with a kill-class attempt in this ladder — the exit-1 rescue stays CLOSED (a killed turn's artifact may be torn mid-write and a validator cannot prove it whole); failing honestly instead`);
|
|
@@ -1427,16 +1436,28 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1427
1436
|
//
|
|
1428
1437
|
// A stage with no validator, or whose artifact fails it, or which never wrote, is untouched.
|
|
1429
1438
|
else if (fail === "timeout" && turn.signals?.hardWall && files.length && validate && wallRescueEnabled()) {
|
|
1430
|
-
// —
|
|
1431
|
-
//
|
|
1432
|
-
//
|
|
1433
|
-
//
|
|
1434
|
-
//
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1439
|
+
// — A FILE THAT COULD NOT BE STAT'ED IS NAMED, WITH WHY, rather than folded into a number. It
|
|
1440
|
+
// used to be a -1 in `quiescentMs` and the cause `artifact-unreadable`, and the commonest way to get
|
|
1441
|
+
// there is not a filesystem fault at all: the turn was killed before it wrote. Measured on two
|
|
1442
|
+
// stages of one run, 2026-09-10: both rows read `wrote: false`, `quiescentMs: -1`,
|
|
1443
|
+
// `artifact-unreadable`, for files that did not exist. An ABSENT file (ENOENT) is its own cause now;
|
|
1444
|
+
// `artifact-unreadable` stays for a stat that fails any other way, with its error code. Rows
|
|
1445
|
+
// written before this change say `artifact-unreadable` for both.
|
|
1446
|
+
//
|
|
1447
|
+
// `quiescentMs` IS A MEASUREMENT OR IT IS ABSENT. With a file unstat'able there is no quiescence of
|
|
1448
|
+
// the set to measure, so it stays null and the row omits it: a -1 reads as a number to anything that
|
|
1449
|
+
// averages or compares it. An mtime that lands after the settle instant (written in that same
|
|
1450
|
+
// instant, or a clock step) measures 0 rather than a negative; it is the under-quiescence case.
|
|
1451
|
+
let missing = null;
|
|
1452
|
+
const quiet = [];
|
|
1453
|
+
for (const f of files) {
|
|
1454
|
+
try { quiet.push(Math.max(0, settledAt - statSync(f).mtimeMs)); }
|
|
1455
|
+
catch (e) { if (!missing) missing = { file: rel(f), code: String(e?.code ?? "unknown") }; }
|
|
1456
|
+
}
|
|
1457
|
+
quiescentMs = missing || !quiet.length ? null : Math.min(...quiet);
|
|
1438
1458
|
const bar = wallRescueQuiesceMs();
|
|
1439
|
-
|
|
1459
|
+
const why = {};
|
|
1460
|
+
if (quiescentMs !== null && quiescentMs >= bar && attemptWroteTruth(why)) {
|
|
1440
1461
|
rescued = fail;
|
|
1441
1462
|
fail = null;
|
|
1442
1463
|
note(`[${name}] hard-wall kill at ${Math.round(wall)}s, but every expected artifact was written by this attempt, passes its validator and had been untouched for ${Math.round(quiescentMs / 1000)}s when the turn settled — the stage FINISHED and the wall is a fact about the dispatch, not a failure of the stage (the resume's skip path would accept these same bytes)`);
|
|
@@ -1451,12 +1472,24 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1451
1472
|
// rescue was refused on the third cause, its validator — and 31 minutes of finished work were
|
|
1452
1473
|
// discarded and re-run cold. `grep -c rescue run.jsonl` on that run returns 0. Nothing in the
|
|
1453
1474
|
// record said why, and reconstructing it took a file-mtime comparison against a preserved run dir.
|
|
1454
|
-
rescueRefused =
|
|
1475
|
+
rescueRefused = missing ? (missing.code === "ENOENT" ? "artifact-absent" : "artifact-unreadable")
|
|
1455
1476
|
: quiescentMs < bar ? "under-quiescence"
|
|
1456
1477
|
: "not-written-by-this-attempt-or-invalid";
|
|
1457
|
-
|
|
1478
|
+
// — AND WHICH FILE, AND ON WHAT: an unstat'able file with its error code; under the bar, the file
|
|
1479
|
+
// touched last (`quiescentMs` carries the number); otherwise the first file the artifact judgement
|
|
1480
|
+
// failed, with its cause — `absent`, `not-written-by-this-attempt`, or the validator's own reason.
|
|
1481
|
+
if (missing) { rescueRefusedFile = missing.file; rescueRefusedReason = missing.code; }
|
|
1482
|
+
else if (quiescentMs < bar) rescueRefusedFile = rel(files[quiet.indexOf(quiescentMs)]);
|
|
1483
|
+
else { rescueRefusedFile = why.file ?? null; rescueRefusedReason = why.reason ?? why.cause ?? null; }
|
|
1484
|
+
note(`[${name}] hard-wall kill at ${Math.round(wall)}s — wall rescue REFUSED (${rescueRefused}: ${rescueRefusedFile ?? "no file named"}${rescueRefusedReason ? `, ${rescueRefusedReason}` : quiescentMs < bar ? `, touched ${Math.round(quiescentMs / 1000)}s before the kill, under the ${Math.round(bar / 1000)}s bar` : ""}); failing honestly as timeout`);
|
|
1458
1485
|
}
|
|
1459
1486
|
}
|
|
1487
|
+
// — WHAT A KILLED ATTEMPT WAS DOING, where its transport kept a record. A tool-written artifact's
|
|
1488
|
+
// refusal journal says how many times this attempt sent its record and was told no, and the reason the
|
|
1489
|
+
// last time. The missing-file row has carried that last reason since the journal existed; a timeout row
|
|
1490
|
+
// carried nothing, so an attempt that spent forty minutes being refused read the same as one that never
|
|
1491
|
+
// called. Counted from THIS dispatch only: the journal is per run, and every attempt appends to it.
|
|
1492
|
+
if (fail === "timeout" && files.length) attemptRefusals = refusalsInWindow(files, runDir, dispatchedAt, settledAt);
|
|
1460
1493
|
// Arm the ladder-wide refusal for every LATER attempt. Read before `classifyWedge` below only because
|
|
1461
1494
|
// the rescue above needs it; the two can never disagree in reach — isTaintRow excludes lane_wedge, and
|
|
1462
1495
|
// a wedge is a `timeout` fail that breaks the ladder immediately, so no later attempt exists to judge.
|
|
@@ -1675,6 +1708,8 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1675
1708
|
// arm did not apply (no wall, no validator, no declared file).
|
|
1676
1709
|
quiescentMs: Number.isFinite(quiescentMs) ? Math.round(quiescentMs) : undefined,
|
|
1677
1710
|
rescueRefused: rescueRefused ?? undefined, // — the cause, when the rescue looked and refused
|
|
1711
|
+
rescueRefusedFile: rescueRefusedFile ?? undefined, rescueRefusedReason: rescueRefusedReason ?? undefined,
|
|
1712
|
+
refusedCalls: attemptRefusals ?? undefined, // — {count, last}: this dispatch's refusals, on a timeout
|
|
1678
1713
|
//: the verbatim message this attempt was dispatched with — {file, sha, bytes, chars, kind}.
|
|
1679
1714
|
// null when the run has no directory or the gate is off; {present:false, error} when the write
|
|
1680
1715
|
// failed. An absence is a record here, never a silence.
|
|
@@ -1740,6 +1775,8 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1740
1775
|
rescued: rescued ?? undefined, killed: killed || undefined,
|
|
1741
1776
|
quiescentMs: Number.isFinite(quiescentMs) ? Math.round(quiescentMs) : undefined, // — see the per-stage row
|
|
1742
1777
|
rescueRefused: rescueRefused ?? undefined, // — the cause, when the rescue looked and refused
|
|
1778
|
+
rescueRefusedFile: rescueRefusedFile ?? undefined, rescueRefusedReason: rescueRefusedReason ?? undefined,
|
|
1779
|
+
refusedCalls: attemptRefusals ?? undefined, // — see the per-stage row
|
|
1743
1780
|
// — AND THE BILLING PAIR, by the same argument makes for the model pair one field up:
|
|
1744
1781
|
// the spine carries it or the two logs disagree about what ran. This is the row a sweep across
|
|
1745
1782
|
// archived runs actually reads, and the question "has this box ever billed API" could not be
|
|
@@ -2007,6 +2044,30 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
2007
2044
|
return { ok: false, attempts: attempt, fail: lastFail, sessionKey: lastKey, modelWire: lastModelWire, modelUsed: lastModelUsed, attemptFails: [...attemptFails], warmEscalated: warmEscalatedAt > 0 || undefined, quantity: lastQuantity, reads: lastReads, readsTruncated: lastReadsTruncated, warm: lastWarm, wrote: lastWrote, formRepairs: formRepairsUsed };
|
|
2008
2045
|
}
|
|
2009
2046
|
|
|
2047
|
+
/**
|
|
2048
|
+
* One dispatch's refusals, read from the refusal journals of the tool-written artifacts in `files`.
|
|
2049
|
+
* An entry is placed by its `at`; one with no readable `at` cannot be placed in a dispatch, so it is
|
|
2050
|
+
* counted apart rather than guessed into this one.
|
|
2051
|
+
* @returns {{ count: number, last: string|null, unattributed?: number } | null} null when no file in
|
|
2052
|
+
* `files` has a refusal journal, so "this stage keeps no journal" never reads as "nothing was refused".
|
|
2053
|
+
*/
|
|
2054
|
+
function refusalsInWindow(files, runDir, from, to) {
|
|
2055
|
+
let journals = 0, count = 0, unattributed = 0, last = null, lastAt = -Infinity;
|
|
2056
|
+
for (const f of files) {
|
|
2057
|
+
const reader = toolWrittenArtifact(f)?.refusals;
|
|
2058
|
+
if (!reader) continue;
|
|
2059
|
+
journals++;
|
|
2060
|
+
for (const r of reader(runDir, f) ?? []) {
|
|
2061
|
+
const at = Date.parse(String(r?.at ?? ""));
|
|
2062
|
+
if (!Number.isFinite(at)) { unattributed++; continue; }
|
|
2063
|
+
if (at < from || at > to) continue;
|
|
2064
|
+
count++;
|
|
2065
|
+
if (at >= lastAt) { lastAt = at; last = String(r?.reason ?? ""); }
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
return journals ? { count, last, ...(unattributed ? { unattributed } : {}) } : null;
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2010
2071
|
function rel(p) {
|
|
2011
2072
|
const i = p.indexOf("/prelim-search/");
|
|
2012
2073
|
return i >= 0 ? p.slice(i + 1) : p;
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.0-beta.
|
|
5
|
+
"version": "0.3.0-beta.3",
|
|
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": {
|
|
@@ -4589,7 +4589,7 @@ const PORT = PORT_CHOICE.port;
|
|
|
4589
4589
|
const recipeCommit = committing(recipeRepoRoot, "saved-search");
|
|
4590
4590
|
// — same, against the RECIPE repo root, which may differ from the profile one.
|
|
4591
4591
|
const recAudit = makeCommittableAudit({ auditPath: recipeAuditPath, repoRoot: recipeRepoRoot });
|
|
4592
|
-
const recipes = makeRecipeService({ recipesDir, profileDir, gitCommit: recipeCommit, audit: recAudit });
|
|
4592
|
+
const recipes = makeRecipeService({ recipesDir, profileDir, readLayered: true, gitCommit: recipeCommit, audit: recAudit });
|
|
4593
4593
|
callRecipes = (method, path, body, identity) => recipes.route(method, path, { email: identity?.email }, body ?? {});
|
|
4594
4594
|
log(`saved searches ON — store=${recipesDir} repo=${recipeRepoRoot}`);
|
|
4595
4595
|
} else {
|
|
@@ -471,13 +471,41 @@ export function makeProfileService({
|
|
|
471
471
|
throw e;
|
|
472
472
|
}
|
|
473
473
|
|
|
474
|
+
// A CREATE THE STORE CANNOT RECORD IS REFUSED, NOT REPORTED AS DONE. This answered 201 with the
|
|
475
|
+
// company live and a `commitError` beside it, so a machine with no git identity got a company with no
|
|
476
|
+
// record of who made it or when, on its first company, and the store move had been made to close
|
|
477
|
+
// exactly that gap. The store is asked first, and a refusal writes nothing: no profile, no audit row
|
|
478
|
+
// and, since the organisation's grant is filed only on a 201, no grant.
|
|
479
|
+
const cannot = typeof gitCommit?.refusal === "function" ? gitCommit.refusal() : null;
|
|
480
|
+
if (cannot) {
|
|
481
|
+
return { status: 409, json: { error: `No company was created: ${cannot.message}, then try again.`, code: `store_${cannot.code.replace(/-/g, "_")}` } };
|
|
482
|
+
}
|
|
474
483
|
const { files } = writeProfile({ profileDir, key: wanted, profile, contextPack: "" });
|
|
475
|
-
// WRITTEN AND RECORDED ARE TWO EVENTS. The write is live the instant it renames; the commit can
|
|
476
|
-
// fail on its own. Reporting them as one is how somebody is told nothing happened about a company
|
|
477
|
-
// that is already governing runs.
|
|
478
484
|
const message = `chore(clearotron): create company ${wanted} (via portal, by ${by})`;
|
|
479
485
|
const { commit, commitError } = commitWithAuditRow({ audit, gitCommit, files, message, by,
|
|
480
486
|
row: { event: "profile-create", key: wanted, by, fields: Object.keys(profile) } });
|
|
487
|
+
// AND ONE THAT FAILS ANYWAY IS WITHDRAWN. A hook or a full disk can refuse the commit after the store
|
|
488
|
+
// said it could record. The paths this create wrote were absent before it, so they are returned to
|
|
489
|
+
// absent, and a third audit row says so after the two the failed commit left.
|
|
490
|
+
if (commitError) {
|
|
491
|
+
// GIT'S LAST WORD, NOT ITS ECHO. The error opens with the whole command line it ran, message and
|
|
492
|
+
// author included, and ends with the refusal itself: a hook's own sentence, "No space left on
|
|
493
|
+
// device". The operator acts on the last line.
|
|
494
|
+
const cause = String(commitError).trim().split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? String(commitError);
|
|
495
|
+
let left = null;
|
|
496
|
+
try {
|
|
497
|
+
if (typeof gitCommit?.withdraw !== "function") throw new Error("this store cannot withdraw what it wrote");
|
|
498
|
+
gitCommit.withdraw(files);
|
|
499
|
+
} catch (e) { left = String(e?.message ?? e).slice(0, 200); }
|
|
500
|
+
try {
|
|
501
|
+
audit({ event: "profile-create-withdrawn", of: "profile-create", key: wanted, by,
|
|
502
|
+
note: left ? `the create could not be recorded and its files could not be removed (${left})` : "the create could not be recorded, so its files were removed" });
|
|
503
|
+
} catch { /* the refusal below is the report; a journal failure must not mask it */ }
|
|
504
|
+
if (left) {
|
|
505
|
+
return { status: 500, json: { key: wanted, error: `The company could not be recorded (${cause}), and its file could not be removed afterwards (${left}). It is on disk with no record behind it — tell an administrator.` } };
|
|
506
|
+
}
|
|
507
|
+
return { status: 409, json: { error: `No company was created: the store could not record it (${cause}). Nothing was left behind.`, code: "store_commit_failed" } };
|
|
508
|
+
}
|
|
481
509
|
return { status: 201, json: {
|
|
482
510
|
key: wanted, name, written: true, created: true, commit,
|
|
483
511
|
// The receipt reads off THESE, so it can say which framework and how many marketplaces, and
|
package/driver/publish/index.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { reportIdentityFor, productCoverageNote, isRegisterOnly } from '../searc
|
|
|
22
22
|
import { readRecordArtifacts, bindFindingsToRecords, joinEvidenceStatus } from '../registry-fidelity.mjs';
|
|
23
23
|
import { deliveryFlagLines } from '../predelivery-lint.mjs';
|
|
24
24
|
import { PROVIDERS, config } from '../driver.config.mjs';
|
|
25
|
-
import {
|
|
25
|
+
import { declaredRecordOrigins } from '../record-origins.mjs';
|
|
26
26
|
import { NEUTRAL_DELIVERY, loadProfiles } from '../profiles.mjs';
|
|
27
27
|
import { resolveDemoData, demoBannerMd } from './demo-marking.mjs'; // — one demo question, every product; 2134 — every SURFACE
|
|
28
28
|
import { engineCommit } from '../engine-build.mjs';
|
|
@@ -546,7 +546,7 @@ export async function regenSurfaces(poolRoot) {}
|
|
|
546
546
|
* all three client surfaces state the same link. That agreement is the point:
|
|
547
547
|
* a repair that reached the report and not the workbook would put two
|
|
548
548
|
* different registers in one delivery.
|
|
549
|
-
* @param {string[]|null} origins
|
|
549
|
+
* @param {string[]|null} origins declaredRecordOrigins(the run's own provider). `null` = the run has no fetch
|
|
550
550
|
* receipts (legacy/archived): a NO-OP, byte-identical output, because a run
|
|
551
551
|
* that never named its register cannot be judged against one. `[]` is a
|
|
552
552
|
* different answer — this provider publishes no per-record page at all, so no
|
|
@@ -773,7 +773,8 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
773
773
|
//
|
|
774
774
|
// So this REPAIRS rather than refuses, keyed on the RUN's own provider (the receipts above), never on
|
|
775
775
|
// today's CLEAROTRON_DATABASE — a republished archive keeps its own register.
|
|
776
|
-
|
|
776
|
+
// Receipts that name no provider are read as no receipts: the gate is off, as it is for a legacy run.
|
|
777
|
+
const runOrigins = fetchReceipts ? declaredRecordOrigins(fetchReceipts[0]?.provider) : null;
|
|
777
778
|
const foreignRecordLinks = normalizeRecordLinks(findings, runOrigins);
|
|
778
779
|
// — the SAME list reaches the renderer. normalizeRecordLinks repairs foreign ABSOLUTE links on
|
|
779
780
|
// register-sourced findings; render.mjs separately CONSTRUCTS links from bare record paths, and until
|
|
@@ -859,7 +860,7 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
859
860
|
const esPath = driverDir(runDir ?? dirname(reportMd), 'enforcer-signals.json');
|
|
860
861
|
if (existsSync(esPath)) { const es = JSON.parse(readFileSync(esPath, 'utf8')); if (Array.isArray(es)) enforcerSignals = es; }
|
|
861
862
|
} catch { /* absent — no telemetry lines */ }
|
|
862
|
-
const officeLinks = officeLinksFor(findings, recordsByUri,
|
|
863
|
+
const officeLinks = officeLinksFor(findings, recordsByUri, runOrigins); bindFindingsToRecords(findings, recordsByUri);
|
|
863
864
|
// 404-card caveat (2026-07-22): the V4-2 closure pass persisted every cited record its targeted
|
|
864
865
|
// fetch definitively could not retrieve (predelivery-lint.json artifactSet.recordFetchFailures);
|
|
865
866
|
// the evidence join stamps `_recordFetchFailure` from it so the card render carries the
|
|
@@ -1621,12 +1622,14 @@ export function composeEmailHtml(reportMdPath, url, auditFile, names = [], deliv
|
|
|
1621
1622
|
|
|
1622
1623
|
// ── The office's own page for each fetched record, where the run's register publishes none of its own ──
|
|
1623
1624
|
// Addressed from the record's numbers (office-record-links.mjs), never from the model or the handle, and
|
|
1624
|
-
// null on every
|
|
1625
|
-
//
|
|
1626
|
-
//
|
|
1625
|
+
// null on every register that publishes pages of its own, so those runs publish exactly as before. Which
|
|
1626
|
+
// registers those are is `runOrigins`, the same answer the record-URL normalisation above keys on, so the
|
|
1627
|
+
// two cannot disagree about one register. The tally goes to meta.json and the log, so a register whose
|
|
1628
|
+
// numbers never fit shows up as a count rather than as silence. Kept down here, below every line the rest
|
|
1629
|
+
// of the tree cites by number.
|
|
1627
1630
|
import { recordLinksFor } from './office-record-links.mjs';
|
|
1628
|
-
function officeLinksFor(findings, recordsByUri,
|
|
1629
|
-
const links = recordLinksFor(findings, recordsByUri,
|
|
1631
|
+
function officeLinksFor(findings, recordsByUri, runOrigins) {
|
|
1632
|
+
const links = recordLinksFor(findings, recordsByUri, runOrigins);
|
|
1630
1633
|
if (links) console.log(`[record-links] ${links.summary}`);
|
|
1631
1634
|
return links;
|
|
1632
1635
|
}
|
|
@@ -401,8 +401,9 @@ export async function publishKnockout({ runId, codename, runDir, findings, plan,
|
|
|
401
401
|
// THE OFFICE'S OWN PAGE FOR EACH LISTED FILING, where the run's register publishes none of its own:
|
|
402
402
|
// the addressing the clearance gives its register findings (office-record-links.mjs), set on the
|
|
403
403
|
// sidecar here for the same reason the normalisation above is. Keyed on the run's own provider, and
|
|
404
|
-
// the tally goes to meta.json, so numbers that never fit show as a count rather than as silence.
|
|
405
|
-
|
|
404
|
+
// the tally goes to meta.json, so numbers that never fit show as a count rather than as silence. A
|
|
405
|
+
// sidecar that names no provider is left as it was, as the normalisation above leaves it.
|
|
406
|
+
const officeLinks = addressListedFilings(registerRecords, declaredRecordOrigins(registerRecords?.provider));
|
|
406
407
|
if (officeLinks) note(`[record-links] ${officeLinks.summary}`);
|
|
407
408
|
|
|
408
409
|
// ── Predelivery lint — the APPLICABLE subset, FLAGS not FAILS (2026-07-31) ─────────────────────────
|
|
@@ -753,3 +754,4 @@ export function knockoutDocumentRoutes(reports, { auditFile = null } = {}) {
|
|
|
753
754
|
// The office's own page for each listed filing (office-record-links.mjs). Kept down here, below every
|
|
754
755
|
// line the rest of the tree cites by number.
|
|
755
756
|
import { addressListedFilings, reasonCellFor } from './office-record-links.mjs';
|
|
757
|
+
import { declaredRecordOrigins } from '../record-origins.mjs';
|