clearotron 0.3.0-beta.1 → 0.3.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +13 -2
- package/CONTRIBUTING.md +1 -1
- package/INSTALL.md +12 -10
- package/README.md +3 -2
- package/bin/brandowner.mjs +94 -1
- package/bin/onboard.mjs +156 -55
- package/bin/passphrase.mjs +23 -4
- package/bin/start.mjs +177 -38
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +5 -2
- package/docs/architecture/05-config-governance.md +3 -2
- package/driver/CHANGELOG.md +95 -0
- package/driver/demo-posture.mjs +59 -8
- package/driver/driver.config.mjs +36 -7
- package/driver/engine/child-record.mjs +93 -0
- package/driver/findings-model.mjs +6 -1
- package/driver/knockout-assess-record.mjs +6 -3
- package/driver/package.json +1 -1
- package/driver/portal-local-auth.mjs +98 -3
- package/driver/portal-service.mjs +65 -29
- package/driver/publish/knockout.mjs +12 -1
- package/driver/publish/office-record-links.mjs +61 -10
- package/driver/publish/render-knockout.mjs +31 -11
- package/driver/publish/xlsx.mjs +33 -1
- package/driver/register-records.mjs +6 -0
- package/driver/run-requirements.mjs +24 -1
- package/driver/runner.mjs +10 -5
- package/driver/skills/knockout-assess/SKILL.md +1 -1
- package/driver/suite-census.json +174 -30
- package/driver/unit-inventory.mjs +109 -5
- package/driver/updater-identity.mjs +178 -0
- package/driver/usage-ledger.mjs +5 -5
- package/mcp-server/CHANGELOG.md +6 -0
- package/mcp-server/http-server.mjs +16 -13
- package/mcp-server/lib/driver.mjs +7 -0
- package/mcp-server/lib/knockout.mjs +14 -2
- package/mcp-server/lib/ops.mjs +38 -16
- package/mcp-server/package.json +2 -2
- package/mcp-server/server.mjs +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-CWTHP0sH.js → index-CcFjgM78.js} +32 -17
- package/portal-ui/dist/assets/{index-KpytsmNH.css → index-CsCuPshD.css} +7 -2
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +3 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +2 -2
- package/scripts/changelog-plain-language.mjs +7 -30
- package/scripts/e2e.mjs +13 -2
- package/scripts/env-audit.mjs +10 -3
- package/scripts/env-classify.mjs +205 -12
- package/scripts/live-surface-check.mjs +74 -2
- package/scripts/plain-language-rules.mjs +103 -0
- package/scripts/release-note-required.mjs +118 -24
- package/scripts/release-notes-lint.mjs +22 -43
- package/scripts/release-version.mjs +59 -0
- package/scripts/revisit-render-check.mjs +6 -2
- package/scripts/text-difference.mjs +22 -0
- package/shared/env-local.mjs +25 -2
- package/shared/names-in-force.mjs +1 -0
- package/shared/reap-on-exit.mjs +27 -14
- package/shared/store-in-repo.mjs +147 -0
- package/shared/withheld-paths-access.mjs +6 -6
- package/shared/yes-no-echo.mjs +35 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
// updater-identity.mjs — WHICH COPY OF THE UPDATER IS ACTUALLY DEPLOYING THIS BOX.
|
|
4
|
+
//
|
|
5
|
+
// Deploy health attributes a service to a commit by resolving a tree from the unit's WorkingDirectory
|
|
6
|
+
// and from `/proc/<pid>/cmdline`. Both halves fail for exactly one unit, and it is the one that does the
|
|
7
|
+
// deploying: the updater is a `oneshot` that runs from a home directory with no checkout under it, and
|
|
8
|
+
// its command line is the update script rather than the product entry point. So the unit that places
|
|
9
|
+
// every commit on this box was the one unit the check reported as "could NOT be read and was NOT
|
|
10
|
+
// compared" — an updater running from a stale copy would place a stale tree, and nothing would notice.
|
|
11
|
+
//
|
|
12
|
+
// THE UPDATER STAMPS ITSELF, the way the drainer already does, and this reads that stamp. The two shapes
|
|
13
|
+
// are deliberately the same: a best-effort write at the top of a tick, a verdict at the reading end, and
|
|
14
|
+
// an absent stamp treated as a failure rather than a skip.
|
|
15
|
+
//
|
|
16
|
+
// AN ABSENT STAMP IS THE LOUD CASE, and that is the whole point rather than a severity preference. The
|
|
17
|
+
// stamp writer shipped in the updater itself, so a copy of the updater old enough to predate it writes
|
|
18
|
+
// no stamp at all — which is precisely "the updater is running from a stale copy", the condition this
|
|
19
|
+
// exists to catch. Recording that as "not probed" would pass the exact box the check is for.
|
|
20
|
+
//
|
|
21
|
+
// WHAT THE SHA PAIR PROVES, AND WHAT IT DOES NOT. The stamp records the running copy's digest and the
|
|
22
|
+
// digest of the master it was taken from, and those two agreeing means the running copy matches the
|
|
23
|
+
// master AS THAT BOX CAN SEE IT. The master is read from a local store that nothing pulls
|
|
24
|
+
// automatically, so a store nobody has fetched leaves both sides equally old and equally in agreement.
|
|
25
|
+
// That case is real and this reader cannot see it; the pass message says so rather than claiming the
|
|
26
|
+
// updater is current, and the updater's own tick reports the store's age on the line above.
|
|
27
|
+
|
|
28
|
+
import { readFileSync } from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
|
|
31
|
+
export const UPDATER_STAMP_BASENAME = "_updater-identity.json";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Where the updater stamps itself: beside the update script, in the deploy directory it runs from.
|
|
35
|
+
*
|
|
36
|
+
* NEVER A HARDCODED PATH. The obvious spelling here is the service account's own home, and it is wrong
|
|
37
|
+
* twice over: it publishes a login on a public surface, and it disagrees with any box that points the
|
|
38
|
+
* updater somewhere else — which would red a healthy deployment forever, for a reason no message names.
|
|
39
|
+
* The caller passes the directory it derived from the unit itself, and the environment overrides both.
|
|
40
|
+
*/
|
|
41
|
+
export function updaterStampPath(deployDir) { return join(deployDir, UPDATER_STAMP_BASENAME); }
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The one path the reader and the writer both resolve, from the one name the writer already honours.
|
|
45
|
+
*
|
|
46
|
+
* THE READER MUST NOT INVENT A SECOND ENV NAME. A reader-only variable is a seam: a box that redirects
|
|
47
|
+
* its updater sets the writer's name, the reader keeps looking at the default, finds nothing, and
|
|
48
|
+
* reports "no stamp" — the loud branch — about a deployment that is stamping correctly. One name, read
|
|
49
|
+
* as the writer writes it: a FULL PATH to the stamp file, not a directory.
|
|
50
|
+
*
|
|
51
|
+
* @param {string|null} deployDir where the updater runs, derived from the unit; used only as fallback
|
|
52
|
+
* @param {object} [env] process.env, injectable so a test can drive both branches
|
|
53
|
+
*/
|
|
54
|
+
export function resolveUpdaterStampPath(deployDir, env = process.env) {
|
|
55
|
+
const named = env?.CLEAROTRON_UPDATER_STAMP || null;
|
|
56
|
+
if (named) return named;
|
|
57
|
+
return deployDir ? updaterStampPath(deployDir) : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* PURE. Why this box has no updater to judge, or null when it has one.
|
|
62
|
+
*
|
|
63
|
+
* A box with no updater unit (production today, and any install that nothing keeps current) answers
|
|
64
|
+
* `systemctl show` for it with `LoadState=not-found`, an empty WorkingDirectory and no process, and it
|
|
65
|
+
* arrives as an idle row. Judging that row fails on "where it stamps could not be resolved", a red that
|
|
66
|
+
* nothing on that box can clear. So an absent unit is a skip that says why, unless the environment
|
|
67
|
+
* redirects the stamp, which says an updater exists somewhere and is to be judged. No row at all is a
|
|
68
|
+
* could-not-look, and is left to the check that says so.
|
|
69
|
+
*
|
|
70
|
+
* @param {object|undefined} row the updater's row from the unit probe
|
|
71
|
+
* @param {object} [env] process.env, injectable so a test can drive the redirect
|
|
72
|
+
*/
|
|
73
|
+
export function updaterAbsentHere(row, env = process.env) {
|
|
74
|
+
if (!row || row.load !== "not-found") return null;
|
|
75
|
+
if (resolveUpdaterStampPath(null, env)) return null;
|
|
76
|
+
return "this box has no updater unit, so nothing deploys it on a schedule and there is no running copy to compare — NOT PROBED";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The stamp, or null when there is none to read. A parse failure is a null — the verdict says so. */
|
|
80
|
+
export function readUpdaterStamp(deployDir, { read = null, env = process.env } = {}) {
|
|
81
|
+
const rd = read ?? ((p) => readFileSync(p, "utf8"));
|
|
82
|
+
const path = resolveUpdaterStampPath(deployDir, env);
|
|
83
|
+
if (!path) return null;
|
|
84
|
+
try { return JSON.parse(rd(path)); } catch { return null; }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Eight characters is enough to name a commit in a message, and null stays visibly null. */
|
|
88
|
+
const short = (c) => (c ? String(c).slice(0, 8) : "(unknown)");
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* PURE. What deploy health should say about the copy of the updater that is deploying this box.
|
|
92
|
+
*
|
|
93
|
+
* @param {object} o
|
|
94
|
+
* @param {object|null} o.stamp readUpdaterStamp()'s answer
|
|
95
|
+
* @param {number|null} o.now epoch seconds, for the stamp's age
|
|
96
|
+
* @param {string|null} o.deployClone the git toplevel this deploy is checking
|
|
97
|
+
* @param {number} [o.maxAgeSeconds] how old a stamp may be before the updater counts as not running
|
|
98
|
+
* @returns {{state:"pass"|"fail", message:string}}
|
|
99
|
+
*
|
|
100
|
+
* THE ALLOWANCE IS THREE HOURS AGAINST AN HOURLY TICK, and it is a judgement rather than a measurement,
|
|
101
|
+
* so it is written down with its reasoning. The stamp is written at the top of every run, so on the
|
|
102
|
+
* cadence the deployment keeps, one missed tick is an hour. It is not one hour here because a tick can
|
|
103
|
+
* legitimately defer — the updater exits early while a clearance is queued, claimed or in flight — and
|
|
104
|
+
* an arm that fired on a single deferral would red a box that is busy rather than broken. Three hours
|
|
105
|
+
* is two consecutive misses: past any single deferral, and still inside the window where somebody would
|
|
106
|
+
* want to know. Six hours was the first number written and it is five missed ticks, most of a working
|
|
107
|
+
* day, which is slack a one-hour cadence does not need.
|
|
108
|
+
*/
|
|
109
|
+
export function updaterVerdict({ stamp, now = null, deployClone = null, maxAgeSeconds = 3 * 3600 } = {}) {
|
|
110
|
+
if (!stamp) {
|
|
111
|
+
return { state: "fail", message:
|
|
112
|
+
`no updater identity stamp at ${UPDATER_STAMP_BASENAME}: the copy of the updater that deploys this `
|
|
113
|
+
+ "box did NOT say what it is, so whether it is the current one was not established. A copy old "
|
|
114
|
+
+ "enough to predate the stamp writes none, which is itself the stale-updater case. This is a "
|
|
115
|
+
+ "failure to look, never a pass." };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// — AN UNREADABLE SIDE IS NOT A MISMATCH. A digest that could not be taken arrives as null, and
|
|
119
|
+
// calling that "the two differ" would report drift invented out of a failure to read.
|
|
120
|
+
const wsha = stamp.wrapperSha256 ?? null;
|
|
121
|
+
const msha = stamp.masterSha256 ?? null;
|
|
122
|
+
if (!wsha || !msha) {
|
|
123
|
+
// The clause is written out per case rather than interpolated into one sentence: the single-sided
|
|
124
|
+
// wording needs the negation and the both-sided wording carries it in "neither", so the one shared
|
|
125
|
+
// sentence read "the master could be read as a digest" — the opposite of what happened, past an arm
|
|
126
|
+
// that matched the phrase and never read the verb.
|
|
127
|
+
const which = !wsha && !msha ? "neither the running copy of the updater nor its master could be read as a digest"
|
|
128
|
+
: !wsha ? "the running copy of the updater could NOT be read as a digest"
|
|
129
|
+
: "the master could NOT be read as a digest";
|
|
130
|
+
const why = stamp.masterCommitError ? ` The updater reported: ${stamp.masterCommitError}` : "";
|
|
131
|
+
return { state: "fail", message:
|
|
132
|
+
`the updater stamp is present but ${which}, so the running copy was NOT `
|
|
133
|
+
+ `compared against its master.${why} This is a failure to look, never a pass.` };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// — THE AGE IS ALWAYS PRINTED AND RARELY JUDGED. A stopped deploy timer already fails on its own arm,
|
|
137
|
+
// and a second red for one condition teaches a reader to discount both. This fires only when the stamp
|
|
138
|
+
// is old enough that the updater cannot be running on any schedule the box could be keeping.
|
|
139
|
+
const writtenAt = Number(stamp.writtenAt) || 0;
|
|
140
|
+
const ageSeconds = now && writtenAt ? Math.max(0, now - writtenAt) : null;
|
|
141
|
+
const ageNote = ageSeconds === null
|
|
142
|
+
? " (the stamp carries no readable time, so its age is unknown)"
|
|
143
|
+
: ` (stamped ${Math.floor(ageSeconds / 60)} min ago)`;
|
|
144
|
+
|
|
145
|
+
if (wsha !== msha) {
|
|
146
|
+
return { state: "fail", message:
|
|
147
|
+
`the running copy of the updater DIFFERS from its master${ageNote}: the copy that ran digests `
|
|
148
|
+
+ `${short(wsha)} and the master it was taken from digests ${short(msha)}. This box is being `
|
|
149
|
+
+ "deployed by an updater that is not the one in the store — reported, not reconciled." };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (ageSeconds !== null && ageSeconds > maxAgeSeconds) {
|
|
153
|
+
return { state: "fail", message:
|
|
154
|
+
`the updater last stamped itself ${Math.floor(ageSeconds / 3600)}h ago${
|
|
155
|
+
maxAgeSeconds ? `, past the ${Math.floor(maxAgeSeconds / 3600)}h this check allows` : ""}. `
|
|
156
|
+
+ "The stamp is written at the top of every tick, so a stamp this old means the updater has not "
|
|
157
|
+
+ "run — whatever the box is serving, it is not tracking the branch." };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// — IS THIS UPDATER DEPLOYING THE TREE BEING CHECKED? The stamp records the checkout it places
|
|
161
|
+
// commits into. A box whose health check reads one clone while its updater feeds another is the
|
|
162
|
+
// two-clone straddle with the deploying mechanism on the far side of it, and the digests above agree
|
|
163
|
+
// in that state exactly as they do in a healthy one.
|
|
164
|
+
const norm = (p) => String(p ?? "").replace(/\/+$/, "");
|
|
165
|
+
if (deployClone && stamp.checkout && norm(stamp.checkout) !== norm(deployClone)) {
|
|
166
|
+
return { state: "fail", message:
|
|
167
|
+
`the updater matches its master${ageNote}, but it deploys a DIFFERENT tree than the one being `
|
|
168
|
+
+ `checked: it places commits into ${stamp.checkout} while this check reads ${deployClone}. `
|
|
169
|
+
+ "Whatever it keeps current, it is not this one." };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const into = stamp.checkout ? `, into ${stamp.checkout}` : "";
|
|
173
|
+
return { state: "pass", message:
|
|
174
|
+
`the running copy of the updater matches its master${ageNote}${into}, both digesting ${short(wsha)}`
|
|
175
|
+
+ `, master read at commit ${short(stamp.masterCommit)}. This says the running copy matches the `
|
|
176
|
+
+ "master AS THIS BOX HOLDS IT: the store it is read from is not pulled automatically, so a store "
|
|
177
|
+
+ "nobody has fetched leaves both sides equally old and equally in agreement." };
|
|
178
|
+
}
|
package/driver/usage-ledger.mjs
CHANGED
|
@@ -22,12 +22,12 @@ import { readFileSync, readdirSync } from "node:fs";
|
|
|
22
22
|
import { join, dirname } from "node:path";
|
|
23
23
|
import { isLiveQueueMarker } from "./queue-markers.mjs"; // the wall's live-state vocabulary, not a fourth copy
|
|
24
24
|
|
|
25
|
-
// The daily allowance a brand owner gets when its profile names no runCaps.
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// The daily allowance a brand owner gets when its profile names no runCaps. THE ONE DECLARATION, here in
|
|
26
|
+
// the leaf because a request path must not pull the runner (and its queue/dispatch machinery) in;
|
|
27
|
+
// runner.mjs imports it from here. There used to be a second, held equal by a test, and the screen that
|
|
28
|
+
// advertises the allowance and the gate that enforces it now cannot disagree by construction.
|
|
28
29
|
//
|
|
29
|
-
//
|
|
30
|
-
// what the gate refuses. raised both from the beta's 2 to 20.
|
|
30
|
+
// Raised from the beta's 2 to 20.
|
|
31
31
|
export const DEFAULT_CLIENT_DAILY_RUNS = 20;
|
|
32
32
|
|
|
33
33
|
/**
|
package/mcp-server/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# trademark-artifacts-mcp
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- eacfce4: Fixed: An assistant reading a knockout's filings through the connector now gets each filing's page at the trade mark office, as the report does. Where that register publishes no page for a single record, it gets the office and the number instead. Before, it got a reference that opens nowhere.
|
|
8
|
+
|
|
3
9
|
## 0.3.0-beta.1
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -259,22 +259,25 @@ if (isMain) {
|
|
|
259
259
|
// are useful without it, and failing closed here would turn a config slip into an outage.
|
|
260
260
|
try {
|
|
261
261
|
const { loadProfiles } = await import("../driver/profiles.mjs");
|
|
262
|
-
const
|
|
262
|
+
const profiles = loadProfiles({ force: true });
|
|
263
|
+
const roster = [...profiles.keys()].sort();
|
|
263
264
|
const where = envFrom(process.env, "CLEAROTRON_CUSTOMERS_DIR") || "(unset — BUNDLED demo roster at driver/profiles)";
|
|
264
265
|
log(`roster: ${roster.length} customer(s) from ${where} — [${roster.join(", ")}]`);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
266
|
+
// — SAME FACT, DIFFERENT READER. "Every real customer will be refused" is
|
|
267
|
+
// true and useless to a demo visitor, who has none and wants none; what a first-time reader needs
|
|
268
|
+
// is what the demo IS. Composed once, in driver/demo-posture.mjs, because the other mis-aimed
|
|
269
|
+
// warning of this pair lives in the portal process and the two must not answer differently.
|
|
270
|
+
//
|
|
271
|
+
// ASKED BEFORE THE STORE IS. A demo runs on a store of its own under its base, so an unset store no
|
|
272
|
+
// longer tells a demo from an operator's install. The line is composed from the roster read above.
|
|
273
|
+
//
|
|
274
|
+
// OUTSIDE A DEMO IT IS UNCHANGED AND STILL A WARNING — an operator whose roster is the bundled one
|
|
275
|
+
// has a real deployment that will refuse every real customer.
|
|
276
|
+
const posture = demoPostureLine(process.env, { roster: profiles });
|
|
277
|
+
if (posture) log(posture);
|
|
278
|
+
else if (!envFrom(process.env, "CLEAROTRON_CUSTOMERS_DIR"))
|
|
279
|
+
log(`WARNING: CLEAROTRON_CUSTOMERS_DIR is unset, so start_run is validating against the BUNDLED demo roster.`
|
|
276
280
|
+ " Every real customer will be refused with \"names no known customer\". Set it to the config store's profiles directory.");
|
|
277
|
-
}
|
|
278
281
|
const granted = grantedAccounts(envFrom(process.env, "CLEAROTRON_ACCESS_FILE"));
|
|
279
282
|
const missing = granted.filter((a) => !roster.includes(a));
|
|
280
283
|
if (missing.length) {
|
|
@@ -20,6 +20,13 @@ export {
|
|
|
20
20
|
// exists: a second copy of a path table drifts, and the drift shows up as an artifact reported missing.
|
|
21
21
|
export { koPaths } from "../../driver/stages-knockout.mjs";
|
|
22
22
|
|
|
23
|
+
// HOW PUBLISH ADDRESSES A KNOCKOUT'S LISTED FILINGS. On a register with no record pages of its own, each
|
|
24
|
+
// filing with an office number is given the office's own page, or the office and the number and why that is
|
|
25
|
+
// not a link, and the report, the workbook and report-data.json state it. The evidence view states it from
|
|
26
|
+
// this same function rather than from a second reading of the numbers. office-record-links.mjs imports
|
|
27
|
+
// nothing, so the read-only server stays light.
|
|
28
|
+
export { addressListedFilings } from "../../driver/publish/office-record-links.mjs";
|
|
29
|
+
|
|
23
30
|
// stripInternal/stripEngineInternals/stripTelemetry are the driver's OWN client-safety transforms — the
|
|
24
31
|
// same ones publish/render.mjs applies to the client HTML export. lib/scrub.mjs composes them so the MCP
|
|
25
32
|
// client surface and the delivered report answer "what may a client see?" from ONE definition ( R1:
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
28
28
|
import { basename, join } from "node:path";
|
|
29
29
|
|
|
30
|
-
import { koPaths } from "./driver.mjs";
|
|
30
|
+
import { koPaths, addressListedFilings } from "./driver.mjs";
|
|
31
31
|
import { driverDir } from "../../shared/driver-dir.mjs";
|
|
32
32
|
|
|
33
33
|
const readJson = (p) => { try { return JSON.parse(readFileSync(p, "utf8")); } catch { return null; } };
|
|
@@ -207,14 +207,26 @@ const knockoutNegatives = (doc) => marksOf(doc).flatMap((m) =>
|
|
|
207
207
|
export function knockoutEvidence(run) {
|
|
208
208
|
const doc = knockoutDoc(run);
|
|
209
209
|
const recordsDoc = readJson(koPaths(run.runDir).registerRecords);
|
|
210
|
+
// EACH FILING ADDRESSED AS PUBLISH ADDRESSES IT, keyed on the sidecar's own register. On a register with
|
|
211
|
+
// no record pages of its own, the listing's link is the engine's handle, `/mark/<office>/<id>`, which a
|
|
212
|
+
// reader can open nowhere, and the report never shows it for a filing with an office number: it gives the
|
|
213
|
+
// office's own page, or the office and the number and why that is not a link. This view states the same,
|
|
214
|
+
// from the same function over the same sidecar, so the connector and the report cannot give one filing
|
|
215
|
+
// two links. On any other register the call sets nothing, and every row is exactly as it was.
|
|
216
|
+
if (Array.isArray(recordsDoc?.marks)) addressListedFilings(recordsDoc);
|
|
210
217
|
const out = [];
|
|
211
218
|
|
|
212
219
|
for (const entry of (Array.isArray(recordsDoc?.marks) ? recordsDoc.marks : [])) {
|
|
213
220
|
for (const r of (Array.isArray(entry.records) ? entry.records : [])) {
|
|
221
|
+
// report-data.json's own field for the same filing, in its shape.
|
|
222
|
+
const office = r.officeLink ? { label: r.officeLink.label, href: r.officeLink.href, reason: r.officeLink.reason } : null;
|
|
214
223
|
out.push({
|
|
215
224
|
layer: "register", mark: r.mark ?? entry.name ?? null, owner: r.owner ?? null,
|
|
216
225
|
country: r.territory ?? null, classes: r.classes ?? [], status: r.status ?? null,
|
|
217
|
-
|
|
226
|
+
// The source link: the office's page where the filing has one, none where it is cited by number,
|
|
227
|
+
// and the listing's own link only for a filing publish did not address.
|
|
228
|
+
url: office ? office.href : (r.url ?? null), recordId: r.recordId ?? null,
|
|
229
|
+
...(office ? { officeRecord: office } : {}),
|
|
218
230
|
matchedForm: r.matchedForm ?? null, matchedBasis: r.matchedBasis ?? null,
|
|
219
231
|
// `retrieved` is what the provider actually returned for this row, and it is a FACT about the
|
|
220
232
|
// fetch rather than about the filing. A reader deciding how much weight to give a row needs it.
|
package/mcp-server/lib/ops.mjs
CHANGED
|
@@ -19,7 +19,8 @@ import { probeQueueWatch, unwatchedQueueWarning } from "../../driver/queue-watch
|
|
|
19
19
|
import { resolveRun, runAccountKey } from "./runs.mjs";
|
|
20
20
|
import { assertAccountAccess, attributionOf } from "../../shared/scope.mjs";
|
|
21
21
|
import { requestCancel, isCancelled } from "../../driver/cancel.mjs"; // one marker shape, shared with the engine
|
|
22
|
-
import { readEngineChild, engineChildIsLive } from "../../driver/engine/child-record.mjs"; //
|
|
22
|
+
import { readEngineChild, engineChildIsLive, endEngineChild } from "../../driver/engine/child-record.mjs"; //
|
|
23
|
+
import { killEscalateMs } from "../../driver/engine/common.mjs"; // the watchdogs' grace, reused by an immediate stop
|
|
23
24
|
import { stopReason } from "../../shared/stop-reason.mjs"; //
|
|
24
25
|
import { envFrom } from "../../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
|
|
25
26
|
|
|
@@ -290,9 +291,11 @@ export function startRun(args = {}, { scope } = {}) {
|
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
// stop_run — cancel. A not-yet-claimed queue job is removed (a REAL cancel before any spend). A running run
|
|
293
|
-
// gets a .cancel sentinel + honest status
|
|
294
|
-
// queue id; runId cancels a started run.
|
|
295
|
-
|
|
294
|
+
// gets a .cancel sentinel + honest status; its turn in flight is ended from here only when `immediate` asks
|
|
295
|
+
// for that. id cancels by queue id; runId cancels a started run.
|
|
296
|
+
//
|
|
297
|
+
// `endTurn` is what ends that turn. The dispatch passes `{ scope }` alone, so only a test reaches it.
|
|
298
|
+
export async function stopRun(args = {}, { scope, endTurn = endEngineChild } = {}) {
|
|
296
299
|
const agent = args.agent ? String(args.agent) : "";
|
|
297
300
|
if (args.id) {
|
|
298
301
|
const qdir = queueDir(agent);
|
|
@@ -384,14 +387,27 @@ export function stopRun(args = {}, { scope } = {}) {
|
|
|
384
387
|
// kills a stranger. This is acceptance 5's already-exited case.
|
|
385
388
|
immediate = { attempted: false, why: "the recorded engine turn has already exited — the cancel stands", pid: child.pid };
|
|
386
389
|
} else {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
390
|
+
// SIGNALLED IS NOT ENDED, so the answer waits to see. `endTurn` sends SIGTERM to the turn — to its
|
|
391
|
+
// whole process group when the record names the group's leader, which both engines' detached
|
|
392
|
+
// spawns make it — watches until nothing of it is left, and sends SIGKILL to whatever remains
|
|
393
|
+
// when the grace runs out. `ended` is what it SAW, and the sentences below are written from that
|
|
394
|
+
// field alone: `process.kill` returning says only that the OS accepted the signal for delivery.
|
|
395
|
+
//
|
|
396
|
+
// Measured on the owner's WSL install of 0.3.0-beta.1: this branch answered "the step in flight
|
|
397
|
+
// has been ended ... terminal in seconds", read off the signal alone, and the run stopped at the
|
|
398
|
+
// next step boundary.
|
|
399
|
+
//
|
|
400
|
+
// The portal waits 30 seconds for this answer, and this waits for the turn before answering: the
|
|
401
|
+
// grace, then up to two seconds after a SIGKILL. The cap keeps a long grace set for the watchdogs
|
|
402
|
+
// from outlasting the portal's wait.
|
|
403
|
+
const took = await endTurn(child, { graceMs: Math.min(killEscalateMs(), 10000) });
|
|
404
|
+
immediate = took?.signalled
|
|
405
|
+
? { attempted: true, signalled: took.signalled, escalated: took.escalated ?? null, group: took.group === true,
|
|
406
|
+
pid: child.pid, ended: took.ended === true,
|
|
407
|
+
...(took.ended === true ? {} : { why: "the step in flight was told to stop and had not ended when it was last checked" }) }
|
|
391
408
|
// ESRCH — it died between the liveness read and the signal, which is a race we lose harmlessly.
|
|
392
409
|
// EPERM — it is not ours to signal, which is a real finding and must not read as success.
|
|
393
|
-
|
|
394
|
-
}
|
|
410
|
+
: { attempted: true, signalled: null, pid: child.pid, error: took?.error ?? "no signal was sent" };
|
|
395
411
|
}
|
|
396
412
|
}
|
|
397
413
|
|
|
@@ -443,18 +459,24 @@ export function stopRun(args = {}, { scope } = {}) {
|
|
|
443
459
|
// distinguish them is the state the owner already told us says nothing. A pressed-immediate that
|
|
444
460
|
// could not find a turn to end is reported as the boundary stop it actually became; presenting it
|
|
445
461
|
// as an immediate stop would be the second silent thing in a row on the same control.
|
|
462
|
+
//
|
|
463
|
+
// AND IT SAYS "ENDED" ONLY FROM `ended`, which is what the stop saw. A turn that was told to stop and
|
|
464
|
+
// was still there afterwards is reported as the boundary stop it became.
|
|
465
|
+
const fellBack = immediate?.why ?? immediate?.error ?? "the step in flight could not be ended";
|
|
446
466
|
if (alreadyStopping) {
|
|
447
467
|
return { ok: true, action: "already-stopping", runId: run.runId, requestedAt: rec?.ts ?? null, immediate,
|
|
448
|
-
note: immediate?.
|
|
449
|
-
? `Already stopping — the request was filed at ${rec?.ts ?? "an earlier press"}, and the step in flight has now
|
|
450
|
-
:
|
|
468
|
+
note: immediate?.ended === true
|
|
469
|
+
? `Already stopping — the request was filed at ${rec?.ts ?? "an earlier press"}, and the step in flight has now ended. Nothing will be delivered.`
|
|
470
|
+
: immediate?.attempted
|
|
471
|
+
? `Already stopping — the request was filed at ${rec?.ts ?? "an earlier press"}. An immediate stop was asked for and could not be made: ${fellBack}. The run ends at its next step boundary.`
|
|
472
|
+
: `Already stopping — the request was filed at ${rec?.ts ?? "an earlier press"} and the step in flight is being allowed to finish. Pressing again changes nothing.` };
|
|
451
473
|
}
|
|
452
|
-
if (immediate?.
|
|
474
|
+
if (immediate?.ended === true)
|
|
453
475
|
return { ok: true, action: "cancel-requested", runId: run.runId, requestedAt: rec?.ts ?? null, immediate,
|
|
454
|
-
note: "Stopping now. The step in flight has
|
|
476
|
+
note: "Stopping now. The step in flight has ended: what it had already spent is spent, everything recorded before it is kept, and nothing will be delivered." };
|
|
455
477
|
if (args.immediate === true)
|
|
456
478
|
return { ok: true, action: "cancel-requested", runId: run.runId, requestedAt: rec?.ts ?? null, immediate,
|
|
457
|
-
note: `Stopping at the next step boundary. An immediate stop was asked for and could not be made: ${
|
|
479
|
+
note: `Stopping at the next step boundary. An immediate stop was asked for and could not be made: ${fellBack}. A step already under way finishes first. Nothing will be delivered.` };
|
|
458
480
|
return { ok: true, action: "cancel-requested", runId: run.runId, requestedAt: rec?.ts ?? null,
|
|
459
481
|
note: "Stopping. The run ends at its next step boundary; a step already under way finishes first, and what it has already spent is spent. Nothing will be delivered." };
|
|
460
482
|
}
|
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.2",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
|
|
@@ -17,6 +17,6 @@
|
|
|
17
17
|
"jose": "^6.2.3"
|
|
18
18
|
},
|
|
19
19
|
"engines": {
|
|
20
|
-
"node": ">=22"
|
|
20
|
+
"node": ">=22.13.0"
|
|
21
21
|
}
|
|
22
22
|
}
|
package/mcp-server/server.mjs
CHANGED
|
@@ -594,7 +594,7 @@ const TOOL_DEFS = [
|
|
|
594
594
|
{ name: "plan_run", description: "FREE PREVIEW — resolve what a search WOULD do and return it, spending nothing and queueing nothing. Takes the SAME arguments as start_run. Use this BEFORE start_run whenever the requester has not already confirmed the specifics: it reports the PRODUCT that resolves and WHERE it came from (their request, the account default, a saved search, or the territories themselves), the territories and marketplaces that would ACTUALLY be searched, the turnaround, and any blockers as questions to put back. Show the result to the requester; then call start_run with the SAME arguments to actually run it. Nothing here reserves or holds anything.", inputSchema: { type: "object", properties: { markName: { type: "string", description: "The mark (single-mark form). REQUIRED unless marks[] is given." }, marks: { type: "array", items: { type: "object", properties: { name: { type: "string" }, classes: { type: "array", items: { type: "number" } }, ref: { type: "string" } }, required: ["name"] } }, classes: { type: "array", items: { type: "number" } }, goods: { type: "string" }, jurisdictions: { type: "array", items: { type: "string" } }, platforms: { type: "array", items: { type: "string" } }, customer: { type: "string" }, profileKey: { type: "string", description: "The customer ACCOUNT key (see list_profiles). REQUIRED for an accounts-scoped session (your grant names the keys — see describe_options)." }, projectKey: { type: "string" }, forwarder: { type: "string", description: "Requester/reply-routing key — same as start_run, so the preview describes the job that would actually be built. Account sessions may omit it — it is stamped from your verified identity." }, forwarderEmail: { type: "string" }, ref: { type: "string" }, product: { type: "string", enum: ORDERABLE_PRODUCTS, description: PRODUCT_DESC }, nativeLanguage: { type: "boolean", description: "OPTIONAL, and only on a multi-country-focus-search: the native-language investigation. Send TRUE or omit the field — FALSE IS REFUSED, not honoured: the toggle only ever added, so there is nothing for false to switch off, and accepting it would let you believe you had removed a reading that runs anyway. It runs AUTOMATICALLY on a full-country-search and is not part of the other two, so asking for it there is refused rather than accepted and ignored. It routes on territory — the scope must name one it covers." }, worldwide: { type: "boolean", description: "Search EVERYWHERE, and refuse to be narrowed by the account's own default territories. This is not the same as omitting jurisdictions, which means \"whatever the account says\" — the two used to be indistinguishable on this wire, which is how a request that bought everywhere ran an account's seven countries. Never send \"Worldwide\" as a jurisdictions entry." }, recipeKey: { type: "string" }, upfrontInstructions: { type: "string" }, deadline: { type: "string" }, deliveryRoute: { type: "string", enum: ["email", "portal"], description: "OPTIONAL: how the delivered packet leaves — email (default). \"portal\" is reserved for the portal delivery lane and CLARIFIES at admission until it ships (never a silent no-op) — so the preview BLOCKS it here rather than describing a job start_run could not run." }, commercialFlexibility: { type: "string" }, priorUse: { type: "string" }, campaignShape: { type: "string", description: "Campaign-shape FACTS from the request/client — how the mark will be deployed: standalone brand vs flavour/sub-brand under a NAMED house mark; seasonal/limited vs permanent; launch scale. Verbatim facts only — the engine records them on the matter frame instead of inferring a launch shape." }, customerUnknown: { type: "boolean" } }, required: ["forwarder"] } },
|
|
595
595
|
// ---- OPS write-face (requires an ops token; never exposed to a user/internal session) ----
|
|
596
596
|
{ name: "start_run", description: "OPS-ONLY. Enqueue a NEW clearance/search run the driver will pick up. Provide markName (or marks[] for a batch) + forwarder (and ideally classes/goods/customer/profileKey/ref). Returns the queue id + slug; the runId/codename is assigned when the runner claims it — poll list_runs (by markName) or run_changes for progress. Spends real money once it runs.", inputSchema: { type: "object", properties: { agent: { type: "string", description: "Which agent workspace's queue. Omit for the deployment default (CLEAROTRON_QUEUE_DIR or the default agent's queue — docs/INTAKE.md)." }, markName: { type: "string", description: "The mark (single-mark form). Required unless marks[] is provided." }, marks: { type: "array", items: { type: "object", properties: { name: { type: "string" }, classes: { type: "array", items: { type: "number" } }, ref: { type: "string" } }, required: ["name"] }, description: "Batch form: EVERY name of the batch in ONE job. Only a knockout-search reads more than one name at a time; a clearance reads one, and more is refused rather than truncated." }, classes: { type: "array", items: { type: "number" }, description: "Nice classes — whole numbers 1–45 (1–34 goods, 35–45 services). classes OR goods; either suffices." }, goods: { type: "string" }, jurisdictions: { type: "array", items: { type: "string" }, description: "WHERE the search points — and, for a clearance, WHICH search it is: one country is a full-country-search, a region or two-or-more countries a multi-country-focus-search. AUTHORITATIVE (the matter frame is told not to widen past them). Omit ⇒ the project/customer default territories; send worldwide:true to search everywhere instead. Names or codes both read; max 20." }, platforms: { type: "array", items: { type: "string" }, description: "OPTIONAL per-run SCOPE: extra marketplaces to sweep, as bare store domains e.g. [\"gnc.com\"]. ADDED to the account's own marketplaces — additive only, never a replacement (a client's platform list is a mandate). Max 10; \"web\" is implicit and must not be listed." }, customer: { type: "string" }, profileKey: { type: "string", description: "The customer ACCOUNT key (see list_profiles) whose profile rates this run; omit for the neutral generic profile. REQUIRED for an accounts-scoped session (your grant names the keys — see describe_options)." }, forwarder: { type: "string", description: "Requester/reply-routing key — rides the delivery packet so the integrator knows who gets the report (docs/DELIVERY.md). Account sessions may omit it — it is stamped from your verified identity." }, forwarderEmail: { type: "string" }, ref: { type: "string" }, provider: { type: "string" }, brief: { type: "string" }, upfrontInstructions: { type: "string" }, projectKey: { type: "string", description: "OPTIONAL (spec 62): the PROJECT/engagement key UNDER profileKey (see that customer's projects[] in list_profiles) whose overlay rates this run; omit to run on the customer profile. An unknown key clarifies at intake." }, product: { type: "string", enum: ORDERABLE_PRODUCTS, description: PRODUCT_DESC }, nativeLanguage: { type: "boolean", description: "OPTIONAL, and only on a multi-country-focus-search: the native-language investigation. Send TRUE or omit the field — FALSE IS REFUSED, not honoured: the toggle only ever added, so there is nothing for false to switch off, and accepting it would let you believe you had removed a reading that runs anyway. It runs AUTOMATICALLY on a full-country-search and is not part of the other two, so asking for it there is refused rather than accepted and ignored. It routes on territory — the scope must name one it covers." }, worldwide: { type: "boolean", description: "Search EVERYWHERE, and refuse to be narrowed by the account's own default territories. This is not the same as omitting jurisdictions, which means \"whatever the account says\" — the two used to be indistinguishable on this wire, which is how a request that bought everywhere ran an account's seven countries. Never send \"Worldwide\" as a jurisdictions entry." }, recipeKey: { type: "string", description: "OPTIONAL alternative selector: a saved search (recipe) slug for this customer. Mutually exclusive with product — a saved search already carries one." }, deliveryRoute: { type: "string", enum: ["email", "portal"], description: "OPTIONAL: how the delivered packet leaves — email (default). \"portal\" is reserved for the portal delivery lane and CLARIFIES at admission until it ships (never a silent no-op)." }, parentRunId: { type: "string", description: "OPTIONAL escalation lineage: the runId this run escalates from (e.g. a knockout HIGH mark → this clearotron)." }, deliverableSpec: { type: "string" }, commercialFlexibility: { type: "string" }, priorUse: { type: "string" }, campaignShape: { type: "string", description: "Campaign-shape FACTS from the request/client — how the mark will be deployed: standalone brand vs flavour/sub-brand under a NAMED house mark; seasonal/limited vs permanent; launch scale. Verbatim facts only — the engine records them on the matter frame instead of inferring a launch shape." }, deadline: { type: "string", description: "ISO-8601 — drives the deadline envelope." }, customerUnknown: { type: "boolean" }, dupOverride: { type: "boolean", description: "Requester-confirmed force-run past matter dedup." }, clientPrincipal: { type: "boolean", description: "Set by the client portal only: this run was started by a CLIENT and consumes that account's runCaps.dailyRuns allowance. Omit for staff, agent and email-door runs — absence means uncapped, and setting it can only restrict the run that carries it." }, rawRequest: { type: "string" }, forwarderDomain: { type: "string" }, msgId: { type: "string" }, conversationId: { type: "string" }, id: { type: "string" } }, required: ["forwarder"] } },
|
|
597
|
-
{ name: "stop_run", description: "OPS-ONLY. Cancel a run. Pass id to remove a not-yet-claimed queued job (a real cancel, no spend). Pass runId to file a cancel request on a started run. By default the cancel is COOPERATIVE — the sentinel is written and a turn already in flight is allowed to finish, which has no deadline: a reasoning turn can run for tens of minutes. Add immediate:true to also end that turn,
|
|
597
|
+
{ name: "stop_run", description: "OPS-ONLY. Cancel a run. Pass id to remove a not-yet-claimed queued job (a real cancel, no spend). Pass runId to file a cancel request on a started run. By default the cancel is COOPERATIVE — the sentinel is written and a turn already in flight is allowed to finish, which has no deadline: a reasoning turn can run for tens of minutes. Add immediate:true to also end that turn: it is sent a stop, anything of it still running a few seconds later is killed, and the answer says the turn ended only once it has seen it end. If it could not be ended, the run ends at its next step boundary, and the answer says that instead.", inputSchema: { type: "object", properties: { agent: { type: "string" }, id: { type: "string", description: "A queued job id (from start_run) — dequeues before the runner claims it." }, runId: { type: "string", description: "A started run's runId — files a cancel sentinel." }, immediate: { type: "boolean", description: "Stop NOW instead of at the next step boundary. The cancel sentinel is written FIRST and then the run's own recorded engine turn is stopped, in that order — the sentinel is what makes the record clean, so the terminal names the stage, the actor and the request time instead of leaving a run that reads as still running. The turn is sent SIGTERM (to its whole process group when it leads one, as both engines spawn it, so the tools it started are reached too), watched, and sent SIGKILL if anything of it is still running when the grace runs out; the answer waits for that, which takes seconds. A turn that is stopped loses its step's work; everything already recorded is kept. Sending the stop is not the same as the turn ending, and the answer claims the second only when it has seen it (immediate.ended). Falls back to the boundary stop, and SAYS SO in the answer, when there is no live turn to signal — a run between steps, or one whose turn has already exited — or when the turn was still there after the SIGKILL. The answer states which of the two actually happened."}, onBehalfOf: { type: "string", description: "The identifier of a human this caller has itself verified (#1378). RECORDED BESIDE the token's own principal, never instead of it — a stop by the portal for alice@x is written `portal:alice@x`, so an asserted name is never mistaken for a proved one. Bare identifiers only; anything else is dropped." } } } },
|
|
598
598
|
{ name: "server_info", description: "The AGPL §13 source offer: this server's name, version, licence, source repository and the COMMIT IT IS RUNNING. Callable by every session kind — the offer is owed to whoever interacts with the service. Reads no run and no account data.", inputSchema: { type: "object", properties: {} } },
|
|
599
599
|
{ name: "list_outbox_events", description: "Integrator discovery read (docs/DELIVERY.md): every pending outbox event — delivered markers plus the self-contained run-failed / intake-rejected / duplicate-skipped / late-bind-ack packets. Route each, then mark_sent (delivered) or ack_event (the rest).", inputSchema: { type: "object", properties: {} } },
|
|
600
600
|
{ name: "get_delivery_packet", description: "The run's send payload: _driver/delivery.json (subject, recipient routing, verbatim emailBodyHtml, whatsapp line) and/or failure.json, plus sendPending/.sent state. NOT exposed to client tokens.", inputSchema: { type: "object", properties: { runId: { type: "string" } }, required: ["runId"] } },
|
package/package.json
CHANGED
|
@@ -10943,6 +10943,7 @@ var NAV = [
|
|
|
10943
10943
|
path: "/portal/people",
|
|
10944
10944
|
icon: "users",
|
|
10945
10945
|
needs: "manage",
|
|
10946
|
+
hidden: true,
|
|
10946
10947
|
scope: "account"
|
|
10947
10948
|
},
|
|
10948
10949
|
{
|
|
@@ -11065,6 +11066,7 @@ function avatarMenuFor(who, entries = NAV) {
|
|
|
11065
11066
|
const pick = (id) => all.find((e) => e.id === id);
|
|
11066
11067
|
return [
|
|
11067
11068
|
pick("preferences"),
|
|
11069
|
+
pick("people"),
|
|
11068
11070
|
pick("admin.config"),
|
|
11069
11071
|
pick("about")
|
|
11070
11072
|
].filter((e) => !!e);
|
|
@@ -11405,6 +11407,9 @@ function companyFactsMap(granted, roster) {
|
|
|
11405
11407
|
//#endregion
|
|
11406
11408
|
//#region src/shell/companyRows.ts
|
|
11407
11409
|
var GENERIC_KEY = GENERIC_ACCOUNT;
|
|
11410
|
+
function switcherKeys(me, roster) {
|
|
11411
|
+
return [...(me.allAccounts ? (roster ?? []).map((c) => c.key) : me.accounts).filter((k) => !isGenericKey(k)), ...me.genericOrgs.map(genericFor)];
|
|
11412
|
+
}
|
|
11408
11413
|
var GENERIC_LINE = "Run any clearance against the default settings, with no company set up.";
|
|
11409
11414
|
function pickerGroups(keys, orgOf, organisations, companyName, factsFor) {
|
|
11410
11415
|
const rowFor = (key) => isGenericKey(key) ? {
|
|
@@ -11445,6 +11450,9 @@ function pickerGroups(keys, orgOf, organisations, companyName, factsFor) {
|
|
|
11445
11450
|
function pickerRows(keys, orgOf, organisations, companyName, factsFor) {
|
|
11446
11451
|
return pickerGroups(keys, orgOf, organisations, companyName, factsFor).groups.flatMap((g) => g.rows);
|
|
11447
11452
|
}
|
|
11453
|
+
function switcherLabel(row) {
|
|
11454
|
+
return row.generic ? `${row.name} (Default)` : row.name;
|
|
11455
|
+
}
|
|
11448
11456
|
//#endregion
|
|
11449
11457
|
//#region src/shell/accessWords.ts
|
|
11450
11458
|
function permissionsPhrase(p) {
|
|
@@ -11646,7 +11654,7 @@ function AppShell({ render }) {
|
|
|
11646
11654
|
};
|
|
11647
11655
|
const facts = companyFactsMap(me.accountFacts, rosterResult?.kind === "ok" ? rosterResult.value : []);
|
|
11648
11656
|
const factsFor = (key) => facts[isGenericKey(key) ? GENERIC_ACCOUNT : key];
|
|
11649
|
-
const ownerKeys =
|
|
11657
|
+
const ownerKeys = switcherKeys(me, rosterResult?.kind === "ok" ? rosterResult.value : null);
|
|
11650
11658
|
const rosterOrgs = Object.fromEntries((rosterResult?.kind === "ok" ? rosterResult.value : []).flatMap((c) => c.org ? [[c.key, c.org]] : []));
|
|
11651
11659
|
const orgOf = (key) => orgOfGeneric(key) ?? me.accountOrgs[key] ?? rosterOrgs[key] ?? null;
|
|
11652
11660
|
const organisations = me.organisations;
|
|
@@ -11903,7 +11911,7 @@ function AppShell({ render }) {
|
|
|
11903
11911
|
function BrandOwnerSwitcher({ grouped, value, onChange }) {
|
|
11904
11912
|
const option = (r) => (0, import_jsx_runtime.jsx)("option", {
|
|
11905
11913
|
value: r.key,
|
|
11906
|
-
children: r
|
|
11914
|
+
children: switcherLabel(r)
|
|
11907
11915
|
}, r.key);
|
|
11908
11916
|
return (0, import_jsx_runtime.jsxs)("select", {
|
|
11909
11917
|
value: value ?? "",
|
|
@@ -12523,14 +12531,6 @@ function Card({ run, ctx, onChanged }) {
|
|
|
12523
12531
|
onBoundary: () => void stop(false),
|
|
12524
12532
|
onCancel: () => setAsking(false)
|
|
12525
12533
|
}) : null,
|
|
12526
|
-
stopping ? (0, import_jsx_runtime.jsx)("div", {
|
|
12527
|
-
className: "home2-stopping",
|
|
12528
|
-
children: took?.note ? took.note : (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
12529
|
-
"Stopping — ",
|
|
12530
|
-
run.step ? `letting “${run.step}” finish` : "letting the step in flight finish",
|
|
12531
|
-
". A reasoning step can take tens of minutes and has no deadline. Nothing further will start, and nothing will be delivered."
|
|
12532
|
-
] })
|
|
12533
|
-
}) : null,
|
|
12534
12534
|
canAck ? (0, import_jsx_runtime.jsx)("button", {
|
|
12535
12535
|
type: "button",
|
|
12536
12536
|
className: "home2-ack",
|
|
@@ -12539,7 +12539,15 @@ function Card({ run, ctx, onChanged }) {
|
|
|
12539
12539
|
children: "Acknowledge"
|
|
12540
12540
|
}) : null
|
|
12541
12541
|
]
|
|
12542
|
-
})
|
|
12542
|
+
}),
|
|
12543
|
+
stopping ? (0, import_jsx_runtime.jsx)("div", {
|
|
12544
|
+
className: "home2-stop-note",
|
|
12545
|
+
children: took?.note ? took.note : (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
12546
|
+
"Stopping — ",
|
|
12547
|
+
run.step ? `letting “${run.step}” finish` : "letting the step in flight finish",
|
|
12548
|
+
". A reasoning step can take tens of minutes and has no deadline. Nothing further will start, and nothing will be delivered."
|
|
12549
|
+
] })
|
|
12550
|
+
}) : null
|
|
12543
12551
|
]
|
|
12544
12552
|
})]
|
|
12545
12553
|
});
|
|
@@ -12633,13 +12641,13 @@ function StopChoice({ name, step, stoppable, onImmediate, onBoundary, onCancel }
|
|
|
12633
12641
|
className: "stop-choice-opt stop-choice-now",
|
|
12634
12642
|
onClick: onImmediate,
|
|
12635
12643
|
children: [(0, import_jsx_runtime.jsx)("b", { children: "Stop now" }), (0, import_jsx_runtime.jsxs)("span", { children: [
|
|
12636
|
-
"
|
|
12644
|
+
"Sends a stop to ",
|
|
12637
12645
|
step ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
12638
12646
|
"“",
|
|
12639
12647
|
step,
|
|
12640
12648
|
"”"
|
|
12641
12649
|
] }) : (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: "the step in flight" }),
|
|
12642
|
-
" rather than waiting for it.
|
|
12650
|
+
" rather than waiting for it. If it takes the stop the run is over in seconds; if it will not, the run stops at the next step instead. That step’s work is lost; everything recorded before it is kept."
|
|
12643
12651
|
] })]
|
|
12644
12652
|
})]
|
|
12645
12653
|
}),
|
|
@@ -13084,7 +13092,7 @@ function Clearances({ ctx }) {
|
|
|
13084
13092
|
})
|
|
13085
13093
|
});
|
|
13086
13094
|
if (!result) return (0, import_jsx_runtime.jsx)(Loading, {});
|
|
13087
|
-
if (!
|
|
13095
|
+
if (!allRuns.length) return (0, import_jsx_runtime.jsx)(FirstRun, { onNew: canRun(ctx.me) ? () => ctx.go("/portal/new") : null });
|
|
13088
13096
|
const allMarks = rows.flatMap((r) => r.kind === "family" ? r.marks : [r]);
|
|
13089
13097
|
const pickedRunIds = () => allMarks.filter((m) => picked.has(m.id)).flatMap((m) => m.reads.map((r) => r.runId));
|
|
13090
13098
|
const ungroupFamily = async (family) => {
|
|
@@ -13372,7 +13380,14 @@ function Clearances({ ctx }) {
|
|
|
13372
13380
|
(0, import_jsx_runtime.jsx)("th", { children: sortBtn("risk", "Risk") }),
|
|
13373
13381
|
(0, import_jsx_runtime.jsx)("th", { children: sortBtn("date", "Updated") })
|
|
13374
13382
|
] }) }),
|
|
13375
|
-
(0, import_jsx_runtime.
|
|
13383
|
+
(0, import_jsx_runtime.jsxs)("tbody", { children: [!visible.length ? (0, import_jsx_runtime.jsx)("tr", { children: (0, import_jsx_runtime.jsx)("td", {
|
|
13384
|
+
colSpan: (canGroup ? 6 : 5) + (showOwnerColumn ? 1 : 0),
|
|
13385
|
+
style: {
|
|
13386
|
+
padding: "22px 12px",
|
|
13387
|
+
color: "var(--text-muted)"
|
|
13388
|
+
},
|
|
13389
|
+
children: "No clearances match this view. Pick another company above, or widen the status filter."
|
|
13390
|
+
}) }) : null, visible.map((r, i) => {
|
|
13376
13391
|
const before = visible[i - 1];
|
|
13377
13392
|
const newGroup = grouped && (!before || runKey(r) !== runKey(before));
|
|
13378
13393
|
return (0, import_jsx_runtime.jsxs)(import_react.Fragment, { children: [newGroup ? (0, import_jsx_runtime.jsx)("tr", {
|
|
@@ -13414,7 +13429,7 @@ function Clearances({ ctx }) {
|
|
|
13414
13429
|
onRetire: canGroup ? retireMark : void 0,
|
|
13415
13430
|
onRetireRun: canGroup ? retireRun : void 0
|
|
13416
13431
|
})] }, r.id);
|
|
13417
|
-
}) })
|
|
13432
|
+
})] })
|
|
13418
13433
|
]
|
|
13419
13434
|
})
|
|
13420
13435
|
}),
|
|
@@ -19039,7 +19054,7 @@ function NewCompany({ ctx }) {
|
|
|
19039
19054
|
if (!raw.trim()) return [];
|
|
19040
19055
|
return parseLines(raw, spec.commaSeparated ?? false).filter((e) => !spec.item.ok(e));
|
|
19041
19056
|
});
|
|
19042
|
-
const unmet = !typedName ? "Needs a name." : !key ? "Needs a key — type one below." : !orgChosen ? "Choose the organisation it belongs to." : refusedEntries.length ? `${refusedEntries.join(", ")} cannot be searched — fix or remove ${refusedEntries.length === 1 ? "it" : "them"}.` : null;
|
|
19057
|
+
const unmet = !typedName ? "Needs a name." : !key ? "Needs a key — type one below." : !orgChosen ? orgsHeld.length === 0 ? "No organisation is filed on this install yet. File one with: clearotron start --organisation \"<name>\"" : "Choose the organisation it belongs to." : refusedEntries.length ? `${refusedEntries.join(", ")} cannot be searched — fix or remove ${refusedEntries.length === 1 ? "it" : "them"}.` : null;
|
|
19043
19058
|
async function create() {
|
|
19044
19059
|
if (unmet || busy) return;
|
|
19045
19060
|
setBusy(true);
|