@tikoci/centrs 0.1.9-next.0 → 0.1.9-next.23
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 +177 -40
- package/GLOSSARY.txt +32 -0
- package/README.md +12 -8
- package/SECURITY.md +11 -3
- package/commands/api/README.md +39 -15
- package/commands/api/examples.md +44 -6
- package/commands/execute/README.md +108 -14
- package/commands/execute/examples.md +137 -3
- package/commands/explain/README.md +430 -129
- package/commands/explain/examples.md +260 -1
- package/commands/mcp/README.md +11 -0
- package/commands/mcp/examples.md +6 -0
- package/commands/retrieve/README.md +30 -13
- package/commands/retrieve/examples.md +10 -6
- package/commands/transfer/examples.md +9 -0
- package/docs/CLI.md +8 -8
- package/docs/CONSTITUTION.md +29 -2
- package/docs/MATRIX.md +26 -13
- package/docs/errors/cdb/empty-group.md +19 -3
- package/docs/errors/cdb/empty-selection.md +9 -3
- package/docs/errors/cmr/caller-policy-not-enforced.md +46 -0
- package/docs/errors/cmr/empty-selection.md +17 -3
- package/docs/errors/cmr/rule-selects-every-device.md +32 -0
- package/docs/errors/cmr/selection-includes-controller.md +27 -0
- package/docs/errors/usage/flag-after-separator.md +34 -0
- package/package.json +10 -5
- package/src/api-fanout.ts +11 -1
- package/src/api.ts +205 -65
- package/src/btest.ts +7 -2
- package/src/cli/api.ts +30 -16
- package/src/cli/common.ts +180 -1
- package/src/cli/devices.ts +0 -2
- package/src/cli/discover.ts +0 -1
- package/src/cli/execute.ts +44 -20
- package/src/cli/explain.ts +2 -0
- package/src/cli/mcp.ts +0 -1
- package/src/cli/retrieve.ts +40 -18
- package/src/cli/settings.ts +0 -3
- package/src/cli/terminal.ts +0 -2
- package/src/cli/transfer.ts +10 -8
- package/src/cli/version.ts +87 -0
- package/src/cli.ts +5 -3
- package/src/core/cmr-fleet.ts +486 -0
- package/src/core/cmr.ts +78 -0
- package/src/core/envelope.ts +32 -3
- package/src/core/error-catalog.ts +25 -3
- package/src/core/execute-command.ts +144 -19
- package/src/core/fanout.ts +130 -6
- package/src/errors.ts +2 -1
- package/src/execute-cmr.ts +95 -78
- package/src/execute-fanout.ts +11 -1
- package/src/execute.ts +105 -23
- package/src/explain/args.ts +69 -8
- 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 +927 -0
- package/src/explain/highlight-projection.ts +10 -8
- package/src/explain/operators.ts +20 -19
- 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 +103 -6
- package/src/explain/segment.ts +41 -3
- package/src/explain/spaced-args.ts +116 -0
- package/src/explain/symbols.ts +17 -1
- package/src/explain/transport.ts +13 -0
- package/src/explain/verbsplit.ts +14 -188
- package/src/explain/write.ts +8 -1
- package/src/explain.ts +378 -18
- package/src/index.ts +18 -1
- package/src/mcp/safety.ts +11 -0
- package/src/mcp/tools.ts +4 -1
- package/src/offline-gate.ts +30 -9
- package/src/protocols/btest-session.ts +30 -10
- package/src/protocols/mac-telnet-console.ts +16 -0
- package/src/resolver/cdb.ts +13 -1
- package/src/resolver/index.ts +4 -0
- package/src/resolver/selection.ts +37 -1
- package/src/resolver/target.ts +96 -8
- package/src/retrieve-fanout.ts +4 -1
- package/src/retrieve-follow.ts +29 -33
- package/src/retrieve-sample.ts +14 -8
- package/src/retrieve-stream.ts +38 -6
- package/src/retrieve.ts +73 -59
- package/src/transfer-fanout.ts +8 -1
- package/src/transfer.ts +15 -19
|
@@ -392,7 +392,8 @@ bare menu path. The positional grammar stays **target-first like every other
|
|
|
392
392
|
router-taking command** (amended in PR review — the shared resolver/selection
|
|
393
393
|
helpers assume it): one positional means offline and it *is* the input; two
|
|
394
394
|
positionals mean live with the router first; `--` is accepted before the
|
|
395
|
-
input
|
|
395
|
+
input, and one of explain's own long flags after it is refused as
|
|
396
|
+
`usage/flag-after-separator` (#459); **`--file` replaces the input positional in either form**, so with it
|
|
396
397
|
every positional is a target. Adding a
|
|
397
398
|
router therefore never reinterprets a previously valid offline invocation.
|
|
398
399
|
|
|
@@ -459,6 +460,7 @@ Standard envelope (constitution: result envelope); `data` sketch:
|
|
|
459
460
|
"statementCount": 1,
|
|
460
461
|
"statements": [
|
|
461
462
|
{
|
|
463
|
+
"id": "s0",
|
|
462
464
|
"span": { "start": 0, "end": 51 },
|
|
463
465
|
"resolution": "resolved",
|
|
464
466
|
"kind": "command",
|
|
@@ -510,6 +512,36 @@ Standard envelope (constitution: result envelope); `data` sketch:
|
|
|
510
512
|
classification, and evidence id. This is where multi-statement scripts and
|
|
511
513
|
partial recovery live; a single top-level transport object cannot represent
|
|
512
514
|
them.
|
|
515
|
+
- **Statement ownership is syntactic (#497).** Every published statement has
|
|
516
|
+
an `id` (`s0`, `s1`, …) in discovery order, stable within one result. A nested
|
|
517
|
+
scope-body statement's `parent` names its owning statement; absence means
|
|
518
|
+
top level. Each substitution's `owner` names its containing statement,
|
|
519
|
+
including nested brackets. These links come from the resolver's traversal,
|
|
520
|
+
so widened Unicode spans do not turn siblings into ancestors. IDs are not
|
|
521
|
+
persistent across edits.
|
|
522
|
+
|
|
523
|
+
The existing `blocks[]` inventory still lists outer scope bodies only.
|
|
524
|
+
A block's optional `owner` names the statement carrying it, and a statement's
|
|
525
|
+
optional `block` indexes its innermost **published** enclosing block. A nested
|
|
526
|
+
body can therefore share its ancestor's block index. Bare/menu containers
|
|
527
|
+
add no synthetic statement or block nodes; the tree links only statements
|
|
528
|
+
the existing analysis exposes. Existing lists, order, spans and readings stay
|
|
529
|
+
unchanged. A widened span remains a coordinate limitation, not a second
|
|
530
|
+
parentage rule.
|
|
531
|
+
|
|
532
|
+
Stored/deferred `source=`, `on-event=` and callback bodies remain opaque:
|
|
533
|
+
their statements are not added to this tree. Creation versus explicit
|
|
534
|
+
stored-script execution is grounded by
|
|
535
|
+
`test/fixtures/explain/statement-ownership.json` and replayed by
|
|
536
|
+
`test/integration/explain-structure.test.ts`. Ownership asserts no lexical
|
|
537
|
+
inheritance, immediate execution, or shared user/policy environment;
|
|
538
|
+
context-sensitive binding/value semantics remain #507's responsibility.
|
|
539
|
+
|
|
540
|
+
`test/unit/explain-structure.test.ts` independently checks ancestry against
|
|
541
|
+
span containment where spans recover exact source text, with explicit
|
|
542
|
+
widened-span controls. After `bun run corpus:fetch`, run
|
|
543
|
+
`CENTRS_CHECK_CORPUS=1 bun test test/unit/explain-structure.test.ts` for the
|
|
544
|
+
same contract over the pinned study population; the corpus CI job runs it.
|
|
513
545
|
- **Every derived fact keeps provenance via `evidence[]`** — facts reference
|
|
514
546
|
a stable evidence id (`ev`), and the evidence entry carries source
|
|
515
547
|
(`canonicalizer` vs `live-inspect`), probe, basis (direct response vs
|
|
@@ -906,7 +938,16 @@ and its phase is named below.
|
|
|
906
938
|
top-level brace value in any other command argument emits
|
|
907
939
|
`explain/canonicalizer/invalid-command-brace` at the `{`; parentheses or a
|
|
908
940
|
`[…]` substitution restore the expression context where the same literal is
|
|
909
|
-
legal.
|
|
941
|
+
legal. So does a top-level `where` clause (#502): `print where interface in
|
|
942
|
+
{"ether1";"ether2"}` is a read, and its array is read member by member like
|
|
943
|
+
its `(…)` twin. The exception is the right side of `<name>=`, `!=` or `==`,
|
|
944
|
+
spaced or not, which the device reads with that property's own parser
|
|
945
|
+
(`expected interface value`), so `where comment={1;2}` keeps its error. The
|
|
946
|
+
device does that only for a property the menu knows (`where foo-bar={…}`
|
|
947
|
+
parses). Offline has no property list, so it treats every bare name as one.
|
|
948
|
+
The same comparison inside `( … )` or a `[ … ]` command
|
|
949
|
+
(`[find where interface={"x"}]`, `where (interface={"x"})`) is caught by the
|
|
950
|
+
group walk (#509, below) and reported with the same code. An empty member is a
|
|
910
951
|
syntax error (`{;}`, `{;1}`, `{1;;2}`, `(1,)`) and withdraws the enclosing
|
|
911
952
|
`array` shape with it; a single trailing separator is legal in the brace form
|
|
912
953
|
only (`{1;}` and `{1;2;}` parse, `{1;2,}` and `{2,}` do not).
|
|
@@ -946,15 +987,16 @@ and its phase is named below.
|
|
|
946
987
|
|
|
947
988
|
<!-- BEGIN GENERATED value-census — regenerate with `bun run explain:value-census:readme` -->
|
|
948
989
|
The corpus census is re-derivable with `bun run explain:value-census` and
|
|
949
|
-
covers 948 source scripts
|
|
950
|
-
`
|
|
951
|
-
|
|
990
|
+
covers 948 source scripts of corpus snapshot `e12d5d4e58a4`, with 882
|
|
991
|
+
auxiliary rows (7 `capture-controls`, 875 `wiresage-conformance`) excluded.
|
|
992
|
+
The figures below are generated from `test/fixtures/explain/values.json` →
|
|
993
|
+
`corpus` by `bun run explain:value-census:readme` and gated against it by
|
|
952
994
|
`bun run explain:value-census:readme:check`; the fixture itself is gated
|
|
953
995
|
against a fresh corpus run by `bun run explain:value-census:check`. Of the
|
|
954
|
-
13,
|
|
996
|
+
13,152 emitted values in a statement the strict argument lexer ALSO read, 0
|
|
955
997
|
disagree with it on half-open byte span or decoded text, while the
|
|
956
998
|
prefix-safe scan retains a further 6,538 values across 568 statements whose
|
|
957
|
-
strict REST reading abstains. Of 19,
|
|
999
|
+
strict REST reading abstains. Of 19,690 emitted occurrences, 5,636 are array
|
|
958
1000
|
members (529 keyed, 1,147 nested inside another member) and 808 are arrays;
|
|
959
1001
|
the corpus holds no source-literal `id` example. The three structural
|
|
960
1002
|
counters — spans addressing bytes outside their own source, members naming a
|
|
@@ -981,7 +1023,8 @@ comment, a literal hash value, and a hard error at the exact byte.
|
|
|
981
1023
|
| 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. |
|
|
982
1024
|
| 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. |
|
|
983
1025
|
| Immediate line start after `\` + newline | `comment`; the pending statement survives | Mask it in both argument views; the continuation-reach rules remain H5/#215. |
|
|
984
|
-
| 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. |
|
|
1026
|
+
| 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. |
|
|
1027
|
+
| 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). |
|
|
985
1028
|
| 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. |
|
|
986
1029
|
| 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. |
|
|
987
1030
|
| In an attribute or bare value (`comment=#test`, `comment=a#b`, `:global y #test`) | literal value (`none` highlight class) | Keep it as content. |
|
|
@@ -1019,6 +1062,14 @@ JSON-like transcript, and two shell-shaped snippets. No RouterOS command reading
|
|
|
1019
1062
|
moved; these diagnostics say those bytes are not valid RouterOS, not that the
|
|
1020
1063
|
foreign snippets were expected to parse as RouterOS.
|
|
1021
1064
|
|
|
1065
|
+
Corpus re-measurement for #503: one script of 947 moves. It is a pasted console
|
|
1066
|
+
session (`forum/amm0/topic-264287-…/post-0005-snippet-01.rsc`, which opens with
|
|
1067
|
+
`[admin@MikroTik] > {`), and it moves from `fail` to `warn`. Its `invalid-hash` sat on a column-0
|
|
1068
|
+
`# ... more code` line inside the brace after `>`, which the device reads as
|
|
1069
|
+
an array (`:local z (1 > {⏎:local a 1⏎# c⏎})` parses). The device rejects the
|
|
1070
|
+
script earlier, at the prompt's `@`, which is #512. Every census gate except
|
|
1071
|
+
the token census is unchanged; that one gains the two comment spans.
|
|
1072
|
+
|
|
1022
1073
|
The default text renderer surfaces comment spans in a `comments:` section; JSON
|
|
1023
1074
|
continues to carry the same ranges as `spans[]` entries with `class: "comment"`.
|
|
1024
1075
|
|
|
@@ -1073,10 +1124,17 @@ navigated, so the context stays as unknown as it already was.
|
|
|
1073
1124
|
`:parse` `expected message value`, grounded stable+testing #247),
|
|
1074
1125
|
`bad-sigil`,
|
|
1075
1126
|
`invalid-hash`, `invalid-apostrophe` (#355, below),
|
|
1076
|
-
`missing-statement-separator` (#311, below)
|
|
1077
|
-
|
|
1127
|
+
`missing-statement-separator` (#311, below), the four group faults
|
|
1128
|
+
`empty-group`, `misplaced-separator`, `missing-operand` and
|
|
1129
|
+
`invalid-at-sign` (#505, #511, #512, below), and the four token faults
|
|
1130
|
+
`glued-token`, `invalid-variable`, `bad-string-variable` and
|
|
1131
|
+
`invalid-name` (#506, #518, below). Seventeen classes the device itself
|
|
1132
|
+
rejects.
|
|
1078
1133
|
- `warning` — `over-depth`, because it is centrs's own resource bound and says
|
|
1079
|
-
nothing about whether the input is legal;
|
|
1134
|
+
nothing about whether the input is legal; `possible-spaced-argument`
|
|
1135
|
+
(#350), a spelling RouterOS accepts and marks obsolete; `discarded-close`
|
|
1136
|
+
(#504), a script RouterOS accepts and stops reading early, which the
|
|
1137
|
+
execute gate refuses by code; and an `ambiguous`/`unknown`
|
|
1080
1138
|
resolution, never an error, so the default `--fail-on error` cannot fail a
|
|
1081
1139
|
document whose only sin is being unreadable without a schema — which is most
|
|
1082
1140
|
of RouterOS scripting.
|
|
@@ -1181,6 +1239,151 @@ it. The corpus count is unchanged at **0** for the same reason it always was
|
|
|
1181
1239
|
the rule is an unterminated string or an unbalanced delimiter, because neither
|
|
1182
1240
|
walk can find a boundary to resume from.
|
|
1183
1241
|
|
|
1242
|
+
### Faults inside groups (#505, #509, #511, #512)
|
|
1243
|
+
|
|
1244
|
+
Four missed-rejection families share one walk, `src/explain/group-faults.ts`
|
|
1245
|
+
(evidence pass `findGroupFaults`). It reads the comment-masked document with
|
|
1246
|
+
the frame roles every other walker uses: `[` opens a statement, `(` an
|
|
1247
|
+
expression, and `{` a body when `braceStartsStatements` says so or the
|
|
1248
|
+
directive takes a program in that slot (`:execute {…}`, `:time {…}`). Each
|
|
1249
|
+
rule is a `:parse` answer on CHR 7.24.5 and 7.23.8, identical on both, and
|
|
1250
|
+
`test/fixtures/explain/group-faults.json` pins all 186 rows (100 rejections,
|
|
1251
|
+
86 controls). `test/integration/explain-group-faults.test.ts` holds the
|
|
1252
|
+
device to that fixture, and `test/unit/explain-group-faults.test.ts` holds
|
|
1253
|
+
offline to it.
|
|
1254
|
+
|
|
1255
|
+
| Code | Device rule | Rejected | Accepted |
|
|
1256
|
+
| ---- | ----------- | -------- | -------- |
|
|
1257
|
+
| `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 {}` |
|
|
1258
|
+
| `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})` |
|
|
1259
|
+
| `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.)` |
|
|
1260
|
+
| `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` |
|
|
1261
|
+
|
|
1262
|
+
`invalid-command-brace` gains the #509 positions: a brace after `<name>=`
|
|
1263
|
+
inside `( … )` or a `[ … ]` command whose statement says `where`. The top level
|
|
1264
|
+
and scope bodies stay with `args.ts` (#502), and a `do={` body is never read as
|
|
1265
|
+
a comparison.
|
|
1266
|
+
|
|
1267
|
+
**Prefix operators are the device's set, not the operator table's arity.**
|
|
1268
|
+
`-`, `!`, `>`, `~` and `any` take one operand after them, so `(1 + -2)` and
|
|
1269
|
+
`(-> 1)` (`(- (> 1))`) parse. Every other operator needs a left operand. At
|
|
1270
|
+
operand position `and`, `or`, `in`, `.` and `/` are variable names
|
|
1271
|
+
(`(and 1)` is `( $and 1)`). `1.`, `1/`, `a_b`, `1:2` and `1.2.3.4/24` are
|
|
1272
|
+
single words, while `-` is always an operator (`(a-b)` is `(- $a $b)`).
|
|
1273
|
+
|
|
1274
|
+
**A query is not an expression.** Inside `where …` and `find …`, the device
|
|
1275
|
+
reads the value after `<name>=` with the property's parser. So
|
|
1276
|
+
`where (comment=+)`, `where (comment=a-)`, `where (comment=@)` and
|
|
1277
|
+
`where (.id=*1)` parse, though each is an error in a plain group. A group in a
|
|
1278
|
+
query gets only the structural checks (empty, `;`, newline) and #509.
|
|
1279
|
+
|
|
1280
|
+
**A brace offline cannot place is opaque, not an array.** A `{` after a
|
|
1281
|
+
relative menu word is a menu body (`/ip { route { } }` parses empty), and a
|
|
1282
|
+
brace in a command argument is `args.ts`'s. Neither is checked here. The
|
|
1283
|
+
array rules apply where the role is grounded: a directive slot in the
|
|
1284
|
+
`brace-slots.ts` array table, and an array nested in an array or an expression.
|
|
1285
|
+
|
|
1286
|
+
**Three readings the review round grounded (PR #516).** A `\` continuation
|
|
1287
|
+
runs on across blank lines and line-leading comment lines, as H5 does
|
|
1288
|
+
elsewhere, so `(1 +\⏎# c⏎2)` and `{1\⏎⏎;2}` parse, while a blank line after a
|
|
1289
|
+
comment line ends it (`(1 +\⏎# c⏎⏎2)` is rejected at the blank line). A
|
|
1290
|
+
directive's operand is a name, so `:local where (comment={})` parses and
|
|
1291
|
+
`:local where (1 +)` is checked like any expression. A bare `local`/`global`
|
|
1292
|
+
is the same directive at the root (`local where (comment={})` is
|
|
1293
|
+
`/localname=$where`), while a bare `set` or `find` may be a menu command and
|
|
1294
|
+
stays unread. And a named element takes
|
|
1295
|
+
a trailing `,` (`{a=1,}` is `value=a`, `{a=1,;b=2}` is `value=a;b=2`), while
|
|
1296
|
+
`{a=,1}`, `{a=1,,2}` and a positional `{1,}` are rejected. A faulted group's
|
|
1297
|
+
descendants are opaque, since the device stops at the fault.
|
|
1298
|
+
|
|
1299
|
+
**Spans.** Each fault spans the token it is about: the operator, the
|
|
1300
|
+
separator, the `@`, or the whole empty group. The device's column is the same
|
|
1301
|
+
byte for every rule except an empty group and a dangling operator, where it is
|
|
1302
|
+
the byte after.
|
|
1303
|
+
|
|
1304
|
+
**Measured.** Corpus `e12d5d4e58a4`: 70 of 948 scripts gain an error and all
|
|
1305
|
+
70 are device rejections on every channel. 42 of those errors sit on the exact
|
|
1306
|
+
byte the 7.24.5 diagnostic names, and the other 28 come after an earlier device
|
|
1307
|
+
error in pasted non-RouterOS text. None precedes the device's error. 41
|
|
1308
|
+
verdicts move to `fail` (40 from `warn`, one from `pass`), so offline now fails
|
|
1309
|
+
133 of the 374 device-rejected scripts, up from 92, and false rejects stay at 1.
|
|
1310
|
+
`invalid-at-sign` is mostly pasted console prompts. On the conformance cases,
|
|
1311
|
+
22 `plain-diagnostic` missed rejections move to `fail`, with no new false
|
|
1312
|
+
reject.
|
|
1313
|
+
|
|
1314
|
+
Not covered here: a `$( … )` group, a group under a `find` head
|
|
1315
|
+
(`[find (1 +)]`, read as a query), a `@` in a later head word
|
|
1316
|
+
(`/ip address@ print`), operand rules for array elements other than `,`, a
|
|
1317
|
+
group in a query beyond its structure, and adjacency without whitespace
|
|
1318
|
+
(`"x"~a`, `}else=`), which is the next section's.
|
|
1319
|
+
|
|
1320
|
+
### Token faults and the discarded tail (#504, #506, #518)
|
|
1321
|
+
|
|
1322
|
+
Three more missed-rejection families, and one reading that was wrong in the
|
|
1323
|
+
other direction. The glued-token, variable and name rules ride the same walk as
|
|
1324
|
+
the group faults (`src/explain/group-faults.ts`), so they share its statement,
|
|
1325
|
+
query and brace roles. The `$` inside a string is checked by the escape walk
|
|
1326
|
+
(`src/explain/quoted-string.ts`), and the stray close by the segmenter
|
|
1327
|
+
(`src/explain/segment.ts` → `discardFrom`). Every rule is a `:parse` answer on
|
|
1328
|
+
CHR 7.24.5 and 7.23.8, identical on both, and
|
|
1329
|
+
`test/fixtures/explain/token-faults.json` pins all 145 rows (80 rejections and
|
|
1330
|
+
65 acceptances, with the acceptances' IL). `test/integration/explain-token-faults.test.ts`
|
|
1331
|
+
holds the device to that fixture, and `test/unit/explain-token-faults.test.ts`
|
|
1332
|
+
holds offline to it.
|
|
1333
|
+
|
|
1334
|
+
| Code | Device rule | Rejected | Accepted |
|
|
1335
|
+
| ---- | ----------- | -------- | -------- |
|
|
1336
|
+
| `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`) |
|
|
1337
|
+
| `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"` |
|
|
1338
|
+
| `invalid-variable` | Outside a string, `$` takes a name (`[A-Za-z0-9]+`) or `$"…"` | `:put $`, `$_`, `$-`, `$.`, a `$` before a space | `$v1`, `$"a b"` |
|
|
1339
|
+
| `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]"` |
|
|
1340
|
+
| `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` |
|
|
1341
|
+
|
|
1342
|
+
**The stray close was the honest-reading bug.** Offline used to fail
|
|
1343
|
+
`name=a}` with `unbalanced-close`, and the device accepts it. But the device
|
|
1344
|
+
does not just ignore the `}`: it stops reading there. `}⏎:put 1` lowers to `/`,
|
|
1345
|
+
and a write after the brace never runs. So `explainCommand` now analyzes only
|
|
1346
|
+
the prefix the device reads and adds one `warning` that spans the dropped
|
|
1347
|
+
bytes. The token partition still covers the whole input, with the tail
|
|
1348
|
+
`unclassified`. A `)` in that position, and any stray close inside an open
|
|
1349
|
+
group, are rejections and stay `unbalanced-close` errors. The execute gate
|
|
1350
|
+
refuses `discarded-close` by code, the one warning it refuses
|
|
1351
|
+
(`src/offline-gate.ts`; `commands/execute/examples.md` V9).
|
|
1352
|
+
|
|
1353
|
+
**Where a property's parser decides, the glued-token rule abstains.** In a
|
|
1354
|
+
command argument the device hands the value to the property's parser, which can
|
|
1355
|
+
take a glued part after a prefix or separator: `dst-address-list=!"my list"`
|
|
1356
|
+
and `dst-port=80,"443"` parse, while `name=!"a"` and `name=-"a"` do not. So
|
|
1357
|
+
outside a `:` directive, a bare word followed by a `"`, `$` or bracket is a
|
|
1358
|
+
fault only after an alphanumeric byte. After a closed part no property was found
|
|
1359
|
+
that accepts anything (`"80",443`, `$p-90`, `$v%ether1`, `"ether"1` are all
|
|
1360
|
+
rejected), so that half of the rule applies everywhere outside a query.
|
|
1361
|
+
|
|
1362
|
+
**Spans.** Each fault spans its byte, which is the device's byte too. For the
|
|
1363
|
+
two `$` rules the span is the `$` and the byte after it, and the device names
|
|
1364
|
+
that second byte.
|
|
1365
|
+
|
|
1366
|
+
**Measured.** Corpus `e12d5d4e58a4`: 23 scripts move to `fail` (18 from
|
|
1367
|
+
`warn`, 5 from `pass`), and 29 already-failing scripts gain a new code. All are
|
|
1368
|
+
device rejections on every channel. 9 of their first new errors sit on the
|
|
1369
|
+
7.24.5 diagnostic's byte, 45 come after it, and none precedes it. Offline now
|
|
1370
|
+
fails 154 of the device-rejected scripts, up from 133. The one corpus false
|
|
1371
|
+
reject (a stray `}`) is gone, so false rejects are 0. Three scripts move from
|
|
1372
|
+
`fail` to `warn`: their `unbalanced-close` became `discarded-close`, and two of
|
|
1373
|
+
them are device rejections for an earlier reason offline does not see. On the
|
|
1374
|
+
conformance cases, false rejects go from 3 to 0 and missed rejections from 278
|
|
1375
|
+
to 151.
|
|
1376
|
+
|
|
1377
|
+
**Not covered here.** `"a$"`: the device reads `$"` in a string as a quoted
|
|
1378
|
+
variable name, so the next `"` does not close the string. Every scanner in
|
|
1379
|
+
`explain` closes it there, so both `$` rules and the glued-token rule abstain
|
|
1380
|
+
around a string that ends in `$"` (2 of 948 corpus scripts use the form).
|
|
1381
|
+
`$$`, `$(…)`, `$[…]` and `${…}` outside a string, where the device's byte is
|
|
1382
|
+
not the `$`. A `\` continuation inside a glued token (`"a"\⏎"b"`). The head
|
|
1383
|
+
word of a statement (`$v"a"`, `{…}:put 2`). `:local a=b`, which 7.24.5 reads as
|
|
1384
|
+
a named argument and 7.23.8 as a syntax error. And a name in `/local` or
|
|
1385
|
+
`:local name=a_b`.
|
|
1386
|
+
|
|
1184
1387
|
### The token stream a rule reads (#316)
|
|
1185
1388
|
|
|
1186
1389
|
`lexArguments` is all-or-nothing by design: its consumer renders a runnable REST
|
|
@@ -1393,7 +1596,8 @@ print } }` is `(evl (evl (evl /ip/address/print)))` on device and
|
|
|
1393
1596
|
form failed. A menu block's body is a statement list, so it may open with a
|
|
1394
1597
|
submenu, with one of the thirteen frozen CRUD verbs (`verbs.ts`), with a
|
|
1395
1598
|
catalog-named command, with a `:` scripting directive, or with an absolute
|
|
1396
|
-
command — corpus
|
|
1599
|
+
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,
|
|
1600
|
+
`ethernet { :put … }`.
|
|
1397
1601
|
|
|
1398
1602
|
That last rule is bounded by what a STATEMENT can start with, which is what
|
|
1399
1603
|
keeps `{1;2}` an array literal: an array member opens with a digit or a quote
|
|
@@ -1405,6 +1609,47 @@ That is the catalog's own "a hit is decisive, a miss says nothing" contract, and
|
|
|
1405
1609
|
turning such a miss into an acceptance would be the mirror-image defect of the
|
|
1406
1610
|
one being fixed here.
|
|
1407
1611
|
|
|
1612
|
+
### The `=` is optional in ordinary arguments too (#350)
|
|
1613
|
+
|
|
1614
|
+
The #347 rule is the narrow half of a general one: a space can stand in for the
|
|
1615
|
+
`=` of any command argument. `/ip/firewall/layer7-protocol add comment
|
|
1616
|
+
"something" regexp 1.1` lowers to the IL of `comment="something" regexp=1.1`
|
|
1617
|
+
on CHR 7.22.1, 7.23.8 and 7.24.5, and `highlight` marks each gap
|
|
1618
|
+
`syntax-obsolete`, which is RouterOS calling the spelling deprecated. Unlike
|
|
1619
|
+
the four #347 scope names, this one cannot be read offline. The device pairs
|
|
1620
|
+
`comment` with its value because it knows the command has a `comment`
|
|
1621
|
+
argument, and `/system/script add zzznotanarg beep` is rejected. Which words
|
|
1622
|
+
are argument names is per-command schema, which offline does not ship.
|
|
1623
|
+
|
|
1624
|
+
Offline used to publish the worst reading available: `arguments.read: true`,
|
|
1625
|
+
`args: {}`, the operands as positionals. That is an affirmative claim that the
|
|
1626
|
+
list was read and holds no arguments. Now any adjacent pair of a
|
|
1627
|
+
name-shaped bare operand and another operand withdraws the argument reading,
|
|
1628
|
+
as #311 does. The statement stays `resolved`, `command.args` is absent,
|
|
1629
|
+
`arguments.why` names the pair, and transport stays the `unknown` it already
|
|
1630
|
+
was. A **warning** (`possible-spaced-argument`, example 33) is raised only
|
|
1631
|
+
where the second operand cannot be a flag (quoted, numeric, `$`-led). That is
|
|
1632
|
+
because the same bytes are also two flags: `print detail without-paging`
|
|
1633
|
+
lowers to `detail=true;without-paging=true` with no `syntax-obsolete` marker,
|
|
1634
|
+
while `set ether1 name x` pairs. So two bare words abstain silently. `where`
|
|
1635
|
+
ends the scan, and root directives are excluded because their operands are
|
|
1636
|
+
positional by grammar (`:pick abc 0 1` lowers to `counter=abc`). Live
|
|
1637
|
+
`request=child` discovery (#491) is what confirms a name and makes the warning
|
|
1638
|
+
definite.
|
|
1639
|
+
|
|
1640
|
+
The same applies before a script body. `source { :beep }`,
|
|
1641
|
+
`on-event { :beep }` and `down-script { :beep }` lower to their `=` IL, while
|
|
1642
|
+
`comment { :beep }` is a `syntax error`. So the brace exemption accepts the
|
|
1643
|
+
bare spelling, and its name shape gained the `…-script` arguments
|
|
1644
|
+
(`up-script`, `lease-script`) that hard-failed even with the `=`. Which slots
|
|
1645
|
+
take a body is per-menu and per-version schema (`lease-script={…}` parses on
|
|
1646
|
+
7.24.5 and not on 7.22.1). The shape decides the brace's role for every
|
|
1647
|
+
reader, not just the error, and it errs toward a body: an argument the
|
|
1648
|
+
command does not have (`zzz-script={…}`, and `on-zzz={…}` before #350) is a
|
|
1649
|
+
device `syntax error` that offline does not report. That is a missed
|
|
1650
|
+
rejection for the device preflight to catch, never a false reject. On the
|
|
1651
|
+
pinned corpus the widening changed no census.
|
|
1652
|
+
|
|
1408
1653
|
### `'` is not a RouterOS token (#355)
|
|
1409
1654
|
|
|
1410
1655
|
`'` (U+0027) is not a string delimiter and not a legal bare byte. Only `"` opens
|
|
@@ -1442,11 +1687,14 @@ Occurrences BEFORE the defect stand, which is the lab's X1 rule.
|
|
|
1442
1687
|
|
|
1443
1688
|
**This was a false ACCEPT, which the corpus join cannot see.** The false-reject
|
|
1444
1689
|
measurement scores one direction only, so the analyzer passing all four forms
|
|
1445
|
-
with zero diagnostics never appeared in it. Over the pinned
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
all (still 1, the extra `}` of
|
|
1449
|
-
|
|
1690
|
+
with zero diagnostics never appeared in it. Over the corpus pinned at the time
|
|
1691
|
+
(stable 7.24.2, before the #498 schema-5 repin) the rule fires on 40 scripts and
|
|
1692
|
+
**every one of them is a script the device rejects** — none of the 617 it
|
|
1693
|
+
accepted. It moves the false-reject count not at all (still 1, the extra `}` of
|
|
1694
|
+
corpus script
|
|
1695
|
+
`forum/amm0/topic-167087-can-t-turn-code-into-a-function/post-0005-snippet-02.rsc`)
|
|
1696
|
+
while taking the caught count from 62 to 93. On the #498 sessions both figures
|
|
1697
|
+
hold on every channel: 1 false reject among 573 accepts, 93 caught.
|
|
1450
1698
|
|
|
1451
1699
|
The token census moves, and only in the direction the fail-closed symbol stop
|
|
1452
1700
|
predicts: 68 fewer `variable-*` tokens and their 306 bytes returning to
|
|
@@ -1523,12 +1771,12 @@ Grounded, not transcribed, for the same reason the escape set above is: the
|
|
|
1523
1771
|
[manual's operator list](https://manual.mikrotik.com/docs/developer-guides/scripting/index.md#operators)
|
|
1524
1772
|
is a lower bound. `bun run explain:probe:operators` sweeps the manual's list
|
|
1525
1773
|
**plus** plausible non-operators (`not`, `..`, `xor`, `mod`, `is`, `div`, …)
|
|
1526
|
-
**plus** every IL head the corpus census saw
|
|
1527
|
-
long-term and 7.
|
|
1528
|
-
|
|
1529
|
-
scored on the same firmware (#342)
|
|
1530
|
-
not a channel claim but the only live evidence
|
|
1531
|
-
on a pre-7.23 branch. Re-cut the fixture with
|
|
1774
|
+
**plus** every IL head the corpus census saw. It runs on CHR 7.24.5 stable,
|
|
1775
|
+
7.23.8 long-term, 7.25rc2 testing and 7.26beta1 development, the builds the
|
|
1776
|
+
pinned corpus records a channel session for, so the operator axis and the token
|
|
1777
|
+
partition are scored on the same firmware (#342, #498). It also runs on 7.21.5
|
|
1778
|
+
as an **age anchor**, which is not a channel claim but the only live evidence
|
|
1779
|
+
that the surface behaves the same on a pre-7.23 branch. Re-cut the fixture with
|
|
1532
1780
|
`bun run explain:operator-slice`; the table
|
|
1533
1781
|
below is generated from `src/explain/operators.ts` by
|
|
1534
1782
|
`bun run explain:operator-readme` and gated by
|
|
@@ -1631,13 +1879,13 @@ not concat at all** — `1.` lexes as a *variable name* and the row comes back a
|
|
|
1631
1879
|
juxtaposition, `( $1. 2)`. A rule that claims every `.` byte for the operator
|
|
1632
1880
|
emits a span the device does not have.
|
|
1633
1881
|
|
|
1634
|
-
Everything in the sweep is identical across all
|
|
1635
|
-
row: `:put ({2;1} > {1;2;3})` evaluates to `true` on 7.24.
|
|
1636
|
-
errors on both long-term builds. **Array comparison is not fixed
|
|
1637
|
-
long-term** — 7.23.
|
|
1638
|
-
still errors — so the change rode the 7.24 line and was not
|
|
1639
|
-
errors also differ: 7.23.
|
|
1640
|
-
7.21.5 says `cannot compare if nothing is more than nothing`, so on 7.21.5 the
|
|
1882
|
+
Everything in the sweep is identical across all five builds except one runtime
|
|
1883
|
+
row: `:put ({2;1} > {1;2;3})` evaluates to `true` on 7.24.5, 7.25rc2 and
|
|
1884
|
+
7.26beta1 and errors on both long-term builds. **Array comparison is not fixed
|
|
1885
|
+
on current long-term** — 7.23.8 is newer than the 7.23.3 this was first
|
|
1886
|
+
measured on and still errors — so the change rode the 7.24 line and was not
|
|
1887
|
+
backported. The two errors also differ: 7.23.8 says `cannot compare if array
|
|
1888
|
+
is more than array`, 7.21.5 says `cannot compare if nothing is more than nothing`, so on 7.21.5 the
|
|
1641
1889
|
operands did not survive to the comparison at all.
|
|
1642
1890
|
Identical is measured, not assumed — the slice diffs verdict, arity, precedence,
|
|
1643
1891
|
associativity, `highlight` run, unary placement and op-axis per version. So the
|
|
@@ -1732,13 +1980,14 @@ And the grounded complement — asked, and refused:
|
|
|
1732
1980
|
|
|
1733
1981
|
<!-- BEGIN GENERATED token-census — regenerate with `bun run explain:token-census:readme` -->
|
|
1734
1982
|
The token census is re-derivable with `bun run explain:token-census` and
|
|
1735
|
-
covers 948 source scripts
|
|
1736
|
-
`
|
|
1737
|
-
|
|
1983
|
+
covers 948 source scripts of corpus snapshot `e12d5d4e58a4`, with 882
|
|
1984
|
+
auxiliary rows (7 `capture-controls`, 875 `wiresage-conformance`) excluded.
|
|
1985
|
+
The figures below are generated from `test/fixtures/explain/tokens.json` →
|
|
1986
|
+
`corpus` by `bun run explain:token-census:readme` and gated against it by
|
|
1738
1987
|
`bun run explain:token-census:readme:check`; the fixture itself is gated
|
|
1739
1988
|
against a fresh corpus run by `bun run explain:token-census:check`. Of
|
|
1740
|
-
1,426,731 analyzed bytes, 1,
|
|
1741
|
-
403,
|
|
1989
|
+
1,426,731 analyzed bytes, 1,022,782 are classified (71.69%), the remaining
|
|
1990
|
+
403,949 are `unclassified`. The census emits 241,279 tokens (avg 254.5 per
|
|
1742
1991
|
script). Every byte belongs to exactly one token — sorted by `start`, no
|
|
1743
1992
|
gaps, no overlaps, `join(slice) === input` — and the `class` field is the
|
|
1744
1993
|
vocabulary #264 B5 settled. Each B2 fill should move the classified
|
|
@@ -1819,7 +2068,7 @@ what was wrong was the accounting of it. They are re-derived by
|
|
|
1819
2068
|
`bun run explain:bracket-equals:readme:check`.
|
|
1820
2069
|
|
|
1821
2070
|
<!-- BEGIN GENERATED bracket-equals — regenerate with `bun run explain:bracket-equals:readme` -->
|
|
1822
|
-
Measured by `bun run explain:bracket-equals` over the
|
|
2071
|
+
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:
|
|
1823
2072
|
**1,079** `=` bytes sit inside `[ … ]`, across
|
|
1824
2073
|
263 of 948 scripts, and **1,067** of them come back
|
|
1825
2074
|
`unclassified` — the operator fill abstains on essentially all of it. No
|
|
@@ -1834,15 +2083,15 @@ genuine comparisons — the device lowers `[find address=$IP]` to
|
|
|
1834
2083
|
fill can take the query half later. For contrast the fill does claim
|
|
1835
2084
|
**1,402** `=` bytes whose innermost opener is `(`.
|
|
1836
2085
|
|
|
1837
|
-
The query reading is the device's, not a guess: on 7.24.
|
|
1838
|
-
137 scripts carrying one have IL with at least as many
|
|
1839
|
-
Of the rest,
|
|
1840
|
-
with no nodes of any kind — and
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
counter-example to the reading. Whole-corpus `(= …)` head nodes
|
|
1844
|
-
count expression comparisons the fill already claims:
|
|
1845
|
-
7.23.
|
|
2086
|
+
The query reading is the device's, not a guess: on 7.24.5, 91 of the
|
|
2087
|
+
137 scripts carrying one have IL with at least as many comparison nodes.
|
|
2088
|
+
Of the rest, 25 are scripts the console refused outright — their readout is
|
|
2089
|
+
prose, with no nodes of any kind — and 21 lowered fewer comparison nodes than the
|
|
2090
|
+
scan counted, typically because a command the capture build could not resolve
|
|
2091
|
+
(`bad command name …`) stopped the device before it reached the query. Neither is
|
|
2092
|
+
a counter-example to the reading. Whole-corpus `(= …)` head nodes per channel
|
|
2093
|
+
session, which also count expression comparisons the fill already claims:
|
|
2094
|
+
7.23.8 1,300, 7.24.5 1,306, 7.25rc2 1,306, 7.26beta1 1,306.
|
|
1846
2095
|
<!-- END GENERATED bracket-equals -->
|
|
1847
2096
|
|
|
1848
2097
|
### Device agreement for the token partition (#264 B4, #263)
|
|
@@ -1892,13 +2141,13 @@ to be filled by assumption.
|
|
|
1892
2141
|
|
|
1893
2142
|
**The device stops classifying at its first `error`, and mostly does not
|
|
1894
2143
|
recover.** Measured over the slice, `error` appears **at most once per script
|
|
1895
|
-
and is always exactly one byte wide**, and on long-term 7.23.
|
|
2144
|
+
and is always exactly one byte wide**, and on long-term 7.23.8 every byte after
|
|
1896
2145
|
it comes back `none`. Most of the slice sits in that tail. A percentage computed
|
|
1897
2146
|
over the whole stream therefore measures the device giving up, not the parser,
|
|
1898
2147
|
which is why the tail is its own outcome and is never folded into agreement. The
|
|
1899
2148
|
rule is not absolute across versions, so the report does not assert it: stable
|
|
1900
|
-
7.24.
|
|
1901
|
-
stop on fewer scripts, and
|
|
2149
|
+
7.24.5, testing 7.25rc2 and development 7.26beta1 classify again after the
|
|
2150
|
+
`error` in one run and stop on fewer scripts, and
|
|
1902
2151
|
`parser-recovered` in the table below is the figure that says by how much —
|
|
1903
2152
|
zero would have been the claim, and it is not zero. Both counts are generated,
|
|
1904
2153
|
not quoted here, so neither can go stale against the fixture.
|
|
@@ -1920,56 +2169,57 @@ a score.
|
|
|
1920
2169
|
The agreement report is re-derivable with
|
|
1921
2170
|
`bun run explain:highlight-agreement` and needs no router and no corpus: it
|
|
1922
2171
|
scores the token partition against
|
|
1923
|
-
`test/fixtures/explain/highlight-streams.slice.json`,
|
|
2172
|
+
`test/fixtures/explain/highlight-streams.slice.json`, 67 stratified scripts of
|
|
1924
2173
|
committed `/console/inspect request=highlight` output. The figures below are
|
|
1925
2174
|
generated from `test/fixtures/explain/highlight-agreement.json` by
|
|
1926
2175
|
`bun run explain:highlight-agreement:readme`, gated against it by
|
|
1927
2176
|
`bun run explain:highlight-agreement:readme:check`, and the fixture itself is
|
|
1928
2177
|
gated against a fresh measurement by
|
|
1929
|
-
`test/unit/explain-highlight-agreement.test.ts`. Of
|
|
1930
|
-
27,
|
|
1931
|
-
those agree (dev 99.99%, holdout 100.00%). Set `comment` aside — it is
|
|
2178
|
+
`test/unit/explain-highlight-agreement.test.ts`. Of 81,143 bytes at 7.24.5,
|
|
2179
|
+
27,538 are bytes **both** sides decided a syntax class for, and 99.99% of
|
|
2180
|
+
those agree (dev 99.99%, holdout 100.00%). Set `comment` aside — it is 63.66%
|
|
1932
2181
|
of that decided region and mostly the corpus's harness-injected `# Source:`
|
|
1933
|
-
banner (#203) — and the remaining
|
|
1934
|
-
classifying at its one-byte `error`:
|
|
1935
|
-
|
|
1936
|
-
itself moves between captures:
|
|
1937
|
-
stop at 7.
|
|
1938
|
-
trend line, not a pass gate** — the slice is a
|
|
1939
|
-
corpus that is 96.8% two authors, so the
|
|
2182
|
+
banner (#203) — and the remaining 10,008 bytes agree 99.98%. The device stops
|
|
2183
|
+
classifying at its one-byte `error`: 34 of 67 scripts carry one, and the
|
|
2184
|
+
38,278 bytes from there on are not a judgment about anything. The oracle
|
|
2185
|
+
itself moves between captures: 34 stop at 7.24.5 and 39 stop at 7.23.8 and 34
|
|
2186
|
+
stop at 7.25rc2 and 34 stop at 7.26beta1, and 25 of the 67 streams differ
|
|
2187
|
+
between them. **This is a trend line, not a pass gate** — the slice is a
|
|
2188
|
+
per-(split, class) quota over a corpus that is 96.8% two authors, so the
|
|
2189
|
+
percentage describes this slice.
|
|
1940
2190
|
|
|
1941
2191
|
| centrs class | projects to | agree | disagree | unprojected | of decided |
|
|
1942
2192
|
| ------------ | ----------- | ----: | -------: | ----------: | ---------: |
|
|
1943
|
-
| `comment` | `comment` |
|
|
1944
|
-
| `variable-local` | `variable-local` | 1,
|
|
2193
|
+
| `comment` | `comment` | 17,530 | 0 | 0 | 100.00% |
|
|
2194
|
+
| `variable-local` | `variable-local` | 1,547 | 0 | 0 | 100.00% |
|
|
1945
2195
|
| `variable-global` | `variable-global` | 582 | 0 | 0 | 100.00% |
|
|
1946
|
-
| `variable-auto` | `variable-auto` |
|
|
1947
|
-
| `variable-parameter` | `variable-parameter` |
|
|
1948
|
-
| `dir` | `dir` | 1,
|
|
1949
|
-
| `cmd` | `cmd` | 2,
|
|
1950
|
-
| `arg` | `arg`, `arg-dot`, `arg-scope` | 2,
|
|
1951
|
-
| `arg-sep` | `syntax-meta` |
|
|
1952
|
-
| `operator` | `syntax-meta` |
|
|
1953
|
-
| `brace` | `syntax-meta` |
|
|
1954
|
-
| `string` | *abstains* | 0 | 0 |
|
|
2196
|
+
| `variable-auto` | `variable-auto` | 93 | 0 | 0 | 100.00% |
|
|
2197
|
+
| `variable-parameter` | `variable-parameter` | 456 | 0 | 0 | 100.00% |
|
|
2198
|
+
| `dir` | `dir` | 1,701 | 0 | 0 | 100.00% |
|
|
2199
|
+
| `cmd` | `cmd` | 2,224 | 0 | 0 | 100.00% |
|
|
2200
|
+
| `arg` | `arg`, `arg-dot`, `arg-scope` | 2,167 | 0 | 0 | 100.00% |
|
|
2201
|
+
| `arg-sep` | `syntax-meta` | 368 | 0 | 0 | 100.00% |
|
|
2202
|
+
| `operator` | `syntax-meta` | 240 | 2 | 0 | 99.17% |
|
|
2203
|
+
| `brace` | `syntax-meta` | 210 | 0 | 0 | 100.00% |
|
|
2204
|
+
| `string` | *abstains* | 0 | 0 | 573 | — |
|
|
1955
2205
|
| `escaped` | `escaped` | 418 | 0 | 0 | 100.00% |
|
|
1956
|
-
| `value` | *abstains* | 0 | 0 |
|
|
1957
|
-
|
|
1958
|
-
| outcome | bytes @7.24.
|
|
1959
|
-
| ------- | ----: | ----: | ----: |
|
|
1960
|
-
| agree — both decided, projection accepts | 27,
|
|
1961
|
-
| disagree — both decided, projection rejects | 2 | 2 | 2 |
|
|
1962
|
-
| unprojected — no declared projection covers the pair |
|
|
1963
|
-
| offline-silent — device decided, centrs abstained | 4,
|
|
1964
|
-
| non-syntax — the device answered something syntax cannot decide |
|
|
1965
|
-
| device-silent — centrs decided, device said `none` | 5,
|
|
1966
|
-
| both-silent | 3,
|
|
1967
|
-
| parser-stopped — at/after the device's `error` byte, and silent from there |
|
|
1968
|
-
| parser-recovered — past that `error`, and the device classified anyway | 96 | 0 | 96 |
|
|
1969
|
-
|
|
1970
|
-
Where the device decided and offline analysis did not (4,
|
|
1971
|
-
the next fill's target list): `syntax-meta` 3,
|
|
1972
|
-
`variable-local`
|
|
2206
|
+
| `value` | *abstains* | 0 | 0 | 135 | — |
|
|
2207
|
+
|
|
2208
|
+
| outcome | bytes @7.24.5 | bytes @7.23.8 | bytes @7.25rc2 | bytes @7.26beta1 |
|
|
2209
|
+
| ------- | ----: | ----: | ----: | ----: |
|
|
2210
|
+
| agree — both decided, projection accepts | 27,536 | 27,173 | 27,536 | 27,536 |
|
|
2211
|
+
| disagree — both decided, projection rejects | 2 | 2 | 2 | 2 |
|
|
2212
|
+
| unprojected — no declared projection covers the pair | 708 | 705 | 708 | 708 |
|
|
2213
|
+
| offline-silent — device decided, centrs abstained | 4,288 | 4,187 | 4,288 | 4,288 |
|
|
2214
|
+
| non-syntax — the device answered something syntax cannot decide | 731 | 662 | 721 | 721 |
|
|
2215
|
+
| device-silent — centrs decided, device said `none` | 5,676 | 5,606 | 5,676 | 5,676 |
|
|
2216
|
+
| both-silent | 3,828 | 3,736 | 3,838 | 3,838 |
|
|
2217
|
+
| parser-stopped — at/after the device's `error` byte, and silent from there | 38,278 | 39,072 | 38,278 | 38,278 |
|
|
2218
|
+
| parser-recovered — past that `error`, and the device classified anyway | 96 | 0 | 96 | 96 |
|
|
2219
|
+
|
|
2220
|
+
Where the device decided and offline analysis did not (4,288 bytes at 7.24.5,
|
|
2221
|
+
the next fill's target list): `syntax-meta` 3,090, `comment` 341,
|
|
2222
|
+
`variable-local` 328, `arg` 230, `escaped` 145, `dir` 65, 89 across the rest.
|
|
1973
2223
|
Most of the `syntax-meta` share is whitespace the device merged into an
|
|
1974
2224
|
adjacent structure run rather than a token centrs missed.
|
|
1975
2225
|
|
|
@@ -1981,14 +2231,14 @@ every measured byte, the post-`error` tail included: where one capture stops
|
|
|
1981
2231
|
parsing and the next does not, that tail IS the version disagreement, which is
|
|
1982
2232
|
why it dominates the category.
|
|
1983
2233
|
|
|
1984
|
-
| applicability @7.24.
|
|
2234
|
+
| applicability @7.24.5 | bytes | leading cells |
|
|
1985
2235
|
| ---------------------------- | ----: | ------------- |
|
|
1986
|
-
| offline-decidable | 32,
|
|
1987
|
-
| schema-dependent |
|
|
1988
|
-
| state-dependent |
|
|
1989
|
-
| version-dependent |
|
|
1990
|
-
| uncategorized |
|
|
1991
|
-
| no-device-answer |
|
|
2236
|
+
| offline-decidable | 32,043 | `comment` → `comment` 17,395, `unclassified` → `syntax-meta` 3,002, `cmd` → `cmd` 2,158 |
|
|
2237
|
+
| schema-dependent | 307 | `arg` → `obj-inactive` 307 |
|
|
2238
|
+
| state-dependent | 94 | `dir` → `obj-inactive` 49, `unclassified` → `obj-dynamic` 20, `cmd` → `obj-inactive` 18 |
|
|
2239
|
+
| version-dependent | 707 | `comment` → `comment` 135, `unclassified` → `obj-inactive` 124, `unclassified` → `syntax-meta` 88 |
|
|
2240
|
+
| uncategorized | 240 | `unclassified` → `obj-inactive` 116, `unclassified` → `variable-undefined` 75, `value` → `variable-undefined` 21 |
|
|
2241
|
+
| no-device-answer | 47,752 | `none` where the captures agree, wherever it falls |
|
|
1992
2242
|
|
|
1993
2243
|
One representative run per cell, so B5 reads a fragment rather than a count.
|
|
1994
2244
|
`runs` is how often that exact fragment occurs; the location is a slice script
|
|
@@ -1997,9 +2247,8 @@ and a byte offset into the stream the device saw.
|
|
|
1997
2247
|
| outcome | cell | fragment | runs | first at |
|
|
1998
2248
|
| ------- | ---- | -------- | ---: | -------- |
|
|
1999
2249
|
| 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 |
|
|
2000
|
-
| unprojected | `string` → `syntax-meta` | `"\""` |
|
|
2001
|
-
| unprojected | `value` → `syntax-meta` | `"\""` |
|
|
2002
|
-
| unprojected | `string` → `arg` | `"as-value"` | 1 | `forum/amm0/topic-266499-how-to-condense-variables-passed-through-to-function/post-0020-snippet-01.rsc` @279 |
|
|
2250
|
+
| unprojected | `string` → `syntax-meta` | `"\""` | 326 | `eworm/ppp-on-up.rsc` @435 |
|
|
2251
|
+
| 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 |
|
|
2003
2252
|
| unprojected | `string` → `cmd` | `"len"` | 1 | `forum/amm0/topic-155978-zerotier-on-mikrotik-a-rosetta-stone-v7-1-1/post-0001-snippet-01.rsc` @1794 |
|
|
2004
2253
|
| unprojected | `string` → `dir` | `":"` | 1 | `forum/amm0/topic-155978-zerotier-on-mikrotik-a-rosetta-stone-v7-1-1/post-0001-snippet-01.rsc` @1793 |
|
|
2005
2254
|
<!-- END GENERATED highlight-agreement -->
|
|
@@ -2089,9 +2338,10 @@ the 16,526 bytes `arg-sep` claims, one per attribute. Token count rises 214,503
|
|
|
2089
2338
|
→ 231,026 because one run became two. **This is a retag.**
|
|
2090
2339
|
|
|
2091
2340
|
(Those totals are the corpus as it stood for THIS measurement. The classified
|
|
2092
|
-
total is 1,
|
|
2341
|
+
total is 1,022,805 today: #347/#348 later classified 8 further bytes, #355's
|
|
2093
2342
|
fail-closed symbol stop then unclaimed 306 of them — variable bytes sitting
|
|
2094
|
-
after a bare apostrophe, which the device never reaches
|
|
2343
|
+
after a bare apostrophe, which the device never reaches — and #211's unlisted
|
|
2344
|
+
bare-path abstention unclaimed 302 more. The identity being
|
|
2095
2345
|
claimed here is between the two sides of the split, not with any later total —
|
|
2096
2346
|
restating it against today's figure would credit this retag with a movement a
|
|
2097
2347
|
different change made. The generated block above carries the current numbers.)
|
|
@@ -2179,7 +2429,7 @@ bytes came from a fill that already held it — `string` gives up 17,672 and
|
|
|
2179
2429
|
last decimal**, 1,023,405 of 1,426,731 (71.73076073906013%), so in byte terms
|
|
2180
2430
|
this is a retag even though it took a new walk to make — again, the identity is
|
|
2181
2431
|
between this change's own before and after, and the corpus total has since moved
|
|
2182
|
-
to 1,
|
|
2432
|
+
to 1,022,805 / 241,442 tokens under #347/#348, #355 and #211. Only the token count
|
|
2183
2433
|
moves, 231,026 → 241,663: the 7,408 new `escaped` tokens plus the 3,229
|
|
2184
2434
|
fragments they leave behind when they split a run they sit inside (`string`
|
|
2185
2435
|
+2,643, `value` +586). That second number is well under one per escape because
|
|
@@ -2449,31 +2699,65 @@ config genre in question). Since phase 0 the corpus has grown to 948 with a
|
|
|
2449
2699
|
export-banner share to 4.6%; **that stratum is not part of any phase-0 figure
|
|
2450
2700
|
above**. Current whole-corpus offline censuses include its source text.
|
|
2451
2701
|
|
|
2452
|
-
The pinned snapshot
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
7.
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
`
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2702
|
+
The pinned snapshot (schema 5, tikoci/lsp-routeros-ts#40, plus the
|
|
2703
|
+
`wiresage-conformance` collection from tikoci/lsp-routeros-ts#42) carries one
|
|
2704
|
+
recorded capture session per release channel — long-term `7.23.8`, stable `7.24.5`,
|
|
2705
|
+
testing `7.25rc2` and development `7.26beta1`. Each is a `:parse` run and a
|
|
2706
|
+
`highlight` run over all 948 population scripts, bound to the exact submitted
|
|
2707
|
+
bytes and one device environment with every package installed. Two legacy
|
|
2708
|
+
`:parse` captures (7.20.8 and 7.22.1: 913 scripts, unknown input bytes, base
|
|
2709
|
+
package only) and a legacy 7.9.2 highlight summary remain as history, never as
|
|
2710
|
+
exact-input evidence.
|
|
2711
|
+
|
|
2712
|
+
Every census reads the corpus through `scripts/corpus-db.ts`, in three ways
|
|
2713
|
+
(#498):
|
|
2714
|
+
|
|
2715
|
+
- **Population.** `v_corpus_population` leaves out the upstream auxiliary
|
|
2716
|
+
collections: 7 `capture-controls` rows and 875 `wiresage-conformance`
|
|
2717
|
+
synthetic grammar cases. An observation is in the population by `script_id`,
|
|
2718
|
+
not by path prefix, so a collection added upstream cannot leak into a census
|
|
2719
|
+
(it did, briefly, when #42 added 875 rows to every run).
|
|
2720
|
+
- **Sessions.** Comparable figures name their channel run ids
|
|
2721
|
+
(`capture_channels`), not a coverage label.
|
|
2722
|
+
- **Verdict.** The device verdict is the upstream `response_class`. Only
|
|
2723
|
+
`il-no-recognized-diagnostic` is an accept, so IL carrying an embedded
|
|
2724
|
+
`bad command name …` is a rejection. The shipped `:parse` reader
|
|
2725
|
+
(`classifyParseResult`) agrees with it on all 5,640 channel and legacy
|
|
2726
|
+
observations.
|
|
2727
|
+
|
|
2728
|
+
The population text is byte-identical to the previous snapshot, so the
|
|
2729
|
+
text-derived reports are unchanged by construction: the token and value
|
|
2730
|
+
censuses, `explain:arg-reach` and the browser-consumer proof. The IL- and
|
|
2731
|
+
highlight-derived figures moved.
|
|
2732
|
+
|
|
2733
|
+
**The channels do not agree with each other, and that is the finding.** Scored
|
|
2734
|
+
over all 948 scripts:
|
|
2735
|
+
|
|
2736
|
+
| oracle | long-term 7.23.8 ↔ stable 7.24.5 | stable ↔ testing 7.25rc2 | testing ↔ development 7.26beta1 |
|
|
2737
|
+
| ------ | -------------------------------: | -----------------------: | ------------------------------: |
|
|
2738
|
+
| `:parse` readout | 95 scripts differ | 12 | 13 |
|
|
2739
|
+
| `:parse` response class | 5 | 0 | 0 |
|
|
2740
|
+
| `highlight` stream | 24 | 1 | 0 |
|
|
2741
|
+
|
|
2742
|
+
The newer channels are nearly the same build for these purposes; long-term is
|
|
2743
|
+
the one that moves. So an operator-axis or token-partition figure is a claim
|
|
2744
|
+
*about a channel*, not about RouterOS. That is why the census reports its
|
|
2745
|
+
builds as separate columns and states its session set, rather than collapsing
|
|
2746
|
+
them to one number. Long-term is also the channel a real device most likely
|
|
2747
|
+
runs.
|
|
2748
|
+
|
|
2749
|
+
**The `wiresage-conformance` cases are read separately, never as population.**
|
|
2750
|
+
`conformanceCases` returns the 875 third-party grammar probes with their
|
|
2751
|
+
upstream label, and `parseObservations(…, { collection })` returns their
|
|
2752
|
+
device answers. `bun run explain:conformance` scores the execute gate's offline
|
|
2753
|
+
answer (`fail` refuses) against `:parse` on every channel. It lists false
|
|
2754
|
+
rejects, missed rejections (split by device class: `plain-diagnostic` is
|
|
2755
|
+
syntax, while `embedded-diagnostic` is mostly a menu or parameter name, which
|
|
2756
|
+
needs the #501 argument lists), cases where the channels disagree, cases
|
|
2757
|
+
with no device answer (`unscorable`, never read as a rejection), and the
|
|
2758
|
+
upstream-label disagreements that #507 needs. The lists are committed in
|
|
2759
|
+
`test/fixtures/explain/conformance.json` and gated in CI, so a fix for any
|
|
2760
|
+
of #504–#506 shows the cases it moved.
|
|
2477
2761
|
|
|
2478
2762
|
The corpus itself is not in this repo and is not moving here — `lsp-routeros-ts`
|
|
2479
2763
|
owns producing snapshots, centrs owns which snapshot it measures against.
|
|
@@ -2543,10 +2827,27 @@ to mention. `/system/reboot` no longer moves the context, so a statement after
|
|
|
2543
2827
|
it resolves where it actually is; `verbsplit` and `pathresolve` agree, which
|
|
2544
2828
|
they would otherwise now do *confidently* in opposite directions. What is left
|
|
2545
2829
|
is the honest residue of both floors — `/terminal/cuu` is a real no-argument
|
|
2546
|
-
command MikroTik publishes nowhere
|
|
2547
|
-
fixture rather than assumed closed. The contradiction that #202 would have
|
|
2830
|
+
command MikroTik publishes nowhere. The contradiction that #202 would have
|
|
2548
2831
|
rendered is gone.
|
|
2549
2832
|
|
|
2833
|
+
**#211 makes that residue abstain (R14).** A verb-free absolute path that
|
|
2834
|
+
neither table knows used to still move the context, so `/terminal/cuu` then
|
|
2835
|
+
`print` resolved to an invented `/terminal/cuu/print`. On CHR 7.24.5 `:parse`
|
|
2836
|
+
reads `/terminal/cuu` as a command and that `print` as `bad command name print`
|
|
2837
|
+
at the root. `/ip fire conn` has the same shape and is navigation to
|
|
2838
|
+
`/ip/firewall/connection`, because RouterOS accepts abbreviated menus. A table
|
|
2839
|
+
miss therefore cannot decide either way. The statement now reads as R13's
|
|
2840
|
+
relative abstention does: not navigation, flagged `maybeNav`, with the
|
|
2841
|
+
statements after it `unknown` until a known absolute navigation recovers the
|
|
2842
|
+
context ([example 34](examples.md#34-an-unlisted-bare-path-does-not-become-a-menu)).
|
|
2843
|
+
Over the pinned corpus, 14 of 18,650 statement readings lose a path, in 6
|
|
2844
|
+
scripts. Every one was an invented path: `/ip/fire/conn/remove`,
|
|
2845
|
+
`/ip/firewall/layer7/add`, the v6 `/routing/bgp/network/add`, and
|
|
2846
|
+
`/terminal/style/error/put`. No `explain` verdict changes. Four `containsWrite`
|
|
2847
|
+
verdicts move from `true` to `unknown`, because their write was proven only at
|
|
2848
|
+
an invented path. Rule P, which would read the tables as a per-menu closed
|
|
2849
|
+
world and expand abbreviations, is not adopted (#372).
|
|
2850
|
+
|
|
2550
2851
|
**The command axis also outranks the punctuation guess.** `splitRun`'s last
|
|
2551
2852
|
rule reads the first space-separated token as the verb, which lands on the
|
|
2552
2853
|
operand whenever the command itself was written slash-joined:
|
|
@@ -2676,8 +2977,8 @@ The offline capability can advance from `coded` to `verified` when:
|
|
|
2676
2977
|
- **Reachable evidence and explicit residue:** changed RouterOS semantic claims
|
|
2677
2978
|
have committed CHR-grounded controls and a reproducible capture/replay path.
|
|
2678
2979
|
Every remaining issue has a stated scope and dependency; #225 distinguishes
|
|
2679
|
-
offline value work from live types, #272 owns evidence reachability, and
|
|
2680
|
-
unlisted
|
|
2980
|
+
offline value work from live types, #272 owns evidence reachability, and an
|
|
2981
|
+
unlisted bare path abstains (#211) until #372 prices a closed-world reading.
|
|
2681
2982
|
|
|
2682
2983
|
Retain the existing checks: `bun run lint`, `bun run test`, `bun run build`, and
|
|
2683
2984
|
`bun run lint:ci`, plus the affected CHR grounding/integration procedure from
|