@tikoci/centrs 0.1.4 → 0.1.7

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 (85) hide show
  1. package/CHANGELOG.md +417 -1
  2. package/GLOSSARY.txt +40 -1
  3. package/README.md +16 -8
  4. package/commands/AGENTS.md +7 -4
  5. package/commands/api/AGENTS.md +13 -1
  6. package/commands/api/README.md +50 -10
  7. package/commands/api/examples.md +100 -2
  8. package/commands/devices/README.md +10 -1
  9. package/commands/devices/examples.md +33 -2
  10. package/commands/execute/README.md +68 -25
  11. package/commands/execute/examples.md +168 -7
  12. package/commands/explain/README.md +1206 -155
  13. package/commands/explain/examples.md +99 -3
  14. package/commands/mcp/README.md +9 -5
  15. package/commands/mcp/examples.md +2 -2
  16. package/commands/retrieve/examples.md +20 -0
  17. package/commands/terminal/examples.md +7 -1
  18. package/docs/CLI.md +3 -3
  19. package/docs/CONSTITUTION.md +83 -21
  20. package/docs/MATRIX.md +89 -52
  21. package/docs/errors/cdb/create-failed.md +20 -0
  22. package/docs/errors/cdb/file-exists.md +23 -0
  23. package/docs/errors/input/invalid-target.md +25 -3
  24. package/docs/errors/routeros/invalid-value.md +3 -2
  25. package/docs/errors/transport/cancel-unacknowledged.md +24 -0
  26. package/docs/errors/validation/syntax.md +29 -4
  27. package/package.json +13 -2
  28. package/src/api-fanout.ts +17 -4
  29. package/src/api.ts +346 -75
  30. package/src/cli/api.ts +1 -1
  31. package/src/cli/btest.ts +1 -1
  32. package/src/cli/devices.ts +16 -3
  33. package/src/cli/explain.ts +1 -1
  34. package/src/cli/terminal.ts +1 -1
  35. package/src/core/envelope.ts +53 -0
  36. package/src/core/error-catalog.ts +16 -1
  37. package/src/core/execute-command.ts +455 -0
  38. package/src/core/inspect.ts +58 -26
  39. package/src/core/routeros-errors.ts +66 -0
  40. package/src/core/version.ts +4 -0
  41. package/src/core/yaml.ts +86 -0
  42. package/src/data/winbox-cdb-write.ts +22 -8
  43. package/src/devices.ts +142 -28
  44. package/src/execute-fanout.ts +19 -5
  45. package/src/execute.ts +247 -295
  46. package/src/explain/arg-tokens.ts +62 -25
  47. package/src/explain/args.ts +398 -52
  48. package/src/explain/blocks.ts +54 -11
  49. package/src/explain/brace-tokens.ts +4 -3
  50. package/src/explain/catalog.ts +42 -44
  51. package/src/explain/coordinates.ts +159 -0
  52. package/src/explain/defects.ts +2 -1
  53. package/src/explain/escape-tokens.ts +77 -0
  54. package/src/explain/highlight-projection.ts +308 -0
  55. package/src/explain/menus.ts +9 -5
  56. package/src/explain/operator-tokens.ts +13 -7
  57. package/src/explain/operators.ts +27 -20
  58. package/src/explain/path-tokens.ts +1 -1
  59. package/src/explain/pathresolve.ts +358 -113
  60. package/src/explain/quoted-string.ts +110 -20
  61. package/src/explain/scope-brace.ts +175 -22
  62. package/src/explain/segment.ts +429 -21
  63. package/src/explain/separator.ts +187 -0
  64. package/src/explain/split-owner.ts +194 -0
  65. package/src/explain/string-tokens.ts +14 -4
  66. package/src/explain/symbol-values.ts +209 -173
  67. package/src/explain/symbols.ts +21 -1
  68. package/src/explain/transport.ts +17 -0
  69. package/src/explain/value-tokens.ts +15 -12
  70. package/src/explain/verbsplit.ts +165 -23
  71. package/src/explain/write.ts +47 -6
  72. package/src/explain.ts +522 -72
  73. package/src/index.ts +21 -2
  74. package/src/mcp/server.ts +2 -1
  75. package/src/mcp/tools.ts +1 -1
  76. package/src/offline-gate.ts +148 -0
  77. package/src/protocols/adapter.ts +12 -1
  78. package/src/protocols/mac-telnet-console.ts +6 -4
  79. package/src/protocols/native-api.ts +29 -3
  80. package/src/resolver/target.ts +32 -12
  81. package/src/retrieve-fanout.ts +1 -1
  82. package/src/retrieve.ts +18 -63
  83. package/src/settings.ts +2 -1
  84. package/src/transfer-fanout.ts +1 -1
  85. package/src/transfer.ts +1 -1
