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

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 (91) hide show
  1. package/CHANGELOG.md +163 -0
  2. package/GLOSSARY.txt +23 -1
  3. package/README.md +30 -18
  4. package/commands/api/AGENTS.md +21 -3
  5. package/commands/api/README.md +18 -7
  6. package/commands/api/examples.md +1 -1
  7. package/commands/btest/README.md +9 -0
  8. package/commands/btest/examples.md +7 -0
  9. package/commands/devices/README.md +72 -3
  10. package/commands/devices/examples.md +56 -1
  11. package/commands/execute/README.md +37 -6
  12. package/commands/execute/examples.md +24 -5
  13. package/commands/explain/README.md +675 -64
  14. package/commands/explain/examples.md +199 -42
  15. package/commands/retrieve/README.md +35 -3
  16. package/commands/retrieve/examples.md +114 -1
  17. package/commands/transfer/README.md +33 -4
  18. package/commands/transfer/examples.md +46 -0
  19. package/docs/CLI.md +30 -8
  20. package/docs/CONSTITUTION.md +14 -9
  21. package/docs/MATRIX.md +37 -22
  22. package/docs/errors/cdb/encrypt-failed.md +10 -0
  23. package/docs/errors/cdb/password-required.md +5 -4
  24. package/docs/errors/cdb/plaintext-backups.md +11 -0
  25. package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
  26. package/docs/errors/input/invalid-routeros-path.md +25 -3
  27. package/docs/errors/routeros/permission-denied.md +24 -0
  28. package/docs/errors/transport/edge-auth-failed.md +22 -0
  29. package/docs/errors/transport/write-outcome-unknown.md +3 -0
  30. package/docs/errors/usage/invalid-cursor.md +20 -0
  31. package/docs/errors/validation/unknown-path.md +9 -0
  32. package/package.json +9 -5
  33. package/src/api-fanout.ts +2 -0
  34. package/src/api.ts +46 -39
  35. package/src/btest.ts +7 -0
  36. package/src/cli/api.ts +18 -1
  37. package/src/cli/btest.ts +9 -0
  38. package/src/cli/devices.ts +76 -8
  39. package/src/cli/execute.ts +9 -0
  40. package/src/cli/explain.ts +206 -47
  41. package/src/cli/retrieve.ts +9 -0
  42. package/src/cli/terminal.ts +9 -0
  43. package/src/cli/transfer.ts +10 -1
  44. package/src/core/envelope.ts +10 -3
  45. package/src/core/error-catalog.ts +28 -2
  46. package/src/core/execute-command.ts +53 -0
  47. package/src/core/fanout.ts +12 -2
  48. package/src/core/inspect.ts +4 -5
  49. package/src/core/menu-path.ts +89 -0
  50. package/src/core/routeros-errors.ts +25 -0
  51. package/src/devices.ts +154 -9
  52. package/src/execute-cmr.ts +2 -8
  53. package/src/execute-fanout.ts +2 -0
  54. package/src/execute.ts +55 -20
  55. package/src/explain/args.ts +115 -10
  56. package/src/explain/blocks.ts +2 -2
  57. package/src/explain/catalog.ts +22 -12
  58. package/src/explain/group-faults.ts +162 -29
  59. package/src/explain/operators.ts +2 -1
  60. package/src/explain/pasted-prompt.ts +198 -0
  61. package/src/explain/pathresolve.ts +2 -2
  62. package/src/explain/scope-brace.ts +226 -11
  63. package/src/explain/segment.ts +138 -3
  64. package/src/explain/symbols.ts +148 -8
  65. package/src/explain/values.ts +55 -4
  66. package/src/explain/verbsplit.ts +2 -2
  67. package/src/explain/write.ts +2 -2
  68. package/src/explain-live-facets.ts +739 -0
  69. package/src/explain-live-types.ts +490 -0
  70. package/src/explain-live.ts +593 -0
  71. package/src/explain.ts +397 -43
  72. package/src/index.ts +10 -0
  73. package/src/protocols/adapter.ts +90 -10
  74. package/src/protocols/btest-session.ts +10 -30
  75. package/src/protocols/ec-srp5.ts +19 -10
  76. package/src/protocols/mac-telnet-console.ts +2 -2
  77. package/src/protocols/mac-telnet.ts +38 -10
  78. package/src/protocols/native-api.ts +13 -12
  79. package/src/resolver/cdb.ts +58 -2
  80. package/src/resolver/index.ts +1 -0
  81. package/src/resolver/quickchr-provider.ts +5 -0
  82. package/src/resolver/selection.ts +11 -0
  83. package/src/retrieve-fanout.ts +2 -0
  84. package/src/retrieve-follow.ts +2 -1
  85. package/src/retrieve-sample.ts +2 -1
  86. package/src/retrieve-wait.ts +10 -6
  87. package/src/retrieve.ts +88 -27
  88. package/src/settings.ts +1 -0
  89. package/src/terminal.ts +7 -0
  90. package/src/transfer-fanout.ts +2 -0
  91. package/src/transfer.ts +117 -27
