@tikoci/centrs 0.1.9-next.23 → 0.1.9-next.24

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 (53) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/GLOSSARY.txt +14 -0
  3. package/README.md +2 -2
  4. package/commands/api/AGENTS.md +18 -1
  5. package/commands/api/README.md +10 -6
  6. package/commands/api/examples.md +1 -1
  7. package/commands/execute/README.md +26 -6
  8. package/commands/execute/examples.md +24 -5
  9. package/commands/explain/README.md +452 -61
  10. package/commands/explain/examples.md +191 -41
  11. package/commands/retrieve/README.md +13 -2
  12. package/commands/retrieve/examples.md +30 -1
  13. package/commands/transfer/README.md +8 -4
  14. package/docs/CLI.md +19 -5
  15. package/docs/CONSTITUTION.md +14 -9
  16. package/docs/MATRIX.md +34 -21
  17. package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
  18. package/docs/errors/routeros/permission-denied.md +24 -0
  19. package/docs/errors/transport/edge-auth-failed.md +22 -0
  20. package/docs/errors/transport/write-outcome-unknown.md +3 -0
  21. package/docs/errors/usage/invalid-cursor.md +20 -0
  22. package/docs/errors/validation/unknown-path.md +9 -0
  23. package/package.json +2 -2
  24. package/src/api.ts +34 -39
  25. package/src/cli/explain.ts +206 -47
  26. package/src/core/envelope.ts +10 -3
  27. package/src/core/error-catalog.ts +15 -0
  28. package/src/core/execute-command.ts +53 -0
  29. package/src/core/fanout.ts +12 -2
  30. package/src/core/inspect.ts +4 -5
  31. package/src/core/routeros-errors.ts +25 -0
  32. package/src/execute-cmr.ts +2 -8
  33. package/src/execute.ts +48 -20
  34. package/src/explain/args.ts +115 -10
  35. package/src/explain/group-faults.ts +162 -29
  36. package/src/explain/operators.ts +2 -1
  37. package/src/explain/scope-brace.ts +226 -11
  38. package/src/explain/segment.ts +136 -1
  39. package/src/explain/symbols.ts +146 -6
  40. package/src/explain/values.ts +55 -4
  41. package/src/explain-live-facets.ts +739 -0
  42. package/src/explain-live-types.ts +490 -0
  43. package/src/explain-live.ts +593 -0
  44. package/src/explain.ts +395 -43
  45. package/src/index.ts +6 -0
  46. package/src/protocols/adapter.ts +88 -10
  47. package/src/protocols/mac-telnet.ts +38 -10
  48. package/src/protocols/native-api.ts +13 -12
  49. package/src/retrieve-follow.ts +2 -1
  50. package/src/retrieve-sample.ts +2 -1
  51. package/src/retrieve-wait.ts +10 -6
  52. package/src/retrieve.ts +77 -27
  53. package/src/transfer.ts +38 -13
@@ -1,15 +1,12 @@
1
1
  # explain — examples
2
2
 
