@tikoci/centrs 0.1.3 → 0.1.6

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 (107) hide show
  1. package/CHANGELOG.md +348 -0
  2. package/GLOSSARY.txt +252 -0
  3. package/LICENSE +21 -0
  4. package/README.md +44 -12
  5. package/commands/AGENTS.md +14 -5
  6. package/commands/api/AGENTS.md +13 -1
  7. package/commands/api/README.md +56 -26
  8. package/commands/api/examples.md +46 -1
  9. package/commands/btest/README.md +21 -32
  10. package/commands/check/README.md +6 -1
  11. package/commands/discover/README.md +5 -11
  12. package/commands/execute/README.md +77 -36
  13. package/commands/execute/examples.md +120 -10
  14. package/commands/explain/README.md +2756 -0
  15. package/commands/explain/examples.md +702 -0
  16. package/commands/mcp/README.md +7 -4
  17. package/commands/mcp/examples.md +2 -2
  18. package/commands/retrieve/README.md +25 -27
  19. package/commands/retrieve/examples.md +16 -0
  20. package/commands/terminal/examples.md +7 -1
  21. package/commands/transfer/README.md +28 -20
  22. package/commands/transfer/examples.md +29 -0
  23. package/docs/CLI.md +353 -0
  24. package/docs/CONSTITUTION.md +138 -30
  25. package/docs/MATRIX.md +92 -18
  26. package/docs/errors/README.md +3 -0
  27. package/docs/errors/quickchr/machine-not-found.md +22 -0
  28. package/docs/errors/quickchr/machine-stopped.md +20 -0
  29. package/docs/errors/quickchr/package-unavailable.md +27 -0
  30. package/docs/errors/quickchr/unsupported-via.md +35 -0
  31. package/docs/errors/quickchr/unsupported.md +35 -0
  32. package/docs/errors/routeros/version-unsupported.md +26 -0
  33. package/docs/errors/transport/provider-trust.md +29 -0
  34. package/docs/errors/usage/stdin-ignored.md +28 -0
  35. package/docs/errors/validation/syntax.md +29 -4
  36. package/docs/errors/validation/unknown-attribute.md +37 -4
  37. package/package.json +56 -10
  38. package/src/api-fanout.ts +46 -18
  39. package/src/api.ts +351 -121
  40. package/src/cli/api.ts +26 -7
  41. package/src/cli/btest.ts +19 -3
  42. package/src/cli/execute.ts +25 -6
  43. package/src/cli/explain.ts +503 -0
  44. package/src/cli/retrieve.ts +33 -1
  45. package/src/cli/selection.ts +71 -11
  46. package/src/cli/settings.ts +2 -2
  47. package/src/cli/terminal.ts +1 -1
  48. package/src/cli/transfer.ts +23 -6
  49. package/src/cli.ts +15 -2
  50. package/src/core/envelope.ts +58 -2
  51. package/src/core/error-catalog.ts +42 -1
  52. package/src/core/execute-command.ts +148 -0
  53. package/src/core/inspect.ts +36 -4
  54. package/src/core/preflight.ts +306 -0
  55. package/src/core/routeros-string.ts +24 -0
  56. package/src/core/version.ts +4 -0
  57. package/src/core/yaml.ts +86 -0
  58. package/src/errors.ts +1 -0
  59. package/src/execute-fanout.ts +47 -18
  60. package/src/execute.ts +339 -307
  61. package/src/explain/arg-tokens.ts +151 -0
  62. package/src/explain/args.ts +1749 -0
  63. package/src/explain/blocks.ts +175 -0
  64. package/src/explain/brace-slots.ts +143 -0
  65. package/src/explain/brace-tokens.ts +39 -0
  66. package/src/explain/catalog.ts +1343 -0
  67. package/src/explain/coordinates.ts +488 -0
  68. package/src/explain/defects.ts +218 -0
  69. package/src/explain/escape-tokens.ts +77 -0
  70. package/src/explain/highlight-projection.ts +308 -0
  71. package/src/explain/is-known-menu.ts +17 -0
  72. package/src/explain/menus.ts +668 -0
  73. package/src/explain/operator-tokens.ts +316 -0
  74. package/src/explain/operators.ts +465 -0
  75. package/src/explain/path-tokens.ts +150 -0
  76. package/src/explain/pathresolve.ts +1569 -0
  77. package/src/explain/quoted-string.ts +331 -0
  78. package/src/explain/scope-brace.ts +461 -0
  79. package/src/explain/segment.ts +1165 -0
  80. package/src/explain/separator.ts +187 -0
  81. package/src/explain/split-owner.ts +194 -0
  82. package/src/explain/string-tokens.ts +77 -0
  83. package/src/explain/symbol-values.ts +556 -0
  84. package/src/explain/symbols.ts +1617 -0
  85. package/src/explain/token-ranges.ts +72 -0
  86. package/src/explain/transport.ts +514 -0
  87. package/src/explain/value-tokens.ts +83 -0
  88. package/src/explain/values.ts +357 -0
  89. package/src/explain/verbs.ts +64 -0
  90. package/src/explain/verbsplit.ts +955 -0
  91. package/src/explain/write.ts +856 -0
  92. package/src/explain.ts +2602 -0
  93. package/src/index.ts +225 -6
  94. package/src/mcp/server.ts +2 -1
  95. package/src/offline-gate.ts +148 -0
  96. package/src/protocols/mac-telnet-console.ts +61 -17
  97. package/src/resolver/facts.ts +1 -1
  98. package/src/resolver/index.ts +23 -4
  99. package/src/resolver/quickchr-provider.ts +597 -0
  100. package/src/resolver/selection.ts +99 -12
  101. package/src/resolver/service-endpoint.ts +155 -0
  102. package/src/resolver/target.ts +1 -1
  103. package/src/retrieve-fanout.ts +27 -13
  104. package/src/retrieve.ts +82 -77
  105. package/src/settings.ts +2 -1
  106. package/src/transfer-fanout.ts +43 -26
  107. package/src/transfer.ts +88 -45
