@tikoci/centrs 0.1.9-next.22 → 0.1.9-next.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +106 -0
- package/GLOSSARY.txt +38 -0
- package/README.md +2 -2
- package/commands/api/AGENTS.md +18 -1
- package/commands/api/README.md +10 -6
- package/commands/api/examples.md +1 -1
- package/commands/execute/README.md +26 -6
- package/commands/execute/examples.md +43 -5
- package/commands/explain/README.md +873 -182
- package/commands/explain/examples.md +447 -38
- package/commands/retrieve/README.md +13 -2
- package/commands/retrieve/examples.md +30 -1
- package/commands/transfer/README.md +8 -4
- package/docs/CLI.md +19 -5
- package/docs/CONSTITUTION.md +18 -9
- package/docs/MATRIX.md +51 -26
- package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
- package/docs/errors/routeros/permission-denied.md +24 -0
- package/docs/errors/transport/edge-auth-failed.md +22 -0
- package/docs/errors/transport/write-outcome-unknown.md +3 -0
- package/docs/errors/usage/invalid-cursor.md +20 -0
- package/docs/errors/validation/unknown-path.md +9 -0
- package/package.json +5 -2
- package/src/api.ts +34 -39
- package/src/cli/explain.ts +206 -47
- package/src/core/envelope.ts +10 -3
- package/src/core/error-catalog.ts +15 -0
- package/src/core/execute-command.ts +53 -0
- package/src/core/fanout.ts +12 -2
- package/src/core/inspect.ts +4 -5
- package/src/core/routeros-errors.ts +25 -0
- package/src/execute-cmr.ts +2 -8
- package/src/execute.ts +48 -20
- package/src/explain/args.ts +181 -15
- package/src/explain/blocks.ts +3 -4
- package/src/explain/brace-slots.ts +5 -0
- package/src/explain/catalog.ts +13 -6
- package/src/explain/defects.ts +1 -0
- package/src/explain/group-faults.ts +1060 -0
- package/src/explain/highlight-projection.ts +10 -8
- package/src/explain/operators.ts +22 -20
- package/src/explain/pathresolve.ts +102 -20
- package/src/explain/quoted-string.ts +41 -6
- package/src/explain/run-tokens.ts +223 -0
- package/src/explain/scope-brace.ts +319 -7
- package/src/explain/segment.ts +177 -4
- package/src/explain/spaced-args.ts +116 -0
- package/src/explain/symbols.ts +163 -7
- package/src/explain/transport.ts +13 -0
- package/src/explain/values.ts +55 -4
- package/src/explain/verbsplit.ts +14 -188
- package/src/explain/write.ts +8 -1
- package/src/explain-live-facets.ts +739 -0
- package/src/explain-live-types.ts +490 -0
- package/src/explain-live.ts +593 -0
- package/src/explain.ts +772 -60
- package/src/index.ts +6 -0
- package/src/offline-gate.ts +30 -9
- package/src/protocols/adapter.ts +88 -10
- package/src/protocols/btest-session.ts +30 -10
- package/src/protocols/mac-telnet-console.ts +16 -0
- package/src/protocols/mac-telnet.ts +38 -10
- package/src/protocols/native-api.ts +13 -12
- package/src/retrieve-follow.ts +2 -1
- package/src/retrieve-sample.ts +2 -1
- package/src/retrieve-wait.ts +10 -6
- package/src/retrieve.ts +77 -27
- package/src/transfer.ts +38 -13
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
# explain — examples
|
|
2
2
|
|
|
3
|
-
Each numbered example is an executable spec.
|
|
4
|
-
|
|
5
|
-
`test/integration/explain.test.ts
|
|
6
|
-
`test/integration/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
155
|
-
`remove`, `print`, …)
|
|
156
|
-
|
|
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`, …)
|
|
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
|
|
175
|
-
contain —
|
|
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`;
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
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`
|
|
196
|
-
|
|
197
|
-
|
|
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`
|
|
217
|
-
partial
|
|
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
|
|
226
|
-
|
|
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` —
|
|
237
|
-
a free-form argument like `comment=`, the same probe yields
|
|
238
|
-
|
|
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
|
-
|
|
347
|
-
`
|
|
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
|
|
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
|
|
|
@@ -483,7 +507,8 @@ REST body; the comment text is never rendered as an operand.
|
|
|
483
507
|
|
|
484
508
|
The CHR assertion grounds the wider placement rule on the requested RouterOS
|
|
485
509
|
channel via `/console/inspect request=highlight` plus `:parse`: unquoted `#`
|
|
486
|
-
inside brace arrays and parenthesized expressions is a hard error
|
|
510
|
+
inside brace arrays and parenthesized expressions is a hard error, except at
|
|
511
|
+
column 0 inside an array (example 37); the first
|
|
487
512
|
non-space `#` in `do`, `else`, `foreach do`, and `on-event` bodies that are
|
|
488
513
|
themselves in a statement context is a comment — the same `do={ # c` nested
|
|
489
514
|
inside an array is an array and `#` is an error (placement table, #249), while
|
|
@@ -700,3 +725,387 @@ envelope would otherwise have to derive the name/value boundary from
|
|
|
700
725
|
The same command also shows the two `=` bytes staying apart: the one in
|
|
701
726
|
`($x = 1)` is a comparison and comes back `operator`, decided by fill order
|
|
702
727
|
rather than by a scan for the byte.
|
|
728
|
+
|
|
729
|
+
### 33. A space for the `=` is not an empty argument list
|
|
730
|
+
|
|
731
|
+
```bash
|
|
732
|
+
centrs explain '/ip/firewall/layer7-protocol add comment "something" regexp 1.1' --json
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
RouterOS accepts this as `comment="something" regexp=1.1`: `:parse` lowers both
|
|
736
|
+
spellings to the same IL, and `highlight` marks each gap `syntax-obsolete`.
|
|
737
|
+
Which word is an argument name is per-command schema, so offline publishes
|
|
738
|
+
neither reading. The statement stays `resolved` on
|
|
739
|
+
`/ip/firewall/layer7-protocol add`, `command.args` is absent, `arguments.read`
|
|
740
|
+
is `false` with a `why` naming `` `comment "something"` ``, and
|
|
741
|
+
`transport.classification` stays `"unknown"`.
|
|
742
|
+
|
|
743
|
+
`data.verdict` is `warn`, with one
|
|
744
|
+
`explain/canonicalizer/possible-spaced-argument` **warning** per pair,
|
|
745
|
+
spanning `comment "something"` and `regexp 1.1`. Each says to write the `=`.
|
|
746
|
+
The warning needs a value that cannot be a flag: `/interface set ether1 name x`
|
|
747
|
+
also abstains but does not warn, because `/interface print detail
|
|
748
|
+
without-paging` is the same shape and is two flags
|
|
749
|
+
(`detail=true;without-paging=true`). The `=` spelling still reads both
|
|
750
|
+
arguments, and a script body may drop its `=` too:
|
|
751
|
+
`/system/script add name=beep source { :beep }` is the `source={ :beep }` body,
|
|
752
|
+
not an invalid brace.
|
|
753
|
+
|
|
754
|
+
### 34. An unlisted bare path does not become a menu
|
|
755
|
+
|
|
756
|
+
```bash
|
|
757
|
+
centrs explain $'/terminal/cuu\nprint\nadd name=x' --json
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
`/terminal/cuu` is a no-argument command that neither the menu table nor the
|
|
761
|
+
command catalog lists. `/ip fire conn` has the same shape and is navigation:
|
|
762
|
+
RouterOS accepts abbreviated menus. CHR 7.24.5 `:parse` confirms both, so a
|
|
763
|
+
table miss cannot decide between a menu and a command.
|
|
764
|
+
|
|
765
|
+
The first statement stays `ambiguous`. The two after it are `unknown`
|
|
766
|
+
(`context lost to an earlier unreadable statement`, `contextCertain: false`),
|
|
767
|
+
with no `command` and no `transport`, where they used to resolve to an
|
|
768
|
+
invented `/terminal/cuu/print` and `/terminal/cuu/add`. On the device they are
|
|
769
|
+
`bad command name print` and `bad command name add` at the root.
|
|
770
|
+
`structure.containsWrite` is `unknown` and `data.verdict` is `warn`. A known
|
|
771
|
+
absolute navigation recovers the context: in
|
|
772
|
+
`/terminal/cuu`, `/ip address`, `print`, the last statement resolves to `print`
|
|
773
|
+
at `/ip/address` again.
|
|
774
|
+
|
|
775
|
+
### 35. A brace after a command is its argument, not a menu block
|
|
776
|
+
|
|
777
|
+
```bash
|
|
778
|
+
centrs explain '/system/script add name beep source { :beep }' --json
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
`/menu { … }` opens a menu block only when the words before the brace name a
|
|
782
|
+
menu. Here `add` is a command, so the brace is the value of `source` with the
|
|
783
|
+
`=` left out. CHR 7.22.1 and 7.24.5 `:parse` give the same IL as
|
|
784
|
+
`source={ :beep }`.
|
|
785
|
+
|
|
786
|
+
`data.structure.statements` has one statement, `/system/script add`, whose
|
|
787
|
+
span covers the whole input. `:beep` is no longer published as a second
|
|
788
|
+
top-level statement, which RouterOS never runs. `data.verdict` is `warn`, from
|
|
789
|
+
the `possible-spaced-argument` diagnostic on `source { :beep }`.
|
|
790
|
+
|
|
791
|
+
RouterOS accepts any unique prefix of an argument name. An abbreviation that
|
|
792
|
+
can only stand for a script-valued name keeps its body in either spelling:
|
|
793
|
+
`sour { :beep }`, `down-scr { :beep }` and `o={ :beep }` lower to `source=`,
|
|
794
|
+
`down-script=` and `on-event=` on CHR 7.24.5 and 7.23.8. Abbreviating the first
|
|
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`.
|
|
801
|
+
|
|
802
|
+
A brace straight after a command is a device error, and offline now reports it.
|
|
803
|
+
`/system/script add { :beep }`, `/ip/route/print {1;2}` and
|
|
804
|
+
`/system/reboot { :beep }` each get `invalid-command-brace` and `data.verdict`
|
|
805
|
+
`fail`. A run that names no command still opens a block, so
|
|
806
|
+
`/system script { print }` is navigation to `/system/script` followed by
|
|
807
|
+
`print`.
|
|
808
|
+
|
|
809
|
+
### 36. A brace in a `where` clause is an array value
|
|
810
|
+
|
|
811
|
+
```bash
|
|
812
|
+
centrs explain '/ip address print where interface in {"ether1";"ether2"}' --json
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
A `where` clause is expression context, so the brace is the array operand of
|
|
816
|
+
`in`. CHR 7.24.5 and 7.23.8 `:parse` lower it to
|
|
817
|
+
`(in $interface ether1;ether2)`. `data.verdict` is `pass`, so `centrs execute`
|
|
818
|
+
runs this read. Before #502 its offline stage refused it with
|
|
819
|
+
`invalid-command-brace`. `data.values.occurrences` holds the array and its two
|
|
820
|
+
string members, the same three records as
|
|
821
|
+
`where interface in ("ether1","ether2")`.
|
|
822
|
+
|
|
823
|
+
The brace is also a value after `~`, `<`, `>`, `<=` and `>=`, on the left of an
|
|
824
|
+
operator (`where {"ether1"} in interface`), and after `!interface=` or `"a"=`.
|
|
825
|
+
|
|
826
|
+
After `<name>=`, `<name>!=` or `<name>==`, spaced or not, the device reads the
|
|
827
|
+
value with that property's parser and rejects the brace. Two examples are
|
|
828
|
+
`where interface={"ether1"}` (`expected interface value`) and
|
|
829
|
+
`where disabled={"y"}` (`expected yes or no`). Offline keeps
|
|
830
|
+
`invalid-command-brace` at the `{` there. The device does this only for a
|
|
831
|
+
property the menu knows: `where foo-bar={"y"}` parses. Offline cannot tell
|
|
832
|
+
property names apart without a per-menu list, so it reports that one too.
|
|
833
|
+
|
|
834
|
+
### 37. A column-0 `#` line inside an array is a comment
|
|
835
|
+
|
|
836
|
+
```bash
|
|
837
|
+
centrs explain ':local z {a=1;
|
|
838
|
+
# c
|
|
839
|
+
b=2}' --json
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
`data.verdict` is `pass`. `data.spans` holds a `comment` span over `# c`, and
|
|
843
|
+
`data.values.occurrences` holds the array and its two members `a` and `b`. CHR
|
|
844
|
+
7.24.5 and 7.22.1 `:parse` lower it to `value=a=1;b=2`. Before #503, offline
|
|
845
|
+
failed it with `invalid-hash` at the `#`.
|
|
846
|
+
|
|
847
|
+
The rule holds at any depth (`{a={⏎# c⏎1}}`) and in argument values
|
|
848
|
+
(`:foreach i in={1;⏎# c⏎2} do={:put $i}`). It needs column 0: an indented `#`
|
|
849
|
+
(`{a=1;⏎ # c⏎b=2}`), and one after `{`, a value or `;` on the same line, keep
|
|
850
|
+
`invalid-hash` at the `#`. A `#` directly inside `( … )` stays an error too. A
|
|
851
|
+
comment-only array, `:local z {⏎# c⏎}`, fails like `:local z {}`: both are empty
|
|
852
|
+
arrays, which the device rejects (#511, example 38).
|
|
853
|
+
|
|
854
|
+
### 38. An empty array value and a misplaced `;` are rejected
|
|
855
|
+
|
|
856
|
+
```bash
|
|
857
|
+
centrs explain ':local z {}' --json
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
`data.verdict` is `fail` with `explain/canonicalizer/empty-group` over `{}`. CHR
|
|
861
|
+
7.24.5 and 7.23.8 `:parse` answer `syntax error (line 1 column 12)`, the byte
|
|
862
|
+
after the brace. The same holds for `:put {}`, `:return {}`, `[:len {}]` and
|
|
863
|
+
`:foreach i in={} do={…}`. A nested `{}` (`{1;{}}`), one in parentheses
|
|
864
|
+
(`({})`), `where … in {}` and a body (`do={}`, `:execute {}`) parse and pass.
|
|
865
|
+
|
|
866
|
+
`:local z {1⏎;2}` fails with `misplaced-separator` at the `;`: a `;` needs an
|
|
867
|
+
element before it on its line. `{1;}` and `{1⏎2}` pass.
|
|
868
|
+
|
|
869
|
+
### 39. An expression with a missing operand is rejected
|
|
870
|
+
|
|
871
|
+
```bash
|
|
872
|
+
centrs explain ':put (1 +)' --json
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
`data.verdict` is `fail` with `explain/canonicalizer/missing-operand` at the
|
|
876
|
+
`+`. The device answers `syntax error (line 1 column 11)`, at the `)`. `()`
|
|
877
|
+
fails with `empty-group`, and `(1; 2)` and `(1⏎+ 2)` fail with
|
|
878
|
+
`misplaced-separator`. `(1 2)`, `(- 1)` and `(1 + -2)` pass. Inside a query,
|
|
879
|
+
`where (comment=+)` passes: the device reads that value with the property's
|
|
880
|
+
parser.
|
|
881
|
+
|
|
882
|
+
### 40. A `@` in a command word is rejected
|
|
883
|
+
|
|
884
|
+
```bash
|
|
885
|
+
centrs explain '[admin@MikroTik] > /ip address print' --json
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
`data.verdict` is `fail` with `explain/canonicalizer/invalid-at-sign` at the
|
|
889
|
+
`@`. The device answers `syntax error (line 1 column 7)`, the same byte. This
|
|
890
|
+
is a pasted console prompt. `:put a@b` and `comment=a@b` pass, because a `@` is
|
|
891
|
+
a value byte in an argument.
|
|
892
|
+
|
|
893
|
+
### 41. A named comparison brace inside a `[find where …]` is rejected
|
|
894
|
+
|
|
895
|
+
```bash
|
|
896
|
+
centrs explain ':put [/ip address find where interface={"x"}]' --json
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
`data.verdict` is `fail` with `explain/canonicalizer/invalid-command-brace` at
|
|
900
|
+
the `{`, as at the top level (example 36). The device answers
|
|
901
|
+
`expected interface value (line 1 column 40)`. `where (interface={"x"})` fails
|
|
902
|
+
the same way, and `[find where interface in {"x"}]` passes.
|
|
903
|
+
|
|
904
|
+
### 42. A stray top-level `}` ends the script, and the rest is discarded
|
|
905
|
+
|
|
906
|
+
```bash
|
|
907
|
+
centrs explain ':put 1};:put 2' --json
|
|
908
|
+
```
|
|
909
|
+
|
|
910
|
+
`data.verdict` is `warn` with `explain/canonicalizer/discarded-close` over the
|
|
911
|
+
`}` and everything after it. CHR 7.24.5 and 7.23.8 accept the script and lower
|
|
912
|
+
it to `(evl /putmessage=1)`: the device stops reading at a `}` or `]` that closes
|
|
913
|
+
nothing at the top level, so `:put 2` never runs and
|
|
914
|
+
`/system identity set name=a}` sets `a`. `data.structure` reads only the
|
|
915
|
+
prefix. A stray close inside an open group (`:put (1})`, `{:put 1]}`) and any
|
|
916
|
+
stray `)` are still rejections and still fail with `unbalanced-close`. The
|
|
917
|
+
execute gate refuses this warning (`commands/execute/examples.md` V9).
|
|
918
|
+
|
|
919
|
+
### 43. A variable name is letters and digits
|
|
920
|
+
|
|
921
|
+
```bash
|
|
922
|
+
centrs explain ':local a_b 1' --json
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
`data.verdict` is `fail` with `explain/canonicalizer/invalid-name` at the `_`.
|
|
926
|
+
The device answers `expected end of command (line 1 column 9)`, the same byte.
|
|
927
|
+
`:global`, `:set`, `:for` and `:foreach` read the name the same way, and
|
|
928
|
+
`:local $a` and `:local (a)` fail at their first byte. `:local "a_b"`,
|
|
929
|
+
`:local 1a`, `:foreach a,b in={1} do={}` and `:local name=x value=1` pass.
|
|
930
|
+
|
|
931
|
+
### 44. A `$` must start a variable
|
|
932
|
+
|
|
933
|
+
```bash
|
|
934
|
+
centrs explain ':put "a$ b"' --json
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
`data.verdict` is `fail` with `explain/canonicalizer/bad-string-variable` over
|
|
938
|
+
the `$` and the space after it. The device answers `syntax error (line 1 column 9)`, at the space. Outside
|
|
939
|
+
a string, `:put $_` fails with `invalid-variable` the same way. `"$v_x"` (which
|
|
940
|
+
is `$v` then `_x`), `"\$ x"`, `"$(1)"` and `$"a b"` pass. `"a$"` also passes,
|
|
941
|
+
although the device rejects it: it reads `$"` as the start of a quoted name, and
|
|
942
|
+
offline's string scanners end the string there, so offline abstains.
|
|
943
|
+
|
|
944
|
+
### 45. Nothing may be glued to a closed token
|
|
945
|
+
|
|
946
|
+
```bash
|
|
947
|
+
centrs explain ':if (true) do={:put 1}else={:put 2}' --json
|
|
948
|
+
```
|
|
949
|
+
|
|
950
|
+
`data.verdict` is `fail` with `explain/canonicalizer/glued-token` at the `e`.
|
|
951
|
+
The device answers `expected end of command (line 1 column 23)`. The same rule
|
|
952
|
+
rejects `:put "a"b`, `$v.b`, `(1)(2)`, `[:len a]b`, `src-address=$v/24` and a
|
|
953
|
+
bare word run into a `"`, `$` or bracket (`:put a"b"`, `name=a$v`). Inside
|
|
954
|
+
`where` and `find`, and inside a group, the rule does not apply (`where
|
|
955
|
+
comment="a"b` and `("a"b)` parse). A command argument whose property parser
|
|
956
|
+
takes the glued part also passes: `dst-address-list=!"my list"` and
|
|
957
|
+
`dst-port=80,"443"`.
|
|
958
|
+
|
|
959
|
+
### 46. Statement ids and source ownership
|
|
960
|
+
|
|
961
|
+
```bash
|
|
962
|
+
centrs explain ':if (true) do={ :foreach i in={1;2} do={ :put $i } } else={ :put 0 }; :put 3' --json
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
The statements keep discovery order and have ids `s0` through `s4`.
|
|
966
|
+
The `foreach` (`s1`) has `parent: "s0"`; its `put` (`s2`) has `parent: "s1"`;
|
|
967
|
+
the `else` body's `put` (`s3`) has `parent: "s0"`. The final `put` (`s4`) has
|
|
968
|
+
no parent. The published `do` and `else` blocks both have `owner: "s0"`;
|
|
969
|
+
`s1` and `s2` share `block: 0`, and `s3` has `block: 1`. A `[find]` inside a
|
|
970
|
+
body names that body's statement as its `owner`. Links describe source
|
|
971
|
+
ownership; binding and execution-context claims remain separate (#507).
|
|
972
|
+
|
|
973
|
+
### 47. A stored script stays opaque
|
|
974
|
+
|
|
975
|
+
```bash
|
|
976
|
+
centrs explain '/system/script/add name=proof source={ :local x 1; :put $x }; :put 2' --json
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
Only the installing command (`s0`) and final `put` (`s1`) appear in
|
|
980
|
+
`statements[]`; neither has a parent, and `blocks[]` stays empty. The deferred
|
|
981
|
+
body's local and `put` are not exposed as statements. Scheduler `on-event=`
|
|
982
|
+
and Netwatch `down-script=` have the same structural boundary.
|
|
983
|
+
|
|
984
|
+
The CHR controls in `test/integration/explain-structure.test.ts` parse the
|
|
985
|
+
scope examples and compare stored-script creation, explicit `run`, and an
|
|
986
|
+
immediate `:do` body. Those are test operations on disposable CHR; explain
|
|
987
|
+
itself never executes the analyzed input.
|
|
988
|
+
|
|
989
|
+
### 48. Safe argument discovery distinguishes spaced arguments from flags
|
|
990
|
+
|
|
991
|
+
```bash
|
|
992
|
+
centrs explain $R '/interface set ether1 name x' -u $U -p $P --json
|
|
993
|
+
centrs explain $R '/interface print detail without-paging' -u $U -p $P --json
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
`data.live.arguments[]` cites command-level child evidence and statement `s0`.
|
|
997
|
+
The first result includes `name`, the second includes `detail`. Both parse;
|
|
998
|
+
only `name x` gets the device's obsolete-syntax warning. An unknown argument
|
|
999
|
+
control (`/interface set ether1 definitely-unknown-491=x`) carries the shared
|
|
1000
|
+
parse classifier's rejection. No command-level completion is requested.
|
|
1001
|
+
|
|
1002
|
+
The integration controls also explain a write-shaped identity change and a
|
|
1003
|
+
substitution without executing either, verify identity before/after, exercise
|
|
1004
|
+
CLI stdin and quickchr resolution, and skip oversized highlight without
|
|
1005
|
+
submitting a prefix. Parse/hash/normalization evidence preserves the Unicode
|
|
1006
|
+
byte coordinates. Run on long-term as well as stable before changing these
|
|
1007
|
+
claims. Sanitized captures can be regenerated with `CENTRS_EXPLAIN_CAPTURE`
|
|
1008
|
+
pointing at an ignored output file when running the integration suite.
|
|
1009
|
+
|
|
1010
|
+
### 49. `?`, a backtick and a second `=` need quotes
|
|
1011
|
+
|
|
1012
|
+
```bash
|
|
1013
|
+
centrs explain '/tool/fetch url=http://x/a?b=c' --json
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
`data.verdict` is `fail` with `explain/canonicalizer/unquoted-reserved-byte`
|
|
1017
|
+
at the `?`. The device answers `expected end of command (line 1 column 27)`,
|
|
1018
|
+
the same byte. A backtick is rejected the same way, in a value, a
|
|
1019
|
+
positional, a `where`, a `find`, an array and an expression. A second `=` in
|
|
1020
|
+
a menu command's argument is rejected at that `=`. So `name=a=b` and a bare
|
|
1021
|
+
base64 key ending in `=` both fail. The value's property parser never takes
|
|
1022
|
+
these bytes: `regexp=a?b` and `secret=abc==` are rejected too. In a menu
|
|
1023
|
+
command's argument name, the device reports from the name's first byte
|
|
1024
|
+
(`/ip/address/add a?b=1` at the `a`). `url="http://x/a?b=c"`,
|
|
1025
|
+
`where comment=a=b`, `[find comment=a=b]`, `{a=b}` and `# a?b` pass.
|
|
1026
|
+
`:put a=b` abstains: 7.24.5 reads it as a named argument and 7.23.8 rejects
|
|
1027
|
+
the `=`.
|
|
1028
|
+
|
|
1029
|
+
### 50. A non-ASCII character outside a string is rejected
|
|
1030
|
+
|
|
1031
|
+
```bash
|
|
1032
|
+
centrs explain ':put $café' --json
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
`data.verdict` is `fail` with `explain/canonicalizer/unquoted-non-ascii` over
|
|
1036
|
+
the `é`. The device answers `expected end of command (line 1 column 10)`, at
|
|
1037
|
+
the `é`'s first byte. RouterOS reads only ASCII outside a string. A bare
|
|
1038
|
+
value (`name=router-café`, `name=🛰`), a reference, a path (`/systemé`), a
|
|
1039
|
+
verb, an argument name, a `where` value, an array and an expression are all
|
|
1040
|
+
rejected. So are pasted non-breaking spaces, curly quotes and ellipses. A
|
|
1041
|
+
declared name (`:local café`) keeps its `invalid-name` error. Strings and
|
|
1042
|
+
comments carry any byte: `:put "café"`, `:local "café" 1`, `$"café"`,
|
|
1043
|
+
`name="router-🚀"`, `# café` and a multi-line string pass. The informational
|
|
1044
|
+
`non-ascii` fact is still published beside the error, and a leading BOM is
|
|
1045
|
+
not read as part of the first word.
|
|
1046
|
+
|
|
1047
|
+
### 51. A brace after a submenu opens a block at any depth
|
|
1048
|
+
|
|
1049
|
+
```bash
|
|
1050
|
+
centrs explain '/ip { address { print {1;2} } }' --json
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
`data.verdict` is `fail` with `explain/canonicalizer/invalid-command-brace` at
|
|
1054
|
+
the inner `{`. The device answers `expected end of command (line 1 column 23)`,
|
|
1055
|
+
the same byte. `address {` inside `/ip { … }` is a block over `/ip/address`,
|
|
1056
|
+
so `data.structure.statements` reads `/ip`, `/ip/address` and
|
|
1057
|
+
`/ip/address print`, and the `{1;2}` is the operand of `print`.
|
|
1058
|
+
|
|
1059
|
+
The device reads a brace after a menu word as statements whatever the body
|
|
1060
|
+
holds, at any depth. A body offline cannot resolve abstains with
|
|
1061
|
+
`unresolved-statement` instead of failing as an array:
|
|
1062
|
+
`/ip { address { 1;2 } }` (the device says `bad command name 1`) and
|
|
1063
|
+
`/ip { address { zzznotamenu { print } } }` (`syntax error`) both give `warn`,
|
|
1064
|
+
as `/ip/address { 1;2 }` and `/ip { zzznotamenu { print } }` do.
|
|
1065
|
+
`/ip { address { print } }` and `/ip { firewall { filter { print } } }` pass.
|
|
1066
|
+
The menu must be known from the source alone, so a block after a navigation
|
|
1067
|
+
statement (`/ip⏎address { … }`), an abbreviation (`addr {`) and a digit-led
|
|
1068
|
+
menu (`6to4 {`) keep the older reading.
|
|
1069
|
+
|
|
1070
|
+
### 52. A live call observes a type only where the device converts a literal
|
|
1071
|
+
|
|
1072
|
+
```bash
|
|
1073
|
+
centrs explain $R ':local x 2.2; :set x "2.2"; :local z (1,0x10,2.2); :local t 200ms; :local i *1; :local y yes' -u $U -p $P --json
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
This is example 26's input against a target. The bare `2.2` gains
|
|
1077
|
+
`facts.observedType: { value: "ip", rendered: "2.0.0.2" }`, `200ms` gains
|
|
1078
|
+
`time` (`00:00:00.200`) and `yes` gains `bool` (`true`). Of the array's
|
|
1079
|
+
members, `0x10` is `num` (`16`) and `2.2` is `ip`, while `1` renders unchanged
|
|
1080
|
+
and gets nothing. The quoted `"2.2"` and the id `*1` also get nothing, because
|
|
1081
|
+
an unchanged rendering proves no type: `:parse` prints a string unquoted. The
|
|
1082
|
+
`shapeHints` are untouched, and no record has `schemaType`, because no value
|
|
1083
|
+
here is a menu command's argument.
|
|
1084
|
+
|
|
1085
|
+
A scripting directive's named argument is an operand too:
|
|
1086
|
+
`:for i from=0x10 to=20 do={:put $i}` gives `from` the fact `num` (`16`), and
|
|
1087
|
+
`to=20` renders unchanged and gets nothing. A NAME slot is a variable, never a
|
|
1088
|
+
value: in `:local yes 2` and `:local name=no value=yes`, the names `yes` and
|
|
1089
|
+
`no` get nothing (the device renders `name=$yes`, `name=$no`), while
|
|
1090
|
+
`value=yes` is `bool`.
|
|
1091
|
+
|
|
1092
|
+
Each fact cites one `:parse` evidence entry per context (operands, members)
|
|
1093
|
+
with `facet: "types"` and `basis: "derived"`. The probe re-parses each literal
|
|
1094
|
+
alone, as `:local vN <lit>` or `:local vN {<lit>}`; the analyzed input and
|
|
1095
|
+
the literals are never evaluated, and no `:typeof` is sent.
|
|
1096
|
+
|
|
1097
|
+
### 53. A live call normalizes each argument's declared type
|
|
1098
|
+
|
|
1099
|
+
```bash
|
|
1100
|
+
centrs explain $R '/ip/dns/set servers=1.1.1.1,8.8.8.8; /ip/route/add gateway=2.2 dist=2 comment=abc; /ip/firewall/filter/add chain=input dst-port=80,443 protocol=tcp disabled=yes' -u $U -p $P --json
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
Each `name=value` gains `facts.schemaType` from one argument-level `syntax`
|
|
1104
|
+
probe. `servers` is `{ values: ["ip", "ip6", "enum"], declared:
|
|
1105
|
+
"Address[@VRF]" }`, and `gateway` is `ip`, `ip6`, `enum`. `dist` resolves as
|
|
1106
|
+
the unique prefix of `distance` and is `num`. `comment` is `str`. `chain` is
|
|
1107
|
+
`enum` plus `str`, since custom chains are legal. `dst-port` is `num` with
|
|
1108
|
+
`list: true`. `disabled` gets nothing, because the device declares a boolean
|
|
1109
|
+
with a blank row. No menu argument gets `observedType`: its own parser
|
|
1110
|
+
converts it, which is the schema's slot. The text output prints
|
|
1111
|
+
`observed=ip(2.0.0.2)` and `schema=ip|ip6|enum` on the value rows.
|
|
@@ -87,8 +87,11 @@ alternatives in the error envelope.
|
|
|
87
87
|
`failed` when the gate refuses: `validation/*`, or `--query` on a singleton.
|
|
88
88
|
It is absent when a `transport/*`, `auth/*`, `routeros/*` or `internal/*` error
|
|
89
89
|
stopped inspect from answering; the device stage then says `not reached`
|
|
90
|
-
(examples 12 and 16b; 16c is the `passed` case).
|
|
91
|
-
|
|
90
|
+
(examples 12 and 16b; 16c is the `passed` case). The same holds for a
|
|
91
|
+
`wait/*` error from a `--wait` whose gate never settled, and for
|
|
92
|
+
`--sample` / `--follow` cancelled before the gate ran. That last case has no
|
|
93
|
+
error code, so its reason is `not reached: interrupted` (#537). With
|
|
94
|
+
`--validate=false` it is `{enabled: false, source: "disabled"}`.
|
|
92
95
|
|
|
93
96
|
If `--all-attributes` is combined with `--attribute(s)`, fail with
|
|
94
97
|
`usage/conflicting-flags` *before* hitting the network.
|
|
@@ -147,6 +150,14 @@ handling.
|
|
|
147
150
|
}
|
|
148
151
|
```
|
|
149
152
|
|
|
153
|
+
Projection rule: the bare value is about the attribute *count*, not the flag
|
|
154
|
+
spelling. On a singleton, exactly one projected attribute — `--attribute x`
|
|
155
|
+
or `--attributes x` alike — returns the bare value (`data` is the scalar,
|
|
156
|
+
not an object); two or more return an object with those keys. On a list, any
|
|
157
|
+
projection returns an array of objects, even a single attribute. Unprojected
|
|
158
|
+
reads return the full object (singleton) or the row array (list). (Examples
|
|
159
|
+
5–6, 20, N5–N6 pin every cell of this table.)
|
|
160
|
+
|
|
150
161
|
`--max-bytes` / `--max-rows` truncation populates
|
|
151
162
|
`meta.truncated = { returned, total, totalBytes }` and keeps `ok: true` with a
|
|
152
163
|
warning entry — it is not an error. (Earlier text said a byte overflow returns an
|
|
@@ -22,7 +22,8 @@ centrs retrieve $R /system/resource --username $U --password $P --json
|
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Envelope: `ok: true`, `data` is an object, `meta.via=rest-api`,
|
|
25
|
-
`meta.validation.source=/console/inspect
|
|
25
|
+
`meta.validation.source=/console/inspect`, and `meta.validation.stages` is the
|
|
26
|
+
one `device: passed` stage (#535).
|
|
26
27
|
|
|
27
28
|
### 2. /system/identity
|
|
28
29
|
|
|
@@ -70,6 +71,10 @@ centrs retrieve $R /interface --attributes name,type --username $U --password $P
|
|
|
70
71
|
|
|
71
72
|
Envelope: `data` is an array of objects each containing only `name` and `type`.
|
|
72
73
|
|
|
74
|
+
A single attribute (`--attributes name`) keeps the array shape: an array of
|
|
75
|
+
single-key objects. On a singleton, one item via either flag returns the bare
|
|
76
|
+
value (example 5) — the rule is the count, not the flag.
|
|
77
|
+
|
|
73
78
|
### 7. --all-attributes (RouterOS details=true)
|
|
74
79
|
|
|
75
80
|
```bash
|
|
@@ -121,6 +126,18 @@ available.
|
|
|
121
126
|
centrs retrieve $R /not/a/real/path --username $U --password $P --json
|
|
122
127
|
```
|
|
123
128
|
|
|
129
|
+
`/system/routerboard` earns a targeted remediation instead of the generic
|
|
130
|
+
one: the menu exists only on MikroTik hardware, so its absence on CHR and
|
|
131
|
+
x86 systems is expected rather than a typo, and the remediation points at
|
|
132
|
+
`/system/license` for the device's license ID (`system-id` on CHR,
|
|
133
|
+
`software-id` on x86 — a projected attribute would fail on one platform or
|
|
134
|
+
the other, so the example reads the whole object).
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
centrs retrieve $R /system/routerboard --username $U --password $P --json
|
|
138
|
+
centrs retrieve $R /system/license --username $U --password $P --json
|
|
139
|
+
```
|
|
140
|
+
|
|
124
141
|
### 11. Unknown attribute
|
|
125
142
|
|
|
126
143
|
`validation/unknown-attribute` with suggestions.
|
|
@@ -362,6 +379,10 @@ centrs retrieve $A /interface --attributes name,type --via native-api --port $AP
|
|
|
362
379
|
|
|
363
380
|
`data` is an array of objects each containing only `name` and `type`.
|
|
364
381
|
|
|
382
|
+
A single attribute (`--attributes name`) keeps the array shape here too: an
|
|
383
|
+
array of single-key objects. On a singleton, one item via either flag returns
|
|
384
|
+
the bare value (N5) — the rule is the count, not the flag.
|
|
385
|
+
|
|
365
386
|
### N7. `--all-attributes` (native `print detail`)
|
|
366
387
|
|
|
367
388
|
```bash
|
|
@@ -380,6 +401,14 @@ centrs retrieve $A /system/resource --list-attributes --via native-api --port $A
|
|
|
380
401
|
centrs retrieve $A /not/a/real/path --via native-api --port $API_PORT --username $U --password $P
|
|
381
402
|
```
|
|
382
403
|
|
|
404
|
+
`/system/routerboard` on CHR earns the targeted remediation (no routerboard
|
|
405
|
+
outside MikroTik hardware; read `/system/license` for the license ID
|
|
406
|
+
instead), same as example 10.
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
centrs retrieve $A /system/routerboard --via native-api --port $API_PORT --username $U --password $P
|
|
410
|
+
```
|
|
411
|
+
|
|
383
412
|
### N10. Unknown attribute → `validation/unknown-attribute`
|
|
384
413
|
|
|
385
414
|
```bash
|