3
- Each numbered example is an executable spec. Once `explain` is implemented,
4
- offline examples run under `test/unit/explain.test.ts` and live examples under
5
- `test/integration/explain.test.ts` (rest-api) and
6
- `test/integration/native-api-explain.test.ts` (native-api) against a CHR booted by
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**:
11
- those cells are `designed`, and flag and field names track the ratified surface
12
- in `README.md`.
3
+ Each numbered example is an executable spec. Offline examples run under the
4
+ named unit/fixture suites. Live foundation examples 11, 12, 16, 24 and 48 run
5
+ under `test/integration/explain-live.test.ts`, and describe/completion examples
6
+ 7–10, 13–15 and 19 under `test/integration/explain-live-facets.test.ts`, on REST
7
+ and native API with a CHR booted by `@tikoci/quickchr`. Live types 52–53 run
8
+ under `test/integration/explain-live-types.test.ts`. All of them pass on stable
9
+ 7.24.5 and long-term 7.23.8, so both protocol cells are `CHR-passed` (#236).
13
10
 
14
11
  A letter-suffixed example (`1b`, `4b`, `18b`) is the **counterpart** of the number it
15
12
  follows: the same question with the other answer, added where implementing the
@@ -151,10 +148,10 @@ renders no curl.
151
148
  centrs explain $R /ip/address -u $U -p $P --schema --json
152
149
  ```
153
150
 
154
- `data.schema.verbs` lists the verbs available at `/ip/address` (`add`, `set`,
155
- `remove`, `print`, …), each with an arg count or a drill-down hint rather than
156
- the full arg expansion; facts cite `live-inspect` evidence stamped with the
157
- CHR's RouterOS version.
151
+ `data.schema.node` is `menu`, and `data.schema.verbs` lists the verbs at
152
+ `/ip/address` (`add`, `set`, `remove`, `print`, …). Each carries its argument
153
+ count from one more `child` probe rather than the full argument expansion.
154
+ Facts cite `live-inspect` evidence stamped with the CHR's RouterOS version.
158
155
 
159
156
  ### 8. Describe a verb: its arguments
160
157
 
@@ -162,8 +159,9 @@ CHR's RouterOS version.
162
159
  centrs explain $R /ip/address/set -u $U -p $P --schema --json
163
160
  ```
164
161
 
165
- `data.schema` enumerates the settable arguments of `set` (`address`,
166
- `interface`, `disabled`, …) with types where the device reports them.
162
+ `data.schema.arguments` enumerates the settable arguments of `set` (`address`,
163
+ `interface`, `disabled`, …). Each has its argument-level `syntax` rows where the
164
+ device reports them (`address`: `A.B.C.D (IP address)`).
167
165
 
168
166
  ### 9. Describe print: the proplist special case
169
167
 
@@ -171,10 +169,12 @@ centrs explain $R /ip/address/set -u $U -p $P --schema --json
171
169
  centrs explain $R /ip/address/print -u $U -p $P --schema --json
172
170
  ```
173
171
 
174
- `data.schema` returns the `.proplist` value set — what the output *can*
175
- contain — rather than treating `print` like a plain verb. The grounded probe is
172
+ `data.schema.proplist` returns the `.proplist` value set — what the output
173
+ *can* contain — besides `print`'s own arguments. The grounded probe is
176
174
  `completion input="/ip/address/print proplist="` (dot-free console spelling),
177
- filtered to rows with `show=true`; each returned fact cites that live evidence.
175
+ filtered to rows with `show=true`; it cites that live evidence. A singleton
176
+ settings menu (`/ip/dns/print`) offers no proplist names, so
177
+ `get value-name=` answers with `source: "get-value-name"`.
178
178
 
179
179
  ### 10. Smart sizing truncates with counts, `--full` lifts it
180
180
 
@@ -182,9 +182,11 @@ filtered to rows with `show=true`; each returned fact cites that live evidence.
182
182
  centrs explain $R /ip -u $U -p $P --schema --json
183
183
  ```
184
184
 
185
- The `/ip` subtree is large: `data.schema.truncated` is `true`, counts are
186
- reported in place of the full expansion, and a warning/hint names the
187
- narrower query (or `--full`) that expands it.
185
+ The `/ip` subtree is large (31 children, past the 16-probe default):
186
+ `data.schema.truncated` is `true`, the names are listed with `counts` but not
187
+ expanded, and `tip/explain-schema-truncated` names the narrower inputs in
188
+ `data.schema.next` or `--full`. With `--full`, every submenu carries its verb
189
+ and menu counts.
188
190
 
189
191
  ### 11. Spans and the error byte (centrs vocabulary)
190
192
 
@@ -192,9 +194,21 @@ narrower query (or `--full`) that expands it.
192
194
  centrs explain $R '/ip/address add address=bogus interface=' -u $U -p $P --json
193
195
  ```
194
196
 
195
- `data.spans` classify the input byte-by-byte using the **centrs** span
196
- vocabulary (not raw RouterOS highlight classes); the first hard error's byte
197
- offset appears in a diagnostic citing `live-inspect` evidence.
197
+ `data.spans` includes device-derived spans in the **centrs** vocabulary,
198
+ citing `live-inspect` evidence. On CHR 7.24.5, this original example produces
199
+ no parse rejection or highlight error: `address=bogus` is not value-validated,
200
+ and the trailing empty argument is omitted from the parse output. Keep it as
201
+ a probe-limit control, not a promised syntax error.
202
+
203
+ A confirmed hard-error control is:
204
+
205
+ ```bash
206
+ centrs explain $R '/ip/route add dst-address=10.9.0.0/16 blackhole=yes' -u $U -p $P --json
207
+ ```
208
+
209
+ On CHR 7.24.5, highlight reports byte 47 and parse reports line 1 column 48.
210
+ The test asserts that source-located diagnostic separately from runtime
211
+ acceptance; the original input remains a control on both transports.
198
212
 
199
213
  ### 12. Structure via :parse
200
214
 
@@ -213,8 +227,14 @@ here over rest-api; example 24 asserts the same readout over native API using
213
227
  centrs explain $R '/ip/address add comm' -u $U -p $P --complete --json
214
228
  ```
215
229
 
216
- `data.completion` includes `comment` as a completing candidate for the
217
- partial word `comm` — a valid partial, no unknown-word diagnostic.
230
+ `data.completion` is `context: "argument-name"` with `comment` as a
231
+ `partial` candidate replacing bytes 16–20. No canonicalizer diagnostic calls
232
+ `comm` unknown. The device still rejects the input as written: `:parse` and
233
+ the highlight `error` both name byte 16. So pass `--fail-on never` when only
234
+ the candidates matter. `--cursor 20` gives the same answer inside a longer
235
+ input. The same candidate comes back for a relative path after `;`
236
+ (`/ip { :put 1; address add comm`), and `where comm` completes `comment` as a
237
+ `variable`.
218
238
 
219
239
  ### 14. Completion: complete argument name, no value yet
220
240
 
@@ -222,9 +242,10 @@ partial word `comm` — a valid partial, no unknown-word diagnostic.
222
242
  centrs explain $R '/ip/address add comment' -u $U -p $P --complete --json
223
243
  ```
224
244
 
225
- The candidate set marks `comment` as a complete argument name expecting a
226
- value (`=` continuation) — distinct from example 13's partial match and from
227
- an unknown word.
245
+ The candidate set marks `comment` as `match: "exact"` with `takesValue: true`,
246
+ because the device offers `=` at the cursor. That is distinct from example 13's
247
+ partial match and from an unknown word. The flag `/interface print detail` is
248
+ exact with `takesValue: false`.
228
249
 
229
250
  ### 15. Completion: value position
230
251
 
@@ -233,9 +254,11 @@ centrs explain $R '/ip/firewall/filter add chain=' -u $U -p $P --complete --json
233
254
  ```
234
255
 
235
256
  With the input ending in `=`, candidates are **values** for `chain`
236
- (`forward`, `input`, `output` — labeled as observed, never a closed set). For
237
- a free-form argument like `comment=`, the same probe yields type info (string)
238
- rather than candidates.
257
+ (`forward`, `input`, `output` — observed from the completion probe, never a closed set). For
258
+ a free-form argument like `comment=`, the same probe yields no candidates and
259
+ `data.completion.syntax` carries `string value`. An unclosed substitution
260
+ (`:local x [/ip/address add interface=`) still completes, from the device's
261
+ own path runs.
239
262
 
240
263
  ### 16. Runtime acceptance is never claimed
241
264
 
@@ -343,9 +366,9 @@ curated read verb; before the command axis this document abstained.
343
366
  centrs explain $R '/ip/route' -u $U -p $P --json
344
367
  ```
345
368
 
346
- `/console/inspect` completion and highlight agree that the terminal token is a
347
- `dir`; the first statement is `resolution: "resolved"`, `kind: "menu"`, and
348
- has no runnable transport rendering. The evidence entry is version-stamped and
369
+ The highlight classes every byte `dir`, and with `--schema` the `child` self
370
+ row says `menu`; the first statement is `resolution: "resolved"`,
371
+ `kind: "menu"`, and has no runnable transport rendering. The evidence entry is version-stamped and
349
372
  the probe never executes the path. Since #207 the offline reading (example 18b)
350
373
  already says the same thing from the generated table, so what the live probe
351
374
  adds here is **provenance and a version stamp**, not the answer — and it is the
@@ -452,7 +475,8 @@ full MAC spelling carry `["array"]`, `["time"]`, `["id"]`, and `["mac"]`.
452
475
  is `str` while the CLI Reference declares `mac-address` as `macAddr`. Each hint
453
476
  fact cites canonicalizer evidence with
454
477
  `basis: "heuristic"`; no record has `observedType` or `schemaType`, because
455
- offline analysis has neither live parser output nor an argument schema. A hint
478
+ offline analysis has neither live parser output nor an argument schema
479
+ (examples 52 and 53 are the live counterparts). A hint
456
480
  is not a diagnostic and does not change `data.verdict` or
457
481
  `runtimeAcceptance: "not-proven"`.
458
482
 
@@ -768,10 +792,12 @@ RouterOS accepts any unique prefix of an argument name. An abbreviation that
768
792
  can only stand for a script-valued name keeps its body in either spelling:
769
793
  `sour { :beep }`, `down-scr { :beep }` and `o={ :beep }` lower to `source=`,
770
794
  `down-script=` and `on-event=` on CHR 7.24.5 and 7.23.8. Abbreviating the first
771
- segment (`down={ :beep }` for `down-script`) is accepted on the device too, but
772
- offline still reports `invalid-command-brace`. Every word is a prefix of some
773
- `<word>-script`, so telling it from `comment={ :beep }` needs the command's
774
- argument list.
795
+ segment (`down={ :beep }` for `down-script`) is accepted on the device too, and
796
+ offline warns `possible-script-body-abbreviation` there instead of rejecting
797
+ (#525): every word is a prefix of some `<word>-script`, so telling it from
798
+ `comment={ :beep }` needs the command's argument list, and the device decides.
799
+ The tradeoff is deliberate — `comment={ :beep }`, which the device rejects,
800
+ carries the same warning — and `data.verdict` stays `warn`, never `pass`.
775
801
 
776
802
  A brace straight after a command is a device error, and offline now reports it.
777
803
  `/system/script add { :beep }`, `/ip/route/print {1;2}` and
@@ -959,3 +985,127 @@ The CHR controls in `test/integration/explain-structure.test.ts` parse the
959
985
  scope examples and compare stored-script creation, explicit `run`, and an
960
986
  immediate `:do` body. Those are test operations on disposable CHR; explain
961
987
  itself never executes the analyzed input.
988
+
989
+ ### 48. Safe argument discovery distinguishes spaced arguments from flags
990
+
991
+ ```bash
992
+ centrs explain $R '/interface set ether1 name x' -u $U -p $P --json
993
+ centrs explain $R '/interface print detail without-paging' -u $U -p $P --json
994
+ ```
995
+
996
+ `data.live.arguments[]` cites command-level child evidence and statement `s0`.
997
+ The first result includes `name`, the second includes `detail`. Both parse;
998
+ only `name x` gets the device's obsolete-syntax warning. An unknown argument
999
+ control (`/interface set ether1 definitely-unknown-491=x`) carries the shared
1000
+ parse classifier's rejection. No command-level completion is requested.
1001
+
1002
+ The integration controls also explain a write-shaped identity change and a
1003
+ substitution without executing either, verify identity before/after, exercise
1004
+ CLI stdin and quickchr resolution, and skip oversized highlight without
1005
+ submitting a prefix. Parse/hash/normalization evidence preserves the Unicode
1006
+ byte coordinates. Run on long-term as well as stable before changing these
1007
+ claims. Sanitized captures can be regenerated with `CENTRS_EXPLAIN_CAPTURE`
1008
+ pointing at an ignored output file when running the integration suite.
1009
+
1010
+ ### 49. `?`, a backtick and a second `=` need quotes
1011
+
1012
+ ```bash
1013
+ centrs explain '/tool/fetch url=http://x/a?b=c' --json
1014
+ ```
1015
+
1016
+ `data.verdict` is `fail` with `explain/canonicalizer/unquoted-reserved-byte`
1017
+ at the `?`. The device answers `expected end of command (line 1 column 27)`,
1018
+ the same byte. A backtick is rejected the same way, in a value, a
1019
+ positional, a `where`, a `find`, an array and an expression. A second `=` in
1020
+ a menu command's argument is rejected at that `=`. So `name=a=b` and a bare
1021
+ base64 key ending in `=` both fail. The value's property parser never takes
1022
+ these bytes: `regexp=a?b` and `secret=abc==` are rejected too. In a menu
1023
+ command's argument name, the device reports from the name's first byte
1024
+ (`/ip/address/add a?b=1` at the `a`). `url="http://x/a?b=c"`,
1025
+ `where comment=a=b`, `[find comment=a=b]`, `{a=b}` and `# a?b` pass.
1026
+ `:put a=b` abstains: 7.24.5 reads it as a named argument and 7.23.8 rejects
1027
+ the `=`.
1028
+
1029
+ ### 50. A non-ASCII character outside a string is rejected
1030
+
1031
+ ```bash
1032
+ centrs explain ':put $café' --json
1033
+ ```
1034
+
1035
+ `data.verdict` is `fail` with `explain/canonicalizer/unquoted-non-ascii` over
1036
+ the `é`. The device answers `expected end of command (line 1 column 10)`, at
1037
+ the `é`'s first byte. RouterOS reads only ASCII outside a string. A bare
1038
+ value (`name=router-café`, `name=🛰`), a reference, a path (`/systemé`), a
1039
+ verb, an argument name, a `where` value, an array and an expression are all
1040
+ rejected. So are pasted non-breaking spaces, curly quotes and ellipses. A
1041
+ declared name (`:local café`) keeps its `invalid-name` error. Strings and
1042
+ comments carry any byte: `:put "café"`, `:local "café" 1`, `$"café"`,
1043
+ `name="router-🚀"`, `# café` and a multi-line string pass. The informational
1044
+ `non-ascii` fact is still published beside the error, and a leading BOM is
1045
+ not read as part of the first word.
1046
+
1047
+ ### 51. A brace after a submenu opens a block at any depth
1048
+
1049
+ ```bash
1050
+ centrs explain '/ip { address { print {1;2} } }' --json
1051
+ ```
1052
+
1053
+ `data.verdict` is `fail` with `explain/canonicalizer/invalid-command-brace` at
1054
+ the inner `{`. The device answers `expected end of command (line 1 column 23)`,
1055
+ the same byte. `address {` inside `/ip { … }` is a block over `/ip/address`,
1056
+ so `data.structure.statements` reads `/ip`, `/ip/address` and
1057
+ `/ip/address print`, and the `{1;2}` is the operand of `print`.
1058
+
1059
+ The device reads a brace after a menu word as statements whatever the body
1060
+ holds, at any depth. A body offline cannot resolve abstains with
1061
+ `unresolved-statement` instead of failing as an array:
1062
+ `/ip { address { 1;2 } }` (the device says `bad command name 1`) and
1063
+ `/ip { address { zzznotamenu { print } } }` (`syntax error`) both give `warn`,
1064
+ as `/ip/address { 1;2 }` and `/ip { zzznotamenu { print } }` do.
1065
+ `/ip { address { print } }` and `/ip { firewall { filter { print } } }` pass.
1066
+ The menu must be known from the source alone, so a block after a navigation
1067
+ statement (`/ip⏎address { … }`), an abbreviation (`addr {`) and a digit-led
1068
+ menu (`6to4 {`) keep the older reading.
1069
+
1070
+ ### 52. A live call observes a type only where the device converts a literal
1071
+
1072
+ ```bash
1073
+ centrs explain $R ':local x 2.2; :set x "2.2"; :local z (1,0x10,2.2); :local t 200ms; :local i *1; :local y yes' -u $U -p $P --json
1074
+ ```
1075
+
1076
+ This is example 26's input against a target. The bare `2.2` gains
1077
+ `facts.observedType: { value: "ip", rendered: "2.0.0.2" }`, `200ms` gains
1078
+ `time` (`00:00:00.200`) and `yes` gains `bool` (`true`). Of the array's
1079
+ members, `0x10` is `num` (`16`) and `2.2` is `ip`, while `1` renders unchanged
1080
+ and gets nothing. The quoted `"2.2"` and the id `*1` also get nothing, because
1081
+ an unchanged rendering proves no type: `:parse` prints a string unquoted. The
1082
+ `shapeHints` are untouched, and no record has `schemaType`, because no value
1083
+ here is a menu command's argument.
1084
+
1085
+ A scripting directive's named argument is an operand too:
1086
+ `:for i from=0x10 to=20 do={:put $i}` gives `from` the fact `num` (`16`), and
1087
+ `to=20` renders unchanged and gets nothing. A NAME slot is a variable, never a
1088
+ value: in `:local yes 2` and `:local name=no value=yes`, the names `yes` and
1089
+ `no` get nothing (the device renders `name=$yes`, `name=$no`), while
1090
+ `value=yes` is `bool`.
1091
+
1092
+ Each fact cites one `:parse` evidence entry per context (operands, members)
1093
+ with `facet: "types"` and `basis: "derived"`. The probe re-parses each literal
1094
+ alone, as `:local vN <lit>` or `:local vN {<lit>}`; the analyzed input and
1095
+ the literals are never evaluated, and no `:typeof` is sent.
1096
+
1097
+ ### 53. A live call normalizes each argument's declared type
1098
+
1099
+ ```bash
1100
+ centrs explain $R '/ip/dns/set servers=1.1.1.1,8.8.8.8; /ip/route/add gateway=2.2 dist=2 comment=abc; /ip/firewall/filter/add chain=input dst-port=80,443 protocol=tcp disabled=yes' -u $U -p $P --json
1101
+ ```
1102
+
1103
+ Each `name=value` gains `facts.schemaType` from one argument-level `syntax`
1104
+ probe. `servers` is `{ values: ["ip", "ip6", "enum"], declared:
1105
+ "Address[@VRF]" }`, and `gateway` is `ip`, `ip6`, `enum`. `dist` resolves as
1106
+ the unique prefix of `distance` and is `num`. `comment` is `str`. `chain` is
1107
+ `enum` plus `str`, since custom chains are legal. `dst-port` is `num` with
1108
+ `list: true`. `disabled` gets nothing, because the device declares a boolean
1109
+ with a blank row. No menu argument gets `observedType`: its own parser
1110
+ converts it, which is the schema's slot. The text output prints
1111
+ `observed=ip(2.0.0.2)` and `schema=ip|ip6|enum` on the value rows.
@@ -87,8 +87,11 @@ alternatives in the error envelope.
87
87
  `failed` when the gate refuses: `validation/*`, or `--query` on a singleton.
88
88
  It is absent when a `transport/*`, `auth/*`, `routeros/*` or `internal/*` error
89
89
  stopped inspect from answering; the device stage then says `not reached`
90
- (examples 12 and 16b; 16c is the `passed` case). With `--validate=false` it
91
- is `{enabled: false, source: "disabled"}`.
90
+ (examples 12 and 16b; 16c is the `passed` case). The same holds for a
91
+ `wait/*` error from a `--wait` whose gate never settled, and for
92
+ `--sample` / `--follow` cancelled before the gate ran. That last case has no
93
+ error code, so its reason is `not reached: interrupted` (#537). With
94
+ `--validate=false` it is `{enabled: false, source: "disabled"}`.
92
95
 
93
96
  If `--all-attributes` is combined with `--attribute(s)`, fail with
94
97
  `usage/conflicting-flags` *before* hitting the network.
@@ -147,6 +150,14 @@ handling.
147
150
  }
148
151
  ```
149
152
 
153
+ Projection rule: the bare value is about the attribute *count*, not the flag
154
+ spelling. On a singleton, exactly one projected attribute — `--attribute x`
155
+ or `--attributes x` alike — returns the bare value (`data` is the scalar,
156
+ not an object); two or more return an object with those keys. On a list, any
157
+ projection returns an array of objects, even a single attribute. Unprojected
158
+ reads return the full object (singleton) or the row array (list). (Examples
159
+ 5–6, 20, N5–N6 pin every cell of this table.)
160
+
150
161
  `--max-bytes` / `--max-rows` truncation populates
151
162
  `meta.truncated = { returned, total, totalBytes }` and keeps `ok: true` with a
152
163
  warning entry — it is not an error. (Earlier text said a byte overflow returns an
@@ -22,7 +22,8 @@ centrs retrieve $R /system/resource --username $U --password $P --json
22
22
  ```
23
23
 
24
24
  Envelope: `ok: true`, `data` is an object, `meta.via=rest-api`,
25
- `meta.validation.source=/console/inspect`.
25
+ `meta.validation.source=/console/inspect`, and `meta.validation.stages` is the
26
+ one `device: passed` stage (#535).
26
27
 
27
28
  ### 2. /system/identity
28
29
 
@@ -70,6 +71,10 @@ centrs retrieve $R /interface --attributes name,type --username $U --password $P
70
71
 
71
72
  Envelope: `data` is an array of objects each containing only `name` and `type`.
72
73
 
74
+ A single attribute (`--attributes name`) keeps the array shape: an array of
75
+ single-key objects. On a singleton, one item via either flag returns the bare
76
+ value (example 5) — the rule is the count, not the flag.
77
+
73
78
  ### 7. --all-attributes (RouterOS details=true)
74
79
 
75
80
  ```bash
@@ -121,6 +126,18 @@ available.
121
126
  centrs retrieve $R /not/a/real/path --username $U --password $P --json
122
127
  ```
123
128
 
129
+ `/system/routerboard` earns a targeted remediation instead of the generic
130
+ one: the menu exists only on MikroTik hardware, so its absence on CHR and
131
+ x86 systems is expected rather than a typo, and the remediation points at
132
+ `/system/license` for the device's license ID (`system-id` on CHR,
133
+ `software-id` on x86 — a projected attribute would fail on one platform or
134
+ the other, so the example reads the whole object).
135
+
136
+ ```bash
137
+ centrs retrieve $R /system/routerboard --username $U --password $P --json
138
+ centrs retrieve $R /system/license --username $U --password $P --json
139
+ ```
140
+
124
141
  ### 11. Unknown attribute
125
142
 
126
143
  `validation/unknown-attribute` with suggestions.
@@ -362,6 +379,10 @@ centrs retrieve $A /interface --attributes name,type --via native-api --port $AP
362
379
 
363
380
  `data` is an array of objects each containing only `name` and `type`.
364
381
 
382
+ A single attribute (`--attributes name`) keeps the array shape here too: an
383
+ array of single-key objects. On a singleton, one item via either flag returns
384
+ the bare value (N5) — the rule is the count, not the flag.
385
+
365
386
  ### N7. `--all-attributes` (native `print detail`)
366
387
 
367
388
  ```bash
@@ -380,6 +401,14 @@ centrs retrieve $A /system/resource --list-attributes --via native-api --port $A
380
401
  centrs retrieve $A /not/a/real/path --via native-api --port $API_PORT --username $U --password $P
381
402
  ```
382
403
 
404
+ `/system/routerboard` on CHR earns the targeted remediation (no routerboard
405
+ outside MikroTik hardware; read `/system/license` for the license ID
406
+ instead), same as example 10.
407
+
408
+ ```bash
409
+ centrs retrieve $A /system/routerboard --via native-api --port $API_PORT --username $U --password $P
410
+ ```
411
+
383
412
  ### N10. Unknown attribute → `validation/unknown-attribute`
384
413
 
385
414
  ```bash
@@ -96,7 +96,7 @@ size-aware. Grounding: [Files](https://help.mikrotik.com/docs/spaces/ROS/pages/2
96
96
  | `rest` / `native` | rest-api / native-api | small via `/file/get … contents` (**≤ 60 KB**); **large via chunked `/file/read offset chunk-size≤32768`** | **`/file/set contents=` ≤ 60 KB only** — no chunked write |
97
97
  | `sftp` (default secure) | ssh | any size | any size |
98
98
  | `scp` | ssh | any size | any size |
99
- | `fetch` *(deferred)* | rest-api/native-api + inbound HTTP | router PUTs to centrs (`upload=yes http-method=put src-path=…`) | router GETs from centrs (`url=http://centrs/… dst-path=…`) |
99
+ | `fetch` *(deferred, upload-only)* | rest-api/native-api + inbound HTTP | chunked `/file/read` (router PUT fails: `only [s]ftp modes support upload`, #548) | router GETs from centrs (`url=http://centrs/… dst-path=…`) |
100
100
  | `ftp` *(gated)* | ftp | any size | any size |
101
101
 
102
102
  Auto-selection (no `--via`):
@@ -302,6 +302,10 @@ gate as in `retrieve` / `execute`; RouterOS's own menu errors (missing remote
302
302
  file, permission denied) map to `routeros/*`, and SFTP/SCP/fetch errors map to
303
303
  `transport/*` and `auth/*`.
304
304
 
305
+ `meta.validation` reports that existence probe as one `device` stage. `list`
306
+ runs no probe, so it is not a gate: its `meta.validation` is
307
+ `{ enabled: false }`, on success and on failure (#535).
308
+
305
309
  A failure **after** target resolution keeps what resolution found: the error
306
310
  envelope carries the resolved `meta.target` (host, port, identity, sources),
307
311
  `meta.via`, the setting sources in `meta.settings`, and `meta.operation`
@@ -371,9 +375,9 @@ done rule. The first coded pass targets `rest`/`native` (no SSH dependency);
371
375
  (random URL token, bound to the interface facing the router) and drives the
372
376
  router with `/tool/fetch` over `rest`/`native`. **upload**: serve the local
373
377
  file, `fetch url=http://<advertise>/<token> dst-path=<remote>` → router GETs.
374
- **download**: `fetch upload=yes http-method=put url=http://<advertise>/<token>
375
- src-path=<remote>` → router PUTs to centrs (`http-method` any since 7.21;
376
- binary REST serialization fixed 7.17). Open questions: advertise-host
378
+ **download** stays on chunked `/file/read`: router PUTs fail with `only
379
+ [s]ftp modes support upload` (#548), and HTTP `http-data` caps at 60000
380
+ bytes, so the PUT half cannot carry files. Open questions: advertise-host
377
381
  auto-detection behind NAT, HTTP-vs-HTTPS + `check-certificate`, and whether to
378
382
  add fetch's `user`/`password` on top of the URL token.
379
383
  - **ssh** — `sftp` is **built** (`src/protocols/sftp.ts`, host OpenSSH `sftp`
package/docs/CLI.md CHANGED
@@ -15,7 +15,7 @@ implemented flags live in each command's `commands/<name>/README.md`.
15
15
  | [`api`](#api) | Structured RouterOS API passthrough (gh-api style) over REST or native API. |
16
16
  | [`transfer`](#transfer) | Copy files to/from a RouterOS device and manage device files (rest/native/sftp). |
17
17
  | [`terminal`](#terminal) | Open an interactive RouterOS console (ssh or mac-telnet). |
18
- | [`explain`](#explain) | Analyze a RouterOS command offline: canonical form, structure, and syntax diagnostics. |
18
+ | [`explain`](#explain) | Analyze RouterOS input offline or add read-only device evidence from one router. |
19
19
  | [`devices`](#devices) | Inspect and mutate the CDB-backed device registry. `devices` is the only command that writes the CDB. |
20
20
  | [`discover`](#discover) | Discover RouterOS neighbors over MNDP and optionally save them into the CDB. |
21
21
  | [`btest`](#btest) | Run MikroTik's bandwidth test as a client or server (peer measurement). |
@@ -220,18 +220,32 @@ Usage: centrs terminal <router> [flags] | centrs terminal --host <host> [flags]
220
220
 
221
221
  ## explain
222
222
 
223
- Analyze a RouterOS command offline: canonical form, structure, and syntax diagnostics.
223
+ Analyze RouterOS input offline or add read-only device evidence from one router.
224
224
 
225
225
  ```text
226
- Usage: centrs explain '<input>' [flags]
226
+ Usage: centrs explain [<router>] '<input>' [flags]
227
227
  ```
228
228
 
229
229
  | Flag | Value | Description |
230
230
  | ---- | ----- | ----------- |
231
+ | `--via` | &lt;rest-api\|native-api&gt; | Pin the transport; no silent downgrade. Default rest-api. |
232
+ | `--host` | &lt;host\|url&gt; | Host or base URL to dial. With no `<router>` it is the target (no CDB lookup); with one it overrides the resolved host. |
233
+ | `--port` | `<port>` | Override the resolved management port. |
234
+ | `--username / --user / -u` | `<name>` | RouterOS username. Falls back to `CENTRS_USERNAME`. |
235
+ | `--password` | `<secret>` | RouterOS password. Falls back to `CENTRS_PASSWORD`. |
236
+ | `--insecure` | | Accept a self-signed `api-ssl`/REST TLS cert. Default verifies. |
237
+ | `--cdb-file` | `<path>` | Read target credentials from a WinBox CDB file. |
238
+ | `--cdb-password` | `<password>` | Decrypt an encrypted WinBox CDB file. |
239
+ | `--resolve` | &lt;none\|arp&gt; | Resolve a MAC-address target to an IP via the host ARP cache (default none). |
240
+ | `--timeout` | `<duration>` | Per-request timeout (for REST, max 60s). |
241
+ | `-p` | `<secret>` | Alias for --password. |
242
+ | `--quickchr` | `<name>` | Resolve one running quickchr VM by name. |
231
243
  | `--file` | `<path>` | Read the input from a file instead of the positional (`-` reads stdin). Piped stdin is used automatically when no input positional is given. |
232
244
  | `--fail-on` | &lt;error\|warning\|never&gt; | Exit 2 when the verdict reaches this severity. Default `error`; an ambiguous or unknown statement is a warning, never an error. |
233
- | `--complete` | | Continuation candidates at the cursor. Live evidence — offline emits a tip and enumerates nothing. |
234
- | `--schema` | | Path enumeration (verbs, args, types, enums). Live evidence — offline emits a tip and enumerates nothing. |
245
+ | `--complete` | | Candidates at the cursor: menus and verbs, argument names, or values. Live evidence — offline emits a tip and enumerates nothing. |
246
+ | `--schema` | | Describe the path at the cursor: a menu's verbs and submenus, a command's arguments, `print`'s proplist. Live evidence — offline emits a tip and enumerates nothing. |
247
+ | `--cursor` | `<byte>` | UTF-8 byte offset for --complete/--schema. Default: the end of the input. |
248
+ | `--full` | | Lift --complete/--schema sizing to its safety ceiling (1024 names, 128 expansion probes). |
235
249
  | `--curl` | | Render a ready-to-edit REST curl for statements covered by a runtime-tested mapping rule. |
236
250
  | `--tokens` | | Emit the total, gapless token partition behind `data.tokens[]` (every byte not claimed by an analyzer is `unclassified`). |
237
251
  | `--format` | &lt;text\|json\|yaml&gt; | Output format for the CLI response. |
@@ -73,15 +73,20 @@ gate passed keeps `passed`. When something stopped the gate before it gave a
73
73
  verdict (a `transport/*` or `auth/*` failure, a device error that is not a
74
74
  gate answer, or a refusal before the gate ran), `result` and the
75
75
  `syntax`/`semantic` summaries are absent. Each stage the gate did not reach is
76
- then `skipped` with `reason: "not reached: <error.code>"` (GH#435). On an
77
- `execute` rejection, `syntax: true` survives only for a name-level refusal (an
78
- unknown attribute in a structured command). Any other device or offline
79
- refusal is `syntax: false`, and `semantic` is then absent (`not-applicable` for
80
- script input), because the name-level check was never asked. `api`'s
81
- structured requests run no `:parse`, so their `syntax: false` means "not run",
82
- even on success. That overload is part of the review below. This is an
83
- interim shape: the validation meta across commands is reviewed again with live
84
- `explain` (#236).
76
+ then `skipped` with `reason: "not reached: <error.code>"` (GH#435).
77
+
78
+ The flat `syntax` / `semantic` fields summarize `stages[]`, and each is present
79
+ **only when a stage gave that verdict** (#535). Absent means no stage was
80
+ asked, never "passed" and never "failed". So `--validate=false` carries
81
+ neither. A script has no `semantic` field, because it has no names to check,
82
+ and `api`'s structured requests have no `syntax` field, because they run no
83
+ `:parse`. On a rejection, `syntax: true` with `semantic: false` is a
84
+ name-level refusal (an unknown attribute in a structured command). A `:parse`
85
+ or offline refusal is `syntax: false` with no `semantic`. Every gated success
86
+ reports its `stages[]`, retrieve and transfer included. `explain` is not a
87
+ gate, because it never runs the input: its `meta.validation` is
88
+ `{ enabled: false }` with no `result`, offline and live, and its verdict is
89
+ `data.verdict`.
85
90
 
86
91
  `--validate=false` disables **both** stages — one flag, one meaning. A bypass
87
92
  that covered only the device stage would leave a caller stranded behind a
package/docs/MATRIX.md CHANGED
@@ -40,7 +40,7 @@ their command-specific evidence contracts.
40
40
  | devices | — | — | — | — | — | — | — | — |
41
41
  | discover | — | — | — | — | — | `CHR-passed` | — | — |
42
42
  | check | `designed` | `designed` | `designed` | `designed` | `not-started` | `not-started` | `not-started` | `not-started` |
43
- | explain | `designed` | `designed` | — | — | — | — | — | — |
43
+ | explain | `CHR-passed` | `CHR-passed` | — | — | — | — | — | — |
44
44
  | settings | — | — | — | — | — | — | — | — |
45
45
 
46
46
  ## Status pointers
@@ -101,10 +101,14 @@ and implementation sequencing in the linked issues.
101
101
  `winbox-terminal` stay `not-started`. Its per-host fan-out is the machinery the
102
102
  IP-scan discovery (#149) will iterate; the L2-default timing evidence (#136) is
103
103
  recorded in the README before the cells advance past `designed`.
104
- - `explain`'s two `designed` protocol cells are the future live probes
105
- (`/console/inspect` + `:parse`, #236). The live grammar
106
- (`explain <router> '<input>'`) is parsed and refused with
107
- `usage/not-implemented`, never degraded to an offline run. Offline analysis
104
+ - `explain`'s two `CHR-passed` protocol cells are the whole live capability
105
+ (#236). `explain <router> '<input>'` collects read-only `:parse`, highlight
106
+ and child argument-name evidence (#491), and `--schema`/`--complete` describe
107
+ paths and complete at a cursor (#492). Live value types (#225) fill
108
+ `observedType` from visible `:parse` conversions and `schemaType` from
109
+ argument `syntax`. Examples 7–16, 19, 24, 48 and 52–53 pass on REST and
110
+ native API in `test/integration/explain-live*.test.ts`, on stable 7.24.5 and
111
+ long-term 7.23.8. `runtimeAcceptance` stays `not-proven`. Offline analysis
108
112
  has its own capability entry below. The generated structure tables
109
113
  `src/explain/menus.ts` and `src/explain/catalog.ts` keep analysis offline;
110
114
  their ownership and regeneration contracts live in `commands/explain/README.md`.
@@ -182,16 +186,25 @@ Wire, auth, TCP/UDP, and CI caveats live in `commands/btest/README.md` and the
182
186
 
183
187
  ## Current priority
184
188
 
189
+ Live `explain` (#236) is `CHR-passed` on REST and native API: the #491
190
+ foundation, #492's describe and completion facets, and #225's live value types,
191
+ with every applicable example green on stable and long-term CHR. Issue #90
192
+ owns the remaining sequencing, including #507 context grounding.
193
+
185
194
  **First: `explain`** (maintainer decision 2026-10-16 on
186
195
  [#90](https://github.com/tikoci/centrs/issues/90)). In order: make the
187
- published offline readings honest (#350, #211), give the result a statement
188
- tree, then the live probe foundation (#491). Then use those probes to
189
- ground #351, #358 and #371, and finish live (#492 → #236 closeout). The
196
+ published offline readings honest (#350, #211; complete), give the result a
197
+ statement tree (#497; complete), then the live probe foundation (#491;
198
+ complete). Those probes grounded #351, #358 and #371 (complete), #492
199
+ added the describe/completion facets, and #225 the live value types. The
200
+ closeout of #236 promoted both live cells (complete). The
190
201
  plan, and what is deliberately out of this run, is the 2026-10-16 plan
191
202
  comment on #90. It is step 3 of the validation track below.
192
203
 
193
- **The CMR track is paused until RouterOS 7.26beta2** (maintainer decision
194
- 2026-10-16). It was first from the 2026-10-12 decision on
204
+ **The CMR track is paused** (maintainer decision 2026-10-16; held for the
205
+ 7.26beta caller-policy fix — #527 now carries direct 7.26beta3 evidence that a
206
+ restricted caller is refused, with the first refusing release still
207
+ unbracketed). It was first from the 2026-10-12 decision on
195
208
  [#441](https://github.com/tikoci/centrs/issues/441). Done: `execute --cmr`
196
209
  (#450), which runs one validated command on CMR clients through their
197
210
  controller. The contract is in
@@ -222,11 +235,11 @@ order agreed on [#236](https://github.com/tikoci/centrs/issues/236):
222
235
  2. **Done** — front the `execute` / `api` device preflight with the offline
223
236
  analyzer (#354). The gate is now two-stage; the contract is in
224
237
  [`docs/CONSTITUTION.md` → Validation is the product](CONSTITUTION.md#validation-is-the-product).
225
- 3. **Next** — **live `explain` (#236)**, as stage two of a gate that now exists.
226
- Start with [the #90 task index](https://github.com/tikoci/centrs/issues/90),
227
- then follow [`commands/explain/README.md`](../commands/explain/README.md) and
228
- the live examples in
229
- [`commands/explain/examples.md`](../commands/explain/examples.md).
238
+ 3. **Done** — **live `explain` (#236)**, as stage two of a gate that now exists.
239
+ The contract is [`commands/explain/README.md`](../commands/explain/README.md)
240
+ and the live examples in
241
+ [`commands/explain/examples.md`](../commands/explain/examples.md);
242
+ [#90](https://github.com/tikoci/centrs/issues/90) indexes what follows.
230
243
 
231
244
  The offline baseline stays `verified`: #264 closed the measured token vocabulary
232
245
  and #322 closed the final bounded-execution gap, including the public depth-axis
@@ -235,17 +248,17 @@ guard.
235
248
  The live pass is the device-backed cross-check on the offline result, not a
236
249
  replacement for it. It adds read-only `/console/inspect` and `:parse` evidence,
237
250
  the describe/completion facets, and the live `observedType`/`schemaType`
238
- producers owned with #225. REST and native-api cells advance only when their
251
+ producers (#225). REST and native-api cells advance only when their
239
252
  applicable examples pass on CHR via `bun run test:integration`.
240
253
 
241
254
  Open offline follow-ups remain explicit but do not reopen the verified baseline.
242
255
  Issue #211 made an unlisted absolute bare path abstain (rule F, R14); rule
243
256
  P's closed-world reading is still not adopted (#372). #272 owns
244
- non-authoritative `.scratch/` citation cleanup. #350 (the broad optional `=`,
245
- schema-dependent and so belonging to step 3), #351 (an unresolvable menu that
246
- abstains at 2-deep nesting but hard-fails at 3-deep) and #355 (`'` is not a
247
- RouterOS token — a false ACCEPT the corpus join cannot see) are bounded analyzer
248
- defects that step 2 now surfaces through the `execute`/`api` gate. #336, #342 and #498 are complete: the
257
+ non-authoritative `.scratch/` citation cleanup. Issues #350 (the broad
258
+ optional `=`), #351 (a relative submenu brace read as a command operand)
259
+ and #355 (`'` is not a RouterOS token) are complete. A submenu block after a navigation statement
260
+ (`/ip⏎address { … }`) keeps the pre-#351 reading; see
261
+ `commands/explain/README.md`. #336, #342 and #498 are complete: the
249
262
  corpus is pinned to schema 5, with one recorded session per release channel
250
263
  (long-term 7.23.8, stable 7.24.5, testing 7.25rc2, development 7.26beta1) read
251
264
  through `scripts/corpus-db.ts`. The highlight slice is cut from those sessions