@@ -9,15 +9,22 @@ This file is short on purpose. Resist the urge to grow it.
9
9
 
10
10
  ## What centrs is
11
11
 
12
- A typed library, with CLI/MCP/TUI/proxy frontends, that reaches RouterOS over
13
- multiple protocols and gives the caller **validated** access plus structured,
14
- actionable diagnostics. The TypeScript API is the root surface; everything else
15
- is an adapter over it.
12
+ A typed library for RouterOS analysis and **validated** device access, with
13
+ CLI/MCP/TUI/proxy frontends and structured, actionable diagnostics. The
14
+ TypeScript API is the root surface; everything else is an adapter over it.
15
+ The initial consumers are other TIKOCI tools and developers building integrations.
16
16
 
17
17
  The product is *not* "REST/SSH/native API wrappers." The product is
18
18
  **validation + structured envelopes + identity resolution** on top of those
19
19
  transports. Without those, this is a worse `curl`.
20
20
 
21
+ `explain` is shared analysis across transports and a useful offline capability
22
+ in its own right. It exposes source structure, facts, uncertainty, and evidence
23
+ without executing the input. Those facts can support future diagnostics in
24
+ other commands' envelopes; that integration is not implemented by declaring
25
+ this boundary. Analysis findings do not grant execution permission or replace
26
+ the runners' validation gates.
27
+
21
28
  centrs also speaks a small set of MikroTik **peer protocols** that are *not*
22
29
  RouterOS-command transports — currently the **bandwidth test** (`btest`), which
23
30
  mirrors `/tool/bandwidth-test` and `/tool/bandwidth-server`. This is a
@@ -29,29 +36,69 @@ friendly errors, but the "validate a RouterOS-shaped command" gate below does
29
36
 
30
37
  ## Validation is the product
31
38
 