@@ -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
@@ -862,7 +888,14 @@ centrs explain '[admin@MikroTik] > /ip address print' --json
862
888
  `data.verdict` is `fail` with `explain/canonicalizer/invalid-at-sign` at the
863
889
  `@`. The device answers `syntax error (line 1 column 7)`, the same byte. This
864
890
  is a pasted console prompt. `:put a@b` and `comment=a@b` pass, because a `@` is
865
- a value byte in an argument.
891
+ a value byte in an argument. The envelope also carries
892
+ `tip/explain-pasted-prompt` offering the stripped `/ip address print` as
893
+ repair text (never applied). A `/menu>` prompt such as
894
+ `/interface/lte> monitor lte1` keeps a `warn` abstention with the same tip
895
+ shape, offering `monitor lte1` typed at `/interface/lte`. The manual's
896
+ in-menu prompt `[admin@MikroTik] /ip/address> add …` fails the same way and
897
+ offers `add …` typed at `/ip/address`; a HotLock `>>` is stripped with the
898
+ prompt.
866
899
 
867
900
  ### 41. A named comparison brace inside a `[find where …]` is rejected
868
901
 
@@ -959,3 +992,127 @@ The CHR controls in `test/integration/explain-structure.test.ts` parse the
959
992
  scope examples and compare stored-script creation, explicit `run`, and an
960
993
  immediate `:do` body. Those are test operations on disposable CHR; explain
961
994
  itself never executes the analyzed input.
