@tikoci/centrs 0.1.9-next.22 → 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.
@@ -460,6 +460,7 @@ Standard envelope (constitution: result envelope); `data` sketch:
460
460
  "statementCount": 1,
461
461
  "statements": [
462
462
  {
463
+ "id": "s0",
463
464
  "span": { "start": 0, "end": 51 },
464
465
  "resolution": "resolved",
465
466
  "kind": "command",
@@ -511,6 +512,36 @@ Standard envelope (constitution: result envelope); `data` sketch:
511
512
  classification, and evidence id. This is where multi-statement scripts and
512
513
  partial recovery live; a single top-level transport object cannot represent
513
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.
514
545
  - **Every derived fact keeps provenance via `evidence[]`** — facts reference
515
546
  a stable evidence id (`ev`), and the evidence entry carries source
516
547
  (`canonicalizer` vs `live-inspect`), probe, basis (direct response vs
@@ -907,7 +938,16 @@ and its phase is named below.
907
938
  top-level brace value in any other command argument emits
908
939
  `explain/canonicalizer/invalid-command-brace` at the `{`; parentheses or a
909
940
  `[…]` substitution restore the expression context where the same literal is
910
- legal. An empty member is a
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
911
951
  syntax error (`{;}`, `{;1}`, `{1;;2}`, `(1,)`) and withdraws the enclosing
912
952
  `array` shape with it; a single trailing separator is legal in the brace form
913
953
  only (`{1;}` and `{1;2;}` parse, `{1;2,}` and `{2,}` do not).
@@ -947,15 +987,16 @@ and its phase is named below.
947
987
 
948
988
  <!-- BEGIN GENERATED value-census — regenerate with `bun run explain:value-census:readme` -->
949
989
  The corpus census is re-derivable with `bun run explain:value-census` and
950
- covers 948 source scripts. The figures below are generated from
951
- `test/fixtures/explain/values.json` → `corpus` by
952
- `bun run explain:value-census:readme` and gated against it by
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
953
994
  `bun run explain:value-census:readme:check`; the fixture itself is gated
954
995
  against a fresh corpus run by `bun run explain:value-census:check`. Of the
955
- 13,161 emitted values in a statement the strict argument lexer ALSO read, 0
996
+ 13,152 emitted values in a statement the strict argument lexer ALSO read, 0
956
997
  disagree with it on half-open byte span or decoded text, while the
957
998
  prefix-safe scan retains a further 6,538 values across 568 statements whose
958
- strict REST reading abstains. Of 19,699 emitted occurrences, 5,636 are array
999
+ strict REST reading abstains. Of 19,690 emitted occurrences, 5,636 are array
959
1000
  members (529 keyed, 1,147 nested inside another member) and 808 are arrays;
960
1001
  the corpus holds no source-literal `id` example. The three structural
961
1002
  counters — spans addressing bytes outside their own source, members naming a
@@ -982,7 +1023,8 @@ comment, a literal hash value, and a hard error at the exact byte.
982
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. |
983
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. |
984
1025
  | 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. |
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). |
986
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. |
987
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. |
988
1030
  | 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 +1062,14 @@ JSON-like transcript, and two shell-shaped snippets. No RouterOS command reading
1020
1062
  moved; these diagnostics say those bytes are not valid RouterOS, not that the
1021
1063
  foreign snippets were expected to parse as RouterOS.
1022
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
+
1023
1073
  The default text renderer surfaces comment spans in a `comments:` section; JSON
1024
1074
  continues to carry the same ranges as `spans[]` entries with `class: "comment"`.
1025
1075
 
@@ -1074,10 +1124,17 @@ navigated, so the context stays as unknown as it already was.
1074
1124
  `:parse` `expected message value`, grounded stable+testing #247),
1075
1125
  `bad-sigil`,
1076
1126
  `invalid-hash`, `invalid-apostrophe` (#355, below),
1077
- `missing-statement-separator` (#311, below). Nine classes
1078
- the device itself rejects.
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.
1079
1133
  - `warning` — `over-depth`, because it is centrs's own resource bound and says
1080
- nothing about whether the input is legal; and an `ambiguous`/`unknown`
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`
1081
1138
  resolution, never an error, so the default `--fail-on error` cannot fail a
1082
1139
  document whose only sin is being unreadable without a schema — which is most
1083
1140
  of RouterOS scripting.
@@ -1182,6 +1239,151 @@ it. The corpus count is unchanged at **0** for the same reason it always was
1182
1239
  the rule is an unterminated string or an unbalanced delimiter, because neither
1183
1240
  walk can find a boundary to resume from.
1184
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
+
1185
1387
  ### The token stream a rule reads (#316)
1186
1388
 
1187
1389
  `lexArguments` is all-or-nothing by design: its consumer renders a runnable REST
@@ -1394,7 +1596,8 @@ print } }` is `(evl (evl (evl /ip/address/print)))` on device and
1394
1596
  form failed. A menu block's body is a statement list, so it may open with a
1395
1597
  submenu, with one of the thirteen frozen CRUD verbs (`verbs.ts`), with a
1396
1598
  catalog-named command, with a `:` scripting directive, or with an absolute
1397
- command — corpus 521 is the directive case, `ethernet { :put … }`.
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 … }`.
1398
1601
 
1399
1602
  That last rule is bounded by what a STATEMENT can start with, which is what
1400
1603
  keeps `{1;2}` an array literal: an array member opens with a digit or a quote
@@ -1406,6 +1609,47 @@ That is the catalog's own "a hit is decisive, a miss says nothing" contract, and
1406
1609
  turning such a miss into an acceptance would be the mirror-image defect of the
1407
1610
  one being fixed here.
1408
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
+
1409
1653
  ### `'` is not a RouterOS token (#355)
1410
1654
 
1411
1655
  `'` (U+0027) is not a string delimiter and not a legal bare byte. Only `"` opens
@@ -1443,11 +1687,14 @@ Occurrences BEFORE the defect stand, which is the lab's X1 rule.
1443
1687
 
1444
1688
  **This was a false ACCEPT, which the corpus join cannot see.** The false-reject
1445
1689
  measurement scores one direction only, so the analyzer passing all four forms
1446
- with zero diagnostics never appeared in it. Over the pinned corpus the rule
1447
- fires on 40 of the 7.24.2 scripts and **every one of them is a script the device
1448
- rejects** — none of the 617 it accepts. It moves the false-reject count not at
1449
- all (still 1, the extra `}` of corpus 228) while taking the caught count from 62
1450
- to 93.
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.
1451
1698
 
1452
1699
  The token census moves, and only in the direction the fail-closed symbol stop
1453
1700
  predicts: 68 fewer `variable-*` tokens and their 306 bytes returning to
@@ -1524,12 +1771,12 @@ Grounded, not transcribed, for the same reason the escape set above is: the
1524
1771
  [manual's operator list](https://manual.mikrotik.com/docs/developer-guides/scripting/index.md#operators)
1525
1772
  is a lower bound. `bun run explain:probe:operators` sweeps the manual's list
1526
1773
  **plus** plausible non-operators (`not`, `..`, `xor`, `mod`, `is`, `div`, …)
1527
- **plus** every IL head the corpus census saw, on CHR 7.24.2 stable, 7.23.5
1528
- long-term and 7.25beta3 development — the same three builds the pinned corpus
1529
- classes `complete`, so the operator axis and the token partition are finally
1530
- scored on the same firmware (#342) — plus 7.21.5 as an **age anchor**, which is
1531
- not a channel claim but the only live evidence that the surface behaves the same
1532
- 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
1533
1780
  `bun run explain:operator-slice`; the table
1534
1781
  below is generated from `src/explain/operators.ts` by
1535
1782
  `bun run explain:operator-readme` and gated by
@@ -1632,13 +1879,13 @@ not concat at all** — `1.` lexes as a *variable name* and the row comes back a
1632
1879
  juxtaposition, `( $1. 2)`. A rule that claims every `.` byte for the operator
1633
1880
  emits a span the device does not have.
1634
1881
 
1635
- Everything in the sweep is identical across all four builds except one runtime
1636
- row: `:put ({2;1} > {1;2;3})` evaluates to `true` on 7.24.2 and 7.25beta3 and
1637
- errors on both long-term builds. **Array comparison is not fixed on current
1638
- long-term** — 7.23.5 is newer than the 7.23.3 this was first measured on and
1639
- still errors — so the change rode the 7.24 line and was not backported. The two
1640
- errors also differ: 7.23.5 says `cannot compare if array is more than array`,
1641
- 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
1642
1889
  operands did not survive to the comparison at all.
1643
1890
  Identical is measured, not assumed — the slice diffs verdict, arity, precedence,
1644
1891
  associativity, `highlight` run, unary placement and op-axis per version. So the
@@ -1733,13 +1980,14 @@ And the grounded complement — asked, and refused:
1733
1980
 
1734
1981
  <!-- BEGIN GENERATED token-census — regenerate with `bun run explain:token-census:readme` -->
1735
1982
  The token census is re-derivable with `bun run explain:token-census` and
1736
- covers 948 source scripts. The figures below are generated from
1737
- `test/fixtures/explain/tokens.json` → `corpus` by
1738
- `bun run explain:token-census:readme` and gated against it by
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
1739
1987
  `bun run explain:token-census:readme:check`; the fixture itself is gated
1740
1988
  against a fresh corpus run by `bun run explain:token-census:check`. Of
1741
- 1,426,731 analyzed bytes, 1,023,107 are classified (71.71%), the remaining
1742
- 403,624 are `unclassified`. The census emits 241,539 tokens (avg 254.8 per
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
1743
1991
  script). Every byte belongs to exactly one token — sorted by `start`, no
1744
1992
  gaps, no overlaps, `join(slice) === input` — and the `class` field is the
1745
1993
  vocabulary #264 B5 settled. Each B2 fill should move the classified
@@ -1820,7 +2068,7 @@ what was wrong was the accounting of it. They are re-derived by
1820
2068
  `bun run explain:bracket-equals:readme:check`.
1821
2069
 
1822
2070
  <!-- BEGIN GENERATED bracket-equals — regenerate with `bun run explain:bracket-equals:readme` -->
1823
- Measured by `bun run explain:bracket-equals` over the pinned corpus:
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:
1824
2072
  **1,079** `=` bytes sit inside `[ … ]`, across
1825
2073
  263 of 948 scripts, and **1,067** of them come back
1826
2074
  `unclassified` — the operator fill abstains on essentially all of it. No
@@ -1835,15 +2083,15 @@ genuine comparisons — the device lowers `[find address=$IP]` to
1835
2083
  fill can take the query half later. For contrast the fill does claim
1836
2084
  **1,402** `=` bytes whose innermost opener is `(`.
1837
2085
 
1838
- The query reading is the device's, not a guess: on 7.24.2, 93 of the
1839
- 137 scripts carrying one have IL with at least as many `(= …)` nodes.
1840
- Of the rest, 28 are scripts the console refused outright — their IL is prose,
1841
- with no nodes of any kind — and 16 use a menu the capture build could not
1842
- resolve (a forum snippet on `/interface wireless` answers `bad command name
1843
- wireless`), so the device never reached the query to lower it. Neither is a
1844
- counter-example to the reading. Whole-corpus `(= …)` head nodes, which also
1845
- count expression comparisons the fill already claims:
1846
- 7.23.5 1,233, 7.24.2 1,240, 7.25beta3 1,240.
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.
1847
2095
  <!-- END GENERATED bracket-equals -->
1848
2096
 
1849
2097
  ### Device agreement for the token partition (#264 B4, #263)
@@ -1893,13 +2141,13 @@ to be filled by assumption.
1893
2141
 
1894
2142
  **The device stops classifying at its first `error`, and mostly does not
1895
2143
  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.5 every byte after
2144
+ and is always exactly one byte wide**, and on long-term 7.23.8 every byte after
1897
2145
  it comes back `none`. Most of the slice sits in that tail. A percentage computed
1898
2146
  over the whole stream therefore measures the device giving up, not the parser,
1899
2147
  which is why the tail is its own outcome and is never folded into agreement. The
1900
2148
  rule is not absolute across versions, so the report does not assert it: stable
1901
- 7.24.2 and development 7.25beta3 classify again after the `error` in one run and
1902
- 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
1903
2151
  `parser-recovered` in the table below is the figure that says by how much —
1904
2152
  zero would have been the claim, and it is not zero. Both counts are generated,
1905
2153
  not quoted here, so neither can go stale against the fixture.
@@ -1921,56 +2169,57 @@ a score.
1921
2169
  The agreement report is re-derivable with
1922
2170
  `bun run explain:highlight-agreement` and needs no router and no corpus: it
1923
2171
  scores the token partition against
1924
- `test/fixtures/explain/highlight-streams.slice.json`, 70 stratified scripts of
2172
+ `test/fixtures/explain/highlight-streams.slice.json`, 67 stratified scripts of
1925
2173
  committed `/console/inspect request=highlight` output. The figures below are
1926
2174
  generated from `test/fixtures/explain/highlight-agreement.json` by
1927
2175
  `bun run explain:highlight-agreement:readme`, gated against it by
1928
2176
  `bun run explain:highlight-agreement:readme:check`, and the fixture itself is
1929
2177
  gated against a fresh measurement by
1930
- `test/unit/explain-highlight-agreement.test.ts`. Of 85,529 bytes at 7.24.2,
1931
- 27,974 are bytes **both** sides decided a syntax class for, and 99.99% of
1932
- those agree (dev 99.99%, holdout 100.00%). Set `comment` aside — it is 65.63%
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%
1933
2181
  of that decided region and mostly the corpus's harness-injected `# Source:`
1934
- banner (#203) — and the remaining 9,615 bytes agree 99.98%. The device stops
1935
- classifying at its one-byte `error`: 36 of 70 scripts carry one, and the
1936
- 41,968 bytes from there on are not a judgment about anything. The oracle
1937
- itself moves between captures: 36 stop at 7.24.2 and 43 stop at 7.23.5 and 36
1938
- stop at 7.25beta3, and 27 of the 70 streams differ between them. **This is a
1939
- trend line, not a pass gate** — the slice is a per-(split, class) quota over a
1940
- corpus that is 96.8% two authors, so the percentage describes this slice.
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.
1941
2190
 
1942
2191
  | centrs class | projects to | agree | disagree | unprojected | of decided |
1943
2192
  | ------------ | ----------- | ----: | -------: | ----------: | ---------: |
1944
- | `comment` | `comment` | 18,359 | 0 | 0 | 100.00% |
1945
- | `variable-local` | `variable-local` | 1,123 | 0 | 0 | 100.00% |
2193
+ | `comment` | `comment` | 17,530 | 0 | 0 | 100.00% |
2194
+ | `variable-local` | `variable-local` | 1,547 | 0 | 0 | 100.00% |
1946
2195
  | `variable-global` | `variable-global` | 582 | 0 | 0 | 100.00% |
1947
- | `variable-auto` | `variable-auto` | 92 | 0 | 0 | 100.00% |
1948
- | `variable-parameter` | `variable-parameter` | 459 | 0 | 0 | 100.00% |
1949
- | `dir` | `dir` | 1,677 | 0 | 0 | 100.00% |
1950
- | `cmd` | `cmd` | 2,244 | 0 | 0 | 100.00% |
1951
- | `arg` | `arg`, `arg-dot`, `arg-scope` | 2,188 | 0 | 0 | 100.00% |
1952
- | `arg-sep` | `syntax-meta` | 382 | 0 | 0 | 100.00% |
1953
- | `operator` | `syntax-meta` | 237 | 2 | 0 | 99.16% |
1954
- | `brace` | `syntax-meta` | 211 | 0 | 0 | 100.00% |
1955
- | `string` | *abstains* | 0 | 0 | 499 | — |
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 | — |
1956
2205
  | `escaped` | `escaped` | 418 | 0 | 0 | 100.00% |
1957
- | `value` | *abstains* | 0 | 0 | 144 | — |
1958
-
1959
- | outcome | bytes @7.24.2 | bytes @7.23.5 | bytes @7.25beta3 |
1960
- | ------- | ----: | ----: | ----: |
1961
- | agree — both decided, projection accepts | 27,972 | 27,218 | 27,972 |
1962
- | disagree — both decided, projection rejects | 2 | 2 | 2 |
1963
- | unprojected — no declared projection covers the pair | 643 | 633 | 643 |
1964
- | offline-silent — device decided, centrs abstained | 4,300 | 4,150 | 4,300 |
1965
- | non-syntax — the device answered something syntax cannot decide | 847 | 733 | 847 |
1966
- | device-silent — centrs decided, device said `none` | 5,815 | 5,674 | 5,815 |
1967
- | both-silent | 3,886 | 3,702 | 3,886 |
1968
- | parser-stopped — at/after the device's `error` byte, and silent from there | 41,968 | 43,417 | 41,968 |
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,300 bytes at 7.24.2,
1972
- the next fill's target list): `syntax-meta` 3,122, `comment` 355,
1973
- `variable-local` 326, `arg` 197, `escaped` 145, `dir` 66, 89 across the rest.
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.
1974
2223
  Most of the `syntax-meta` share is whitespace the device merged into an
1975
2224
  adjacent structure run rather than a token centrs missed.
1976
2225
 
@@ -1982,14 +2231,14 @@ every measured byte, the post-`error` tail included: where one capture stops
1982
2231
  parsing and the next does not, that tail IS the version disagreement, which is
1983
2232
  why it dominates the category.
1984
2233
 
1985
- | applicability @7.24.2 | bytes | leading cells |
2234
+ | applicability @7.24.5 | bytes | leading cells |
1986
2235
  | ---------------------------- | ----: | ------------- |
1987
- | offline-decidable | 32,002 | `comment` → `comment` 18,006, `unclassified` → `syntax-meta` 2,999, `cmd` → `cmd` 2,132 |
1988
- | schema-dependent | 326 | `arg` → `obj-inactive` 326 |
1989
- | state-dependent | 168 | `dir` → `obj-inactive` 120, `cmd` → `obj-inactive` 21, `unclassified` → `obj-dynamic` 20 |
1990
- | version-dependent | 1,158 | `comment` → `comment` 353, `unclassified` → `obj-inactive` 159, `unclassified` → `syntax-meta` 123 |
1991
- | uncategorized | 213 | `unclassified` → `obj-inactive` 90, `unclassified` → `variable-undefined` 75, `value` → `variable-undefined` 21 |
1992
- | no-device-answer | 51,662 | `none` where the captures agree, wherever it falls |
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 |
1993
2242
 
1994
2243
  One representative run per cell, so B5 reads a fragment rather than a count.
1995
2244
  `runs` is how often that exact fragment occurs; the location is a slice script
@@ -1998,9 +2247,8 @@ and a byte offset into the stream the device saw.
1998
2247
  | outcome | cell | fragment | runs | first at |
1999
2248
  | ------- | ---- | -------- | ---: | -------- |
2000
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 |
2001
- | unprojected | `string` → `syntax-meta` | `"\""` | 311 | `eworm/ppp-on-up.rsc` @435 |
2002
- | unprojected | `value` → `syntax-meta` | `"\""` | 88 | `forum/amm0/topic-153357-using-wifiwave2-to-bridge-two-audience-wirelessly-thoughts-4-address-mode/post-0001-snippet-01.rsc` @1436 |
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 |
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 |
2004
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 |
2005
2253
  | unprojected | `string` → `dir` | `":"` | 1 | `forum/amm0/topic-155978-zerotier-on-mikrotik-a-rosetta-stone-v7-1-1/post-0001-snippet-01.rsc` @1793 |
2006
2254
  <!-- END GENERATED highlight-agreement -->
@@ -2090,9 +2338,10 @@ the 16,526 bytes `arg-sep` claims, one per attribute. Token count rises 214,503
2090
2338
  → 231,026 because one run became two. **This is a retag.**
2091
2339
 
2092
2340
  (Those totals are the corpus as it stood for THIS measurement. The classified
2093
- total is 1,023,107 today: #347/#348 later classified 8 further bytes, and #355's
2341
+ total is 1,022,805 today: #347/#348 later classified 8 further bytes, #355's
2094
2342
  fail-closed symbol stop then unclaimed 306 of them — variable bytes sitting
2095
- after a bare apostrophe, which the device never reaches. The identity being
2343
+ after a bare apostrophe, which the device never reaches — and #211's unlisted
2344
+ bare-path abstention unclaimed 302 more. The identity being
2096
2345
  claimed here is between the two sides of the split, not with any later total —
2097
2346
  restating it against today's figure would credit this retag with a movement a
2098
2347
  different change made. The generated block above carries the current numbers.)
@@ -2180,7 +2429,7 @@ bytes came from a fill that already held it — `string` gives up 17,672 and
2180
2429
  last decimal**, 1,023,405 of 1,426,731 (71.73076073906013%), so in byte terms
2181
2430
  this is a retag even though it took a new walk to make — again, the identity is
2182
2431
  between this change's own before and after, and the corpus total has since moved
2183
- to 1,023,107 / 241,539 tokens under #347/#348 and #355. Only the token count
2432
+ to 1,022,805 / 241,442 tokens under #347/#348, #355 and #211. Only the token count
2184
2433
  moves, 231,026 → 241,663: the 7,408 new `escaped` tokens plus the 3,229
2185
2434
  fragments they leave behind when they split a run they sit inside (`string`
2186
2435
  +2,643, `value` +586). That second number is well under one per escape because
@@ -2450,31 +2699,65 @@ config genre in question). Since phase 0 the corpus has grown to 948 with a
2450
2699
  export-banner share to 4.6%; **that stratum is not part of any phase-0 figure
2451
2700
  above**. Current whole-corpus offline censuses include its source text.
2452
2701
 
2453
- The pinned snapshot carries **three complete builds** — long-term `7.23.5`,
2454
- stable `7.24.2` and development `7.25beta3` — each with *both* oracles over all
2455
- 948 scripts, alongside six partial captures (parseIL at 7.20.8, 7.22.1,
2456
- 7.23rc1; highlight at 7.9.2, 7.23.2, 7.24rc2) that cover only 913. Schema v4
2457
- names that split rather than leaving it to be discovered by hand: the
2458
- `v_version_coverage` view classifies each build, and
2459
- `coverage_class = 'complete'` is the filter every version-comparable query uses.
2460
- `source_scripts` is byte-identical across both repins that reached this
2461
- snapshot, so every text-derived report — the token and value censuses,
2462
- `explain:arg-reach`, the browser-consumer proof — is unchanged by construction,
2463
- and only the IL-derived operator census moved.
2464
-
2465
- **The three complete builds do not agree with each other, and that is the
2466
- finding.** Scored over all 948 scripts:
2467
-
2468
- | oracle | long-term 7.23.5 ↔ stable 7.24.2 | stable 7.24.2 ↔ development 7.25beta3 |
2469
- | ------ | -------------------------------: | ------------------------------------: |
2470
- | `:parse` IL (`il_sha256`) | 87 scripts differ | 11 |
2471
- | `highlight` (count + class set) | 33 | 0 |
2472
-
2473
- Stable and development are the same build for these purposes; long-term is the
2474
- one that moves. So an operator-axis or token-partition figure is a claim *about
2475
- a channel*, not about RouterOS — which is why the census reports its builds as
2476
- separate columns and states its version set, rather than collapsing them to one
2477
- number. Long-term is also the channel a real device most likely runs.
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.
2478
2761
 
2479
2762
  The corpus itself is not in this repo and is not moving here — `lsp-routeros-ts`
2480
2763
  owns producing snapshots, centrs owns which snapshot it measures against.
@@ -2544,10 +2827,27 @@ to mention. `/system/reboot` no longer moves the context, so a statement after
2544
2827
  it resolves where it actually is; `verbsplit` and `pathresolve` agree, which
2545
2828
  they would otherwise now do *confidently* in opposite directions. What is left
2546
2829
  is the honest residue of both floors — `/terminal/cuu` is a real no-argument
2547
- command MikroTik publishes nowhere — and it stays pinned as a KNOWN LIMIT
2548
- fixture rather than assumed closed. The contradiction that #202 would have
2830
+ command MikroTik publishes nowhere. The contradiction that #202 would have
2549
2831
  rendered is gone.
2550
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
+
2551
2851
  **The command axis also outranks the punctuation guess.** `splitRun`'s last
2552
2852
  rule reads the first space-separated token as the verb, which lands on the
2553
2853
  operand whenever the command itself was written slash-joined:
@@ -2677,8 +2977,8 @@ The offline capability can advance from `coded` to `verified` when:
2677
2977
  - **Reachable evidence and explicit residue:** changed RouterOS semantic claims
2678
2978
  have committed CHR-grounded controls and a reproducible capture/replay path.
2679
2979
  Every remaining issue has a stated scope and dependency; #225 distinguishes
2680
- offline value work from live types, #272 owns evidence reachability, and #211's
2681
- unlisted-path decision retains the current abstention until separately decided.
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.
2682
2982
 
2683
2983
  Retain the existing checks: `bun run lint`, `bun run test`, `bun run build`, and
2684
2984
  `bun run lint:ci`, plus the affected CHR grounding/integration procedure from