32
- Every RouterOS-shaped call goes through canonicalize → validate → run →
33
- re-validate-server-side. Validation is not optional polish.
39
+ Every RouterOS-shaped operation sent to a device goes through canonicalize →
40
+ validate → run → re-validate-server-side. Analysis-only `explain` never runs
41
+ the analyzed input; offline analysis opens no connection. Validation on the
42
+ runners is not optional polish.
43
+
44
+ The `validate` step itself has **two stages, in this order** (GH#354):
45
+
46
+ 1. **Offline** — the same corpus-gated analyzer `explain` publishes, run over
47
+ the input with no connection (`src/offline-gate.ts`). A rejection is a byte
48
+ span and no round trip; the protocol adapter is never dialed. This stage runs
49
+ on every transport, for `execute` and for `api`'s `/execute` script mode.
50
+ 2. **Device** — the RouterOS-side probe below: `:put [:parse "…"]` and/or
51
+ `/console/inspect request=child`, depending on the command shape and the
52
+ transport.
53
+
54
+ The stages are not interchangeable and neither subsumes the other. Offline has
55
+ no per-menu schema (decision 3 — no offline schema snapshot), so it cannot see
56
+ an unknown attribute; the device probe is version-fragile (GH#343), so where it
57
+ is unsafe the offline stage is what keeps the gate from degrading to nothing. A
58
+ clean offline pass is **necessary, never sufficient** — `explain` publishes
59
+ `runtimeAcceptance: "not-proven"` for exactly this reason. An offline `warn` is
60
+ an ABSTENTION and passes through to the device stage; only a `fail` rejects.
61
+
62
+ `meta.validation.stages[]` reports what each stage concluded, including a stage
63
+ that was skipped and why. A reader must always be able to tell "the device
64
+ accepted it" from "the device was never asked".
65
+
66
+ `--validate=false` disables **both** stages — one flag, one meaning. A bypass
67
+ that covered only the device stage would leave a caller stranded behind a
68
+ stage-1 defect with no escape at all, and the analyzer is demonstrably wrong in
69
+ both directions today (GH#355 is a live false accept, GH#351 a live false
70
+ reject). That is a reason for the flag to reach both stages, not a license to
71
+ use it: the rule below still holds — a wrong rejection is fixed in the
72
+ validator, with CHR evidence.
34
73
 
35
74
  - `retrieve` and other read-shaped calls validate against
36
75
  `/console/inspect request=syntax path=...,print` (path joined by commas) and
37
76
  attribute inspection. The path syntax (commas, no leading slash, last token
38
77
  is the verb) is part of the validator contract.
39
78
  - `execute` and other CLI-shaped calls validate with `:put [:parse "..."]` plus
40
- a semantic gate. **How `:parse` surfaces an error is transport-specific**
41
- (grounded on CHR 7.23.1): `:parse` *does* reject an unknown attribute
42
- (`bad parameter <name>`) and malformed CLI (`syntax error`), but over REST that
43
- text rides the **HTTP-200 `ret` value** (no error status), and over the
44
- **native API** `:put [:parse]` returns an opaque `*NN` handle that reveals
45
- nothing at all. centrs's REST/native syntax gate runs `:parse` but does not
46
- read the `ret`, so for those transports the unknown-attribute catch comes from
47
- the separate **`/console/inspect`** semantic gate (or the server's own
48
- write re-validation); the `:parse` round-trip there mainly backstops transport
49
- errors and the local quote preflight. Over **mac-telnet** the interactive
50
- console *prints* the `:parse` result, so centrs reads it and a single console
51
- `:parse` covers both syntax and the unknown-attribute (semantic) gate — no
52
- `/console/inspect` table needed. In every case a clean parse is necessary, not
53
- sufficient on its own; semantic validation is a distinct, transport-appropriate
54
- step (`/console/inspect`, the console `:parse` text, or server re-validation).
79
+ a semantic gate. Grounded on CHR 7.23.3 (GH#230), **`:parse` never throws** —
80
+ it returns a script value whose printed form carries the verdict. For a bare
81
+ menu path a clean parse prints `(evl /canonical/path)` with abbreviations
82
+ expanded; for script-shaped input (e.g. `:put …`, `:global …`, `where …`) the
83
+ accepted return echoes the script body and may contain arbitrary text such as
84
+ `expected`. Rejection forms are `(evl bad parameter <name> (line N column M)
85
+ /path)`, `(<%% bad command name <name> (line N column M) …)`, and the
86
+ line-anchored `expected … (line N column M)` (spellings: `expected end of
87
+ command`, `expected command name`, `expected input value`) / `syntax error
88
+ (line N column M)`. That text rides the **HTTP-200 `ret` value** (REST,
89
+ `as-string`), the native `as-string` `ret`, and the console's printed output
90
+ alike — so a single `:put [:parse ...]` covers both the syntax and the
91
+ **unknown-attribute** gate on every transport. centrs sends `as-string` and now
92
+ reads the `ret`/printed value (the previous gate ignored it, so it rejected
93
+ nothing). Because the accepted return echoes the body, the matcher must anchor
94
+ on the diagnostic shape (the `(<%%` wrapper, or a trailing `(line N column M)`)
95
+ and never on a bare English word — `expected` is also a real
96
+ `/ip/firewall/connection` field name.
97
+ That unknown-attribute check is **name-level only**: `:parse` does not validate
98
+ parameter *values* (`/ip/address/add address=not-an-ip` parses clean). So a
99
+ clean parse is necessary, not sufficient, and value-type plus the remaining
100
+ semantic validation stay distinct steps — `/console/inspect` or the server's
101
+ own re-validation on the write round-trip.
55
102
  - **Peer-measurement commands (`btest`) are exempt from this gate** — they issue
56
103
  no RouterOS command, so they validate *option-grammar* + the **EC-SRP5** auth
57
104
  exchange instead (the envelope, identity-resolution, and friendly-error
@@ -260,6 +307,43 @@ falls in `--near`'s radius **or** `--bbox`.
260
307
  by a distinct `--new-group` flag on `devices add`/`set`, so record selection and
261
308
  field-setting never collide on one flag.
262
309
 
310
+ #### Resolution providers
311
+
312
+ Target resolution has **two independent axes**, and centrs keeps them separate so
313
+ the resolver is generalized **once**, not per source (the umbrella plan is #174):
314
+
315
+ - **Axis A — selection** ("which devices"): the target-selection grammar above.
316
+ - **Axis B — connection-fact origin** ("where host/port/auth for a member come
317
+ from"): what a resolved member yields. Today: a **CDB record** or an ad-hoc
318
+ **literal** (+ the `__default__` ladder). Tomorrow: a live descriptor or an
319
+ inventory entry.
320
+
321
+ Sources fall into two kinds, and the distinction is normative:
322
+
323
+ - **Selector-source** — participates in the Axis-A grammar. **CDB** (now), and
324
+ **TikTOML** next (#137). Multiple selector-sources union into one selection;
325
+ the split-brain precedence between them is #137's problem, not resolved here.
326
+ - **Named-live-provider** — addressed *only* by an explicit named flag, **never**
327
+ matched by a selector; fanned out by repeating the flag. Because it owns an
328
+ **ephemeral** endpoint it also **bypasses** the settings/`__default__` ladder and
329
+ **conflicts** with `--host`/`--port`/`--username`/`--password`/`--ssh-key`. These
330
+ are properties of *live/ephemeral* providers, not of any one provider. **quickchr**
331
+ (`--quickchr <name>`, #134) is the first. A named-live-provider selection is
332
+ **exclusive** (v1): mixing it with `<router>` positionals or selector flags is
333
+ `usage/conflicting-flags` — one flag stays single-target, repeating it fans
334
+ out. Relaxing exclusivity later is additive.
335
+
336
+ Connection facts resolve into one neutral **per-service endpoint map**
337
+ (`src/resolver/service-endpoint.ts`: `via → ServiceEndpoint`, each carrying
338
+ availability, host, port, TLS, auth, and provenance). A **selector-source** is the
339
+ *degenerate single-endpoint* case — one record materialized lazily once `--via` is
340
+ known. A **named-live-provider** populates the map directly, since it genuinely
341
+ exposes different facts per transport. Providers must **not** be flattened to a
342
+ single host/port: that shape is the contract quickchr's descriptor and TikTOML's
343
+ `services` block both map onto, and flattening it forces exactly the re-plumb #174
344
+ exists to prevent. A `--via` whose service key a provider does not supply is a
345
+ typed unsupported-provider error, never a silent fallback.
346
+
263
347
  ### MCP surface: the CDB is the allowlist
264
348
 
265
349
  The MCP frontend (`commands/mcp/`) is an adapter over this core, not a new set of
@@ -352,22 +436,38 @@ default), so key auth is the normal sftp path.
352
436
 
353
437
  ## Done definition
354
438
 
355
- A feature is done when its **CHR integration test is green** against a real
356
- RouterOS CHR booted by `@tikoci/quickchr`. Not when code exists. Not when unit
357
- tests pass. Not when the spec says so. This is the **normative** done
358
- definition; `AGENTS.md` and
439
+ Done requires executable evidence for the capability being claimed. Device
440
+ operations and live analysis require **green CHR integration tests** against a
441
+ real RouterOS CHR booted by `@tikoci/quickchr`, via `bun run test:integration`.
442
+ Code existing or unit tests passing cannot establish live behavior. This is
443
+ the **normative** done definition; `AGENTS.md` and
359
444
  `.github/instructions/done-definition.instructions.md` point here (the latter
360
445
  carries the Copilot-workflow procedure for satisfying it).
361
446
 
362
- `docs/MATRIX.md` is the single source of truth for *what* is done and defines the
363
- cell states (`not-started` / `designed` / `coded` / `CHR-passed`). A cell
364
- advances only with the matching evidence in the same change, and the commit that
365
- advances it must include the test name and CHR version.
447
+ Offline analysis is `verified` when its applicable command examples and
448
+ contract checks pass without a router, its RouterOS semantic claims have
449
+ committed CHR-grounded evidence, and the command's baseline acceptance criteria
450
+ are satisfied. New or changed RouterOS semantics still require live grounding
451
+ with accepted and rejected controls plus a reproducible capture/replay path;
452
+ offline tests do not establish new device facts. Changes to shared execution
453
+ or transport behavior still require `bun run test:integration`. The offline
454
+ call itself never needs a router merely to satisfy a workflow label.
455
+
456
+ `docs/MATRIX.md` owns capability status and the selected work track. A
457
+ capability's acceptance criteria — like `explain`'s Offline baseline
458
+ acceptance — live in its `commands/<name>/README.md`; GitHub issues own the
459
+ tasks and dependencies that satisfy those criteria, not the criteria
460
+ themselves. Umbrella issues index that work rather than replacing capability
461
+ status. Status advances only with matching evidence in the same change.
462
+ Record test names, fixture provenance, and CHR versions where applicable.
463
+ Offline verification never advances a live protocol cell to `CHR-passed`.
366
464
 
367
465
  ## Boundaries
368
466
 
369
467
  centrs does:
370
468
 
469
+ - Analyze RouterOS source independently of a connection, preserving ordered
470
+ structure, source locations, evidence, and ambiguous/unknown readings.
371
471
  - Resolve devices/groups from explicit input, env, CDB, MNDP cache.
372
472
  - Choose protocols per the table above and configure them.
373
473
  - Validate before execute; re-validate server-side immediately before run.
@@ -380,7 +480,9 @@ centrs does:
380
480
 
381
481
  centrs does not:
382
482
 
383
- - Replace RouterOS syntax with high-level abstractions.
483
+ - Replace RouterOS syntax with high-level configuration helpers. Normalized
484
+ representations that preserve its semantics are in scope; reconstructing
485
+ final device state or predicting network behavior is not an offline parse claim.
384
486
  - Treat passive discovery (MNDP) as authoritative inventory.
385
487
  - Run write-shaped operations without an explicit target and validation policy.
386
488
  - Make generated output the hand-edited source of truth.
@@ -440,3 +542,9 @@ canonicalization only, never the structured-mode predicate. The gate contract,
440
542
  its pinning test, and the parser-vendoring preconditions are documented in
441
543
  `commands/execute/README.md` and the `canonicalizeExecuteCommand` doc-comment
442
544
  (`src/execute.ts`).
545
+
546
+ Richer schema-free analysis must distinguish **ambiguous** (multiple plausible
547
+ readings) from **unknown** (no safe reading) and abstain in both cases rather than
548
+ inventing a command, path, symbol class, or transport mapping. These explain-only
549
+ verdicts never widen or reinterpret the execution gate's `mode` or `writeShaped`
550
+ decision.
package/docs/MATRIX.md CHANGED
@@ -1,11 +1,15 @@
1
1
  # Matrix
2
2
 
3
- The product is a 2D grid of commands × protocols. This file is the **only**
4
- source of truth for what is done. No alpha gates, no milestones, no roadmap
5
- prose.
3
+ This file owns **capability status and the selected work track**. The
4
+ command × protocol grid covers live operations; offline analysis, frontends,
5
+ and peer measurement have separate entries below. A capability's acceptance
6
+ criteria live in its `commands/<name>/README.md`; GitHub issues own the tasks
7
+ and dependencies that satisfy them.
6
8
 
7
- Pick the highest-priority cell that is not `CHR-passed`. That is the next
8
- work. See `docs/CONSTITUTION.md` for the cell-state definitions.
9
+ Start with [Current priority](#current-priority), then the command's README,
10
+ examples, and linked issue. Verification follows `docs/CONSTITUTION.md` →
11
+ Done definition. A completed implementation slice is not proof that the whole
12
+ capability is complete.
9
13
 
10
14
  ## Cell states
11
15
 
@@ -15,10 +19,15 @@ work. See `docs/CONSTITUTION.md` for the cell-state definitions.
15
19
  | `not-started` | No code and no design |
16
20
  | `designed` | `commands/<name>/README.md` describes intent and flags |
17
21
  | `coded` | Implementation exists in `src/` |
22
+ | `verified` | Offline capability meets its command README's baseline acceptance criteria in full (examples, contract checks, measured/consumer gates, and CHR-grounded evidence). |
18
23
  | `CHR-passed` | Every example in `commands/<name>/examples.md` is green on CHR |
19
24
 
20
25
  A cell advances only with the matching evidence in the same change.
21
26
 
27
+ `verified` applies to offline capabilities; it does not advance a live protocol
28
+ cell. Existing `CHR-passed` fixture-backed `devices`/`settings` entries retain
29
+ their command-specific evidence contracts.
30
+
22
31
  ## Grid
23
32
 
24
33
  | Command | rest-api | native-api | ssh | mac-telnet | snmp | mndp | romon | winbox-terminal |
@@ -31,13 +40,14 @@ A cell advances only with the matching evidence in the same change.
31
40
  | devices | — | — | — | — | — | — | — | — |
32
41
  | discover | — | — | — | — | — | `CHR-passed` | — | — |
33
42
  | check | `designed` | `designed` | `designed` | `designed` | `not-started` | `not-started` | `not-started` | `not-started` |
43
+ | explain | `designed` | `designed` | — | — | — | — | — | — |
34
44
  | settings | — | — | — | — | — | — | — | — |
35
45
 
36
46
  ## Status pointers
37
47
 
38
48
  Detailed command contracts live in `commands/<name>/README.md`; example evidence
39
- lives in `commands/<name>/examples.md` and the named integration tests. Keep this
40
- section short enough that the grid remains the status surface.
49
+ lives in `commands/<name>/examples.md` and the named tests. Keep task history
50
+ and implementation sequencing in the linked issues.
41
51
 
42
52
  - `devices` is transport-less, so its row stays `—`; command state is
43
53
  `CHR-passed` by fixture-backed integration tests in `test/integration/devices.test.ts`.
@@ -59,6 +69,13 @@ section short enough that the grid remains the status surface.
59
69
  `winbox-terminal` stay `not-started`. Its per-host fan-out is the machinery the
60
70
  IP-scan discovery (#149) will iterate; the L2-default timing evidence (#136) is
61
71
  recorded in the README before the cells advance past `designed`.
72
+ - `explain`'s two `designed` protocol cells are the future live probes
73
+ (`/console/inspect` + `:parse`, #236). The live grammar
74
+ (`explain <router> '<input>'`) is parsed and refused with
75
+ `usage/not-implemented`, never degraded to an offline run. Offline analysis
76
+ has its own capability entry below. The generated structure tables
77
+ `src/explain/menus.ts` and `src/explain/catalog.ts` keep analysis offline;
78
+ their ownership and regeneration contracts live in `commands/explain/README.md`.
62
79
  - `transfer / ssh` means the SFTP-backed transfer method. Deferred file-transfer
63
80
  methods such as `scp`, `fetch`, and `ftp` are tracked in
64
81
  `commands/transfer/README.md`; `fetch` is not a grid column.
@@ -71,9 +88,20 @@ section short enough that the grid remains the status surface.
71
88
  - Transport wire-format and delivery caveats belong in `src/protocols/*.ts`
72
89
  module headers. Do not expand this file with per-packet or per-router findings.
73
90
 
74
- Beyond the grid: post-`CHR-passed` hardening, ergonomics, and research work is
75
- tracked in GitHub issues (`dry-july` and successors). Those issues may point back
76
- to this matrix for current status, but they are not additional cell states.
91
+ ### Offline analysis
92
+
93
+ This capability opens no protocol connection. It is usable independently and
94
+ provides shared analysis for future integrations across the command grid.
95
+
96
+ | Capability | State | Contract and evidence | Work |
97
+ | ---------- | ----- | --------------------- | ---- |
98
+ | explain / offline baseline | `verified` | `commands/explain/README.md` → Offline baseline acceptance; offline examples in `test/unit/explain.test.ts`, focused `test/unit/explain-*` suites, and CHR captures under `test/fixtures/explain/`. The token partition is scored against device `highlight` by `test/unit/explain-highlight-agreement.test.ts` and `test/fixtures/explain/highlight-agreement.json`. The public entry is `@tikoci/centrs/explain`; its dependency boundary and executed browser-consumer proof are gated by `test/unit/explain-browser-entry.test.ts` (`bun run explain:browser-consumer`). #322's fixed-size 16× depth guard and deterministic structural-work guards close the bounded-execution criterion. | #90 indexes correctness, token agreement (#264/#263), consumer verification, and bounded execution (#322). |
99
+
100
+ The CLI/library expose structure, diagnostics, token partitions, symbols, and
101
+ flow-sensitive value facts. `verified` records the offline acceptance evidence;
102
+ it is not a parser-coverage score and does not advance either live protocol cell.
103
+ Live probes (#236), MCP response alignment (#223), and future configuration
104
+ policies are distinct work, not prerequisites for using offline analysis.
77
105
 
78
106
  ### Transport-base readiness
79
107
 
@@ -89,13 +117,13 @@ single home for wire-format, auth, and delivery facts.
89
117
 
90
118
  ### Frontend surfaces (orthogonal to the command grid)
91
119
 
92
- The grid tracks the core (commands × protocols). Frontends are adapters over that
93
- core and carry their own state, tracked here:
120
+ Frontends adapt the shared analysis and device-operation core and carry their
121
+ own state, tracked here:
94
122
 
95
123
  | Surface | State | Spec | Notes |
96
124
  | ------- | ----- | ---- | ----- |
97
125
  | ts-api | `CHR-passed` | `src/index.ts` | Root TypeScript library surface; everything else adapts it. |
98
- | cli | `coded` | `src/cli/` | `retrieve`/`execute`/`transfer`/`terminal`/`devices`/`discover`/`btest` wired. |
126
+ | cli | `coded` | `src/cli/` | Every command in `cliCommands` (`src/cli.ts`) is wired: `retrieve`/`execute`/`api`/`transfer`/`terminal`/`explain`/`devices`/`discover`/`btest`/`mcp`/`settings`, plus the `upload`/`download` shortcuts. |
99
127
  | mcp | `CHR-passed` | `commands/mcp/` | Scoped-verb stdio MCP server; details in `commands/mcp/README.md` and `test/integration/mcp.test.ts`. |
100
128
  | tui | `not-started` | `src/tui.ts` | Stub. |
101
129
  | proxy | `not-started` | `src/webproxy.ts` | Stub. |
@@ -120,11 +148,57 @@ its own capability axis and is exempt from the RouterOS command-validation gate.
120
148
  Wire, auth, TCP/UDP, and CI caveats live in `commands/btest/README.md` and the
121
149
  `src/protocols/btest*.ts` / `src/protocols/ec-srp5.ts` module headers.
122
150
 
123
- ## Priority order
124
-
125
- Do not start a later item until the earlier cell or dependency checkpoint has
126
- matching evidence. The grid above is the live status; per-item detail lives in
127
- the linked `commands/<name>/`.
151
+ ## Current priority
152
+
153
+ **Tightening validation as a whole** is the selected track, in the three-step
154
+ order agreed on [#236](https://github.com/tikoci/centrs/issues/236):
155
+
156
+ 1. **Done** — fix the offline false rejects so the analyzer can be trusted as a
157
+ gate (#347/#348, shipped in #349; the measured corpus false-reject count went
158
+ 4 → 1).
159
+ 2. **Done** — front the `execute` / `api` device preflight with the offline
160
+ analyzer (#354). The gate is now two-stage; the contract is in
161
+ [`docs/CONSTITUTION.md` → Validation is the product](CONSTITUTION.md#validation-is-the-product).
162
+ 3. **Next** — **live `explain` (#236)**, as stage two of a gate that now exists.
163
+ Start with [the #90 task index](https://github.com/tikoci/centrs/issues/90),
164
+ then follow [`commands/explain/README.md`](../commands/explain/README.md) and
165
+ the live examples in
166
+ [`commands/explain/examples.md`](../commands/explain/examples.md).
167
+
168
+ The offline baseline stays `verified`: #264 closed the measured token vocabulary
169
+ and #322 closed the final bounded-execution gap, including the public depth-axis
170
+ guard.
171
+
172
+ The live pass is the device-backed cross-check on the offline result, not a
173
+ replacement for it. It adds read-only `/console/inspect` and `:parse` evidence,
174
+ the describe/completion facets, and the live `observedType`/`schemaType`
175
+ producers owned with #225. REST and native-api cells advance only when their
176
+ applicable examples pass on CHR via `bun run test:integration`.
177
+
178
+ Open offline follow-ups remain explicit but do not reopen the verified baseline.
179
+ Issue #211 retains the fail-closed unlisted-path decision; #272 owns
180
+ non-authoritative `.scratch/` citation cleanup. #350 (the broad optional `=`,
181
+ schema-dependent and so belonging to step 3), #351 (an unresolvable menu that
182
+ abstains at 2-deep nesting but hard-fails at 3-deep) and #355 (`'` is not a
183
+ RouterOS token — a false ACCEPT the corpus join cannot see) are bounded analyzer
184
+ defects that step 2 now surfaces through the `execute`/`api` gate. #336 and #342 are complete: the
185
+ corpus is repinned to three *complete* builds (long-term 7.23.5, stable 7.24.2,
186
+ development 7.25beta3), and the operator sweep and highlight slice are re-swept
187
+ onto those same builds — plus 7.21.5 retained as an age anchor — so the operator
188
+ axis and the token partition are scored on the same firmware. The #335
189
+ source refresh pins both architectures of those versions beside the 7.10.2 /
190
+ 7.16 history anchors and accepts the current CLI-Reference drift. Issue #223 is
191
+ a separate MCP response migration. If live evidence finds a wrong offline
192
+ assertion, give that defect a bounded issue and matching CHR-grounded controls
193
+ rather than folding it silently into the live implementation.
194
+
195
+ Unfinished SNMP, RoMON, TUI, or other protocol/frontend cells do not preempt this
196
+ track. Change this selection when the maintainer selects another track.
197
+
198
+ ### Protocol backlog order
199
+
200
+ Retained for protocol work when selected; this is not a dependency chain for
201
+ offline analysis. Per-item detail lives in the linked `commands/<name>/`.
128
202
 
129
203
  1. **retrieve / rest-api** — `CHR-passed` (`commands/retrieve/`).
130
204
  2. **CDB resolution + metadata overrides** — `CHR-passed` (`commands/devices/`).
@@ -47,6 +47,9 @@ Codes are slash-namespaced `family/slug`. The families and where they originate:
47
47
  - `input/*` — malformed caller input (paths, MACs, arguments).
48
48
  - `internal/*` — centrs bugs / unreachable states.
49
49
  - `mndp/*` — MNDP wire codec and listener (`src/data/mndp.ts`).
50
+ - `quickchr/*` — the quickchr named-live-provider (`--quickchr <name>`): optional
51
+ package availability, descriptor version support, and machine lookup/liveness
52
+ (`src/resolver/quickchr-provider.ts`).
50
53
  - `routeros/*` — RouterOS-surfaced faults mapped by the grounded vocabulary in
51
54
  `src/core/routeros-errors.ts` (REST `detail` and native-api `!trap`, identical
52
55
  on CHR 7.23).
@@ -0,0 +1,22 @@
1
+ # `quickchr/machine-not-found`
2
+
3
+ No quickchr machine is registered under the name passed to `--quickchr <name>`.
4
+
5
+ ## Typical trigger
6
+
7
+ The name does not match any machine quickchr knows about — a typo, a machine
8
+ that was never created, or one that has been removed. centrs raises this both
9
+ when `QuickCHR.get(name)` returns nothing and when the descriptor call reports
10
+ quickchr's own `MACHINE_NOT_FOUND`.
11
+
12
+ ## Fix
13
+
14
+ - List the registered machines with `quickchr list` and pass an existing name.
15
+ - Create/start the machine first if it does not exist yet (`quickchr start
16
+ <name>`).
17
+ - Names are exact — check for typos and case.
18
+
19
+ If the machine exists but is not running, you will see
20
+ [`quickchr/machine-stopped`](./machine-stopped.md) instead.
21
+
22
+ See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) → Resolution providers.
@@ -0,0 +1,20 @@
1
+ # `quickchr/machine-stopped`
2
+
3
+ The named quickchr machine exists but is not running, so it has no live
4
+ connection facts (host, forwarded ports, credentials) for centrs to dial.
5
+
6
+ ## Typical trigger
7
+
8
+ `--quickchr <name>` named a real machine, but its VM is stopped. quickchr only
9
+ knows the forwarded loopback ports and running-state auth while the machine is
10
+ up, so a stopped machine cannot yield a `ServiceEndpointMap`.
11
+
12
+ ## Fix
13
+
14
+ - Start it: `quickchr start <name>`, then retry `--quickchr <name>`.
15
+ - Confirm state with `quickchr list` / `quickchr inspect <name>`.
16
+
17
+ This is a per-target failure: in a fan-out over several `--quickchr` targets,
18
+ one stopped machine fails only its own row, not the whole run.
19
+
20
+ See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) → Resolution providers.
@@ -0,0 +1,27 @@
1
+ # `quickchr/package-unavailable`
2
+
3
+ The optional `@tikoci/quickchr` package is not installed, so a `--quickchr
4
+ <name>` target cannot be resolved into connection facts.
5
+
6
+ ## Typical trigger
7
+
8
+ You passed `--quickchr <name>` (or otherwise resolved a quickchr target) in an
9
+ install that does not have `@tikoci/quickchr`. centrs declares it as an
10
+ **optional dependency** and loads it through a runtime import, so a normal
11
+ install without it works for every non-quickchr path and only fails here.
12
+
13
+ ## Fix
14
+
15
+ - Install the package: `bun add @tikoci/quickchr` (0.4.5+ — 0.4.4 introduced
16
+ the descriptor v1 API but advertises dead TLS endpoints on a stock CHR;
17
+ see quickchr#95).
18
+ - Or bypass quickchr entirely and target the device directly with `--host`
19
+ (plus `--username`/`--password`/`--ssh-key` or a CDB record).
20
+
21
+ This code is raised **only** when the module is genuinely not found. An
22
+ installed package that throws while loading reports
23
+ [`quickchr/unsupported`](./unsupported.md) instead, so the two are never
24
+ confused.
25
+
26
+ See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) → Resolution providers for
27
+ the named-live-provider contract.
@@ -0,0 +1,35 @@
1
+ # `quickchr/unsupported-via`
2
+
3
+ The quickchr machine resolved fine, but the chosen `--via` has no usable
4
+ service on it. The descriptor is per-service; centrs reads exactly the
5
+ endpoint for the chosen protocol and never falls back to another transport.
6
+
7
+ ## Typical trigger
8
+
9
+ Any of:
10
+
11
+ - The `--via` is a protocol the descriptor does not forward. quickchr v1
12
+ forwards `rest-api`, `native-api`, and `ssh` only — `mac-telnet`, `romon`,
13
+ `btest`, and `winbox-terminal` are never available on a quickchr target.
14
+ - The service exists but quickchr marked it `available: false` (the error
15
+ carries the provider's `unavailableReason`).
16
+ - `--via ssh` (or `transfer --via sftp`/`scp`): the SSH endpoint reports no
17
+ batch-capable auth mode (`batchModes` is empty), so a non-interactive
18
+ handoff is impossible. centrs fails typed here rather than prompt for a
19
+ password or silently pick another transport.
20
+
21
+ ## Fix
22
+
23
+ - Pick one of the machine's live services; the error's `availableServices`
24
+ context lists them (typically `--via rest-api` / `--via native-api` /
25
+ `--via ssh`).
26
+ - For the SSH/sftp gate, verify key-based login works from quickchr's side
27
+ (`quickchr ssh <name>`); once quickchr has a verified key, the descriptor
28
+ advertises `private-key` in `batchModes` and centrs will use it.
29
+ - If the machine should be forwarding the service but is not, inspect it with
30
+ `quickchr inspect <name>`.
31
+ - To use a transport quickchr does not forward, target the device directly
32
+ (`--host`/CDB record) instead of `--quickchr`.
33
+
34
+ See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) → Resolution providers for
35
+ why a provider's missing service is a typed error, never a silent fallback.
@@ -0,0 +1,35 @@
1
+ # `quickchr/unsupported`
2
+
3
+ The installed `@tikoci/quickchr` cannot give centrs a usable descriptor: it is
4
+ too old to expose the descriptor API, it failed to load, or it emitted a
5
+ descriptor centrs cannot read.
6
+
7
+ ## Typical trigger
8
+
9
+ Any of:
10
+
11
+ - The installed quickchr predates the `QuickCHR.get(name).descriptor()` API
12
+ (before 0.4.4), so the descriptor entry point is missing.
13
+ - The package is installed but throws while loading (a broken/partial install),
14
+ which is a real fault — distinct from
15
+ [`quickchr/package-unavailable`](./package-unavailable.md), which means "not
16
+ installed at all".
17
+ - The descriptor is malformed or pre-v1: no numeric `descriptorVersion`, no
18
+ `services` map, or missing `name`/`version`/`arch`. centrs rejects it rather
19
+ than crash on a later field access or silently treat `services` as empty
20
+ (which would hide a provider contract violation).
21
+ - The descriptor version is **newer** than centrs supports
22
+ (`descriptorVersion` > 1); a newer quickchr speaks a schema this centrs
23
+ predates.
24
+
25
+ ## Fix
26
+
27
+ - Upgrade quickchr to a release that emits descriptor v1: `bun add
28
+ @tikoci/quickchr` (0.4.5+ — 0.4.4 has the API but a descriptor endpoint
29
+ bug, quickchr#95).
30
+ - If the descriptor looks wrong, inspect the machine directly with `quickchr
31
+ inspect <name>` — it may be in a transient state.
32
+ - If the descriptor is newer than centrs supports, upgrade centrs.
33
+
34
+ See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) → Resolution providers for
35
+ the descriptor contract and the supported version boundary.
@@ -0,0 +1,26 @@
1
+ # `routeros/version-unsupported`
2
+
3
+ The device's RouterOS version does not provide the operation this command
4
+ needs. centrs pre-flighted the generated command against the device's known
5
+ version (a live read, or the stored CDB `version=` fact) and refused it before
6
+ touching the router, instead of letting the device return a raw protocol
7
+ error.
8
+
9
+ Distinct from `routeros/unsupported-capability`, where the chosen *protocol*
10
+ cannot carry the operation at all regardless of device version.
11
+
12
+ ## Context fields
13
+
14
+ - `capability` — stable slug of the missing operation.
15
+ - `version` — the device version that was checked.
16
+ - `versionSource` — `live`, `cdb-fact`, or `caller`. A `cdb-fact` version is
17
+ stale-by-nature; it was recorded by `discover --save` or `devices --check`.
18
+ - `supported` — the version windows where RouterOS provides the operation
19
+ (inclusive `min`, optional exclusive `maxExclusive` for backport windows).
20
+
21
+ ## Fix
22
+
23
+ Upgrade RouterOS to one of the versions listed in `supported`. If the device
24
+ was already upgraded and the check used a stale stored fact
25
+ (`versionSource: "cdb-fact"`), refresh the record's `version=` comment fact
26
+ and retry.
@@ -0,0 +1,29 @@
1
+ # `transport/provider-trust`
2
+
3
+ A warning (not an error): the target came from a named-live-provider
4
+ (`--quickchr <name>`), and centrs relaxed peer verification to use its
5
+ endpoint.
6
+
7
+ ## Why this happens
8
+
9
+ quickchr's descriptor prefers the VM's TLS forwards (`https` / `api-ssl`),
10
+ whose certificate is the CHR's own self-signed one, and its SSH endpoint's
11
+ host key is regenerated per VM. The descriptor carries no trust material, so
12
+ strict verification would reject every quickchr target and force `--insecure`
13
+ ceremony onto every lab call.
14
+
15
+ Instead, centrs treats the endpoint as **trusted by provenance**: quickchr
16
+ vouches for a loopback-forwarded port (`127.0.0.1:<port>`) of a VM the user
17
+ owns, so peer verification is relaxed for exactly that connection — with
18
+ `provider` provenance on `meta.settings.insecure` and this warning on the
19
+ envelope, never silently.
20
+
21
+ ## When to care
22
+
23
+ - If this warning appears on a target you did **not** select with
24
+ `--quickchr`, that is a bug — report it.
25
+ - The relaxation never applies to CDB, literal, or env-resolved targets;
26
+ those still require an explicit `--insecure` (which raises
27
+ [`transport/insecure-trust`](./insecure-trust.md) instead).
28
+
29
+ See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) → Resolution providers.