@tikoci/centrs 0.1.3 → 0.1.4

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 (90) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/GLOSSARY.txt +213 -0
  3. package/LICENSE +21 -0
  4. package/README.md +36 -8
  5. package/commands/AGENTS.md +7 -1
  6. package/commands/api/README.md +19 -18
  7. package/commands/api/examples.md +15 -0
  8. package/commands/btest/README.md +21 -32
  9. package/commands/check/README.md +6 -1
  10. package/commands/discover/README.md +5 -11
  11. package/commands/execute/README.md +41 -29
  12. package/commands/execute/examples.md +41 -3
  13. package/commands/explain/README.md +1716 -0
  14. package/commands/explain/examples.md +606 -0
  15. package/commands/retrieve/README.md +25 -27
  16. package/commands/retrieve/examples.md +16 -0
  17. package/commands/transfer/README.md +28 -20
  18. package/commands/transfer/examples.md +29 -0
  19. package/docs/CLI.md +353 -0
  20. package/docs/CONSTITUTION.md +66 -15
  21. package/docs/MATRIX.md +38 -1
  22. package/docs/errors/README.md +3 -0
  23. package/docs/errors/quickchr/machine-not-found.md +22 -0
  24. package/docs/errors/quickchr/machine-stopped.md +20 -0
  25. package/docs/errors/quickchr/package-unavailable.md +27 -0
  26. package/docs/errors/quickchr/unsupported-via.md +35 -0
  27. package/docs/errors/quickchr/unsupported.md +35 -0
  28. package/docs/errors/routeros/version-unsupported.md +26 -0
  29. package/docs/errors/transport/provider-trust.md +29 -0
  30. package/docs/errors/usage/stdin-ignored.md +28 -0
  31. package/docs/errors/validation/unknown-attribute.md +37 -4
  32. package/package.json +45 -10
  33. package/src/api-fanout.ts +29 -14
  34. package/src/api.ts +97 -54
  35. package/src/cli/api.ts +25 -6
  36. package/src/cli/btest.ts +18 -2
  37. package/src/cli/execute.ts +25 -6
  38. package/src/cli/explain.ts +503 -0
  39. package/src/cli/retrieve.ts +33 -1
  40. package/src/cli/selection.ts +71 -11
  41. package/src/cli/settings.ts +2 -2
  42. package/src/cli/transfer.ts +23 -6
  43. package/src/cli.ts +15 -2
  44. package/src/core/envelope.ts +5 -2
  45. package/src/core/error-catalog.ts +40 -0
  46. package/src/core/preflight.ts +306 -0
  47. package/src/core/routeros-string.ts +24 -0
  48. package/src/errors.ts +1 -0
  49. package/src/execute-fanout.ts +29 -14
  50. package/src/execute.ts +114 -63
  51. package/src/explain/arg-tokens.ts +114 -0
  52. package/src/explain/args.ts +1403 -0
  53. package/src/explain/blocks.ts +132 -0
  54. package/src/explain/brace-slots.ts +143 -0
  55. package/src/explain/brace-tokens.ts +38 -0
  56. package/src/explain/catalog.ts +1345 -0
  57. package/src/explain/coordinates.ts +329 -0
  58. package/src/explain/defects.ts +217 -0
  59. package/src/explain/is-known-menu.ts +17 -0
  60. package/src/explain/menus.ts +664 -0
  61. package/src/explain/operator-tokens.ts +310 -0
  62. package/src/explain/operators.ts +458 -0
  63. package/src/explain/path-tokens.ts +150 -0
  64. package/src/explain/pathresolve.ts +1324 -0
  65. package/src/explain/quoted-string.ts +241 -0
  66. package/src/explain/scope-brace.ts +308 -0
  67. package/src/explain/segment.ts +757 -0
  68. package/src/explain/string-tokens.ts +67 -0
  69. package/src/explain/symbol-values.ts +520 -0
  70. package/src/explain/symbols.ts +1597 -0
  71. package/src/explain/token-ranges.ts +72 -0
  72. package/src/explain/transport.ts +497 -0
  73. package/src/explain/value-tokens.ts +80 -0
  74. package/src/explain/values.ts +357 -0
  75. package/src/explain/verbs.ts +64 -0
  76. package/src/explain/verbsplit.ts +813 -0
  77. package/src/explain/write.ts +815 -0
  78. package/src/explain.ts +2152 -0
  79. package/src/index.ts +206 -4
  80. package/src/protocols/mac-telnet-console.ts +61 -17
  81. package/src/resolver/facts.ts +1 -1
  82. package/src/resolver/index.ts +23 -4
  83. package/src/resolver/quickchr-provider.ts +597 -0
  84. package/src/resolver/selection.ts +99 -12
  85. package/src/resolver/service-endpoint.ts +155 -0
  86. package/src/resolver/target.ts +1 -1
  87. package/src/retrieve-fanout.ts +26 -12
  88. package/src/retrieve.ts +81 -18
  89. package/src/transfer-fanout.ts +42 -25
  90. package/src/transfer.ts +87 -44