@@ -4,9 +4,10 @@ Each numbered example is an executable spec. Once `explain` is implemented,
4
4
  offline examples run under `test/unit/explain.test.ts` and live examples under
5
5
  `test/integration/explain.test.ts` (rest-api) and
6
6
  `test/integration/native-api-explain.test.ts` (native-api) against a CHR booted by
7
- `@tikoci/quickchr` — one assertion per example (example N ↔ assertion N).
8
- The offline examples are **green** as of #202c-2, including 1, 2, 6 and 23's
9
- `transport`/`--curl` assertions. The live examples are still the **target**:
7
+ `@tikoci/quickchr` — one named test per example (example N ↔ named test N); a
8
+ named test may carry several assertions. The offline examples are **green**
9
+ as of #202c-2, including 1, 2, 6 and 23's `transport`/`--curl` assertions.
10
+ The live examples are still the **target**:
10
11
  those cells are `designed`, and flag and field names track the ratified surface
11
12
  in `README.md`.
12
13
 
@@ -604,3 +605,98 @@ No members are emitted for a bare comma run, because whether it splits at all is
604
605
  the schema's answer. The `(1,2)` spelling is different — its delimiters prove
605
606
  the array — so `servers=(1.1.1.1,8.8.8.8)` yields the array plus two `["ip"]`
606
607
  members.
