clearotron 0.3.2-beta.7 → 0.3.2-beta.9
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 +58 -26
- package/CONTRIBUTING.md +8 -8
- package/INSTALL.md +148 -81
- package/README.md +3 -3
- package/SECURITY.md +3 -3
- package/bin/brandowner.mjs +3 -3
- package/bin/framework-preflight.mjs +1 -1
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +151 -27
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +2 -1
- package/docs/INTAKE.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/architecture/03-run-lifecycle.md +6 -6
- package/docs/architecture/04-configuration-reference.md +32 -14
- package/docs/architecture/05-config-governance.md +23 -8
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/08-development-guide.md +6 -6
- package/docs/configuration.md +5 -5
- package/docs/decisions/0003-credential-model.md +1 -1
- package/docs/writing-standard.md +4 -0
- package/driver/CHANGELOG.md +124 -0
- package/driver/README.md +3 -3
- package/driver/band-size.mjs +59 -0
- package/driver/binding-layers.mjs +1 -1
- package/driver/citation-census.json +3 -3
- package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/company-bundle.mjs +3 -3
- package/driver/compose-read.mjs +8 -14
- package/driver/config-inventory.mjs +112 -9
- package/driver/consumption-ledger.mjs +2 -2
- package/driver/contract-arm2-baseline.json +2 -5
- package/driver/contract-dictation-registry.mjs +19 -19
- package/driver/contract-e3-backlog.mjs +43 -43
- package/driver/contract-e3-baseline.json +14 -14
- package/driver/contract-vocabulary.mjs +68 -27
- package/driver/deliver-trigger.sh +16 -16
- package/driver/demo-container.mjs +3 -3
- package/driver/dev-portal.mjs +3 -3
- package/driver/disposition-call.mjs +1 -1
- package/driver/door-gates.mjs +41 -7
- package/driver/doubt-ledger.mjs +2 -2
- package/driver/drainer-identity.mjs +34 -8
- package/driver/driver.config.mjs +367 -104
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/README.md +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +3 -3
- package/driver/engine/mcp/gather-config.mjs +9 -9
- package/driver/engine/mcp/perplexity-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +18 -5
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/enqueue-schema.mjs +6 -2
- package/driver/findings-model.mjs +6 -3
- package/driver/flag-snapshot.mjs +34 -8
- package/driver/form-neighbourhood.mjs +54 -7
- package/driver/framework.mjs +4 -4
- package/driver/gateway.mjs +36 -24
- package/driver/jx-lanes.mjs +23 -4
- package/driver/jx-units.mjs +7 -4
- package/driver/jx.mjs +34 -4
- package/driver/knockout-review-record.mjs +56 -4
- package/driver/known-conflicts.mjs +1 -1
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +1 -1
- package/driver/ordinary-words.mjs +51 -0
- package/driver/outbox-backoff.mjs +31 -16
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +2 -2
- package/driver/phase0.mjs +3 -3
- package/driver/pipeline-knockout.mjs +5 -5
- package/driver/pipeline.mjs +396 -81
- package/driver/placement-form.mjs +77 -1
- package/driver/placement-model.mjs +1 -1
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +107 -6
- package/driver/portal-service.mjs +80 -14
- package/driver/portal-upstream.mjs +1 -1
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/preserve-merge.mjs +3 -3
- package/driver/product-rows.mjs +2 -2
- package/driver/products.mjs +1 -1
- package/driver/profiles/README.md +3 -3
- package/driver/profiles/demo-brand-owner.json +2 -2
- package/driver/profiles.mjs +55 -17
- package/driver/progress.mjs +18 -8
- package/driver/provider-usage.mjs +8 -8
- package/driver/publish/index.mjs +154 -8
- package/driver/publish/knockout.mjs +39 -5
- package/driver/publish/pool-admin.mjs +1 -1
- package/driver/publish/publish-inputs.mjs +18 -2
- package/driver/publish/render-knockout.mjs +184 -31
- package/driver/publish/render.mjs +323 -93
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/search-depth.mjs +133 -4
- package/driver/publish/templates/report.css +78 -4
- package/driver/publish/xlsx.mjs +20 -1
- package/driver/queue-order.mjs +2 -2
- package/driver/recording-agreement.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-count.mjs +50 -5
- package/driver/register-coverage.mjs +161 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-grant-vocabulary.mjs +1 -1
- package/driver/register-plan.mjs +189 -2
- package/driver/registry-fidelity.mjs +3 -3
- package/driver/repair-composers.mjs +1 -1
- package/driver/repair-contract.mjs +1 -1
- package/driver/replay-archive.mjs +6 -6
- package/driver/report-overview-record.mjs +2 -2
- package/driver/result-noun-fields.mjs +2 -2
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +5 -5
- package/driver/scope-facts.mjs +20 -5
- package/driver/scope-ledger.mjs +5 -5
- package/driver/search-policy.mjs +22 -12
- package/driver/skills/README.md +15 -15
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/case-law-citation/SKILL.md +4 -4
- package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
- package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
- package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
- package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
- package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
- package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
- package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
- package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
- package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
- package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
- package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
- package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
- package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
- package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
- package/driver/skills/frame-diff/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +10 -7
- package/driver/skills/matter-frame/SKILL.md +3 -3
- package/driver/skills/narrative-refutation/SKILL.md +9 -9
- package/driver/skills/placement-inquiry/SKILL.md +5 -5
- package/driver/stage-context.mjs +1 -1
- package/driver/stages-knockout.mjs +4 -4
- package/driver/stages.mjs +65 -61
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +340 -136
- package/driver/surface-exit-verdict.mjs +58 -0
- package/driver/systemd/README.md +9 -6
- package/driver/systemd/clearotron-worker.service +1 -1
- package/driver/terminal-clamp.mjs +109 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/usage-ledger.mjs +1 -1
- package/driver/variant-manifest-model.mjs +4 -4
- package/driver/verify-knockout.mjs +27 -0
- package/driver/verify.mjs +94 -6
- package/driver/whatif-queue.mjs +1 -1
- package/driver/wordlists/en.txt +63906 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/README.md +1 -1
- package/mcp-server/lib/options.mjs +8 -7
- package/mcp-server/lib/plan.mjs +18 -2
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/lib/usage.mjs +3 -3
- package/mcp-server/lib/whatif.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +18 -1
- package/package.json +12 -11
- package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/README.md +1 -1
- package/providers/_shared/enumerate.mjs +6 -6
- package/providers/_shared/execute-plan.mjs +3 -3
- package/providers/_shared/ledger.mjs +119 -5
- package/providers/_shared/provider-text.mjs +2 -2
- package/providers/_shared/screen.mjs +2 -2
- package/providers/_shared/script-form.mjs +3 -3
- package/providers/_shared/territory-codes.mjs +23 -3
- package/providers/clarivate/README.md +1 -1
- package/providers/clarivate/src/capabilities.js +12 -12
- package/providers/clarivate/src/core.js +37 -43
- package/providers/corsearch/README.md +1 -1
- package/providers/corsearch/src/capabilities.js +5 -5
- package/providers/corsearch/src/core.js +3 -3
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/src/core.js +1 -1
- package/providers/signa/README.md +1 -1
- package/providers/signa/src/capabilities.js +42 -49
- package/providers/signa/src/core.js +106 -29
- package/providers/uspto-local/README.md +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +1 -0
- package/scripts/ask-ai-render-check.mjs +127 -1
- package/scripts/authority-boundary-probe.mjs +8 -6
- package/scripts/backfill-started-at.mjs +2 -2
- package/scripts/census-merge-driver.mjs +33 -2
- package/scripts/citation-anchor-report.mjs +181 -0
- package/scripts/dead-names.mjs +1 -1
- package/scripts/deprecate-below.mjs +66 -8
- package/scripts/drain-preflight.mjs +1 -1
- package/scripts/e2e.mjs +174 -0
- package/scripts/env-audit.mjs +39 -6
- package/scripts/env-classify.mjs +20 -2
- package/scripts/freeze-example-run.mjs +61 -18
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/live-surface-check.mjs +124 -41
- package/scripts/markdown-link-check.mjs +1 -1
- package/scripts/merge-shape-check.mjs +242 -0
- package/scripts/mint-names-in-force.mjs +5 -5
- package/scripts/mint-offered-territories.mjs +72 -0
- package/scripts/mint-public-residue.mjs +2 -2
- package/scripts/mint-reference-strip-backlog.mjs +2 -2
- package/scripts/mint-suite-census.mjs +75 -2
- package/scripts/mint-writing-standard-backlog.mjs +2 -2
- package/scripts/purge-runs.mjs +7 -7
- package/scripts/reconcile-runs.mjs +2 -2
- package/scripts/release-approve-parked.mjs +20 -2
- package/scripts/release-await-cut.mjs +120 -1
- package/scripts/release-note-required.mjs +76 -8
- package/scripts/report-header-render-check.mjs +164 -0
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/brand.mjs +27 -0
- package/shared/connect-clients.mjs +39 -11
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/identifier-scan.mjs +65 -9
- package/shared/identifier-sentinels.mjs +22 -0
- package/shared/names-in-force.mjs +4 -2
- package/shared/offered-territories.json +738 -0
- package/shared/pre-rename-spellings.mjs +53 -0
- package/shared/reference-guard-classes.mjs +40 -2
- package/shared/stdio-connect.mjs +39 -4
- package/shared/tree-commit.mjs +48 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sarl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// WHICH ANSWER THE DEPLOYMENT CHECK GIVES, as a number a caller can act on.
|
|
5
|
+
//
|
|
6
|
+
// Extracted from scripts/live-surface-check.mjs for the reason roster-verdict.mjs and
|
|
7
|
+
// unit-state-verdict.mjs were: that file is a program, and importing it to reach one decision runs its
|
|
8
|
+
// preflight and exits. A decision nobody can drive without a deployment to point at is a decision nobody
|
|
9
|
+
// drives.
|
|
10
|
+
//
|
|
11
|
+
// ── THE DEFECT THIS SETTLES ──────────────────────────────────────────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// The check reported "I could not look" and "this has drifted" through the same FAIL. Two arms said in
|
|
14
|
+
// their own message "This is a failure to look, never a pass" and then returned the verdict a genuine
|
|
15
|
+
// drift returns, so a reader could not tell them apart without reading to the end of the message.
|
|
16
|
+
//
|
|
17
|
+
// They want different things done. A drift is fixed by redeploying. A could-not-look is fixed by pointing
|
|
18
|
+
// the check at something it can read, and until somebody does, nothing is known about that surface in
|
|
19
|
+
// either direction — redeploying would change nothing, because nothing was compared.
|
|
20
|
+
//
|
|
21
|
+
// The cost of leaving it is the familiar one: a FAIL that turns out to be "could not look" teaches
|
|
22
|
+
// whoever runs the check to read FAIL as noise, and the run where it means drift is the one nobody acts
|
|
23
|
+
// on. A deployment check is exactly where that is expensive.
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The exit code for a finished run.
|
|
27
|
+
*
|
|
28
|
+
* 0 every surface was read and none disagreed
|
|
29
|
+
* 1 a surface DRIFTED — redeploy; the report names which
|
|
30
|
+
* 3 nothing drifted, and a surface COULD NOT BE READ
|
|
31
|
+
*
|
|
32
|
+
* 2 belongs to the preflight refusals, which fire before any surface is examined: the instance did not
|
|
33
|
+
* say where its pool or its doors are, so the run has nothing to report rather than something to report
|
|
34
|
+
* badly.
|
|
35
|
+
*
|
|
36
|
+
* A DRIFT OUTRANKS A COULD-NOT-LOOK when both are present. The drift is actionable now and the unreadable
|
|
37
|
+
* surface is a second errand; both are on the report, and only the code is ordered.
|
|
38
|
+
*
|
|
39
|
+
* ORDINARY SKIPS MUST NOT REACH HERE, which is why the caller passes a separate count rather than every
|
|
40
|
+
* skip. Several surfaces are deliberately not probed — a client door behind an access proxy, a door this
|
|
41
|
+
* instance does not name — and those are the resting state of a healthy box. A code that moved on them
|
|
42
|
+
* would fire on every good run and be ignored inside a week, which is this same defect one level up.
|
|
43
|
+
*
|
|
44
|
+
* @param {{failed?: number, couldNotLook?: number}} counts
|
|
45
|
+
*/
|
|
46
|
+
export function exitFor({ failed = 0, couldNotLook = 0 } = {}) {
|
|
47
|
+
if (failed) return 1;
|
|
48
|
+
if (couldNotLook) return 3;
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What each code means, for a caller printing it or a reader wondering. Keyed by the code itself. */
|
|
53
|
+
export const EXIT_MEANING = Object.freeze({
|
|
54
|
+
0: "every surface was read and none disagreed",
|
|
55
|
+
1: "a surface drifted — redeploy; the report names which",
|
|
56
|
+
2: "the instance did not say where its pool or its doors are, so nothing was examined",
|
|
57
|
+
3: "nothing drifted, and a surface could not be read — nothing is known about it either way",
|
|
58
|
+
});
|
package/driver/systemd/README.md
CHANGED
|
@@ -137,7 +137,7 @@ on a **literal glob** -- a `.path` unit cannot read an environment variable, so
|
|
|
137
137
|
your configuration:
|
|
138
138
|
|
|
139
139
|
```
|
|
140
|
-
PathExistsGlob=%h/.openclaw/workspace-clawdi/studio/
|
|
140
|
+
PathExistsGlob=%h/.openclaw/workspace-clawdi/studio/clearance-search/queue/*.json
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
**That prefix is one deployment's layout, not yours.** Enable this unit without editing that line to
|
|
@@ -153,10 +153,13 @@ checkout by a debugging session cannot quietly reconfigure the queue drain.
|
|
|
153
153
|
|
|
154
154
|
**If the service fails to start**, `systemctl --user status prelim-driver.service` names the reason.
|
|
155
155
|
The two that have actually happened: `CLEAROTRON_CHECKOUT_DIR` unset — so `ExecStart` resolves to
|
|
156
|
-
`/driver/runner.mjs`, which does not exist — and `claude`
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
156
|
+
`/driver/runner.mjs`, which does not exist — and no `claude` the unit can find, which is not a start
|
|
157
|
+
failure at all but shows later as every stage burning its retry ladder for zero tokens. `clearotron
|
|
158
|
+
install` offers to install the chosen engine's program into `~/.local/share/clearotron/engines`, and the
|
|
159
|
+
unit uses that copy when its `PATH` has none, so the second happens only when that offer was declined.
|
|
160
|
+
Accept it by running `clearotron install` again, or set `CLEAROTRON_CLAUDE_PATH`
|
|
161
|
+
in `~/.env` to the binary's absolute path; the unit's `PATH` already covers `~/.local/bin` and
|
|
162
|
+
`~/.npm-global/bin`, the two layouts the documents permit.
|
|
160
163
|
|
|
161
164
|
### `profile-service.service` needs its checkout path edited by hand
|
|
162
165
|
|
|
@@ -191,7 +194,7 @@ degrades and the degradation should be findable.
|
|
|
191
194
|
|
|
192
195
|
| Part | Needs | What happens without it |
|
|
193
196
|
|---|---|---|
|
|
194
|
-
| **Outbox delivery trigger** — `driver/deliver-trigger.sh`, driven by the `
|
|
197
|
+
| **Outbox delivery trigger** — `driver/deliver-trigger.sh`, driven by the `clearance-outbox` systemd `.path`/`.service`/`.timer` units | systemd; bash >= 4 for `declare -A`; `timeout(1)` (GNU coreutils, or `gtimeout` from Homebrew) as the enforced wall on every courier wake | The script refuses at startup and names the missing piece. It will not wake a courier it cannot put a wall around — an unkillable wake wedged a lane for 19h once. No event is touched: everything stays pending and delivers as soon as the host is fixed. |
|
|
195
198
|
| **PID-reuse claim defence** — the queue runner telling a live claimer from a recycled pid | A birth stamp for a process:`/proc/<pid>/stat` on Linux, `ps -o lstart` on macOS and anywhere else POSIX. Absent only where neither answers — WSL1, some sandboxes | Degrades **fail-safe**, and the runner says so once at startup. Claims record a bare pid, so a claim whose liveness cannot be proved counts as alive: no run is ever double-claimed and no lawyer double-delivered to. What is lost is the escape hatch — a `.processing` marker held by a recycled pid waits for the max-claim-age ceiling instead of being freed on the next tick. |
|
|
196
199
|
|
|
197
200
|
CI runs the suite on macOS as well as Linux, and an assertion covering a capability the box lacks skips
|
|
@@ -49,7 +49,7 @@ Environment=PATH=%h/.local/bin:%h/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
|
|
|
49
49
|
Environment=CLEAROTRON_NO_ENV_FILE=1
|
|
50
50
|
ExecStart=/usr/bin/node ${CLEAROTRON_CHECKOUT_DIR}/driver/runner.mjs --watch
|
|
51
51
|
# ── REFRESH THE CAPABILITY PAGE'S CAPTURE AT EVERY START ─────────────────────────────────────────────
|
|
52
|
-
# The capability page renders `pool/_state/
|
|
52
|
+
# The capability page renders `pool/_state/clearance-flag-snapshot.json`, and until this line its only
|
|
53
53
|
# writers were a RUN and the `clearotron start` launcher. A hosted box runs neither: these units are the
|
|
54
54
|
# entrypoint, and a deployment being CONFIGURED runs nothing by definition. So the page reported the old
|
|
55
55
|
# configuration as current fact for exactly as long as somebody was working on the configuration, which
|
|
@@ -87,6 +87,37 @@ export function recordNamesDefect(record) {
|
|
|
87
87
|
return typeof record?.defect === "string" && record.defect.trim().length > 0 && record?.delivered === true;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The conditions `clientConditions` could NOT render, so the surface does not carry them. PURE.
|
|
93
|
+
*
|
|
94
|
+
* A REFUSAL NOBODY RECORDS IS THE DEFECT ONE LAYER ALONG. The fallback drops a token-bearing reason it
|
|
95
|
+
* cannot compose a sentence for, which is the right answer for the client and a silent loss for
|
|
96
|
+
* everyone else: the condition was in the run record, it applies, and the delivered page no longer says
|
|
97
|
+
* so. Before this, the operator's signal was the voice lint flagging the token on the page. Take the
|
|
98
|
+
* token off the page and that flag goes quiet — the gap would be closed and the disclosure would go
|
|
99
|
+
* with it, with nothing in between to say which. So the drop reports itself here, the lint reads it,
|
|
100
|
+
* and the run record keeps the reason either way.
|
|
101
|
+
*
|
|
102
|
+
* @param {{reasons?: string[], clauses?: string[]}} sidecar the parsed `_driver/verdict.json`
|
|
103
|
+
* @returns {string[]} the run-record reasons that reach no client surface
|
|
104
|
+
*/
|
|
105
|
+
// IT DECIDES THE SAME THREE THINGS `clientConditions` DECIDES, and they have to keep agreeing: what
|
|
106
|
+
// counts as a stored clause, what counts as an empty reason, and which reasons the authority can
|
|
107
|
+
// render. The third cannot drift — both call `clauseFromReason` — and the first two are the two lines
|
|
108
|
+
// below. A change to either belongs in both, and arm 9 drives them together against one sidecar.
|
|
109
|
+
export function unrenderableConditions({ reasons, clauses } = {}) {
|
|
110
|
+
const rs = Array.isArray(reasons) ? reasons : [];
|
|
111
|
+
const cs = Array.isArray(clauses) ? clauses : [];
|
|
112
|
+
return rs.filter((r, i) => {
|
|
113
|
+
if (cs[i] === null) return false; // run record only BY RULING, not a clause that failed to compose
|
|
114
|
+
if (typeof cs[i] === "string" && cs[i].trim()) return false;
|
|
115
|
+
const text = String(r ?? "").trim();
|
|
116
|
+
if (!text) return false;
|
|
117
|
+
return !clauseFromReason(text) && ENGINE_TOKEN_RE.test(text);
|
|
118
|
+
}).map((r) => String(r).trim());
|
|
119
|
+
}
|
|
120
|
+
|
|
90
121
|
/**
|
|
91
122
|
* THE LEDE IS THE OPINION'S, BY DECISION AND NOT BY PUSH ORDER. The terminal guards run
|
|
92
123
|
* earlier in the delivery block than the coverage floor, so their clauses landed at index 0 and the
|
|
@@ -107,6 +138,70 @@ export function orderClausesForLede(clauses, reasons, guardSet) {
|
|
|
107
138
|
return { clauses: ordered.map((x) => x.c), reasons: ordered.map((x) => x.r) };
|
|
108
139
|
}
|
|
109
140
|
|
|
141
|
+
/**
|
|
142
|
+
* THE CLAUSE AUTHORITY — one composer for the reader's sentence, called both where the numbers are
|
|
143
|
+
* known and where only the run-record reason survives. PURE.
|
|
144
|
+
*
|
|
145
|
+
* A run recorded before clauses were persisted carries `reasons` and no `clauses`, so every condition
|
|
146
|
+
* fell back to the run-record sentence and a republished archived report opened its Conditions list
|
|
147
|
+
* with `floor_duty_undischarged:4 of 430 floor row(s)…` — on the page and in the exported PDF. Dropping
|
|
148
|
+
* the condition instead loses a point the reader must weigh, and re-generating every archived run is
|
|
149
|
+
* not on offer. The third way is this: for both defects that can reach that list the reader's sentence
|
|
150
|
+
* is fully determined by the two numbers the reason already carries in its own prefix. An archived run
|
|
151
|
+
* therefore holds everything needed to compose the client's sentence; what it lacks is only the store.
|
|
152
|
+
*
|
|
153
|
+
* ONE DEFINITION, TWO ENTRY POINTS. The clamp site calls `clauseForDefect` with the counts it already
|
|
154
|
+
* holds; the republish path calls `clauseFromReason`, which reads the same two numbers off the reason's
|
|
155
|
+
* prefix and hands them to the same composer. A second spelling of either sentence would drift, and the
|
|
156
|
+
* drift would be invisible: both surfaces render, and only a reader comparing a fresh run against a
|
|
157
|
+
* republished one would ever see it.
|
|
158
|
+
*
|
|
159
|
+
* AN UNRECOGNISED SHAPE IS REFUSED, AND REFUSED HERE MEANS `null` — NEVER A THROW. This runs on the
|
|
160
|
+
* republish path. A throw there is the failure at the top of this file: a live run died at delivery
|
|
161
|
+
* after 5.55 hours and the client received nothing instead of a report naming one gap. The caller
|
|
162
|
+
* decides what the absence means.
|
|
163
|
+
*/
|
|
164
|
+
const CLAUSE_AUTHORITY = Object.freeze({
|
|
165
|
+
synthesis_unaccounted_delivered: (n, m) =>
|
|
166
|
+
`${n} of the ${m} register records this search surfaced are neither addressed as findings nor expressly set aside in this report — they remain open points a reader must weigh`,
|
|
167
|
+
floor_duty_undischarged: (n, m) =>
|
|
168
|
+
`${n} of the ${m} live registrations identical or near-identical to the mark are not individually addressed in this report — each remains an open point a reader must weigh`,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The reader's sentence for a defect, from its counts. PURE.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} defect the machine token, with or without its `:N` tail
|
|
175
|
+
* @param {number} n the count the defect names
|
|
176
|
+
* @param {number} m the population it is out of
|
|
177
|
+
* @returns {string|null} the client's sentence, or null for a shape this module cannot render
|
|
178
|
+
*/
|
|
179
|
+
export function clauseForDefect(defect, n, m) {
|
|
180
|
+
const compose = CLAUSE_AUTHORITY[String(defect ?? "").split(":")[0].trim()];
|
|
181
|
+
if (!compose) return null;
|
|
182
|
+
const a = Number(n), b = Number(m);
|
|
183
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) return null;
|
|
184
|
+
return compose(a, b);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// The run-record reason's own opening: `<token>:<n> of <m> <noun>`. The NOUN is deliberately not part
|
|
188
|
+
// of the match — `record(s)` and `floor row(s)` are the two spellings today, and a third would make
|
|
189
|
+
// this stop recognising a shape it can render perfectly well. The token is what selects the sentence.
|
|
190
|
+
const REASON_PREFIX_RE = /^([a-z][a-z0-9_]*):(\d[\d,]*)\s+of\s+(\d[\d,]*)\s/i;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The reader's sentence for a condition that has only its run-record reason. PURE.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} reason the run-record sentence
|
|
196
|
+
* @returns {string|null} the client's sentence, or null when the reason is not one this can render
|
|
197
|
+
*/
|
|
198
|
+
export function clauseFromReason(reason) {
|
|
199
|
+
const m = String(reason ?? "").match(REASON_PREFIX_RE);
|
|
200
|
+
if (!m) return null;
|
|
201
|
+
const num = (t) => Number(String(t).replace(/,/g, ""));
|
|
202
|
+
return clauseForDefect(m[1], num(m[2]), num(m[3]));
|
|
203
|
+
}
|
|
204
|
+
|
|
110
205
|
/**
|
|
111
206
|
* THE CLIENT'S CONDITION LIST, from a verdict sidecar. PURE.
|
|
112
207
|
*
|
|
@@ -135,6 +230,15 @@ export function orderClausesForLede(clauses, reasons, guardSet) {
|
|
|
135
230
|
* · a clean reason with no clause survives → break: return "" for a missing clause, arm 2 red
|
|
136
231
|
* · a legacy sidecar still yields its conditions → break: require the clauses key, arm 3 red
|
|
137
232
|
* · clauses shorter than reasons loses nothing → break: map over clauses, arm 4 red
|
|
233
|
+
* · a pre-split reason renders as the lawyer's → break: return the reason, arm 5 red
|
|
234
|
+
* · an unrenderable token-bearing reason is gone → break: return it, arm 6 red
|
|
235
|
+
*
|
|
236
|
+
* THE FALLBACK NO LONGER PRINTS THE RUN RECORD. Where no clause was stored, the clause authority above
|
|
237
|
+
* composes one from the reason's own counts; where it cannot, the condition is DROPPED rather than
|
|
238
|
+
* rendered in engine voice. Dropping is a loss and it is the smaller one: a client who reads
|
|
239
|
+
* `floor_duty_undischarged:4` has been handed the engine's private vocabulary as their own advice. A
|
|
240
|
+
* reason carrying no engine identifier is a factual open-state already — the three machinery sites push
|
|
241
|
+
* the reason AS the clause — and it still survives untouched, which is arm 2.
|
|
138
242
|
*
|
|
139
243
|
* @param {{reasons?: string[], clauses?: string[]}} sidecar the parsed `_driver/verdict.json`
|
|
140
244
|
* @returns {string[]} one condition per reason, in the sidecar's own order
|
|
@@ -143,7 +247,11 @@ export function clientConditions({ reasons, clauses } = {}) {
|
|
|
143
247
|
const rs = Array.isArray(reasons) ? reasons : [];
|
|
144
248
|
const cs = Array.isArray(clauses) ? clauses : [];
|
|
145
249
|
return rs.map((r, i) => {
|
|
250
|
+
if (cs[i] === null) return ""; // a stored null is a condition ruled for the run record alone
|
|
146
251
|
const clause = typeof cs[i] === "string" ? cs[i].trim() : "";
|
|
147
|
-
|
|
252
|
+
if (clause) return clause;
|
|
253
|
+
const text = String(r ?? "").trim();
|
|
254
|
+
if (!text) return "";
|
|
255
|
+
return clauseFromReason(text) ?? (ENGINE_TOKEN_RE.test(text) ? "" : text);
|
|
148
256
|
}).filter(Boolean);
|
|
149
257
|
}
|
package/driver/tokens.mjs
CHANGED
|
@@ -46,10 +46,10 @@
|
|
|
46
46
|
import { readdirSync, readFileSync } from "node:fs";
|
|
47
47
|
import { join } from "node:path";
|
|
48
48
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
49
|
-
import { resolveModel } from "./driver.config.mjs";
|
|
49
|
+
import { resolveModel, modelFamily } from "./driver.config.mjs";
|
|
50
50
|
import { runLog, note } from "./log.mjs";
|
|
51
51
|
import { writeRunStatus } from "./progress.mjs";
|
|
52
|
-
import { stampRunEconomics, isCodeSide } from "./run-economics.mjs";
|
|
52
|
+
import { stampRunEconomics, isCodeSide, vendorOf } from "./run-economics.mjs";
|
|
53
53
|
|
|
54
54
|
function emptyAcc() {
|
|
55
55
|
return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, attempts: 0, thoughtTurns: 0 };
|
|
@@ -84,9 +84,27 @@ function modelKey(rec) {
|
|
|
84
84
|
// them would break byModel summing to total, and an invisible gap is the failure this file already
|
|
85
85
|
// fixed once for byEngine), and the key says what is missing rather than asserting an Anthropic
|
|
86
86
|
// model produced them.
|
|
87
|
+
//
|
|
88
|
+
// A TURN THAT NAMED NO MODEL, a native-language row recording `modelActual: null` (see isAttemptRow),
|
|
89
|
+
// has no id at all. Its tokens still account, under a key that says the model is missing rather than
|
|
90
|
+
// one built from the absent field. Such a row always carries its vendor's stamp, so it reaches here.
|
|
91
|
+
if (typeof rec.model !== "string") return `${engine || "unknown"}/no-model-reported`;
|
|
87
92
|
return `${engine}/unstamped:${rec.model}`;
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
/**
|
|
96
|
+
* WHETHER A ROW IS A PROVIDER ATTEMPT, the one test rollupTokens and servedModels both apply. A row that
|
|
97
|
+
* names a model is one: every stage attempt row carries the tier it asked for, and a native-language row
|
|
98
|
+
* the id its turn reported. So is a native-language row whose turn ran and named no model, which records
|
|
99
|
+
* `modelActual: null` instead (jxModelFields in jx-lanes.mjs). Without that second half, such a turn's
|
|
100
|
+
* attempt and the tokens it reported were dropped from every total, and a run made only of such turns
|
|
101
|
+
* read as one where nothing was looked at. A row with neither field is not a turn: the run log's events,
|
|
102
|
+
* a tool call, a native-language call no provider served.
|
|
103
|
+
*/
|
|
104
|
+
export function isAttemptRow(rec) {
|
|
105
|
+
return Boolean(rec) && (typeof rec.model === "string" || rec.modelActual === null);
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
// usage shape (gateway.mjs): {input,output,cacheRead,cacheWrite}. There is deliberately NO reasoning-token
|
|
91
109
|
// field: the old `reasoning`/`reasoningTokens` pair was an unfillable slot — no reasoning count exists
|
|
92
110
|
// anywhere in the claude payload (not result.usage, not usage.iterations), so no shipped adapter ever
|
|
@@ -128,7 +146,7 @@ export function rollupTokens(runDir) {
|
|
|
128
146
|
if (!ln.trim()) continue;
|
|
129
147
|
let rec;
|
|
130
148
|
try { rec = JSON.parse(ln); } catch { continue; }
|
|
131
|
-
if (!rec
|
|
149
|
+
if (!isAttemptRow(rec)) continue; // only provider attempts carry tokens (see isAttemptRow)
|
|
132
150
|
const t = tokensOf(rec.usage);
|
|
133
151
|
const accs = [total, (byStage[stage] ??= emptyAcc()), (byModel[modelKey(rec)] ??= emptyAcc())];
|
|
134
152
|
// Engine and billing mode are split out because a token is not a portable unit of cost: a turn on a
|
|
@@ -168,6 +186,154 @@ export function rollupTokens(runDir) {
|
|
|
168
186
|
return { total, byStage, byModel, byEngine, byAuthMode };
|
|
169
187
|
}
|
|
170
188
|
|
|
189
|
+
/**
|
|
190
|
+
* THE MODELS THAT SERVED THIS RUN, as the engine reported them: distinct ids, in the order each first
|
|
191
|
+
* served a turn. Read from every attempt row's `modelActual`, the id the wire named: the stage rows
|
|
192
|
+
* gateway.mjs writes and the native-language rows jx.mjs and jx-units.mjs write, one list across both. Never
|
|
193
|
+
* the tier a stage asked for in place of a model the wire named: a tier goes to the CLI as the vendor's
|
|
194
|
+
* alias, so the request says nothing about which model ran, and this is the record that does. The tier
|
|
195
|
+
* stands in only for a name no client may read (below).
|
|
196
|
+
*
|
|
197
|
+
* THREE-VALUED. `null` when there is no attempt row to read (no telemetry directory, or no row in it is
|
|
198
|
+
* a provider turn), so nothing was looked at. `[]` when attempt rows exist and none names a served model
|
|
199
|
+
* a client may read (an engine that does not report one, a turn killed before it said, a turn the Claude
|
|
200
|
+
* program answered itself), on a stage turn and a native-language turn alike. One more case reads `[]`:
|
|
201
|
+
* a Claude turn served under a deployment name whose requested tier the tier reader cannot place (a request
|
|
202
|
+
* outside opus, sonnet, haiku and fable) is left off, because the name must not be printed and there is no
|
|
203
|
+
* tier word to print instead. In a run that mixes such turns with others, the list names only the others.
|
|
204
|
+
* So does a turn stamped by an engine whose vendor the closed table in run-economics.mjs does not name:
|
|
205
|
+
* nobody can say whose model served it. An empty list is never a guess.
|
|
206
|
+
*
|
|
207
|
+
* WHAT IS LISTED IS WHAT A CLIENT MAY READ, mapped here and nowhere else (servedName below), so meta.json,
|
|
208
|
+
* report-data.json and the report's closing line carry one list and cannot disagree. Through a cloud, a
|
|
209
|
+
* turn reports either that cloud's spelling of a Claude model or a name the company gave its own
|
|
210
|
+
* deployment. The first is listed as the Claude id it names; the second is listed as the tier the turn
|
|
211
|
+
* asked for ("Opus"), never as the name. The attempt row itself keeps what the program reported.
|
|
212
|
+
*/
|
|
213
|
+
export function servedModels(runDir) {
|
|
214
|
+
const dDir = driverDir(runDir);
|
|
215
|
+
let files;
|
|
216
|
+
try { files = readdirSync(dDir).filter((f) => f.endsWith(".jsonl") && f !== "run.jsonl"); }
|
|
217
|
+
catch { return null; }
|
|
218
|
+
const firstSeen = new Map(); // id → the earliest row timestamp that named it
|
|
219
|
+
let attempts = 0;
|
|
220
|
+
for (const file of files) {
|
|
221
|
+
let raw;
|
|
222
|
+
try { raw = readFileSync(join(dDir, file), "utf8"); } catch { continue; }
|
|
223
|
+
for (const ln of raw.split("\n")) {
|
|
224
|
+
if (!ln.trim()) continue;
|
|
225
|
+
let rec;
|
|
226
|
+
try { rec = JSON.parse(ln); } catch { continue; }
|
|
227
|
+
// The same test rollupTokens applies for an attempt row, less the driver's own code-side rows:
|
|
228
|
+
// no provider served those, so they cannot stand for "a turn ran and named no model".
|
|
229
|
+
if (!isAttemptRow(rec) || isCodeSide(rec)) continue;
|
|
230
|
+
attempts += 1;
|
|
231
|
+
const id = typeof rec.modelActual === "string" ? rec.modelActual.trim() : "";
|
|
232
|
+
// `<synthetic>` is the Claude CLI's name for a message it wrote itself, measured in testing on a
|
|
233
|
+
// turn a cloud refused for a missing deployment (2026-09-14). No model served that turn, so a
|
|
234
|
+
// bracketed marker is never listed as one.
|
|
235
|
+
if (!id || /^<.*>$/.test(id)) continue;
|
|
236
|
+
// Keyed on the name a client reads, so two deployments serving one tier, or one model reached
|
|
237
|
+
// through two clouds, are listed once.
|
|
238
|
+
const name = servedName(rec, id);
|
|
239
|
+
if (!name) continue;
|
|
240
|
+
const ts = String(rec.ts ?? "");
|
|
241
|
+
if (!firstSeen.has(name) || ts < firstSeen.get(name)) firstSeen.set(name, ts);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!attempts) return null;
|
|
245
|
+
return [...firstSeen].sort((a, b) => (a[1] < b[1] ? -1 : a[1] > b[1] ? 1 : 0)).map(([id]) => id);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Imported here, beside its one reader: the tier every native-language step asks for (see servedName).
|
|
249
|
+
import { JX_TIER } from "./engine/jx-turn.mjs";
|
|
250
|
+
|
|
251
|
+
// AMAZON'S SPELLING OF A CLAUDE ID: an optional cross-region prefix (`us.`, `eu.`, `apac.`, `global.`), the
|
|
252
|
+
// vendor prefix `anthropic.`, and a version suffix (`-v1:0`), optionally at the end of an inference
|
|
253
|
+
// profile's full address (`arn:aws:bedrock:<region>:<account>:inference-profile/…`), whose account number
|
|
254
|
+
// is the company's and is dropped with the rest. `us.anthropic.claude-opus-4-1-20250805-v1:0` is
|
|
255
|
+
// `claude-opus-4-1-20250805`. Anchored on `anthropic.claude-`, so no other vendor's id is rewritten.
|
|
256
|
+
const AMAZON_CLAUDE_ID_RE = /^(?:arn:aws[\w-]*:bedrock:[^/]*\/)?(?:[a-z]{2,6}(?:-[a-z]+)?\.)?anthropic\.(claude-[a-z0-9.-]+?)(?:-v\d+(?::\d+)?)?$/i;
|
|
257
|
+
// GOOGLE'S SPELLING, `claude-opus-4-1@20250805`. It already names the model; it is listed as the dated id
|
|
258
|
+
// `claude-opus-4-1-20250805` because that is the same model's name on Anthropic's own API and on Amazon's,
|
|
259
|
+
// so a model reached through two routes is one entry rather than two spellings of one model. An older
|
|
260
|
+
// model's Google name carries a version mark before the date (`claude-3-5-sonnet-v2@20241022`), the same
|
|
261
|
+
// mark Amazon writes as `-v2:0`; it is not in the model's own name, so it goes too.
|
|
262
|
+
const GOOGLE_CLAUDE_ID_RE = /^(claude-[a-z0-9.-]+?)(?:-v\d+)?@(\d{8})$/i;
|
|
263
|
+
// THE SHAPE OF A CLAUDE MODEL ID, which is what lets an id be printed as itself. A family and one or two
|
|
264
|
+
// version numbers (`claude-opus-4-1`), or the older order of version before family
|
|
265
|
+
// (`claude-3-5-sonnet`); then an optional date; then an optional context-window mark (`[1m]`), which
|
|
266
|
+
// the program may report beside the model and is kept as reported. Tested lower-cased, after the cloud
|
|
267
|
+
// spellings above are rewritten. A prefix test is not enough: a company may name its own deployment
|
|
268
|
+
// `claude-acme-prod`, or wrap its own name in Amazon's form, and neither is a Claude model. A family with
|
|
269
|
+
// no version, `claude-opus`, names no model either: it is the name an operator types for a deployment of
|
|
270
|
+
// that tier, and printed as itself it would put that name on the report, and a Sonnet deployment's name
|
|
271
|
+
// on a turn that asked for Haiku. A `latest` alias, `-latest` or Google's `@latest`, is a pointer the
|
|
272
|
+
// provider moves, never the name of the model a turn reports, so it reads as the tier too.
|
|
273
|
+
const CLAUDE_MODEL_ID_RE = /^claude-(?:(?:opus|sonnet|haiku|fable)(?:-\d{1,2}){1,2}|\d(?:-\d)?-(?:opus|sonnet|haiku))(?:-\d{8})?(?:\[\d+[km]\])?$/;
|
|
274
|
+
const CLAUDE_TIERS = new Set(["opus", "sonnet", "haiku", "fable"]); // fable is reached through the synthesis override
|
|
275
|
+
// A FABLE REQUEST IS READ HERE, NOT BY modelFamily. modelFamily is also the gateway's family comparison on every
|
|
276
|
+
// turn, and it places opus, sonnet and haiku only: an id naming fable stays unknown there, so it can never
|
|
277
|
+
// refuse a turn (driver.config.mjs says why). The report still needs the tier word of a fable turn served under
|
|
278
|
+
// a company's deployment name, so it is read for the report alone, placed where the family reader places the
|
|
279
|
+
// other three: the whole request, or first in it or after a `/`, with or without `claude-`. So `fable` and a
|
|
280
|
+
// request in a pinned id's spelling (`claude-fable-5-1`) both read fable, and `acme-fable` does not. A turn
|
|
281
|
+
// served as a fable id never reaches this: that id is a Claude model's name and prints as itself. modelFamily is
|
|
282
|
+
// asked first, so a request it places reads as that tier even when it also names fable (`fable-x/sonnet` is
|
|
283
|
+
// Sonnet), and one it cannot place falls through to this reader (`sonnet/fable-x` is Fable).
|
|
284
|
+
const FABLE_REQUEST_RE = /(?:^|\/)(?:claude-)?fable(?:[-.]|$)/i;
|
|
285
|
+
const requestedTier = (asked) =>
|
|
286
|
+
modelFamily(asked) ?? (FABLE_REQUEST_RE.test(String(resolveModel(asked) ?? "")) ? "fable" : null);
|
|
287
|
+
|
|
288
|
+
/** A Claude model id in any cloud's spelling, as its own lower-case name, or null when it is not one. */
|
|
289
|
+
function claudeModelId(id) {
|
|
290
|
+
const raw = String(id ?? "").trim();
|
|
291
|
+
const amazon = AMAZON_CLAUDE_ID_RE.exec(raw);
|
|
292
|
+
const google = amazon ? null : GOOGLE_CLAUDE_ID_RE.exec(raw);
|
|
293
|
+
const named = (amazon ? amazon[1] : google ? `${google[1]}-${google[2]}` : raw).toLowerCase();
|
|
294
|
+
return CLAUDE_MODEL_ID_RE.test(named) ? named : null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The name a client reads for one served id, or null when it must not be listed.
|
|
299
|
+
*
|
|
300
|
+
* A CLAUDE ID, in any cloud's spelling, is the model it names. ANY OTHER ID ON A CLAUDE TURN names no
|
|
301
|
+
* Claude model, and on Azure Foundry that is the name a company gave its deployment (`acme-prod-opus`): a
|
|
302
|
+
* company's internal name, and never one to print on its client's report. The turn is listed as the tier
|
|
303
|
+
* it asked for instead, which is what the company deployed under that name.
|
|
304
|
+
*
|
|
305
|
+
* WHOSE TURN IT WAS IS THE VENDOR'S QUESTION, answered by the one closed table of engines (vendorOf in
|
|
306
|
+
* run-economics.mjs), not by a list kept here. An OpenAI turn's id is listed as reported: a Codex id is the
|
|
307
|
+
* model's own name. An Anthropic turn under any engine name is mapped as above; a second list of Claude
|
|
308
|
+
* engines here printed a deployment name, as reported, for every engine it left out. An engine the table
|
|
309
|
+
* does not name is left off: printing its id would make a vendor claim nobody can check.
|
|
310
|
+
*
|
|
311
|
+
* A ROW WITH NO ENGINE STAMP reads as Claude's, as modelKey above reads it, EXCEPT when the id is plainly
|
|
312
|
+
* another vendor's (a `gpt-` or o-series id, by modelFamily's OpenAI reader): printing that as a Claude
|
|
313
|
+
* tier would put a false vendor on a client's report, where a wrong guess in modelKey costs only a key.
|
|
314
|
+
*/
|
|
315
|
+
function servedName(rec, id) {
|
|
316
|
+
const claude = claudeModelId(id);
|
|
317
|
+
if (claude) return claude;
|
|
318
|
+
const engine = typeof rec.engine === "string" ? rec.engine : "";
|
|
319
|
+
const family = engine ? null : modelFamily(id);
|
|
320
|
+
const vendor = engine ? vendorOf(engine) : family && !CLAUDE_TIERS.has(family) ? "openai" : "anthropic";
|
|
321
|
+
if (vendor === "openai") return id;
|
|
322
|
+
if (vendor !== "anthropic") return null;
|
|
323
|
+
// THE TIER THE TURN ASKED FOR, told apart by the kind of row, which its engine stamp names. A stage row
|
|
324
|
+
// (engine "anthropic-agent", another Anthropic engine, or no stamp) records that request as `model`
|
|
325
|
+
// ("opus"), and that holds even when the served id is spelled the same as the request, as it is for a
|
|
326
|
+
// deployment named after its tier. A native-language row (engine "anthropic") records its SERVED id as `model`, beside
|
|
327
|
+
// `modelActual` (jxModelFields), so reading it as the request would hand back the deployment name;
|
|
328
|
+
// every one of those steps asks for JX_TIER. A request in a cloud's spelling is read as the Claude id it
|
|
329
|
+
// names first. modelFamily reads opus, sonnet and haiku, and requestedTier adds fable, for the report
|
|
330
|
+
// alone. A tier neither can place returns null and the id is left off: listing nothing is honest, and
|
|
331
|
+
// listing the name is the leak this prevents.
|
|
332
|
+
const asked = engine === "anthropic" ? JX_TIER : rec.model;
|
|
333
|
+
const tier = requestedTier(claudeModelId(asked) ?? asked);
|
|
334
|
+
return CLAUDE_TIERS.has(tier) ? tier[0].toUpperCase() + tier.slice(1) : null;
|
|
335
|
+
}
|
|
336
|
+
|
|
171
337
|
/**
|
|
172
338
|
* Stamp the rollup onto the run: the `token-rollup` event in _driver/run.jsonl plus `status.json.tokens`.
|
|
173
339
|
*
|
|
@@ -51,36 +51,63 @@ const OPTIONAL = "-";
|
|
|
51
51
|
* @returns {{path: string}|{unresolved: string}}
|
|
52
52
|
*/
|
|
53
53
|
function expandSpecifiers(raw, home) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
// ONE PASS, LEFT TO RIGHT, as systemd reads them. `%%` is an escaped percent and becomes one `%`, so
|
|
55
|
+
// `%%h` is a literal `%h`, never the home. Replacing `%h` first and unescaping after read `%%h` as a
|
|
56
|
+
// `%` followed by the home, and left `50%%` doubled, so a value reached doctor and connect in a form
|
|
57
|
+
// the service was never given. Any other letter after a `%` is a specifier this reader does not
|
|
58
|
+
// implement.
|
|
59
|
+
let unresolved = false;
|
|
60
|
+
const path = String(raw).replace(/%([%A-Za-z])/g, (whole, c) => {
|
|
61
|
+
if (c === "%") return "%";
|
|
62
|
+
if (c === "h" && home) return home;
|
|
63
|
+
unresolved = true;
|
|
64
|
+
return whole;
|
|
65
|
+
});
|
|
66
|
+
return unresolved ? { unresolved: raw } : { path };
|
|
59
67
|
}
|
|
60
68
|
|
|
61
69
|
/**
|
|
62
|
-
* Merge one unit file's environment directives
|
|
70
|
+
* Merge one unit file's environment directives THE WAY SYSTEMD MERGES THEM: every `Environment=`
|
|
71
|
+
* assignment first, then every `EnvironmentFile=`'s contents over them, the files in the order listed.
|
|
63
72
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
73
|
+
* WHERE A LINE SITS DOES NOT DECIDE IT. systemd.exec(5) on `EnvironmentFile=`: "Settings from these files
|
|
74
|
+
* override settings made with Environment=." This reader used to apply the two kinds in file order, so a
|
|
75
|
+
* name set by both came back with the unit's value whenever its `Environment=` line followed the file,
|
|
76
|
+
* which is how every shipped unit is written, while the service ran with the file's. A PATH in the
|
|
77
|
+
* settings file was the case that showed: doctor looked for the engine's program on the unit's PATH,
|
|
78
|
+
* found it, and passed a machine whose services would not find it. The renderer's header
|
|
79
|
+
* (driver/systemd/render-units.mjs) states the same rule, and one unit loads no settings file because of it.
|
|
80
|
+
*
|
|
81
|
+
* Within each kind a later assignment still overrides an earlier one.
|
|
68
82
|
*
|
|
69
83
|
* @param {string} unitText the unit file's contents
|
|
70
84
|
* @param {(path: string) => string|null} readEnvFile returns the file's text, or null if unreadable
|
|
71
|
-
* @returns {{env: Object, missing: string[]}} `missing` names REQUIRED files that could not be read
|
|
85
|
+
* @returns {{env: Object, missing: string[]}} `missing` names REQUIRED files that could not be read, and
|
|
86
|
+
* assignments whose value carries a specifier that could not be expanded
|
|
72
87
|
*/
|
|
73
88
|
function applyUnit(unitText, readEnvFile, home) {
|
|
74
89
|
const env = {};
|
|
90
|
+
const fromFiles = {};
|
|
75
91
|
const missing = [];
|
|
76
92
|
for (const raw of String(unitText ?? "").split("\n")) {
|
|
77
93
|
const line = raw.trim();
|
|
78
94
|
// `Environment=` may carry several assignments on one line; systemd splits on whitespace.
|
|
95
|
+
//
|
|
96
|
+
// ITS VALUES ARE EXPANDED TOO, by the same rule as a file path. Every shipped unit writes
|
|
97
|
+
// `Environment=PATH=%h/.local/bin:%h/.npm-global/bin:…`, and systemd hands the service that PATH with
|
|
98
|
+
// the home filled in. Passed through as written, it named a folder called `%h/.local/bin` that exists
|
|
99
|
+
// nowhere, so a check that looked for the engine's program on the units' PATH found nothing on a
|
|
100
|
+
// machine whose searches found it and ran. A value that cannot be expanded is a hole in the picture,
|
|
101
|
+
// for the reason the file branch below gives: a literal `%h` answers "absent" for a reader that
|
|
102
|
+
// failed.
|
|
79
103
|
const direct = /^Environment=(.*)$/.exec(line);
|
|
80
104
|
if (direct) {
|
|
81
105
|
for (const pair of direct[1].trim().split(/\s+/)) {
|
|
82
106
|
const m = /^"?([A-Za-z_][A-Za-z0-9_]*)=(.*?)"?$/.exec(pair);
|
|
83
|
-
if (m)
|
|
107
|
+
if (!m) continue;
|
|
108
|
+
const value = expandSpecifiers(m[2], home);
|
|
109
|
+
if (value.unresolved !== undefined) missing.push(`${m[1]}=${value.unresolved} (unresolved systemd specifier)`);
|
|
110
|
+
else env[m[1]] = value.path;
|
|
84
111
|
}
|
|
85
112
|
continue;
|
|
86
113
|
}
|
|
@@ -106,10 +133,10 @@ function applyUnit(unitText, readEnvFile, home) {
|
|
|
106
133
|
if (!optional) missing.push(path);
|
|
107
134
|
continue;
|
|
108
135
|
}
|
|
109
|
-
Object.assign(
|
|
136
|
+
Object.assign(fromFiles, parseEnvFile(text));
|
|
110
137
|
}
|
|
111
138
|
}
|
|
112
|
-
return { env, missing };
|
|
139
|
+
return { env: { ...env, ...fromFiles }, missing };
|
|
113
140
|
}
|
|
114
141
|
|
|
115
142
|
/**
|
|
@@ -143,7 +170,7 @@ export function unitEnvironment({ units = [], readEnvFile = () => null, home = n
|
|
|
143
170
|
// name we did not find might live in it — and reporting those as absent would be the original bug
|
|
144
171
|
// with a smaller blast radius. The whole picture is refused instead.
|
|
145
172
|
return { known: false, env, read,
|
|
146
|
-
why: `the units require environment file(s) this command could not read: ${[...new Set(holes)].join(", ")}` };
|
|
173
|
+
why: `the units require environment file(s) or values this command could not read: ${[...new Set(holes)].join(", ")}` };
|
|
147
174
|
}
|
|
148
175
|
return { known: true, env, read, why: null };
|
|
149
176
|
}
|
|
@@ -73,7 +73,24 @@
|
|
|
73
73
|
// it for current state.)
|
|
74
74
|
|
|
75
75
|
/** Where a unit is expected to be installed. "none" is a claim, not an absence — see ORPHANED below. */
|
|
76
|
-
export const BOXES = Object.freeze(["prod", "test", "dev"]);
|
|
76
|
+
export const BOXES = Object.freeze(["prod", "preprod", "test", "dev"]);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Whose DECLARED units a box is expected to carry, where that is not its own name.
|
|
80
|
+
*
|
|
81
|
+
* `runsOn` is a MEASURED claim — this file says so in as many words: an entry gains a box the day an
|
|
82
|
+
* enumeration of that box shows the unit, never the day somebody intends it. So pre-prod cannot be
|
|
83
|
+
* written into `runsOn` from a machine that has not enumerated pre-prod, and it must not be: that would
|
|
84
|
+
* turn a measurement into a plan, which is the one thing these entries are not.
|
|
85
|
+
*
|
|
86
|
+
* What CAN be stated from here is the expectation. Pre-prod is a packaged install of the same product on
|
|
87
|
+
* its own account, with the same doors and the same worker, so what it is expected to carry is what
|
|
88
|
+
* production is expected to carry. The expectation derives; the measurement stays measured; and a unit
|
|
89
|
+
* genuinely absent on pre-prod is reported rather than skipped, which is the whole point of the box
|
|
90
|
+
* being able to name itself.
|
|
91
|
+
*/
|
|
92
|
+
const EXPECTS_LIKE = Object.freeze({ preprod: "prod" });
|
|
93
|
+
const expectationBox = (box) => EXPECTS_LIKE[box] ?? box;
|
|
77
94
|
|
|
78
95
|
// ── RESOLVED UNITS (ruling 2026-08-25 — option B) ──────────────────────
|
|
79
96
|
//
|
|
@@ -773,7 +790,7 @@ export function unitInventoryVerdict({
|
|
|
773
790
|
// unit that is gone from a box is the ruling taking effect, not drift; reporting it as a fault trains
|
|
774
791
|
// a reader to skim the arm that would have caught a real one. Both are still REPORTED — the
|
|
775
792
|
// distinction is which of them is a fault.
|
|
776
|
-
const declaredHere = (u) => box && u.runsOn.includes(box) && !liveBases.includes(u.unit);
|
|
793
|
+
const declaredHere = (u) => box && u.runsOn.includes(expectationBox(box)) && !liveBases.includes(u.unit);
|
|
777
794
|
const absent = box
|
|
778
795
|
? inventory.filter((u) => declaredHere(u) && !u.retired).map((u) => u.unit).sort()
|
|
779
796
|
: [];
|
package/driver/usage-ledger.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// portal-service re-exports both names, so every existing caller is untouched.
|
|
12
12
|
//
|
|
13
13
|
// THIS FILE OWNS THE LEDGER PATH. It used to reconstruct a workspace-relative one —
|
|
14
|
-
// `<workspaceRoot>/workspace-*/studio/
|
|
14
|
+
// `<workspaceRoot>/workspace-*/studio/clearance-search/.matter-ledger.jsonl` — under a comment claiming it
|
|
15
15
|
// was what runner.mjs computed. The comment was right and the code was not: once the product moved the
|
|
16
16
|
// queue to a standalone directory, that path resolved to nothing, and because a missing ledger is a low
|
|
17
17
|
// count rather than an error, every account read as ZERO on every request. Two copies of one calculation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
-
// variant-manifest-model.mjs — strict parser for the
|
|
3
|
+
// variant-manifest-model.mjs — strict parser for the clearance-variants stage's machine output
|
|
4
4
|
// (variant-manifest.json, the structured SIBLING of variant-manifest.md).
|
|
5
5
|
//
|
|
6
6
|
// WS2 (F2 — reproducible search): the model reasons ONCE about what to search (elements,
|
|
@@ -34,7 +34,7 @@ export const ELEMENT_KINDS = ["distinctive", "common", "saturated-common"];
|
|
|
34
34
|
// transliteration-numeric axis); composite = the element inside a larger mark.
|
|
35
35
|
//
|
|
36
36
|
// — THE DOCTRINE'S FOUR UNIVERSAL TAGS ARE WRITABLE. The variants doctrine
|
|
37
|
-
// (
|
|
37
|
+
// (clearance-variants SKILL.md, "Universal categories") tells the seat to tag rows `exact-phrase`,
|
|
38
38
|
// `exact-element`, `plural-root` and `formative-family`, and this enum refused every one — a manifest
|
|
39
39
|
// that obeyed the doctrine literally failed the stage, so seats substituted (`other` on one run,
|
|
40
40
|
// `composite` in the fixture corpus) and the family lane's contains-not-exact mandate had nothing to
|
|
@@ -171,7 +171,7 @@ export function parseVariantManifestModel(raw) {
|
|
|
171
171
|
// compromise: it is the granularity at which BOTH sides are typed, and the only one where the join
|
|
172
172
|
// cannot go quietly wrong.
|
|
173
173
|
//
|
|
174
|
-
// WHY IT IS DESIGNATED HERE AND JUDGED ELSEWHERE.
|
|
174
|
+
// WHY IT IS DESIGNATED HERE AND JUDGED ELSEWHERE. clearance-variants designates; clearance-register writes the
|
|
175
175
|
// coverage row that either honours it or does not. Different stage, earlier turn, before any outcome is
|
|
176
176
|
// known — which is the whole mechanism. A floor field on the coverage row itself would let the seat that
|
|
177
177
|
// missed the work also decide the work was never obliged: the self-grading loophole rebuilt inside its
|
|
@@ -266,7 +266,7 @@ export function variantRomanizationGaps(model) {
|
|
|
266
266
|
* deferred row both stop the nil search, and neither one tells the stage that AUTHORED the string.
|
|
267
267
|
* A `**`-wrapped variant silently becomes a deferred row and the manifest still says the family was
|
|
268
268
|
* covered — the search shrinks and nothing asks for it back. This arm hands the reason to the
|
|
269
|
-
* corrective ladder in-turn, so
|
|
269
|
+
* corrective ladder in-turn, so clearance-variants restates the term while the run is still cheap: the
|
|
270
270
|
* half of the issue's first acceptance criterion the plan-side screen cannot reach.
|
|
271
271
|
*
|
|
272
272
|
* MARKUP ARM ONLY, deliberately. The prose/long-form arm is legitimately shielded for a slogan mark
|