995
+
996
+ ### 48. Safe argument discovery distinguishes spaced arguments from flags
997
+
998
+ ```bash
999
+ centrs explain $R '/interface set ether1 name x' -u $U -p $P --json
1000
+ centrs explain $R '/interface print detail without-paging' -u $U -p $P --json
1001
+ ```
1002
+
1003
+ `data.live.arguments[]` cites command-level child evidence and statement `s0`.
1004
+ The first result includes `name`, the second includes `detail`. Both parse;
1005
+ only `name x` gets the device's obsolete-syntax warning. An unknown argument
1006
+ control (`/interface set ether1 definitely-unknown-491=x`) carries the shared
1007
+ parse classifier's rejection. No command-level completion is requested.
1008
+
1009
+ The integration controls also explain a write-shaped identity change and a
1010
+ substitution without executing either, verify identity before/after, exercise
1011
+ CLI stdin and quickchr resolution, and skip oversized highlight without
1012
+ submitting a prefix. Parse/hash/normalization evidence preserves the Unicode
1013
+ byte coordinates. Run on long-term as well as stable before changing these
1014
+ claims. Sanitized captures can be regenerated with `CENTRS_EXPLAIN_CAPTURE`
1015
+ pointing at an ignored output file when running the integration suite.
1016
+
1017
+ ### 49. `?`, a backtick and a second `=` need quotes
1018
+
1019
+ ```bash
1020
+ centrs explain '/tool/fetch url=http://x/a?b=c' --json
1021
+ ```
1022
+
1023
+ `data.verdict` is `fail` with `explain/canonicalizer/unquoted-reserved-byte`
1024
+ at the `?`. The device answers `expected end of command (line 1 column 27)`,
1025
+ the same byte. A backtick is rejected the same way, in a value, a
1026
+ positional, a `where`, a `find`, an array and an expression. A second `=` in
1027
+ a menu command's argument is rejected at that `=`. So `name=a=b` and a bare
1028
+ base64 key ending in `=` both fail. The value's property parser never takes
1029
+ these bytes: `regexp=a?b` and `secret=abc==` are rejected too. In a menu
1030
+ command's argument name, the device reports from the name's first byte
1031
+ (`/ip/address/add a?b=1` at the `a`). `url="http://x/a?b=c"`,
1032
+ `where comment=a=b`, `[find comment=a=b]`, `{a=b}` and `# a?b` pass.
1033
+ `:put a=b` abstains: 7.24.5 reads it as a named argument and 7.23.8 rejects
1034
+ the `=`.
1035
+
1036
+ ### 50. A non-ASCII character outside a string is rejected
1037
+
1038
+ ```bash
1039
+ centrs explain ':put $café' --json
1040
+ ```
1041
+
1042
+ `data.verdict` is `fail` with `explain/canonicalizer/unquoted-non-ascii` over
1043
+ the `é`. The device answers `expected end of command (line 1 column 10)`, at
1044
+ the `é`'s first byte. RouterOS reads only ASCII outside a string. A bare
1045
+ value (`name=router-café`, `name=🛰`), a reference, a path (`/systemé`), a
1046
+ verb, an argument name, a `where` value, an array and an expression are all
1047
+ rejected. So are pasted non-breaking spaces, curly quotes and ellipses. A
1048
+ declared name (`:local café`) keeps its `invalid-name` error. Strings and
1049
+ comments carry any byte: `:put "café"`, `:local "café" 1`, `$"café"`,
1050
+ `name="router-🚀"`, `# café` and a multi-line string pass. The informational
1051
+ `non-ascii` fact is still published beside the error, and a leading BOM is
1052
+ not read as part of the first word.
1053
+
1054
+ ### 51. A brace after a submenu opens a block at any depth
1055
+
1056
+ ```bash
1057
+ centrs explain '/ip { address { print {1;2} } }' --json
1058
+ ```
1059
+
1060
+ `data.verdict` is `fail` with `explain/canonicalizer/invalid-command-brace` at
1061
+ the inner `{`. The device answers `expected end of command (line 1 column 23)`,
1062
+ the same byte. `address {` inside `/ip { … }` is a block over `/ip/address`,
1063
+ so `data.structure.statements` reads `/ip`, `/ip/address` and
1064
+ `/ip/address print`, and the `{1;2}` is the operand of `print`.
1065
+
1066
+ The device reads a brace after a menu word as statements whatever the body
1067
+ holds, at any depth. A body offline cannot resolve abstains with
1068
+ `unresolved-statement` instead of failing as an array:
1069
+ `/ip { address { 1;2 } }` (the device says `bad command name 1`) and
1070
+ `/ip { address { zzznotamenu { print } } }` (`syntax error`) both give `warn`,
1071
+ as `/ip/address { 1;2 }` and `/ip { zzznotamenu { print } }` do.
1072
+ `/ip { address { print } }` and `/ip { firewall { filter { print } } }` pass.
1073
+ The menu must be known from the source alone, so a block after a navigation
1074
+ statement (`/ip⏎address { … }`), an abbreviation (`addr {`) and a digit-led
1075
+ menu (`6to4 {`) keep the older reading.
1076
+
1077
+ ### 52. A live call observes a type only where the device converts a literal
1078
+
1079
+ ```bash
1080
+ 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
1081
+ ```
1082
+
1083
+ This is example 26's input against a target. The bare `2.2` gains
1084
+ `facts.observedType: { value: "ip", rendered: "2.0.0.2" }`, `200ms` gains
1085
+ `time` (`00:00:00.200`) and `yes` gains `bool` (`true`). Of the array's
1086
+ members, `0x10` is `num` (`16`) and `2.2` is `ip`, while `1` renders unchanged
1087
+ and gets nothing. The quoted `"2.2"` and the id `*1` also get nothing, because
1088
+ an unchanged rendering proves no type: `:parse` prints a string unquoted. The
1089
+ `shapeHints` are untouched, and no record has `schemaType`, because no value
1090
+ here is a menu command's argument.
1091
+
1092
+ A scripting directive's named argument is an operand too:
1093
+ `:for i from=0x10 to=20 do={:put $i}` gives `from` the fact `num` (`16`), and
1094
+ `to=20` renders unchanged and gets nothing. A NAME slot is a variable, never a
1095
+ value: in `:local yes 2` and `:local name=no value=yes`, the names `yes` and
1096
+ `no` get nothing (the device renders `name=$yes`, `name=$no`), while
1097
+ `value=yes` is `bool`.
1098
+
1099
+ Each fact cites one `:parse` evidence entry per context (operands, members)
1100
+ with `facet: "types"` and `basis: "derived"`. The probe re-parses each literal
1101
+ alone, as `:local vN <lit>` or `:local vN {<lit>}`; the analyzed input and
1102
+ the literals are never evaluated, and no `:typeof` is sent.
1103
+
1104
+ ### 53. A live call normalizes each argument's declared type
1105
+
1106
+ ```bash
1107
+ 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
1108
+ ```
1109
+
1110
+ Each `name=value` gains `facts.schemaType` from one argument-level `syntax`
1111
+ probe. `servers` is `{ values: ["ip", "ip6", "enum"], declared:
1112
+ "Address[@VRF]" }`, and `gateway` is `ip`, `ip6`, `enum`. `dist` resolves as
1113
+ the unique prefix of `distance` and is `num`. `comment` is `str`. `chain` is
1114
+ `enum` plus `str`, since custom chains are legal. `dst-port` is `num` with
1115
+ `list: true`. `disabled` gets nothing, because the device declares a boolean
1116
+ with a blank row. No menu argument gets `observedType`: its own parser
1117
+ converts it, which is the schema's slot. The text output prints
1118
+ `observed=ip(2.0.0.2)` and `schema=ip|ip6|enum` on the value rows.
@@ -87,12 +87,32 @@ 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.
95
98
 
