@tikoci/centrs 0.1.4 → 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.
- package/CHANGELOG.md +309 -0
- package/GLOSSARY.txt +40 -1
- package/README.md +8 -4
- package/commands/AGENTS.md +7 -4
- package/commands/api/AGENTS.md +13 -1
- package/commands/api/README.md +37 -8
- package/commands/api/examples.md +31 -1
- package/commands/execute/README.md +39 -10
- package/commands/execute/examples.md +79 -7
- package/commands/explain/README.md +1195 -155
- package/commands/explain/examples.md +99 -3
- package/commands/mcp/README.md +7 -4
- package/commands/mcp/examples.md +2 -2
- package/commands/terminal/examples.md +7 -1
- package/docs/CLI.md +2 -2
- package/docs/CONSTITUTION.md +72 -15
- package/docs/MATRIX.md +89 -52
- package/docs/errors/validation/syntax.md +29 -4
- package/package.json +13 -2
- package/src/api-fanout.ts +17 -4
- package/src/api.ts +254 -67
- package/src/cli/api.ts +1 -1
- package/src/cli/btest.ts +1 -1
- package/src/cli/explain.ts +1 -1
- package/src/cli/terminal.ts +1 -1
- package/src/core/envelope.ts +53 -0
- package/src/core/error-catalog.ts +2 -1
- package/src/core/execute-command.ts +148 -0
- package/src/core/inspect.ts +36 -4
- package/src/core/version.ts +4 -0
- package/src/core/yaml.ts +86 -0
- package/src/execute-fanout.ts +18 -4
- package/src/execute.ts +225 -244
- package/src/explain/arg-tokens.ts +62 -25
- package/src/explain/args.ts +398 -52
- package/src/explain/blocks.ts +54 -11
- package/src/explain/brace-tokens.ts +4 -3
- package/src/explain/catalog.ts +42 -44
- package/src/explain/coordinates.ts +159 -0
- package/src/explain/defects.ts +2 -1
- package/src/explain/escape-tokens.ts +77 -0
- package/src/explain/highlight-projection.ts +308 -0
- package/src/explain/menus.ts +9 -5
- package/src/explain/operator-tokens.ts +13 -7
- package/src/explain/operators.ts +27 -20
- package/src/explain/path-tokens.ts +1 -1
- package/src/explain/pathresolve.ts +358 -113
- package/src/explain/quoted-string.ts +110 -20
- package/src/explain/scope-brace.ts +175 -22
- package/src/explain/segment.ts +429 -21
- package/src/explain/separator.ts +187 -0
- package/src/explain/split-owner.ts +194 -0
- package/src/explain/string-tokens.ts +14 -4
- package/src/explain/symbol-values.ts +209 -173
- package/src/explain/symbols.ts +21 -1
- package/src/explain/transport.ts +17 -0
- package/src/explain/value-tokens.ts +15 -12
- package/src/explain/verbsplit.ts +165 -23
- package/src/explain/write.ts +47 -6
- package/src/explain.ts +522 -72
- package/src/index.ts +19 -2
- package/src/mcp/server.ts +2 -1
- package/src/offline-gate.ts +148 -0
- package/src/retrieve-fanout.ts +1 -1
- package/src/retrieve.ts +1 -59
- package/src/settings.ts +2 -1
- package/src/transfer-fanout.ts +1 -1
- package/src/transfer.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,315 @@ 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
|
+
|
|
11
320
|
## 0.1.4 — 2026-08-31
|
|
12
321
|
|
|
13
322
|
Preview release. `@tikoci/centrs@0.1.4` on npm's `next` tag.
|
package/GLOSSARY.txt
CHANGED
|
@@ -149,10 +149,12 @@ npk # MikroTik package format
|
|
|
149
149
|
nvme-over-tcp # supported by RouterOS **with** rose-storage.npk extra-package
|
|
150
150
|
OID
|
|
151
151
|
PII # Personally Identifiable Information
|
|
152
|
+
PON # passive optical network; RouterOS /interface/ethernet/pon menu
|
|
152
153
|
POV
|
|
153
154
|
PRGA
|
|
154
155
|
proplist
|
|
155
156
|
pywinauto
|
|
157
|
+
QKD # quantum key distribution; RouterOS IPsec/keymat-provider vocabulary
|
|
156
158
|
QML
|
|
157
159
|
qll # CodeQL library file extension (.qll), e.g. CleartextLoggingCustomizations.qll
|
|
158
160
|
qmljsdebugger
|
|
@@ -384,6 +386,16 @@ evl
|
|
|
384
386
|
# as "(evl /putmessage=x)", ":global n v" as "(evl /globalname=$n;value=v)" (GH#230).
|
|
385
387
|
putmessage
|
|
386
388
|
globalname
|
|
389
|
+
# localname/setnote/runnumber: more of the same head rendering, quoted verbatim as
|
|
390
|
+
# evidence for the missing-separator rule (GH#311). ":put [:parse …]" joins the IL
|
|
391
|
+
# array with NO separator, so the path and its first argument run together —
|
|
392
|
+
# ":local a v" is "(evl /localname=$a;value=v)", "/system/note set note=hello" is
|
|
393
|
+
# "(evl /system/note/setnote=hello)", "/system/script/run /ip/route" is
|
|
394
|
+
# "(evl /system/script/runnumber=/ip/route)". Artefacts of the rendering, not
|
|
395
|
+
# RouterOS argument names; kept unsplit because the quote must match the bytes.
|
|
396
|
+
localname
|
|
397
|
+
setnote
|
|
398
|
+
runnumber
|
|
387
399
|
# unconfigured: a freshly-flashed RouterOS device with no IP — the mac-telnet use case.
|
|
388
400
|
unconfigured
|
|
389
401
|
unwired
|
|
@@ -676,6 +688,7 @@ mmips # RouterOS architecture (multi-core MIPS), named in `conditions:` gates
|
|
|
676
688
|
CRS # MikroTik Cloud Router Switch — carries the switch-chip menus no CHR has
|
|
677
689
|
CCR # MikroTik Cloud Core Router
|
|
678
690
|
superlinear # growth faster than linear, used for resolveDocument wall-clock on separator-free input (#248)
|
|
691
|
+
superlinearity # the property a growth-ratio guard bounds; `superlinear+` is NOT usable here, cspell's compound form forbids the bare word (#313)
|
|
679
692
|
stmts # `explainCommand` corpus measurement: statement count (shorthand for `statements`)
|
|
680
693
|
argCandidates # `explainCommand` corpus measurement: arg-bearing candidates
|
|
681
694
|
argReadable # `explainCommand` corpus measurement: readable statements
|
|
@@ -691,7 +704,7 @@ llms # `llms.txt` — MikroTik's published page inventory; the only one listing
|
|
|
691
704
|
musicswitch # syscap gate: the Marvell-era switch-chip menu shape — one half of the `/interface/ethernet/switch` pair published under two gates
|
|
692
705
|
rbswitch # syscap gate: the RouterBOARD switch-chip menu shape — the other half of that pair
|
|
693
706
|
prestera # Marvell Prestera switch chip — named in the `crs_prestera` syscap gate on `/interface/ethernet/switch/qos`
|
|
694
|
-
xfrm # `/interface/xfrm` — the IPsec transform interface,
|
|
707
|
+
xfrm # `/interface/xfrm` — the IPsec transform interface, device-confirmed by the pinned 7.25beta3 trees and published by the CLI Reference
|
|
695
708
|
btrfs # the Linux B-tree filesystem, a `/disk/btrfs/...` menu on RouterOS 7.20+
|
|
696
709
|
|
|
697
710
|
# #255 operator-axis vocabulary: the device-grounded operator surface.
|
|
@@ -718,3 +731,29 @@ occ # short for occurrence — local variable in symbol-values flow analysis
|
|
|
718
731
|
vid # short for valueId — result-local value occurrence identifier (v0, v1)
|
|
719
732
|
idx # short for index — loop/event index in flow merging
|
|
720
733
|
mis # prefix in mis-linking — hyphenated in comments for symbol-values guard
|
|
734
|
+
|
|
735
|
+
# #313 offline performance vocabulary
|
|
736
|
+
JIT # just-in-time compiler — warmed before either half of the growth-ratio timing so the two sizes are compared in the same compilation state
|
|
737
|
+
|
|
738
|
+
# #317 statement-ownership index vocabulary
|
|
739
|
+
laminarity # the property of a set of spans in which every pair is nested or disjoint, never crossing — what makes the shortest containing span reachable by a parent walk
|
|
740
|
+
|
|
741
|
+
# #264 B4 / #263 device-agreement vocabulary
|
|
742
|
+
unprojected # of a byte whose offline/device class PAIR the declared projection does not cover, so it can neither agree nor disagree: an offline class that deliberately maps to no RouterOS `highlight` class, or a device class this build has never seen (#264 B4)
|
|
743
|
+
|
|
744
|
+
# #324 / #325 offline-reading vocabulary. Both words are corpus data: they are
|
|
745
|
+
# quoted from the two slice scripts the device-agreement report names as the
|
|
746
|
+
# exemplars of the cells these fixes empty, so their spellings must match the
|
|
747
|
+
# bytes the device saw.
|
|
748
|
+
ytkids # an address-list name in the corpus line `/ip/firewall/address-list name=ytkids timeout=1h` — the #324 exemplar
|
|
749
|
+
apikey # a local variable in the corpus line `apikey-len=$([:len $apikey])` — the `$(…)` interpolation #325 deliberately leaves unreached
|
|
750
|
+
|
|
751
|
+
# #312 offline-entry boundary vocabulary
|
|
752
|
+
dgram # Node's `node:dgram` UDP socket module — reached by `mac-telnet`, and one of the builtins that made the old `explain` import graph unusable outside Bun; named in the boundary gate's allowlist rationale
|
|
753
|
+
|
|
754
|
+
# #347 / #348 offline false-reject controls. Both words are deliberate
|
|
755
|
+
# NON-words: each is the rejected half of a CHR-confirmed accepted/rejected
|
|
756
|
+
# pair, so its spelling is load-bearing and must not be "corrected".
|
|
757
|
+
elsy # the misspelled `else` in `:if (1=1) do={…} elsy {…}` — rejected as `expected end of command` on CHR 7.21.5/7.23.5/7.24.2/7.25beta3, which is what proves the bare-brace `else {` acceptance is a real device fact and not a probe artifact (#347)
|
|
758
|
+
prnt # a misspelt `print` verb — `/ip/address/prnt` is the offline analyzer's ABSTENTION fixture (`verdict: warn`, not `fail`), which is what proves the two-stage validation gate passes an abstention through to the device rather than refusing it (#354)
|
|
759
|
+
zzznotamenu # a menu name no RouterOS build has — `/ip { zzznotamenu { print } }` is a `syntax error` on device, so menu-scope resolution is schema-dependent and a catalog MISS must abstain rather than accept (#348)
|
package/README.md
CHANGED
|
@@ -178,7 +178,9 @@ and the comment grammar — lives in `commands/devices/README.md` and
|
|
|
178
178
|
|
|
179
179
|
The load-bearing contract — envelope shape, error model, protocol selection,
|
|
180
180
|
settings precedence, and the "done" definition — lives in
|
|
181
|
-
`docs/CONSTITUTION.md`.
|
|
181
|
+
`docs/CONSTITUTION.md`. Capability status and the selected work track,
|
|
182
|
+
including the command×protocol grid and offline analysis, live in
|
|
183
|
+
`docs/MATRIX.md`.
|
|
182
184
|
|
|
183
185
|
## Repository layout
|
|
184
186
|
|
|
@@ -187,13 +189,15 @@ code and authority docs live.
|
|
|
187
189
|
|
|
188
190
|
- **`docs/CONSTITUTION.md`** — load-bearing rules (validation, envelope, error
|
|
189
191
|
model, settings precedence, identity/CDB, protocol selection, done).
|
|
190
|
-
- **`docs/MATRIX.md`** —
|
|
192
|
+
- **`docs/MATRIX.md`** — capability status and the selected work track (the
|
|
193
|
+
command×protocol grid plus offline analysis and frontend surfaces); the
|
|
194
|
+
only status surface.
|
|
191
195
|
- **`commands/<name>/`** — per-command README and executable examples.
|
|
192
196
|
- **`src/`** — Bun/TypeScript source.
|
|
193
197
|
- **`test/`** — unit and CHR-backed integration tests.
|
|
194
198
|
|
|
195
|
-
There is no `docs/specs/`, no `work/`, no roadmap doc. The matrix
|
|
196
|
-
roadmap.
|
|
199
|
+
There is no `docs/specs/`, no `work/`, no roadmap doc. The matrix and the
|
|
200
|
+
GitHub issues it links are the roadmap.
|
|
197
201
|
|
|
198
202
|
## Development
|
|
199
203
|
|
package/commands/AGENTS.md
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
Each `commands/<name>/` holds the **executable spec** for one command:
|
|
4
4
|
|
|
5
5
|
- `README.md` — intent, flags, behavior. The "designed" tier.
|
|
6
|
-
- `examples.md` — numbered, runnable examples
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
- `examples.md` — numbered, runnable examples mapped to named tests:
|
|
7
|
+
example N ↔ named test N. Live examples require CHR integration; offline
|
|
8
|
+
examples use the unit/fixture tests named by the command — a named test may
|
|
9
|
+
carry several assertions. Verification follows `docs/CONSTITUTION.md` →
|
|
10
|
+
Done definition.
|
|
9
11
|
|
|
10
12
|
**Implemented** flags are generated from `CliCommandMetadata` into
|
|
11
13
|
`docs/CLI.md` (`bun run docs:cli`; drift-gated in CI) — a command README keeps
|
|
@@ -19,7 +21,8 @@ keys, ambiguity, `__default__`, comment-kv allowlist, write strategy) lives in
|
|
|
19
21
|
`commands/devices/README.md` — the registry command owns it; the constitution
|
|
20
22
|
keeps only the identity/CDB principle. A command file covers only what is
|
|
21
23
|
specific to that command. `docs/MATRIX.md` is the only status surface; a
|
|
22
|
-
README's "Status" line must agree with it.
|
|
24
|
+
README's "Status" line must agree with it. GitHub issues track the tasks and
|
|
25
|
+
dependencies within the selected capability; they do not replace its status.
|
|
23
26
|
|
|
24
27
|
## Verb vocabulary
|
|
25
28
|
|
package/commands/api/AGENTS.md
CHANGED
|
@@ -183,11 +183,23 @@ green on CHR 7.23.1.
|
|
|
183
183
|
not `print`/`get`), so it is a `POST` and **write-classed** → needs `--yes`.
|
|
184
184
|
- **CONFIRMED — the inspect gate over both transports.** Path existence
|
|
185
185
|
(`request=child` empty ⇒ `validation/unknown-path`) and add/set attribute
|
|
186
|
-
validity (`request=child
|
|
186
|
+
validity (`request=child` ⇒ `validation/unknown-attribute`) fire
|
|
187
187
|
identically over rest-api and native-api, **before** any write — no `:put
|
|
188
188
|
[:parse]` involved (api input is a path, not a CLI string). A `/execute` script
|
|
189
189
|
is a CLI string ⇒ `meta.validation.semantic = "not-applicable"`.
|
|
190
190
|
|
|
191
|
+
## CONFIRMED ON CHR (RouterOS 7.12.2 compatibility — GH#343, 2026-09-14)
|
|
192
|
+
|
|
193
|
+
- **Command-level `request=completion` is unsafe on 7.12.2.** For
|
|
194
|
+
`path=system,identity,print`, REST did not answer before the 10 s client
|
|
195
|
+
timeout and native API closed the connection immediately. Adding
|
|
196
|
+
`.proplist=completion` did not change either result. The preceding `:parse`
|
|
197
|
+
and `request=child` controls returned in 1–8 ms on both transports, and
|
|
198
|
+
`child` contained the complete command argument list. Execute/API attribute
|
|
199
|
+
validation therefore uses command-level `request=child` only. This does not
|
|
200
|
+
prohibit argument-level completion probes such as `print,proplist`, which are
|
|
201
|
+
a separate shape and returned immediately on the same 7.12.2 controls.
|
|
202
|
+
|
|
191
203
|
## CONFIRMED ON CHR (Phase 4 `--stream` follow — CHR 7.23.1, 2026-06-30)
|
|
192
204
|
|
|
193
205
|
Validated by `test/integration/api-listen.test.ts` (L1–L4) on CHR 7.23.1.
|
package/commands/api/README.md
CHANGED
|
@@ -99,19 +99,48 @@ vs `--query` distinction.
|
|
|
99
99
|
|
|
100
100
|
## Validation
|
|
101
101
|
|
|
102
|
-
Default on. Because the input is a structured path (not a CLI string), the
|
|
103
|
-
`/console/inspect` — **not** `:put [:parse]`:
|
|
102
|
+
Default on. Because the input is a structured path (not a CLI string), the device
|
|
103
|
+
gate is `/console/inspect` — **not** `:put [:parse]`:
|
|
104
104
|
|
|
105
105
|
- Path existence + per-verb attribute validity via `request=child`
|
|
106
106
|
(`validation/unknown-path`, `validation/unknown-attribute`).
|
|
107
|
-
-
|
|
107
|
+
- **Designed, not implemented:** value-level advice via `request=completion`
|
|
108
|
+
(+`input=`) feeding `tips`/`warnings`. Whatever implements it must not fall
|
|
109
|
+
back to `request=completion` at a *command* path (`path=` ending at the verb):
|
|
110
|
+
that shape hangs REST and closes native API on RouterOS 7.12.2, and no centrs
|
|
111
|
+
code uses it. Completion at an *argument* path (`print,proplist`, as `retrieve`
|
|
112
|
+
does) answers normally on the same build. See the 7.12.2 finding in
|
|
113
|
+
`AGENTS.md` (GH#343).
|
|
108
114
|
- **Carve-out:** a script-shaped `POST /rest/execute` is a CLI string, not a path,
|
|
109
115
|
so the inspect gate is `not-applicable` (`meta.validation.semantic`); RouterOS
|
|
110
|
-
re-validates on the run.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
re-validates on the run. Until GH#354 that carve-out meant the script was sent
|
|
117
|
+
with *no* preflight at all. It is now gated by the offline stage (constitution:
|
|
118
|
+
validation) — the same analyzer `explain` publishes, run over the `script`
|
|
119
|
+
field with no connection, so a syntax fault is a byte span in
|
|
120
|
+
`error.context.span` instead of a round trip. `meta.validation.stages[]` shows
|
|
121
|
+
the offline stage `passed` and the device stage `skipped` with its reason. A
|
|
122
|
+
missing or blank `script` is still `input/invalid-command`, not a syntax
|
|
123
|
+
rejection.
|
|
124
|
+
- Structured path requests are **not** offline-gated. The input is a path plus a
|
|
125
|
+
body, not a CLI string, and an offline *path* gate collides with GH#211's
|
|
126
|
+
unlisted-path decision. On the success path `meta.validation.stages[]` is
|
|
127
|
+
absent there; on a `validation/*` rejection it is present and reports the
|
|
128
|
+
offline stage `skipped` with that reason, so the breakdown never implies an
|
|
129
|
+
analysis that did not run.
|
|
130
|
+
|
|
131
|
+
`--validate=false` skips **both** stages of the preflight — one flag, one meaning
|
|
132
|
+
(constitution: validation); RouterOS still re-validates writes server-side.
|
|
133
|
+
Disabling validation to make a call pass is forbidden (constitution: validation
|
|
134
|
+
is the product).
|
|
135
|
+
|
|
136
|
+
`--raw` **defaults** `--validate` to false — it does not override it (GH#154).
|
|
137
|
+
The resolution order is the explicit `--validate` flag, then `--raw`, then
|
|
138
|
+
`CENTRS_VALIDATE` / the CDB comment-kv / config, then the `true` default. So
|
|
139
|
+
`--raw` alone behaves as it always has and behaves the same on every machine
|
|
140
|
+
whatever the environment holds, while `--raw --validate=true` runs the gate and
|
|
141
|
+
reports a rejection in the `--raw` error shape (`{code,message}` on stderr,
|
|
142
|
+
nonzero exit, no `data` key) — which is how you debug `api` when centrs itself is
|
|
143
|
+
the suspect.
|
|
115
144
|
|
|
116
145
|
## Write confirmation
|
|
117
146
|
|
package/commands/api/examples.md
CHANGED
|
@@ -155,7 +155,23 @@ centrs api $R execute -X POST -f script=':put [/system/identity/get name]' --use
|
|
|
155
155
|
```
|
|
156
156
|
|
|
157
157
|
Envelope: `ok: true`, `data` is string-shaped and contains the CHR identity;
|
|
158
|
-
`meta.validation.semantic=not-applicable` (script, not a path)
|
|
158
|
+
`meta.validation.semantic=not-applicable` (script, not a path) and
|
|
159
|
+
`meta.validation.stages` is `offline: passed` then `device: skipped`.
|
|
160
|
+
|
|
161
|
+
### 16b. The `/execute` script is gated offline (GH#354)
|
|
162
|
+
|
|
163
|
+
Until GH#354 the script-mode carve-out meant this surface ran **no** preflight at
|
|
164
|
+
all. The offline analyzer is the whole stage-1 gate here, and it opens no
|
|
165
|
+
connection — the unreachable port proves it.
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
centrs api 127.0.0.1:1 execute -X POST -f script=':put [' --username $U --password $P --yes
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Envelope: `ok: false`, `error.code=validation/syntax`,
|
|
172
|
+
`error.context.validationStage="offline"`, `error.context.surface="api /execute"`,
|
|
173
|
+
`error.context.span={"start":5,"end":6}`, and **not** a `transport/*` code.
|
|
174
|
+
`meta.validation.stages` is `offline: failed` then `device: skipped`.
|
|
159
175
|
|
|
160
176
|
### 17. `--via rest-api --listen` is rejected
|
|
161
177
|
|
|
@@ -209,6 +225,20 @@ proplist). Combining an id with `--query`/`--proplist` folds the id into the RES
|
|
|
209
225
|
matching the native `?.id=` read. (Plain id-only reads still use the `GET …/$ID`
|
|
210
226
|
URL form; see example 5's re-read.)
|
|
211
227
|
|
|
228
|
+
### 22. `--raw --validate=true` still runs the gate (#154)
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
centrs api $R ip/no-such-menu --raw --validate=true --username $U --password $P
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`--raw` only **defaults** `--validate` to false, so an explicit `--validate=true`
|
|
235
|
+
wins and the `/console/inspect` preflight runs. The rejection comes back in the
|
|
236
|
+
`--raw` error shape, not the envelope: stdout empty, stderr a compact
|
|
237
|
+
`{"code":"validation/unknown-path","message":…}`, nonzero exit, and no `data`
|
|
238
|
+
key. Contrast example 13, where bare `--raw` skips the preflight entirely. This
|
|
239
|
+
pair is how you tell a RouterOS rejection from a centrs bug while staying on the
|
|
240
|
+
bare-passthrough surface.
|
|
241
|
+
|
|
212
242
|
## native-api (`--via native-api`)
|
|
213
243
|
|
|
214
244
|
The same contract over the binary API. Validation still runs through
|