608
+
609
+ ### 29. A second command-shaped run is a missing separator, not an operand
610
+
611
+ ```bash
612
+ centrs explain '/ip/address add interface=ether1 /ip/route add gateway=192.168.88.1' --json
613
+ ```
614
+
615
+ `data.verdict` is `fail` with one
616
+ `explain/canonicalizer/missing-statement-separator` **error** spanning the whole
617
+ second run (`[33,46)` — `/ip/route add`), whose message says to insert `;` or a
618
+ newline before it.
619
+
620
+ The source stays **one** statement: `resolution` is `"resolved"` on
621
+ `/ip/address add`, because that head really was read and the device's own
622
+ `highlight` agrees. What is withdrawn is the reading the run contradicts —
623
+ `command.args` is absent, `arguments.read` is `false`, and `transport.
624
+ classification` is `"unknown"`, so no `centrs execute` line is offered for bytes
625
+ the same result rejects. `data.values.occurrences` is cut at the same byte: it
626
+ keeps `interface` and drops `gateway`, which the device reads as the value of
627
+ `/ip/address/add`'s own `address=` rather than as an attribute name.
628
+
629
+ The rule is a known **menu path** followed by a console **verb**, never "a `/`
630
+ after the verb": `/file remove /flash/skins/foo.html` is legal and lowers to
631
+ `numbers=/flash/skins/foo.html`. It abstains on a menu path with no verb after
632
+ it and on an operand no structure table lists, both of which draw the device's
633
+ generic `bad parameter` and need per-menu positional schema to decide.
634
+
635
+ ### 29b. The same commands, separated, read as two
636
+
637
+ ```bash
638
+ centrs explain '/ip/address add interface=ether1; /ip/route add gateway=192.168.88.1' --json
639
+ ```
640
+
641
+ `data.verdict` is `pass` with no diagnostics, two statements, and each keeps its
642
+ own `command.args`. The newline spelling is identical.
643
+
644
+ ### 30. A known menu path with arguments does not get an invented verb
645
+
646
+ ```bash
647
+ centrs explain '/ip/firewall/address-list name=ytkids timeout=1h' --json
648
+ ```
649
+
650
+ One statement with `resolution: "ambiguous"` and no `command` at all, plus an
651
+ `explain/canonicalizer/ambiguous-statement` **warning** over the whole
652
+ statement naming both premises: `/ip/firewall/address-list` is a known menu,
653
+ and `address-list` is not a console verb. `data.verdict` is `warn`, so the
654
+ default `--fail-on error` still exits `0` — offline is saying it cannot read
655
+ this, not that RouterOS rejects it.
656
+
657
+ It used to answer `resolution: "resolved"`, `command.path: "/ip/firewall"`,
658
+ `command.verb: "address-list"` — a verb assembled out of a path segment. The
659
+ spaced spelling `/ip firewall address-list name=x` was worse, promoting
660
+ `firewall` and reporting the menu as `/ip`. Adding the verb resolves it again
661
+ (`/ip/firewall/address-list add name=ytkids` is `resolved`/`command`), and the
662
+ bare path without arguments is still `resolved`/`menu` (example 18b's rule).
663
+
664
+ ### 31. A command substitution inside a string keeps its own tokens
665
+
666
+ ```bash
667
+ centrs explain ':put "$[/ip address print as-value]"' --tokens --json
668
+ ```
669
+
670
+ `data.structure.subcommands[0]` resolves `/ip/address` verb `print` — it always
671
+ did — and `data.tokens[]` now agrees with it: `dir` over `/ip` and `address`,
672
+ `cmd` over `print`, with the quote, the `$[`, the separators and the trailing
673
+ `as-value]"` left as `string`. The whole quoted run used to be one opaque
674
+ `string` token, so an editor rendering the partition painted a command centrs
675
+ had already read as flat text.
676
+
677
+ Byte coverage does not change — the partition is still total and gapless — only
678
+ which fill owns those bytes. The `$` sigil and the `[` / `]` delimiters stay
679
+ `string`: #264 B5 kept them there because `structure.subcommands` already
680
+ publishes the resolution and its `innerSpan` locates the brackets around it.
681
+
682
+ ### 32. A block binder the envelope cannot publish is still located
683
+
684
+ ```bash
685
+ centrs explain ':if ($x = 1) do={ :put "hi" }' --tokens --json
686
+ ```
687
+
688
+ `data.structure.statements[0].arguments` is `{ read: false, why: "a
689
+ substitution or expression value" }` — the strict reading is all-or-nothing and
690
+ a runnable request cannot be rendered from a half-read argument list, so the
691
+ envelope publishes no `ExplainArgumentToken` here at all. `data.tokens[]` still
692
+ locates the argument: `arg` over `do`, `arg-sep` over its `=`, `brace` over the
693
+ `{`.
694
+
695
+ That is why #264 B5 gave the `=` its own class. A consumer holding only the
696
+ envelope would otherwise have to derive the name/value boundary from
697
+ `arguments[].valueSpan.start - 1`, and for 30.8% of the corpus's `=` runs —
698
+ `do=` first among them — there is no published `valueSpan` to derive it from.
699
+
700
+ The same command also shows the two `=` bytes staying apart: the one in
701
+ `($x = 1)` is a comparison and comes back `operator`, decided by fill order
702
+ rather than by a scan for the byte.
@@ -80,8 +80,8 @@ benchmarked RouterOS agent support. Load-bearing findings that shape this design
80
80
  rejected. RouterOS 7.23 no longer reproduces that exact `blackhole=yes` case,
81
81
  but the lesson stands: schema-presence is not parser-acceptance.
82
82
  `:put [:parse "<cmd>"]` runs the real parser, and centrs runs **both** `:parse`
83
- and `/console/inspect`, so the MCP exposes validation as a first-class
84
- **dry-run** tool (`centrs_validate`) that never mutates.
83
+ and `/console/inspect request=child`, so the MCP exposes validation as a
84
+ first-class **dry-run** tool (`centrs_validate`) that never mutates.
85
85
  - **Stateful intents need device-validate-before-apply.** Gold using
86
86
  `interface=ether2` fails on a CHR exposing only `ether1`. Pair `execute` with
87
87
  cheap readback (`retrieve`).
@@ -104,7 +104,7 @@ benchmarked RouterOS agent support. Load-bearing findings that shape this design
104
104
  | Tool | Core mapping | Reads/Writes | Target source |
105
105
  | ---- | ------------ | ------------ | ------------- |
106
106
  | `centrs_explain` | `canonicalizeExecuteCommand` | offline, none | none (string only) |
107
- | `centrs_validate` | execute validation only (`:parse` + `/console/inspect`), **no run** | read-only | CDB |
107
+ | `centrs_validate` | execute validation only, **no run**: offline analysis, then the device stage — `:put [:parse ...]` alone over ssh/mac-telnet, `:parse` + `/console/inspect` for structured REST/native | read-only | CDB |
108
108
  | `centrs_retrieve` | `retrieve` / `retrieveGroup` | read-only | CDB |
109
109
  | `centrs_execute` | `execute` | read or write | CDB |
110
110
  | `centrs_devices` | `devices` (`list`/`show`/`groups`/`add`/`edit`/`set`/`remove`) | CDB read/write | CDB file |
@@ -118,11 +118,15 @@ benchmarked RouterOS agent support. Load-bearing findings that shape this design
118
118
  - `centrs_validate` is the headline distilled from the bench: a dry-run that runs
119
119
  the real parser plus schema inspection against a registered target and returns
120
120
  the validation envelope **without executing**. Use it to reject schema-invalid
121
- commands before any write.
121
+ commands before any write. It shares `execute`'s two-stage gate (constitution:
122
+ validation is the product, GH#354), so a syntax fault comes back from the
123
+ offline stage with a byte span and without contacting the target at all, and
124
+ `meta.validation.stages[]` says which stage spoke.
122
125
  - `centrs_retrieve` mirrors `retrieve`, including attribute projection,
123
126
  `listAttributes`, and `group` fanout.
124
127
  - `centrs_execute` mirrors `execute`. Read-shaped commands run subject only to
125
- CDB membership; write-shaped add/set/remove are gated by the CDB policy below.
128
+ CDB membership; every write-shaped command is gated by the CDB policy below,
129
+ including selector/positional script forms and non-CRUD mutation verbs.
126
130
  Its MCP `destructiveHint` is **conservative — always `true`**, even though the
127
131
  tool also serves read-shaped commands: a single execute tool cannot vary the
128
132
  static annotation by argument, so it advertises the worst case and lets the
@@ -65,8 +65,8 @@ matters for the bench gold-bug in example 4.
65
65
  ```
66
66
 
67
67
  Envelope: `ok: false`, `error.code = "validation/unknown-attribute"` — the
68
- `/console/inspect` semantic gate rejects the unknown attribute and **no route is
69
- created**. This proves the validate tool fails closed on bad input.
68
+ `/console/inspect request=child` semantic gate rejects the unknown attribute and
69
+ **no route is created**. This proves the validate tool fails closed on bad input.
70
70
 
71
71
  > Bench note: the bench's original gold-bug command
72
72
  > `/ip/route/add dst-address=10.99.0.0/24 blackhole=yes` (where `/console/inspect`
@@ -84,6 +84,18 @@ Rejected before any network call, with `usage/conflicting-flags`.
84
84
  centrs retrieve $R /system/resource --attribute uptime --all-attributes --username $U --password $P --json
85
85
  ```
86
86
 
87
+ ### 20. Comma-list --attributes on any singleton
88
+
89
+ Singletons are recognized from `/console/inspect` — their `get` takes no
90
+ `number` argument — so every singleton validates against its `get value-name`
91
+ properties, not against `print`'s flags (#377).
92
+
93
+ ```bash
94
+ centrs retrieve $R /tool/romon --attributes enabled,id --username $U --password $P --json
95
+ ```
96
+
97
+ Envelope: `data` is an object containing only `enabled` and `id`.
98
+
87
99
  ## --list-attributes
88
100
 
89
101
  ### 9. List attributes for a path
@@ -336,6 +348,14 @@ centrs retrieve $A /system/resource --attribute bogus-attr --via native-api --po
336
348
  centrs retrieve $A /system/resource --via native-api --port $API_PORT --username wrong --password wrong
337
349
  ```
338
350
 
351
+ ### N12. Singleton `--attributes` projection (#377)
352
+
353
+ ```bash
354
+ centrs retrieve $A /tool/romon --attributes enabled,id --via native-api --port $API_PORT --username $U --password $P
355
+ ```
356
+
357
+ `data` is an object containing only `enabled` and `id`.
358
+
339
359
  ## quickchr targets (#134)
340
360
 
341
361
  `$NAME` is the machine name of a running quickchr-managed CHR
@@ -42,6 +42,9 @@ printf '/system/identity/print\n' | centrs terminal $MAC --via mac-telnet --host
42
42
  ```
43
43
 
44
44
  stdout contains the device identity (cross-checked against REST). Exit code `0`.
45
+ RouterOS 7.23.6/7.24.3/7.25beta4 print a one-property menu's value one
46
+ character per line (GH#352), so the comparison collapses `print` column
47
+ wrapping on both sides rather than asserting the device's layout.
45
48
 
46
49
  ### T2. `--via rest-api` has no terminal capability
47
50
 
@@ -99,7 +102,10 @@ analog of T4** (a piped subprocess stdin cannot be a TTY), not in CI.
99
102
  printf '/system/identity/print\n/quit\n' | centrs terminal 127.0.0.1 --via ssh --port $SSH_PORT --username $U --ssh-key $KEY --insecure
100
103
  ```
101
104
 
102
- stdout contains the device identity (cross-checked against REST). centrs returns
105
+ stdout contains the device identity (cross-checked against REST).
106
+ RouterOS 7.23.6/7.24.3/7.25beta4 print a one-property menu's value one
107
+ character per line (GH#352), so the comparison collapses `print` column
108
+ wrapping on both sides rather than asserting the device's layout. centrs returns
103
109
  ssh's exit code (a no-PTY console closed by EOF can exit non-zero — that is the
104
110
  device/ssh's result, not a centrs failure; a clean `/quit` from a real TTY exits
105
111
  `0`).
package/docs/CLI.md CHANGED
@@ -114,7 +114,7 @@ Usage: centrs api <router> <endpoint> [flags]
114
114
  | `--query / --filter` | `<expr>` | RouterOS-side row filter, AND-combined, repeatable: name=value, name!=value, name>value, name<value, name. |
115
115
  | `--raw-query` | `<word>` | Verbatim RouterOS query word (repeatable) for OR / absence / stack expressions. |
116
116
  | `--attribute / --proplist` | `<a,b>` | Property projection → `.proplist`. |
117
- | `--raw` | | Strip the envelope; emit bare RouterOS JSON. Implies `--validate=false`; does not imply `--yes`. |
117
+ | `--raw` | | Strip the envelope; emit bare RouterOS JSON. Defaults `--validate` to false (an explicit `--validate=true` still runs the gate); does not imply `--yes`. |
118
118
  | `--yes` | | Confirm a mutating (non-read) request in non-interactive runs. |
119
119
  | `--stream / --listen` | | Follow changes as an NDJSON envelope stream (native-api only; the `/listen` endpoint infers it). Ends with a summary envelope. |
120
120
  | `--count` | `<n>` | Stop a `--stream` after N change frames. |
@@ -223,7 +223,7 @@ Usage: centrs explain '<input>' [flags]
223
223
  | `--complete` | | Continuation candidates at the cursor. Live evidence — offline emits a tip and enumerates nothing. |
224
224
  | `--schema` | | Path enumeration (verbs, args, types, enums). Live evidence — offline emits a tip and enumerates nothing. |
225
225
  | `--curl` | | Render a ready-to-edit REST curl for statements covered by a runtime-tested mapping rule. |
226
- | `--tokens` | | Emit the total, gapless token partition behind `data.tokens[]` (provisional `class` until #264 B5: every byte not claimed by an analyzer is `unclassified`). |
226
+ | `--tokens` | | Emit the total, gapless token partition behind `data.tokens[]` (every byte not claimed by an analyzer is `unclassified`). |
227
227
  | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
228
228
  | `--json` | | Shortcut for `--format json`. |
229
229
  | `--yaml` | | Shortcut for `--format yaml`. |
@@ -234,7 +234,7 @@ Usage: centrs explain '<input>' [flags]
234
234
  Inspect and mutate the CDB-backed device registry. `devices` is the only command that writes the CDB.
235
235
 
236
236
  ```text
237
- Usage: centrs devices <list|show|groups|add|edit|set|remove> [args] [flags]
237
+ Usage: centrs devices <init|list|show|groups|add|edit|set|remove> [args] [flags]
238
238
  ```
239
239
 
240
240
  | Flag | Value | Description |
@@ -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,8 +36,40 @@ 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
@@ -45,8 +84,10 @@ re-validate-server-side. Validation is not optional polish.
45
84
  `expected`. Rejection forms are `(evl bad parameter <name> (line N column M)
46
85
  /path)`, `(<%% bad command name <name> (line N column M) …)`, and the
47
86
  line-anchored `expected … (line N column M)` (spellings: `expected end of
48
- command`, `expected command name`, `expected input value`) / `syntax error
49
- (line N column M)`. That text rides the **HTTP-200 `ret` value** (REST,
87
+ command`, `expected command name`, `expected input value`), `missing … (line
88
+ N column M)` (`missing closing brace`, `missing value for where`; GH#375) and
89
+ `syntax error (line N column M)`. Every bare form is anchored at a line start
90
+ with the position suffix, because an accepted return can echo those words. That text rides the **HTTP-200 `ret` value** (REST,
50
91
  `as-string`), the native `as-string` `ret`, and the console's printed output
51
92
  alike — so a single `:put [:parse ...]` covers both the syntax and the
52
93
  **unknown-attribute** gate on every transport. centrs sends `as-string` and now
@@ -397,22 +438,38 @@ default), so key auth is the normal sftp path.
397
438
 
398
439
  ## Done definition
399
440
 
400
- A feature is done when its **CHR integration test is green** against a real
401
- RouterOS CHR booted by `@tikoci/quickchr`. Not when code exists. Not when unit
402
- tests pass. Not when the spec says so. This is the **normative** done
403
- definition; `AGENTS.md` and
441
+ Done requires executable evidence for the capability being claimed. Device
442
+ operations and live analysis require **green CHR integration tests** against a
443
+ real RouterOS CHR booted by `@tikoci/quickchr`, via `bun run test:integration`.
444
+ Code existing or unit tests passing cannot establish live behavior. This is
445
+ the **normative** done definition; `AGENTS.md` and
404
446
  `.github/instructions/done-definition.instructions.md` point here (the latter
405
447
  carries the Copilot-workflow procedure for satisfying it).
406
448
 
407
- `docs/MATRIX.md` is the single source of truth for *what* is done and defines the
408
- cell states (`not-started` / `designed` / `coded` / `CHR-passed`). A cell
409
- advances only with the matching evidence in the same change, and the commit that
410
- advances it must include the test name and CHR version.
449
+ Offline analysis is `verified` when its applicable command examples and
450
+ contract checks pass without a router, its RouterOS semantic claims have
451
+ committed CHR-grounded evidence, and the command's baseline acceptance criteria
452
+ are satisfied. New or changed RouterOS semantics still require live grounding
453
+ with accepted and rejected controls plus a reproducible capture/replay path;
454
+ offline tests do not establish new device facts. Changes to shared execution
455
+ or transport behavior still require `bun run test:integration`. The offline
456
+ call itself never needs a router merely to satisfy a workflow label.
457
+
458
+ `docs/MATRIX.md` owns capability status and the selected work track. A
459
+ capability's acceptance criteria — like `explain`'s Offline baseline
460
+ acceptance — live in its `commands/<name>/README.md`; GitHub issues own the
461
+ tasks and dependencies that satisfy those criteria, not the criteria
462
+ themselves. Umbrella issues index that work rather than replacing capability
463
+ status. Status advances only with matching evidence in the same change.
464
+ Record test names, fixture provenance, and CHR versions where applicable.
465
+ Offline verification never advances a live protocol cell to `CHR-passed`.
411
466
 
412
467
  ## Boundaries
413
468
 
414
469
  centrs does:
415
470
 
471
+ - Analyze RouterOS source independently of a connection, preserving ordered
472
+ structure, source locations, evidence, and ambiguous/unknown readings.
416
473
  - Resolve devices/groups from explicit input, env, CDB, MNDP cache.
417
474
  - Choose protocols per the table above and configure them.
418
475
  - Validate before execute; re-validate server-side immediately before run.
@@ -425,7 +482,9 @@ centrs does:
425
482
 
426
483
  centrs does not:
427
484
 
428
- - Replace RouterOS syntax with high-level abstractions.
485
+ - Replace RouterOS syntax with high-level configuration helpers. Normalized
486
+ representations that preserve its semantics are in scope; reconstructing
487
+ final device state or predicting network behavior is not an offline parse claim.
429
488
  - Treat passive discovery (MNDP) as authoritative inventory.
430
489
  - Run write-shaped operations without an explicit target and validation policy.
431
490
  - Make generated output the hand-edited source of truth.
@@ -478,8 +537,10 @@ as bibliography entries in `GLOSSARY.txt` — e.g. `netmiko`,
478
537
  ### Canonicalizer ownership
479
538
 
480
539
  centrs owns the **script-vs-structured execution gate** — the load-bearing
481
- discriminator for which validation runs and whether the write-confirmation prompt
482
- fires; widening what counts as `structured` is a product regression. The shared,
540
+ discriminator for validation and transport shape; widening what counts as
541
+ `structured` is a product regression. Write confirmation is a separate,
542
+ fail-closed safety predicate over the original CLI string: falling back to raw
543
+ script transport must never turn a write into a read. The shared,
483
544
  pure command canonicalizer that `rosetta` / `lsp-routeros-ts` publish is for
484
545
  canonicalization only, never the structured-mode predicate. The gate contract,
485
546
  its pinning test, and the parser-vendoring preconditions are documented in
@@ -489,5 +550,6 @@ its pinning test, and the parser-vendoring preconditions are documented in
489
550
  Richer schema-free analysis must distinguish **ambiguous** (multiple plausible
490
551
  readings) from **unknown** (no safe reading) and abstain in both cases rather than
491
552
  inventing a command, path, symbol class, or transport mapping. These explain-only
492
- verdicts never widen or reinterpret the execution gate's `mode` or `writeShaped`
493
- decision.
553
+ verdicts never widen or reinterpret the execution gate's `mode`. The execution
554
+ safety predicate remains deliberately conservative and independent of richer
555
+ explain-only inference.
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 |
@@ -37,8 +46,8 @@ A cell advances only with the matching evidence in the same change.
37
46
  ## Status pointers
38
47
 
39
48
  Detailed command contracts live in `commands/<name>/README.md`; example evidence
40
- lives in `commands/<name>/examples.md` and the named integration tests. Keep this
41
- 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.
42
51
 
43
52
  - `devices` is transport-less, so its row stays `—`; command state is
44
53
  `CHR-passed` by fixture-backed integration tests in `test/integration/devices.test.ts`.
@@ -60,42 +69,13 @@ section short enough that the grid remains the status surface.
60
69
  `winbox-terminal` stay `not-started`. Its per-host fan-out is the machinery the
61
70
  IP-scan discovery (#149) will iterate; the L2-default timing evidence (#136) is
62
71
  recorded in the README before the cells advance past `designed`.
63
- - `explain` is `designed` (`commands/explain/README.md`): the explain →
64
- validate → run knowledge tier — canonical structure, LSP-like
65
- spans/diagnostics, transport classification (api-able vs execute, `curl`
66
- rendering), and live schema/completion facts. Its offline mode is the
67
- canonicalizer only and opens no protocol connection (evidence is
68
- unit/fixture tests); the
69
- live probes (`/console/inspect` + `:parse`) ride rest-api/native-api, so
70
- those two cells are `designed`. The surface was decided in the 2026-07-19
71
- design round (#90) and the spec is **ratified** on the phase-0 grounding
72
- lab (#185, ratification PR #187). Phase 0.5 has since promoted the ratified
73
- lab logic into the library — `src/explain/{segment,blocks,coordinates,defects,
74
- pathresolve,verbsplit,write,symbols,menus,verbs,args,values,brace-slots}.ts`
75
- with frozen fixtures under `test/fixtures/explain/` — and phase 1 has composed
76
- them into one analysis and envelope in `src/explain.ts` (`explainCommand` /
77
- `explainEnvelope`, #202a). **`centrs explain '<input>'` is a real command as
78
- of #202b** (`src/cli/explain.ts`), and #202c adds statement argument reading,
79
- the closed Q8 transport classifier, equivalent centrs invocations, and
80
- opt-in `--curl`. All offline examples, including 1, 2, 6 and 23, run green in
81
- `test/unit/explain.test.ts`. The row nonetheless stays `designed`: offline
82
- mode occupies no protocol cell, and the two `designed` cells are the live
83
- `/console/inspect` + `:parse` probes, which are phase 2. The live grammar
72
+ - `explain`'s two `designed` protocol cells are the future live probes
73
+ (`/console/inspect` + `:parse`, #236). The live grammar
84
74
  (`explain <router> '<input>'`) is parsed and refused with
85
- `usage/not-implemented` rather than degraded to an offline run. Cells advance
86
- only when the live probes land and their examples run green per the done rule.
87
- `src/explain/menus.ts` and `src/explain/catalog.ts` are the **generated**
88
- files in that set — the RouterOS menu-vs-command table (#207, from pinned
89
- restraml typed trees) and the path catalog (#228, those trees unioned with
90
- MikroTik's published CLI Reference, carrying kind, per-entry provenance and
91
- the published applicability gate). Regenerate with `bun run explain:menus` /
92
- `bun run explain:catalog`; both are drift-checked in QA. They keep the offline
93
- analyzer offline; do not hand-edit them. The catalog's **command axis is read**
94
- by `pathresolve.ts`, `verbsplit.ts` and `write.ts` (#228 step 2); #235 also
95
- reads its menu/settings kind, unioned with `menus.ts` behind
96
- `src/explain/is-known-menu.ts`, for both a context-applied relative bare path
97
- and an absolute one. Both changes are inside the offline canonicalizer, so
98
- this grid is unchanged by them.
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`.
99
79
  - `transfer / ssh` means the SFTP-backed transfer method. Deferred file-transfer
100
80
  methods such as `scp`, `fetch`, and `ftp` are tracked in
101
81
  `commands/transfer/README.md`; `fetch` is not a grid column.
@@ -108,9 +88,20 @@ section short enough that the grid remains the status surface.
108
88
  - Transport wire-format and delivery caveats belong in `src/protocols/*.ts`
109
89
  module headers. Do not expand this file with per-packet or per-router findings.
110
90
 
111
- Beyond the grid: post-`CHR-passed` hardening, ergonomics, and research work is
112
- tracked in GitHub issues (`dry-july` and successors). Those issues may point back
113
- 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.
114
105
 
115
106
  ### Transport-base readiness
116
107
 
@@ -126,8 +117,8 @@ single home for wire-format, auth, and delivery facts.
126
117
 
127
118
  ### Frontend surfaces (orthogonal to the command grid)
128
119
 
129
- The grid tracks the core (commands × protocols). Frontends are adapters over that
130
- 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:
131
122
 
132
123
  | Surface | State | Spec | Notes |
133
124
  | ------- | ----- | ---- | ----- |
@@ -157,11 +148,57 @@ its own capability axis and is exempt from the RouterOS command-validation gate.
157
148
  Wire, auth, TCP/UDP, and CI caveats live in `commands/btest/README.md` and the
158
149
  `src/protocols/btest*.ts` / `src/protocols/ec-srp5.ts` module headers.
159
150
 
160
- ## Priority order
161
-
162
- Do not start a later item until the earlier cell or dependency checkpoint has
163
- matching evidence. The grid above is the live status; per-item detail lives in
164
- 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>/`.
165
202
 
166
203
  1. **retrieve / rest-api** — `CHR-passed` (`commands/retrieve/`).
167
204
  2. **CDB resolution + metadata overrides** — `CHR-passed` (`commands/devices/`).
@@ -0,0 +1,20 @@
1
+ # `cdb/create-failed`
2
+
3
+ A new CDB file could not be created at the resolved location.
4
+
5
+ `centrs devices init` creates an empty CDB at the path from `--cdb-file`,
6
+ `CENTRS_CDB_FILE`, or the default `~/.config/tikoci/winbox.cdb`, making any
7
+ missing parent directories. This error means the filesystem refused: typically
8
+ a directory on the path is not writable by the current user, or a path
9
+ component is a file rather than a directory. The underlying filesystem error
10
+ (for example `EACCES`) is preserved as the error's `cause`; run
11
+ with `--json` to see it.
12
+
13
+ Nothing is written when this fails.
14
+
15
+ ## Fix
16
+
17
+ - Check the permissions of the directories on the path shown in the error.
18
+ - Or choose a writable location:
19
+ `centrs devices init --cdb-file PATH`, then pass the same `--cdb-file PATH`
20
+ (or set `CENTRS_CDB_FILE`) on later commands.