99
+ A read only ever dispatches a `print`/`get`, on every transport and even with
100
+ `--validate=false`: that flag skips command validation, not the read/write
101
+ split (#586). Each path segment must be a RouterOS menu name — letters,
102
+ digits and hyphens (case folds), the grammar of every dir and command in the
103
+ 7.9–7.26 tree — or the read fails with `input/invalid-routeros-path` before
104
+ resolution. Before this check, a `#` or `?` cut the appended `/print` off the
105
+ REST URL, so `retrieve '/system/reboot#' --no-validate --attribute uptime`
106
+ posted `/rest/system/reboot` and rebooted CHR 7.24.5. A well-formed command
107
+ path is safe: RouterOS answers `POST /rest/system/reboot/print` (and a `GET` of
108
+ a command) with "no such command", and native API answers
109
+ `/system/reboot/print` the same way (CHR 7.24.5). Under the REST adapter, a
110
+ second guard refuses any request path containing `?`, `#`, `\`, a control
111
+ character, or a `.`/`..` segment (including the percent-encoded `%2e%2e` and
112
+ `.%2E` forms, which the URL parser also collapses) for every command. RouterOS
113
+ does not decode percent-escapes in REST paths, so nothing else changes the
114
+ target.
115
+
96
116
  SNMP validation is separate from RouterOS `/console/inspect`: OIDs and MIB
97
117
  names must resolve through the MIB cache before any SNMP request is sent.
98
118
 
@@ -147,6 +167,14 @@ handling.
147
167
  }
148
168
  ```
149
169
 
170
+ Projection rule: the bare value is about the attribute *count*, not the flag
171
+ spelling. On a singleton, exactly one projected attribute — `--attribute x`
172
+ or `--attributes x` alike — returns the bare value (`data` is the scalar,
173
+ not an object); two or more return an object with those keys. On a list, any
174
+ projection returns an array of objects, even a single attribute. Unprojected
175
+ reads return the full object (singleton) or the row array (list). (Examples
176
+ 5–6, 20, N5–N6 pin every cell of this table.)
177
+
150
178
  `--max-bytes` / `--max-rows` truncation populates
151
179
  `meta.truncated = { returned, total, totalBytes }` and keeps `ok: true` with a
152
180
  warning entry — it is not an error. (Earlier text said a byte overflow returns an
@@ -513,7 +541,11 @@ stays the single-target envelope; a selector flag or a second positional target
513
541
  switches to fan-out. `--quickchr <name>` (the named-live-provider) behaves like a
514
542
  lone positional: one flag stays single-target, repeating it fans out, and mixing
515
543
  it with positionals or CDB selectors is `usage/conflicting-flags` (constitution:
516
- resolution providers). The `--host` **flag** with no `<router>` positional
544
+ resolution providers). When several CDB records share one `<router>` (the
545
+ same address saved under two users), `--match user=<name>` (or
546
+ `target=<addr>`, or a record type) picks one, as on `devices show`; it is
547
+ single-target only (see
548
+ [devices → Ambiguity](../devices/README.md#ambiguity), #583). The `--host` **flag** with no `<router>` positional
517
549
  (`retrieve --host http://192.0.2.10:8080 /system/resource`) is a single target
518
550
  too: no CDB lookup, so auth comes from flags, env or config. `CENTRS_HOST` never
519
551
  stands in for `<router>`.
@@ -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.
@@ -129,6 +146,43 @@ centrs retrieve $R /not/a/real/path --username $U --password $P --json
129
146
  centrs retrieve $R /system/resource --attribute bogus --username $U --password $P --json
130
147
  ```
131
148
 
149
+ ## Read paths dispatch only reads (#586)
150
+
151
+ A read sends only a `print`/`get`, even with `--validate=false`. Covered by
152
+ the CHR test `test/integration/read-path-guard.test.ts` (RP1–RP2, over REST
153
+ and native-api, for `retrieve` and `api`; the router's uptime must never go
154
+ backwards) and the offline grammar and REST-guard tests in
155
+ `test/unit/menu-path.test.ts` (RP3).
156
+
157
+ ### RP1. A `#` or `?` in the path is refused before dispatch
158
+
159
+ ```bash
160
+ centrs retrieve $R '/system/reboot#' --no-validate --attribute uptime --username $U --password $P --json
161
+ centrs api $R '/system/reboot?' --validate=false --attribute uptime --username $U --password $P --json
162
+ ```
163
+
164
+ Envelope: `ok: false`, `error.code=input/invalid-routeros-path`,
165
+ `error.context.invalidSegments` names the bad segment, and nothing is sent.
166
+ Before #586 the first line posted `/rest/system/reboot` and rebooted the CHR.
167
+
168
+ ### RP2. A well-formed command path is RouterOS's refusal, not a dispatch
169
+
170
+ ```bash
171
+ centrs retrieve $R /system/reboot --no-validate --username $U --password $P --json
172
+ centrs retrieve $R /system/reset-configuration --no-validate --attribute uptime --username $U --password $P --json
173
+ ```
174
+
175
+ Envelope: `ok: false` with a `routeros/*` code. RouterOS answers
176
+ `<command>/print` and a `GET` of a command with "no such command", so the
177
+ appended `/print` is the guarantee once the path cannot cut it off.
178
+
179
+ ### RP3. Menu-name grammar and the REST URL guard
180
+
181
+ Path segments are letters, digits and hyphens (case folds:
182
+ `/IP/Address` is accepted). Under every REST caller, a request path with
183
+ `?`, `#`, `\`, a control character or a `.`/`..` segment (including
184
+ `%2e%2e` and `.%2E`) fails `input/invalid-routeros-path` before `fetch`.
185
+
132
186
  ## Transport / error contract
133
187
 
134
188
  ### 12. Bad credentials
@@ -362,6 +416,10 @@ centrs retrieve $A /interface --attributes name,type --via native-api --port $AP
362
416
 
363
417
  `data` is an array of objects each containing only `name` and `type`.
364
418
 
419
+ A single attribute (`--attributes name`) keeps the array shape here too: an
420
+ array of single-key objects. On a singleton, one item via either flag returns
421
+ the bare value (N5) — the rule is the count, not the flag.
422
+
365
423
  ### N7. `--all-attributes` (native `print detail`)
366
424
 
367
425
  ```bash
@@ -380,6 +438,14 @@ centrs retrieve $A /system/resource --list-attributes --via native-api --port $A
380
438
  centrs retrieve $A /not/a/real/path --via native-api --port $API_PORT --username $U --password $P
381
439
  ```
382
440
 
441
+ `/system/routerboard` on CHR earns the targeted remediation (no routerboard
442
+ outside MikroTik hardware; read `/system/license` for the license ID
443
+ instead), same as example 10.
444
+
445
+ ```bash
446
+ centrs retrieve $A /system/routerboard --via native-api --port $API_PORT --username $U --password $P
447
+ ```
448
+
383
449
  ### N10. Unknown attribute → `validation/unknown-attribute`
384
450
 
385
451
  ```bash
@@ -792,3 +858,50 @@ centrs retrieve $R /interface --until name=missing --wait 500ms --username $U --
792
858
  One envelope goes to stdout, with `stopReason: "deadline-elapsed"`; exit 1.
793
859
  No intermediate sample frames are printed. Unit anchors cover slow validation,
794
860
  unfinished reads, cancellation and narrow retry classification.
861
+
862
+ ## Record selection (`--match`, #583)
863
+
864
+ Covered by the named tests `MA1`–`MA3` in `test/integration/cdb-match.test.ts`
865
+ on disposable CHR; MA4 is the offline contract in the `MA4.` tests of
866
+ `test/unit/cdb-match.test.ts`, which drive `retrieve`/`api` through the CLI. `$CDB` holds
867
+ two `ipAdmin` records for the same target `$R`: user `$U` with a deliberately
868
+ wrong password, and a `read`-group user `reader` with its real password.
869
+ The same selector works on `execute`, `api`, `transfer`, `terminal` and
870
+ `btest client`.
871
+
872
+ ### MA1. Two records for one target are ambiguous
873
+
874
+ ```bash
875
+ centrs retrieve $R /system/identity --cdb-file $CDB --json
876
+ ```
877
+
878
+ Envelope: `ok: false`, `error.code=identity/ambiguous`, `context.matches`
879
+ lists both records, and the remediation names `--match user=<name>`.
880
+
881
+ ### MA2. `--match user=` picks the record and its credentials
882
+
883
+ ```bash
884
+ centrs retrieve $R /system/identity --cdb-file $CDB --match user=reader --json
885
+ centrs execute $R /system/identity/print --cdb-file $CDB --match user=reader --json
886
+ centrs api $R system/identity --cdb-file $CDB --match user=reader --json
887
+ ```
888
+
889
+ Each is `ok: true` with `meta.target.recordIndex=1`. With
890
+ `--match user=$U` instead, each fails `transport/auth-failed`: the decoy's
891
+ wrong password went on the wire, so the selection is real.
892
+
893
+ ### MA3. A selector that matches nothing
894
+
895
+ ```bash
896
+ centrs retrieve $R /system/identity --cdb-file $CDB --match user=nobody --json
897
+ ```
898
+
899
+ Envelope: `ok: false`, `error.code=identity/no-match`.
900
+
901
+ ### MA4. `--match` is single-target and never ignored
902
+
903
+ With a fan-out selection (`--group`, `--where`, `--all`, several
904
+ positionals) or `--quickchr`, `--match` is `usage/conflicting-flags`. When no
905
+ CDB record is in play (no CDB file, an unregistered literal host, a
906
+ `__default__`-only fallback) it is `identity/no-match`, never a silent
907
+ literal-host read.
@@ -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`):
@@ -118,6 +118,31 @@ past 60 KB) rather than quietly switching to sftp. `scp`, `fetch`, and `ftp` are
118
118
  explicit-only; `ftp` additionally requires `ALLOW_UNSAFE_PROTOCOLS=ftp` because
119
119
  it is cleartext.
120
120
 
121
+ The method resolves `--via` → `CENTRS_TRANSFER_VIA` → the CDB record's `via=`
122
+ preference → config → auto ([constitution: settings](../../docs/CONSTITUTION.md)).
123
+ A record's `via=` names a *transport* (shared with `execute`), so transfer maps
124
+ it to a method (#591):
125
+
126
+ | CDB / `--via` transport name | Transfer method |
127
+ | --- | --- |
128
+ | `ssh` | `sftp` |
129
+ | `rest-api` (also `--via rest`) | `rest` |
130
+ | `native-api` (also `--via native`) | `native` |
131
+ | `mac-telnet`, `romon`, `winbox-terminal`, `snmp`, `mndp`, `btest` | none — `settings/invalid-via` |
132
+
133
+ `--via ssh` (and `CENTRS_TRANSFER_VIA=ssh`) is accepted as an alias of
134
+ `sftp`. A record whose `via=` cannot carry files fails `settings/invalid-via`
135
+ naming the record, rather than being ignored; pass `--via sftp|rest|native`
136
+ for that transfer. A record `via=` holds a transport id: method names
137
+ (`sftp`, `rest`, `ftp`) and unknown values are rejected when the record is
138
+ read (`cdb/invalid-option` warning) and leave the method on auto. A record
139
+ preference is a setting, not an auto hop: a `via=rest-api` record uploading
140
+ more than 60 KB fails `transport/unsupported-operation` like a pinned
141
+ `--via rest` does, and the remediation names the record. The endpoint and
142
+ credentials still come from the same record; the mapping never selects a
143
+ different one. A record's `port=` travels with a `--via` pin, so if the
144
+ record's port belongs to another service, point `--port` at the pinned one.
145
+
121
146
  **File contents are bytes on `rest`/`native`.** RouterOS carries `/file`
122
147
  contents as raw bytes on both wires — REST puts bytes 0x80–0xff unescaped in the
123
148
  JSON string, so the body is not UTF-8 — and stores the UTF-8 encoding of every
@@ -302,6 +327,10 @@ gate as in `retrieve` / `execute`; RouterOS's own menu errors (missing remote
302
327
  file, permission denied) map to `routeros/*`, and SFTP/SCP/fetch errors map to
303
328
  `transport/*` and `auth/*`.
304
329
 
330
+ `meta.validation` reports that existence probe as one `device` stage. `list`
331
+ runs no probe, so it is not a gate: its `meta.validation` is
332
+ `{ enabled: false }`, on success and on failure (#535).
333
+
305
334
  A failure **after** target resolution keeps what resolution found: the error
306
335
  envelope carries the resolved `meta.target` (host, port, identity, sources),
307
336
  `meta.via`, the setting sources in `meta.settings`, and `meta.operation`
@@ -371,9 +400,9 @@ done rule. The first coded pass targets `rest`/`native` (no SSH dependency);
371
400
  (random URL token, bound to the interface facing the router) and drives the
372
401
  router with `/tool/fetch` over `rest`/`native`. **upload**: serve the local
373
402
  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
403
+ **download** stays on chunked `/file/read`: router PUTs fail with `only
404
+ [s]ftp modes support upload` (#548), and HTTP `http-data` caps at 60000
405
+ bytes, so the PUT half cannot carry files. Open questions: advertise-host
377
406
  auto-detection behind NAT, HTTP-vs-HTTPS + `check-certificate`, and whether to
378
407
  add fetch's `user`/`password` on top of the URL token.
379
408
  - **ssh** — `sftp` is **built** (`src/protocols/sftp.ts`, host OpenSSH `sftp`