clearotron 0.4.0 → 0.4.1-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/build-info.json +2 -2
- package/docs/SECURITY-OWASP.md +21 -13
- package/docs/SECURITY.md +20 -14
- package/docs/architecture/04-configuration-reference.md +3 -2
- package/docs/architecture/05-config-governance.md +1 -1
- package/docs/architecture/06-operations-runbook.md +1 -1
- package/docs/architecture/09-security-and-data.md +16 -11
- package/driver/CHANGELOG.md +18 -0
- package/driver/binding-layers.mjs +28 -6
- package/driver/citation-census.json +3 -3
- package/driver/common-law-coverage-status.mjs +52 -0
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/connotation-search.mjs +5 -5
- package/driver/contract-dictation-registry.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/contract-vocabulary.mjs +18 -18
- package/driver/degraded-parts.mjs +10 -3
- package/driver/dispatch-record.mjs +19 -0
- package/driver/doubt-closure-tool.mjs +2 -2
- package/driver/driver.config.mjs +39 -6
- package/driver/engagement-receipt.mjs +1 -1
- package/driver/engine/CONTRACT.md +41 -1
- package/driver/engine/anthropic-agent.mjs +56 -4
- package/driver/engine/mcp/codex-config.mjs +1 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +31 -4
- package/driver/engine/mcp/stdio-server.mjs +45 -7
- package/driver/engine/openai-agent.mjs +100 -17
- package/driver/engine/session-record.mjs +223 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/findings-model.mjs +3 -3
- package/driver/form-neighbourhood.mjs +7 -7
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/framework.mjs +22 -0
- package/driver/gateway.mjs +94 -12
- package/driver/named-band.mjs +2 -0
- package/driver/package.json +1 -1
- package/driver/phase0.mjs +13 -2
- package/driver/pipeline-knockout.mjs +1 -1
- package/driver/pipeline.mjs +46 -33
- package/driver/portal-families.mjs +3 -3
- package/driver/portal-service.mjs +1 -1
- package/driver/predelivery-lint.mjs +19 -19
- package/driver/publish/index.mjs +1 -1
- package/driver/publish/knockout.mjs +5 -4
- package/driver/publish/office-record-links.mjs +17 -2
- package/driver/publish/publish-inputs.mjs +1 -1
- package/driver/publish/render-knockout.mjs +17 -5
- package/driver/publish/render.mjs +4 -2
- package/driver/record-fold.mjs +177 -0
- package/driver/register-count.mjs +18 -1
- package/driver/register-plan.mjs +71 -17
- package/driver/register-records.mjs +7 -0
- package/driver/registry-fidelity.mjs +2 -2
- package/driver/repairs.mjs +1 -1
- package/driver/report-card-record.mjs +4 -4
- package/driver/scope-facts.mjs +11 -3
- package/driver/search-policy.mjs +20 -1
- package/driver/skills/clearance-common-law/SKILL.md +22 -22
- package/driver/skills/clearance-register/digest.md +3 -3
- package/driver/skills/clearance-register/register-recipes.md +11 -11
- package/driver/skills/clearance-register/status-rules.md +1 -1
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
- package/driver/skills/clearance-search/phase2-execution.md +6 -5
- package/driver/skills/clearance-search/synthesis-rules.md +4 -4
- package/driver/skills/clearance-search/worked-examples.md +39 -39
- package/driver/skills/clearance-variants/SKILL.md +16 -16
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/stage-context.mjs +27 -1
- package/driver/stages.mjs +12 -12
- package/driver/suite-census.json +175 -31
- package/driver/synthesis-record.mjs +5 -0
- package/driver/unit-inventory.mjs +17 -28
- package/driver/verify.mjs +6 -6
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/evidence.mjs +8 -8
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/package.json +2 -2
- package/package.json +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +4 -1
- package/providers/_shared/enumerate.mjs +61 -22
- package/providers/_shared/execute-plan.mjs +2 -0
- package/providers/corsearch/src/capabilities.js +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +2 -2
- package/providers/signa/src/capabilities.js +17 -2
- package/providers/signa/src/core.js +234 -37
- package/scripts/band-shape-probe.mjs +2 -2
- package/scripts/deprecate-below.mjs +28 -4
- package/scripts/e2e-first-time.mjs +16 -1
- package/scripts/freeze-example-run.mjs +2 -2
- package/shared/scope.mjs +18 -2
|
@@ -83,8 +83,24 @@ export function refToOffice(ref) {
|
|
|
83
83
|
// providers/_shared/answer-memory.mjs. The question is the base address, the method, the path and the
|
|
84
84
|
// whole body, and never the credential; the base is in it so a test server and the register never share
|
|
85
85
|
// an answer. With no memory for the run (the default), this is the plain fetch it always was.
|
|
86
|
+
// ── WHAT EACH CALL COST, AS THE REGISTER STATES IT ──────────────────────────────────────────────
|
|
87
|
+
//
|
|
88
|
+
// The register charges by the credit and says how many on every response, `X-Credits-Charged`, errors
|
|
89
|
+
// included (0). A search page and a record fetch differ tenfold, and the ledger's `tool` names the caller,
|
|
90
|
+
// not the endpoint, so a run's spend could not be added up from the ledger: every figure was a guess. The
|
|
91
|
+
// header is copied onto the ledger row with the endpoint it answered. NULL when the header is absent or
|
|
92
|
+
// unreadable, never 0 — a missing figure is not a free call. A call answered from the run memory sent
|
|
93
|
+
// nothing, so it is a real 0.
|
|
94
|
+
const creditsCharged = (resp) => {
|
|
95
|
+
const v = resp?.headers?.get?.("x-credits-charged");
|
|
96
|
+
const n = v == null || String(v).trim() === "" ? NaN : Number(v);
|
|
97
|
+
return Number.isFinite(n) ? n : null;
|
|
98
|
+
};
|
|
99
|
+
const endpointOf = (method, path) => `${method} ${String(path).split("?")[0].replace(/^(\/v1\/trademarks\/)[^/]+/, "$1{id}")}`;
|
|
100
|
+
|
|
86
101
|
export async function signaFetch(apiKey, base, path, { method = "GET", body = null, retries = 1, tctx = null } = {}) {
|
|
87
102
|
const url = `${base}${path}`;
|
|
103
|
+
const endpoint = endpointOf(method, path);
|
|
88
104
|
const headers = { Authorization: `Bearer ${apiKey}`, Accept: "application/json" };
|
|
89
105
|
const init = { method, headers };
|
|
90
106
|
if (body !== null) { headers["Content-Type"] = "application/json"; init.body = JSON.stringify(body); }
|
|
@@ -106,7 +122,7 @@ export async function signaFetch(apiKey, base, path, { method = "GET", body = nu
|
|
|
106
122
|
// cache hit, so the run's usage shows the request was made and was not paid for.
|
|
107
123
|
const { body: parsed, parseError } = parseJsonBody(held.raw);
|
|
108
124
|
const ok = held.status >= 200 && held.status < 300;
|
|
109
|
-
logCall(tctx, { http_status: held.status, ok, attempts: 0, took_ms: Date.now() - t0, bytes: held.raw.length, cache_hit: true });
|
|
125
|
+
logCall(tctx, { http_status: held.status, ok, attempts: 0, took_ms: Date.now() - t0, bytes: held.raw.length, cache_hit: true, endpoint, credits_charged: 0 });
|
|
110
126
|
note({ held: true, served: true, status: held.status });
|
|
111
127
|
return { status: held.status, ok, url, body: parsed, raw: held.raw, parseError };
|
|
112
128
|
}
|
|
@@ -119,14 +135,14 @@ export async function signaFetch(apiKey, base, path, { method = "GET", body = nu
|
|
|
119
135
|
await new Promise((r) => setTimeout(r, 1500 * (i + 1)));
|
|
120
136
|
}
|
|
121
137
|
} catch (err) {
|
|
122
|
-
logCall(tctx, { http_status: 0, ok: false, attempts, took_ms: Date.now() - t0, bytes: 0, cache_hit: false });
|
|
138
|
+
logCall(tctx, { http_status: 0, ok: false, attempts, took_ms: Date.now() - t0, bytes: 0, cache_hit: false, endpoint, credits_charged: null });
|
|
123
139
|
if (mem) note({ held: Boolean(held), status: 0 });
|
|
124
140
|
throw err;
|
|
125
141
|
}
|
|
126
142
|
const raw = await resp.text();
|
|
127
143
|
// The parse failure travels on `parseError` instead of being swallowed — providers/_shared/http-body.mjs.
|
|
128
144
|
const { body: parsed, parseError } = parseJsonBody(raw);
|
|
129
|
-
logCall(tctx, { http_status: resp.status, ok: resp.ok, attempts, took_ms: Date.now() - t0, bytes: raw.length, cache_hit: false });
|
|
145
|
+
logCall(tctx, { http_status: resp.status, ok: resp.ok, attempts, took_ms: Date.now() - t0, bytes: raw.length, cache_hit: false, endpoint, credits_charged: creditsCharged(resp) });
|
|
130
146
|
if (mem) {
|
|
131
147
|
// In `watch` every request reaches this line, held or not, and the line says what the memory would
|
|
132
148
|
// have done. The FIRST answer is the one kept: a later identical request is compared against it
|
|
@@ -180,7 +196,24 @@ export function rememberableAnswer(method, status, body, parseError) {
|
|
|
180
196
|
// `match_mode: "starts_with"` for a trailing-`*` wildcard entry, and with no deterministic shape to
|
|
181
197
|
// put it in, that entry fell through to a plain `exact` — a NARROWER query than the plan asked for,
|
|
182
198
|
// answering as though it were the one requested.
|
|
183
|
-
|
|
199
|
+
//
|
|
200
|
+
// ── ON THE WIRE, A DETERMINISTIC MODE IS A TEXT FILTER, NOT `match` ──────────────────────────────
|
|
201
|
+
//
|
|
202
|
+
// The register retired `match` on the same date as `query` and `strategies`, and after that date it
|
|
203
|
+
// refuses the parameter rather than ignoring it. That would take the containing count with it: a
|
|
204
|
+
// refused request is an error, and an error on that band is "not available" on the report. So
|
|
205
|
+
// `match` stays this file's own word for the mode, and the request builder sends the text filter the
|
|
206
|
+
// register maps it to: `filters.mark_text.<op>` with the term as a one-value list, and no `q` and no
|
|
207
|
+
// `similarity`, because a text filter on its own is the plain listing the old `match` was.
|
|
208
|
+
//
|
|
209
|
+
// Compared old form against new for every mode, by record and paged to exhaustion, on neutral terms
|
|
210
|
+
// under one class filter and again with a territory filter: the same records in the same order, and
|
|
211
|
+
// the new form draws no deprecation notice. The register already ran the old form as this filter, so
|
|
212
|
+
// the order it lists in is unchanged; nothing here adds a `sort`. The register's three-character floor
|
|
213
|
+
// on `contains` and `ends_with` refused a shorter term under the old form as well, so a short term
|
|
214
|
+
// fails the same way it always has. `similar` has no filter: the register's word for it is "remove it",
|
|
215
|
+
// because a term on its own is always ranked now, so it is sent as the ranked shape.
|
|
216
|
+
const TEXT_FILTER_FOR = Object.freeze({ exact: "is", starts_with: "starts_with", ends_with: "ends_with", contains: "contains" });
|
|
184
217
|
|
|
185
218
|
// ── THE RANKED STRATEGIES, AS SIMILARITY CHANNELS ────────────────────────────────────────────────
|
|
186
219
|
//
|
|
@@ -273,30 +306,58 @@ export function buildSearchRequest(p) {
|
|
|
273
306
|
// serializes away anyway, but writing it conditionally is what makes the owner-only shape legible here
|
|
274
307
|
// rather than an accident of JSON.stringify.
|
|
275
308
|
const body = {};
|
|
276
|
-
// ── `q` CARRIES ONE TERM, AND AN ARRAY HERE
|
|
309
|
+
// ── `q` CARRIES ONE TERM, AND AN ARRAY HERE IS A DIFFERENT SEARCH ──────────────────────────────
|
|
277
310
|
//
|
|
278
|
-
// A
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
311
|
+
// A list in `q` is a ranked search over every term, and it does not answer the same question as one
|
|
312
|
+
// request per term. Its results are ranked across the terms, so a page is not complete for any one of
|
|
313
|
+
// them; its total counts the whole list, so the enumerate ceiling cannot read a per-term crowd off it;
|
|
314
|
+
// and the register's own reference says a list skips two single-term ranking boosts and the
|
|
315
|
+
// word-by-word fuzzy retry. Sending several terms in one request is a change to what the engine asks
|
|
316
|
+
// the register, decided on its own and wired through the planner, never reached by an array that
|
|
317
|
+
// happens to arrive here.
|
|
285
318
|
//
|
|
286
319
|
// It is refused rather than joined or first-element-picked, for the same reason the stack is.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
320
|
+
//
|
|
321
|
+
// THE ONE LIST THAT IS SENT: exact spellings on the exact channels, which `toSignaParams` builds only
|
|
322
|
+
// where `exactListable` holds. Anything else arriving as a list is still refused.
|
|
323
|
+
if (Array.isArray(p.query)) {
|
|
324
|
+
const channels = similarityFor(p.strategies);
|
|
325
|
+
const exactChannels = channels.length === 2 && channels[0] === "identical" && channels[1] === "lookalike";
|
|
326
|
+
const terms = p.query.filter((t) => String(t ?? "").trim());
|
|
327
|
+
if (p.match || !exactChannels || terms.length < 2 || terms.length > CAPABILITIES.exactOrWidth)
|
|
328
|
+
throw new Error("[signa] `query` reached the request builder as a list outside the one shape the register "
|
|
329
|
+
+ "answers term for term: exact spellings on the identical and lookalike channels, 2 to "
|
|
330
|
+
+ `${CAPABILITIES.exactOrWidth} of them. Send one term per request.`);
|
|
331
|
+
body.q = terms;
|
|
332
|
+
body.similarity = channels;
|
|
333
|
+
const listFilters = buildFilters(p);
|
|
334
|
+
if (Object.keys(listFilters).length) body.filters = listFilters;
|
|
335
|
+
return finishSearchRequest(body, p);
|
|
336
|
+
}
|
|
337
|
+
const term = String(p.query ?? "").trim() ? p.query : null;
|
|
292
338
|
const match = typeof p.match === "string" ? p.match.trim() : "";
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
339
|
+
const filters = buildFilters(p);
|
|
340
|
+
const textOp = TEXT_FILTER_FOR[match];
|
|
341
|
+
if (textOp && term != null) {
|
|
342
|
+
// A text filter alone: no `q`, and no `similarity` — the register refuses channels without a `q`.
|
|
343
|
+
filters.mark_text = { [textOp]: [term] };
|
|
344
|
+
} else if (term != null) {
|
|
345
|
+
body.q = term;
|
|
296
346
|
body.similarity = similarityFor(p.strategies);
|
|
297
347
|
}
|
|
298
|
-
|
|
348
|
+
// No term at all is the owner-only shape: the owner filter is the whole question, and `similarity`
|
|
349
|
+
// "requires q" in the register's reference, so none is sent. With no owner either, the request would be
|
|
350
|
+
// an unfiltered listing of the register — a stack of names that could not go as a list arrives here
|
|
351
|
+
// with no term — so it is refused rather than sent.
|
|
352
|
+
if (term == null && !(typeof p.owner === "string" && p.owner.trim()))
|
|
353
|
+
throw new Error("[signa] a search reached the request builder with no term and no owner. A stack of names goes "
|
|
354
|
+
+ "to this register whole only as exact spellings on the exact channels; send one term per request.");
|
|
299
355
|
if (Object.keys(filters).length) body.filters = filters;
|
|
356
|
+
return finishSearchRequest(body, p);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** What every search request carries whatever its term: territory layer, paging and the total. */
|
|
360
|
+
function finishSearchRequest(body, p) {
|
|
300
361
|
if (Array.isArray(p.jurisdictions) && p.jurisdictions.length && p.territory_match) {
|
|
301
362
|
// Governs `filters.jurisdictions` ONLY — it has no effect on `filters.offices`, which is always
|
|
302
363
|
// literal. Sent verbatim rather than defaulted here: the vendor's own default is `protection`, and
|
|
@@ -362,6 +423,11 @@ export function normalizeClasses(rec) {
|
|
|
362
423
|
return out.length ? Array.from(new Set(out)).sort((a, b) => a - b) : null;
|
|
363
424
|
}
|
|
364
425
|
|
|
426
|
+
const httpsUrlOrNull = (v) => {
|
|
427
|
+
if (typeof v !== "string") return null;
|
|
428
|
+
try { return new URL(v).protocol === "https:" ? v : null; } catch { return null; }
|
|
429
|
+
};
|
|
430
|
+
|
|
365
431
|
// Map a raw Signa record → the NEUTRAL normalized shape the driver + skill consume. officeHint = the
|
|
366
432
|
// office of the synthetic ref the caller cited (keeps cited==logged); record's jurisdiction_code is the
|
|
367
433
|
// fallback (Signa records carry an ISO jurisdiction_code directly, so no office map is needed).
|
|
@@ -397,7 +463,14 @@ export function normalizeRecord(rec, officeHint = null) {
|
|
|
397
463
|
ownerCountry: owner0?.country_code ?? owner0?.country ?? null,
|
|
398
464
|
representative: atty0?.name ?? null,
|
|
399
465
|
imageAvailable: rec.has_media ?? null,
|
|
400
|
-
|
|
466
|
+
// Signa publishes no page per record of its own, so no record URL is minted here and the model's
|
|
467
|
+
// record links stay refused for this register (hasPublicRecordUrl: false).
|
|
468
|
+
resolved_link: null,
|
|
469
|
+
// THE OFFICE'S PAGE, AS THE REGISTER GIVES IT: `office_url`, a link to the record on the office's own
|
|
470
|
+
// site, absent on some records. Carried as data for publish (office-record-links.mjs), which addresses
|
|
471
|
+
// the office's page from the record's numbers first and falls back to this only where that table
|
|
472
|
+
// gives no address. Only an https address is kept.
|
|
473
|
+
officeUrl: httpsUrlOrNull(rec.office_url),
|
|
401
474
|
// ── WHICH LAYER THIS RIGHT SITS ON, carried as data ( →) ──────────────────────────
|
|
402
475
|
// The normalizer read under half the fields a search row carries. Among the ones it dropped were the
|
|
403
476
|
// four that say what KIND of right a record is — and those are not extras, they are the whole
|
|
@@ -488,6 +561,10 @@ function normalizeSearchRow(rec) {
|
|
|
488
561
|
application_date: filed,
|
|
489
562
|
registration_date: toIso(rec.registration_date),
|
|
490
563
|
relevance_score: Number.isInteger(rec.relevance_score) ? rec.relevance_score : null,
|
|
564
|
+
// Why the register returned this row: `{ tier, via, terms }` — the tier it rates the resemblance,
|
|
565
|
+
// the channels that found it, and the terms or filter values it matched. It replaced the per-row
|
|
566
|
+
// `match_explanation` and `matched_terms`, which the register removed on 2026-10-05.
|
|
567
|
+
match: rec.match && typeof rec.match === "object" ? rec.match : null,
|
|
491
568
|
raw: rec,
|
|
492
569
|
};
|
|
493
570
|
}
|
|
@@ -546,8 +623,10 @@ export function normalizeSearchResponse(body, echoQuery) {
|
|
|
546
623
|
const { total, approximate, floor } = readTotal(body);
|
|
547
624
|
return {
|
|
548
625
|
query: meta.query ?? echoQuery,
|
|
549
|
-
|
|
550
|
-
|
|
626
|
+
// The channels the register says it ran. It removed `strategies_used` and `match` from this object
|
|
627
|
+
// on 2026-10-05 and names this field as their replacement; reading the old two returned `[]` and
|
|
628
|
+
// `null` on every answer after that date while looking like a report of what ran.
|
|
629
|
+
similarity_applied: Array.isArray(meta.similarity_applied) ? meta.similarity_applied : null,
|
|
551
630
|
search_id: meta.search_id ?? null,
|
|
552
631
|
// ── THE REGISTER'S OWN WARNINGS, CARRIED WHOLE ──────────────────────────────────────────────
|
|
553
632
|
//
|
|
@@ -571,8 +650,17 @@ export function normalizeSearchResponse(body, echoQuery) {
|
|
|
571
650
|
// back one row per RECORD instead of one row per MARK — and the register's own note says that
|
|
572
651
|
// inflates the total. The total is what the enumerate ceiling reads to call a band a crowd, so a
|
|
573
652
|
// band can be declared a crowd on a number that counts designations rather than marks. This field
|
|
574
|
-
// is what makes that visible; it
|
|
653
|
+
// is what makes that visible; `total_counts` below says it where the total is read.
|
|
575
654
|
warnings: Array.isArray(meta.warnings) ? meta.warnings : [],
|
|
655
|
+
// ── WHAT THE TOTAL COUNTS, WHEN THE REGISTER SAYS ────────────────────────────────────────────
|
|
656
|
+
//
|
|
657
|
+
// `records` when the register flagged `expanded_fallback` on this answer: the total then counts one
|
|
658
|
+
// row per record, so a mark filed in several countries is counted once per country it covers. The
|
|
659
|
+
// count stays the register's own number and is never presented as a count of marks (ruled
|
|
660
|
+
// 2026-10-02: record only, no extra call). Absent when the register said nothing: its grouped view
|
|
661
|
+
// answered, or the response was not an answer. Keyed on the code the register writes into
|
|
662
|
+
// `search_meta.warnings`, in the shape measured live and kept by the warnings test beside this file.
|
|
663
|
+
...(answered && (Array.isArray(meta.warnings) ? meta.warnings : []).some(isExpandedFallback) ? { total_counts: "records" } : {}),
|
|
576
664
|
// The corpus total when the vendor counted it exactly; null when it did not answer, when the
|
|
577
665
|
// total was not requested, and when the figure it returned is an approximation. NEVER the page
|
|
578
666
|
// size — `data.length` is `count`, and conflating the two is how a page reads as a corpus.
|
|
@@ -722,9 +810,74 @@ export async function doSearch(apiKey, base, params, tctx, { mock = false } = {}
|
|
|
722
810
|
if (!isSearchResponseBody(r.body)) {
|
|
723
811
|
return { type: "text", text: nonAnswerBodyError("signa_search", r, "a search response (no data[] — the key every /v1/trademarks answer carries)", ` ${searchTargetLabel(params).slice(0, 120)}`) };
|
|
724
812
|
}
|
|
813
|
+
// A 200 that did not run every channel it was asked for answered a narrower question; see skippedChannels.
|
|
814
|
+
const skipped = skippedChannels(r.body, body.similarity);
|
|
815
|
+
if (skipped.length) {
|
|
816
|
+
return { type: "text", text: `ERROR: signa_search — the register skipped the ${skipped.join(", ")} channel`
|
|
817
|
+
+ `${skipped.length > 1 ? "s" : ""} this search asked for, so its answer covers a narrower question than the one `
|
|
818
|
+
+ `asked and is not taken as this search's answer. The register decides this from the term, so asking the same `
|
|
819
|
+
+ `search again will be skipped the same way. ${searchTargetLabel(params).slice(0, 120)}` };
|
|
820
|
+
}
|
|
821
|
+
// A text filter compares letters as written, so a term mixing alphabets matches nothing; see mixedScriptFilters.
|
|
822
|
+
const unreadable = mixedScriptFilters(r.body, body.filters?.mark_text);
|
|
823
|
+
if (unreadable.length) {
|
|
824
|
+
return { type: "text", text: `ERROR: signa_search — the register reports that the term mixes alphabets, and its `
|
|
825
|
+
+ `${unreadable.join(", ")} filter compares letters as written, so it can match nothing. Its zero is not a count `
|
|
826
|
+
+ `of filings and is not taken as this search's answer. ${searchTargetLabel(params).slice(0, 120)}` };
|
|
827
|
+
}
|
|
725
828
|
return { type: "text", text: JSON.stringify(normalizeSearchResponse(r.body, params.query), null, 2) };
|
|
726
829
|
}
|
|
727
830
|
|
|
831
|
+
// ── A TEXT FILTER CANNOT READ A TERM THAT MIXES ALPHABETS ─────────────────────────────────────────
|
|
832
|
+
//
|
|
833
|
+
// The text filters compare letters as written. A term mixing Latin with Greek or Cyrillic letters
|
|
834
|
+
// therefore matches no filing, and the register answers 200 with zero rows and an `info`-level
|
|
835
|
+
// `mixed_script` warning naming the filter — in its own words, "a ranked q with the lookalike similarity
|
|
836
|
+
// channel folds look-alike letters". Measured live on an invented term with one Cyrillic letter: the
|
|
837
|
+
// ranked search found 219 filings, the containing filter 0. That zero would be the containing count on the
|
|
838
|
+
// report, an exact zero on a figure the client reads. So the answer is refused, naming the filter, and
|
|
839
|
+
// the count reads as unknown rather than none.
|
|
840
|
+
//
|
|
841
|
+
// Matched on the warning's `affected_filter` (`mark_text_<op>`) against the filter this request sent; a
|
|
842
|
+
// warning that names no filter counts against any text filter the request carried.
|
|
843
|
+
export function mixedScriptFilters(body, markText) {
|
|
844
|
+
const sent = Object.keys(markText && typeof markText === "object" ? markText : {});
|
|
845
|
+
if (!sent.length) return [];
|
|
846
|
+
const warnings = Array.isArray(body?.search_meta?.warnings) ? body.search_meta.warnings : [];
|
|
847
|
+
const out = [];
|
|
848
|
+
for (const w of warnings) {
|
|
849
|
+
if (String(w?.code ?? "") !== "mixed_script") continue;
|
|
850
|
+
const named = String(w?.affected_filter ?? "").replace(/^mark_text_/, "");
|
|
851
|
+
for (const op of named ? sent.filter((o) => o === named) : sent) if (!out.includes(op)) out.push(op);
|
|
852
|
+
}
|
|
853
|
+
return out;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ── A CHANNEL THE REGISTER SKIPPED IS A QUESTION IT DID NOT ANSWER ─────────────────────────────────
|
|
857
|
+
//
|
|
858
|
+
// The register runs a term through the similarity channels a request names, and when one of them cannot
|
|
859
|
+
// run on that term it says so in `search_meta.warnings` — `<channel>_skipped`, with the channel in
|
|
860
|
+
// `channel` — and answers 200 with what the other channels found. Its migration guide states it in those
|
|
861
|
+
// words. A sound-alike search answered with `phonetic_skipped` has been answered by `identical` alone: the
|
|
862
|
+
// same rows an exact search gives, under the sound-alike question's name, and the band would read as
|
|
863
|
+
// searched. That is the failure this connector refuses everywhere else — a narrower query wearing a
|
|
864
|
+
// complete answer — so it is refused here too, and the band is left incomplete with the channel named.
|
|
865
|
+
//
|
|
866
|
+
// Only a channel the request ASKED for counts. A warning about a channel nobody asked for changes nothing
|
|
867
|
+
// about the answer to the question that was asked. A warning carrying no `channel` is read from its code.
|
|
868
|
+
export function skippedChannels(body, requested) {
|
|
869
|
+
const asked = new Set(Array.isArray(requested) ? requested : []);
|
|
870
|
+
const warnings = Array.isArray(body?.search_meta?.warnings) ? body.search_meta.warnings : [];
|
|
871
|
+
const out = [];
|
|
872
|
+
for (const w of warnings) {
|
|
873
|
+
const code = String(w?.code ?? "");
|
|
874
|
+
if (!code.endsWith("_skipped")) continue;
|
|
875
|
+
const channel = String(w?.channel ?? code.slice(0, -"_skipped".length)).trim().toLowerCase();
|
|
876
|
+
if (asked.has(channel) && !out.includes(channel)) out.push(channel);
|
|
877
|
+
}
|
|
878
|
+
return out;
|
|
879
|
+
}
|
|
880
|
+
|
|
728
881
|
// ── Record fetch ─────────────────────────────────────────────────────────────────────────────────
|
|
729
882
|
// Accepts a synthetic `/mark/<office>/<id>` ref OR a raw Signa id. Returns the NORMALIZED record and
|
|
730
883
|
// persists it (keyed by the synthetic ref) for the driver's A1 citation-fidelity gate.
|
|
@@ -766,25 +919,57 @@ export async function doRecordFetch(apiKey, base, params, tctx, { mock = false }
|
|
|
766
919
|
// speaks the same dialect as the code under test cannot find a dialect mismatch, which is why the one
|
|
767
920
|
// below builds its params the way the kernel does and not the way doSearch prefers.
|
|
768
921
|
//
|
|
769
|
-
// `names` (an OR-stack)
|
|
770
|
-
//
|
|
771
|
-
//
|
|
772
|
-
//
|
|
922
|
+
// `names` (an OR-stack) goes to the register whole only as exact spellings, as one ranked list (see
|
|
923
|
+
// `exactListable`); every other stack must reach here already split. Silently searching only its first
|
|
924
|
+
// term would be a narrowed query wearing a complete answer, which is this whole track's failure mode.
|
|
925
|
+
// ── WHEN A STACK OF SPELLINGS GOES AS ONE REQUEST ─────────────────────────────────────────────────
|
|
926
|
+
//
|
|
927
|
+
// An exact question over several spellings, each long enough for the ranked search, with no owner
|
|
928
|
+
// clause: the shape the register answers as one ranked list exactly as it answers each spelling alone
|
|
929
|
+
// (see `exactOrWidth` in capabilities.js for the measurement). The list's terms ride a POST body the
|
|
930
|
+
// register caps at 8 KB, so a stack of long spellings is cut to fit with room for the filters.
|
|
931
|
+
const LIST_BODY_BUDGET = 6000;
|
|
932
|
+
const termBytes = (t) => Buffer.byteLength(JSON.stringify(String(t)), "utf8") + 1;
|
|
933
|
+
function exactStackShape(p) {
|
|
934
|
+
if (String(p?.match_mode ?? "").trim() !== "exact") return null;
|
|
935
|
+
if (typeof p?.owner === "string" && p.owner.trim()) return null;
|
|
936
|
+
if (Array.isArray(p?.owners) && p.owners.length) return null;
|
|
937
|
+
const stack = Array.isArray(p?.names) ? p.names.filter(Boolean) : [];
|
|
938
|
+
if (stack.length < 2) return null;
|
|
939
|
+
if (stack.some((t) => foldedLength(t) < CAPABILITIES.rankedMinLength)) return null;
|
|
940
|
+
return stack;
|
|
941
|
+
}
|
|
942
|
+
/** How many spellings one request carries for this kernel query, or null when it is one per request. */
|
|
943
|
+
export function namesChunkFor(p) {
|
|
944
|
+
const stack = exactStackShape(p);
|
|
945
|
+
if (!stack) return null;
|
|
946
|
+
const widest = Math.max(...stack.map(termBytes));
|
|
947
|
+
return Math.max(1, Math.min(CAPABILITIES.exactOrWidth, Math.floor(LIST_BODY_BUDGET / widest)));
|
|
948
|
+
}
|
|
949
|
+
/** May this stack go to the register whole, as one ranked list? */
|
|
950
|
+
export function exactListable(p) {
|
|
951
|
+
const stack = exactStackShape(p);
|
|
952
|
+
return Boolean(stack) && stack.length <= namesChunkFor(p)
|
|
953
|
+
&& stack.reduce((n, t) => n + termBytes(t), 0) <= LIST_BODY_BUDGET;
|
|
954
|
+
}
|
|
955
|
+
|
|
773
956
|
export function toSignaParams(p = {}) {
|
|
774
957
|
const out = { ...p };
|
|
775
958
|
if (p.name != null && p.query == null) out.query = p.name;
|
|
776
959
|
// ── A ONE-TERM `names` WINDOW IS A TERM, NOT A STACK ────────────────────────────────────────────
|
|
777
|
-
//
|
|
778
|
-
// enumerate contract on each — that is what `namesChunkDefault: 1` is for. Every window therefore
|
|
960
|
+
// Outside an exact spelling stack the kernel CHUNKS a wide names band into windows of one and runs the
|
|
961
|
+
// full enumerate contract on each — that is what `namesChunkDefault: 1` is for. Every window therefore
|
|
779
962
|
// arrives here as `names: ["one-term"]`, and with no mapping it came back "ERROR: query is
|
|
780
963
|
// required": a names band could not run on this provider AT ALL, and the failure was reported as
|
|
781
964
|
// chunk 1 failing rather than as the adapter refusing a shape the kernel had already split.
|
|
782
965
|
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
966
|
+
// A MULTI-term stack is never narrowed to names[0] — that would be a narrowed query wearing a complete
|
|
967
|
+
// answer. It is sent whole, as one ranked list, only where `exactListable` says the register answers
|
|
968
|
+
// the list exactly as it answers each spelling alone; otherwise it reaches the request builder with no
|
|
969
|
+
// term and is refused there.
|
|
970
|
+
const stack = Array.isArray(p.names) ? p.names.filter(Boolean) : [];
|
|
971
|
+
if (out.query == null && stack.length === 1) out.query = stack[0];
|
|
972
|
+
else if (out.query == null && stack.length > 1 && exactListable(p)) out.query = stack;
|
|
788
973
|
// ── STAGE 2 — A TERRITORY IS A STACK OF RIGHTS, AND THIS PROVIDER CAN SEARCH THE STACK ─────
|
|
789
974
|
//
|
|
790
975
|
// The plan's `regions` are the territories the matter ordered, already translated into this vendor's
|
|
@@ -846,7 +1031,7 @@ export function toSignaParams(p = {}) {
|
|
|
846
1031
|
// identical question relies on. So exact stays ranked from the floor up, and only a term below it
|
|
847
1032
|
// goes deterministic, where it is answered rather than refused.
|
|
848
1033
|
const mode = String(p.match_mode ?? "").trim();
|
|
849
|
-
if (mode === "exact" && foldedLength(out.query) < CAPABILITIES.rankedMinLength) out.match = "exact";
|
|
1034
|
+
if (mode === "exact" && !Array.isArray(out.query) && foldedLength(out.query) < CAPABILITIES.rankedMinLength) out.match = "exact";
|
|
850
1035
|
else if (mode === "exact" || mode === "phonetic" || mode === "prefix") out.strategies = [mode];
|
|
851
1036
|
else if (mode === "starts_with" || mode === "ends_with" || mode === "contains") out.match = mode;
|
|
852
1037
|
// ── `default` IS THE COUNT LANE'S WORD FOR THE SAME UNANCHORED QUERY THE PLAN LANE CALLS `{}` ────
|
|
@@ -987,6 +1172,9 @@ const { enumerate: __enumerate } = makeEnumerate({
|
|
|
987
1172
|
missingElementError: MISSING_ELEMENT_ERROR,
|
|
988
1173
|
capabilities: { ...CAPABILITIES.kernel },
|
|
989
1174
|
ceilingFor: ownerWindowCeiling,
|
|
1175
|
+
// Exact spelling stacks go as one ranked list of up to `exactOrWidth`; every other names band stays at
|
|
1176
|
+
// one term per request (`namesChunkDefault`).
|
|
1177
|
+
namesChunkFor,
|
|
990
1178
|
// The kernel's default cheap-probe params are CORSEARCH'S — `{limit:1, fields:["uri"]}` — and `uri`
|
|
991
1179
|
// is not a field this vendor has. Its `fields` projection rejects unknown names, so inheriting the
|
|
992
1180
|
// default would 400 every count-first rescue while the ordinary search beside it worked. `limit: 1`
|
|
@@ -1098,3 +1286,12 @@ export async function doCountHits(apiKey, base, params, tctx, { mock = false } =
|
|
|
1098
1286
|
}, null, 2),
|
|
1099
1287
|
};
|
|
1100
1288
|
}
|
|
1289
|
+
|
|
1290
|
+
/**
|
|
1291
|
+
* The register's own flag that a total counts records rather than marks: the `expanded_fallback` warning,
|
|
1292
|
+
* which it writes into `search_meta.warnings` when its grouped view cannot serve a filter. Matched on the
|
|
1293
|
+
* code, or on a message that opens with it, so a warning reshaped either way is still read. PURE.
|
|
1294
|
+
*/
|
|
1295
|
+
export function isExpandedFallback(w) {
|
|
1296
|
+
return w?.code === "expanded_fallback" || /^expanded_fallback\b/.test(String(w?.message ?? ""));
|
|
1297
|
+
}
|
|
@@ -64,7 +64,7 @@ if (!/^\d+$/.test(String(opts.seed)) || Number(opts.seed) <= 0)
|
|
|
64
64
|
|
|
65
65
|
// ── the inputs deriveBandShape uses, read the same way it reads them ─────────────────────────────────
|
|
66
66
|
//
|
|
67
|
-
// Deliberately mirrors driver/pipeline.mjs:441-490 rather than inventing a gathering step. If the two
|
|
67
|
+
// Deliberately mirrors driver/pipeline.mjs:441-490 mergeRegisterBands rather than inventing a gathering step. If the two
|
|
68
68
|
// diverge, this probe measures a shape the run never had.
|
|
69
69
|
|
|
70
70
|
const runDir = opts.run;
|
|
@@ -243,7 +243,7 @@ if (membershipBroken.length) {
|
|
|
243
243
|
|
|
244
244
|
console.log(`\n${"═".repeat(78)}`);
|
|
245
245
|
console.log(`This measures the BAND SHAPE only, and it is free. It is NOT the noise floor #217 needs.`);
|
|
246
|
-
console.log(`That floor lives in the model-authored placement tier (placement-model.mjs:46) and needs`);
|
|
246
|
+
console.log(`That floor lives in the model-authored placement tier (placement-model.mjs:46 PLACEMENT_TIERS) and needs`);
|
|
247
247
|
console.log(`repeat paid arms on a named run dir with approved spend. Nothing here substitutes for it.`);
|
|
248
248
|
console.log(`Nothing was written. Exit code is 0 either way — this records, it does not judge.\n`);
|
|
249
249
|
process.exit(0);
|
|
@@ -114,9 +114,26 @@ export function selectRange(all, { from, through, except }, distTags = {}) {
|
|
|
114
114
|
if (!inRange(e)) throw new Error(`the exception ${e} lies outside ${from}..${through}`);
|
|
115
115
|
}
|
|
116
116
|
const picked = all.filter((v) => inRange(v) && !except.includes(v)).sort(compareVersions);
|
|
117
|
+
return refuseTagged(picked, distTags, "the range");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The published versions below a stable. Throws, before anything is written, when one of them is a
|
|
122
|
+
* version a dist-tag points at: the same refusal the range mode makes, because the hazard is the same.
|
|
123
|
+
*/
|
|
124
|
+
export function selectBelow(all, below, distTags = {}) {
|
|
125
|
+
return refuseTagged(all.filter((v) => compareVersions(v, below) < 0), distTags, `the sweep below ${below}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* ONE CHECK FOR EVERY MODE. `latest` and `beta` are what `npm install` resolves, so deprecating the version
|
|
130
|
+
* a tag points at warns every installer about the version they were just given. Each mode passes its
|
|
131
|
+
* selection through here, so a mode added later cannot select versions without it.
|
|
132
|
+
*/
|
|
133
|
+
export function refuseTagged(picked, distTags = {}, what = "the selection") {
|
|
117
134
|
const tagged = Object.entries(distTags).filter(([, v]) => picked.includes(v));
|
|
118
135
|
if (tagged.length) {
|
|
119
|
-
throw new Error(
|
|
136
|
+
throw new Error(`${what} takes ${tagged.map(([t, v]) => `${v} (${t})`).join(", ")}, which a dist-tag points at; `
|
|
120
137
|
+ "move the tag or hold the version out");
|
|
121
138
|
}
|
|
122
139
|
return picked;
|
|
@@ -124,6 +141,9 @@ export function selectRange(all, { from, through, except }, distTags = {}) {
|
|
|
124
141
|
|
|
125
142
|
const npm = (...args) => execFileSync("npm", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
126
143
|
|
|
144
|
+
/** The registry's dist-tags, read in one place so every mode is refused on the same facts. */
|
|
145
|
+
const readDistTags = () => JSON.parse(npm("view", PKG, "dist-tags", "--json"));
|
|
146
|
+
|
|
127
147
|
/** Every published version, and the deprecation message each carries. Read once, as a list. */
|
|
128
148
|
function published() {
|
|
129
149
|
const versions = JSON.parse(npm("view", PKG, "versions", "--json"));
|
|
@@ -202,7 +222,12 @@ async function main() {
|
|
|
202
222
|
return 2;
|
|
203
223
|
}
|
|
204
224
|
|
|
205
|
-
|
|
225
|
+
let candidates;
|
|
226
|
+
try { candidates = selectBelow(all, below, readDistTags()); }
|
|
227
|
+
catch (e) {
|
|
228
|
+
console.error(`deprecate-below: refusing — ${e.message}. Nothing was written.`);
|
|
229
|
+
return 2;
|
|
230
|
+
}
|
|
206
231
|
console.log(`deprecate-below: ${all.length} published version(s); ${candidates.length} below ${below}.`);
|
|
207
232
|
|
|
208
233
|
let done = 0, skipped = 0, failed = 0;
|
|
@@ -262,8 +287,7 @@ async function deprecateRange(spec, DRY) {
|
|
|
262
287
|
const parsed = parseRangeSpec(spec);
|
|
263
288
|
const all = published();
|
|
264
289
|
if (!all.length) throw new Error("the registry listed no versions");
|
|
265
|
-
|
|
266
|
-
picked = selectRange(all, parsed, distTags);
|
|
290
|
+
picked = selectRange(all, parsed, readDistTags());
|
|
267
291
|
console.log(`deprecate-below: ${all.length} published version(s); ${picked.length} in ${spec}.`);
|
|
268
292
|
} catch (e) {
|
|
269
293
|
console.error(`deprecate-below: refusing — ${e.message}. Nothing was written.`);
|
|
@@ -36,6 +36,18 @@ import { driverDir, labelOfDriverFile } from "../shared/driver-dir.mjs";
|
|
|
36
36
|
// What no file of the run record holds, stated on every answer. The register's own HTTP retries are
|
|
37
37
|
// written to the provider call log, which is per machine, not per run; and a tool result whose text reports a
|
|
38
38
|
// failure while its call settles ok carries no marker the call log keeps.
|
|
39
|
+
/** One line for what interrupted a session, from its record: counts only, never the program's words. */
|
|
40
|
+
function interruptionOf(session) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
const cuts = session?.classifierCuts?.length ?? 0, refusals = session?.refusals?.length ?? 0;
|
|
43
|
+
const errors = (session?.results ?? []).filter((r) => r?.isError).length, starts = Number(session?.starts) || 0;
|
|
44
|
+
if (cuts) parts.push(`${cuts} classifier cut${cuts === 1 ? "" : "s"}`);
|
|
45
|
+
if (refusals) parts.push(`${refusals} refusal${refusals === 1 ? "" : "s"}`);
|
|
46
|
+
if (errors) parts.push(`${errors} error result${errors === 1 ? "" : "s"}`);
|
|
47
|
+
if (starts > 1) parts.push(`${starts - 1} restart${starts === 2 ? "" : "s"}`);
|
|
48
|
+
return parts.length ? `the session went on to deliver after ${parts.join(", ")}` : "the session's record marks it interrupted";
|
|
49
|
+
}
|
|
50
|
+
|
|
39
51
|
export const NOT_RECORDED = [
|
|
40
52
|
"register calls the provider retried inside one tool call (the call log is per machine, not per run)",
|
|
41
53
|
"a tool result that reports a failure in its text while the call settles ok",
|
|
@@ -347,7 +359,10 @@ export function firstTimeRows({ attempts = [], runLog = [], status = {}, markers
|
|
|
347
359
|
if (prev === undefined || n <= prev) cycles.set(stage, [...(cycles.get(stage) ?? []), { start: n, after: prev ?? null, row }]);
|
|
348
360
|
prevAttempt.set(stage, n);
|
|
349
361
|
}
|
|
350
|
-
|
|
362
|
+
// A session that was cut, refused or restarted and still delivered is not ok and did not fail: it is
|
|
363
|
+
// named for what happened to it (engine/session-record.mjs), never as a failure with no cause.
|
|
364
|
+
if (row.ok === false && !row.fail && row.session?.interrupted) add("interrupted session", stage, n, interruptionOf(row.session), row);
|
|
365
|
+
else if (row.ok === false) add("failed attempt", stage, n, row.fail ?? "no cause recorded", row);
|
|
351
366
|
else if (Number.isFinite(n) && n > 1) add("retry", stage, n, `attempt ${n} succeeded`, row);
|
|
352
367
|
if (row.rescued) add("rescue", stage, n, `rescued: ${row.rescued}`, row);
|
|
353
368
|
if (row.selfReportContradicted === true && row.ok !== false) add("self-report contradicted", stage, n, "the model reported done and its file said otherwise", row);
|
|
@@ -100,8 +100,8 @@ for (const path of Object.keys(PUBLISH_INPUTS)) {
|
|
|
100
100
|
const KNOCKOUT_FILES = [
|
|
101
101
|
{ path: "knockout-findings.json", required: true, why: "report-registry.mjs:66 the batch findings ARE the report source" },
|
|
102
102
|
{ path: "knockout-plan.json", why: "report-registry.mjs:77 plan" },
|
|
103
|
-
{ path: "knockout-assessment.md", why: "the merged prose the lane writes (gateway.mjs:184)" },
|
|
104
|
-
{ path: "knockout-frame.md", why: "the batch scope note (gateway.mjs:65)" },
|
|
103
|
+
{ path: "knockout-assessment.md", why: "the merged prose the lane writes (gateway.mjs:184 TOOL_WRITTEN_PATTERNS)" },
|
|
104
|
+
{ path: "knockout-frame.md", why: "the batch scope note (gateway.mjs:65 TOOL_WRITTEN_ARTIFACTS)" },
|
|
105
105
|
{ path: "email-body.md", why: "the delivery prose the lane writes beside the assessment" },
|
|
106
106
|
{ path: "status.json", why: "publish/index.mjs:913 machineLedgerNote + markName" },
|
|
107
107
|
{ path: "audit.md", why: "publish/index.mjs:1022 auditMd, the audit workbook source" },
|
package/shared/scope.mjs
CHANGED
|
@@ -792,6 +792,12 @@ function tenantStamp(scope, asked) {
|
|
|
792
792
|
throw new Error(`name the organisation whose Generic this is (tenant) — your access covers ${orgs.length}`);
|
|
793
793
|
}
|
|
794
794
|
|
|
795
|
+
// The install's organisation when the grants file holds exactly one; null with several, none, or no file.
|
|
796
|
+
function soleOrganisation() {
|
|
797
|
+
const orgs = Object.keys(loadGrants()?.tenants ?? {});
|
|
798
|
+
return orgs.length === 1 ? orgs[0] : null;
|
|
799
|
+
}
|
|
800
|
+
|
|
795
801
|
// True iff `email`'s domain (the part after the final '@') is one of firmDomains. PURE (no jose), so the HTTP
|
|
796
802
|
// handler can derive firm-staff-ness from the already-verified CF identity without pulling the auth lib. The
|
|
797
803
|
// `internal` (read-all) grant rests on THIS positive check — NOT on which CF app admitted the caller — so a
|
|
@@ -1016,8 +1022,18 @@ export function authorize(scope, toolName, args = {}) {
|
|
|
1016
1022
|
// paid run whose delivery packet routes nowhere. A refusal naming the missing field is the cheaper
|
|
1017
1023
|
// failure by far, and it is not the "accepted then quietly narrower" shape: the preview describes a
|
|
1018
1024
|
// job stamped from the verified identity, and start_run says plainly which field to add.
|
|
1019
|
-
|
|
1020
|
-
|
|
1025
|
+
//
|
|
1026
|
+
// WHICH ORGANISATION'S GENERIC. An ops token names no person, so there is nobody's reach to read the
|
|
1027
|
+
// organisation from — and passed through bare, every Generic run a connector started was filed under
|
|
1028
|
+
// no organisation, where the Generic filter on Clearances can never find it. On an install that holds
|
|
1029
|
+
// exactly one organisation there is nothing to guess, so the job takes it: the same answer the
|
|
1030
|
+
// person-scoped branch gives a person who sees that one organisation. A named tenant passes as it
|
|
1031
|
+
// stands, and with several organisations (or none) the job is left as it was.
|
|
1032
|
+
const placed = (toolName === "start_run" || toolName === "plan_run") && (args?.profileKey ?? "generic") === "generic"
|
|
1033
|
+
&& !(typeof args?.tenant === "string" && args.tenant.trim()) ? soleOrganisation() : null;
|
|
1034
|
+
const routed = placed ? { ...args, tenant: placed } : args;
|
|
1035
|
+
if (toolName === "plan_run") return stampForwarder(scope, routed);
|
|
1036
|
+
return routed;
|
|
1021
1037
|
}
|
|
1022
1038
|
// kind === "account": a signed-in CLIENT, across their OWN account(s). Reads the client layer on any of
|
|
1023
1039
|
// their runs, and drives the run lifecycle for them. Run OWNERSHIP is not checked here — it is enforced
|