clearotron 0.4.1-beta.0 → 0.4.1-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.
Files changed (79) hide show
  1. package/THIRD-PARTY-NOTICES.md +1 -1
  2. package/bin/README.md +1 -1
  3. package/bin/clearotron.mjs +2 -0
  4. package/bin/mcp.mjs +80 -0
  5. package/bin/start.mjs +14 -1
  6. package/build-info.json +2 -2
  7. package/demo/README.md +1 -1
  8. package/docs/architecture/04-configuration-reference.md +0 -1
  9. package/docs/architecture/06-operations-runbook.md +4 -3
  10. package/docs/architecture/08-development-guide.md +3 -1
  11. package/driver/CHANGELOG.md +17 -0
  12. package/driver/citation-census.json +3 -3
  13. package/driver/compare.mjs +1 -1
  14. package/driver/contract-audit.mjs +6 -6
  15. package/driver/contract-dictation-registry.mjs +1 -1
  16. package/driver/contract-e3-backlog.mjs +17 -16
  17. package/driver/contract-vocabulary.mjs +138 -125
  18. package/driver/declination-call.mjs +5 -1
  19. package/driver/dev-portal.mjs +2 -1
  20. package/driver/engine/mcp/codex-config.mjs +7 -2
  21. package/driver/engine/mcp/gather-config.mjs +4 -0
  22. package/driver/engine/mcp/recording-server.mjs +1 -1
  23. package/driver/engine/mcp/supplemental.mjs +26 -1
  24. package/driver/engine/openai-agent.mjs +13 -3
  25. package/driver/findings-model.mjs +80 -13
  26. package/driver/flag-snapshot.mjs +2 -1
  27. package/driver/foreign-record-links.mjs +50 -0
  28. package/driver/frame-diff-model.mjs +1 -1
  29. package/driver/gateway.mjs +4 -4
  30. package/driver/jx.mjs +1 -1
  31. package/driver/knockout-review-record.mjs +55 -1
  32. package/driver/named-band.mjs +2 -0
  33. package/driver/owner-screen.mjs +7 -4
  34. package/driver/package.json +1 -1
  35. package/driver/pipeline-knockout.mjs +14 -12
  36. package/driver/pipeline.mjs +118 -20
  37. package/driver/portal-service.mjs +5 -4
  38. package/driver/products.mjs +1 -1
  39. package/driver/profiles.mjs +4 -3
  40. package/driver/publish/publish-inputs.mjs +2 -2
  41. package/driver/publish/xlsx.mjs +1 -1
  42. package/driver/register-count.mjs +2 -2
  43. package/driver/register-coverage.mjs +1 -1
  44. package/driver/register-plan.mjs +13 -2
  45. package/driver/result-noun-fields.mjs +3 -3
  46. package/driver/scope-rules.mjs +2 -2
  47. package/driver/stage-context.mjs +9 -0
  48. package/driver/stages-knockout.mjs +6 -5
  49. package/driver/stages.mjs +23 -23
  50. package/driver/suite-census.json +97 -31
  51. package/driver/synthesis-record.mjs +14 -5
  52. package/driver/verify.mjs +1 -1
  53. package/mcp-server/CHANGELOG.md +8 -0
  54. package/mcp-server/CONNECT.md +9 -3
  55. package/mcp-server/demo-mode.mjs +10 -0
  56. package/mcp-server/lib/runs.mjs +7 -1
  57. package/mcp-server/lib/scrub.mjs +1 -1
  58. package/mcp-server/package.json +1 -1
  59. package/mcp-server/packs/README.md +1 -1
  60. package/mcp-server/server.mjs +16 -2
  61. package/package.json +3 -2
  62. package/portal-ui/package.json +3 -3
  63. package/providers/_shared/count.mjs +5 -1
  64. package/providers/_shared/enumerate.mjs +66 -21
  65. package/providers/_shared/execute-plan.mjs +13 -4
  66. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  67. package/providers/oauth-mcp-bridge/package.json +1 -1
  68. package/providers/signa/src/capabilities.js +15 -0
  69. package/providers/signa/src/core.js +55 -26
  70. package/scripts/e2e-first-time.mjs +3 -3
  71. package/scripts/env-audit.mjs +3 -2
  72. package/scripts/freeze-example-run.mjs +8 -8
  73. package/scripts/mcp-registry-listing.mjs +199 -0
  74. package/scripts/mint-plugin-folder.mjs +65 -0
  75. package/scripts/release-version.mjs +44 -0
  76. package/scripts/score.mjs +1 -1
  77. package/shared/register-floor.mjs +19 -0
  78. package/shared/trigger-lane.mjs +1 -1
  79. package/skills/clearotron-ops/SKILL.md +81 -24
package/driver/stages.mjs CHANGED
@@ -1206,7 +1206,7 @@ export const VARIANT_CATEGORY_BRIEF =
1206
1206
  // on an element a move was supposed to discharge is indistinguishable from a relabel.
1207
1207
  //
1208
1208
  // `tokens` lists the validator failure tokens that SPEAK ABOUT that element. PER STAGE, never global:
1209
- // `too_short` and `missing` come from the shared nonEmpty()/needs() helpers (verify.mjs:123-133) and are
1209
+ // `too_short` and `missing` come from the shared helpers nonEmpty() and needs() in verify.mjs, and are
1210
1210
  // legitimately owned by DIFFERENT elements in matter-frame, clearance-variants and frame-diff. A global
1211
1211
  // token→element map sees several owners for one token and "fixes" a partition that was never violated.
1212
1212
  //
@@ -1287,7 +1287,7 @@ export const STAGES = {
1287
1287
  },
1288
1288
  "Applicant's own & affiliated marks — the self-exclusion set (mandatory)": {
1289
1289
  class: "judgment", tokens: [],
1290
- why: "naming affiliates beyond the seed is judgment; the profile half is already pre-bound — stages.mjs:844 hands the model exclusionSeed verbatim and _driver/instructed-scope.json carries job.customer [citation unverified]",
1290
+ why: "naming affiliates beyond the seed is judgment; the profile half is already pre-bound — this stage's `message` hands the model exclusionSeed verbatim and _driver/instructed-scope.json carries job.customer",
1291
1291
  },
