@tikoci/centrs 0.1.3 → 0.1.6

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 (107) hide show
  1. package/CHANGELOG.md +348 -0
  2. package/GLOSSARY.txt +252 -0
  3. package/LICENSE +21 -0
  4. package/README.md +44 -12
  5. package/commands/AGENTS.md +14 -5
  6. package/commands/api/AGENTS.md +13 -1
  7. package/commands/api/README.md +56 -26
  8. package/commands/api/examples.md +46 -1
  9. package/commands/btest/README.md +21 -32
  10. package/commands/check/README.md +6 -1
  11. package/commands/discover/README.md +5 -11
  12. package/commands/execute/README.md +77 -36
  13. package/commands/execute/examples.md +120 -10
  14. package/commands/explain/README.md +2756 -0
  15. package/commands/explain/examples.md +702 -0
  16. package/commands/mcp/README.md +7 -4
  17. package/commands/mcp/examples.md +2 -2
  18. package/commands/retrieve/README.md +25 -27
  19. package/commands/retrieve/examples.md +16 -0
  20. package/commands/terminal/examples.md +7 -1
  21. package/commands/transfer/README.md +28 -20
  22. package/commands/transfer/examples.md +29 -0
  23. package/docs/CLI.md +353 -0
  24. package/docs/CONSTITUTION.md +138 -30
  25. package/docs/MATRIX.md +92 -18
  26. package/docs/errors/README.md +3 -0
  27. package/docs/errors/quickchr/machine-not-found.md +22 -0
  28. package/docs/errors/quickchr/machine-stopped.md +20 -0
  29. package/docs/errors/quickchr/package-unavailable.md +27 -0
  30. package/docs/errors/quickchr/unsupported-via.md +35 -0
  31. package/docs/errors/quickchr/unsupported.md +35 -0
  32. package/docs/errors/routeros/version-unsupported.md +26 -0
  33. package/docs/errors/transport/provider-trust.md +29 -0
  34. package/docs/errors/usage/stdin-ignored.md +28 -0
  35. package/docs/errors/validation/syntax.md +29 -4
  36. package/docs/errors/validation/unknown-attribute.md +37 -4
  37. package/package.json +56 -10
  38. package/src/api-fanout.ts +46 -18
  39. package/src/api.ts +351 -121
  40. package/src/cli/api.ts +26 -7
  41. package/src/cli/btest.ts +19 -3
  42. package/src/cli/execute.ts +25 -6
  43. package/src/cli/explain.ts +503 -0
  44. package/src/cli/retrieve.ts +33 -1
  45. package/src/cli/selection.ts +71 -11
  46. package/src/cli/settings.ts +2 -2
  47. package/src/cli/terminal.ts +1 -1
  48. package/src/cli/transfer.ts +23 -6
  49. package/src/cli.ts +15 -2
  50. package/src/core/envelope.ts +58 -2
  51. package/src/core/error-catalog.ts +42 -1
  52. package/src/core/execute-command.ts +148 -0
  53. package/src/core/inspect.ts +36 -4
  54. package/src/core/preflight.ts +306 -0
  55. package/src/core/routeros-string.ts +24 -0
  56. package/src/core/version.ts +4 -0
  57. package/src/core/yaml.ts +86 -0
  58. package/src/errors.ts +1 -0
  59. package/src/execute-fanout.ts +47 -18
  60. package/src/execute.ts +339 -307
  61. package/src/explain/arg-tokens.ts +151 -0
  62. package/src/explain/args.ts +1749 -0
  63. package/src/explain/blocks.ts +175 -0
  64. package/src/explain/brace-slots.ts +143 -0
  65. package/src/explain/brace-tokens.ts +39 -0
  66. package/src/explain/catalog.ts +1343 -0
  67. package/src/explain/coordinates.ts +488 -0
  68. package/src/explain/defects.ts +218 -0
  69. package/src/explain/escape-tokens.ts +77 -0
  70. package/src/explain/highlight-projection.ts +308 -0
  71. package/src/explain/is-known-menu.ts +17 -0
  72. package/src/explain/menus.ts +668 -0
  73. package/src/explain/operator-tokens.ts +316 -0
  74. package/src/explain/operators.ts +465 -0
  75. package/src/explain/path-tokens.ts +150 -0
  76. package/src/explain/pathresolve.ts +1569 -0
  77. package/src/explain/quoted-string.ts +331 -0
  78. package/src/explain/scope-brace.ts +461 -0
  79. package/src/explain/segment.ts +1165 -0
  80. package/src/explain/separator.ts +187 -0
  81. package/src/explain/split-owner.ts +194 -0
  82. package/src/explain/string-tokens.ts +77 -0
  83. package/src/explain/symbol-values.ts +556 -0
  84. package/src/explain/symbols.ts +1617 -0
  85. package/src/explain/token-ranges.ts +72 -0
  86. package/src/explain/transport.ts +514 -0
  87. package/src/explain/value-tokens.ts +83 -0
  88. package/src/explain/values.ts +357 -0
  89. package/src/explain/verbs.ts +64 -0
  90. package/src/explain/verbsplit.ts +955 -0
  91. package/src/explain/write.ts +856 -0
  92. package/src/explain.ts +2602 -0
  93. package/src/index.ts +225 -6
  94. package/src/mcp/server.ts +2 -1
  95. package/src/offline-gate.ts +148 -0
  96. package/src/protocols/mac-telnet-console.ts +61 -17
  97. package/src/resolver/facts.ts +1 -1
  98. package/src/resolver/index.ts +23 -4
  99. package/src/resolver/quickchr-provider.ts +597 -0
  100. package/src/resolver/selection.ts +99 -12
  101. package/src/resolver/service-endpoint.ts +155 -0
  102. package/src/resolver/target.ts +1 -1
  103. package/src/retrieve-fanout.ts +27 -13
  104. package/src/retrieve.ts +82 -77
  105. package/src/settings.ts +2 -1
  106. package/src/transfer-fanout.ts +43 -26
  107. package/src/transfer.ts +88 -45
