clearotron 0.3.0-beta.2 → 0.3.0-beta.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/INSTALL.md +32 -6
- package/bin/connect.mjs +68 -25
- package/bin/disconnect.mjs +16 -11
- package/bin/onboard.mjs +65 -6
- package/bin/start.mjs +7 -1
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +38 -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 +34 -28
- package/mcp-server/CHANGELOG.md +8 -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-CsCuPshD.css → index-Cv-E_agg.css} +199 -86
- package/portal-ui/dist/assets/{index-CcFjgM78.js → index-DWYCsOCJ.js} +514 -380
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/clarivate/src/core.js +5 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -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/connect-clients.mjs +282 -321
- package/shared/names-in-force.mjs +1 -0
- package/shared/stdio-connect.mjs +63 -0
- package/shared/store-in-repo.mjs +50 -2
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
-->
|
|
50
50
|
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
|
|
51
51
|
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
|
|
52
|
-
<script type="module" crossorigin src="/portal/assets/index-
|
|
53
|
-
<link rel="stylesheet" crossorigin href="/portal/assets/index-
|
|
52
|
+
<script type="module" crossorigin src="/portal/assets/index-DWYCsOCJ.js"></script>
|
|
53
|
+
<link rel="stylesheet" crossorigin href="/portal/assets/index-Cv-E_agg.css">
|
|
54
54
|
</head>
|
|
55
55
|
<body>
|
|
56
56
|
<div id="root"></div>
|
package/portal-ui/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "portal-ui",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.0-beta.
|
|
5
|
+
"version": "0.3.0-beta.4",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
|
|
8
8
|
"engines": {
|
|
@@ -1214,6 +1214,11 @@ export async function doBatchScreen(apiKey, base, params, tctx) {
|
|
|
1214
1214
|
application_date: nr.applicationDate,
|
|
1215
1215
|
registration_date: nr.registrationDate,
|
|
1216
1216
|
expiry_date: nr.expiryDate,
|
|
1217
|
+
// The office's own numbers, which the record carries and a reader looks the record up by. A
|
|
1218
|
+
// knockout's filings list is built from these rows, and without them it could name no record a
|
|
1219
|
+
// reader can open.
|
|
1220
|
+
application_number: nr.applicationNumber,
|
|
1221
|
+
registration_number: nr.registrationNumber,
|
|
1217
1222
|
live_status: liveStatusOf(rec),
|
|
1218
1223
|
all_class: isAllClass(nr.niceClasses),
|
|
1219
1224
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-oauth-mcp-bridge",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.4",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
|
|
@@ -240,7 +240,7 @@ export function classify(doors, names, now, probe = runStateProbe) {
|
|
|
240
240
|
// dropped off this list while remaining exactly as unclosed as the ones that were never opened —
|
|
241
241
|
// so reading a round was enough to make it stop being counted, whatever the read found.
|
|
242
242
|
//
|
|
243
|
-
// The harness already knows this. previousRoundNotice in e2e.mjs
|
|
243
|
+
// The harness already knows this. previousRoundNotice in e2e.mjs says the round closes on
|
|
244
244
|
// `reportedState === "settled"`, NEVER on `reportedAt != null`, and gives the incident that
|
|
245
245
|
// taught it. This lister was the last reader keying on the weaker field.
|
|
246
246
|
if (String(r?.reportedState ?? "") === "settled") continue;
|
package/scripts/e2e.mjs
CHANGED
|
@@ -1002,11 +1002,59 @@ export function doorUnavailableLabel(a, { max = 90 } = {}) {
|
|
|
1002
1002
|
return "no status and no reason recorded";
|
|
1003
1003
|
}
|
|
1004
1004
|
|
|
1005
|
-
|
|
1005
|
+
/**
|
|
1006
|
+
* ── THE ASYMMETRY RULE IS WRITTEN FOR A CASE BOTH DOORS SHOULD REFUSE ───────────────────────────
|
|
1007
|
+
*
|
|
1008
|
+
* "The one that ACCEPTED is the defect" rests on a premise nobody wrote down: that the case EXPECTS a
|
|
1009
|
+
* refusal. Eight of R0's nine cases do, so the premise held by accident for as long as the ninth could
|
|
1010
|
+
* not be measured.
|
|
1011
|
+
*
|
|
1012
|
+
* R0e is the one admit case. Its own `expect.terminal` is `"delivered"` and it asserts the run resolves
|
|
1013
|
+
* to `generic`. On 2026-09-10, the first round in which both doors actually ANSWERED it, the cli door
|
|
1014
|
+
* accepted and delivered `profileKey: "generic"` — the case's whole contract, met — and the ops-mcp door
|
|
1015
|
+
* refused at `assertScopedProfileKey`, a deliberate cross-customer bypass gate doing its job for an
|
|
1016
|
+
* accounts-scoped session. Both doors were right. The rule named the one that behaved.
|
|
1017
|
+
*
|
|
1018
|
+
* The three earlier rounds hid it: the ops-mcp door was lost to the transport every time, so the case
|
|
1019
|
+
* never reached this comparison as two real answers.
|
|
1020
|
+
*
|
|
1021
|
+
* SO THE POLARITY IS NOT INVERTED HERE, and that matters. "On an admit case the door that REFUSED is the
|
|
1022
|
+
* defect" would blame a security gate for holding. What is wrong is the VERDICT, not its direction: on an
|
|
1023
|
+
* admit case this comparison does not know which door is at fault, and it says so instead of guessing.
|
|
1024
|
+
* A case that declares which doors can admit it (`doors`) stops the guess entirely.
|
|
1025
|
+
*/
|
|
1026
|
+
export function doorAsymmetry(answers, { expectTerminal = null } = {}) {
|
|
1006
1027
|
const compared = answers.filter((a) => doorAnswerClass(a) === DOOR_ANSWER.ANSWERED);
|
|
1007
1028
|
const unavailable = answers.filter((a) => doorAnswerClass(a) === DOOR_ANSWER.INFRA_UNAVAILABLE);
|
|
1008
1029
|
const agreed = compared.length < 2 ? true : compared.every((a) => a.ok === compared[0].ok);
|
|
1009
|
-
return { agreed, compared, unavailable, reducedCoverage: unavailable.length > 0 };
|
|
1030
|
+
return { agreed, compared, unavailable, reducedCoverage: unavailable.length > 0, admit: isAdmitCase(expectTerminal) };
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** A case whose own contract is that a door ACCEPTS. `delivered` and `duplicate` both admit; the rest refuse. */
|
|
1034
|
+
export function isAdmitCase(expectTerminal) {
|
|
1035
|
+
const t = String(expectTerminal ?? "").trim().toLowerCase();
|
|
1036
|
+
return t === "delivered" || t === "duplicate";
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* The sentence a disagreement gets — ONE author, because the run printed it and the report pushed it into
|
|
1041
|
+
* `toInvestigate` from two separate literals, and a fix to either would have left the other saying the
|
|
1042
|
+
* old thing.
|
|
1043
|
+
*
|
|
1044
|
+
* `accepted`/`refused` read `ok` at run time and `accepted` on the receipt; both spellings are the same
|
|
1045
|
+
* fact and one formatter has to serve both surfaces or they drift.
|
|
1046
|
+
*/
|
|
1047
|
+
export function doorDisagreementLine(caseId, answers, { expectTerminal = null } = {}) {
|
|
1048
|
+
const took = (a) => (a?.ok ?? a?.accepted) === true;
|
|
1049
|
+
const accepted = answers.filter(took).map((a) => a.door);
|
|
1050
|
+
const refused = answers.filter((a) => !took(a)).map((a) => a.door);
|
|
1051
|
+
const both = `accepted by ${accepted.join(", ") || "(none)"}, refused by ${refused.join(", ") || "(none)"}`;
|
|
1052
|
+
if (!isAdmitCase(expectTerminal))
|
|
1053
|
+
return `${caseId}: THE DOORS DISAGREE — ${both}; the door that ACCEPTED is the defect`;
|
|
1054
|
+
return `${caseId}: THE DOORS DISAGREE on a case whose own expect.terminal ADMITS — ${both}. `
|
|
1055
|
+
+ `The accepting door met this case's stated contract; the refusing door answered a question the case did not ask. `
|
|
1056
|
+
+ `WHICH IS AT FAULT IS NOT DECIDED HERE — a door may refuse for a reason this case cannot satisfy at that door, `
|
|
1057
|
+
+ `and naming the accepter would blame the door that behaved. Declare \`doors\` on the case to say which doors can admit it.`;
|
|
1010
1058
|
}
|
|
1011
1059
|
|
|
1012
1060
|
const enqueue = (job, door) =>
|
|
@@ -1115,6 +1163,46 @@ const OPS = {
|
|
|
1115
1163
|
length: (v, want) => ({ ok: Array.isArray(v) && v.length === want, saw: Array.isArray(v) ? `${v.length}` : JSON.stringify(v) }),
|
|
1116
1164
|
};
|
|
1117
1165
|
|
|
1166
|
+
/**
|
|
1167
|
+
* The file `delivery-settled` reads, and the ONLY one it reads.
|
|
1168
|
+
*
|
|
1169
|
+
* Named rather than inlined because two places have to agree about it: the op itself, and the check that
|
|
1170
|
+
* asks every scenario whether the path it declared for an op is one that op can read. A literal in both
|
|
1171
|
+
* is the shape that produced the defect this constant exists to prevent.
|
|
1172
|
+
*/
|
|
1173
|
+
export const DELIVERY_STATUS_FILE = "status.json";
|
|
1174
|
+
|
|
1175
|
+
/**
|
|
1176
|
+
* Which ops read a FIXED file rather than the path the scenario declares, and what that file is.
|
|
1177
|
+
*
|
|
1178
|
+
* DERIVED CHECKS NEED THIS TABLE, not a hand-written list of known-bad scenarios: a scenario nobody
|
|
1179
|
+
* thought to add is exactly the one that goes unnoticed, and R13 went unnoticed for four rounds.
|
|
1180
|
+
*/
|
|
1181
|
+
export const FIXED_FILE_OPS = Object.freeze({ "delivery-settled": DELIVERY_STATUS_FILE });
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Every scenario case whose declared `path` names a file its `op` does not read.
|
|
1185
|
+
*
|
|
1186
|
+
* Not a pass/fail — the op reads the right file either way. It is a REPORT, so a scenario stating a wrong
|
|
1187
|
+
* belief about an op is visible to whoever runs it. Built from the scenario set, never from a list of
|
|
1188
|
+
* names: the point is to find the next one.
|
|
1189
|
+
*/
|
|
1190
|
+
export function pathsAnOpDoesNotRead(scenarios) {
|
|
1191
|
+
const out = [];
|
|
1192
|
+
for (const s of scenarios ?? []) {
|
|
1193
|
+
const cases = s?.cases ?? (s?.expect ? [{ id: s.id, expect: s.expect }] : []);
|
|
1194
|
+
for (const c of cases) {
|
|
1195
|
+
for (const a of c?.expect?.assert ?? []) {
|
|
1196
|
+
const fixed = FIXED_FILE_OPS[a?.op];
|
|
1197
|
+
if (!fixed) continue;
|
|
1198
|
+
const declared = String(a?.path ?? "").split(":")[0];
|
|
1199
|
+
if (declared && declared !== fixed) out.push({ scenario: s.id, case: c.id ?? s.id, op: a.op, declared, reads: fixed });
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return out;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1118
1206
|
function evalAssertion(a, runDir) {
|
|
1119
1207
|
const [file, field] = String(a.path ?? "").split(":");
|
|
1120
1208
|
const full = join(runDir, file || "");
|
|
@@ -1335,8 +1423,21 @@ function evalAssertion(a, runDir) {
|
|
|
1335
1423
|
// exist. A run that flipped sendPending without writing one would have passed the old assertion and
|
|
1336
1424
|
// fails this one.
|
|
1337
1425
|
if (a.op === "delivery-settled") {
|
|
1338
|
-
|
|
1339
|
-
|
|
1426
|
+
// — THIS OP READS `status.json`, WHATEVER PATH THE SCENARIO DECLARED, and the error text three
|
|
1427
|
+
// lines below always said so while the read above it took `full`. R13 declared `_driver/delivery.json`,
|
|
1428
|
+
// which parses cleanly and carries no `sendPending`, so the assertion read `undefined`, failed a run
|
|
1429
|
+
// that had delivered correctly, and pushed a product defect that did not exist into `INVESTIGATE`.
|
|
1430
|
+
// It went unseen for four rounds because R13 had been REPORTED once.
|
|
1431
|
+
//
|
|
1432
|
+
// Reading the run's own status is not a correction of the scenario — it is what the op has always
|
|
1433
|
+
// meant. The scenario's `path` is still ANSWERED FOR rather than ignored: `declaredPathNote` says so
|
|
1434
|
+
// in the line, so a scenario stating a belief about this op that is wrong is visible to the reader
|
|
1435
|
+
// who runs it, not only to whoever next reads this function.
|
|
1436
|
+
const st = readJson(join(runDir, DELIVERY_STATUS_FILE));
|
|
1437
|
+
const declaredPathNote = (file && file !== DELIVERY_STATUS_FILE)
|
|
1438
|
+
? ` — NOTE: this scenario declares \`path: "${file}"\` for \`delivery-settled\`, which reads \`${DELIVERY_STATUS_FILE}\` and nothing else. The declared path was not read.`
|
|
1439
|
+
: "";
|
|
1440
|
+
if (!st) return { ok: false, saw: `${DELIVERY_STATUS_FILE} absent or unparseable${declaredPathNote}` };
|
|
1340
1441
|
// — THERE IS NO MODE TO READ ANY MORE, and this is the one place where that mattered rather
|
|
1341
1442
|
// than being a comment fix. This used to open `process.env.CLEAROTRON_DELIVERY || "email"` and take a
|
|
1342
1443
|
// `sendPending === false` branch for anything that was not `handoff`. With the variable deleted that
|
|
@@ -1392,7 +1493,7 @@ function evalAssertion(a, runDir) {
|
|
|
1392
1493
|
const { packets, unreadable } = outboxPackets({ runIds: runIdForms(st, runDir) });
|
|
1393
1494
|
return { ok: st.sendPending === true && packets.length > 0,
|
|
1394
1495
|
saw: `mode=handoff sendPending=${JSON.stringify(st.sendPending)} packet=${
|
|
1395
|
-
packets.length ? packets.join(", ") : unreadable ? `NOT LOOKED FOR — ${unreadable}` : "NONE WRITTEN"}` };
|
|
1496
|
+
packets.length ? packets.join(", ") : unreadable ? `NOT LOOKED FOR — ${unreadable}` : "NONE WRITTEN"}${declaredPathNote}` };
|
|
1396
1497
|
}
|
|
1397
1498
|
|
|
1398
1499
|
// `absent` — the file must NOT exist, and its absence is the asserted state, not a silent pass.
|
|
@@ -1811,7 +1912,7 @@ async function cmdRun(id) {
|
|
|
1811
1912
|
// ever wants the launch to stop.
|
|
1812
1913
|
printPreviousRoundNotice(s);
|
|
1813
1914
|
|
|
1814
|
-
const jobs = s.job ? [{ id: s.id, job: s.job }] : (s.cases ?? []).map((c) => ({ id: c.id, job: c.job, what: c.what, oneMatterAcrossDoors: c.oneMatterAcrossDoors === true }));
|
|
1915
|
+
const jobs = s.job ? [{ id: s.id, job: s.job }] : (s.cases ?? []).map((c) => ({ id: c.id, job: c.job, what: c.what, oneMatterAcrossDoors: c.oneMatterAcrossDoors === true, expectTerminal: (c.expect ?? {}).terminal ?? null }));
|
|
1815
1916
|
|
|
1816
1917
|
// door: "all" is the point of R0 — a rule enforced in one door and not another is exactly the #98
|
|
1817
1918
|
// asymmetry. Every case goes through EVERY drivable door and the answers are compared. runner.mjs's
|
|
@@ -1827,7 +1928,7 @@ async function cmdRun(id) {
|
|
|
1827
1928
|
// over whatever was there, so the second run of a pair destroyed the first round's token and the
|
|
1828
1929
|
// first half became unreportable with nothing saying so.
|
|
1829
1930
|
const round = { token: RUN_TOKEN, startedAt: new Date().toISOString(), startedAtSource: "run", doors, cases: [] };
|
|
1830
|
-
for (const { id: caseId, job, what, oneMatterAcrossDoors } of jobs) {
|
|
1931
|
+
for (const { id: caseId, job, what, oneMatterAcrossDoors, expectTerminal } of jobs) {
|
|
1831
1932
|
// The round token goes on the BASE ref, so the door suffix (and R0d's opt-out from it) still decides
|
|
1832
1933
|
// whether the doors are one matter or two. See refForRun and refForDoor.
|
|
1833
1934
|
const roundRef = refForRun(job.ref);
|
|
@@ -1857,7 +1958,7 @@ async function cmdRun(id) {
|
|
|
1857
1958
|
console.log(` [${a.door}] ${a.ok ? "accepted" : "refused"}: ${a.out.split("\n").slice(0, 2).join(" ").slice(0, 200)}`);
|
|
1858
1959
|
}
|
|
1859
1960
|
}
|
|
1860
|
-
if (!agreed) console.log(` ⚠
|
|
1961
|
+
if (!agreed) console.log(` ⚠ ${doorDisagreementLine(caseId, answers, { expectTerminal })}`);
|
|
1861
1962
|
// — named, never silent. A door lost to the transport costs this case a share of its door
|
|
1862
1963
|
// coverage, and that is a fact about the ROUND, not about the product. Saying nothing here is how
|
|
1863
1964
|
// the R0e 429 cost a third of a scenario's coverage without appearing anywhere.
|
|
@@ -3083,9 +3184,13 @@ async function cmdReport(id, { round: requestedToken = null } = {}) {
|
|
|
3083
3184
|
.filter((x) => x.out.length);
|
|
3084
3185
|
console.log(`\ndoors: ${rec.doors.join(", ") || "(not recorded)"} — ${rec.cases.length} case(s), ${dis.length} disagreement(s), ${unavailable.length} case(s) with reduced door coverage`);
|
|
3085
3186
|
for (const c of dis) {
|
|
3187
|
+
// — the RECEIPT does not carry the case's contract, so it is joined back to the scenario by
|
|
3188
|
+
// id. A receipt written before this field existed reads null and gets the old sentence, which is
|
|
3189
|
+
// the right reading for it rather than a retrospective one.
|
|
3190
|
+
const expectTerminal = ((s.cases ?? []).find((x) => x.id === c.id)?.expect ?? {}).terminal ?? null;
|
|
3086
3191
|
const accepted = c.answers.filter((a) => a.accepted).map((a) => a.door);
|
|
3087
3192
|
console.log(` ⚠ ${c.id}: accepted by ${accepted.join(", ") || "(none)"}, refused by ${c.answers.filter((a) => !a.accepted).map((a) => a.door).join(", ") || "(none)"}`);
|
|
3088
|
-
toInvestigate.push(
|
|
3193
|
+
toInvestigate.push(doorDisagreementLine(c.id, c.answers, { expectTerminal }));
|
|
3089
3194
|
}
|
|
3090
3195
|
for (const { c, out } of unavailable) {
|
|
3091
3196
|
const who = out.map((a) => `${a.door} (${doorUnavailableLabel(a)})`).join(", ");
|
|
@@ -221,14 +221,31 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
|
|
|
221
221
|
// the truncation as a lower request count — the failure mode that would make this whole check lie in the
|
|
222
222
|
// safe-looking direction. Wait until the server has been silent for `quiet` ms, capped so a screen that
|
|
223
223
|
// requests forever fails loudly instead of hanging.
|
|
224
|
-
|
|
224
|
+
//
|
|
225
|
+
// SILENCE BEFORE THE FIRST REQUEST IS NOT QUIET, WHERE A REQUEST IS OWED. `Page.navigate` returns as soon
|
|
226
|
+
// as the navigation is committed, long before the app's bundle has run, so the quiet clock above used to
|
|
227
|
+
// start while the server had heard nothing at all — and a first fetch that arrived later than `quiet`
|
|
228
|
+
// closed the window empty. Every request that followed was then counted into the NEXT window, so the
|
|
229
|
+
// whole drive read one bucket late: visit 1 empty, the revisit holding what boot had asked for. On a warm
|
|
230
|
+
// machine the bundle runs in well under a second and it never showed; on a slow runner it failed nine
|
|
231
|
+
// assertions about a product that was behaving. Reproduced by delaying the bundle 1.5s at this fixture.
|
|
232
|
+
//
|
|
233
|
+
// `expectTraffic` is for a window that follows a FULL NAVIGATION — the shell's boot and the Result
|
|
234
|
+
// screen's first visit, the two places this drive calls `Page.navigate`. Each reloads the bundle, so each
|
|
235
|
+
// has the same gap between the navigation committing and the app's first fetch. A client-side visit runs
|
|
236
|
+
// on the bundle already loaded and has no such gap, which is why it is not the default: a revisit is
|
|
237
|
+
// allowed to ask for nothing, and that is the very thing this file measures, so a window there must still
|
|
238
|
+
// be able to close on silence.
|
|
239
|
+
const settle = async ({ quiet = 900, cap = 12000, expectTraffic = false } = {}) => {
|
|
225
240
|
const started = Date.now()
|
|
241
|
+
const before = hits.length
|
|
226
242
|
let last = hits.length
|
|
227
243
|
let lastChange = Date.now()
|
|
228
244
|
for (;;) {
|
|
229
245
|
await sleep(100)
|
|
230
246
|
if (hits.length !== last) { last = hits.length; lastChange = Date.now() }
|
|
231
|
-
|
|
247
|
+
const heard = hits.length > before
|
|
248
|
+
if ((heard || !expectTraffic) && Date.now() - lastChange >= quiet) return true
|
|
232
249
|
if (Date.now() - started >= cap) return false
|
|
233
250
|
}
|
|
234
251
|
}
|
|
@@ -290,7 +307,8 @@ const record = {}
|
|
|
290
307
|
// The shell, once. Everything after this is client-side.
|
|
291
308
|
epoch = 'boot'
|
|
292
309
|
await navigateOrRefuse(cmd, `${origin}/portal/home`, { what: 'revisit-render-check' })
|
|
293
|
-
if (!(await settle(
|
|
310
|
+
if (!(await settle({ expectTraffic: true })))
|
|
311
|
+
say(false, 'the shell did not finish loading within 12s — it never reached the fixture server, or never stopped requesting')
|
|
294
312
|
const bootPath = await where()
|
|
295
313
|
say(bootPath === '/portal/home', `the shell loaded on /portal/home (got ${bootPath})`)
|
|
296
314
|
|
|
@@ -384,7 +402,7 @@ await twice('clearances', () => clickNav('Clearances'), '/portal/clearances')
|
|
|
384
402
|
// runs AppShell's popstate listener rather than its click funnel.
|
|
385
403
|
epoch = 'result:1'
|
|
386
404
|
await navigateOrRefuse(cmd, `${origin}/portal/result/${RUN_ID}`, { what: 'revisit-render-check' })
|
|
387
|
-
const rq1 = await settle()
|
|
405
|
+
const rq1 = await settle({ expectTraffic: true }) // a full navigation, like boot
|
|
388
406
|
const rp1 = await where()
|
|
389
407
|
const rt1 = await screenText()
|
|
390
408
|
say(rp1 === `/portal/result/${RUN_ID}`, `result: visit 1 landed on the run (got ${rp1})`)
|
|
@@ -101,7 +101,7 @@ function isPlumbing(node, child) {
|
|
|
101
101
|
// A ternary BRANCH is plumbing; a ternary TEST is a decision, and classify() handles that separately.
|
|
102
102
|
// This was the whole of the unresolved bucket — three sites, all one shape:
|
|
103
103
|
//
|
|
104
|
-
// const wrote = files.length ? files.some(…) : null; gateway.mjs
|
|
104
|
+
// const wrote = files.length ? files.some(…) : null; gateway.mjs (by name)
|
|
105
105
|
// const inScope = scope.size ? tokens.some(…) : (…); reasoning-tripwires.mjs:82
|
|
106
106
|
// const reached = b.layer === "national" ? (…) : regions.some(…); register-plan.mjs:318
|
|
107
107
|
//
|