1292
1292
  "Campaign shape (stated) — the intake's campaign facts retyped from the `Stated campaign shape` line the dispatch already carries": {
1293
1293
  class: "mechanical:code-rendered", tokens: [],
@@ -1436,7 +1436,7 @@ export const STAGES = {
1436
1436
  },
1437
1437
  "mark — \"<the mark verbatim>\"": {
1438
1438
  class: "mechanical:pre-bound", tokens: ["variantmodel_mark_missing"],
1439
- why: "_driver/instructed-scope.json.marks holds it and stage-context.mjs:175 TOOL_GROUP_EDGES binds that file to this stage. variant-manifest-model.mjs:225-231 parseVariantManifestModel already concedes the field is not the model's: it is excluded from the term-shape gate because \"the stage is told to emit it VERBATIM, and it arrives from the job… a value this stage CANNOT restate\" [citation unverified]",
1439
+ why: "_driver/instructed-scope.json.marks holds it and VALIDATOR_SIDECARS declared in stage-context.mjs binds that file to this stage. variantTermShapeGaps() in variant-manifest-model.mjs already concedes the field is not the model's: it is excluded from the term-shape gate because \"the stage is told to emit it VERBATIM, and it arrives from the job… a value this stage CANNOT restate\"",
1440
1440
  },
1441
1441
  "dominant_element — the distinctive anchor the sweep enumerates": {
1442
1442
  class: "judgment", tokens: ["variantmodel_dominant_element_missing"],
@@ -1456,7 +1456,7 @@ export const STAGES = {
1456
1456
  },
1457
1457
  "variants[].value — the search terms themselves": {
1458
1458
  class: "judgment", tokens: ["variantmodel_term_markup"],
1459
- why: "the repo forbids code from minting a search term twice over — stages.mjs:771-773 and variant-manifest-model.mjs:387 (\"VALIDATES, NEVER GENERATES… the moment code mints a search term, judgment has moved into the funnel\"). variantmodel_term_markup is therefore a shape rule that stays model-facing BY DESIGN; do not reclassify it mechanical to satisfy E6. It also walks dominant_element and elements[].value (variantTermShapeGaps), which is why they carry no token of their own [citation unverified]",
1459
+ why: "the repo forbids code from minting a search term twice over — scriptFloorDirective() in stages.mjs and variantCompletenessGaps() in variant-manifest-model.mjs (\"VALIDATES, NEVER GENERATES… the moment code mints a search term, judgment has moved into the funnel\"). variantmodel_term_markup is therefore a shape rule that stays model-facing BY DESIGN; do not reclassify it mechanical to satisfy E6. It also walks dominant_element and elements[].value (variantTermShapeGaps), which is why they carry no token of their own",
1460
1460
  },
1461
1461
  "variants[].category — one of core|phonetic|visual|transliteration|numeric|composite|other": {
1462
1462
  class: "judgment", tokens: [],
@@ -1472,7 +1472,7 @@ export const STAGES = {
1472
1472
  },
1473
1473
  "Family completeness — at minimum one core, one phonetic and one visual variant, plus a transliteration variant when the mark, the dominant element or any elements[] value is non-Latin": {
1474
1474
  class: "judgment", tokens: ["variantmodel_family_incomplete", "variantmodel_variants_empty"],
1475
- why: "structural completeness of the family the plan compiles from; code refuses but must never mint the missing member (variant-manifest-model.mjs:387). variantmodel_family_incomplete carries THREE arms — category:<cat>, script:<term> and script-coverage:<script>:<territories> — and owns all three here, including the per-script arm declared below [citation unverified]",
1475
+ why: "structural completeness of the family the plan compiles from; code refuses but must never mint the missing member (variantCompletenessGaps() in variant-manifest-model.mjs). variantmodel_family_incomplete carries THREE arms — category:<cat>, script:<term> and script-coverage:<script>:<territories> — and owns all three here, including the per-script arm declared below",
1476
1476
  },
1477
1477
  "Per-script coverage — at least one rendering of the mark in each script the in-scope territories register marks in, as a transliteration variant carrying its romanization": {
1478
1478
  class: "judgment", tokens: [],
@@ -1516,7 +1516,7 @@ export const STAGES = {
1516
1516
  },
1517
1517
  "### Scope ledger rows — {Layer, Item, Status: applied|dropped, Reason, Reopen trigger} across variant / field / source / jurisdiction": {
1518
1518
  class: "judgment", tokens: [],
1519
- why: "defending an omission with the concrete observation that should reopen it is the whole judgment, and frame-diff diffs it against the blind re-derivation. The ROW SHAPE is mechanical — scope-ledger.mjs:123-126 renderScopeLedgerJson parses the table into scope-ledger.json — and the jurisdiction rows are largely a re-carry of the matter frame's own Scope-jurisdictions line [citation unverified]",
1519
+ why: "defending an omission with the concrete observation that should reopen it is the whole judgment, and frame-diff diffs it against the blind re-derivation. The ROW SHAPE is mechanical — renderScopeLedgerJson() in scope-ledger.mjs parses the table into scope-ledger.json — and the jurisdiction rows are largely a re-carry of the matter frame's own Scope-jurisdictions line",
1520
1520
  },
1521
1521
  "Prose variant-table category token — translit-<script>, and for Chinese translit-zh-meaning vs translit-zh-phonetic": {
1522
1522
  class: "judgment", tokens: [],
@@ -1531,7 +1531,7 @@ export const STAGES = {
1531
1531
  },
1532
1532
  "Per-jurisdiction sub-queries": {
1533
1533
  class: "judgment", tokens: [],
1534
- why: "a per-territory search decision no artifact holds — the dispatch names the deliverable and no file the model reads says what one looks like. coverage-ledger.mjs:139-140 shows a downstream consumer that expects them (\"the per-jurisdiction sub-query belongs to primary-sweep\") [citation unverified]",
1534
+ why: "a per-territory search decision no artifact holds — the dispatch names the deliverable and no file the model reads says what one looks like. normalizeAxis() in coverage-ledger.mjs shows a downstream consumer that expects them (\"the per-jurisdiction sub-query belongs to primary-sweep\")",
1535
1535
  },
1536
1536
  "Verify? ✅ column on every transliteration row of the prose Variants table": {
1537
1537
  class: "mechanical:code-rendered", tokens: [],
@@ -1807,7 +1807,7 @@ export const STAGES = {
1807
1807
  },
1808
1808
  "PR / reputational section — the readings the meaning sweep surfaced, per form, each labelled benign or loaded": {
1809
1809
  class: "judgment", tokens: ["connotation_search_missing"],
1810
- why: "The semantic read of a surfaced passage is the model's. `connotation_search_missing` (verify.mjs:362) fires when the ledger recorded no meaning query at all. [citation unverified]",
1810
+ why: "The semantic read of a surfaced passage is the model's. `connotation_search_missing` (commonLawEvidence() in verify.mjs) fires when the ledger recorded no meaning query at all.",
1811
1811
  },
1812
1812
  "`Connotation-search source: <URL | \"perplexity_research — no result\">` line on a clean PR row": {
1813
1813
  class: "mechanical:code-extracted", tokens: ["connotation_search_missing"],
@@ -1970,7 +1970,7 @@ export const STAGES = {
1970
1970
  },
1971
1971
  "disposition form `note` — one line saying what the receipt says and why it reads that way": {
1972
1972
  class: "judgment", tokens: ["connotation_no_ruling"],
1973
- why: "#850 keeps it. The empty-note state lands in the `no_ruling` residual (connotation-search.mjs:2129-2133). [citation unverified]",
1973
+ why: "#850 keeps it. The empty-note state lands in the `no_ruling` residual (findConnotationViolations() in connotation-search.mjs).",
1974
1974
  },
1975
1975
  "disposition form `receipt_index` — the 1-based POSITION of the candidate you ruled on, in that row's own list": {
1976
1976
  class: "mechanical:pre-bound", tokens: ["connotation_form_damaged", "connotation_cite_absent"],
@@ -2102,7 +2102,7 @@ export const STAGES = {
2102
2102
  contractElements: {
2103
2103
  "execute the frozen plan — ONE register_execute_plan call with {plan_path, axis, output_path}": {
2104
2104
  class: "mechanical:tool-written", tokens: ["named_band_missing", "tool_timeout"],
2105
- why: "All three args are driver values interpolated into the message; the tool writes every band block. #850 calls this already right. Note #793: `named_band_missing` and `tool_timeout` are one evidence state with two causes, and registerPlanCallKilled (verify.mjs:1291) separates them from the call log, not from the model. [citation unverified]",
2105
+ why: "All three args are driver values interpolated into the message; the tool writes every band block. #850 calls this already right. Note #793: `named_band_missing` and `tool_timeout` are one evidence state with two causes, and registerPlanCallKilled() in tool-calls.mjs separates them from the call log, not from the model.",
2106
2106
  },
2107
2107
  "the dictated entry list — qid, predicate, terms, owner, nice_classes, regions, when-guard, expected_kind, covered_by": {
2108
2108
  class: "mechanical:pre-bound", tokens: [],
@@ -2126,11 +2126,11 @@ export const STAGES = {
2126
2126
  },
2127
2127
  "named-band block envelope — {state: enumerated|incomplete, query, total_hits, records|fetched+sample+reason}": {
2128
2128
  class: "mechanical:tool-written", tokens: ["named_band_unparseable", "named_band_block_invalid", "named_band_state_invalid", "band_block_unplanned", "named_band_invalid"],
2129
- why: "#850: M (envelope), tool-written blocks. `band_block_unplanned` (verify.mjs:1297) is the backstop that kills a hand-authored qid-less block — a token that exists only because the legacy lane still offers the duty in the same message. [citation unverified]",
2129
+ why: "#850: M (envelope), tool-written blocks. `band_block_unplanned` (validators.registerUnit in verify.mjs) is the backstop that kills a hand-authored qid-less block — a token that exists only because the legacy lane still offers the duty in the same message.",
2130
2130
  },
2131
2131
  "enumerated block records carried verbatim — record_id, mark_text, classes, status, owner_name, owner_country, application_date, registration_date, expiry_date, jurisdictions, screen_verdict": {
2132
2132
  class: "mechanical:tool-written", tokens: ["named_band_collapsed"],
2133
- why: "register_enumerate already returns each record batch-screened; carrying it is transcription. `named_band_collapsed` (verify.mjs:1325, findCollapsedBands) is exactly the recall loss that transcription produces — a slice claiming total_hits with zero records reaching the band. [citation unverified]",
2133
+ why: "register_enumerate already returns each record batch-screened; carrying it is transcription. `named_band_collapsed` (validators.registerUnit in verify.mjs, findCollapsedBands) is exactly the recall loss that transcription produces — a slice claiming total_hits with zero records reaching the band.",
2134
2134
  },
2135
2135
  "per-axis prose digest — the SHORT AUDIT NOTE at register-units/<axis>.md": {
2136
2136
  class: "judgment", tokens: ["too_short"],
@@ -2142,7 +2142,7 @@ export const STAGES = {
2142
2142
  },
2143
2143
  "axis applicability — decide the axis has no work in this manifest and write a one-line \"not applicable\" band block": {
2144
2144
  class: "judgment", tokens: ["band_block_unplanned"],
2145
- why: "The applicability call is the model's; the block is not. Under the supplemental lane a seat obeying unit.md:89-91 writes exactly the qid-less block verify.mjs:1297 kills. The skill file and the stage message contradict each other on the one path the skill offers for an empty axis. [citation unverified]",
2145
+ why: "The applicability call is the model's; the block is not. Under the supplemental lane a seat obeying unit.md's \"not applicable\" rule ('write a single block … so the file always lands') writes exactly the qid-less block validators.registerUnit in verify.mjs kills. The skill file and the stage message contradict each other on the one path the skill offers for an empty axis.",
2146
2146
  },
2147
2147
  "reading the band back and judging it — keep proposing until the axis's dangerous named band is covered": {
2148
2148
  class: "judgment", tokens: [],
@@ -2150,7 +2150,7 @@ export const STAGES = {
2150
2150
  },
2151
2151
  "layer-execution declaration — whether the prose says the register layer / provider tools were not executed or not bound": {
2152
2152
  class: "mechanical:code-extracted", tokens: ["declared_not_executed"],
2153
- why: "_driver/plan-execution.json and the tool-call log already hold whether the call ran — registerPlanCallKilled (verify.mjs:1291) reads exactly that to settle the same question one arm below. This arm still decides it from the model's sentence. [citation unverified]",
2153
+ why: "_driver/plan-execution.json and the tool-call log already hold whether the call ran — registerPlanCallKilled() in tool-calls.mjs reads exactly that to settle the same question one arm below. This arm still decides it from the model's sentence.",
2154
2154
  },
2155
2155
  "`CROSS-CHECK REQUIRED: <what> — <why>` — the check that is needed and why": {
2156
2156
  class: "judgment", tokens: [],
@@ -2364,7 +2364,7 @@ export const STAGES = {
2364
2364
  },
2365
2365
  "reason — the short paragraph carrying the stated ground for the tier (owner characterisation, customer/channel read, decisive ground, Stage-2 mitigant)": {
2366
2366
  class: "judgment", tokens: ["placement_reason_missing", "placement_reason_bare"],
2367
- why: "Four downstream stages adopt or counter-reason BY ARGUING WITH THIS TEXT (stages.mjs:2485-2489). It is authored reasoning, not a value any artifact holds. [citation unverified]",
2367
+ why: "Four downstream stages adopt or counter-reason BY ARGUING WITH THIS TEXT (the B2 comment in this stage's `message` says so). It is authored reasoning, not a value any artifact holds.",
2368
2368
  },
2369
2369
  "borderline: true — the declaration that the promotion question could be argued either way on this record": {
2370
2370
  class: "judgment", tokens: ["placement_borderline_invalid"],
@@ -2372,7 +2372,7 @@ export const STAGES = {
2372
2372
  },
2373
2373
  "select — one record URI of the register candidate being placed": {
2374
2374
  class: "mechanical:code-extracted", tokens: [],
2375
- why: "buildSelectionIndex() in placement-form.mjs already holds every selectable record, built from _driver/register-positions.json plus the band shape's floors, and resolves any URI of a family to the canonical row. #850 M1: the model should return the 1-based index into that driver-written list; code resolves index→id. WHICH candidate to place stays judgment — the row existing at all is the judgment; only the pointing token is mechanical. NO VALIDATOR TOKEN speaks about it: an unresolved select is recorded on the form as `unresolved[]` (placement-union.mjs:134 pushSelection) and handed back in the next dispatch, never a fail. [citation unverified]",
2375
+ why: "buildSelectionIndex() in placement-form.mjs already holds every selectable record, built from _driver/register-positions.json plus the band shape's floors, and resolves any URI of a family to the canonical row. #850 M1: the model should return the 1-based index into that driver-written list; code resolves index→id. WHICH candidate to place stays judgment — the row existing at all is the judgment; only the pointing token is mechanical. NO VALIDATOR TOKEN speaks about it: an unresolved select is recorded on the form as `unresolved[]` (pushSelection() in placement-union.mjs) and handed back in the next dispatch, never a fail.",
2376
2376
  },
2377
2377
  "seat rows for candidates the register does not hold — mark / owner / jurisdiction / records: [] written in full": {
2378
2378
  class: "judgment", tokens: ["placement_mark_missing", "placement_owner_missing", "placement_jurisdiction_invalid", "placement_records_invalid"],
@@ -2404,7 +2404,7 @@ export const STAGES = {
2404
2404
  },
2405
2405
  "retract: <row_id> — withdrawing a seat row already on the form": {
2406
2406
  class: "judgment", tokens: [],
2407
- why: "Whether a placed candidate should come off is a call only the model makes. The row_id it names is driver-assigned (shortId 'PS'/'PR', placement-form.mjs:205 renderEntry/273), so the HANDLE is code-assigned even though the act is judgment. [citation unverified]",
2407
+ why: "Whether a placed candidate should come off is a call only the model makes. The row_id it names is driver-assigned (shortId 'PS'/'PR', in seatRows() and buildSelectionIndex() in placement-form.mjs), so the HANDLE is code-assigned even though the act is judgment.",
2408
2408
  },
2409
2409
  "return payload — the absolute output path plus a 2-3 line summary": {
2410
2410
  class: "mechanical:pre-bound", tokens: [],
@@ -2490,7 +2490,7 @@ export const STAGES = {
2490
2490
  },
2491
2491
  "seat-added coverage rows — the decision to add one, and the axis it is filed under (expressed in the `unit` label)": {
2492
2492
  class: "judgment", tokens: ["coverage_form_axis_invalid"],
2493
- why: "The shipped code rules it judgment in its own words: 'THE AXIS IS PART OF THE JUDGMENT, and on a SEAT row it is the one identifier the seat still supplies' and normalizeAxis 'repair[s] formatting, never invent[s] an axis' (coverage-form.mjs:519-524 coverageFormRows). The driver cannot know which axis a coverage unit it never planned belongs under. [citation unverified]",
2493
+ why: "The shipped code rules it judgment in its own words: 'THE AXIS IS PART OF THE JUDGMENT, and on a SEAT row it is the one identifier the seat still supplies' and normalizeAxis 'repair[s] formatting, never invent[s] an axis' (rowIsSettled() in coverage-form.mjs). The driver cannot know which axis a coverage unit it never planned belongs under.",
2494
2494
  },
2495
2495
  "the seat row's duplicate `axis` cell (the same token already typed as the leading segment of `unit`)": {
2496
2496
  class: "mechanical:code-extracted", tokens: ["coverage_form_axis_invalid"],
@@ -2703,11 +2703,11 @@ export const STAGES = {
2703
2703
  },
2704
2704
  "the escalation decision — which register axes carry a material, unresolved, genuinely closeable gap, and the one-line reason for each": {
2705
2705
  class: "judgment", tokens: [],
2706
- why: "Whether a documented coverage-limited row, a capability-gap deferral or a fresh concern on a confirmed-clean row warrants spending a re-run. The driver hands the coverage/execution truth in as a computed table (stages.mjs:1479; skepticDeferralExtra) precisely so this is a call over data rather than a re-derivation — but the call itself is nobody else's. [citation unverified]",
2706
+ why: "Whether a documented coverage-limited row, a capability-gap deferral or a fresh concern on a confirmed-clean row warrants spending a re-run. The driver hands the coverage/execution truth in as a computed table (skepticDeferralExtra() in pipeline.mjs) precisely so this is a call over data rather than a re-derivation — but the call itself is nobody else's.",
2707
2707
  },
2708
2708
  "escalation decisions — one {axis, reason} per axis that must be re-run, sent through record_skeptic": {
2709
2709
  class: "mechanical:code-rendered", tokens: [],
2710
- why: "#850 rules the line shape M: typed rows, code renders. The axis is one of the closed list the driver wrote into the same message (`Valid axes: ${axes}`, stages.mjs:2628), and the parse at skeptic-record.mjs:49-53 recognises only /ESCALATE:\\s*<axis>\\b/i per known axis — the em-dash, the reason, the section title and the literal 'none' are parsed by nothing. NO TOKEN: skepticFlags never inspects these lines, so a malformed ESCALATE line is a silent no-escalation, not a failure. [citation unverified]",
2710
+ why: "#850 rules the line shape M: typed rows, code renders. The axis is one of the closed list the driver wrote into the same message (`Valid axes: ${axes}`), and the parse in escalatedAxes() in skeptic-record.mjs recognises only /ESCALATE:\\s*<axis>\\b/i per known axis — the em-dash, the reason, the section title and the literal 'none' are parsed by nothing. NO TOKEN: skepticFlags never inspects these lines, so a malformed ESCALATE line is a silent no-escalation, not a failure.",
2711
2711
  },
2712
2712
  "the verbatim ESCALATE string dictated for a `translit-too-generic` unit digest": {
2713
2713
  class: "mechanical:code-rendered", tokens: [],
@@ -2715,7 +2715,7 @@ export const STAGES = {
2715
2715
  },
2716
2716
  "the `risk: <category>` field inside that dictated ESCALATE line": {
2717
2717
  class: "mechanical:code-rendered", tokens: [],
2718
- why: "IT HAS NO CONSUMER AT ALL. stages.mjs:333-336 records that the risk-category filter was deleted, and pipeline.mjs states 'spec-48 D3 — the escalation-risk SHADOW loop (risk-tag parse + would-skip telemetry for the never-enabled CLEAROTRON_ESCALATION_FILTER) is DELETED' at the parse site (envelopeDecision() in pipeline.mjs). The doc block at envelopeDecision() in pipeline.mjs that describes reading the field is a stale header left standing over envelopeDecision — the function it documented is gone. Classed code-rendered because it is not judgment; the correct move is deletion from the skill file. [citation unverified]",
2718
+ why: "IT HAS NO CONSUMER AT ALL. The D3 note in stages.mjs (\"the escalation-risk SHADOW apparatus … is DELETED\") records that the risk-category filter was deleted, and pipeline.mjs states 'spec-48 D3 — the escalation-risk SHADOW loop (risk-tag parse + would-skip telemetry for the never-enabled CLEAROTRON_ESCALATION_FILTER) is DELETED' at the parse site (envelopeDecision() in pipeline.mjs). The doc block at envelopeDecision() in pipeline.mjs that describes reading the field is a stale header left standing over envelopeDecision — the function it documented is gone. Classed code-rendered because it is not judgment; the correct move is deletion from the skill file.",
2719
2719
  },
2720
2720
  "return payload — a 2-3 line summary; the audit itself rides record_skeptic": {
2721
2721
  class: "mechanical:tool-written", tokens: ["skeptic_"],
@@ -3707,7 +3707,7 @@ export const STAGES = {
3707
3707
  },
3708
3708
  "actions.checks_we_ran[].source_link (the `([source](<url>))` URL in each bullet)": {
3709
3709
  class: "mechanical:code-rendered", tokens: [],
3710
- why: "A URL the run already fetched, retyped by hand: the same 127-URI transcription class #850 rules M for synthesis. findings.json carries the typed source.resolved_link (findings-model.mjs:616, render.mjs:1858 fullDetail) and the coverage rows carry theirs; code can render the link once the model names the check. [citation unverified]",
3710
+ why: "A URL the run already fetched, retyped by hand: the same 127-URI transcription class #850 rules M for synthesis. findings.json carries the typed source.resolved_link (validateSource() in findings-model.mjs, fullDetail() in render.mjs) and the coverage rows carry theirs; code can render the link once the model names the check.",
3711
3711
  },
3712
3712
  // RETIRED BY CONVERSION 4 — `actions.only_you_can_close_these[]`. buildOnlyYouSection already
3713
3713
  // replaced any authored section wholesale, so this row described an ask the driver overwrote. The
@@ -3724,7 +3724,7 @@ export const STAGES = {
3724
3724
  },
3725
3725
  "lint_contracts.withdrawn_findings_excluded (a withdrawn finding 'DOES NOT EXIST for this report')": {
3726
3726
  class: "mechanical:pre-bound", tokens: [],
3727
- why: "`withdrawn` is a typed disposition (findings-model.mjs:61 DISPOSITIONS) and fullProseOrdinals already filters it (fullProseOrdinals in pipeline.mjs). The driver can hand a filtered findings view instead of handing the whole file plus a prohibition. [citation unverified]",
3727
+ why: "`withdrawn` is a typed disposition (DISPOSITIONS declared in findings-model.mjs) and fullProseOrdinals already filters it (fullProseOrdinals in pipeline.mjs). The driver can hand a filtered findings view instead of handing the whole file plus a prohibition.",
3728
3728
  },
3729
3729
  // RETIRED BY CONVERSION 4 — the `::p::` MARKER. Which note is internal is still the model's call
3730
3730
  // and rides `internal: true` on the entry; the marker itself is a rendering instruction the driver
@@ -52,8 +52,8 @@
52
52
  "todos": 0
53
53
  },
54
54
  "a-batch-of-names-is-one-question-per-name-without-or.test.mjs": {
55
- "tests": 4,
56
- "asserts": 20,
55
+ "tests": 6,
56
+ "asserts": 28,
57
57
  "skips": 0,
58
58
  "todos": 0
59
59
  },
@@ -195,9 +195,15 @@
195
195
  "skips": 0,
196
196
  "todos": 0
197
197
  },
198
+ "a-clean-knockout-hands-the-reviewer-its-first-read-lines.test.mjs": {
199
+ "tests": 6,
200
+ "asserts": 25,
201
+ "skips": 0,
202
+ "todos": 0
203
+ },
198
204
  "a-client-cell-carries-no-raw-engine-words.test.mjs": {
199
- "tests": 4,
200
- "asserts": 13,
205
+ "tests": 5,
206
+ "asserts": 18,
201
207
  "skips": 0,
202
208
  "todos": 0
203
209
  },
@@ -532,8 +538,8 @@
532
538
  "todos": 0
533
539
  },
534
540
  "a-failed-run-resumes-from-the-command-line.test.mjs": {
535
- "tests": 3,
536
- "asserts": 9,
541
+ "tests": 4,
542
+ "asserts": 14,
537
543
  "skips": 0,
538
544
  "todos": 0
539
545
  },
@@ -579,6 +585,12 @@
579
585
  "skips": 0,
580
586
  "todos": 0
581
587
  },
588
+ "a-foreign-record-link-is-named-once-and-blanked-at-salvage.test.mjs": {
589
+ "tests": 8,
590
+ "asserts": 31,
591
+ "skips": 0,
592
+ "todos": 0
593
+ },
582
594
  "a-frame-call-with-places-per-name-passes-the-gate.test.mjs": {
583
595
  "tests": 2,
584
596
  "asserts": 7,
@@ -664,8 +676,8 @@
664
676
  "todos": 0
665
677
  },
666
678
  "a-job-on-the-older-goods-spelling-scopes-the-same.test.mjs": {
667
- "tests": 10,
668
- "asserts": 23,
679
+ "tests": 11,
680
+ "asserts": 28,
669
681
  "skips": 0,
670
682
  "todos": 0
671
683
  },
@@ -765,6 +777,12 @@
765
777
  "skips": 0,
766
778
  "todos": 0
767
779
  },
780
+ "a-late-synthesis-save-keeps-the-seams-writes.test.mjs": {
781
+ "tests": 2,
782
+ "asserts": 16,
783
+ "skips": 0,
784
+ "todos": 0
785
+ },
768
786
  "a-link-in-the-workbook-is-not-banned-vocabulary.test.mjs": {
769
787
  "tests": 2,
770
788
  "asserts": 5,
@@ -1173,6 +1191,12 @@
1173
1191
  "skips": 0,
1174
1192
  "todos": 0
1175
1193
  },
1194
+ "a-register-floor-is-carried-and-never-a-zero.test.mjs": {
1195
+ "tests": 7,
1196
+ "asserts": 19,
1197
+ "skips": 0,
1198
+ "todos": 0
1199
+ },
1176
1200
  "a-register-manual-states-what-its-provider-declares.test.mjs": {
1177
1201
  "tests": 6,
1178
1202
  "asserts": 13,
@@ -1314,7 +1338,7 @@
1314
1338
  "a-screenshot-of-an-error-page-is-not-a-report.test.mjs": {
1315
1339
  "tests": 21,
1316
1340
  "asserts": 55,
1317
- "skips": 4,
1341
+ "skips": 5,
1318
1342
  "todos": 0
1319
1343
  },
1320
1344
  "a-script-is-asked-once-per-market-that-files-in-it.test.mjs": {
@@ -1360,8 +1384,8 @@
1360
1384
  "todos": 0
1361
1385
  },
1362
1386
  "a-session-the-classifier-cut-is-not-recorded-as-clean.test.mjs": {
1363
- "tests": 12,
1364
- "asserts": 77,
1387
+ "tests": 13,
1388
+ "asserts": 80,
1365
1389
  "skips": 0,
1366
1390
  "todos": 0
1367
1391
  },
@@ -1419,6 +1443,12 @@
1419
1443
  "skips": 0,
1420
1444
  "todos": 0
1421
1445
  },
1446
+ "a-stable-cut-moves-the-plugin-pin.test.mjs": {
1447
+ "tests": 4,
1448
+ "asserts": 13,
1449
+ "skips": 0,
1450
+ "todos": 0
1451
+ },
1422
1452
  "a-stable-is-published-only-over-a-clean-code-scan.test.mjs": {
1423
1453
  "tests": 9,
1424
1454
  "asserts": 19,
@@ -1829,7 +1859,7 @@
1829
1859
  },
1830
1860
  "an-exact-spelling-band-stacks-at-the-register-s-exact-width.test.mjs": {
1831
1861
  "tests": 3,
1832
- "asserts": 7,
1862
+ "asserts": 11,
1833
1863
  "skips": 0,
1834
1864
  "todos": 0
1835
1865
  },
@@ -2068,8 +2098,8 @@
2068
2098
  "todos": 0
2069
2099
  },
2070
2100
  "both-doors-start-on-both-paths.test.mjs": {
2071
- "tests": 26,
2072
- "asserts": 61,
2101
+ "tests": 27,
2102
+ "asserts": 64,
2073
2103
  "skips": 0,
2074
2104
  "todos": 0
2075
2105
  },
@@ -2130,7 +2160,7 @@
2130
2160
  "case-law-credential-state.test.mjs": {
2131
2161
  "tests": 8,
2132
2162
  "asserts": 26,
2133
- "skips": 0,
2163
+ "skips": 1,
2134
2164
  "todos": 0
2135
2165
  },
2136
2166
  "case-law-join.test.mjs": {
@@ -2519,7 +2549,7 @@
2519
2549
  },
2520
2550
  "coverage-judgment-rows.test.mjs": {
2521
2551
  "tests": 9,
2522
- "asserts": 40,
2552
+ "asserts": 45,
2523
2553
  "skips": 0,
2524
2554
  "todos": 0
2525
2555
  },
@@ -2578,8 +2608,8 @@
2578
2608
  "todos": 0
2579
2609
  },
2580
2610
  "declination-is-stated-not-inferred.test.mjs": {
2581
- "tests": 24,
2582
- "asserts": 69,
2611
+ "tests": 25,
2612
+ "asserts": 73,
2583
2613
  "skips": 0,
2584
2614
  "todos": 0
2585
2615
  },
@@ -2991,6 +3021,12 @@
2991
3021
  "skips": 0,
2992
3022
  "todos": 0
2993
3023
  },
3024
+ "each-stable-npm-serves-is-listed-in-the-mcp-registry.test.mjs": {
3025
+ "tests": 11,
3026
+ "asserts": 61,
3027
+ "skips": 0,
3028
+ "todos": 0
3029
+ },
2994
3030
  "effective-scope.test.mjs": {
2995
3031
  "tests": 15,
2996
3032
  "asserts": 44,
@@ -3250,8 +3286,8 @@
3250
3286
  "todos": 0
3251
3287
  },
3252
3288
  "experiment-context.test.mjs": {
3253
- "tests": 24,
3254
- "asserts": 80,
3289
+ "tests": 26,
3290
+ "asserts": 86,
3255
3291
  "skips": 0,
3256
3292
  "todos": 0
3257
3293
  },
@@ -3808,8 +3844,8 @@
3808
3844
  "todos": 0
3809
3845
  },
3810
3846
  "no-stage-is-offered-a-command-tool.test.mjs": {
3811
- "tests": 6,
3812
- "asserts": 17,
3847
+ "tests": 7,
3848
+ "asserts": 19,
3813
3849
  "skips": 0,
3814
3850
  "todos": 0
3815
3851
  },
@@ -4162,8 +4198,8 @@
4162
4198
  "todos": 0
4163
4199
  },
4164
4200
  "portal-service.test.mjs": {
4165
- "tests": 132,
4166
- "asserts": 715,
4201
+ "tests": 133,
4202
+ "asserts": 718,
4167
4203
  "skips": 1,
4168
4204
  "todos": 0
4169
4205
  },
@@ -4614,7 +4650,7 @@
4614
4650
  "register-record-log-run-scoped.test.mjs": {
4615
4651
  "tests": 9,
4616
4652
  "asserts": 21,
4617
- "skips": 0,
4653
+ "skips": 1,
4618
4654
  "todos": 0
4619
4655
  },
4620
4656
  "register-review-fixes.test.mjs": {
@@ -4661,7 +4697,7 @@
4661
4697
  },
4662
4698
  "release-pipeline.test.mjs": {
4663
4699
  "tests": 124,
4664
- "asserts": 479,
4700
+ "asserts": 480,
4665
4701
  "skips": 2,
4666
4702
  "todos": 0
4667
4703
  },
@@ -5410,8 +5446,8 @@
5410
5446
  "todos": 0
5411
5447
  },
5412
5448
  "synthesis-structural-input.test.mjs": {
5413
- "tests": 10,
5414
- "asserts": 67,
5449
+ "tests": 11,
5450
+ "asserts": 69,
5415
5451
  "skips": 0,
5416
5452
  "todos": 0
5417
5453
  },
@@ -7166,6 +7202,30 @@
7166
7202
  "skips": 0,
7167
7203
  "todos": 0
7168
7204
  },
7205
+ "the-handshake-reports-the-package-version.test.mjs": {
7206
+ "tests": 1,
7207
+ "asserts": 3,
7208
+ "skips": 0,
7209
+ "todos": 0
7210
+ },
7211
+ "the-package-starts-the-mcp-server.test.mjs": {
7212
+ "tests": 6,
7213
+ "asserts": 22,
7214
+ "skips": 0,
7215
+ "todos": 0
7216
+ },
7217
+ "the-plugin-folder-is-what-the-directory-installs.test.mjs": {
7218
+ "tests": 3,
7219
+ "asserts": 18,
7220
+ "skips": 0,
7221
+ "todos": 0
7222
+ },
7223
+ "the-plugin-starts-npx-through-the-shell-on-windows.test.mjs": {
7224
+ "tests": 8,
7225
+ "asserts": 24,
7226
+ "skips": 1,
7227
+ "todos": 0
7228
+ },
7169
7229
  "the-portal-can-tell-who-has-connected-an-assistant.test.mjs": {
7170
7230
  "tests": 12,
7171
7231
  "asserts": 35,
@@ -7661,6 +7721,12 @@
7661
7721
  "skips": 0,
7662
7722
  "todos": 0
7663
7723
  },
7724
+ "providers/_shared/test/a-register-floor-is-too-many-to-read.test.mjs": {
7725
+ "tests": 8,
7726
+ "asserts": 47,
7727
+ "skips": 0,
7728
+ "todos": 0
7729
+ },
7664
7730
  "providers/_shared/test/a-slow-question-is-asked-again-in-region-halves.test.mjs": {
7665
7731
  "tests": 9,
7666
7732
  "asserts": 38,
@@ -7723,7 +7789,7 @@
7723
7789
  },
7724
7790
  "providers/_shared/test/owner-count-verification.test.mjs": {
7725
7791
  "tests": 13,
7726
- "asserts": 51,
7792
+ "asserts": 52,
7727
7793
  "skips": 0,
7728
7794
  "todos": 0
7729
7795
  },
@@ -7884,8 +7950,8 @@
7884
7950
  "todos": 0
7885
7951
  },
7886
7952
  "providers/signa/test/exact-spellings-go-as-one-ranked-list.test.mjs": {
7887
- "tests": 5,
7888
- "asserts": 18,
7953
+ "tests": 11,
7954
+ "asserts": 39,
7889
7955
  "skips": 0,
7890
7956
  "todos": 0
7891
7957
  },
@@ -53,7 +53,7 @@ import { declinationCallPaths, readDeclinations } from "./declination-tool.mjs";
53
53
  import { reconcileDeclinationDuty, declinationDutyRefusal } from "./declination-duty.mjs";
54
54
  import { findingUris } from "./record-carry.mjs"; // one derivation of "which records did the findings name", called not copied
55
55
  import { foldAfterSave } from "./record-fold.mjs"; // once the delivery seam is passed, the saved record is folded again
56
-
56
+ import { recordOriginsOr, blankOnRepeat, recordBlanking, FOREIGN_HOST_TOKEN } from "./foreign-record-links.mjs"; // a foreign record link: refused once, then blanked
57
57
  export const NARRATIVE_FILE = "narrative.md";
58
58
  export const FINDINGS_FILE = "findings.json";
59
59
 
@@ -269,7 +269,7 @@ export function withoutWithheldRows(rows, ledger) {
269
269
  return (Array.isArray(rows) ? rows : []).filter((r) => !namesIn(r?.area).some((n) => withheld.has(n)));
270
270
  }
271
271
 
272
- export function acceptSynthesis(params, { asks = [], ledger = null, manifest = null, method = undefined, methodInvalid = null, owed = null, declined = null } = {}) {
272
+ export function acceptSynthesis(params, { asks = [], ledger = null, manifest = null, method = undefined, methodInvalid = null, owed = null, declined = null, recordOrigins = null } = {}) {
273
273
  const doc = params?.findings;
274
274
  if (!doc || typeof doc !== "object" || Array.isArray(doc)) {
275
275
  return { ok: false, reason: "synthesis_findings_missing: `findings` must be the findings document object { schema_version, rated_under_framework, findings, coverage, … } — this is the record the report and the workbook are built from, and there is no path that renders without it" };
@@ -383,7 +383,7 @@ export function acceptSynthesis(params, { asks = [], ledger = null, manifest = n
383
383
  // own file gone wrong, and the seat cannot fix it — so it is refused by name, never read as "no method".
384
384
  if (methodInvalid) return { ok: false, reason: `framework_method_unreadable: _driver/${FROZEN_METHOD_FILE} does not read back (${String(methodInvalid).slice(0, 160)}) — a driver fault, not the writer's` };
385
385
  let parsedDoc;
386
- try { parsedDoc = parseFindingsJson(findings, manifest ? { manifest, ...(method !== undefined ? { method } : {}) } : {}); }
386
+ try { parsedDoc = parseFindingsJson(findings, { ...(manifest ? { manifest, ...(method !== undefined ? { method } : {}) } : {}), ...(Array.isArray(recordOrigins) ? { recordOrigins } : {}) }); }
387
387
  catch (e) {
388
388
  return { ok: false, reason: `synthesis_findings_invalid: ${String(e?.message ?? e).slice(0, 300)}` };
389
389
  }
@@ -608,7 +608,7 @@ export function recordSynthesis(runDir, received, opts = {}) {
608
608
  const auto = driverReadsFor(dir0);
609
609
  const { asks = auto.asks, ledger = auto.ledger, manifest = auto.manifest,
610
610
  method = auto.method, methodInvalid = auto.methodInvalid ?? null,
611
- owed = auto.owed, declined = auto.declined,
611
+ owed = auto.owed, declined = auto.declined, recordOrigins = undefined,
612
612
  now = () => new Date().toISOString() } = opts;
613
613
  const { dir, payload, accepted, refusals } = synthesisCallPaths(dir0);
614
614
  // Its sibling rule — ONE FILE PER CALL, refusals included. This used to
@@ -644,7 +644,16 @@ export function recordSynthesis(runDir, received, opts = {}) {
644
644
  call = merged.merged;
645
645
  }
646
646
 
647
- const v = acceptSynthesis(call, { asks, ledger, manifest, method, methodInvalid, owed, declined });
647
+ // A RECORD LINK ON A HOST THIS REGISTER DOES NOT PUBLISH is gated here as verify.mjs gates the saved
648
+ // file. The run's first such call is refused, naming every finding, so the seat can fix them in this
649
+ // turn; a repeat is accepted with those links blanked and the originals kept (foreign-record-links.mjs).
650
+ const origins = recordOriginsOr(recordOrigins);
651
+ const repeat = blankOnRepeat(call, origins, refusalsFor(dir0).some((r) => String(r?.reason ?? "").includes(FOREIGN_HOST_TOKEN)));
652
+ if (repeat.blanked.length) {
653
+ call = repeat.call;
654
+ recordBlanking(dir0, { stage: "synthesis", layer: "call", blanked: repeat.blanked, kept: repeat.kept });
655
+ }
656
+ const v = acceptSynthesis(call, { asks, ledger, manifest, method, methodInvalid, owed, declined, recordOrigins: origins });
648
657
  if (!v.ok) {
649
658
  noteRefusal(v.reason);
650
659
  return { written: null, refused: v.reason,
package/driver/verify.mjs CHANGED
@@ -158,7 +158,7 @@ function nonEmpty(content, min = MIN) {
158
158
  * true for only one lane, and put back. Appending keeps every existing matcher matching — they all test
159
159
  * substrings — and gives the seat the one word it was missing.
160
160
  *
161
- * It also sharpens `noChange` (`gateway.mjs:1220`, "this attempt's token equals the previous attempt's"):
161
+ * It also sharpens `noChange` (in runStageLadder() in gateway.mjs, "this attempt's token equals the previous attempt's"):
162
162
  * a file that starts failing a DIFFERENT member now reads as changed instead of as a stalled retry.
163
163
  *
164
164
  * `names` is positional against `markers` and optional; a call site that omits it emits exactly the token
@@ -1,5 +1,13 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.4.1-beta.2
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.4.1-beta.1
8
+
9
+ No changes in this release.
10
+
3
11
  ## 0.4.1-beta.0
4
12
 
5
13
  No changes in this release.
@@ -21,9 +21,9 @@ It reads run directories off your disk. So:
21
21
 
22
22
  - **It shows you runs you have already produced.** `CLEAROTRON_WORK_DIR` is where the engine writes
23
23
  them, and that is the one variable the server needs.
24
- - **`npx clearotron demo` runs are not visible to it.** The demo publishes a *report* into a pool; it does not
25
- create a run directory. There is nothing dishonest happening if `list_runs` comes back empty — you have
26
- no runs yet.
24
+ - **With no install, it shows you the demo.** `npx clearotron mcp` starts this server from the npm package.
25
+ On a machine with no install it answers from the four sample runs `npx clearotron demo` replays, and every
26
+ row says it is the demo. On a machine with an install it reads that install's runs.
27
27
  - **Nothing here spends money on its own,** with two exceptions that are gated and named: `start_run`
28
28
  enqueues a real clearance, and `what_if_run` re-runs one pipeline stage. Both are refused by default on
29
29
  a sensible allowlist — see "Hold two tools back" below.
@@ -51,6 +51,12 @@ claude mcp add trademark-artifacts --scope user \
51
51
 
52
52
  Check it: `claude mcp list` prints `trademark-artifacts: … - √ Connected`.
53
53
 
54
+ With no clone, start it from the npm package instead, naming the version:
55
+
56
+ ```sh
57
+ claude mcp add trademark-artifacts --scope user -- npx -y clearotron@<version> mcp
58
+ ```
59
+
54
60
  `--scope user` makes it available in every project, not only the directory you ran it in. In Windows
55
61
  PowerShell 5.1, write the separator as `"--"`: that shell drops a bare `--`. Remove it with
56
62
  `claude mcp remove trademark-artifacts`.
@@ -0,0 +1,10 @@
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
+ //
4
+ // demo-mode.mjs — whether this server is serving the demo's sample runs.
5
+ //
6
+ // `clearotron mcp` decides it and the server reads it, in one process: bin/mcp.mjs sets it before it
7
+ // loads the server, and the server reads it as it answers. It is a module and not an environment
8
+ // variable because nothing outside this process sets it or reads it, and every environment variable the
9
+ // product reads is a setting an operator is told about.
10
+ export const demoMode = { on: false };