@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
|
@@ -10,12 +10,11 @@ validation): the cheap, safe knowledge tier in front of the runners
|
|
|
10
10
|
(`execute` / `api` / `retrieve`).
|
|
11
11
|
|
|
12
12
|
Status: offline baseline `verified`; live probes over `rest-api` and `native-api`
|
|
13
|
-
`
|
|
13
|
+
`CHR-passed`. **`centrs explain '<input>'` runs without a router or CDB**, and
|
|
14
|
+
`centrs explain <router> '<input>'` adds read-only device evidence.
|
|
14
15
|
[`docs/MATRIX.md`](../../docs/MATRIX.md#offline-analysis) tracks these separately;
|
|
15
16
|
[#90](https://github.com/tikoci/centrs/issues/90) indexes the remaining tasks.
|
|
16
17
|
The spec is ratified on the [phase-0 grounding lab](#phase-0-ratification-185).
|
|
17
|
-
The current finish line is [Offline baseline acceptance](#offline-baseline-acceptance),
|
|
18
|
-
not completion of the live protocol cells.
|
|
19
18
|
Load-bearing rules — envelope, errors, settings precedence, identity,
|
|
20
19
|
validation, protocol selection — live in
|
|
21
20
|
[`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md).
|
|
@@ -373,9 +372,8 @@ native API's `/execute` **with `as-string=""`** returns the parser text —
|
|
|
373
372
|
including the `(line N column M)` diagnostic on a syntax error — byte-identical
|
|
374
373
|
to what a console transport prints. The opaque `*NN` value seen otherwise is the
|
|
375
374
|
**no-`as-string` job handle**, not a `:parse` limitation; the readout rule is
|
|
376
|
-
"pass `as-string` and interpret `ret`". The
|
|
377
|
-
`as-string`
|
|
378
|
-
readout rather than copy the omission. `/console/inspect`
|
|
375
|
+
"pass `as-string` and interpret `ret`". The execute gate passes
|
|
376
|
+
`as-string` and classifies `ret`; live explain reuses that shared classifier. `/console/inspect`
|
|
379
377
|
(highlight/syntax/completion) works over the native API's `talk` framing too. So
|
|
380
378
|
the line/column diagnostic is available on both live cells — the earlier caveat
|
|
381
379
|
that only a console transport surfaces the parser text is retired.
|
|
@@ -401,10 +399,8 @@ router therefore never reinterprets a previously valid offline invocation.
|
|
|
401
399
|
the offline form with nothing else to analyze. A positional always wins, and
|
|
402
400
|
when fd 0 is redirected anyway the result carries a
|
|
403
401
|
[`usage/stdin-ignored`](../../docs/errors/usage/stdin-ignored.md) warning rather
|
|
404
|
-
than quietly dropping the pipe. `--file -` names stdin explicitly
|
|
405
|
-
|
|
406
|
-
(`… | centrs explain edge1 --file -`) is parsed and then refused with
|
|
407
|
-
`usage/not-implemented` until phase 2. The narrower rule is
|
|
402
|
+
than quietly dropping the pipe. `--file -` names stdin explicitly in both forms,
|
|
403
|
+
including live `… | centrs explain edge1 --file -`. The narrower rule is
|
|
408
404
|
deliberate and measured: `runCli` is called in-process by tests, so reading fd 0
|
|
409
405
|
on any path a positional invocation reaches consumes the invoking shell's
|
|
410
406
|
stdin — it fed those bytes to the analyzer and broke 21 unrelated tests once,
|
|
@@ -460,6 +456,7 @@ Standard envelope (constitution: result envelope); `data` sketch:
|
|
|
460
456
|
"statementCount": 1,
|
|
461
457
|
"statements": [
|
|
462
458
|
{
|
|
459
|
+
"id": "s0",
|
|
463
460
|
"span": { "start": 0, "end": 51 },
|
|
464
461
|
"resolution": "resolved",
|
|
465
462
|
"kind": "command",
|
|
@@ -511,6 +508,36 @@ Standard envelope (constitution: result envelope); `data` sketch:
|
|
|
511
508
|
classification, and evidence id. This is where multi-statement scripts and
|
|
512
509
|
partial recovery live; a single top-level transport object cannot represent
|
|
513
510
|
them.
|
|
511
|
+
- **Statement ownership is syntactic (#497).** Every published statement has
|
|
512
|
+
an `id` (`s0`, `s1`, …) in discovery order, stable within one result. A nested
|
|
513
|
+
scope-body statement's `parent` names its owning statement; absence means
|
|
514
|
+
top level. Each substitution's `owner` names its containing statement,
|
|
515
|
+
including nested brackets. These links come from the resolver's traversal,
|
|
516
|
+
so widened Unicode spans do not turn siblings into ancestors. IDs are not
|
|
517
|
+
persistent across edits.
|
|
518
|
+
|
|
519
|
+
The existing `blocks[]` inventory still lists outer scope bodies only.
|
|
520
|
+
A block's optional `owner` names the statement carrying it, and a statement's
|
|
521
|
+
optional `block` indexes its innermost **published** enclosing block. A nested
|
|
522
|
+
body can therefore share its ancestor's block index. Bare/menu containers
|
|
523
|
+
add no synthetic statement or block nodes; the tree links only statements
|
|
524
|
+
the existing analysis exposes. Existing lists, order, spans and readings stay
|
|
525
|
+
unchanged. A widened span remains a coordinate limitation, not a second
|
|
526
|
+
parentage rule.
|
|
527
|
+
|
|
528
|
+
Stored/deferred `source=`, `on-event=` and callback bodies remain opaque:
|
|
529
|
+
their statements are not added to this tree. Creation versus explicit
|
|
530
|
+
stored-script execution is grounded by
|
|
531
|
+
`test/fixtures/explain/statement-ownership.json` and replayed by
|
|
532
|
+
`test/integration/explain-structure.test.ts`. Ownership asserts no lexical
|
|
533
|
+
inheritance, immediate execution, or shared user/policy environment;
|
|
534
|
+
context-sensitive binding/value semantics remain #507's responsibility.
|
|
535
|
+
|
|
536
|
+
`test/unit/explain-structure.test.ts` independently checks ancestry against
|
|
537
|
+
span containment where spans recover exact source text, with explicit
|
|
538
|
+
widened-span controls. After `bun run corpus:fetch`, run
|
|
539
|
+
`CENTRS_CHECK_CORPUS=1 bun test test/unit/explain-structure.test.ts` for the
|
|
540
|
+
same contract over the pinned study population; the corpus CI job runs it.
|
|
514
541
|
- **Every derived fact keeps provenance via `evidence[]`** — facts reference
|
|
515
542
|
a stable evidence id (`ev`), and the evidence entry carries source
|
|
516
543
|
(`canonicalizer` vs `live-inspect`), probe, basis (direct response vs
|
|
@@ -621,7 +648,7 @@ and its phase is named below.
|
|
|
621
648
|
| `structure.statements[].command` | `path` + `verb`, plus **`args` where the argument list was read** (#202c-1); the ordered token list is `statements[].arguments.tokens` |
|
|
622
649
|
| `structure.statements[].transport` | complete for commands: `api-candidate`, `execute`, or fail-closed `unknown`, with an equivalent `centrs` invocation and opt-in `curl` for tested REST mappings (#202c-2). The `basis` names the evidence for the route it chose or refused |
|
|
623
650
|
| `symbols.occurrences[]` | Q13 name/span/class plus semantic `role`, result-local `bindingIds`, sigil spelling, and an abstention note where needed; **plus** (#239 S2) `valueId` on declarations/assignments with a literal RHS in the same statement, and `reachingValueIds` + `reachingUnknown` on references (flow-sensitive, branch → set, loop/non-literal → `unknown`, no RHS evaluation) |
|
|
624
|
-
| `values.occurrences[]` | result-local value id/span plus three separate fact homes: offline `shapeHints`; live `observedType` and `schemaType`
|
|
651
|
+
| `values.occurrences[]` | result-local value id/span plus three separate fact homes: offline `shapeHints`; live `observedType` and `schemaType` are added only by a live call ([Live value types](#live-value-types-225)). `kind: "element"` rows are array members, carrying `parent` and, for a keyed member, `name` |
|
|
625
652
|
| `spans` | comment runs and resolved variable classes only; **no value shape or type** |
|
|
626
653
|
| `schema`, `completion` | absent — live evidence, phase 2 |
|
|
627
654
|
|
|
@@ -713,13 +740,10 @@ and its phase is named below.
|
|
|
713
740
|
rejected by RouterOS at `{` and resolve no reference. In an expression the
|
|
714
741
|
remaining `-dns` is scanned as subtraction plus its right operand; inside a
|
|
715
742
|
string it is literal text.
|
|
716
|
-
- **Evidence
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
typed to what offline produces rather than pre-declaring variants nothing can
|
|
721
|
-
emit, so phase 2 widens the union and adds the stamp — an addition, not a
|
|
722
|
-
change to what a caller reads today.
|
|
743
|
+
- **Evidence follows its producer.** Offline calls emit only `canonicalizer`
|
|
744
|
+
entries, with no device version. Live calls add `live-inspect` entries with
|
|
745
|
+
probe outcomes and available device provenance; existing offline entries
|
|
746
|
+
retain their meaning. See [Live probe foundation](#live-probe-foundation-491).
|
|
723
747
|
- **Value facts are three axes, never one `type` string (#225).** RouterOS
|
|
724
748
|
types values at parse time while `highlight` classes every value byte `none`,
|
|
725
749
|
so type evidence is not a subset of Q12 spans. Each result-local
|
|
@@ -732,7 +756,7 @@ and its phase is named below.
|
|
|
732
756
|
The stable/testing CHR matrix fixes the offline lexicon: `num`, `ip`,
|
|
733
757
|
`ip-prefix`, `ip6`, `ip6-prefix`, `id`, `time`, `array`, `mac`, `bool`, and
|
|
734
758
|
`str` — `op` is deliberately not a member: a deferred `(> …)` abstains
|
|
735
|
-
offline and
|
|
759
|
+
offline and live alike (#288, #225); quoted literals
|
|
736
760
|
hint only `str`, while malformed and out-of-range address-like controls
|
|
737
761
|
abstain even in named attributes. Most vocabulary borrows RouterOS scripting
|
|
738
762
|
type names (with the explicit `macAddr` schema exception below), so a hint
|
|
@@ -766,8 +790,9 @@ and its phase is named below.
|
|
|
766
790
|
1 and identical `:parse` IL — only `:typeof` distinguishes them
|
|
767
791
|
(`test/fixtures/explain/operators.json` → `sweep.runtime`
|
|
768
792
|
`typeof-deferred`/`deferred-array-typeof` and `sweep.opAxis` `defer-*` on
|
|
769
|
-
7.21.5, 7.23.3, 7.24rc4). Offline abstains on every such form
|
|
770
|
-
|
|
793
|
+
7.21.5, 7.23.3, 7.24rc4). Offline abstains on every such form, and so does
|
|
794
|
+
the live `observedType`, which never sends `:typeof` (#288, #225). `op` has
|
|
795
|
+
no producer.
|
|
771
796
|
Shape remains non-authoritative: `100000w` is time-shaped but observed as
|
|
772
797
|
`str`. Argument context remains separate: on 7.23.3 and 7.24rc3, both
|
|
773
798
|
`/ip/address address=2.2` and firewall `src-address=2.2` canonicalize it to
|
|
@@ -781,8 +806,8 @@ and its phase is named below.
|
|
|
781
806
|
and does not widen the hint. Thus a hint never validates a value or becomes a
|
|
782
807
|
diagnostic, and never changes `runtimeAcceptance`. Quoting likewise never
|
|
783
808
|
earns a wrong-type diagnostic: commands may cast quoted strings at runtime.
|
|
784
|
-
Offline emits only `shapeHints`;
|
|
785
|
-
existing homes
|
|
809
|
+
Offline emits only `shapeHints`; a live call adds the two live facts into
|
|
810
|
+
their existing homes ([Live value types](#live-value-types-225)).
|
|
786
811
|
|
|
787
812
|
**A located array is read member by member (#225 V1).** Each member of a
|
|
788
813
|
`{…}`/`(…)` literal is its own `values.occurrences[]` row with `kind:
|
|
@@ -907,7 +932,16 @@ and its phase is named below.
|
|
|
907
932
|
top-level brace value in any other command argument emits
|
|
908
933
|
`explain/canonicalizer/invalid-command-brace` at the `{`; parentheses or a
|
|
909
934
|
`[…]` substitution restore the expression context where the same literal is
|
|
910
|
-
legal.
|
|
935
|
+
legal. So does a top-level `where` clause (#502): `print where interface in
|
|
936
|
+
{"ether1";"ether2"}` is a read, and its array is read member by member like
|
|
937
|
+
its `(…)` twin. The exception is the right side of `<name>=`, `!=` or `==`,
|
|
938
|
+
spaced or not, which the device reads with that property's own parser
|
|
939
|
+
(`expected interface value`), so `where comment={1;2}` keeps its error. The
|
|
940
|
+
device does that only for a property the menu knows (`where foo-bar={…}`
|
|
941
|
+
parses). Offline has no property list, so it treats every bare name as one.
|
|
942
|
+
The same comparison inside `( … )` or a `[ … ]` command
|
|
943
|
+
(`[find where interface={"x"}]`, `where (interface={"x"})`) is caught by the
|
|
944
|
+
group walk (#509, below) and reported with the same code. An empty member is a
|
|
911
945
|
syntax error (`{;}`, `{;1}`, `{1;;2}`, `(1,)`) and withdraws the enclosing
|
|
912
946
|
`array` shape with it; a single trailing separator is legal in the brace form
|
|
913
947
|
only (`{1;}` and `{1;2;}` parse, `{1;2,}` and `{2,}` do not).
|
|
@@ -947,15 +981,16 @@ and its phase is named below.
|
|
|
947
981
|
|
|
948
982
|
<!-- BEGIN GENERATED value-census — regenerate with `bun run explain:value-census:readme` -->
|
|
949
983
|
The corpus census is re-derivable with `bun run explain:value-census` and
|
|
950
|
-
covers 948 source scripts
|
|
951
|
-
`
|
|
952
|
-
|
|
984
|
+
covers 948 source scripts of corpus snapshot `e12d5d4e58a4`, with 882
|
|
985
|
+
auxiliary rows (7 `capture-controls`, 875 `wiresage-conformance`) excluded.
|
|
986
|
+
The figures below are generated from `test/fixtures/explain/values.json` →
|
|
987
|
+
`corpus` by `bun run explain:value-census:readme` and gated against it by
|
|
953
988
|
`bun run explain:value-census:readme:check`; the fixture itself is gated
|
|
954
989
|
against a fresh corpus run by `bun run explain:value-census:check`. Of the
|
|
955
|
-
13,
|
|
990
|
+
13,152 emitted values in a statement the strict argument lexer ALSO read, 0
|
|
956
991
|
disagree with it on half-open byte span or decoded text, while the
|
|
957
992
|
prefix-safe scan retains a further 6,538 values across 568 statements whose
|
|
958
|
-
strict REST reading abstains. Of 19,
|
|
993
|
+
strict REST reading abstains. Of 19,690 emitted occurrences, 5,636 are array
|
|
959
994
|
members (529 keyed, 1,147 nested inside another member) and 808 are arrays;
|
|
960
995
|
the corpus holds no source-literal `id` example. The three structural
|
|
961
996
|
counters — spans addressing bytes outside their own source, members naming a
|
|
@@ -982,7 +1017,8 @@ comment, a literal hash value, and a hard error at the exact byte.
|
|
|
982
1017
|
| Statement-leading at the document root, after a real statement separator, in a `do`/`else`/`foreach do` body, or immediately inside a `[…]` command substitution | `comment` through newline | Mask it and preserve byte offsets. A command first consumes that lead, so `[:put #test]` keeps `#test` as the value. |
|
|
983
1018
|
| First non-space content inside a stored-script brace such as `on-event={ # c` | `comment` | Treat exact `source`/`script` and `on-*`-named brace values as comment-bearing script bodies; a suffix such as `myScript` is not one. |
|
|
984
1019
|
| Immediate line start after `\` + newline | `comment`; the pending statement survives | Mask it in both argument views; the continuation-reach rules remain H5/#215. |
|
|
985
|
-
| Inside an array (`{#test}`, `{1;#test}`, `{a=1;#b=2}`) or parenthesized expression (`(1,#test)`) | hard `error` at `#`; `:parse` reports `syntax error` | Do not mask it or emit an array value hint; semantic resolution stops there. |
|
|
1020
|
+
| Inside an array (`{#test}`, `{1;#test}`, `{a=1;#b=2}`) or parenthesized expression (`(1,#test)`), except at column 0 inside an array (next row) | hard `error` at `#`; `:parse` reports `syntax error` | Do not mask it or emit an array value hint; semantic resolution stops there. |
|
|
1021
|
+
| At column 0 of its own line inside an array, at any depth and in argument values (`:local z {a=1;⏎# c⏎b=2}`, `{a={⏎# c⏎1}}`, `({⏎# c⏎1})`, `:foreach i in={1;⏎# c⏎2} do=…`), including after `\r⏎` and a `\`-continued line (#503) | `comment` through newline; `:parse` drops it (`value=a=1;b=2`) and `highlight` has no `error` on CHR 7.24.5 and 7.22.1 | Mask it with `startsArrayCommentLine`, which every walker asks with its innermost frame. It ends nothing: the array's statement carries on. An indented `#` (space or tab), one after `{`, a value or `;` on the same line, and one at column 0 directly inside `( … )` stay errors. A comment-only array (`{⏎# c⏎}`) is rejected only because an empty array is (#511). |
|
|
986
1022
|
| A scope-named brace (`do=`/`else=`/`command=`/`script=`/`source=`/`on-*=`) nested inside an array (`:local z {do={ # c⏎:put 1}}`, `:local z {script={ # c⏎…}}`, `:local z { { # c…}}`) — and likewise a bare `{` or `/menu {` inside an array | hard `error` at `#`; `/console/inspect` request `highlight` classes that byte `error` on CHR 7.23.3 (grounded in #249 on the four `#249` rows plus `bare {`/`source`/`on-event` corners) | A brace bears statements only when its enclosing context already does — `enclosing && braceStartsStatements(…)` on each walker's stack (`contexts`/`delimStack`/`frames`/`stack`). Inside an array the name is an array key and its `{…}` is a nested array. A `[…]` substitution nested in that array does re-enter a statement context, so `[:do { # c…}]` stays a real comment and a bare `{` under a bracket does too. |
|
|
987
1023
|
| Inside a `[…]` substitution that is itself nested in an array (`{[:put #test]}`, `{1;[:put #test]}`) | literal value (`none`) — the bracket restores the statement role the array dropped | Keep it as content and keep the enclosing array readable. The role is per frame, so an array or group opened again inside that bracket (`{[:put {#test}]}`, `{[:put (1,#test)]}`) is `error` once more; skipping whole `[…]` regions would wrongly accept those. |
|
|
988
1024
|
| In an attribute or bare value (`comment=#test`, `comment=a#b`, `:global y #test`) | literal value (`none` highlight class) | Keep it as content. |
|
|
@@ -1020,6 +1056,14 @@ JSON-like transcript, and two shell-shaped snippets. No RouterOS command reading
|
|
|
1020
1056
|
moved; these diagnostics say those bytes are not valid RouterOS, not that the
|
|
1021
1057
|
foreign snippets were expected to parse as RouterOS.
|
|
1022
1058
|
|
|
1059
|
+
Corpus re-measurement for #503: one script of 947 moves. It is a pasted console
|
|
1060
|
+
session (`forum/amm0/topic-264287-…/post-0005-snippet-01.rsc`, which opens with
|
|
1061
|
+
`[admin@MikroTik] > {`), and it moves from `fail` to `warn`. Its `invalid-hash` sat on a column-0
|
|
1062
|
+
`# ... more code` line inside the brace after `>`, which the device reads as
|
|
1063
|
+
an array (`:local z (1 > {⏎:local a 1⏎# c⏎})` parses). The device rejects the
|
|
1064
|
+
script earlier, at the prompt's `@`, which is #512. Every census gate except
|
|
1065
|
+
the token census is unchanged; that one gains the two comment spans.
|
|
1066
|
+
|
|
1023
1067
|
The default text renderer surfaces comment spans in a `comments:` section; JSON
|
|
1024
1068
|
continues to carry the same ranges as `spans[]` entries with `class: "comment"`.
|
|
1025
1069
|
|
|
@@ -1074,10 +1118,18 @@ navigated, so the context stays as unknown as it already was.
|
|
|
1074
1118
|
`:parse` `expected message value`, grounded stable+testing #247),
|
|
1075
1119
|
`bad-sigil`,
|
|
1076
1120
|
`invalid-hash`, `invalid-apostrophe` (#355, below),
|
|
1077
|
-
`missing-statement-separator` (#311, below)
|
|
1078
|
-
|
|
1121
|
+
`missing-statement-separator` (#311, below), the four group faults
|
|
1122
|
+
`empty-group`, `misplaced-separator`, `missing-operand` and
|
|
1123
|
+
`invalid-at-sign` (#505, #511, #512, below), and the four token faults
|
|
1124
|
+
`glued-token`, `invalid-variable`, `bad-string-variable` and
|
|
1125
|
+
`invalid-name` (#506, #518, below), and the two unquoted-byte faults
|
|
1126
|
+
`unquoted-reserved-byte` and `unquoted-non-ascii` (#358, #371, below).
|
|
1127
|
+
Nineteen classes the device itself rejects.
|
|
1079
1128
|
- `warning` — `over-depth`, because it is centrs's own resource bound and says
|
|
1080
|
-
nothing about whether the input is legal;
|
|
1129
|
+
nothing about whether the input is legal; `possible-spaced-argument`
|
|
1130
|
+
(#350), a spelling RouterOS accepts and marks obsolete; `discarded-close`
|
|
1131
|
+
(#504), a script RouterOS accepts and stops reading early, which the
|
|
1132
|
+
execute gate refuses by code; and an `ambiguous`/`unknown`
|
|
1081
1133
|
resolution, never an error, so the default `--fail-on error` cannot fail a
|
|
1082
1134
|
document whose only sin is being unreadable without a schema — which is most
|
|
1083
1135
|
of RouterOS scripting.
|
|
@@ -1182,6 +1234,200 @@ it. The corpus count is unchanged at **0** for the same reason it always was
|
|
|
1182
1234
|
the rule is an unterminated string or an unbalanced delimiter, because neither
|
|
1183
1235
|
walk can find a boundary to resume from.
|
|
1184
1236
|
|
|
1237
|
+
### Faults inside groups (#505, #509, #511, #512)
|
|
1238
|
+
|
|
1239
|
+
Four missed-rejection families share one walk, `src/explain/group-faults.ts`
|
|
1240
|
+
(evidence pass `findGroupFaults`). It reads the comment-masked document with
|
|
1241
|
+
the frame roles every other walker uses: `[` opens a statement, `(` an
|
|
1242
|
+
expression, and `{` a body when `braceStartsStatements` says so or the
|
|
1243
|
+
directive takes a program in that slot (`:execute {…}`, `:time {…}`). Each
|
|
1244
|
+
rule is a `:parse` answer on CHR 7.24.5 and 7.23.8, identical on both, and
|
|
1245
|
+
`test/fixtures/explain/group-faults.json` pins all 186 rows (100 rejections,
|
|
1246
|
+
86 controls). `test/integration/explain-group-faults.test.ts` holds the
|
|
1247
|
+
device to that fixture, and `test/unit/explain-group-faults.test.ts` holds
|
|
1248
|
+
offline to it.
|
|
1249
|
+
|
|
1250
|
+
| Code | Device rule | Rejected | Accepted |
|
|
1251
|
+
| ---- | ----------- | -------- | -------- |
|
|
1252
|
+
| `empty-group` | A `( )` with nothing in it, and a `{}` that is the whole value of a directive slot | `:put ()`, `:if () do={}`, `:local z {}`, `:put [:len {}]`, `:foreach i in={} do={}` | `{1;{}}`, `{a={}}`, `({})`, `[{}]`, `where … in {}`, `do={}`, `source={}`, `:execute {}` |
|
|
1253
|
+
| `misplaced-separator` | A `;` with no array element before it on its line; a `;` or a bare newline directly inside `( … )` | `{;1}`, `{1;;2}`, `{1⏎;2}`, `(1; 2)`, `(1⏎+ 2)` | `{1;}`, `{1;2;⏎}`, `{1⏎2}`, `{1 \⏎;2}`, `(1 +\⏎2)`, `({1;2})` |
|
|
1254
|
+
| `missing-operand` | An operator with no operand: a binary one first in a group or after another operator, any one last, and a leading or trailing `,` in an array | `(1 +)`, `(+ 1)`, `(1 + + 2)`, `(1 == 2)`, `(!)`, `(1 and)`, `{1,}`, `{,1}` | `(1 2)`, `(- 1)`, `(1 + -2)`, `(-> 1)`, `(any any 1)`, `(and 1)`, `(. 1)`, `(1.)` |
|
|
1255
|
+
| `invalid-at-sign` | A `@` in a statement's head word, in an array, or in an expression | `[admin@MikroTik]`, `admin@x`, `:put@x`, `{1;a@b}`, `(a@b)` | `:put a@b`, `comment=a@b`, `[:len a@b]`, `[:len x] a@b` |
|
|
1256
|
+
|
|
1257
|
+
`invalid-command-brace` gains the #509 positions: a brace after `<name>=`
|
|
1258
|
+
inside `( … )` or a `[ … ]` command whose statement says `where`. The top level
|
|
1259
|
+
and scope bodies stay with `args.ts` (#502), and a `do={` body is never read as
|
|
1260
|
+
a comparison.
|
|
1261
|
+
|
|
1262
|
+
**Prefix operators are the device's set, not the operator table's arity.**
|
|
1263
|
+
`-`, `!`, `>`, `~` and `any` take one operand after them, so `(1 + -2)` and
|
|
1264
|
+
`(-> 1)` (`(- (> 1))`) parse. Every other operator needs a left operand. At
|
|
1265
|
+
operand position `and`, `or`, `in`, `.` and `/` are variable names
|
|
1266
|
+
(`(and 1)` is `( $and 1)`). `1.`, `1/`, `a_b`, `1:2` and `1.2.3.4/24` are
|
|
1267
|
+
single words, while `-` is always an operator (`(a-b)` is `(- $a $b)`).
|
|
1268
|
+
|
|
1269
|
+
**A query is not an expression.** Inside `where …` and `find …`, the device
|
|
1270
|
+
reads the value after `<name>=` with the property's parser. So
|
|
1271
|
+
`where (comment=+)`, `where (comment=a-)`, `where (comment=@)` and
|
|
1272
|
+
`where (.id=*1)` parse, though each is an error in a plain group. A group in a
|
|
1273
|
+
query gets only the structural checks (empty, `;`, newline) and #509.
|
|
1274
|
+
|
|
1275
|
+
**A brace offline cannot place is opaque, not an array.** A `{` after a
|
|
1276
|
+
relative menu word is a menu body (`/ip { route { } }` parses empty), and a
|
|
1277
|
+
brace in a command argument is `args.ts`'s. Neither is checked here. The
|
|
1278
|
+
array rules apply where the role is grounded: a directive slot in the
|
|
1279
|
+
`brace-slots.ts` array table, and an array nested in an array or an expression.
|
|
1280
|
+
|
|
1281
|
+
**Three readings the review round grounded (PR #516).** A `\` continuation
|
|
1282
|
+
runs on across blank lines and line-leading comment lines, as H5 does
|
|
1283
|
+
elsewhere, so `(1 +\⏎# c⏎2)` and `{1\⏎⏎;2}` parse, while a blank line after a
|
|
1284
|
+
comment line ends it (`(1 +\⏎# c⏎⏎2)` is rejected at the blank line). A
|
|
1285
|
+
directive's operand is a name, so `:local where (comment={})` parses and
|
|
1286
|
+
`:local where (1 +)` is checked like any expression. A bare `local`/`global`
|
|
1287
|
+
is the same directive at the root (`local where (comment={})` is
|
|
1288
|
+
`/localname=$where`), while a bare `set` or `find` may be a menu command and
|
|
1289
|
+
stays unread. And a named element takes
|
|
1290
|
+
a trailing `,` (`{a=1,}` is `value=a`, `{a=1,;b=2}` is `value=a;b=2`), while
|
|
1291
|
+
`{a=,1}`, `{a=1,,2}` and a positional `{1,}` are rejected. A faulted group's
|
|
1292
|
+
descendants are opaque, since the device stops at the fault.
|
|
1293
|
+
|
|
1294
|
+
**Spans.** Each fault spans the token it is about: the operator, the
|
|
1295
|
+
separator, the `@`, or the whole empty group. The device's column is the same
|
|
1296
|
+
byte for every rule except an empty group and a dangling operator, where it is
|
|
1297
|
+
the byte after.
|
|
1298
|
+
|
|
1299
|
+
**Measured.** Corpus `e12d5d4e58a4`: 70 of 948 scripts gain an error and all
|
|
1300
|
+
70 are device rejections on every channel. 42 of those errors sit on the exact
|
|
1301
|
+
byte the 7.24.5 diagnostic names, and the other 28 come after an earlier device
|
|
1302
|
+
error in pasted non-RouterOS text. None precedes the device's error. 41
|
|
1303
|
+
verdicts move to `fail` (40 from `warn`, one from `pass`), so offline now fails
|
|
1304
|
+
133 of the 374 device-rejected scripts, up from 92, and false rejects stay at 1.
|
|
1305
|
+
`invalid-at-sign` is mostly pasted console prompts. On the conformance cases,
|
|
1306
|
+
22 `plain-diagnostic` missed rejections move to `fail`, with no new false
|
|
1307
|
+
reject.
|
|
1308
|
+
|
|
1309
|
+
Not covered here: a `$( … )` group, a group under a `find` head
|
|
1310
|
+
(`[find (1 +)]`, read as a query), a `@` in a later head word
|
|
1311
|
+
(`/ip address@ print`), operand rules for array elements other than `,`, a
|
|
1312
|
+
group in a query beyond its structure, and adjacency without whitespace
|
|
1313
|
+
(`"x"~a`, `}else=`), which is the next section's.
|
|
1314
|
+
|
|
1315
|
+
### Token faults and the discarded tail (#504, #506, #518)
|
|
1316
|
+
|
|
1317
|
+
Three more missed-rejection families, and one reading that was wrong in the
|
|
1318
|
+
other direction. The glued-token, variable and name rules ride the same walk as
|
|
1319
|
+
the group faults (`src/explain/group-faults.ts`), so they share its statement,
|
|
1320
|
+
query and brace roles. The `$` inside a string is checked by the escape walk
|
|
1321
|
+
(`src/explain/quoted-string.ts`), and the stray close by the segmenter
|
|
1322
|
+
(`src/explain/segment.ts` → `discardFrom`). Every rule is a `:parse` answer on
|
|
1323
|
+
CHR 7.24.5 and 7.23.8, identical on both, and
|
|
1324
|
+
`test/fixtures/explain/token-faults.json` pins these rows and the unquoted-byte
|
|
1325
|
+
rows below: 292 in all (184 rejections and 108 acceptances, with the
|
|
1326
|
+
acceptances' IL). `test/integration/explain-token-faults.test.ts`
|
|
1327
|
+
holds the device to that fixture, and `test/unit/explain-token-faults.test.ts`
|
|
1328
|
+
holds offline to it.
|
|
1329
|
+
|
|
1330
|
+
| Code | Device rule | Rejected | Accepted |
|
|
1331
|
+
| ---- | ----------- | -------- | -------- |
|
|
1332
|
+
| `discarded-close` (warning) | A `}` or `]` that closes nothing at the top level ends the script: the device accepts it and drops that byte and everything after it | — | `}` (IL `/`), `:put 1};:put 2` (IL `(evl /putmessage=1)`), `name=a}` (sets `a`) |
|
|
1333
|
+
| `glued-token` | After a closed string, `$name`, `$"…"` or group, only a separator; after a bare word, no `"`, `$`, `(`, `[` or `{` | `:put "a"b`, `$v.b`, `$v$v`, `(1)(2)`, `[:len a]b`, `}else=`, `src-address=$v/24`, `dst-port="80",443`, `:put a"b"`, `name=a$v` | `:put a.b`, `a-b`, `a_b`, `a#b`, `a~b`, `where comment="a"b`, `[find comment="a"b]`, `("a"b)`, `{"a"b}`, `dst-address-list=!"my list"`, `dst-port=80,"443"` |
|
|
1334
|
+
| `invalid-variable` | Outside a string, `$` takes a name (`[A-Za-z0-9]+`) or `$"…"` | `:put $`, `$_`, `$-`, `$.`, a `$` before a space | `$v1`, `$"a b"` |
|
|
1335
|
+
| `bad-string-variable` | Inside a string, `$` takes a name, `"`, `(` or `[` | `"a$ b"`, `"$-"`, `"$_"`, `"${"`, and the same in `where`, an array, a group and a nested `$[…]` string | `"$v_x"` (`$v` then `_x`), `"\$ x"`, `"$(1)"`, `"$[:len a]"` |
|
|
1336
|
+
| `invalid-name` | The first operand of `:local`, `:global`, `:set`, `:for` and `:foreach` is a name of letters and digits, or a string | `:local a_b`, `a-b`, `a.b`, `a$v`, `:local $a`, `:local (a)`, `:for i,j`, `:foreach a,`, `:foreach a,,b` | `:local "a_b"`, `:local 1a`, `:foreach a,b`, `:foreach "a",b`, `:local name=x value=1`, `:set (a) 1` |
|
|
1337
|
+
|
|
1338
|
+
**The stray close was the honest-reading bug.** Offline used to fail
|
|
1339
|
+
`name=a}` with `unbalanced-close`, and the device accepts it. But the device
|
|
1340
|
+
does not just ignore the `}`: it stops reading there. `}⏎:put 1` lowers to `/`,
|
|
1341
|
+
and a write after the brace never runs. So `explainCommand` now analyzes only
|
|
1342
|
+
the prefix the device reads and adds one `warning` that spans the dropped
|
|
1343
|
+
bytes. The token partition still covers the whole input, with the tail
|
|
1344
|
+
`unclassified`. A `)` in that position, and any stray close inside an open
|
|
1345
|
+
group, are rejections and stay `unbalanced-close` errors. The execute gate
|
|
1346
|
+
refuses `discarded-close` by code, the one warning it refuses
|
|
1347
|
+
(`src/offline-gate.ts`; `commands/execute/examples.md` V9).
|
|
1348
|
+
|
|
1349
|
+
**Where a property's parser decides, the glued-token rule abstains.** In a
|
|
1350
|
+
command argument the device hands the value to the property's parser, which can
|
|
1351
|
+
take a glued part after a prefix or separator: `dst-address-list=!"my list"`
|
|
1352
|
+
and `dst-port=80,"443"` parse, while `name=!"a"` and `name=-"a"` do not. So
|
|
1353
|
+
outside a `:` directive, a bare word followed by a `"`, `$` or bracket is a
|
|
1354
|
+
fault only after an alphanumeric byte. After a closed part no property was found
|
|
1355
|
+
that accepts anything (`"80",443`, `$p-90`, `$v%ether1`, `"ether"1` are all
|
|
1356
|
+
rejected), so that half of the rule applies everywhere outside a query.
|
|
1357
|
+
|
|
1358
|
+
**Spans.** Each fault spans its byte, which is the device's byte too. For the
|
|
1359
|
+
two `$` rules the span is the `$` and the byte after it, and the device names
|
|
1360
|
+
that second byte.
|
|
1361
|
+
|
|
1362
|
+
**Measured.** Corpus `e12d5d4e58a4`: 23 scripts move to `fail` (18 from
|
|
1363
|
+
`warn`, 5 from `pass`), and 29 already-failing scripts gain a new code. All are
|
|
1364
|
+
device rejections on every channel. 9 of their first new errors sit on the
|
|
1365
|
+
7.24.5 diagnostic's byte, 45 come after it, and none precedes it. Offline now
|
|
1366
|
+
fails 154 of the device-rejected scripts, up from 133. The one corpus false
|
|
1367
|
+
reject (a stray `}`) is gone, so false rejects are 0. Three scripts move from
|
|
1368
|
+
`fail` to `warn`: their `unbalanced-close` became `discarded-close`, and two of
|
|
1369
|
+
them are device rejections for an earlier reason offline does not see. On the
|
|
1370
|
+
conformance cases, false rejects go from 3 to 0 and missed rejections from 278
|
|
1371
|
+
to 151.
|
|
1372
|
+
|
|
1373
|
+
**Not covered here.** `"a$"`: the device reads `$"` in a string as a quoted
|
|
1374
|
+
variable name, so the next `"` does not close the string. Every scanner in
|
|
1375
|
+
`explain` closes it there, so both `$` rules and the glued-token rule abstain
|
|
1376
|
+
around a string that ends in `$"` (2 of 948 corpus scripts use the form).
|
|
1377
|
+
`$$`, `$(…)`, `$[…]` and `${…}` outside a string, where the device's byte is
|
|
1378
|
+
not the `$`. A `\` continuation inside a glued token (`"a"\⏎"b"`). A glued
|
|
1379
|
+
head word (`$v"a"`, `{…}:put 2`). `:local a=b`, which 7.24.5 reads as
|
|
1380
|
+
a named argument and 7.23.8 as a syntax error. And a name in `/local` or
|
|
1381
|
+
`:local name=a_b`.
|
|
1382
|
+
|
|
1383
|
+
### Unquoted bytes (#358, #371)
|
|
1384
|
+
|
|
1385
|
+
A `?`, a backtick and a non-ASCII byte are never part of a bare word, and
|
|
1386
|
+
neither is a second `=` in a menu command's argument. The rules ride the
|
|
1387
|
+
token walk (`src/explain/group-faults.ts`). They were grounded with live
|
|
1388
|
+
explain (`explainLiveEnvelope`: `:parse` and `highlight`) on CHR 7.24.5 and
|
|
1389
|
+
7.23.8, which answered identically. The 147 rows are in
|
|
1390
|
+
`test/fixtures/explain/token-faults.json`.
|
|
1391
|
+
|
|
1392
|
+
| Code | Device rule | Rejected | Accepted |
|
|
1393
|
+
| ---- | ----------- | -------- | -------- |
|
|
1394
|
+
| `unquoted-reserved-byte` | `?` and a backtick, at the byte, outside a string or comment. A second `=` in a menu command's argument, at that `=` | `name=a?b`, ``name=a`b``, `name=?b`, `:put a?b`, `[:len a?b]`, `where comment=a?b`, `[find comment=a?b]`, `where (comment=a?b)`, `where comment in {a?b}`, `{a?b}`, `(a?b)`, `/ip/address/print?`, `name=a=b`, `name==b`, `comment=x==`, `url=http://x/a?b=c`, `regexp=^a?b`, `public-key=…=`, `secret=abc==`, `on-event=a=b` | `name="a?b"`, ``name="a`b"``, `name="a=b"`, `where comment=a=b`, `[find comment=a=b]`, `[/ip/address/find comment=a=b]`, `where (comment=a=b)`, `where comment in {"a?b"}`, `{a=b}`, `{"a?"=1}`, `# a?b`, `("a"."?")` |
|
|
1395
|
+
| `unquoted-non-ascii` | A non-ASCII byte outside a string or comment, at its first byte; the span is the whole run | `:put café`, `$café`, `name=router-café`, `name=🛰`, `/systemé/identity`, `printé`, `namé=x`, `where comment=café`, `{café}`, `(café)`, `"x"é`, a non-breaking space, `“x”`, `address=…` | `:put "café"`, `:local "café" 1`, `$"café"`, `"a$café"` (`$caf` then `é`), `name="router-🚀"`, `# café`, `do={:put "café"}` |
|
|
1396
|
+
|
|
1397
|
+
**Where the device reports, offline reports.** In a menu command's argument
|
|
1398
|
+
name the device names the name's first byte (`/ip/address/add addré=1` and
|
|
1399
|
+
`a?b=1` at the `a`), so the span is the name. A directive (`:put café=1`) and
|
|
1400
|
+
a `$` call (`$f café=1`) report at the byte. A declared name keeps the
|
|
1401
|
+
existing `invalid-name` (`:local café`, `:foreach café`). A query, and a
|
|
1402
|
+
group or array inside one, keeps `=` as an operator but not these bytes
|
|
1403
|
+
(`where (comment=café)` and `in {café}` are rejected at the `é`), and the slash-joined `[/ip/address/find …]` is a query, as
|
|
1404
|
+
`[find …]` is. A leading BOM is a positional fact and is not read as part of
|
|
1405
|
+
the first word. The informational `non-ascii` fact is still published beside
|
|
1406
|
+
the error, so `isPositionalFact("non-ascii")` is unchanged.
|
|
1407
|
+
|
|
1408
|
+
**No property parser takes these bytes.** This is the difference from the
|
|
1409
|
+
glued-token rule. `regexp=a?b` (layer7 and DNS static), ``regexp=a`b``,
|
|
1410
|
+
`public-key=<base64>=`, `secret=abc==`, `password=abc=`,
|
|
1411
|
+
`http-header-field=a:b=c` and the script bodies `on-event=a=b` and
|
|
1412
|
+
`up-script=a=b` are all rejected at the byte.
|
|
1413
|
+
|
|
1414
|
+
**Measured.** Corpus `e12d5d4e58a4`: 8 scripts move to `fail` (7 from `warn`,
|
|
1415
|
+
one from `pass`), and 20 already-failing scripts gain a new code. All 28 are
|
|
1416
|
+
device rejections on every channel. 8 of their first new errors sit on the
|
|
1417
|
+
7.24.5 diagnostic's byte, 20 come after it, and none precedes it. There are no
|
|
1418
|
+
false rejects. On the conformance cases, 10 missed rejections move to `fail`
|
|
1419
|
+
(the `non-ascii-outside-strings` u10–u12 and u14–u16, z90, z92, g18 and e33), with no new
|
|
1420
|
+
false reject.
|
|
1421
|
+
|
|
1422
|
+
**Not covered here.** `:put a=b`, `:local x a=b` and `[:len a=b]`: 7.24.5
|
|
1423
|
+
reads them as a named argument (`bad parameter a`, at the end), and 7.23.8
|
|
1424
|
+
rejects the `=`. A query value that starts with `=` (`where comment==b`,
|
|
1425
|
+
`expected comment value`). A bare `source=` value,
|
|
1426
|
+
which the device rejects at its first byte whatever it holds (`source=abc`), so
|
|
1427
|
+
a byte rule there reports after the device's byte. And `:set` of an undeclared
|
|
1428
|
+
name, which the device rejects at the name's first byte (`:set zzz 2`), so
|
|
1429
|
+
`:set café` is reported at the `é`, after it.
|
|
1430
|
+
|
|
1185
1431
|
### The token stream a rule reads (#316)
|
|
1186
1432
|
|
|
1187
1433
|
`lexArguments` is all-or-nothing by design: its consumer renders a runnable REST
|
|
@@ -1394,17 +1640,108 @@ print } }` is `(evl (evl (evl /ip/address/print)))` on device and
|
|
|
1394
1640
|
form failed. A menu block's body is a statement list, so it may open with a
|
|
1395
1641
|
submenu, with one of the thirteen frozen CRUD verbs (`verbs.ts`), with a
|
|
1396
1642
|
catalog-named command, with a `:` scripting directive, or with an absolute
|
|
1397
|
-
command — corpus
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1643
|
+
command — corpus script `forum/amm0/topic-184067-routeros-lsp-for-better-syntax-checking-command-completion-in-editors-like-vscod/post-0014-snippet-02.rsc` is the directive case,
|
|
1644
|
+
`ethernet { :put … }`.
|
|
1645
|
+
|
|
1646
|
+
Issue #348 only stopped the failure. It still read `address { … }` as a command
|
|
1647
|
+
`/ip address` whose operand was the brace, so the body was never segmented.
|
|
1648
|
+
That rule was bounded by what a statement can start with, on the theory that
|
|
1649
|
+
`{1;2}` there is an array. The device says otherwise (#351, CHR 7.24.5 and
|
|
1650
|
+
7.23.8): a brace right after a menu word is a statement block whatever the body
|
|
1651
|
+
holds. `/ip { address { 1;2 } }` is `bad command name 1` at the `1`, and
|
|
1652
|
+
`/ip { address { a;b } }` runs `a` as `add`. The old reading failed the OUTER
|
|
1653
|
+
brace at depth three, and passed `print {1;2}` and `print; zzz` inside the body.
|
|
1654
|
+
|
|
1655
|
+
So a relative run inside a menu block now opens a block of its own when the
|
|
1656
|
+
menu it names is known: the enclosing block's path, read outward from the
|
|
1657
|
+
source, plus the run, named by a shipped structure table
|
|
1658
|
+
(`scope-brace.ts` → `menuBlockPath`). The segmenter, the comment mask, the
|
|
1659
|
+
argument lexer and the group-fault walk all ask it. The body is then read like
|
|
1660
|
+
`/ip/address { … }`: `print {1;2}` there fails at its own `{`, the device's
|
|
1661
|
+
byte, and an unresolvable body statement abstains at every depth.
|
|
1662
|
+
|
|
1663
|
+
It is deliberately not widened into a claim of existence. Menu-scope
|
|
1664
|
+
resolution is schema-dependent — `/ip { zzznotamenu { print } }` is a
|
|
1665
|
+
`syntax error` on device — and offline has no schema, so a catalog MISS
|
|
1666
|
+
abstains rather than being decided either way. That is the catalog's own "a
|
|
1667
|
+
hit is decisive, a miss says nothing" contract. Three spellings keep the older
|
|
1668
|
+
reading because the source alone does not name the menu: a block after a
|
|
1669
|
+
navigation statement (`/ip⏎address { … }`), an abbreviation (`addr {`) and a
|
|
1670
|
+
digit-led menu (`6to4 {`, which the resolver does not read as a path). After a
|
|
1671
|
+
navigation, `/ip⏎address { 1;2 }` still fails at the outer brace and
|
|
1672
|
+
`/ip⏎address { print {1;2} }` still passes; the device answers
|
|
1673
|
+
`bad command name 1` and `expected end of command` at the inner brace.
|
|
1674
|
+
|
|
1675
|
+
### The `=` is optional in ordinary arguments too (#350)
|
|
1676
|
+
|
|
1677
|
+
The #347 rule is the narrow half of a general one: a space can stand in for the
|
|
1678
|
+
`=` of any command argument. `/ip/firewall/layer7-protocol add comment
|
|
1679
|
+
"something" regexp 1.1` lowers to the IL of `comment="something" regexp=1.1`
|
|
1680
|
+
on CHR 7.22.1, 7.23.8 and 7.24.5, and `highlight` marks each gap
|
|
1681
|
+
`syntax-obsolete`, which is RouterOS calling the spelling deprecated. Unlike
|
|
1682
|
+
the four #347 scope names, this one cannot be read offline. The device pairs
|
|
1683
|
+
`comment` with its value because it knows the command has a `comment`
|
|
1684
|
+
argument, and `/system/script add zzznotanarg beep` is rejected. Which words
|
|
1685
|
+
are argument names is per-command schema, which offline does not ship.
|
|
1686
|
+
|
|
1687
|
+
Offline used to publish the worst reading available: `arguments.read: true`,
|
|
1688
|
+
`args: {}`, the operands as positionals. That is an affirmative claim that the
|
|
1689
|
+
list was read and holds no arguments. Now any adjacent pair of a
|
|
1690
|
+
name-shaped bare operand and another operand withdraws the argument reading,
|
|
1691
|
+
as #311 does. The statement stays `resolved`, `command.args` is absent,
|
|
1692
|
+
`arguments.why` names the pair, and transport stays the `unknown` it already
|
|
1693
|
+
was. A **warning** (`possible-spaced-argument`, example 33) is raised only
|
|
1694
|
+
where the second operand cannot be a flag (quoted, numeric, `$`-led). That is
|
|
1695
|
+
because the same bytes are also two flags: `print detail without-paging`
|
|
1696
|
+
lowers to `detail=true;without-paging=true` with no `syntax-obsolete` marker,
|
|
1697
|
+
while `set ether1 name x` pairs. So two bare words abstain silently. `where`
|
|
1698
|
+
ends the scan, and root directives are excluded because their operands are
|
|
1699
|
+
positional by grammar (`:pick abc 0 1` lowers to `counter=abc`). Live
|
|
1700
|
+
`request=child` discovery (#491) is what confirms a name and makes the warning
|
|
1701
|
+
definite.
|
|
1702
|
+
|
|
1703
|
+
The same applies before a script body. `source { :beep }`,
|
|
1704
|
+
`on-event { :beep }` and `down-script { :beep }` lower to their `=` IL, while
|
|
1705
|
+
`comment { :beep }` is a `syntax error`. So the brace exemption accepts the
|
|
1706
|
+
bare spelling, and its name shape gained the `…-script` arguments
|
|
1707
|
+
(`up-script`, `lease-script`) that hard-failed even with the `=`. Which slots
|
|
1708
|
+
take a body is per-menu and per-version schema (`lease-script={…}` parses on
|
|
1709
|
+
7.24.5 and not on 7.22.1). The shape decides the brace's role for every
|
|
1710
|
+
reader, not just the error, and it errs toward a body: an argument the
|
|
1711
|
+
command does not have (`zzz-script={…}`, and `on-zzz={…}` before #350) is a
|
|
1712
|
+
device `syntax error` that offline does not report. That is a missed
|
|
1713
|
+
rejection for the device preflight to catch, never a false reject. On the
|
|
1714
|
+
pinned corpus the widening changed no census.
|
|
1715
|
+
|
|
1716
|
+
### First-segment script-body abbreviations warn instead of rejecting (#525)
|
|
1717
|
+
|
|
1718
|
+
RouterOS takes any unique prefix of an argument name, including the first
|
|
1719
|
+
segment: `down={ :beep }` lowers to `down-script=` on CHR 7.22.1–7.25, as do
|
|
1720
|
+
bodies opening with one bare command word (`down={ beep }`) or after a comment
|
|
1721
|
+
line. Every word is a prefix of `<word>-script`, so without the command's
|
|
1722
|
+
argument list (schema) offline cannot tell `down=` from `comment=`, which the
|
|
1723
|
+
device rejects. The contract: on a non-root command, outside `where`, an
|
|
1724
|
+
unproven `name={` with a script-like body warns
|
|
1725
|
+
`explain/canonicalizer/possible-script-body-abbreviation` (verdict `warn`, the
|
|
1726
|
+
offline gate lets it through) instead of failing with `invalid-command-brace` —
|
|
1727
|
+
glued or with the brace spaced off the `=` (`down= { :beep }` lowers to
|
|
1728
|
+
`down-script=` on CHR 7.25 exactly like the glued spelling, so the spaced
|
|
1729
|
+
owner warns under the same gates). The brace reads as a body for every
|
|
1730
|
+
reader — a `#` inside is a comment, not `invalid-hash` — while proven slots
|
|
1731
|
+
(`down-script=`, `s=`, `on-…`) stay silent and control scopes (`do={…}`) are
|
|
1732
|
+
never guesses. A bare word followed by `;`-separated members is array
|
|
1733
|
+
members, not commands, so `.proplist={name;comment}` keeps its error alongside
|
|
1734
|
+
the other array-content braces (`{1;2}`, `{"x"}`, `{}`), the positional form,
|
|
1735
|
+
and `where`. A root directive's slots (`:put x={…}`, glued or spaced) and a `where` named
|
|
1736
|
+
comparison (`where gateway={…}`, however wrapped) keep their error too: the
|
|
1737
|
+
slot table and the property parser decide there, so the abbreviation guess
|
|
1738
|
+
never shields them (#554). The integration twin pins both device directions.
|
|
1739
|
+
|
|
1740
|
+
The tradeoff is measured: a verdict sweep of the 948-row pinned corpus
|
|
1741
|
+
population against the pre-batch baseline reports zero verdict or diagnostic
|
|
1742
|
+
changes, so nothing in the corpus pays for the abstention. The one deliberate
|
|
1743
|
+
over-warn — `comment={ :beep }` warns though the device rejects it — has no
|
|
1744
|
+
corpus occurrence.
|
|
1408
1745
|
|
|
1409
1746
|
### `'` is not a RouterOS token (#355)
|
|
1410
1747
|
|
|
@@ -1443,11 +1780,14 @@ Occurrences BEFORE the defect stand, which is the lab's X1 rule.
|
|
|
1443
1780
|
|
|
1444
1781
|
**This was a false ACCEPT, which the corpus join cannot see.** The false-reject
|
|
1445
1782
|
measurement scores one direction only, so the analyzer passing all four forms
|
|
1446
|
-
with zero diagnostics never appeared in it. Over the pinned
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
all (still 1, the extra `}` of
|
|
1450
|
-
|
|
1783
|
+
with zero diagnostics never appeared in it. Over the corpus pinned at the time
|
|
1784
|
+
(stable 7.24.2, before the #498 schema-5 repin) the rule fires on 40 scripts and
|
|
1785
|
+
**every one of them is a script the device rejects** — none of the 617 it
|
|
1786
|
+
accepted. It moves the false-reject count not at all (still 1, the extra `}` of
|
|
1787
|
+
corpus script
|
|
1788
|
+
`forum/amm0/topic-167087-can-t-turn-code-into-a-function/post-0005-snippet-02.rsc`)
|
|
1789
|
+
while taking the caught count from 62 to 93. On the #498 sessions both figures
|
|
1790
|
+
hold on every channel: 1 false reject among 573 accepts, 93 caught.
|
|
1451
1791
|
|
|
1452
1792
|
The token census moves, and only in the direction the fail-closed symbol stop
|
|
1453
1793
|
predicts: 68 fewer `variable-*` tokens and their 306 bytes returning to
|
|
@@ -1524,12 +1864,12 @@ Grounded, not transcribed, for the same reason the escape set above is: the
|
|
|
1524
1864
|
[manual's operator list](https://manual.mikrotik.com/docs/developer-guides/scripting/index.md#operators)
|
|
1525
1865
|
is a lower bound. `bun run explain:probe:operators` sweeps the manual's list
|
|
1526
1866
|
**plus** plausible non-operators (`not`, `..`, `xor`, `mod`, `is`, `div`, …)
|
|
1527
|
-
**plus** every IL head the corpus census saw
|
|
1528
|
-
long-term and 7.
|
|
1529
|
-
|
|
1530
|
-
scored on the same firmware (#342)
|
|
1531
|
-
not a channel claim but the only live evidence
|
|
1532
|
-
on a pre-7.23 branch. Re-cut the fixture with
|
|
1867
|
+
**plus** every IL head the corpus census saw. It runs on CHR 7.24.5 stable,
|
|
1868
|
+
7.23.8 long-term, 7.25rc2 testing and 7.26beta1 development, the builds the
|
|
1869
|
+
pinned corpus records a channel session for, so the operator axis and the token
|
|
1870
|
+
partition are scored on the same firmware (#342, #498). It also runs on 7.21.5
|
|
1871
|
+
as an **age anchor**, which is not a channel claim but the only live evidence
|
|
1872
|
+
that the surface behaves the same on a pre-7.23 branch. Re-cut the fixture with
|
|
1533
1873
|
`bun run explain:operator-slice`; the table
|
|
1534
1874
|
below is generated from `src/explain/operators.ts` by
|
|
1535
1875
|
`bun run explain:operator-readme` and gated by
|
|
@@ -1632,13 +1972,13 @@ not concat at all** — `1.` lexes as a *variable name* and the row comes back a
|
|
|
1632
1972
|
juxtaposition, `( $1. 2)`. A rule that claims every `.` byte for the operator
|
|
1633
1973
|
emits a span the device does not have.
|
|
1634
1974
|
|
|
1635
|
-
Everything in the sweep is identical across all
|
|
1636
|
-
row: `:put ({2;1} > {1;2;3})` evaluates to `true` on 7.24.
|
|
1637
|
-
errors on both long-term builds. **Array comparison is not fixed
|
|
1638
|
-
long-term** — 7.23.
|
|
1639
|
-
still errors — so the change rode the 7.24 line and was not
|
|
1640
|
-
errors also differ: 7.23.
|
|
1641
|
-
7.21.5 says `cannot compare if nothing is more than nothing`, so on 7.21.5 the
|
|
1975
|
+
Everything in the sweep is identical across all five builds except one runtime
|
|
1976
|
+
row: `:put ({2;1} > {1;2;3})` evaluates to `true` on 7.24.5, 7.25rc2 and
|
|
1977
|
+
7.26beta1 and errors on both long-term builds. **Array comparison is not fixed
|
|
1978
|
+
on current long-term** — 7.23.8 is newer than the 7.23.3 this was first
|
|
1979
|
+
measured on and still errors — so the change rode the 7.24 line and was not
|
|
1980
|
+
backported. The two errors also differ: 7.23.8 says `cannot compare if array
|
|
1981
|
+
is more than array`, 7.21.5 says `cannot compare if nothing is more than nothing`, so on 7.21.5 the
|
|
1642
1982
|
operands did not survive to the comparison at all.
|
|
1643
1983
|
Identical is measured, not assumed — the slice diffs verdict, arity, precedence,
|
|
1644
1984
|
associativity, `highlight` run, unary placement and op-axis per version. So the
|
|
@@ -1733,13 +2073,14 @@ And the grounded complement — asked, and refused:
|
|
|
1733
2073
|
|
|
1734
2074
|
<!-- BEGIN GENERATED token-census — regenerate with `bun run explain:token-census:readme` -->
|
|
1735
2075
|
The token census is re-derivable with `bun run explain:token-census` and
|
|
1736
|
-
covers 948 source scripts
|
|
1737
|
-
`
|
|
1738
|
-
|
|
2076
|
+
covers 948 source scripts of corpus snapshot `e12d5d4e58a4`, with 882
|
|
2077
|
+
auxiliary rows (7 `capture-controls`, 875 `wiresage-conformance`) excluded.
|
|
2078
|
+
The figures below are generated from `test/fixtures/explain/tokens.json` →
|
|
2079
|
+
`corpus` by `bun run explain:token-census:readme` and gated against it by
|
|
1739
2080
|
`bun run explain:token-census:readme:check`; the fixture itself is gated
|
|
1740
2081
|
against a fresh corpus run by `bun run explain:token-census:check`. Of
|
|
1741
|
-
1,426,731 analyzed bytes, 1,
|
|
1742
|
-
403,
|
|
2082
|
+
1,426,731 analyzed bytes, 1,022,786 are classified (71.69%), the remaining
|
|
2083
|
+
403,945 are `unclassified`. The census emits 241,282 tokens (avg 254.5 per
|
|
1743
2084
|
script). Every byte belongs to exactly one token — sorted by `start`, no
|
|
1744
2085
|
gaps, no overlaps, `join(slice) === input` — and the `class` field is the
|
|
1745
2086
|
vocabulary #264 B5 settled. Each B2 fill should move the classified
|
|
@@ -1820,7 +2161,7 @@ what was wrong was the accounting of it. They are re-derived by
|
|
|
1820
2161
|
`bun run explain:bracket-equals:readme:check`.
|
|
1821
2162
|
|
|
1822
2163
|
<!-- BEGIN GENERATED bracket-equals — regenerate with `bun run explain:bracket-equals:readme` -->
|
|
1823
|
-
Measured by `bun run explain:bracket-equals` over the
|
|
2164
|
+
Measured by `bun run explain:bracket-equals` over the population of corpus snapshot `e12d5d4e58a4`, with 882 auxiliary rows (7 `capture-controls`, 875 `wiresage-conformance`) excluded:
|
|
1824
2165
|
**1,079** `=` bytes sit inside `[ … ]`, across
|
|
1825
2166
|
263 of 948 scripts, and **1,067** of them come back
|
|
1826
2167
|
`unclassified` — the operator fill abstains on essentially all of it. No
|
|
@@ -1835,15 +2176,15 @@ genuine comparisons — the device lowers `[find address=$IP]` to
|
|
|
1835
2176
|
fill can take the query half later. For contrast the fill does claim
|
|
1836
2177
|
**1,402** `=` bytes whose innermost opener is `(`.
|
|
1837
2178
|
|
|
1838
|
-
The query reading is the device's, not a guess: on 7.24.
|
|
1839
|
-
137 scripts carrying one have IL with at least as many
|
|
1840
|
-
Of the rest,
|
|
1841
|
-
with no nodes of any kind — and
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
counter-example to the reading. Whole-corpus `(= …)` head nodes
|
|
1845
|
-
count expression comparisons the fill already claims:
|
|
1846
|
-
7.23.
|
|
2179
|
+
The query reading is the device's, not a guess: on 7.24.5, 91 of the
|
|
2180
|
+
137 scripts carrying one have IL with at least as many comparison nodes.
|
|
2181
|
+
Of the rest, 25 are scripts the console refused outright — their readout is
|
|
2182
|
+
prose, with no nodes of any kind — and 21 lowered fewer comparison nodes than the
|
|
2183
|
+
scan counted, typically because a command the capture build could not resolve
|
|
2184
|
+
(`bad command name …`) stopped the device before it reached the query. Neither is
|
|
2185
|
+
a counter-example to the reading. Whole-corpus `(= …)` head nodes per channel
|
|
2186
|
+
session, which also count expression comparisons the fill already claims:
|
|
2187
|
+
7.23.8 1,300, 7.24.5 1,306, 7.25rc2 1,306, 7.26beta1 1,306.
|
|
1847
2188
|
<!-- END GENERATED bracket-equals -->
|
|
1848
2189
|
|
|
1849
2190
|
### Device agreement for the token partition (#264 B4, #263)
|
|
@@ -1893,13 +2234,13 @@ to be filled by assumption.
|
|
|
1893
2234
|
|
|
1894
2235
|
**The device stops classifying at its first `error`, and mostly does not
|
|
1895
2236
|
recover.** Measured over the slice, `error` appears **at most once per script
|
|
1896
|
-
and is always exactly one byte wide**, and on long-term 7.23.
|
|
2237
|
+
and is always exactly one byte wide**, and on long-term 7.23.8 every byte after
|
|
1897
2238
|
it comes back `none`. Most of the slice sits in that tail. A percentage computed
|
|
1898
2239
|
over the whole stream therefore measures the device giving up, not the parser,
|
|
1899
2240
|
which is why the tail is its own outcome and is never folded into agreement. The
|
|
1900
2241
|
rule is not absolute across versions, so the report does not assert it: stable
|
|
1901
|
-
7.24.
|
|
1902
|
-
stop on fewer scripts, and
|
|
2242
|
+
7.24.5, testing 7.25rc2 and development 7.26beta1 classify again after the
|
|
2243
|
+
`error` in one run and stop on fewer scripts, and
|
|
1903
2244
|
`parser-recovered` in the table below is the figure that says by how much —
|
|
1904
2245
|
zero would have been the claim, and it is not zero. Both counts are generated,
|
|
1905
2246
|
not quoted here, so neither can go stale against the fixture.
|
|
@@ -1921,56 +2262,57 @@ a score.
|
|
|
1921
2262
|
The agreement report is re-derivable with
|
|
1922
2263
|
`bun run explain:highlight-agreement` and needs no router and no corpus: it
|
|
1923
2264
|
scores the token partition against
|
|
1924
|
-
`test/fixtures/explain/highlight-streams.slice.json`,
|
|
2265
|
+
`test/fixtures/explain/highlight-streams.slice.json`, 67 stratified scripts of
|
|
1925
2266
|
committed `/console/inspect request=highlight` output. The figures below are
|
|
1926
2267
|
generated from `test/fixtures/explain/highlight-agreement.json` by
|
|
1927
2268
|
`bun run explain:highlight-agreement:readme`, gated against it by
|
|
1928
2269
|
`bun run explain:highlight-agreement:readme:check`, and the fixture itself is
|
|
1929
2270
|
gated against a fresh measurement by
|
|
1930
|
-
`test/unit/explain-highlight-agreement.test.ts`. Of
|
|
1931
|
-
27,
|
|
1932
|
-
those agree (dev 99.99%, holdout 100.00%). Set `comment` aside — it is
|
|
2271
|
+
`test/unit/explain-highlight-agreement.test.ts`. Of 81,143 bytes at 7.24.5,
|
|
2272
|
+
27,538 are bytes **both** sides decided a syntax class for, and 99.99% of
|
|
2273
|
+
those agree (dev 99.99%, holdout 100.00%). Set `comment` aside — it is 63.66%
|
|
1933
2274
|
of that decided region and mostly the corpus's harness-injected `# Source:`
|
|
1934
|
-
banner (#203) — and the remaining
|
|
1935
|
-
classifying at its one-byte `error`:
|
|
1936
|
-
|
|
1937
|
-
itself moves between captures:
|
|
1938
|
-
stop at 7.
|
|
1939
|
-
trend line, not a pass gate** — the slice is a
|
|
1940
|
-
corpus that is 96.8% two authors, so the
|
|
2275
|
+
banner (#203) — and the remaining 10,008 bytes agree 99.98%. The device stops
|
|
2276
|
+
classifying at its one-byte `error`: 34 of 67 scripts carry one, and the
|
|
2277
|
+
38,278 bytes from there on are not a judgment about anything. The oracle
|
|
2278
|
+
itself moves between captures: 34 stop at 7.24.5 and 39 stop at 7.23.8 and 34
|
|
2279
|
+
stop at 7.25rc2 and 34 stop at 7.26beta1, and 25 of the 67 streams differ
|
|
2280
|
+
between them. **This is a trend line, not a pass gate** — the slice is a
|
|
2281
|
+
per-(split, class) quota over a corpus that is 96.8% two authors, so the
|
|
2282
|
+
percentage describes this slice.
|
|
1941
2283
|
|
|
1942
2284
|
| centrs class | projects to | agree | disagree | unprojected | of decided |
|
|
1943
2285
|
| ------------ | ----------- | ----: | -------: | ----------: | ---------: |
|
|
1944
|
-
| `comment` | `comment` |
|
|
1945
|
-
| `variable-local` | `variable-local` | 1,
|
|
2286
|
+
| `comment` | `comment` | 17,530 | 0 | 0 | 100.00% |
|
|
2287
|
+
| `variable-local` | `variable-local` | 1,547 | 0 | 0 | 100.00% |
|
|
1946
2288
|
| `variable-global` | `variable-global` | 582 | 0 | 0 | 100.00% |
|
|
1947
|
-
| `variable-auto` | `variable-auto` |
|
|
1948
|
-
| `variable-parameter` | `variable-parameter` |
|
|
1949
|
-
| `dir` | `dir` | 1,
|
|
1950
|
-
| `cmd` | `cmd` | 2,
|
|
1951
|
-
| `arg` | `arg`, `arg-dot`, `arg-scope` | 2,
|
|
1952
|
-
| `arg-sep` | `syntax-meta` |
|
|
1953
|
-
| `operator` | `syntax-meta` |
|
|
1954
|
-
| `brace` | `syntax-meta` |
|
|
1955
|
-
| `string` | *abstains* | 0 | 0 |
|
|
2289
|
+
| `variable-auto` | `variable-auto` | 93 | 0 | 0 | 100.00% |
|
|
2290
|
+
| `variable-parameter` | `variable-parameter` | 456 | 0 | 0 | 100.00% |
|
|
2291
|
+
| `dir` | `dir` | 1,701 | 0 | 0 | 100.00% |
|
|
2292
|
+
| `cmd` | `cmd` | 2,224 | 0 | 0 | 100.00% |
|
|
2293
|
+
| `arg` | `arg`, `arg-dot`, `arg-scope` | 2,167 | 0 | 0 | 100.00% |
|
|
2294
|
+
| `arg-sep` | `syntax-meta` | 368 | 0 | 0 | 100.00% |
|
|
2295
|
+
| `operator` | `syntax-meta` | 240 | 2 | 0 | 99.17% |
|
|
2296
|
+
| `brace` | `syntax-meta` | 210 | 0 | 0 | 100.00% |
|
|
2297
|
+
| `string` | *abstains* | 0 | 0 | 573 | — |
|
|
1956
2298
|
| `escaped` | `escaped` | 418 | 0 | 0 | 100.00% |
|
|
1957
|
-
| `value` | *abstains* | 0 | 0 |
|
|
1958
|
-
|
|
1959
|
-
| outcome | bytes @7.24.
|
|
1960
|
-
| ------- | ----: | ----: | ----: |
|
|
1961
|
-
| agree — both decided, projection accepts | 27,
|
|
1962
|
-
| disagree — both decided, projection rejects | 2 | 2 | 2 |
|
|
1963
|
-
| unprojected — no declared projection covers the pair |
|
|
1964
|
-
| offline-silent — device decided, centrs abstained | 4,
|
|
1965
|
-
| non-syntax — the device answered something syntax cannot decide |
|
|
1966
|
-
| device-silent — centrs decided, device said `none` | 5,
|
|
1967
|
-
| both-silent | 3,
|
|
1968
|
-
| parser-stopped — at/after the device's `error` byte, and silent from there |
|
|
1969
|
-
| parser-recovered — past that `error`, and the device classified anyway | 96 | 0 | 96 |
|
|
1970
|
-
|
|
1971
|
-
Where the device decided and offline analysis did not (4,
|
|
1972
|
-
the next fill's target list): `syntax-meta` 3,
|
|
1973
|
-
`variable-local`
|
|
2299
|
+
| `value` | *abstains* | 0 | 0 | 135 | — |
|
|
2300
|
+
|
|
2301
|
+
| outcome | bytes @7.24.5 | bytes @7.23.8 | bytes @7.25rc2 | bytes @7.26beta1 |
|
|
2302
|
+
| ------- | ----: | ----: | ----: | ----: |
|
|
2303
|
+
| agree — both decided, projection accepts | 27,536 | 27,173 | 27,536 | 27,536 |
|
|
2304
|
+
| disagree — both decided, projection rejects | 2 | 2 | 2 | 2 |
|
|
2305
|
+
| unprojected — no declared projection covers the pair | 708 | 705 | 708 | 708 |
|
|
2306
|
+
| offline-silent — device decided, centrs abstained | 4,288 | 4,187 | 4,288 | 4,288 |
|
|
2307
|
+
| non-syntax — the device answered something syntax cannot decide | 731 | 662 | 721 | 721 |
|
|
2308
|
+
| device-silent — centrs decided, device said `none` | 5,676 | 5,606 | 5,676 | 5,676 |
|
|
2309
|
+
| both-silent | 3,828 | 3,736 | 3,838 | 3,838 |
|
|
2310
|
+
| parser-stopped — at/after the device's `error` byte, and silent from there | 38,278 | 39,072 | 38,278 | 38,278 |
|
|
2311
|
+
| parser-recovered — past that `error`, and the device classified anyway | 96 | 0 | 96 | 96 |
|
|
2312
|
+
|
|
2313
|
+
Where the device decided and offline analysis did not (4,288 bytes at 7.24.5,
|
|
2314
|
+
the next fill's target list): `syntax-meta` 3,090, `comment` 341,
|
|
2315
|
+
`variable-local` 328, `arg` 230, `escaped` 145, `dir` 65, 89 across the rest.
|
|
1974
2316
|
Most of the `syntax-meta` share is whitespace the device merged into an
|
|
1975
2317
|
adjacent structure run rather than a token centrs missed.
|
|
1976
2318
|
|
|
@@ -1982,14 +2324,14 @@ every measured byte, the post-`error` tail included: where one capture stops
|
|
|
1982
2324
|
parsing and the next does not, that tail IS the version disagreement, which is
|
|
1983
2325
|
why it dominates the category.
|
|
1984
2326
|
|
|
1985
|
-
| applicability @7.24.
|
|
2327
|
+
| applicability @7.24.5 | bytes | leading cells |
|
|
1986
2328
|
| ---------------------------- | ----: | ------------- |
|
|
1987
|
-
| offline-decidable | 32,
|
|
1988
|
-
| schema-dependent |
|
|
1989
|
-
| state-dependent |
|
|
1990
|
-
| version-dependent |
|
|
1991
|
-
| uncategorized |
|
|
1992
|
-
| no-device-answer |
|
|
2329
|
+
| offline-decidable | 32,043 | `comment` → `comment` 17,395, `unclassified` → `syntax-meta` 3,002, `cmd` → `cmd` 2,158 |
|
|
2330
|
+
| schema-dependent | 307 | `arg` → `obj-inactive` 307 |
|
|
2331
|
+
| state-dependent | 94 | `dir` → `obj-inactive` 49, `unclassified` → `obj-dynamic` 20, `cmd` → `obj-inactive` 18 |
|
|
2332
|
+
| version-dependent | 707 | `comment` → `comment` 135, `unclassified` → `obj-inactive` 124, `unclassified` → `syntax-meta` 88 |
|
|
2333
|
+
| uncategorized | 240 | `unclassified` → `obj-inactive` 116, `unclassified` → `variable-undefined` 75, `value` → `variable-undefined` 21 |
|
|
2334
|
+
| no-device-answer | 47,752 | `none` where the captures agree, wherever it falls |
|
|
1993
2335
|
|
|
1994
2336
|
One representative run per cell, so B5 reads a fragment rather than a count.
|
|
1995
2337
|
`runs` is how often that exact fragment occurs; the location is a slice script
|
|
@@ -1998,9 +2340,8 @@ and a byte offset into the stream the device saw.
|
|
|
1998
2340
|
| outcome | cell | fragment | runs | first at |
|
|
1999
2341
|
| ------- | ---- | -------- | ---: | -------- |
|
|
2000
2342
|
| disagree | `operator` → `arg` | `"in"` | 1 | `forum/amm0/topic-169456-having-the-where-filter-in-scripting-signifantly-increases-the-execution-time-an/post-0006-snippet-01.rsc` @615 |
|
|
2001
|
-
| unprojected | `string` → `syntax-meta` | `"\""` |
|
|
2002
|
-
| unprojected | `value` → `syntax-meta` | `"\""` |
|
|
2003
|
-
| unprojected | `string` → `arg` | `"as-value"` | 1 | `forum/amm0/topic-266499-how-to-condense-variables-passed-through-to-function/post-0020-snippet-01.rsc` @279 |
|
|
2343
|
+
| unprojected | `string` → `syntax-meta` | `"\""` | 326 | `eworm/ppp-on-up.rsc` @435 |
|
|
2344
|
+
| unprojected | `value` → `syntax-meta` | `"\""` | 84 | `forum/amm0/topic-153357-using-wifiwave2-to-bridge-two-audience-wirelessly-thoughts-4-address-mode/post-0001-snippet-01.rsc` @1436 |
|
|
2004
2345
|
| unprojected | `string` → `cmd` | `"len"` | 1 | `forum/amm0/topic-155978-zerotier-on-mikrotik-a-rosetta-stone-v7-1-1/post-0001-snippet-01.rsc` @1794 |
|
|
2005
2346
|
| unprojected | `string` → `dir` | `":"` | 1 | `forum/amm0/topic-155978-zerotier-on-mikrotik-a-rosetta-stone-v7-1-1/post-0001-snippet-01.rsc` @1793 |
|
|
2006
2347
|
<!-- END GENERATED highlight-agreement -->
|
|
@@ -2090,9 +2431,10 @@ the 16,526 bytes `arg-sep` claims, one per attribute. Token count rises 214,503
|
|
|
2090
2431
|
→ 231,026 because one run became two. **This is a retag.**
|
|
2091
2432
|
|
|
2092
2433
|
(Those totals are the corpus as it stood for THIS measurement. The classified
|
|
2093
|
-
total is 1,
|
|
2434
|
+
total is 1,022,805 today: #347/#348 later classified 8 further bytes, #355's
|
|
2094
2435
|
fail-closed symbol stop then unclaimed 306 of them — variable bytes sitting
|
|
2095
|
-
after a bare apostrophe, which the device never reaches
|
|
2436
|
+
after a bare apostrophe, which the device never reaches — and #211's unlisted
|
|
2437
|
+
bare-path abstention unclaimed 302 more. The identity being
|
|
2096
2438
|
claimed here is between the two sides of the split, not with any later total —
|
|
2097
2439
|
restating it against today's figure would credit this retag with a movement a
|
|
2098
2440
|
different change made. The generated block above carries the current numbers.)
|
|
@@ -2180,7 +2522,7 @@ bytes came from a fill that already held it — `string` gives up 17,672 and
|
|
|
2180
2522
|
last decimal**, 1,023,405 of 1,426,731 (71.73076073906013%), so in byte terms
|
|
2181
2523
|
this is a retag even though it took a new walk to make — again, the identity is
|
|
2182
2524
|
between this change's own before and after, and the corpus total has since moved
|
|
2183
|
-
to 1,
|
|
2525
|
+
to 1,022,805 / 241,442 tokens under #347/#348, #355 and #211. Only the token count
|
|
2184
2526
|
moves, 231,026 → 241,663: the 7,408 new `escaped` tokens plus the 3,229
|
|
2185
2527
|
fragments they leave behind when they split a run they sit inside (`string`
|
|
2186
2528
|
+2,643, `value` +586). That second number is well under one per escape because
|
|
@@ -2219,22 +2561,317 @@ last byte. The `$` sigil and the `[` / `]` of a substitution stay `string` on
|
|
|
2219
2561
|
the same ground — `structure.subcommands` already publishes the resolution, and
|
|
2220
2562
|
its `innerSpan` locates the brackets around it.
|
|
2221
2563
|
|
|
2222
|
-
###
|
|
2223
|
-
|
|
2224
|
-
`
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2564
|
+
### Live probe foundation (#491)
|
|
2565
|
+
|
|
2566
|
+
`centrs explain <router> '<input>'` (or `--quickchr <name>` / `--host <host>`)
|
|
2567
|
+
adds read-only device evidence over REST or native API. Library callers use
|
|
2568
|
+
`explainLiveEnvelope({ input, targetInput, via, ... }, options)` from the root
|
|
2569
|
+
package; the browser-safe `@tikoci/centrs/explain` entry remains offline.
|
|
2570
|
+
Connection/authentication settings use the existing resolver and provider
|
|
2571
|
+
precedence. A pinned unsupported transport is refused. No fan-out or protocol
|
|
2572
|
+
fallback is performed. `AbortSignal` cancels library probes.
|
|
2573
|
+
An explicitly supplied connection flag such as `--via`, `--password` or
|
|
2574
|
+
`--timeout` without `<router>`, `--host` or `--quickchr` is a usage error before
|
|
2575
|
+
input is read. Connection defaults from environment and config remain inert in
|
|
2576
|
+
offline mode.
|
|
2577
|
+
Text output summarizes live observations; `--json` retains full parse and
|
|
2578
|
+
per-byte highlight evidence.
|
|
2579
|
+
|
|
2580
|
+
`data.canonical`, structure, symbols, values and optional tokens retain the
|
|
2581
|
+
offline result. Live observations are additional facts:
|
|
2582
|
+
|
|
2583
|
+
- `data.live.environment` records the observed version, architecture and board.
|
|
2584
|
+
- `data.live.parse` carries the printed parse result, rejection classification
|
|
2585
|
+
and authoritative line/byte-column when reported. Only an escaped
|
|
2586
|
+
`:put [:parse "…"]` wrapper runs; the analyzed input, substitutions and
|
|
2587
|
+
deferred bodies are never executed.
|
|
2588
|
+
- `data.live.highlight` retains raw device classes. Additional `data.spans`
|
|
2589
|
+
cite its evidence and use centrs classes: `dir`, `cmd`, `arg`, `escaped`,
|
|
2590
|
+
comments and variable classes; device state, syntax-meta, error/obsolete and
|
|
2591
|
+
unknown classes map to `unclassified`. Error/obsolete markers produce separate
|
|
2592
|
+
diagnostics. Offline and live spans can overlap; distinguish them by `ev`.
|
|
2593
|
+
- `data.live.arguments[]` contains `path`, `names`, `statementIds` and `ev` for
|
|
2594
|
+
command-level `request=child`. Repeated paths share a probe. Names confirm what
|
|
2595
|
+
the device exposes; they do not rewrite offline arguments, transport eligibility
|
|
2596
|
+
or lexical bindings. The highlight obsolete marker distinguishes `name x`
|
|
2597
|
+
from the two flags `detail without-paging` (example 48).
|
|
2598
|
+
|
|
2599
|
+
Every live evidence entry identifies its probe, observation time, known version
|
|
2600
|
+
and acquisition outcome. Input probes include SHA-256 and normalization;
|
|
2601
|
+
`coverage` identifies the submitted byte range for a usable response, not proof
|
|
2602
|
+
that every byte is syntactically valid. Parse uses original UTF-8 input;
|
|
2603
|
+
highlight uses the existing one-SUB-per-non-ASCII-byte normalization. Its facts
|
|
2604
|
+
therefore describe that explicit variant; both spaces align with `positionMap`.
|
|
2605
|
+
Parse can stop at its first error; highlight `none` remains unclassified.
|
|
2606
|
+
|
|
2607
|
+
Probe outcomes distinguish `ok`, `empty`, `unsupported`, `skipped`, `timeout`,
|
|
2608
|
+
`auth-error`, `transport-error`, `interrupted` and `invalid-response`. A parse
|
|
2609
|
+
rejection is an acquired response (`ok`) plus a diagnostic, not a transport
|
|
2610
|
+
failure. A failed/skipped probe adds a warning diagnostic; acquired offline and
|
|
2611
|
+
live facts remain available. `ok:true` means analysis produced a result, never
|
|
2612
|
+
that all probes succeeded. Resolver/usage failures and transport/authentication/interruption failures before
|
|
2613
|
+
any input probe yields a result remain `ok:false`.
|
|
2614
|
+
`meta.operation.mode` is `live`; no execution-validation verdict is invented:
|
|
2615
|
+
`meta.validation` is `{ enabled: false }`, offline and live, because explain
|
|
2616
|
+
is not a gate (#535). `runtimeAcceptance` always remains `not-proven`.
|
|
2617
|
+
|
|
2618
|
+
Work is bounded by the resolved per-request timeout, 32 distinct command paths,
|
|
2619
|
+
32,767 bytes for highlight and 65,536 bytes for parse. Larger probes are skipped
|
|
2620
|
+
with their reason; no silent prefix is submitted and `input.truncated` stays
|
|
2621
|
+
false. A transport/authentication failure stops later requests. Scripting-path
|
|
2622
|
+
discovery is conservatively skipped before 7.21.4 or on unknown versions. No
|
|
2623
|
+
command-level completion, user-expression evaluation or related
|
|
2624
|
+
configuration-table collection is part of this slice. `--schema` and
|
|
2625
|
+
`--complete` add the probes in the next section, and the value-type probes
|
|
2626
|
+
after it run on every live call.
|
|
2627
|
+
|
|
2628
|
+
Committed controls are
|
|
2629
|
+
[`live-probes.stable.json`](../../test/fixtures/explain/live-probes.stable.json)
|
|
2630
|
+
(7.24.5) and
|
|
2631
|
+
[`live-probes.long-term.json`](../../test/fixtures/explain/live-probes.long-term.json)
|
|
2632
|
+
(7.23.8), both CHR reporting `x86_64`, with package manifests, input hashes,
|
|
2633
|
+
observation times, raw parse/highlight output and projected child names. Each
|
|
2634
|
+
fixture records its capture command. `test/unit/explain-live.test.ts` replays
|
|
2635
|
+
the parse classifier and source-aligned highlighter against these controls;
|
|
2636
|
+
`test/integration/explain-live.test.ts` repeats the probes on a real CHR and
|
|
2637
|
+
checks that the analyzed identity-setting command did not run.
|
|
2638
|
+
|
|
2639
|
+
### Live describe and completion (#492)
|
|
2640
|
+
|
|
2641
|
+
`--schema` and `--complete` work on the statement at the cursor. The cursor
|
|
2642
|
+
defaults to the end of the input. `--cursor <byte>` is a UTF-8 byte offset,
|
|
2643
|
+
which is also the analyzed and device byte offset. Library callers may pass
|
|
2644
|
+
`cursor: { line, character }` (0-based, UTF-16) instead. A cursor inside a
|
|
2645
|
+
multi-byte character moves back to its first byte. A cursor outside the input
|
|
2646
|
+
is [`usage/invalid-cursor`](../../docs/errors/usage/invalid-cursor.md), raised
|
|
2647
|
+
before connecting. `--cursor` or `--full` without a facet is
|
|
2648
|
+
`usage/conflicting-flags`. Offline, both facets still return the live-target
|
|
2649
|
+
tip and enumerate nothing.
|
|
2650
|
+
|
|
2651
|
+
**Probe shapes.** Grounded on CHR 7.12.2, 7.20.8, 7.23.8 and 7.24.5 over REST
|
|
2652
|
+
and native API:
|
|
2653
|
+
|
|
2654
|
+
- `child` names what exists at a path and resolves unique abbreviations
|
|
2655
|
+
(`ip,addr` answers as `address`). It is safe on every build tried.
|
|
2656
|
+
- `syntax` is sent only for an **argument** path (`ip,address,set,address`,
|
|
2657
|
+
`put,message`). That shape answers in about 25 ms on every build, scripting
|
|
2658
|
+
keywords included.
|
|
2659
|
+
- `completion` is sent only with `input=`, the text before the cursor. It is
|
|
2660
|
+
safe at every position tried, including scripting keywords and `do={`
|
|
2661
|
+
bodies on 7.20.8.
|
|
2662
|
+
- Never sent: `syntax` or `completion` with `path=` set to a **command**
|
|
2663
|
+
(`ip,address,set`). On 7.12.2 and 7.20.8 REST stops answering, and the
|
|
2664
|
+
requests queued behind it stall, for about 30 s. Native API closes the
|
|
2665
|
+
connection. 7.23.8 and later answer normally, but the rule is
|
|
2666
|
+
version-independent (`commands/api/AGENTS.md`). restraml's crawlers never
|
|
2667
|
+
meet this, because they send `syntax` to `arg` nodes only.
|
|
2668
|
+
|
|
2669
|
+
Every probe is an `evidence[]` entry with `facet: "schema" | "complete"`, its
|
|
2670
|
+
path, version and outcome. Facts cite it by `ev`. A facet probe that answers
|
|
2671
|
+
`empty` is a result, so it adds no warning diagnostic. Other failed outcomes
|
|
2672
|
+
warn as in the foundation, and a transport or authentication failure stops
|
|
2673
|
+
later probes.
|
|
2674
|
+
|
|
2675
|
+
**`data.schema`** describes one path: the resolved statement, or an absolute
|
|
2676
|
+
slash path as typed (`/ip/addr`). The `child` self row decides `node`:
|
|
2677
|
+
|
|
2678
|
+
- `menu`: `verbs[]` and `menus[]`. Each child is expanded by one more `child`
|
|
2679
|
+
probe into `arguments` (verb) or `verbs`/`menus` (submenu) counts.
|
|
2680
|
+
- `command`: `arguments[]`, each with its argument-level `syntax` rows. A
|
|
2681
|
+
flag's blank row is dropped. For `print`, `proplist` holds the output-field
|
|
2682
|
+
names from `print proplist=` (`source: "print-proplist"`). A singleton
|
|
2683
|
+
settings menu offers none there, so `get value-name=` answers instead
|
|
2684
|
+
(`source: "get-value-name"`; `/ip/dns`, `/system/identity`). These are names
|
|
2685
|
+
a print can show, **not print rows**.
|
|
2686
|
+
- `argument`: its `syntax` rows.
|
|
2687
|
+
- `unknown`: the device reports nothing there, or the statement at the cursor
|
|
2688
|
+
is neither resolved nor an absolute path (`reason`).
|
|
2689
|
+
|
|
2690
|
+
Syntax rows are kept verbatim (`symbol`, RouterOS `symbol-type` as `kind`,
|
|
2691
|
+
`text`, `nested` as `depth`). They are declared help, such as
|
|
2692
|
+
`A.B.C.D (IP address)` or `input | forward | output` beside `string value`.
|
|
2693
|
+
They are not the `schemaType` value fact, which normalizes the same rows per
|
|
2694
|
+
value ([Live value types](#live-value-types-225)).
|
|
2695
|
+
|
|
2696
|
+
**Smart sizing.** By default each list shows up to 64 names, and expansion
|
|
2697
|
+
runs only when the listed children fit in 16 probes (a menu's children, or a
|
|
2698
|
+
command's arguments for their `syntax`). Otherwise names are listed without
|
|
2699
|
+
expansion. `counts` always holds the full sizes, `truncated: true`, `next[]`
|
|
2700
|
+
names up to three narrower inputs, and the envelope carries
|
|
2701
|
+
`tip/explain-schema-truncated`. On CHR 7.24.5, `/ip` (2 verbs, 29 menus) and
|
|
2702
|
+
`/ip/address/print` (21 arguments) truncate; `/ip/address` (12 verbs) and
|
|
2703
|
+
`/ip/firewall/filter` (16) do not. `--full` raises the limits to 1024 names and
|
|
2704
|
+
128 probes. That is the safety ceiling, not unlimited.
|
|
2705
|
+
|
|
2706
|
+
**`data.completion`** reports `cursor`, `at` (original line/character),
|
|
2707
|
+
`replace` (the bytes a candidate replaces, ending at the cursor), `prefix`,
|
|
2708
|
+
`context`, `candidates[]`, `count` and `truncated`. Candidates are capped like
|
|
2709
|
+
names. They are the device's own: one input completion of the text before the
|
|
2710
|
+
cursor, keeping shown, non-syntax rows. The row `offset` sets `replace`. The
|
|
2711
|
+
navigation punctuation (`/`, `:`, `..`) offered everywhere is left out. So a
|
|
2712
|
+
relative path, a block, a `where` clause or an unclosed `[` completes exactly
|
|
2713
|
+
as on the console: `/ip { :put 1; address add comm` → `comment`, and
|
|
2714
|
+
`/ip address⏎` → that menu's verbs. Where the device offers nothing, neither
|
|
2715
|
+
does explain: `do={ add comm` runs at root, so `add` is unknown there.
|
|
2716
|
+
|
|
2717
|
+
The `context` comes from the device highlight of the same text. When the
|
|
2718
|
+
cursor is not at the end, that is one extra highlight probe whose `coverage`
|
|
2719
|
+
says so.
|
|
2720
|
+
|
|
2721
|
+
- `path`: in a `dir`/`cmd` word, after a menu, or at the start of a line.
|
|
2722
|
+
Candidates are `menu` or `verb` by row style. A word the device cannot
|
|
2723
|
+
resolve there is `obj-inactive`, and the token before it gives its role:
|
|
2724
|
+
`/ip/address re` → `remove`, `reset`, and `:lo` → `local`, `lock`, `log`.
|
|
2725
|
+
- `argument-name`: in an `arg` word, or after a command, flag or value.
|
|
2726
|
+
`match: "exact"` marks a complete name. `takesValue` is whether the same
|
|
2727
|
+
answer offers `=` at the cursor: `comment` does, and the flag `detail` does
|
|
2728
|
+
not.
|
|
2729
|
+
- `variable`: a `where` clause, whose property names RouterOS scopes as
|
|
2730
|
+
variables (`where comm` → `comment`).
|
|
2731
|
+
- `value`: after `name=`. Candidates may include configuration state, such as
|
|
2732
|
+
interface names. They are never a closed set:
|
|
2733
|
+
`chain=` offers `forward`, `input` and `output`, but custom chains are legal.
|
|
2734
|
+
For a command argument, `syntax` carries its declared type text, so a
|
|
2735
|
+
free-form `comment=` returns no candidates and `string value`. A `where`
|
|
2736
|
+
value (`disabled=` → `no`, `yes`) has no argument path, so no `syntax` is
|
|
2737
|
+
read.
|
|
2738
|
+
- `unsupported` with a `reason`: strings, the obsolete spaced `name value`
|
|
2739
|
+
spelling, and other tokens the device does not class as a path, name or
|
|
2740
|
+
value.
|
|
2741
|
+
|
|
2742
|
+
Every candidate is an observation of the completion probe its `ev` cites.
|
|
2743
|
+
The declared side is `data.schema`: `child` names and argument `syntax`. So
|
|
2744
|
+
completion carries no second provenance label, and nothing in it is a closed
|
|
2745
|
+
set. `path` names the command at the
|
|
2746
|
+
cursor when it is known. It comes from the offline statement or `[…]`
|
|
2747
|
+
substitution when that resolved, otherwise from the device's `dir`/`cmd` runs.
|
|
2748
|
+
The command is needed only for a value's `syntax`. When it is not visible (a
|
|
2749
|
+
relative path after `;`), the candidates are unaffected and `syntax` is
|
|
2750
|
+
omitted.
|
|
2751
|
+
|
|
2752
|
+
The verdict still judges the input as written, and the two probes agree on
|
|
2753
|
+
it. `/ip/address add comm` is rejected by `:parse` (`expected end of command`
|
|
2754
|
+
at byte 16), and the highlight marks the same byte `error`. So the result
|
|
2755
|
+
fails even while its candidates are published. An assist loop that only wants
|
|
2756
|
+
candidates passes `--fail-on never`.
|
|
2757
|
+
|
|
2758
|
+
Committed controls are
|
|
2759
|
+
[`live-facets.stable.json`](../../test/fixtures/explain/live-facets.stable.json)
|
|
2760
|
+
(7.24.5) and
|
|
2761
|
+
[`live-facets.long-term.json`](../../test/fixtures/explain/live-facets.long-term.json)
|
|
2762
|
+
(7.23.8). They hold every recorded call and its raw answer, plus the projected
|
|
2763
|
+
result. `test/unit/explain-live-facets.test.ts` replays them.
|
|
2764
|
+
`test/integration/explain-live-facets.test.ts` asserts the examples on both
|
|
2765
|
+
transports and regenerates the captures when `CENTRS_EXPLAIN_FACETS_CAPTURE`
|
|
2766
|
+
names an output file.
|
|
2767
|
+
|
|
2768
|
+
Not covered here: MCP (#223), related print rows and topic checks (#493),
|
|
2769
|
+
and per-target caching of describe results (open 3).
|
|
2770
|
+
|
|
2771
|
+
### Live value types (#225)
|
|
2772
|
+
|
|
2773
|
+
Every live call fills the two live homes on `data.values.occurrences[].facts`
|
|
2774
|
+
that offline leaves empty. Neither changes the verdict or
|
|
2775
|
+
`runtimeAcceptance`, and neither becomes a diagnostic. Both probes are
|
|
2776
|
+
`evidence[]` entries with `facet: "types"` and `basis: "derived"`; an `empty`
|
|
2777
|
+
answer is a result, not a warning. They run after `--schema`/`--complete`.
|
|
2778
|
+
|
|
2779
|
+
**`observedType: { value, rendered, ev }`** is the type the device's parser
|
|
2780
|
+
visibly converted a bare literal to. `:parse` prints a string unquoted, so
|
|
2781
|
+
`:local a 123` and `:local b "123"` both render `value=123`. A rendering is
|
|
2782
|
+
therefore evidence only when it CHANGED the literal:
|
|
2783
|
+
|
|
2784
|
+
| Literal | Device rendering | `observedType` |
|
|
2785
|
+
| ------- | ---------------- | -------------- |
|
|
2786
|
+
| `2.2`, `2.2/8` | `2.0.0.2`, `2.0.0.2/8` | `ip`, `ip-prefix` |
|
|
2787
|
+
| `1:0:0::1` | `1::1` | `ip6` |
|
|
2788
|
+
| `0x10`, `0123`, `99999999999999999999` | `16`, `123`, `9223372036854775807` | `num` |
|
|
2789
|
+
| `yes`, `no` | `true`, `false` | `bool` |
|
|
2790
|
+
| `200ms`, `1d`, `1:2`, `10000w` | `00:00:00.200`, `1d00:00:00`, `01:02:00`, `10000w00:00:00` | `time` |
|
|
2791
|
+
| `123`, `1.1.1.1`, `true`, `00:00:02`, `"2.2"` | unchanged | absent |
|
|
2792
|
+
| `100000w`, `abc`, `1e5`, `300.1.1.1` | unchanged | absent |
|
|
2793
|
+
| member `{abc}`, `{-1}` | `$abc`, `(- 1)` | absent |
|
|
2794
|
+
|
|
2795
|
+
The literal is not looked up in the IL of the analyzed input, where a quoted
|
|
2796
|
+
`"x;y=1"` prints exactly like two arguments. Each bare literal is re-parsed
|
|
2797
|
+
alone: `:local vN <lit>` for a scripting operand, `:local vN {<lit>}` for an
|
|
2798
|
+
array member, `;`-joined into one escaped `:put [:parse "…"]` per context.
|
|
2799
|
+
`:put <lit>` converts exactly as `:local` does. Only literals of the bytes
|
|
2800
|
+
`A-Z a-z 0-9 . _ : / -` are sent, so no literal can leave its slot, and a
|
|
2801
|
+
quoted value never qualifies. One hard-error member (`{*1}`) rejects a whole
|
|
2802
|
+
batch, so the two contexts are separate probes and a rejected batch is
|
|
2803
|
+
`invalid-response` with the device's message; its literals abstain. A batch is
|
|
2804
|
+
at most 256 literals, otherwise `skipped`.
|
|
2805
|
+
|
|
2806
|
+
A scripting directive's named argument converts exactly as an operand
|
|
2807
|
+
(`:for i from=0x10` → `from=16`, `:put message=yes` → `message=true`), so it
|
|
2808
|
+
joins the operand batch. A directive's NAME slot is a variable and never
|
|
2809
|
+
converts: `:local yes 2` renders `name=$yes`, `:for yes from=1` renders
|
|
2810
|
+
`counter=$yes` and `:local name=no` renders `name=$no`. So a value the symbol
|
|
2811
|
+
view claims as a name, or a directive's `name=`/`counter=` argument, is never
|
|
2812
|
+
probed.
|
|
2813
|
+
|
|
2814
|
+
Menu-command arguments are never observed. Their own parser converts them
|
|
2815
|
+
and prints a per-argument encoding (`src-address=!1.1.1.1` →
|
|
2816
|
+
`true;1.1.1.1`, `address=2.2` → `2.0.0.2;`), and a quoted value converts only
|
|
2817
|
+
at run time (`address="2.2"` stays `2.2`). That slot is `schemaType`'s. `op`
|
|
2818
|
+
has no producer: `(>[:return 1])` and `(>{"a"=1})` parse identically, and only
|
|
2819
|
+
`:typeof` separates them.
|
|
2820
|
+
|
|
2821
|
+
**`schemaType: { values, list?, declared, ev }`** is the argument's declared
|
|
2822
|
+
type. For each `name=value` of a resolved menu command, the name is resolved
|
|
2823
|
+
against that command's live `child` names, exactly or as a unique prefix
|
|
2824
|
+
(`dist=` → `distance`). One `syntax` probe per distinct argument path
|
|
2825
|
+
(`ip,route,add,distance`) reads the declaration; at most 32 per call, later
|
|
2826
|
+
ones `skipped`. An ambiguous or unknown name sends no probe. `syntax` is never
|
|
2827
|
+
sent to a command path (see [Live describe and completion](#live-describe-and-completion-492)).
|
|
2828
|
+
|
|
2829
|
+
The rows are a small grammar (`[!]Range`, `Address[%Interface][@VRF]`), so
|
|
2830
|
+
`values` lists the LEAF types the declaration mentions, not alternatives of
|
|
2831
|
+
one value. They use the shape-hint names so the two axes compare directly,
|
|
2832
|
+
plus three schema-only terms:
|
|
2833
|
+
|
|
2834
|
+
| Device leaf | Term |
|
|
2835
|
+
| ----------- | ---- |
|
|
2836
|
+
| `(integer number)`, `(decimal number)`, `integer value`, `0..65535` | `num` |
|
|
2837
|
+
| `(time interval)`, `time interval` | `time` |
|
|
2838
|
+
| `(IP address)`, `IPv4 address`, `A.B.C.D` | `ip` |
|
|
2839
|
+
| `IPv6 address` | `ip6` |
|
|
2840
|
+
| `(IP prefix)`, `(IPv6 prefix)` | `ip-prefix`, `ip6-prefix` |
|
|
2841
|
+
| `(IP address range)` | `ip-range` |
|
|
2842
|
+
| `(MAC address)` | `mac` |
|
|
2843
|
+
| `string value…`, `hexadecimal string value…`, `script`, `filename` | `str` |
|
|
2844
|
+
| a listed word set (`input \| forward \| output`, `ether1 \| lo \| ...`) | `enum` |
|
|
2845
|
+
|
|
2846
|
+
`enum` is never a closed set: the device lists the current configuration
|
|
2847
|
+
names (`ether1 | lo`) exactly like keywords, and ends either kind with `...`
|
|
2848
|
+
when the list is long (`ftp | h323 | … | ...`). `list: true` is present when
|
|
2849
|
+
the declaration spells a repetition (`[,Type*]`, `(max 15 times)`); its
|
|
2850
|
+
absence is not a claim — `servers` (`Address[@VRF]`) splits on a comma
|
|
2851
|
+
without saying so. A keyword's help row (`tcp` → `transmission control
|
|
2852
|
+
protocol`) is not a leaf, and a blank placeholder beside a real leaf is
|
|
2853
|
+
skipped. The fact is ABSENT when the declaration is blank (every flag and
|
|
2854
|
+
boolean: `disabled`, `copy-from`) or when any leaf is `see documentation`
|
|
2855
|
+
(`/ipv6/address add address`). `declared` is the device's top-level text,
|
|
2856
|
+
verbatim (`Address[@VRF]`, `Num`).
|
|
2857
|
+
|
|
2858
|
+
The vocabulary also names `bool` and `name` for a second schema source: the
|
|
2859
|
+
CLI Reference's `Type` cells (`bool`, `iface_enum`, `ipAddr`, `macAddr`,
|
|
2860
|
+
`ubit (…)`), which declare the booleans and object references inspect leaves
|
|
2861
|
+
blank or prints as words. A CLI Reference producer would fill the same fact,
|
|
2862
|
+
told apart by its `ev`; delivering those tables is decision 3 (#501).
|
|
2863
|
+
|
|
2864
|
+
Grounded on CHR 7.24.5 and 7.23.8 over REST and native API, identical on both:
|
|
2865
|
+
a 40-command crawl (863 arguments) set the leaf table (571 project, 282 are
|
|
2866
|
+
blank, 7 cite `see documentation` and 3 list no names), and the conversion
|
|
2867
|
+
matrix is recorded raw in
|
|
2868
|
+
[`live-types.stable.json`](../../test/fixtures/explain/live-types.stable.json)
|
|
2869
|
+
and [`live-types.long-term.json`](../../test/fixtures/explain/live-types.long-term.json).
|
|
2870
|
+
`test/unit/explain-live-types.test.ts` replays them and pins the matrix
|
|
2871
|
+
reading; `test/integration/explain-live-types.test.ts` asserts examples 52–53
|
|
2872
|
+
on both transports and that every executed script is a `:parse` wrapper and
|
|
2873
|
+
every `syntax` probe names a discovered argument. It regenerates the captures
|
|
2874
|
+
when `CENTRS_EXPLAIN_TYPES_CAPTURE` names an output file.
|
|
2238
2875
|
|
|
2239
2876
|
## MCP and library surfaces
|
|
2240
2877
|
|
|
@@ -2450,31 +3087,65 @@ config genre in question). Since phase 0 the corpus has grown to 948 with a
|
|
|
2450
3087
|
export-banner share to 4.6%; **that stratum is not part of any phase-0 figure
|
|
2451
3088
|
above**. Current whole-corpus offline censuses include its source text.
|
|
2452
3089
|
|
|
2453
|
-
The pinned snapshot
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
7.
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
`
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
3090
|
+
The pinned snapshot (schema 5, tikoci/lsp-routeros-ts#40, plus the
|
|
3091
|
+
`wiresage-conformance` collection from tikoci/lsp-routeros-ts#42) carries one
|
|
3092
|
+
recorded capture session per release channel — long-term `7.23.8`, stable `7.24.5`,
|
|
3093
|
+
testing `7.25rc2` and development `7.26beta1`. Each is a `:parse` run and a
|
|
3094
|
+
`highlight` run over all 948 population scripts, bound to the exact submitted
|
|
3095
|
+
bytes and one device environment with every package installed. Two legacy
|
|
3096
|
+
`:parse` captures (7.20.8 and 7.22.1: 913 scripts, unknown input bytes, base
|
|
3097
|
+
package only) and a legacy 7.9.2 highlight summary remain as history, never as
|
|
3098
|
+
exact-input evidence.
|
|
3099
|
+
|
|
3100
|
+
Every census reads the corpus through `scripts/corpus-db.ts`, in three ways
|
|
3101
|
+
(#498):
|
|
3102
|
+
|
|
3103
|
+
- **Population.** `v_corpus_population` leaves out the upstream auxiliary
|
|
3104
|
+
collections: 7 `capture-controls` rows and 875 `wiresage-conformance`
|
|
3105
|
+
synthetic grammar cases. An observation is in the population by `script_id`,
|
|
3106
|
+
not by path prefix, so a collection added upstream cannot leak into a census
|
|
3107
|
+
(it did, briefly, when #42 added 875 rows to every run).
|
|
3108
|
+
- **Sessions.** Comparable figures name their channel run ids
|
|
3109
|
+
(`capture_channels`), not a coverage label.
|
|
3110
|
+
- **Verdict.** The device verdict is the upstream `response_class`. Only
|
|
3111
|
+
`il-no-recognized-diagnostic` is an accept, so IL carrying an embedded
|
|
3112
|
+
`bad command name …` is a rejection. The shipped `:parse` reader
|
|
3113
|
+
(`classifyParseResult`) agrees with it on all 5,640 channel and legacy
|
|
3114
|
+
observations.
|
|
3115
|
+
|
|
3116
|
+
The population text is byte-identical to the previous snapshot, so the
|
|
3117
|
+
text-derived reports are unchanged by construction: the token and value
|
|
3118
|
+
censuses, `explain:arg-reach` and the browser-consumer proof. The IL- and
|
|
3119
|
+
highlight-derived figures moved.
|
|
3120
|
+
|
|
3121
|
+
**The channels do not agree with each other, and that is the finding.** Scored
|
|
3122
|
+
over all 948 scripts:
|
|
3123
|
+
|
|
3124
|
+
| oracle | long-term 7.23.8 ↔ stable 7.24.5 | stable ↔ testing 7.25rc2 | testing ↔ development 7.26beta1 |
|
|
3125
|
+
| ------ | -------------------------------: | -----------------------: | ------------------------------: |
|
|
3126
|
+
| `:parse` readout | 95 scripts differ | 12 | 13 |
|
|
3127
|
+
| `:parse` response class | 5 | 0 | 0 |
|
|
3128
|
+
| `highlight` stream | 24 | 1 | 0 |
|
|
3129
|
+
|
|
3130
|
+
The newer channels are nearly the same build for these purposes; long-term is
|
|
3131
|
+
the one that moves. So an operator-axis or token-partition figure is a claim
|
|
3132
|
+
*about a channel*, not about RouterOS. That is why the census reports its
|
|
3133
|
+
builds as separate columns and states its session set, rather than collapsing
|
|
3134
|
+
them to one number. Long-term is also the channel a real device most likely
|
|
3135
|
+
runs.
|
|
3136
|
+
|
|
3137
|
+
**The `wiresage-conformance` cases are read separately, never as population.**
|
|
3138
|
+
`conformanceCases` returns the 875 third-party grammar probes with their
|
|
3139
|
+
upstream label, and `parseObservations(…, { collection })` returns their
|
|
3140
|
+
device answers. `bun run explain:conformance` scores the execute gate's offline
|
|
3141
|
+
answer (`fail` refuses) against `:parse` on every channel. It lists false
|
|
3142
|
+
rejects, missed rejections (split by device class: `plain-diagnostic` is
|
|
3143
|
+
syntax, while `embedded-diagnostic` is mostly a menu or parameter name, which
|
|
3144
|
+
needs the #501 argument lists), cases where the channels disagree, cases
|
|
3145
|
+
with no device answer (`unscorable`, never read as a rejection), and the
|
|
3146
|
+
upstream-label disagreements that #507 needs. The lists are committed in
|
|
3147
|
+
`test/fixtures/explain/conformance.json` and gated in CI, so a fix for any
|
|
3148
|
+
of #504–#506 shows the cases it moved.
|
|
2478
3149
|
|
|
2479
3150
|
The corpus itself is not in this repo and is not moving here — `lsp-routeros-ts`
|
|
2480
3151
|
owns producing snapshots, centrs owns which snapshot it measures against.
|
|
@@ -2544,10 +3215,27 @@ to mention. `/system/reboot` no longer moves the context, so a statement after
|
|
|
2544
3215
|
it resolves where it actually is; `verbsplit` and `pathresolve` agree, which
|
|
2545
3216
|
they would otherwise now do *confidently* in opposite directions. What is left
|
|
2546
3217
|
is the honest residue of both floors — `/terminal/cuu` is a real no-argument
|
|
2547
|
-
command MikroTik publishes nowhere
|
|
2548
|
-
fixture rather than assumed closed. The contradiction that #202 would have
|
|
3218
|
+
command MikroTik publishes nowhere. The contradiction that #202 would have
|
|
2549
3219
|
rendered is gone.
|
|
2550
3220
|
|
|
3221
|
+
**#211 makes that residue abstain (R14).** A verb-free absolute path that
|
|
3222
|
+
neither table knows used to still move the context, so `/terminal/cuu` then
|
|
3223
|
+
`print` resolved to an invented `/terminal/cuu/print`. On CHR 7.24.5 `:parse`
|
|
3224
|
+
reads `/terminal/cuu` as a command and that `print` as `bad command name print`
|
|
3225
|
+
at the root. `/ip fire conn` has the same shape and is navigation to
|
|
3226
|
+
`/ip/firewall/connection`, because RouterOS accepts abbreviated menus. A table
|
|
3227
|
+
miss therefore cannot decide either way. The statement now reads as R13's
|
|
3228
|
+
relative abstention does: not navigation, flagged `maybeNav`, with the
|
|
3229
|
+
statements after it `unknown` until a known absolute navigation recovers the
|
|
3230
|
+
context ([example 34](examples.md#34-an-unlisted-bare-path-does-not-become-a-menu)).
|
|
3231
|
+
Over the pinned corpus, 14 of 18,650 statement readings lose a path, in 6
|
|
3232
|
+
scripts. Every one was an invented path: `/ip/fire/conn/remove`,
|
|
3233
|
+
`/ip/firewall/layer7/add`, the v6 `/routing/bgp/network/add`, and
|
|
3234
|
+
`/terminal/style/error/put`. No `explain` verdict changes. Four `containsWrite`
|
|
3235
|
+
verdicts move from `true` to `unknown`, because their write was proven only at
|
|
3236
|
+
an invented path. Rule P, which would read the tables as a per-menu closed
|
|
3237
|
+
world and expand abbreviations, is not adopted (#372).
|
|
3238
|
+
|
|
2551
3239
|
**The command axis also outranks the punctuation guess.** `splitRun`'s last
|
|
2552
3240
|
rule reads the first space-separated token as the verb, which lands on the
|
|
2553
3241
|
operand whenever the command itself was written slash-joined:
|
|
@@ -2677,8 +3365,8 @@ The offline capability can advance from `coded` to `verified` when:
|
|
|
2677
3365
|
- **Reachable evidence and explicit residue:** changed RouterOS semantic claims
|
|
2678
3366
|
have committed CHR-grounded controls and a reproducible capture/replay path.
|
|
2679
3367
|
Every remaining issue has a stated scope and dependency; #225 distinguishes
|
|
2680
|
-
offline value work from live types, #272 owns evidence reachability, and
|
|
2681
|
-
unlisted
|
|
3368
|
+
offline value work from live types, #272 owns evidence reachability, and an
|
|
3369
|
+
unlisted bare path abstains (#211) until #372 prices a closed-world reading.
|
|
2682
3370
|
|
|
2683
3371
|
Retain the existing checks: `bun run lint`, `bun run test`, `bun run build`, and
|
|
2684
3372
|
`bun run lint:ci`, plus the affected CHR grounding/integration procedure from
|
|
@@ -2688,9 +3376,10 @@ test framework is part of this baseline.
|
|
|
2688
3376
|
|
|
2689
3377
|
### Current and following capabilities
|
|
2690
3378
|
|
|
2691
|
-
- **
|
|
2692
|
-
completion/child/syntax
|
|
2693
|
-
|
|
3379
|
+
- **Done — live probes (#236, phase 2):** highlight + `:parse` (#491),
|
|
3380
|
+
completion/child/syntax (#492) and live value types (#225) over
|
|
3381
|
+
rest-api/native-api, `CHR-passed` on stable and long-term.
|
|
3382
|
+
`runtimeAcceptance` stays `not-proven`.
|
|
2694
3383
|
- **MCP alignment (#223):** the deliberate flat-to-rich response migration,
|
|
2695
3384
|
coordinated with rosetta. Independent of the offline token vocabulary.
|
|
2696
3385
|
- **Facet polish (phase 3):** live sizing, `--full`, completion/schema ergonomics.
|
|
@@ -2761,8 +3450,10 @@ round):
|
|
|
2761
3450
|
|
|
2762
3451
|
Still open (implementation opens — none block ratification):
|
|
2763
3452
|
|
|
2764
|
-
1. Final flag names
|
|
2765
|
-
|
|
3453
|
+
1. ~~Final flag names and smart-sizing thresholds.~~ Decided by #492:
|
|
3454
|
+
`--schema`, `--complete`, `--cursor <byte>` and `--full`; 64 names and 16
|
|
3455
|
+
expansion probes by default, and 1024 / 128 with `--full`
|
|
3456
|
+
([Live describe and completion](#live-describe-and-completion-492)).
|
|
2766
3457
|
2. The mapping table from RouterOS highlight classes to **colors**, and how
|
|
2767
3458
|
unknown/new upstream classes degrade. Phase 0 pinned the raw inputs (Q13's
|
|
2768
3459
|
per-occurrence highlight-class corpus; the observed 19 classes on 7.23.2 plus
|