@@ -0,0 +1,606 @@
1
+ # explain — examples
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 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**:
10
+ those cells are `designed`, and flag and field names track the ratified surface
11
+ in `README.md`.
12
+
13
+ A letter-suffixed example (`1b`, `4b`, `18b`) is the **counterpart** of the number it
14
+ follows: the same question with the other answer, added where implementing the
15
+ spec showed that one example was carrying two contradictory readings. It gets its
16
+ own assertion (`1b` ↔ assertion `1b`); the numbering never renumbers, because
17
+ these numbers are cited from issues and commit messages.
18
+
19
+ `$R` is `<host>:<rest-port>` resolved by quickchr; `$A` is `<host>` and
20
+ `$API_PORT` is the native API port (`chr.ports.api`); `$U` / `$P` are CHR
21
+ credentials from the harness. Live examples are **target-first**
22
+ (`explain <router> '<input>'`); RouterOS input is single-quoted so the shell
23
+ never expands `$vars` or splits on `>`. Envelope-asserting examples pass
24
+ `--json`; live examples ride **rest-api** unless they explicitly pin
25
+ `--via native-api`.
26
+
27
+ ## Offline (no router, no CDB — canonicalizer only)
28
+
29
+ ### 1. Canonical form, write shape, and transport
30
+
31
+ ```bash
32
+ centrs explain '/ip/route/add dst-address=10.9.0.0/16 gateway=192.0.2.1' --json
33
+ ```
34
+
35
+ `ok: true`; `data.canonical` is
36
+ `{ path: "/ip/route", verb: "add", mode: "structured", writeShaped: true }`
37
+ with the args split out; `data.structure.statements[0].resolution` is
38
+ `"resolved"` and its `transport.classification` is `"api-candidate"` (the
39
+ statement is covered by a tested REST mapping rule); `data.verdict` is `pass`;
40
+ exit `0`.
41
+
42
+ **The verb must be IN the path for `mode` to be `structured`.** This example
43
+ originally spelled the input `/ip/route add …`, which the execute gate reads as
44
+ `script` — `add` is a bare token with no `=`, and widening the gate to accept it
45
+ is a product regression (`docs/CONSTITUTION.md`). `data.canonical` reproduces
46
+ that gate verbatim, so the example uses the spelling that is actually
47
+ structured. The space spelling is example 1b.
48
+
49
+ ### 1b. The CLI spelling is script to the gate and a command to the analysis
50
+
51
+ ```bash
52
+ centrs explain '/ip/route add dst-address=10.9.0.0/16 gateway=192.0.2.1' --json
53
+ ```
54
+
55
+ `data.canonical.mode` is `"script"` and `writeShaped` is `false` — the gate's
56
+ answer to "may this be sent as a structured REST call". Beside it,
57
+ `data.structure.statements[0].command` is `{ path: "/ip/route", verb: "add" }`
58
+ and `data.structure.containsWrite` is `true`: the analysis answers "what did the
59
+ human write", and the two questions have different answers over the same bytes.
60
+ Neither is wrong and the gate is never widened by the analysis.
61
+
62
+ ### 2. Script mode is reported, not widened — and routes to execute
63
+
64
+ ```bash
65
+ centrs explain ':foreach i in=[/ip/address find] do={ :put $i }' --json
66
+ ```
67
+
68
+ `data.canonical.mode` is `"script"` and `writeShaped` reflects the gate's
69
+ conservative verdict — the same answer `execute` would act on.
70
+ Each resolved command under `data.structure.statements[]` classifies
71
+ `"execute"` with the `centrs execute` invocation rendered, so the right runner
72
+ surfaces even for script input.
73
+
74
+ ### 3. Sub-command paths are re-constituted; the gate verdict is untouched
75
+
76
+ ```bash
77
+ centrs explain '/ip/address remove [find comment=defconf]' --json
78
+ ```
79
+
80
+ `data.canonical` matches `canonicalizeExecuteCommand` exactly as its anchor
81
+ tests pin it: `mode: "script"`, `writeShaped: false` (a bracketed sub-command
82
+ selector is never write-shaped structured). The richer inference lives beside
83
+ it: `data.structure.subcommands` exposes the inner command with its resolved
84
+ enclosing path `{ path: "/ip/address", verb: "find" }` plus its span, and
85
+ `data.structure.containsWrite` is `true` (basis `heuristic` in the referenced
86
+ evidence entry) because of the outer `remove`.
87
+
88
+ ### 4. Same token, different role: `comment` as verb vs argument
89
+
90
+ ```bash
91
+ centrs explain '/ip/address/comment numbers=0 comment=uplink' --json
92
+ ```
93
+
94
+ `data.canonical.verb` is `"comment"` — at this position `comment` is the
95
+ RouterOS *verb* — while `data.canonical.args` carries the `comment` *argument*
96
+ with its value. The two roles of the same word are distinguished by position,
97
+ and both facts cite canonicalizer evidence (offline heuristics; live
98
+ completion is the authority — see examples 13–15).
99
+
100
+ **The verb must be IN the path here too**, for the same reason as example 1:
101
+ this example originally spelled the input `/ip/address comment …`, which the
102
+ gate reads as `script` with an empty `path`/`verb`/`args`, so neither role was
103
+ visible in `canonical` at all. The two-role contrast is a claim about the
104
+ gate's split, so the example uses the spelling the gate splits. The space
105
+ spelling is example 4b.
106
+
107
+ ### 4b. The space spelling keeps both roles in the analysis, and neither in the gate
108
+
109
+ ```bash
110
+ centrs explain '/ip/address comment numbers=0 comment=uplink' --json
111
+ ```
112
+
113
+ `data.canonical.mode` is `"script"` with `verb: ""` and `args: {}` — the gate
114
+ declines the whole input, so it distinguishes no roles at all.
115
+ `data.structure.statements[0].command` is
116
+ `{ path: "/ip/address", verb: "comment", args: { numbers: "0", comment: "uplink" } }`:
117
+ the analysis reads the verb role from either spelling, and since #202c it reads
118
+ the *argument* role here too. Both roles of the same word, from bytes the gate
119
+ declined — which is the clearest statement of why the two surfaces are separate.
120
+
121
+ ### 5. Offline is honest about what it cannot know
122
+
123
+ ```bash
124
+ centrs explain '/ip/address print' --schema --json
125
+ ```
126
+
127
+ No router was named, so no enumeration is fabricated: `data.schema` is absent
128
+ (or empty with `truncated: false`), every emitted fact cites `canonicalizer`
129
+ evidence, and a tip states that richer schema/completion data is available
130
+ when a router is provided.
131
+
132
+ ### 6. curl rendering with a placeholder host
133
+
134
+ ```bash
135
+ centrs explain '/ip/address print' --curl --json
136
+ ```
137
+
138
+ The first statement's `transport.rest` carries the method +
139
+ `/rest/ip/address` mapping and its `transport.curl` renders a ready-to-edit
140
+ `curl` command using a placeholder host (no router was given) and elided
141
+ credentials; the equivalent `centrs api` invocation is rendered alongside. A
142
+ statement *not* covered by a tested mapping rule classifies `"unknown"` and
143
+ renders no curl.
144
+
145
+ ## Live (CHR target, rest-api)
146
+
147
+ ### 7. Describe a path: verbs first
148
+
149
+ ```bash
150
+ centrs explain $R /ip/address -u $U -p $P --schema --json
151
+ ```
152
+
153
+ `data.schema.verbs` lists the verbs available at `/ip/address` (`add`, `set`,
154
+ `remove`, `print`, …), each with an arg count or a drill-down hint rather than
155
+ the full arg expansion; facts cite `live-inspect` evidence stamped with the
156
+ CHR's RouterOS version.
157
+
158
+ ### 8. Describe a verb: its arguments
159
+
160
+ ```bash
161
+ centrs explain $R /ip/address/set -u $U -p $P --schema --json
162
+ ```
163
+
164
+ `data.schema` enumerates the settable arguments of `set` (`address`,
165
+ `interface`, `disabled`, …) with types where the device reports them.
166
+
167
+ ### 9. Describe print: the proplist special case
168
+
169
+ ```bash
170
+ centrs explain $R /ip/address/print -u $U -p $P --schema --json
171
+ ```
172
+
173
+ `data.schema` returns the `.proplist` value set — what the output *can*
174
+ contain — rather than treating `print` like a plain verb. The grounded probe is
175
+ `completion input="/ip/address/print proplist="` (dot-free console spelling),
176
+ filtered to rows with `show=true`; each returned fact cites that live evidence.
177
+
178
+ ### 10. Smart sizing truncates with counts, `--full` lifts it
179
+
180
+ ```bash
181
+ centrs explain $R /ip -u $U -p $P --schema --json
182
+ ```
183
+
184
+ The `/ip` subtree is large: `data.schema.truncated` is `true`, counts are
185
+ reported in place of the full expansion, and a warning/hint names the
186
+ narrower query (or `--full`) that expands it.
187
+
188
+ ### 11. Spans and the error byte (centrs vocabulary)
189
+
190
+ ```bash
191
+ centrs explain $R '/ip/address add address=bogus interface=' -u $U -p $P --json
192
+ ```
193
+
194
+ `data.spans` classify the input byte-by-byte using the **centrs** span
195
+ vocabulary (not raw RouterOS highlight classes); the first hard error's byte
196
+ offset appears in a diagnostic citing `live-inspect` evidence.
197
+
198
+ ### 12. Structure via :parse
199
+
200
+ ```bash
201
+ centrs explain $R ':if (1 > 2) do={ :put x' -u $U -p $P --json
202
+ ```
203
+
204
+ The unclosed block yields a `:parse`-derived diagnostic; no partial structure
205
+ is fabricated past the first hard error. The line/column detail is asserted
206
+ here over rest-api; example 24 asserts the same readout over native API using
207
+ `/execute as-string`.
208
+
209
+ ### 13. Completion: partial argument name
210
+
211
+ ```bash
212
+ centrs explain $R '/ip/address add comm' -u $U -p $P --complete --json
213
+ ```
214
+
215
+ `data.completion` includes `comment` as a completing candidate for the
216
+ partial word `comm` — a valid partial, no unknown-word diagnostic.
217
+
218
+ ### 14. Completion: complete argument name, no value yet
219
+
220
+ ```bash
221
+ centrs explain $R '/ip/address add comment' -u $U -p $P --complete --json
222
+ ```
223
+
224
+ The candidate set marks `comment` as a complete argument name expecting a
225
+ value (`=` continuation) — distinct from example 13's partial match and from
226
+ an unknown word.
227
+
228
+ ### 15. Completion: value position
229
+
230
+ ```bash
231
+ centrs explain $R '/ip/firewall/filter add chain=' -u $U -p $P --complete --json
232
+ ```
233
+
234
+ With the input ending in `=`, candidates are **values** for `chain`
235
+ (`forward`, `input`, `output` — labeled as observed, never a closed set). For
236
+ a free-form argument like `comment=`, the same probe yields type info (string)
237
+ rather than candidates.
238
+
239
+ ### 16. Runtime acceptance is never claimed
240
+
241
+ ```bash
242
+ centrs explain $R '/ip/route add dst-address=10.9.0.0/16 blackhole=yes' -u $U -p $P --json
243
+ ```
244
+
245
+ Even when every probe passes, `data.runtimeAcceptance` is `"not-proven"` —
246
+ the inspect-vs-runtime gap stays machine-readable.
247
+
248
+ ### 17. Fan-out is rejected
249
+
250
+ ```bash
251
+ centrs explain --group lab '/ip/route print' --json
252
+ ```
253
+
254
+ `ok: false` with `usage/fanout-not-supported`; `explain` takes at most one
255
+ router.
256
+
257
+ ## Phase-0-derived contract anchors
258
+
259
+ These examples are the product-facing subset of the phase-0 findings. The
260
+ larger mutation, coordinate, and stress matrices live as product-owned fixture
261
+ tests per README phase 0.5; no test imports `.scratch/` code.
262
+
263
+ ### 18. A bare path neither table knows is ambiguous offline
264
+
265
+ ```bash
266
+ centrs explain '/disk/format-drive' --json
267
+ ```
268
+
269
+ The first statement has `resolution: "ambiguous"`: without a schema, the same
270
+ shape can be a menu or a no-argument command. It carries no invented `command`,
271
+ its transport classification is `"unknown"`, and a canonicalizer diagnostic
272
+ explains the menu-vs-command ambiguity. `data.verdict` remains the independent
273
+ diagnostic severity summary; it is not the statement-resolution field.
274
+
275
+ **The input has changed twice, for the same reason each time.** It first read
276
+ `/ip/route`, from before the baked menu table existed; #207 decided that one (see
277
+ example 18b). It then read `/system/reboot`, which #228's published command axis
278
+ decides (see example 18c). Both tables are floors, so the example needs a path in
279
+ *neither*: `/disk/format-drive` is real enough to appear in the corpus, but 7.23.2
280
+ spells it `/disk format`, and MikroTik publishes no such entry.
281
+
282
+ ### 18b. A bare path the menu table DOES know resolves as a menu, offline
283
+
284
+ ```bash
285
+ centrs explain '/ip/route' --json
286
+ ```
287
+
288
+ `resolution: "resolved"` with `kind: "menu"` and `command: { path: "/ip/route" }`
289
+ — no verb, because a navigation statement names only a menu. The evidence is the
290
+ generated table (#207), not a guess about hyphens or token counts, and it is why
291
+ the export-abstention rate went to zero. Example 19 asserts the same reading
292
+ against a live device, which is a confirmation rather than a promotion.
293
+
294
+ ### 18c. A bare path the COMMAND axis knows resolves as a command, offline
295
+
296
+ ```bash
297
+ centrs explain '/system/reboot' --json
298
+ ```
299
+
300
+ `resolution: "resolved"` with `kind: "command"`, `command.path: "/system"` and
301
+ `command.verb: "reboot"` — the other half of the twin in example 18b, from the
302
+ other half of the same generated table (#228). `/system/reboot` and `/ip/address`
303
+ are the same text shape, and offline now decides both from published evidence
304
+ instead of refusing both.
305
+
306
+ `structure.containsWrite` is deliberately *not* moved by this: knowing a path is a
307
+ command says nothing about whether it mutates, so this document still reports
308
+ `"unknown"`. Examples 18d and 18e are the two things the same evidence *does*
309
+ move.
310
+
311
+ ### 18d. A published command does not move the menu context
312
+
313
+ ```bash
314
+ centrs explain '/ip route
315
+ /system reboot
316
+ add dst-address=8.8.8.8/32 gateway=1.1.1.1' --json
317
+ ```
318
+
319
+ Three statements: `resolved`/`menu` at `/ip/route`, `resolved`/`command` at
320
+ `/system` verb `reboot`, and `resolved`/`command` at **`/ip/route`** verb `add` —
321
+ not at `/system/reboot`. A command is not navigation, so the context it does not
322
+ establish is not inherited by what follows. This was `#211 B2`, pinned as a KNOWN
323
+ LIMIT until the command axis supplied the positive evidence that a path is a
324
+ command; it is why `structure.containsWrite` is `true` here rather than describing
325
+ an `add` under a menu that does not exist.
326
+
327
+ ### 18e. A published command outranks the punctuation guess
328
+
329
+ ```bash
330
+ centrs explain '/system/gps/monitor once' --json
331
+ ```
332
+
333
+ `command.verb: "monitor"` with `once` inside the argument list — where the
334
+ schema-free punctuation rule reads the first space-separated token as the verb and
335
+ so lands on `once`. The publication names the command, so the operand goes back to
336
+ being an operand. `structure.containsWrite` is `false`, because `monitor` is a
337
+ curated read verb; before the command axis this document abstained.
338
+
339
+ ### 19. Live evidence confirms the same bare path as a menu
340
+
341
+ ```bash
342
+ centrs explain $R '/ip/route' -u $U -p $P --json
343
+ ```
344
+
345
+ `/console/inspect` completion and highlight agree that the terminal token is a
346
+ `dir`; the first statement is `resolution: "resolved"`, `kind: "menu"`, and
347
+ has no runnable transport rendering. The evidence entry is version-stamped and
348
+ the probe never executes the path. Since #207 the offline reading (example 18b)
349
+ already says the same thing from the generated table, so what the live probe
350
+ adds here is **provenance and a version stamp**, not the answer — and it is the
351
+ oracle for a path the table does not list, where offline abstains (example 18).
352
+
353
+ ### 20. Explain-only write detection is three-valued
354
+
355
+ ```bash
356
+ centrs explain '/ip/address add address=198.51.100.10/32 interface=ether1' --json
357
+ centrs explain '/ip/address print' --json
358
+ centrs explain '/disk format-drive disk1' --json
359
+ ```
360
+
361
+ `data.structure.containsWrite` is respectively `true`, `false`, and
362
+ `"unknown"`. The third result must not become `false`: `format-drive` is
363
+ write-shaped but outside the small, version-stable write table. These values do
364
+ not alter the execute gate's `canonical.writeShaped` verdict.
365
+
366
+ ### 21. A defect cannot fabricate a following command
367
+
368
+ ```bash
369
+ centrs explain '/interface bridge add name=br;0 protocol-mode=none' --json
370
+ ```
371
+
372
+ The injected `;` has RouterOS statement-separator semantics, so the input is two
373
+ statements, not one with a corrupt value. The tail statement beginning `0`
374
+ resolves `"unknown"` — a bare-word head fails closed — with no confident `/r0`
375
+ command or runnable transport rendered, and a
376
+ `explain/canonicalizer/unresolved-statement` diagnostic carries **that
377
+ statement's** byte region. The analysis itself is `ok: true`; `data.verdict` and
378
+ `--fail-on` report the diagnostic.
379
+
380
+ **No defect region is emitted for the `;` itself.** A stray mid-token delimiter
381
+ is one of the two classes deliberately NOT detected (see *Result shape* → defect
382
+ regions, and #192): it is plausible but ungrounded, and it needs a probe matrix
383
+ before a detector is worth having. What this example pins is the fail-closed
384
+ floor that matters — corruption may degrade a reading, never invent one.
385
+
386
+ ### 22. Normalization preserves device byte offsets and LSP positions
387
+
388
+ ```bash
389
+ centrs explain '/system identity set name="router-🚀"' --json
390
+ ```
391
+
392
+ `data.input.normalized` is `true`; the four UTF-8 bytes of `🚀` occupy four
393
+ analyzed bytes and map to its two original UTF-16 code units through
394
+ `data.input.positionMap[]`. Every span remains half-open and in bounds,
395
+ `end === input.bytes` is legal, and a cursor inside those four bytes snaps to
396
+ the character boundary rather than splitting the original character.
397
+
398
+ ### 23. Selector-less set fails closed offline
399
+
400
+ ```bash
401
+ centrs explain '/ip/dns set use-doh-server=https://resolver.example/dns-query' --curl --json
402
+ ```
403
+
404
+ The statement's transport classification is `"unknown"` and no curl is
405
+ rendered. Offline cannot prove that `/ip/dns` is a singleton; the same shape on
406
+ an id-bearing table requires an id. Live schema evidence may lift this case to
407
+ `api-candidate`.
408
+
409
+ ### 24. Native API returns the same parse diagnostic
410
+
411
+ ```bash
412
+ centrs explain $A ':if (1 > 2) do={ :put x' --via native-api --port $API_PORT --username $U --password $P --json
413
+ ```
414
+
415
+ `meta.via` is `native-api`; `:put [:parse …]` runs through `/execute` with
416
+ `as-string`, and the diagnostic carries the RouterOS line/column text rather
417
+ than an opaque job handle. The corresponding evidence entry names `:parse`,
418
+ is version-stamped, and no command is executed.
419
+
420
+ ### 25. Offline semantic symbols retain roles and binding identity
421
+
422
+ ```bash
423
+ centrs explain '{:local x 1.3; :put [:typeof $x]; :set x 2.1.1; /put $x; :local z (1.1,1::1,"abc",1d,1w7h2s,1.1.1.1/24,123,[:parse "(1+1)"],(1w+1d),2008:1::2/128,[:timestamp],"a"."b"."c",4%2,-1); :put "$[:typeof $z]"; :foreach i,v in=$z do={:put "$i = $v; types i = $[:typeof $i], v = $[:typeof $v]"}}'
424
+ ```
425
+
426
+ The text output has a `symbols:` section. The declaration, `:set` target, and
427
+ references for `x` share one result-local binding identity; `z` has another;
428
+ and the `:foreach` bindings `i` and `v` each have their own identity. Their
429
+ roles are respectively `declaration`, `assignment`, `reference`, and `binding`.
430
+ Duration literals such as `1d` and `1w7h2s` do not fabricate symbol names.
431
+
432
+ The JSON/YAML form carries the same facts under
433
+ `data.symbols.occurrences[]` as `name`, byte `span`, `class`, `role`,
434
+ `bindingIds`, `sigil`, optional `note`, and `ev`. This is the Q13 symbol view;
435
+ it does not infer value-flow types, which remain #239 S2 after #225.
436
+
437
+ ### 26. Value facts keep shape, observed type, and schema type separate
438
+
439
+ ```bash
440
+ centrs explain ':local x 2.2; :set x "2.2"; :local z (1,2,3); :local t 00:00:02; :local i *1; /ip/arp/add mac-address=00:11:22:33:44:55' --json
441
+ ```
442
+
443
+ `data.values.occurrences[]` contains nine result-local value records — six
444
+ argument values and the three members of the array literal (example 28 owns the
445
+ interior). The bare
446
+ `2.2` carries `facts.shapeHints.values: ["ip"]` — RouterOS numbers are integers,
447
+ so a dotted decimal is an IPv4 shortcut (`2.0.0.2`) and never `num`; the quoted
448
+ `"2.2"` carries `["str"]`. The array literal, colon-form time, internal ID, and
449
+ full MAC spelling carry `["array"]`, `["time"]`, `["id"]`, and `["mac"]`.
450
+ `mac` is a schema spelling clue, not a RouterOS scripting type: the bare scalar
451
+ is `str` while the CLI Reference declares `mac-address` as `macAddr`. Each hint
452
+ fact cites canonicalizer evidence with
453
+ `basis: "heuristic"`; no record has `observedType` or `schemaType`, because
454
+ offline analysis has neither live parser output nor an argument schema. A hint
455
+ is not a diagnostic and does not change `data.verdict` or
456
+ `runtimeAcceptance: "not-proven"`.
457
+
458
+ The CHR assertion for this example also pins the boolean boundary: scalar
459
+ `true`/`false` and `yes`/`no` are `bool`, quoted forms are `str`, CLI boolean
460
+ attributes accept `yes`/`no` rather than `true`/`false`, and REST accepts JSON
461
+ booleans. These live/context observations ground the offline `bool` spelling
462
+ hint; they are not emitted as `observedType` or `schemaType` by offline explain.
463
+ The same assertion grounds the new boundaries: brace/comma literals are
464
+ `array`; colon times are `time`; `*1` is `id`; `.` concat can return `str` or
465
+ distribute over an array; `[:toarray ""]` is the only empty-array construction
466
+ and indexing it yields `nothing`; `[:parse "…"]` returns `code`; and `nil` has
467
+ no standalone offline spelling. Produced empty arrays, concat, `code`, and
468
+ `nil` remain abstentions rather than guessed hints.
469
+
470
+ ### 27. Continuation comments are not REST operands
471
+
472
+ ```bash
473
+ centrs explain '/ip/address/add address=1.2.3.4 \
474
+ # a note
475
+ comment=x' --json
476
+ ```
477
+
478
+ The statement's argument reading is complete: `command.args` is
479
+ `{ address: "1.2.3.4", comment: "x" }`, and `arguments.positional` is empty. Its
480
+ transport classification stays `api-candidate`, with the same two fields in the
481
+ REST body; the comment text is never rendered as an operand.
482
+
483
+ The CHR assertion grounds the wider placement rule on the requested RouterOS
484
+ channel via `/console/inspect request=highlight` plus `:parse`: unquoted `#`
485
+ inside brace arrays and parenthesized expressions is a hard error; the first
486
+ non-space `#` in `do`, `else`, `foreach do`, and `on-event` bodies that are
487
+ themselves in a statement context is a comment — the same `do={ # c` nested
488
+ inside an array is an array and `#` is an error (placement table, #249), while
489
+ a `[…]` substitution nested in that array re-enters a statement context so
490
+ `[:do { # c` is a comment again; the same lead inside a `[…]` command
491
+ substitution is a comment while `[:put #test]` keeps `#test` as a value;
492
+ attribute and bare-value `#` is content; and `#` after `}` is a hard error.
493
+ The stable/testing grounding run for #245 used RouterOS 7.23.3 and 7.24rc3;
494
+ the enclosing-array exception is grounded on CHR 7.23.3 via #249.
495
+
496
+ A same-line hash after a completed scripting directive is likewise an error:
497
+ `:local x 2 #` produces an `explain/canonicalizer/invalid-hash` diagnostic at
498
+ the hash byte and stops later semantic claims. Statement-leading comments are
499
+ listed with their byte ranges under `comments:` in the default text output.
500
+
501
+ ### 28. An array literal is read member by member, and only where it is one
502
+
503
+ ```bash
504
+ centrs explain ':local z {1.1;"abc";1d;{2;3};a=0x10;b=100000w}; /ip/route/add comment={1;2}' --json
505
+ ```
506
+
507
+ Members of a located array literal are values in their own right.
508
+ `data.values.occurrences[]` carries one record per member, each with its own
509
+ document-byte span, its own `shapeHints`, and a `parent` naming the array's
510
+ result-local id — `1.1` is `["ip"]`, `"abc"` is `["str"]`, `1d` is `["time"]`,
511
+ the nested `{2;3}` is `["array"]` with two `["num"]` members of its own, and the
512
+ keyed member carries `name: "a"` with `["num"]` for `0x10`. `kind` is `element`,
513
+ so a caller can tell a member from a command argument without re-parsing.
514
+
515
+ **A member is not an argument value, because RouterOS does not parse it as
516
+ one.** Inside a literal the device reads an expression, so `b=100000w` hints
517
+ nothing at all: `:parse` lowers it to `$100000w`, a variable reference, because
518
+ the literal overflows the signed 64-bit nanosecond range that ends between
519
+ `15250w` and `15251w`. The same rule removes three spellings the argument
520
+ lexicon does hint — a bare word (`{abc}` is `$abc`), a full MAC
521
+ (`{00:11:22:33:44:55}` is a variable, though `:local x 00:11:22:33:44:55` is
522
+ `str`), and an internal id (`{*1}` is a hard syntax error, though `:local x *1`
523
+ is `id`) — and adds hexadecimal, which is a number here (`0x10` is 16, while
524
+ `0X10` is a variable). Expressions and substitutions abstain: `{1+1}`,
525
+ `{"a"."b"}`, `{[:parse ":put 1"]}` and `{$x}` produce no member hint.
526
+
527
+ The second statement produces **no value record** and an
528
+ `explain/canonicalizer/invalid-command-brace` error at its `{`. A `{…}` array
529
+ literal is legal only in a scripting directive's value slot:
530
+ `/console/inspect` classes
531
+ the `{` byte `error` and `:parse` refuses `/ip/route/add comment={1;2}`,
532
+ `/ip/dns/set servers={1.1.1.1;8.8.8.8}` — whose attribute really is list-typed,
533
+ which is what rules out a schema-shaped reading — `.proplist={name;comment}`,
534
+ the relative spelling `ip route add comment={1;2}`, and even
535
+ `:log info message={1;2}`. The `(1,2)` spelling is never rejected at the
536
+ delimiter in any of those positions, and a `(…)` or `[…]` around the brace
537
+ restores the expression context (`comment=({1;2})` and `comment=[:tostr {1;2}]`
538
+ both parse). The braces a
539
+ command argument does accept are script bodies (`source=`, `on-event=`), and
540
+ those are refused earlier as scope blocks rather than read as arrays.
541
+
542
+ An empty member is a device syntax error (`{;}`, `{;1}`, `{1;;2}`, `(1,)`), so
543
+ the enclosing `array` shape is withdrawn with it; a single trailing separator is
544
+ legal (`{1;}` is a one-member array). Shape hints never become diagnostics.
545
+ The command-brace error is a separate, device-grounded syntax fact rather than
546
+ an inference from the missing hint.
547
+
548
+ ### 28c. Which slot the brace sits in decides whether it is an array
549
+
550
+ ```bash
551
+ centrs explain ':local z {1;2}; :delay {1;2}; :for i from={1;2} to=2 do={:put 1}; :execute script={1;2}' --json
552
+ ```
553
+
554
+ Only the first and third statements produce an array record. Being a scripting
555
+ directive is necessary but not sufficient: the gate is keyed on **(verb, slot)**
556
+ from a device sweep of every root builtin and every slot it completes, and a
557
+ slot has four outcomes. `:local z {…}` and `:for from={…}` EVALUATE the brace
558
+ (IL `(, 1 2)` for `{(1,2)}`), so they are arrays. `:delay {1;2}` is a syntax
559
+ error — a scalar-typed slot at the same `/` path — and produces nothing.
560
+ `:execute script={1;2}` parses but keeps the brace VERBATIM as a script
561
+ (`script=(1,2)`, not `(, 1 2)`), so it is not an array either, and neither are
562
+ the code slots `:onerror in=`, `:retry command=` and `:if do=`. The same name
563
+ can differ by verb: `:foreach i in={1;2}` is an array while
564
+ `:onerror e in={1;2}` is a program.
565
+
566
+ A positional is keyed by its index, because position decides: `:local {1;2}`
567
+ puts the literal in the NAME slot and does not parse, while `:local z {1;2}`
568
+ puts it in the VALUE slot and does. The verb is matched verbatim for the same
569
+ reason: RouterOS accepts only the lower-case spelling, so `:LOCAL z {1;2}` is
570
+ `expected command name` and gets no array, while an argument's casing is free
571
+ (`:local Z {1;2}` binds `$Z` and does). Everything the sweep could not ask is
572
+ refused, so the gate loses coverage rather than inventing a shape.
573
+
574
+ Two rejections a member walk cannot see from the outside are also refused here.
575
+ A nested literal the device rejects withdraws its container — `{(1,)}`,
576
+ `{a=(1,)}`, `{1;(2,)}`, `{()}`, `{a=()}`, `{(,)}` and `(1,(2,))` are all
577
+ `:parse` syntax errors, even though a `(…)` member is usually a group (`{(1)}`
578
+ is the one-member array `1`). And member descent stops after eight frames: past
579
+ that the interior is unverified, and a fault below the bound still makes the
580
+ whole statement a syntax error, so the literal is refused rather than called an
581
+ array on the strength of its delimiters alone.
582
+
583
+ ### 28b. The comma spelling is accepted everywhere, and means two things
584
+
585
+ ```bash
586
+ centrs explain '/ip/dns/set servers=1.1.1.1,8.8.8.8; /ip/route/add comment=a,b; :local x 1,2' --json
587
+ ```
588
+
589
+ The brace restriction is about the brace, not about lists: `=1,2,3` is a syntax
590
+ error nowhere, and list-taking arguments really do take it. What the device does
591
+ with it depends on the argument's TYPE, which offline does not have —
592
+ `servers=1.1.1.1,8.8.8.8` lowers to `servers=1.1.1.1;8.8.8.8` and
593
+ `dst-port=80,443` to `dst-port=;80;443`, while `comment=a,b` and
594
+ `interface=ether1,ether2` keep the whole run as a single string.
595
+
596
+ So the first two records carry **`["array", "str"]`** — the first genuinely
597
+ plural hint, which is what `shapeHints.values` is a list for. Neither reading is
598
+ validated and neither becomes a diagnostic; the split is a `schemaType` fact and
599
+ that home stays empty offline. The third record, in a scripting directive's
600
+ value slot, carries `["array"]` alone: `:local x 1,2` is a two-member array on
601
+ the device and no text reading exists there.
602
+
603
+ No members are emitted for a bare comma run, because whether it splits at all is
604
+ the schema's answer. The `(1,2)` spelling is different — its delimiters prove
605
+ the array — so `servers=(1.1.1.1,8.8.8.8)` yields the array plus two `["ip"]`
606
+ members.