@tikoci/centrs 0.1.9-next.23 → 0.1.9-next.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/GLOSSARY.txt +14 -0
  3. package/README.md +2 -2
  4. package/commands/api/AGENTS.md +18 -1
  5. package/commands/api/README.md +10 -6
  6. package/commands/api/examples.md +1 -1
  7. package/commands/execute/README.md +26 -6
  8. package/commands/execute/examples.md +24 -5
  9. package/commands/explain/README.md +452 -61
  10. package/commands/explain/examples.md +191 -41
  11. package/commands/retrieve/README.md +13 -2
  12. package/commands/retrieve/examples.md +30 -1
  13. package/commands/transfer/README.md +8 -4
  14. package/docs/CLI.md +19 -5
  15. package/docs/CONSTITUTION.md +14 -9
  16. package/docs/MATRIX.md +34 -21
  17. package/docs/errors/cmr/caller-policy-not-enforced.md +5 -3
  18. package/docs/errors/routeros/permission-denied.md +24 -0
  19. package/docs/errors/transport/edge-auth-failed.md +22 -0
  20. package/docs/errors/transport/write-outcome-unknown.md +3 -0
  21. package/docs/errors/usage/invalid-cursor.md +20 -0
  22. package/docs/errors/validation/unknown-path.md +9 -0
  23. package/package.json +2 -2
  24. package/src/api.ts +34 -39
  25. package/src/cli/explain.ts +206 -47
  26. package/src/core/envelope.ts +10 -3
  27. package/src/core/error-catalog.ts +15 -0
  28. package/src/core/execute-command.ts +53 -0
  29. package/src/core/fanout.ts +12 -2
  30. package/src/core/inspect.ts +4 -5
  31. package/src/core/routeros-errors.ts +25 -0
  32. package/src/execute-cmr.ts +2 -8
  33. package/src/execute.ts +48 -20
  34. package/src/explain/args.ts +115 -10
  35. package/src/explain/group-faults.ts +162 -29
  36. package/src/explain/operators.ts +2 -1
  37. package/src/explain/scope-brace.ts +226 -11
  38. package/src/explain/segment.ts +136 -1
  39. package/src/explain/symbols.ts +146 -6
  40. package/src/explain/values.ts +55 -4
  41. package/src/explain-live-facets.ts +739 -0
  42. package/src/explain-live-types.ts +490 -0
  43. package/src/explain-live.ts +593 -0
  44. package/src/explain.ts +395 -43
  45. package/src/index.ts +6 -0
  46. package/src/protocols/adapter.ts +88 -10
  47. package/src/protocols/mac-telnet.ts +38 -10
  48. package/src/protocols/native-api.ts +13 -12
  49. package/src/retrieve-follow.ts +2 -1
  50. package/src/retrieve-sample.ts +2 -1
  51. package/src/retrieve-wait.ts +10 -6
  52. package/src/retrieve.ts +77 -27
  53. package/src/transfer.ts +38 -13
@@ -10,12 +10,11 @@ validation): the cheap, safe knowledge tier in front of the runners
10
10
  (`execute` / `api` / `retrieve`).
11
11
 
12
12
  Status: offline baseline `verified`; live probes over `rest-api` and `native-api`
