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