package/CHANGELOG.md CHANGED
@@ -8,6 +8,354 @@ documenting cross-cutting shifts that affect contributors and consumers.
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 0.1.6 — 2026-09-15
12
+
13
+ ### Changed
14
+
15
+ - **Release channels now use standard npm pre-release versions, not the even/odd
16
+ minor rule.** `0.1.x` counted as "pre-release" only by an internal convention
17
+ npm could not see, so every cut since 0.1.0 published to `next` and `latest`
18
+ was left pointing at 0.1.0 — a plain `npm install @tikoci/centrs` handed you a
19
+ June build while six releases sat on `next`. The dist-tag is now selected by
20
+ the SemVer pre-release identifier: a `-`-suffixed version (`0.1.7-next.0`)
21
+ publishes to `next` and never moves `latest`; a plain version (`0.1.6`)
22
+ publishes to `latest` and carries `next` forward with it, so `@next` cannot
23
+ resolve older than `@latest` between cuts. This release is the first plain
24
+ version under the new rule, so `latest` and `next` both land on 0.1.6 — which
25
+ is also the fix for the stale `latest`. The pre-1.0 caveats are unchanged:
26
+ this is still an early preview, and `docs/MATRIX.md` remains the source of
27
+ truth for what works.
28
+
29
+ - **Validation now runs offline first, then on the device.** centrs had two
30
+ validators that did not know about each other: the device preflight
31
+ (`:put [:parse …]` + `/console/inspect request=child`) that `execute` / `api` /
32
+ `validate` share, and offline `explain`, which was byte-accurate and
33
+ corpus-gated but took no part in validation at all. `execute` (all four
34
+ transports) and `api`'s `/execute` script mode now run the analyzer as
35
+ **stage 1**, before any connection: a syntax fault comes back with the
36
+ offending byte span in `error.context.span` and the protocol adapter is never
37
+ dialed, where `:put [` previously cost a full round trip to be told no. The
38
+ `api /execute` carve-out is the larger change — that surface ran **no**
39
+ preflight at all, so its script went to the device ungated.
40
+ `meta.validation.stages[]` reports what each stage concluded and names a
41
+ skipped stage's reason, so a reader can tell "the device accepted it" from
42
+ "the device was never asked". Stage 1 is necessary and never sufficient: it
43
+ has no per-menu schema, so it cannot see an unknown attribute, and the device
44
+ stage is unchanged. An offline `warn` is an abstention and passes through;
45
+ only a `fail` rejects. `--validate=false` disables both stages — one flag, one
46
+ meaning (#354, and the ordering agreed on #236).
47
+
48
+ ### Fixed
49
+
50
+ - **`'` is not a RouterOS token, and offline analysis now says so (#355).** `'`
51
+ is not a string delimiter and not a legal bare byte — only `"` opens a string.
52
+ `:parse` rejects `name=don't`, `name=don't'`, `name='abc'` and `:put 'abc'`
53
+ byte-identically on CHR 7.21.5, 7.23.5, 7.24.2 and 7.25beta3, and offline
54
+ `explain` passed all four with ZERO diagnostics, dropping the byte into
55
+ `unclassified`. It now raises `explain/canonicalizer/invalid-apostrophe` on
56
+ the apostrophe's own byte — which is the device's own reported column in every
57
+ case — while an apostrophe inside a `"`-string or a comment stays legal. Rows
58
+ 2-4 above carry BALANCED apostrophes and are rejected anyway, so this is a
59
+ lexical rule and not a quote-balance one. Being a false ACCEPT, it was
60
+ invisible to the corpus false-reject join, which scores one direction only.
61
+ Over the pinned corpus the rule fires on 40 scripts and every one is
62
+ device-rejected: the false-reject count is unchanged at 1 and the caught count
63
+ goes 62 → 93. Symbol resolution stops at the apostrophe too, the way it
64
+ already stopped at a malformed escape, so `:put 'abc'; :put $after` no longer
65
+ publishes an occurrence for `$after` — that returns 306 bytes to
66
+ `unclassified` and is the only census movement; value, operator and
67
+ bracket-equals are unchanged.
68
+ - **The quote-count preflight that modelled `'` as a delimiter is gone.**
69
+ `execute` carried a private `hasUnbalancedQuotes` check, moved into the
70
+ offline gate by #354 and kept there only because it was the one thing catching
71
+ the apostrophe case. It had no comment awareness, so an English contraction in
72
+ a `#` comment opened a quote it never closed — over the pinned corpus it
73
+ false-rejected **51** of the 617 scripts CHR 7.24.2 accepts, a figure the
74
+ published measurement never saw because that measurement ran `explainCommand`
75
+ rather than the whole gate. Removing it costs 21 catches, all on scripts the
76
+ device rejects anyway and where the device stage still applies, and takes the
77
+ gate's false rejects from 52 to 1.
78
+
79
+ - **CI: the three console reads that RouterOS 7.23.6/7.24.3/7.25beta4 broke.**
80
+ Those builds print a settings menu that has exactly one property — of the
81
+ menus centrs reads, only `/system/identity` — one character per line, so
82
+ `name: CHR` comes back with `C`, `H` and `R` on three lines under the label.
83
+ It is neither a terminal-width effect nor transport-specific: REST `POST /rest/execute` wraps identically to the
84
+ ssh and mac-telnet consoles, and every multi-property `print` is unaffected
85
+ (measured on stock CHR 7.24.2 vs 7.24.3). `execute --via ssh`,
86
+ `execute --via mac-telnet`, and `terminal --via ssh` now assert the identity
87
+ they read with `print` column wrapping collapsed on both sides, so the
88
+ examples are evidence about the console round-trip rather than about a
89
+ device layout MikroTik is free to change (#352).
90
+ - **CI: the CHR image cache now rotates with the RouterOS point release.**
91
+ `actions/cache` only writes on an exact-key miss, so the old static key froze
92
+ the first image it ever saw: every stable bump was re-downloaded from
93
+ download.mikrotik.com on *every* job and never saved. A 186s cold download
94
+ duly timed out the 300s `chr-smoke` test on the 7.24.2 → 7.24.3 bump. The key
95
+ now carries the version the job will boot (`scripts/chr-cache-key.ts`), the
96
+ smoke test gets the headroom a cold download needs, and the QA leg's own
97
+ timeout absorbs one — a cold download had also been cancelling a whole
98
+ channel leg mid-suite with every assertion green (#352).
99
+
100
+ ## 0.1.5 — 2026-09-15
101
+
102
+ *Tagged but never published: the release-tier CHR sweep failed on the
103
+ 7.23.6/7.24.3 console-wrap and image-cache defects that #352 then fixed, so npm
104
+ has no 0.1.5. Everything below shipped in 0.1.6.*
105
+
106
+ ### Added
107
+
108
+ - **Offline `explain`'s token partition is now scored against the device.**
109
+ `bun run explain:highlight-agreement` projects each centrs token class onto
110
+ the RouterOS `/console/inspect request=highlight` class it predicts and
111
+ reports where the two agree, over the committed capture slice — no router, no
112
+ corpus, no network. The report separates what it can score from what it
113
+ cannot: each side's abstentions, and the tail after the device's own one-byte
114
+ `error` — half the slice's bytes, where 7.23.2 falls silent for the rest of
115
+ the script and 7.24rc2 mostly does. A second axis says why an answer was out of reach — offline-decidable,
116
+ schema-dependent, state-dependent or version-dependent — assigned per byte
117
+ from context rather than by class name, since one highlight class carries more
118
+ than one category. Representative source fragments ship with the counts.
119
+ Figures are a trend line over a known-biased corpus, never a pass threshold
120
+ (#264 B4, #263).
121
+
122
+ ### Fixed
123
+
124
+ - **Offline `explain` no longer rejects two RouterOS spellings every tested
125
+ build accepts.** Measured, not guessed: running the analyzer over all 948
126
+ corpus scripts and comparing to each build's own `:parse` verdict found four
127
+ device-accepted scripts that offline analysis hard-failed, identical on
128
+ 7.23.5, 7.24.2 and 7.25beta3. They were two language gaps, and both are now
129
+ fixed — the count is 1, and the one that remains is the device tolerating a
130
+ genuinely extra `}`, where explain is arguably the stricter of the two.
131
+ - A scope slot may drop the `=` before its brace: `:if (…) do={…} else {…}`
132
+ and `do {…}` are the same construct as the documented `else={…}` form.
133
+ `:parse` lowers all of the spellings to byte-identical IL on CHR 7.21.5,
134
+ 7.23.5, 7.24.2 and 7.25beta3, with `elsy {` rejected on all four as the
135
+ control. Only the four `SCOPE_ARG_NAMES` members may drop it, so
136
+ `:local z {1;2}` still reads its brace as an array literal, and a colon
137
+ disqualifies the word — the device parses `do {…}` but refuses `:do {…}` in
138
+ an argument slot, since a colon makes it a directive (#347).
139
+ - A relative menu-scope block composes the path: `/ip { address { print } }`
140
+ is `(evl (evl (evl /ip/address/print)))` on the device, and a block body may
141
+ open with a submenu, one of the thirteen frozen CRUD verbs, a scripting
142
+ directive, or an absolute command. The absolute spelling already worked; the
143
+ nested relative one hard-failed. An unknown menu still does not become a
144
+ pass — the catalog's "a miss says nothing" contract means it abstains rather
145
+ than being claimed either way (#348).
146
+
147
+ As a side effect the offline token partition agrees with the device on 8 more
148
+ bytes of the committed slice, all of them moving out of *offline-silent* into
149
+ *agree*, with no new disagreements.
150
+
151
+ - **`centrs api --raw` no longer silently discards an explicit `--validate`.**
152
+ `--raw` short-circuited the whole settings ladder, so `--raw --validate=true`
153
+ ran no preflight at all — and `CENTRS_VALIDATE`, the CDB comment-kv override
154
+ and config lost to it just as quietly. It is now a precedence layer: the
155
+ explicit `--validate` flag wins, then `--raw`, then the ambient sources, then
156
+ the `true` default. `--raw` alone is unchanged and still behaves identically
157
+ on every machine whatever the environment holds, while `--raw --validate=true`
158
+ runs the gate and reports a rejection in the `--raw` error shape
159
+ (`{code,message}` on stderr, nonzero exit, no `data` key) — the intended way
160
+ to debug `api` when centrs itself is the suspect (#154).
161
+
162
+ - **`execute`/`api` validation no longer probes command-level
163
+ `request=completion`.** On RouterOS 7.12.2 that request shape never answers:
164
+ REST hangs past the client timeout (then surfaces the router's own session
165
+ close mislabeled as `validation/syntax`) and the native API closes the
166
+ connection outright, so every validated call — plain reads included — failed
167
+ on that build while `--no-validate` succeeded instantly. The attribute set
168
+ those two commands check against now comes from command-level
169
+ `request=child` alone, through the shared `inspectArgumentNames` helper;
170
+ `child` returns the complete argument list in single-digit milliseconds on
171
+ the same 7.12.2 controls, so nothing is lost by dropping the union.
172
+ `meta.validation.source` reads `/console/inspect request=child`.
173
+ `retrieve` is unchanged — its argument-level completion probe
174
+ (`print,proplist` / `get,value-name`) is a different request shape and
175
+ answers normally on 7.12.2 (#343).
176
+ - **The reported package version is the real one.** The library's
177
+ `centrsVersion` and the MCP `initialize` handshake both announced a
178
+ hard-coded `0.1.0` regardless of what was published, so a bug report from an
179
+ MCP client named the wrong build. Both now derive from `package.json`
180
+ through `src/core/version.ts`, and a unit test holds them to it (#343).
181
+ - **Offline `explain` no longer invents a verb for a menu path with
182
+ arguments.** `/ip/firewall/address-list name=ytkids timeout=1h` reported
183
+ `resolved`, path `/ip/firewall`, verb `address-list` — and the spaced
184
+ spelling `/ip firewall address-list name=x` promoted `firewall`, one level
185
+ higher still. Both premises were already offline facts and both said no: the
186
+ whole run is a known menu, and the promoted segment is not a console verb.
187
+ The reading is now an `ambiguous` abstention with a located diagnostic saying
188
+ which menu and which segment. Priced on the frozen dev/holdout partition, 10
189
+ of 17,212 statements flip, every one of them a fabrication withdrawn — three
190
+ real RouterOS statements missing their verb and seven lines of pasted
191
+ non-RouterOS prose. Presence in the shipped structure tables still narrows a
192
+ reading and absence still decides nothing, so an unlisted menu is read
193
+ exactly as before (#324).
194
+ - **Offline `explain`'s token partition reaches inside a command
195
+ substitution.** `:put "$[/ip address print as-value]"` came back as one
196
+ opaque `string` run over a command `data.structure.subcommands` already
197
+ reported as resolved, so an editor rendering `data.tokens[]` painted an
198
+ interpolated string flat. The path fill was being handed a span computed as
199
+ "one byte inside the brackets", which is wrong for `$[…]` (the sigil is part
200
+ of the span) and for `[ … ]` (the inner text is trimmed); both spellings now
201
+ carry a published `innerSpan` instead. Byte coverage does not move — what
202
+ moves is which fill owns those bytes (#325).
203
+
204
+ ### Changed
205
+
206
+ - **Every live fixture is re-swept onto the current release channels.** The
207
+ operator sweep was pinned to 7.23.3 stable / 7.24rc4 testing / 7.21.5
208
+ long-term and the highlight slice to 7.23.2 / 7.24rc2 — none of them a
209
+ current channel, and none a build the corpus covers with both oracles, so the
210
+ operator axis and the token partition were never scored on the same firmware.
211
+ Both now run on the three builds the corpus classes `complete`: stable
212
+ **7.24.2**, long-term **7.23.5**, development **7.25beta3**. The slice bases
213
+ on stable, where development serializes as `null` throughout and only the 33
214
+ long-term splits cost bytes. The sweep keeps **7.21.5** as a fourth build,
215
+ explicitly an age anchor rather than a channel claim — it is the only live
216
+ evidence that the operator surface behaves the same on a pre-7.23 branch, and
217
+ dropping it would have retired a fact rather than refreshed one (#342, #336).
218
+ - **The live sweep independently reproduces the corpus's channel split.**
219
+ Highlight agreement is byte-identical between stable and development in every
220
+ bucket and differs on long-term — the same shape the corpus IL showed at
221
+ 87/11 and 33/0. Two oracles, two capture paths, same conclusion.
222
+ - **Array comparison is not fixed on current long-term.** `:put ({2;1} >
223
+ {1;2;3})` evaluates on 7.24.2 and 7.25beta3 and errors on 7.23.5 and 7.21.5.
224
+ 7.23.5 is *newer* than the 7.23.3 this was first measured on and still errors,
225
+ so the change rode the 7.24 line and was never backported —
226
+ `ARRAY_COMPARISON_NOTE` now says so. The device's post-`error` recovery flipped
227
+ with the same realignment: `parser-recovered` is 0 on long-term and 96 bytes
228
+ on both stable and development.
229
+
230
+ - **The bracket-`=` abstention figure is generated and gated instead of
231
+ asserted.** `commands/explain/README.md` and `src/explain/operator-tokens.ts`
232
+ both quoted a `224` / `1,259` / `1,035` split for the cost of leaving `=`
233
+ inside `[ … ]` unclaimed. No script produced it, no `:check` gated it, and no
234
+ universe reproduces it — it was written once in PR #292 and had been prose
235
+ ever since, which is exactly the condition a corpus repin can silently
236
+ invalidate. `bun run explain:bracket-equals` now derives it, with `--check`
237
+ and `--readme --check` gates on the corpus → fixture → README chain like the
238
+ other censuses, and the README block is generated. Measured: **1,079** `=`
239
+ bytes inside `[ … ]` across 263 of 948 scripts, **1,067** left
240
+ `unclassified` — **448** genuine `find`/`where` query comparisons against
241
+ **619** plain `arg=value`. The `arg` fill rescues **none** of them, contrary
242
+ to the obvious guess, because it offers only the `=` its own located argument
243
+ token names. The query reading is the device's: on 7.24.2, 93 of the 109
244
+ scripts that parsed carry at least as many `(= …)` IL nodes, and the
245
+ shortfalls are scripts whose enclosing menu the capture build could not
246
+ resolve (#341).
247
+
248
+ - **The operator census reads only version-comparable RouterOS builds.** The
249
+ corpus is repinned to `lsp-routeros-ts` `7d62355` (schema v4), which carries
250
+ three *complete* builds — long-term 7.23.5, stable 7.24.2 and development
251
+ 7.25beta3, each with both oracles over all 948 scripts — beside six partial
252
+ captures covering 913. The census summed occurrences across every version it
253
+ found, so the added build inflated every head count by ~20% while
254
+ `distinctScripts`, the column carrying the argument, did not move at all. It
255
+ now filters on the new `v_version_coverage.coverage_class = 'complete'` view,
256
+ giving a rectangular universe of 2,841 parse-IL rows — 3 builds × the 947 of
257
+ 948 scripts that produce IL — and states its version set so a gate cannot
258
+ compare two different universes. Presence *is* monotone in coverage, so
259
+ the partial builds keep one job: a new `headFirstSeen` axis dates each head
260
+ against every captured build — which is what still grounds the `any` operator
261
+ at 7.20.8, a build the counting side no longer reads. `source_scripts` is
262
+ byte-identical across the repin, so every text-derived report — the token and
263
+ value censuses, `explain:arg-reach`, the browser-consumer proof — is
264
+ unchanged by construction (#336, #342).
265
+ - **The three complete builds are recorded as disagreeing.** `:parse` IL differs
266
+ on 87 of 948 scripts between long-term 7.23.5 and stable 7.24.2 but on only 11
267
+ between stable and development 7.25beta3; `highlight` differs on 33 and 0. So
268
+ an operator-axis or token-partition figure is a claim about a *channel*, not
269
+ about RouterOS, and `commands/explain/README.md` now says so rather than
270
+ collapsing the builds to one number.
271
+ - **A valid string escape is now its own token class in offline `explain`.**
272
+ `ExplainTokenClass` gains `escaped`, the second and last split #264's rule
273
+ admits: only INVALID escapes surfaced anywhere else, as `bad-string-escape`
274
+ diagnostics, so a consumer holding `tokens[]` had no published surface to join
275
+ against to find the valid ones — and the device names them. The escape grammar
276
+ is not walked a second time to do it: `walkStringEscapes` now returns both
277
+ halves of the one reading `bad-string-escape` already came from, so a token
278
+ and a diagnostic cannot disagree about which bytes are an escape, and it stops
279
+ claiming at the first invalid escape rather than guess past a malformed one.
280
+ **Byte coverage does not move** — classified bytes are identical to the last
281
+ decimal; `escaped`'s 19,960 corpus bytes come out of `string` (−17,672) and
282
+ `value` (−2,288) exactly, and the token count rises only because a run
283
+ carrying an escape becomes three. Against the committed device slice the class
284
+ agrees on 418 of 418 bytes and the `unprojected` residue falls 1,051 → 633.
285
+ Consumers reading `data.tokens[]` for `string` or `value` must now also read
286
+ `escaped` to see bytes they used to get inside one run. A `\`-newline line
287
+ continuation in **code** is deliberately not claimed — the device calls it
288
+ `escaped` too, but its run swallows the next line's indentation, and what a
289
+ continuation owns is #225's question (#264).
290
+
291
+ - **Offline `explain`'s token vocabulary is no longer provisional, and the
292
+ argument `=` has its own class.** `ExplainTokenClass` gains `arg-sep`: an
293
+ argument's name bytes stay `arg`, and the single `=` binding it to its value
294
+ is now named separately. #264 B5 settled the whole vocabulary on a stated
295
+ rule — a merged class is split only where a consumer needs the distinction
296
+ AND cannot recover it by joining `tokens[]` to another published surface on
297
+ byte offsets — so the per-spelling `operator`, per-shape `value`,
298
+ scope-versus-array `brace` and string-delimiter merges stay merged, each with
299
+ the surface that already carries the distinction named. The `=` was the one
300
+ that failed the test: 30.8% of the `=` runs the fill paints (5,088 of 16,526
301
+ on the pinned corpus) belong to a statement whose strict `arguments` reading
302
+ refused, led by the `do=` of every `:if`/`:foreach`/`:while`, so there is no
303
+ published `valueSpan` to derive the boundary from. **Byte coverage does not
304
+ move** — classified bytes are identical to the last decimal; this is a retag,
305
+ and the token count rises only because one run became two. Against the
306
+ committed device slice, projection disagreements fall 360 → 2 as centrs stops
307
+ calling a separator a name. Consumers reading `data.tokens[]` for `arg` must
308
+ now also read `arg-sep` to see the bytes they used to get in one run
309
+ (#264 B5, #316).
310
+
311
+ - **Offline `explain` answers statement-ownership from an index, not a scan.**
312
+ The symbol/value surface asked "which statement owns this span" once per
313
+ symbol, value and scope brace by scanning every statement split, so cost grew
314
+ quadratically with symbol-dense input: 253 KiB of declarations took 4.3 s and
315
+ was still steepening. Output is unchanged — all 1,896 `explainCommand` hashes
316
+ over the pinned corpus match byte for byte under both facet settings — and the
317
+ growth-ratio guard now covers a symbol-dense input shape alongside the
318
+ separator-free one (#317).
319
+
320
+ ## 0.1.4 — 2026-08-31
321
+
322
+ Preview release. `@tikoci/centrs@0.1.4` on npm's `next` tag.
323
+
324
+ ### Added
325
+
326
+ - **Offline `explain` is now a real CLI and library surface.** It analyzes
327
+ RouterOS commands and scripts without a router, CDB, or network connection,
328
+ returning canonical structure, diagnostics, write-shape and transport
329
+ classification, optional `curl` rendering, symbols, value-shape hints, and a
330
+ gapless token partition. Try it with
331
+ `bunx @tikoci/centrs@next explain '/ip/address print' --json`.
332
+ - **Generated RouterOS structure evidence.** Pinned inspect trees and
333
+ MikroTik's published CLI Reference now help distinguish known menus from
334
+ commands while preserving the fail-closed rule: missing catalog entries never
335
+ become rejections, schema claims, or runtime-acceptance claims.
336
+ - **Named quickchr targets.** Router-facing commands can resolve an ephemeral
337
+ quickchr instance with `--quickchr <name>` through the shared target-provider
338
+ path.
339
+
340
+ ### Changed
341
+
342
+ - **Offline analysis is substantially more precise and robust.** Grounded
343
+ handling now covers RouterOS comments, escapes, operators, arrays, value
344
+ shapes, path context, and flow-sensitive symbol-to-value references; bounded
345
+ traversal and shared statement indexes keep large inputs near-linear.
346
+ - **Release and QA coverage is broader.** Recency-aware RouterOS channel
347
+ selection, long-term-version expectations, generated-artifact drift checks,
348
+ corpus gates, and faster local Git gates make the published preview easier to
349
+ reproduce and audit.
350
+
351
+ ### Fixed
352
+
353
+ - **Validation now reads RouterOS `:parse` results instead of accepting the
354
+ transport round-trip alone**, and retrieve errors retain the rejected
355
+ parameter. Parser, catalog, cross-platform QA, and MAC-Telnet edge cases were
356
+ also hardened.
357
+ - **The npm package now carries the MIT license text.**
358
+
11
359
  ## 0.1.3 — 2026-07-06
12
360
 
13
361
  Patch release. `@tikoci/centrs@0.1.3`.