13
- `designed`. **`centrs explain '<input>'` runs today**, without a router or CDB.
13
+ `CHR-passed`. **`centrs explain '<input>'` runs without a router or CDB**, and
14
+ `centrs explain <router> '<input>'` adds read-only device evidence.
14
15
  [`docs/MATRIX.md`](../../docs/MATRIX.md#offline-analysis) tracks these separately;
15
16
  [#90](https://github.com/tikoci/centrs/issues/90) indexes the remaining tasks.
16
17
  The spec is ratified on the [phase-0 grounding lab](#phase-0-ratification-185).
17
- The current finish line is [Offline baseline acceptance](#offline-baseline-acceptance),
18
- not completion of the live protocol cells.
19
18
  Load-bearing rules — envelope, errors, settings precedence, identity,
20
19
  validation, protocol selection — live in
21
20
  [`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md).
@@ -373,9 +372,8 @@ native API's `/execute` **with `as-string=""`** returns the parser text —
373
372
  including the `(line N column M)` diagnostic on a syntax error — byte-identical
374
373
  to what a console transport prints. The opaque `*NN` value seen otherwise is the
375
374
  **no-`as-string` job handle**, not a `:parse` limitation; the readout rule is
376
- "pass `as-string` and interpret `ret`". The existing execute gate already passes
377
- `as-string` but currently discards `ret`; phase 2 must extract/fix that shared
378
- readout rather than copy the omission. `/console/inspect`
375
+ "pass `as-string` and interpret `ret`". The execute gate passes
376
+ `as-string` and classifies `ret`; live explain reuses that shared classifier. `/console/inspect`
379
377
  (highlight/syntax/completion) works over the native API's `talk` framing too. So
380
378
  the line/column diagnostic is available on both live cells — the earlier caveat
381
379
  that only a console transport surfaces the parser text is retired.
@@ -401,10 +399,8 @@ router therefore never reinterprets a previously valid offline invocation.
401
399
  the offline form with nothing else to analyze. A positional always wins, and
402
400
  when fd 0 is redirected anyway the result carries a
403
401
  [`usage/stdin-ignored`](../../docs/errors/usage/stdin-ignored.md) warning rather
404
- than quietly dropping the pipe. `--file -` names stdin explicitly; it is the
405
- offline fix today, since the live form it also spells
406
- (`… | centrs explain edge1 --file -`) is parsed and then refused with
407
- `usage/not-implemented` until phase 2. The narrower rule is
402
+ than quietly dropping the pipe. `--file -` names stdin explicitly in both forms,
403
+ including live `… | centrs explain edge1 --file -`. The narrower rule is
408
404
  deliberate and measured: `runCli` is called in-process by tests, so reading fd 0
409
405
  on any path a positional invocation reaches consumes the invoking shell's
410
406
  stdin — it fed those bytes to the analyzer and broke 21 unrelated tests once,
@@ -652,7 +648,7 @@ and its phase is named below.
652
648
  | `structure.statements[].command` | `path` + `verb`, plus **`args` where the argument list was read** (#202c-1); the ordered token list is `statements[].arguments.tokens` |
653
649
  | `structure.statements[].transport` | complete for commands: `api-candidate`, `execute`, or fail-closed `unknown`, with an equivalent `centrs` invocation and opt-in `curl` for tested REST mappings (#202c-2). The `basis` names the evidence for the route it chose or refused |
654
650
  | `symbols.occurrences[]` | Q13 name/span/class plus semantic `role`, result-local `bindingIds`, sigil spelling, and an abstention note where needed; **plus** (#239 S2) `valueId` on declarations/assignments with a literal RHS in the same statement, and `reachingValueIds` + `reachingUnknown` on references (flow-sensitive, branch → set, loop/non-literal → `unknown`, no RHS evaluation) |
655
- | `values.occurrences[]` | result-local value id/span plus three separate fact homes: offline `shapeHints`; live `observedType` and `schemaType` remain absent until phase 2. `kind: "element"` rows are array members, carrying `parent` and, for a keyed member, `name` |
651
+ | `values.occurrences[]` | result-local value id/span plus three separate fact homes: offline `shapeHints`; live `observedType` and `schemaType` are added only by a live call ([Live value types](#live-value-types-225)). `kind: "element"` rows are array members, carrying `parent` and, for a keyed member, `name` |
656
652
  | `spans` | comment runs and resolved variable classes only; **no value shape or type** |
657
653
  | `schema`, `completion` | absent — live evidence, phase 2 |
658
654
 
@@ -744,13 +740,10 @@ and its phase is named below.
744
740
  rejected by RouterOS at `{` and resolve no reference. In an expression the
745
741
  remaining `-dns` is scanned as subtraction plus its right operand; inside a
746
742
  string it is literal text.
747
- - **Evidence is offline-shaped, not the whole contract.** The bullet above says
748
- an evidence entry carries `source` (`canonicalizer` vs `live-inspect`) and a
749
- RouterOS version stamp; phase 1 emits neither the live source nor the stamp,
750
- because no probe ran and there is no device to stamp. `ExplainEvidence` is
751
- typed to what offline produces rather than pre-declaring variants nothing can
752
- emit, so phase 2 widens the union and adds the stamp — an addition, not a
753
- change to what a caller reads today.
743
+ - **Evidence follows its producer.** Offline calls emit only `canonicalizer`
744
+ entries, with no device version. Live calls add `live-inspect` entries with
745
+ probe outcomes and available device provenance; existing offline entries
746
+ retain their meaning. See [Live probe foundation](#live-probe-foundation-491).
754
747
  - **Value facts are three axes, never one `type` string (#225).** RouterOS
755
748
  types values at parse time while `highlight` classes every value byte `none`,
756
749
  so type evidence is not a subset of Q12 spans. Each result-local
@@ -763,7 +756,7 @@ and its phase is named below.
763
756
  The stable/testing CHR matrix fixes the offline lexicon: `num`, `ip`,
764
757
  `ip-prefix`, `ip6`, `ip6-prefix`, `id`, `time`, `array`, `mac`, `bool`, and
765
758
  `str` — `op` is deliberately not a member: a deferred `(> …)` abstains
766
- offline and is reported only as a live `observedType` (#288); quoted literals
759
+ offline and live alike (#288, #225); quoted literals
767
760
  hint only `str`, while malformed and out-of-range address-like controls
768
761
  abstain even in named attributes. Most vocabulary borrows RouterOS scripting
769
762
  type names (with the explicit `macAddr` schema exception below), so a hint
@@ -797,8 +790,9 @@ and its phase is named below.
797
790
  1 and identical `:parse` IL — only `:typeof` distinguishes them
798
791
  (`test/fixtures/explain/operators.json` → `sweep.runtime`
799
792
  `typeof-deferred`/`deferred-array-typeof` and `sweep.opAxis` `defer-*` on
800
- 7.21.5, 7.23.3, 7.24rc4). Offline abstains on every such form; `op` lives
801
- only as a live `observedType` (#288, a bounded #225 decision).
793
+ 7.21.5, 7.23.3, 7.24rc4). Offline abstains on every such form, and so does
794
+ the live `observedType`, which never sends `:typeof` (#288, #225). `op` has
795
+ no producer.
802
796
  Shape remains non-authoritative: `100000w` is time-shaped but observed as
803
797
  `str`. Argument context remains separate: on 7.23.3 and 7.24rc3, both
804
798
  `/ip/address address=2.2` and firewall `src-address=2.2` canonicalize it to
@@ -812,8 +806,8 @@ and its phase is named below.
812
806
  and does not widen the hint. Thus a hint never validates a value or becomes a
813
807
  diagnostic, and never changes `runtimeAcceptance`. Quoting likewise never
814
808
  earns a wrong-type diagnostic: commands may cast quoted strings at runtime.
815
- Offline emits only `shapeHints`; phase 2 adds the two live facts into their
816
- existing homes when its safe IL/schema producers land.
809
+ Offline emits only `shapeHints`; a live call adds the two live facts into
810
+ their existing homes ([Live value types](#live-value-types-225)).
817
811
 
818
812
  **A located array is read member by member (#225 V1).** Each member of a
819
813
  `{…}`/`(…)` literal is its own `values.occurrences[]` row with `kind:
@@ -1128,8 +1122,9 @@ navigated, so the context stays as unknown as it already was.
1128
1122
  `empty-group`, `misplaced-separator`, `missing-operand` and
1129
1123
  `invalid-at-sign` (#505, #511, #512, below), and the four token faults
1130
1124
  `glued-token`, `invalid-variable`, `bad-string-variable` and
1131
- `invalid-name` (#506, #518, below). Seventeen classes the device itself
1132
- rejects.
1125
+ `invalid-name` (#506, #518, below), and the two unquoted-byte faults
1126
+ `unquoted-reserved-byte` and `unquoted-non-ascii` (#358, #371, below).
1127
+ Nineteen classes the device itself rejects.
1133
1128
  - `warning` — `over-depth`, because it is centrs's own resource bound and says
1134
1129
  nothing about whether the input is legal; `possible-spaced-argument`
1135
1130
  (#350), a spelling RouterOS accepts and marks obsolete; `discarded-close`
@@ -1326,8 +1321,9 @@ query and brace roles. The `$` inside a string is checked by the escape walk
1326
1321
  (`src/explain/quoted-string.ts`), and the stray close by the segmenter
1327
1322
  (`src/explain/segment.ts` → `discardFrom`). Every rule is a `:parse` answer on
1328
1323
  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`
1324
+ `test/fixtures/explain/token-faults.json` pins these rows and the unquoted-byte
1325
+ rows below: 292 in all (184 rejections and 108 acceptances, with the
1326
+ acceptances' IL). `test/integration/explain-token-faults.test.ts`
1331
1327
  holds the device to that fixture, and `test/unit/explain-token-faults.test.ts`
1332
1328
  holds offline to it.
1333
1329
 
@@ -1379,11 +1375,59 @@ variable name, so the next `"` does not close the string. Every scanner in
1379
1375
  `explain` closes it there, so both `$` rules and the glued-token rule abstain
1380
1376
  around a string that ends in `$"` (2 of 948 corpus scripts use the form).
1381
1377
  `$$`, `$(…)`, `$[…]` 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
1378
+ not the `$`. A `\` continuation inside a glued token (`"a"\⏎"b"`). A glued
1379
+ head word (`$v"a"`, `{…}:put 2`). `:local a=b`, which 7.24.5 reads as
1384
1380
  a named argument and 7.23.8 as a syntax error. And a name in `/local` or
1385
1381
  `:local name=a_b`.
1386
1382
 
1383
+ ### Unquoted bytes (#358, #371)
1384
+
1385
+ A `?`, a backtick and a non-ASCII byte are never part of a bare word, and
1386
+ neither is a second `=` in a menu command's argument. The rules ride the
1387
+ token walk (`src/explain/group-faults.ts`). They were grounded with live
1388
+ explain (`explainLiveEnvelope`: `:parse` and `highlight`) on CHR 7.24.5 and
1389
+ 7.23.8, which answered identically. The 147 rows are in
1390
+ `test/fixtures/explain/token-faults.json`.
1391
+
1392
+ | Code | Device rule | Rejected | Accepted |
1393
+ | ---- | ----------- | -------- | -------- |
1394
+ | `unquoted-reserved-byte` | `?` and a backtick, at the byte, outside a string or comment. A second `=` in a menu command's argument, at that `=` | `name=a?b`, ``name=a`b``, `name=?b`, `:put a?b`, `[:len a?b]`, `where comment=a?b`, `[find comment=a?b]`, `where (comment=a?b)`, `where comment in {a?b}`, `{a?b}`, `(a?b)`, `/ip/address/print?`, `name=a=b`, `name==b`, `comment=x==`, `url=http://x/a?b=c`, `regexp=^a?b`, `public-key=…=`, `secret=abc==`, `on-event=a=b` | `name="a?b"`, ``name="a`b"``, `name="a=b"`, `where comment=a=b`, `[find comment=a=b]`, `[/ip/address/find comment=a=b]`, `where (comment=a=b)`, `where comment in {"a?b"}`, `{a=b}`, `{"a?"=1}`, `# a?b`, `("a"."?")` |
1395
+ | `unquoted-non-ascii` | A non-ASCII byte outside a string or comment, at its first byte; the span is the whole run | `:put café`, `$café`, `name=router-café`, `name=🛰`, `/systemé/identity`, `printé`, `namé=x`, `where comment=café`, `{café}`, `(café)`, `"x"é`, a non-breaking space, `“x”`, `address=…` | `:put "café"`, `:local "café" 1`, `$"café"`, `"a$café"` (`$caf` then `é`), `name="router-🚀"`, `# café`, `do={:put "café"}` |
1396
+
1397
+ **Where the device reports, offline reports.** In a menu command's argument
1398
+ name the device names the name's first byte (`/ip/address/add addré=1` and
1399
+ `a?b=1` at the `a`), so the span is the name. A directive (`:put café=1`) and
1400
+ a `$` call (`$f café=1`) report at the byte. A declared name keeps the
1401
+ existing `invalid-name` (`:local café`, `:foreach café`). A query, and a
1402
+ group or array inside one, keeps `=` as an operator but not these bytes
1403
+ (`where (comment=café)` and `in {café}` are rejected at the `é`), and the slash-joined `[/ip/address/find …]` is a query, as
1404
+ `[find …]` is. A leading BOM is a positional fact and is not read as part of
1405
+ the first word. The informational `non-ascii` fact is still published beside
1406
+ the error, so `isPositionalFact("non-ascii")` is unchanged.
1407
+
1408
+ **No property parser takes these bytes.** This is the difference from the
1409
+ glued-token rule. `regexp=a?b` (layer7 and DNS static), ``regexp=a`b``,
1410
+ `public-key=<base64>=`, `secret=abc==`, `password=abc=`,
1411
+ `http-header-field=a:b=c` and the script bodies `on-event=a=b` and
1412
+ `up-script=a=b` are all rejected at the byte.
1413
+
1414
+ **Measured.** Corpus `e12d5d4e58a4`: 8 scripts move to `fail` (7 from `warn`,
1415
+ one from `pass`), and 20 already-failing scripts gain a new code. All 28 are
1416
+ device rejections on every channel. 8 of their first new errors sit on the
1417
+ 7.24.5 diagnostic's byte, 20 come after it, and none precedes it. There are no
1418
+ false rejects. On the conformance cases, 10 missed rejections move to `fail`
1419
+ (the `non-ascii-outside-strings` u10–u12 and u14–u16, z90, z92, g18 and e33), with no new
1420
+ false reject.
1421
+
1422
+ **Not covered here.** `:put a=b`, `:local x a=b` and `[:len a=b]`: 7.24.5
1423
+ reads them as a named argument (`bad parameter a`, at the end), and 7.23.8
1424
+ rejects the `=`. A query value that starts with `=` (`where comment==b`,
1425
+ `expected comment value`). A bare `source=` value,
1426
+ which the device rejects at its first byte whatever it holds (`source=abc`), so
1427
+ a byte rule there reports after the device's byte. And `:set` of an undeclared
1428
+ name, which the device rejects at the name's first byte (`:set zzz 2`), so
1429
+ `:set café` is reported at the `é`, after it.
1430
+
1387
1431
  ### The token stream a rule reads (#316)
1388
1432
 
1389
1433
  `lexArguments` is all-or-nothing by design: its consumer renders a runnable REST
@@ -1599,15 +1643,34 @@ catalog-named command, with a `:` scripting directive, or with an absolute
1599
1643
  command — corpus script `forum/amm0/topic-184067-routeros-lsp-for-better-syntax-checking-command-completion-in-editors-like-vscod/post-0014-snippet-02.rsc` is the directive case,
1600
1644
  `ethernet { :put … }`.
1601
1645
 
1602
- That last rule is bounded by what a STATEMENT can start with, which is what
1603
- keeps `{1;2}` an array literal: an array member opens with a digit or a quote
1604
- and never reaches the identifier branch. It is also deliberately not widened
1605
- into a claim of existence. Menu-scope resolution is schema-dependent —
1606
- `/ip { zzznotamenu { print } }` is a `syntax error` on device — and offline has
1607
- no schema, so a catalog MISS abstains rather than being decided either way.
1608
- That is the catalog's own "a hit is decisive, a miss says nothing" contract, and
1609
- turning such a miss into an acceptance would be the mirror-image defect of the
1610
- one being fixed here.
1646
+ Issue #348 only stopped the failure. It still read `address { … }` as a command
1647
+ `/ip address` whose operand was the brace, so the body was never segmented.
1648
+ That rule was bounded by what a statement can start with, on the theory that
1649
+ `{1;2}` there is an array. The device says otherwise (#351, CHR 7.24.5 and
1650
+ 7.23.8): a brace right after a menu word is a statement block whatever the body
1651
+ holds. `/ip { address { 1;2 } }` is `bad command name 1` at the `1`, and
1652
+ `/ip { address { a;b } }` runs `a` as `add`. The old reading failed the OUTER
1653
+ brace at depth three, and passed `print {1;2}` and `print; zzz` inside the body.
1654
+
1655
+ So a relative run inside a menu block now opens a block of its own when the
1656
+ menu it names is known: the enclosing block's path, read outward from the
1657
+ source, plus the run, named by a shipped structure table
1658
+ (`scope-brace.ts` → `menuBlockPath`). The segmenter, the comment mask, the
1659
+ argument lexer and the group-fault walk all ask it. The body is then read like
1660
+ `/ip/address { … }`: `print {1;2}` there fails at its own `{`, the device's
1661
+ byte, and an unresolvable body statement abstains at every depth.
1662
+
1663
+ It is deliberately not widened into a claim of existence. Menu-scope
1664
+ resolution is schema-dependent — `/ip { zzznotamenu { print } }` is a
1665
+ `syntax error` on device — and offline has no schema, so a catalog MISS
1666
+ abstains rather than being decided either way. That is the catalog's own "a
1667
+ hit is decisive, a miss says nothing" contract. Three spellings keep the older
1668
+ reading because the source alone does not name the menu: a block after a
1669
+ navigation statement (`/ip⏎address { … }`), an abbreviation (`addr {`) and a
1670
+ digit-led menu (`6to4 {`, which the resolver does not read as a path). After a
1671
+ navigation, `/ip⏎address { 1;2 }` still fails at the outer brace and
1672
+ `/ip⏎address { print {1;2} }` still passes; the device answers
1673
+ `bad command name 1` and `expected end of command` at the inner brace.
1611
1674
 
1612
1675
  ### The `=` is optional in ordinary arguments too (#350)
1613
1676
 
@@ -1650,6 +1713,36 @@ device `syntax error` that offline does not report. That is a missed
1650
1713
  rejection for the device preflight to catch, never a false reject. On the
1651
1714
  pinned corpus the widening changed no census.
1652
1715
 
1716
+ ### First-segment script-body abbreviations warn instead of rejecting (#525)
1717
+
1718
+ RouterOS takes any unique prefix of an argument name, including the first
1719
+ segment: `down={ :beep }` lowers to `down-script=` on CHR 7.22.1–7.25, as do
1720
+ bodies opening with one bare command word (`down={ beep }`) or after a comment
1721
+ line. Every word is a prefix of `<word>-script`, so without the command's
1722
+ argument list (schema) offline cannot tell `down=` from `comment=`, which the
1723
+ device rejects. The contract: on a non-root command, outside `where`, an
1724
+ unproven `name={` with a script-like body warns
1725
+ `explain/canonicalizer/possible-script-body-abbreviation` (verdict `warn`, the
1726
+ offline gate lets it through) instead of failing with `invalid-command-brace` —
1727
+ glued or with the brace spaced off the `=` (`down= { :beep }` lowers to
1728
+ `down-script=` on CHR 7.25 exactly like the glued spelling, so the spaced
1729
+ owner warns under the same gates). The brace reads as a body for every
1730
+ reader — a `#` inside is a comment, not `invalid-hash` — while proven slots
1731
+ (`down-script=`, `s=`, `on-…`) stay silent and control scopes (`do={…}`) are
1732
+ never guesses. A bare word followed by `;`-separated members is array
1733
+ members, not commands, so `.proplist={name;comment}` keeps its error alongside
1734
+ the other array-content braces (`{1;2}`, `{"x"}`, `{}`), the positional form,
1735
+ and `where`. A root directive's slots (`:put x={…}`, glued or spaced) and a `where` named
1736
+ comparison (`where gateway={…}`, however wrapped) keep their error too: the
1737
+ slot table and the property parser decide there, so the abbreviation guess
1738
+ never shields them (#554). The integration twin pins both device directions.
1739
+
1740
+ The tradeoff is measured: a verdict sweep of the 948-row pinned corpus
1741
+ population against the pre-batch baseline reports zero verdict or diagnostic
1742
+ changes, so nothing in the corpus pays for the abstention. The one deliberate
1743
+ over-warn — `comment={ :beep }` warns though the device rejects it — has no
1744
+ corpus occurrence.
1745
+
1653
1746
  ### `'` is not a RouterOS token (#355)
1654
1747
 
1655
1748
  `'` (U+0027) is not a string delimiter and not a legal bare byte. Only `"` opens
@@ -1986,8 +2079,8 @@ And the grounded complement — asked, and refused:
1986
2079
  `corpus` by `bun run explain:token-census:readme` and gated against it by
1987
2080
  `bun run explain:token-census:readme:check`; the fixture itself is gated
1988
2081
  against a fresh corpus run by `bun run explain:token-census:check`. Of
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
2082
+ 1,426,731 analyzed bytes, 1,022,786 are classified (71.69%), the remaining
2083
+ 403,945 are `unclassified`. The census emits 241,282 tokens (avg 254.5 per
1991
2084
  script). Every byte belongs to exactly one token — sorted by `start`, no
1992
2085
  gaps, no overlaps, `join(slice) === input` — and the `class` field is the
1993
2086
  vocabulary #264 B5 settled. Each B2 fill should move the classified
@@ -2468,22 +2561,317 @@ last byte. The `$` sigil and the `[` / `]` of a substitution stay `string` on
2468
2561
  the same ground — `structure.subcommands` already publishes the resolution, and
2469
2562
  its `innerSpan` locates the brackets around it.
2470
2563
 
2471
- ### Designed, not implemented (the CLI surface, #202b)
2472
-
2473
- `src/cli/explain.ts` ships the offline command; implemented flags are generated
2474
- into [`docs/CLI.md`](../../docs/CLI.md) and are not restated here. What the
2475
- [Surface](#surface-option-a--decided) table above describes and the command does
2476
- **not** yet accept:
2477
-
2478
- | Flag / form | Phase | Why it is not accepted yet |
2479
- | ----------- | ----- | -------------------------- |
2480
- | `explain <router> '<input>'` | 2 | The grammar is parsed and **refused** with `usage/not-implemented`, naming the router it did not contact. Running the offline analysis under an invocation that asked for probes would report `mode: "offline"` about a request for more. |
2481
- | `--cursor <byte>` | 2 | It positions `--complete`, which enumerates nothing offline. A cursor with no candidate surface to position is a flag with no effect. |
2482
- | `--full` | 3 | It lifts smart-sizing limits, and the offline result is never truncated (`input.truncated` is always `false`). Same rule: no accepted flag is a no-op. |
2483
-
2484
- `--complete` and `--schema` **are** accepted, because they have an offline
2485
- behavior the spec names: they enumerate nothing and emit the live-target tip.
2486
- That is the difference — an accepted flag must do something observable.
2564
+ ### Live probe foundation (#491)
2565
+
2566
+ `centrs explain <router> '<input>'` (or `--quickchr <name>` / `--host <host>`)
2567
+ adds read-only device evidence over REST or native API. Library callers use
2568
+ `explainLiveEnvelope({ input, targetInput, via, ... }, options)` from the root
2569
+ package; the browser-safe `@tikoci/centrs/explain` entry remains offline.
2570
+ Connection/authentication settings use the existing resolver and provider
2571
+ precedence. A pinned unsupported transport is refused. No fan-out or protocol
2572
+ fallback is performed. `AbortSignal` cancels library probes.
2573
+ An explicitly supplied connection flag such as `--via`, `--password` or
2574
+ `--timeout` without `<router>`, `--host` or `--quickchr` is a usage error before
2575
+ input is read. Connection defaults from environment and config remain inert in
2576
+ offline mode.
2577
+ Text output summarizes live observations; `--json` retains full parse and
2578
+ per-byte highlight evidence.
2579
+
2580
+ `data.canonical`, structure, symbols, values and optional tokens retain the
2581
+ offline result. Live observations are additional facts:
2582
+
2583
+ - `data.live.environment` records the observed version, architecture and board.
2584
+ - `data.live.parse` carries the printed parse result, rejection classification
2585
+ and authoritative line/byte-column when reported. Only an escaped
2586
+ `:put [:parse "…"]` wrapper runs; the analyzed input, substitutions and
2587
+ deferred bodies are never executed.
2588
+ - `data.live.highlight` retains raw device classes. Additional `data.spans`
2589
+ cite its evidence and use centrs classes: `dir`, `cmd`, `arg`, `escaped`,
2590
+ comments and variable classes; device state, syntax-meta, error/obsolete and
2591
+ unknown classes map to `unclassified`. Error/obsolete markers produce separate
2592
+ diagnostics. Offline and live spans can overlap; distinguish them by `ev`.
2593
+ - `data.live.arguments[]` contains `path`, `names`, `statementIds` and `ev` for
2594
+ command-level `request=child`. Repeated paths share a probe. Names confirm what
2595
+ the device exposes; they do not rewrite offline arguments, transport eligibility
2596
+ or lexical bindings. The highlight obsolete marker distinguishes `name x`
2597
+ from the two flags `detail without-paging` (example 48).
2598
+
2599
+ Every live evidence entry identifies its probe, observation time, known version
2600
+ and acquisition outcome. Input probes include SHA-256 and normalization;
2601
+ `coverage` identifies the submitted byte range for a usable response, not proof
2602
+ that every byte is syntactically valid. Parse uses original UTF-8 input;
2603
+ highlight uses the existing one-SUB-per-non-ASCII-byte normalization. Its facts
2604
+ therefore describe that explicit variant; both spaces align with `positionMap`.
2605
+ Parse can stop at its first error; highlight `none` remains unclassified.
2606
+
2607
+ Probe outcomes distinguish `ok`, `empty`, `unsupported`, `skipped`, `timeout`,
2608
+ `auth-error`, `transport-error`, `interrupted` and `invalid-response`. A parse
2609
+ rejection is an acquired response (`ok`) plus a diagnostic, not a transport
2610
+ failure. A failed/skipped probe adds a warning diagnostic; acquired offline and
2611
+ live facts remain available. `ok:true` means analysis produced a result, never
2612
+ that all probes succeeded. Resolver/usage failures and transport/authentication/interruption failures before
2613
+ any input probe yields a result remain `ok:false`.
2614
+ `meta.operation.mode` is `live`; no execution-validation verdict is invented:
2615
+ `meta.validation` is `{ enabled: false }`, offline and live, because explain
2616
+ is not a gate (#535). `runtimeAcceptance` always remains `not-proven`.
2617
+
2618
+ Work is bounded by the resolved per-request timeout, 32 distinct command paths,
2619
+ 32,767 bytes for highlight and 65,536 bytes for parse. Larger probes are skipped
2620
+ with their reason; no silent prefix is submitted and `input.truncated` stays
2621
+ false. A transport/authentication failure stops later requests. Scripting-path
2622
+ discovery is conservatively skipped before 7.21.4 or on unknown versions. No
2623
+ command-level completion, user-expression evaluation or related
2624
+ configuration-table collection is part of this slice. `--schema` and
2625
+ `--complete` add the probes in the next section, and the value-type probes
2626
+ after it run on every live call.
2627
+
2628
+ Committed controls are
2629
+ [`live-probes.stable.json`](../../test/fixtures/explain/live-probes.stable.json)
2630
+ (7.24.5) and
2631
+ [`live-probes.long-term.json`](../../test/fixtures/explain/live-probes.long-term.json)
2632
+ (7.23.8), both CHR reporting `x86_64`, with package manifests, input hashes,
2633
+ observation times, raw parse/highlight output and projected child names. Each
2634
+ fixture records its capture command. `test/unit/explain-live.test.ts` replays
2635
+ the parse classifier and source-aligned highlighter against these controls;
2636
+ `test/integration/explain-live.test.ts` repeats the probes on a real CHR and
2637
+ checks that the analyzed identity-setting command did not run.
2638
+
2639
+ ### Live describe and completion (#492)
2640
+
2641
+ `--schema` and `--complete` work on the statement at the cursor. The cursor
2642
+ defaults to the end of the input. `--cursor <byte>` is a UTF-8 byte offset,
2643
+ which is also the analyzed and device byte offset. Library callers may pass
2644
+ `cursor: { line, character }` (0-based, UTF-16) instead. A cursor inside a
2645
+ multi-byte character moves back to its first byte. A cursor outside the input
2646
+ is [`usage/invalid-cursor`](../../docs/errors/usage/invalid-cursor.md), raised
2647
+ before connecting. `--cursor` or `--full` without a facet is
2648
+ `usage/conflicting-flags`. Offline, both facets still return the live-target
2649
+ tip and enumerate nothing.
2650
+
2651
+ **Probe shapes.** Grounded on CHR 7.12.2, 7.20.8, 7.23.8 and 7.24.5 over REST
2652
+ and native API:
2653
+
2654
+ - `child` names what exists at a path and resolves unique abbreviations
2655
+ (`ip,addr` answers as `address`). It is safe on every build tried.
2656
+ - `syntax` is sent only for an **argument** path (`ip,address,set,address`,
2657
+ `put,message`). That shape answers in about 25 ms on every build, scripting
2658
+ keywords included.
2659
+ - `completion` is sent only with `input=`, the text before the cursor. It is
2660
+ safe at every position tried, including scripting keywords and `do={`
2661
+ bodies on 7.20.8.
2662
+ - Never sent: `syntax` or `completion` with `path=` set to a **command**
2663
+ (`ip,address,set`). On 7.12.2 and 7.20.8 REST stops answering, and the
2664
+ requests queued behind it stall, for about 30 s. Native API closes the
2665
+ connection. 7.23.8 and later answer normally, but the rule is
2666
+ version-independent (`commands/api/AGENTS.md`). restraml's crawlers never
2667
+ meet this, because they send `syntax` to `arg` nodes only.
2668
+
2669
+ Every probe is an `evidence[]` entry with `facet: "schema" | "complete"`, its
2670
+ path, version and outcome. Facts cite it by `ev`. A facet probe that answers
2671
+ `empty` is a result, so it adds no warning diagnostic. Other failed outcomes
2672
+ warn as in the foundation, and a transport or authentication failure stops
2673
+ later probes.
2674
+
2675
+ **`data.schema`** describes one path: the resolved statement, or an absolute
2676
+ slash path as typed (`/ip/addr`). The `child` self row decides `node`:
2677
+
2678
+ - `menu`: `verbs[]` and `menus[]`. Each child is expanded by one more `child`
2679
+ probe into `arguments` (verb) or `verbs`/`menus` (submenu) counts.
2680
+ - `command`: `arguments[]`, each with its argument-level `syntax` rows. A
2681
+ flag's blank row is dropped. For `print`, `proplist` holds the output-field
2682
+ names from `print proplist=` (`source: "print-proplist"`). A singleton
2683
+ settings menu offers none there, so `get value-name=` answers instead
2684
+ (`source: "get-value-name"`; `/ip/dns`, `/system/identity`). These are names
2685
+ a print can show, **not print rows**.
2686
+ - `argument`: its `syntax` rows.
2687
+ - `unknown`: the device reports nothing there, or the statement at the cursor
2688
+ is neither resolved nor an absolute path (`reason`).
2689
+
2690
+ Syntax rows are kept verbatim (`symbol`, RouterOS `symbol-type` as `kind`,
2691
+ `text`, `nested` as `depth`). They are declared help, such as
2692
+ `A.B.C.D (IP address)` or `input | forward | output` beside `string value`.
2693
+ They are not the `schemaType` value fact, which normalizes the same rows per
2694
+ value ([Live value types](#live-value-types-225)).
2695
+
2696
+ **Smart sizing.** By default each list shows up to 64 names, and expansion
2697
+ runs only when the listed children fit in 16 probes (a menu's children, or a
2698
+ command's arguments for their `syntax`). Otherwise names are listed without
2699
+ expansion. `counts` always holds the full sizes, `truncated: true`, `next[]`
2700
+ names up to three narrower inputs, and the envelope carries
2701
+ `tip/explain-schema-truncated`. On CHR 7.24.5, `/ip` (2 verbs, 29 menus) and
2702
+ `/ip/address/print` (21 arguments) truncate; `/ip/address` (12 verbs) and
2703
+ `/ip/firewall/filter` (16) do not. `--full` raises the limits to 1024 names and
2704
+ 128 probes. That is the safety ceiling, not unlimited.
2705
+
2706
+ **`data.completion`** reports `cursor`, `at` (original line/character),
2707
+ `replace` (the bytes a candidate replaces, ending at the cursor), `prefix`,
2708
+ `context`, `candidates[]`, `count` and `truncated`. Candidates are capped like
2709
+ names. They are the device's own: one input completion of the text before the
2710
+ cursor, keeping shown, non-syntax rows. The row `offset` sets `replace`. The
2711
+ navigation punctuation (`/`, `:`, `..`) offered everywhere is left out. So a
2712
+ relative path, a block, a `where` clause or an unclosed `[` completes exactly
2713
+ as on the console: `/ip { :put 1; address add comm` → `comment`, and
2714
+ `/ip address⏎` → that menu's verbs. Where the device offers nothing, neither
2715
+ does explain: `do={ add comm` runs at root, so `add` is unknown there.
2716
+
2717
+ The `context` comes from the device highlight of the same text. When the
2718
+ cursor is not at the end, that is one extra highlight probe whose `coverage`
2719
+ says so.
2720
+
2721
+ - `path`: in a `dir`/`cmd` word, after a menu, or at the start of a line.
2722
+ Candidates are `menu` or `verb` by row style. A word the device cannot
2723
+ resolve there is `obj-inactive`, and the token before it gives its role:
2724
+ `/ip/address re` → `remove`, `reset`, and `:lo` → `local`, `lock`, `log`.
2725
+ - `argument-name`: in an `arg` word, or after a command, flag or value.
2726
+ `match: "exact"` marks a complete name. `takesValue` is whether the same
2727
+ answer offers `=` at the cursor: `comment` does, and the flag `detail` does
2728
+ not.
2729
+ - `variable`: a `where` clause, whose property names RouterOS scopes as
2730
+ variables (`where comm` → `comment`).
2731
+ - `value`: after `name=`. Candidates may include configuration state, such as
2732
+ interface names. They are never a closed set:
2733
+ `chain=` offers `forward`, `input` and `output`, but custom chains are legal.
2734
+ For a command argument, `syntax` carries its declared type text, so a
2735
+ free-form `comment=` returns no candidates and `string value`. A `where`
2736
+ value (`disabled=` → `no`, `yes`) has no argument path, so no `syntax` is
2737
+ read.
2738
+ - `unsupported` with a `reason`: strings, the obsolete spaced `name value`
2739
+ spelling, and other tokens the device does not class as a path, name or
2740
+ value.
2741
+
2742
+ Every candidate is an observation of the completion probe its `ev` cites.
2743
+ The declared side is `data.schema`: `child` names and argument `syntax`. So
2744
+ completion carries no second provenance label, and nothing in it is a closed
2745
+ set. `path` names the command at the
2746
+ cursor when it is known. It comes from the offline statement or `[…]`
2747
+ substitution when that resolved, otherwise from the device's `dir`/`cmd` runs.
2748
+ The command is needed only for a value's `syntax`. When it is not visible (a
2749
+ relative path after `;`), the candidates are unaffected and `syntax` is
2750
+ omitted.
2751
+
2752
+ The verdict still judges the input as written, and the two probes agree on
2753
+ it. `/ip/address add comm` is rejected by `:parse` (`expected end of command`
2754
+ at byte 16), and the highlight marks the same byte `error`. So the result
2755
+ fails even while its candidates are published. An assist loop that only wants
2756
+ candidates passes `--fail-on never`.
2757
+
2758
+ Committed controls are
2759
+ [`live-facets.stable.json`](../../test/fixtures/explain/live-facets.stable.json)
2760
+ (7.24.5) and
2761
+ [`live-facets.long-term.json`](../../test/fixtures/explain/live-facets.long-term.json)
2762
+ (7.23.8). They hold every recorded call and its raw answer, plus the projected
2763
+ result. `test/unit/explain-live-facets.test.ts` replays them.
2764
+ `test/integration/explain-live-facets.test.ts` asserts the examples on both
2765
+ transports and regenerates the captures when `CENTRS_EXPLAIN_FACETS_CAPTURE`
2766
+ names an output file.
2767
+
2768
+ Not covered here: MCP (#223), related print rows and topic checks (#493),
2769
+ and per-target caching of describe results (open 3).
2770
+
2771
+ ### Live value types (#225)
2772
+
2773
+ Every live call fills the two live homes on `data.values.occurrences[].facts`
2774
+ that offline leaves empty. Neither changes the verdict or
2775
+ `runtimeAcceptance`, and neither becomes a diagnostic. Both probes are
2776
+ `evidence[]` entries with `facet: "types"` and `basis: "derived"`; an `empty`
2777
+ answer is a result, not a warning. They run after `--schema`/`--complete`.
2778
+
2779
+ **`observedType: { value, rendered, ev }`** is the type the device's parser
2780
+ visibly converted a bare literal to. `:parse` prints a string unquoted, so
2781
+ `:local a 123` and `:local b "123"` both render `value=123`. A rendering is
2782
+ therefore evidence only when it CHANGED the literal:
2783
+
2784
+ | Literal | Device rendering | `observedType` |
2785
+ | ------- | ---------------- | -------------- |
2786
+ | `2.2`, `2.2/8` | `2.0.0.2`, `2.0.0.2/8` | `ip`, `ip-prefix` |
2787
+ | `1:0:0::1` | `1::1` | `ip6` |
2788
+ | `0x10`, `0123`, `99999999999999999999` | `16`, `123`, `9223372036854775807` | `num` |
2789
+ | `yes`, `no` | `true`, `false` | `bool` |
2790
+ | `200ms`, `1d`, `1:2`, `10000w` | `00:00:00.200`, `1d00:00:00`, `01:02:00`, `10000w00:00:00` | `time` |
2791
+ | `123`, `1.1.1.1`, `true`, `00:00:02`, `"2.2"` | unchanged | absent |
2792
+ | `100000w`, `abc`, `1e5`, `300.1.1.1` | unchanged | absent |
2793
+ | member `{abc}`, `{-1}` | `$abc`, `(- 1)` | absent |
2794
+
2795
+ The literal is not looked up in the IL of the analyzed input, where a quoted
2796
+ `"x;y=1"` prints exactly like two arguments. Each bare literal is re-parsed
2797
+ alone: `:local vN <lit>` for a scripting operand, `:local vN {<lit>}` for an
2798
+ array member, `;`-joined into one escaped `:put [:parse "…"]` per context.
2799
+ `:put <lit>` converts exactly as `:local` does. Only literals of the bytes
2800
+ `A-Z a-z 0-9 . _ : / -` are sent, so no literal can leave its slot, and a
2801
+ quoted value never qualifies. One hard-error member (`{*1}`) rejects a whole
2802
+ batch, so the two contexts are separate probes and a rejected batch is
2803
+ `invalid-response` with the device's message; its literals abstain. A batch is
2804
+ at most 256 literals, otherwise `skipped`.
2805
+
2806
+ A scripting directive's named argument converts exactly as an operand
2807
+ (`:for i from=0x10` → `from=16`, `:put message=yes` → `message=true`), so it
2808
+ joins the operand batch. A directive's NAME slot is a variable and never
2809
+ converts: `:local yes 2` renders `name=$yes`, `:for yes from=1` renders
2810
+ `counter=$yes` and `:local name=no` renders `name=$no`. So a value the symbol
2811
+ view claims as a name, or a directive's `name=`/`counter=` argument, is never
2812
+ probed.
2813
+
2814
+ Menu-command arguments are never observed. Their own parser converts them
2815
+ and prints a per-argument encoding (`src-address=!1.1.1.1` →
2816
+ `true;1.1.1.1`, `address=2.2` → `2.0.0.2;`), and a quoted value converts only
2817
+ at run time (`address="2.2"` stays `2.2`). That slot is `schemaType`'s. `op`
2818
+ has no producer: `(>[:return 1])` and `(>{"a"=1})` parse identically, and only
2819
+ `:typeof` separates them.
2820
+
2821
+ **`schemaType: { values, list?, declared, ev }`** is the argument's declared
2822
+ type. For each `name=value` of a resolved menu command, the name is resolved
2823
+ against that command's live `child` names, exactly or as a unique prefix
2824
+ (`dist=` → `distance`). One `syntax` probe per distinct argument path
2825
+ (`ip,route,add,distance`) reads the declaration; at most 32 per call, later
2826
+ ones `skipped`. An ambiguous or unknown name sends no probe. `syntax` is never
2827
+ sent to a command path (see [Live describe and completion](#live-describe-and-completion-492)).
2828
+
2829
+ The rows are a small grammar (`[!]Range`, `Address[%Interface][@VRF]`), so
2830
+ `values` lists the LEAF types the declaration mentions, not alternatives of
2831
+ one value. They use the shape-hint names so the two axes compare directly,
2832
+ plus three schema-only terms:
2833
+
2834
+ | Device leaf | Term |
2835
+ | ----------- | ---- |
2836
+ | `(integer number)`, `(decimal number)`, `integer value`, `0..65535` | `num` |
2837
+ | `(time interval)`, `time interval` | `time` |
2838
+ | `(IP address)`, `IPv4 address`, `A.B.C.D` | `ip` |
2839
+ | `IPv6 address` | `ip6` |
2840
+ | `(IP prefix)`, `(IPv6 prefix)` | `ip-prefix`, `ip6-prefix` |
2841
+ | `(IP address range)` | `ip-range` |
2842
+ | `(MAC address)` | `mac` |
2843
+ | `string value…`, `hexadecimal string value…`, `script`, `filename` | `str` |
2844
+ | a listed word set (`input \| forward \| output`, `ether1 \| lo \| ...`) | `enum` |
2845
+
2846
+ `enum` is never a closed set: the device lists the current configuration
2847
+ names (`ether1 | lo`) exactly like keywords, and ends either kind with `...`
2848
+ when the list is long (`ftp | h323 | … | ...`). `list: true` is present when
2849
+ the declaration spells a repetition (`[,Type*]`, `(max 15 times)`); its
2850
+ absence is not a claim — `servers` (`Address[@VRF]`) splits on a comma
2851
+ without saying so. A keyword's help row (`tcp` → `transmission control
2852
+ protocol`) is not a leaf, and a blank placeholder beside a real leaf is
2853
+ skipped. The fact is ABSENT when the declaration is blank (every flag and
2854
+ boolean: `disabled`, `copy-from`) or when any leaf is `see documentation`
2855
+ (`/ipv6/address add address`). `declared` is the device's top-level text,
2856
+ verbatim (`Address[@VRF]`, `Num`).
2857
+
2858
+ The vocabulary also names `bool` and `name` for a second schema source: the
2859
+ CLI Reference's `Type` cells (`bool`, `iface_enum`, `ipAddr`, `macAddr`,
2860
+ `ubit (…)`), which declare the booleans and object references inspect leaves
2861
+ blank or prints as words. A CLI Reference producer would fill the same fact,
2862
+ told apart by its `ev`; delivering those tables is decision 3 (#501).
2863
+
2864
+ Grounded on CHR 7.24.5 and 7.23.8 over REST and native API, identical on both:
2865
+ a 40-command crawl (863 arguments) set the leaf table (571 project, 282 are
2866
+ blank, 7 cite `see documentation` and 3 list no names), and the conversion
2867
+ matrix is recorded raw in
2868
+ [`live-types.stable.json`](../../test/fixtures/explain/live-types.stable.json)
2869
+ and [`live-types.long-term.json`](../../test/fixtures/explain/live-types.long-term.json).
2870
+ `test/unit/explain-live-types.test.ts` replays them and pins the matrix
2871
+ reading; `test/integration/explain-live-types.test.ts` asserts examples 52–53
2872
+ on both transports and that every executed script is a `:parse` wrapper and
2873
+ every `syntax` probe names a discovered argument. It regenerates the captures
2874
+ when `CENTRS_EXPLAIN_TYPES_CAPTURE` names an output file.
2487
2875
 
2488
2876
  ## MCP and library surfaces
2489
2877
 
@@ -2988,9 +3376,10 @@ test framework is part of this baseline.
2988
3376
 
2989
3377
  ### Current and following capabilities
2990
3378
 
2991
- - **Current — live probes (#236, phase 2):** highlight + `:parse` and
2992
- completion/child/syntax over rest-api/native-api. B4's projection/agreement
2993
- informs this work; each live cell requires its own CHR-passing examples.
3379
+ - **Done — live probes (#236, phase 2):** highlight + `:parse` (#491),
3380
+ completion/child/syntax (#492) and live value types (#225) over
3381
+ rest-api/native-api, `CHR-passed` on stable and long-term.
3382
+ `runtimeAcceptance` stays `not-proven`.
2994
3383
  - **MCP alignment (#223):** the deliberate flat-to-rich response migration,
2995
3384
  coordinated with rosetta. Independent of the offline token vocabulary.
2996
3385
  - **Facet polish (phase 3):** live sizing, `--full`, completion/schema ergonomics.
@@ -3061,8 +3450,10 @@ round):
3061
3450
 
3062
3451
  Still open (implementation opens — none block ratification):
3063
3452
 
3064
- 1. Final flag names (`--full`, `--curl`, facet names) and the smart-sizing
3065
- thresholds (what counts as "cheap", what triggers truncation).
3453
+ 1. ~~Final flag names and smart-sizing thresholds.~~ Decided by #492:
3454
+ `--schema`, `--complete`, `--cursor <byte>` and `--full`; 64 names and 16
3455
+ expansion probes by default, and 1024 / 128 with `--full`
3456
+ ([Live describe and completion](#live-describe-and-completion-492)).
3066
3457
  2. The mapping table from RouterOS highlight classes to **colors**, and how
3067
3458
  unknown/new upstream classes degrade. Phase 0 pinned the raw inputs (Q13's
3068
3459
  per-occurrence highlight-class corpus; the observed 19 classes on 7.23.2 plus