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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/CHANGELOG.md +177 -40
  2. package/GLOSSARY.txt +32 -0
  3. package/README.md +12 -8
  4. package/SECURITY.md +11 -3
  5. package/commands/api/README.md +39 -15
  6. package/commands/api/examples.md +44 -6
  7. package/commands/execute/README.md +108 -14
  8. package/commands/execute/examples.md +137 -3
  9. package/commands/explain/README.md +430 -129
  10. package/commands/explain/examples.md +260 -1
  11. package/commands/mcp/README.md +11 -0
  12. package/commands/mcp/examples.md +6 -0
  13. package/commands/retrieve/README.md +30 -13
  14. package/commands/retrieve/examples.md +10 -6
  15. package/commands/transfer/examples.md +9 -0
  16. package/docs/CLI.md +8 -8
  17. package/docs/CONSTITUTION.md +29 -2
  18. package/docs/MATRIX.md +26 -13
  19. package/docs/errors/cdb/empty-group.md +19 -3
  20. package/docs/errors/cdb/empty-selection.md +9 -3
  21. package/docs/errors/cmr/caller-policy-not-enforced.md +46 -0
  22. package/docs/errors/cmr/empty-selection.md +17 -3
  23. package/docs/errors/cmr/rule-selects-every-device.md +32 -0
  24. package/docs/errors/cmr/selection-includes-controller.md +27 -0
  25. package/docs/errors/usage/flag-after-separator.md +34 -0
  26. package/package.json +10 -5
  27. package/src/api-fanout.ts +11 -1
  28. package/src/api.ts +205 -65
  29. package/src/btest.ts +7 -2
  30. package/src/cli/api.ts +30 -16
  31. package/src/cli/common.ts +180 -1
  32. package/src/cli/devices.ts +0 -2
  33. package/src/cli/discover.ts +0 -1
  34. package/src/cli/execute.ts +44 -20
  35. package/src/cli/explain.ts +2 -0
  36. package/src/cli/mcp.ts +0 -1
  37. package/src/cli/retrieve.ts +40 -18
  38. package/src/cli/settings.ts +0 -3
  39. package/src/cli/terminal.ts +0 -2
  40. package/src/cli/transfer.ts +10 -8
  41. package/src/cli/version.ts +87 -0
  42. package/src/cli.ts +5 -3
  43. package/src/core/cmr-fleet.ts +486 -0
  44. package/src/core/cmr.ts +78 -0
  45. package/src/core/envelope.ts +32 -3
  46. package/src/core/error-catalog.ts +25 -3
  47. package/src/core/execute-command.ts +144 -19
  48. package/src/core/fanout.ts +130 -6
  49. package/src/errors.ts +2 -1
  50. package/src/execute-cmr.ts +95 -78
  51. package/src/execute-fanout.ts +11 -1
  52. package/src/execute.ts +105 -23
  53. package/src/explain/args.ts +69 -8
  54. package/src/explain/blocks.ts +3 -4
  55. package/src/explain/brace-slots.ts +5 -0
  56. package/src/explain/catalog.ts +13 -6
  57. package/src/explain/defects.ts +1 -0
  58. package/src/explain/group-faults.ts +927 -0
  59. package/src/explain/highlight-projection.ts +10 -8
  60. package/src/explain/operators.ts +20 -19
  61. package/src/explain/pathresolve.ts +102 -20
  62. package/src/explain/quoted-string.ts +41 -6
  63. package/src/explain/run-tokens.ts +223 -0
  64. package/src/explain/scope-brace.ts +103 -6
  65. package/src/explain/segment.ts +41 -3
  66. package/src/explain/spaced-args.ts +116 -0
  67. package/src/explain/symbols.ts +17 -1
  68. package/src/explain/transport.ts +13 -0
  69. package/src/explain/verbsplit.ts +14 -188
  70. package/src/explain/write.ts +8 -1
  71. package/src/explain.ts +378 -18
  72. package/src/index.ts +18 -1
  73. package/src/mcp/safety.ts +11 -0
  74. package/src/mcp/tools.ts +4 -1
  75. package/src/offline-gate.ts +30 -9
  76. package/src/protocols/btest-session.ts +30 -10
  77. package/src/protocols/mac-telnet-console.ts +16 -0
  78. package/src/resolver/cdb.ts +13 -1
  79. package/src/resolver/index.ts +4 -0
  80. package/src/resolver/selection.ts +37 -1
  81. package/src/resolver/target.ts +96 -8
  82. package/src/retrieve-fanout.ts +4 -1
  83. package/src/retrieve-follow.ts +29 -33
  84. package/src/retrieve-sample.ts +14 -8
  85. package/src/retrieve-stream.ts +38 -6
  86. package/src/retrieve.ts +73 -59
  87. package/src/transfer-fanout.ts +8 -1
  88. package/src/transfer.ts +15 -19
package/CHANGELOG.md CHANGED
@@ -6,16 +6,41 @@ The product evolves as a stream of changes; there are no alpha gates.
6
6
  Per-feature status is tracked in `docs/MATRIX.md`. Use this file for
7
7
  documenting cross-cutting shifts that affect contributors and consumers.
8
8
 
9
- ## Unreleased
9
+ Each stable version has one section. Work in progress goes under
10
+ `## X.Y.Z — next`, which is what `@next` installs (`X.Y.Z-next.N`); the stable
11
+ release dates that heading. A `-next.N` build has no section of its own.
10
12
 
11
- ## 0.1.9-next.0 — 2026-10-13
13
+ ## 0.1.9 — next
12
14
 
13
- Pre-release (npm tag `next`; `latest` stays on 0.1.8). Install with
14
- `bunx @tikoci/centrs@next`. First cut of `execute --cmr`; expect another
15
- `next` after the next round of CMR work.
15
+ In progress on npm tag `next`. The `0.1.9-next.0` cut (2026-10-13) carried
16
+ part of this section.
16
17
 
17
18
  ### Added
18
19
 
20
+ - `centrs --version` / `-V` prints the package version alone; `--json` (or
21
+ `--format json|yaml`) returns an envelope with `data.version` (#463).
22
+ - `execute --cmr` warns `cmr/caller-policy-not-enforced` when a write runs
23
+ through a controller login that lacks `write`, `test` or `reboot`. On
24
+ 7.26beta1 RouterOS runs the client script with all four anyway, so the
25
+ login is not the limit (#456). MikroTik says 7.26beta2 refuses this for a
26
+ `read` group. The warning is also in the confirmation
27
+ error. centrs reads the login's own `/user` row and group once, only for a
28
+ write; a failed read only loses the warning.
29
+ - The write gate of a CMR controller fleet command run with plain `execute` or
30
+ `api` names the devices it reaches (#453). `/cmr/device/reboot labels=all`
31
+ used to read as one router; centrs now reads the controller's `/cmr/device`
32
+ inventory first and says, for example, "2 CMR device(s) through controller
33
+ ctl, the controller itself among them" (`error.context.cmr`). The covered commands are
34
+ `/cmr/device/{reboot,upgrade,pair,run-script}`, `/cmr/upgrade/trigger` and
35
+ rule changes on `/cmr/vlan`, `/cmr/wifi`, `/cmr/wifi/radio` and
36
+ `/cmr/upgrade`. New warnings, with or without `--yes`:
37
+ `cmr/selection-includes-controller` and `cmr/rule-selects-every-device`
38
+ (a VLAN rule with no `labels` and no `port-labels` cuts the fleet's
39
+ management access). `cmr/empty-selection` is also a warning here.
40
+ `run-script` and `reboot` carry `cmr/caller-policy-not-enforced` as
41
+ `execute --cmr` does. The command still runs as typed, and a failed read
42
+ never blocks it.
43
+
19
44
  - `execute <controller> --cmr --labels <expr> -- '<command>'` runs one
20
45
  validated command on CMR clients through their controller (RouterOS
21
46
  7.26beta1+; #450, #451). Only the controller is dialed, with its own
@@ -45,8 +70,109 @@ Pre-release (npm tag `next`; `latest` stays on 0.1.8). Install with
45
70
  - `execute --cmr` accepts a command that opens with a relative menu change
46
71
  (`/system; identity/print`); its `:parse` return was read as incomplete.
47
72
 
73
+ ### Changed
74
+
75
+ - Text stdout is data (#470). In text mode, single-target `execute`, `api`,
76
+ `retrieve` and `transfer` write only the result to stdout. Everything else
77
+ goes to **stderr** on success and on failure: the `--verbose` header, one
78
+ `warning [<code>]: <message>` line per warning, and the `Tips:` footer.
79
+ Plain `execute` and `api` used to drop warnings and envelope tips, so a
80
+ `--yes` write never showed, for example, `cmr/rule-selects-every-device` or
81
+ `transport/write-outcome-unknown`. `retrieve` and `transfer` used to mix
82
+ them into stdout. `transport/provider-trust` stays out of text output.
83
+ `--json` keeps everything, `api --raw` is unchanged, and fan-out text still
84
+ lists warnings in its report.
85
+ - **Breaking:** shared meta is written once (#461, #462). In a fan-out
86
+ envelope, each target's `meta.settings` keeps only the keys whose source differs from the
87
+ outer `meta.settings`. Its `meta.operation` drops `request` and `auth` when
88
+ they equal the outer operation's. When every target agrees on `auth`, or on
89
+ a setting the outer `meta.settings` lacks (`api` and `transfer` had none), it
90
+ moves to the outer envelope. An `execute --cmr` member no longer
91
+ repeats the controller in `meta.target.cmr`, because the outer `meta.target`
92
+ is the controller. A 3-client `execute --cmr` result went from 9.8 KB to
93
+ 5.4 KB. `expandFanoutTarget()` returns a target's full meta. A
94
+ `retrieve --follow` frame now carries only `meta.operation`;
95
+ `started`, `synced` and the summary still carry the full meta. A frame line
96
+ went from about 1.15 KB to 270 bytes.
97
+ - **Breaking:** all three streams (`retrieve --follow`, `retrieve --sample`,
98
+ `api --stream`) open with a full-meta `started` line (`data: null`), replacing
99
+ `notice` where it was emitted (#473, #480). Advice is in its `tips`. Only
100
+ `meta.operation` remains on each `frame` or `sample`; the full meta stays
101
+ on `started`, `summary`, and follow's `synced`. Consumers must handle
102
+ `started` and read shared meta from those lines. `apiStream()` now yields
103
+ `ApiStreamLine`, and `retrieveSample()` yields `RetrieveStreamLine`;
104
+ `ApiStreamFrameEnvelope`, `StreamLineEnvelope` and `isStreamDataLine` are
105
+ exported for consumers. API frames also gain `observedAt` (host receipt
106
+ time, ISO 8601). In text mode only frame/sample rows go to stdout; status
107
+ lines, tips, warnings and errors go to stderr. Structured formats keep
108
+ every line on stdout, and `api --raw` omits `started`.
109
+ - `retrieve --follow` frames carry `observedAt` (ISO 8601, host clock): when
110
+ centrs received the reply the frame is built from (#462).
111
+ - With `--query`, a removal the follow's sweep finds is `source: "sweep"`,
112
+ as documented; it was labelled `membership` (#462).
113
+ - An empty selection is an error for a write: `execute`, mutating `api` and
114
+ `transfer upload|remove|mkdir|copy` fan-outs (`cdb/empty-group`,
115
+ `cdb/empty-selection`) and `execute --cmr` (`cmr/empty-selection`) now exit 1
116
+ with `ok: false`, with or without `--yes`. A mistyped group or label passed
117
+ for a successful bulk write with exit 0 (#460). A read over an empty
118
+ selection is unchanged: `ok: true` with the warning.
119
+ - The write gate treats pure script functions as reads, so a structured read
120
+ such as `:put [:serialize to=json [/system/resource/print as-value]]` no
121
+ longer needs `--yes` (#458). The added directives are `:serialize`,
122
+ `:deserialize`, `:convert`, `:delay`, `:find`, `:timestamp`, `:jobname`,
123
+ `:nothing`, `:range`, `:rndnum`, `:rndstr`, `:retry`, `:time` and the `:to*`
124
+ converters, all from the grounded root-command vocabulary. Commands inside
125
+ their arguments and `{…}` bodies are still checked; on CHR 7.24.5 and
126
+ 7.26beta1, RouterOS refuses a quoted string as a script body. `file-name=`
127
+ (`:serialize`'s output file) now gates like `file=`. `api POST
128
+ /console/inspect` is a read.
129
+ - CMR fleet commands (`/cmr/device/{run-script,reboot,upgrade,pair}`,
130
+ `/cmr/upgrade/trigger`, `/cmr/upgrade/job/run-next`) are pinned as writes in
131
+ every spelling, over `execute`, `api` and MCP `mcp=ro`. On 7.26beta1 a
132
+ controller login with only `read` + `api` can run them (#456).
133
+ - CI, release, Pages and the Docker image run Bun 1.4.2 (was 1.3.14), the
134
+ version local development already used; `@types/bun` moves with it (#438).
135
+ `engines.bun` stays `>=1.3.11 <2`. The `typescript` peer stays `^6.0.3`:
136
+ the source passes `tsc --noEmit` under TypeScript 7.0.2, but typedoc 0.28 (the
137
+ API reference build) supports TypeScript up to 6.0.x only.
138
+ - QA records a CHR leg in `qa-history` only when its test step passed or
139
+ failed. A leg cancelled by a newer push, or skipped after a setup failure,
140
+ was recorded as `fail` (#442).
141
+ - CHR integration runs prefetch their selected image before tests start,
142
+ including direct file runs, so a cold-cache download does not consume a
143
+ test timeout. `bun run chr:prefetch` is the explicit entry point (#468).
144
+ - A centrs flag written after `--` is refused before anything dials, as
145
+ `usage/flag-after-separator`, with the corrected command line (#459). This
146
+ applies to `execute` (including `--cmr`) and `explain`. Before, a trailing
147
+ `--json` was sent to RouterOS and failed there as `unknown-attribute`. Only
148
+ a command's own long flags are matched (short aliases such as `-u` stay
149
+ RouterOS input), and password values are not echoed.
150
+
48
151
  ### Fixed
49
152
 
153
+ - Abbreviated output-file arguments (`fi=`, `file-n=`), and the spaced
154
+ `file-name value` form under print/export/serialize, require write
155
+ confirmation in `execute`, including inside scripts, and are refused for
156
+ MCP `mcp=ro` records. RouterOS accepted these spellings while centrs
157
+ recognized only `file=` and `file-name=`, letting file creation bypass
158
+ both gates.
159
+ - The suggested command in `usage/flag-after-separator` redacts credentials
160
+ in positional targets and `--host` values, including a host flag moved
161
+ before `--` and percent-encoded URL credentials. Inline value flags keep their
162
+ flag names in the correction. This closes error-path gaps in the target
163
+ redaction below; explicit URL credentials are refused before CDB lookup too.
164
+ - Serialized error causes keep `name`, `message` and `code` and omit raw JS
165
+ stack traces and their local installation paths (#476, #480).
166
+ - A target with credentials in it (`http://admin:pass@host`, `admin:pass@host`,
167
+ `admin@host`) is refused with `input/invalid-target`, which names
168
+ `--username` and `--password` (#478). centrs never used those credentials,
169
+ and it echoed the whole string, password included, in `meta.target.input`,
170
+ the fan-out selection and the `--verbose` header. The refusal covers the
171
+ positional, `--host`, `CENTRS_HOST`, CDB targets and fan-out literals, and
172
+ any displayed target has `user:pass@` replaced by `***@`, even when the
173
+ password holds `/`, `?` or `#`. A CDB identity containing `@` (`rtr@site`)
174
+ still resolves.
175
+
50
176
  - `execute` and `api` never replay a write that was sent (#448). Fan-out
51
177
  retried every `transport/network` and `transport/connection-closed` failure
52
178
  up to twice, so a write whose connection dropped after the router had run
@@ -56,44 +182,28 @@ Pre-release (npm tag `next`; `latest` stays on 0.1.8). Install with
56
182
  or fan-out mode, carries the new warning `transport/write-outcome-unknown`,
57
183
  because it may have run. Reads keep the retry.
58
184
 
59
- ## 0.1.8 — 2026-10-12
60
-
61
- Release (npm tag `latest`; `next` moves forward to 0.1.8). Everything in
62
- 0.1.8-next.0 below, plus two fixes from field-testing it.
63
-
64
- ### Fixed
65
-
66
- - `retrieve --wait` with no deadline value names `--wait` (#445). Written as a
67
- bare switch, `--wait --until 'expr'` read `--until` as the deadline and
68
- shifted the predicate into a second target, so the CLI reported
69
- `usage/fanout-not-supported`. It now fails `settings/invalid-timeout`
70
- ("Received the flag: --until"), and every deadline without a unit gets the
71
- `--wait`-specific message. A real multi-target `--wait` still gets the
72
- fanout refusal.
73
- - MCP accepts a registered device by its `identity=`, `mac=` or `ip=` comment
74
- key, as the CLI does (#444). The allowlist gate now matches with the same
75
- resolver the call dials with, so the `mcp=` write policy checked is the
76
- policy of the record the call runs against. A name on more than one record
77
- fails `identity/ambiguous` with the candidates in `context.matches` and an
78
- MCP remediation, instead of resolving to the first.
79
-
80
- ## 0.1.8-next.0 — 2026-10-12
185
+ ### Maintenance
81
186
 
82
- Pre-release (npm tag `next`; `latest` stays on 0.1.7). Install with
83
- `bunx @tikoci/centrs@next`.
187
+ - Releases are one dispatch from `main` (#484, #485). `@next` publishes
188
+ `X.Y.Z-next.<run_number>` with no commit, PR or tag, and reuses main's QA
189
+ run on the commit instead of sweeping every channel again. `@latest` creates
190
+ the tag and a GitHub Release. A pushed `v*` tag no longer publishes. The
191
+ changelog keeps one section per stable version; the `0.1.8-next.0` and
192
+ `0.1.9-next.0` sections are folded into theirs.
193
+ - The CodeQL workflow no longer runs the AI-findings probe job (#486). Code
194
+ Quality AI findings are a paid feature this repository does not enable.
195
+ - Workflows reference actions by major tag, never by commit SHA, and
196
+ `SECURITY.md` records why (#266). Every `setup-bun` step reads the Bun
197
+ version from `packageManager`, so changing Bun is a one-line edit. Every
198
+ checkout except the QA history push drops its git credentials.
199
+ - zizmor audits the workflows: in CI as `Analyze (zizmor)` in `codeql.yaml`
200
+ (Security tab), and locally with `bun run lint:actions` (#266). Its first
201
+ run found two `${{ … }}` expansions inside shell scripts, in `lab.yaml` and
202
+ `qa.yaml`; both now go through `env:`.
84
203
 
85
- ### Maintenance
204
+ ## 0.1.8 — 2026-10-12
86
205
 
87
- - Refresh vulnerable dependencies within existing ranges and use Bun's
88
- Dependabot ecosystem for `bun.lock` (#390). Preserve the dependency group
89
- and the `has-flag` major-version ignore. Remaining dependency advisories
90
- and updater verification are tracked in #390.
91
- - Pre-release dependency alignment (#390): runtime ranges now floor at the
92
- tested versions (`@modelcontextprotocol/sdk ^1.32.1`, `zod ^4.6.7`),
93
- `smol-toml` is overridden past its advisories, and `@types/bun` is pinned
94
- exactly to the 1.3.14 runtime (re-pin tracked in #438). The two residual
95
- advisories (`braces`, `katex`) are dev-tool-only and do not ship in the
96
- package.
206
+ Most of this section first shipped as `0.1.8-next.0` (2026-10-12).
97
207
 
98
208
  ### Added
99
209
 
@@ -131,6 +241,20 @@ Pre-release (npm tag `next`; `latest` stays on 0.1.7). Install with
131
241
 
132
242
  ### Fixed
133
243
 
244
+ - `retrieve --wait` with no deadline value names `--wait` (#445). Written as a
245
+ bare switch, `--wait --until 'expr'` read `--until` as the deadline and
246
+ shifted the predicate into a second target, so the CLI reported
247
+ `usage/fanout-not-supported`. It now fails `settings/invalid-timeout`
248
+ ("Received the flag: --until"), and every deadline without a unit gets the
249
+ `--wait`-specific message. A real multi-target `--wait` still gets the
250
+ fanout refusal.
251
+ - MCP accepts a registered device by its `identity=`, `mac=` or `ip=` comment
252
+ key, as the CLI does (#444). The allowlist gate now matches with the same
253
+ resolver the call dials with, so the `mcp=` write policy checked is the
254
+ policy of the record the call runs against. A name on more than one record
255
+ fails `identity/ambiguous` with the candidates in `context.matches` and an
256
+ MCP remediation, instead of resolving to the first.
257
+
134
258
  - `execute` honors an explicit target URL, and native-api honors a typed port
135
259
  (#426, from #384). With no `via` pinned, an `http(s)://host[:port]` target
136
260
  selects rest-api on that scheme and port instead of silently dialing native
@@ -310,6 +434,19 @@ Pre-release (npm tag `next`; `latest` stays on 0.1.7). Install with
310
434
  unchanged apart from those remediation strings and the restored
311
435
  `context.detail`.
312
436
 
437
+ ### Maintenance
438
+
439
+ - Refresh vulnerable dependencies within existing ranges and use Bun's
440
+ Dependabot ecosystem for `bun.lock` (#390). Preserve the dependency group
441
+ and the `has-flag` major-version ignore. Remaining dependency advisories
442
+ and updater verification are tracked in #390.
443
+ - Pre-release dependency alignment (#390): runtime ranges now floor at the
444
+ tested versions (`@modelcontextprotocol/sdk ^1.32.1`, `zod ^4.6.7`),
445
+ `smol-toml` is overridden past its advisories, and `@types/bun` is pinned
446
+ exactly to the 1.3.14 runtime (re-pin tracked in #438). The two residual
447
+ advisories (`braces`, `katex`) are dev-tool-only and do not ship in the
448
+ package.
449
+
313
450
  ## 0.1.7 — 2026-09-27
314
451
 
315
452
  ### Added
package/GLOSSARY.txt CHANGED
@@ -206,6 +206,8 @@ tzsp
206
206
  TZSP
207
207
  UDP
208
208
  UIA
209
+ # unscorable: a corpus observation with no device answer (failed/missing capture, unknown or ambiguous readout); `parseVerdict` in scripts/corpus-db.ts.
210
+ unscorable
209
211
  uvx
210
212
  vcs
211
213
  venv
@@ -216,6 +218,8 @@ vrf
216
218
  webproxy
217
219
  WinAppDriver
218
220
  winbox
221
+ # wiresage/routeros-lsp: third-party RouterOS checker; its 875 conformance cases are the corpus's `wiresage-conformance` collection (lsp-routeros-ts#42).
222
+ wiresage
219
223
  Wireshark
220
224
  workstream
221
225
  workstreams
@@ -318,6 +322,9 @@ REUSEADDR
318
322
  # L2 bridge frame-parsing terms (test/integration/mndp-l2-bridge.ts):
319
323
  # ethertype: 16-bit EtherType field in an Ethernet frame (0x0800 = IPv4).
320
324
  ethertype
325
+ # binpad: OpenSSL `BN_bn2binpad`, a big number as fixed-width big-endian bytes;
326
+ # MAC-Telnet's mtwei.c hashes EC-SRP `z` with it, btest does not (#516).
327
+ binpad
321
328
  # ihl: IPv4 Internet Header Length (low nibble of the first byte, in 32-bit words).
322
329
  ihl
323
330
  # PIIX: Intel PCI-ISA bridge chipset QEMU emulates; shows up in CHR's REST board-name.
@@ -409,6 +416,16 @@ globalname
409
416
  localname
410
417
  setnote
411
418
  runnumber
419
+ # addcomment/setcomment/setname/setdisabled/setnumbers/printdetail/pickbegin: the
420
+ # same IL rendering, quoted as evidence for the spaced-argument rule (GH#350) —
421
+ # "/interface set ether1 comment \"x\"" is "(evl /interface/setcomment=x;numbers=ether1)".
422
+ addcomment
423
+ setcomment
424
+ setname
425
+ setdisabled
426
+ setnumbers
427
+ printdetail
428
+ pickbegin
412
429
  # unconfigured: a freshly-flashed RouterOS device with no IP — the mac-telnet use case.
413
430
  unconfigured
414
431
  unwired
@@ -487,6 +504,14 @@ iff
487
504
  smol
488
505
  katex
489
506
 
507
+ # CI tooling vocabulary
508
+ # %zizmor: GitHub Actions static analyzer; config .github/zizmor.yml (#266).
509
+ # https://docs.zizmor.sh
510
+ # artipacked: zizmor audit for git credentials persisted by checkout.
511
+ zizmor
512
+ zizmorcore
513
+ artipacked
514
+
490
515
  # GitHub Pages build vocabulary (scripts/build-pages-site.ts, pages.yaml)
491
516
  # typedoc/pandoc: the API-reference generator and the markdown->HTML converter
492
517
  # the Pages build chains together. gfm: GitHub-Flavored Markdown, pandoc's
@@ -775,6 +800,8 @@ dgram # Node's `node:dgram` UDP socket module — reached by `mac-telnet`, and o
775
800
  # pair, so its spelling is load-bearing and must not be "corrected".
776
801
  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)
777
802
  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)
803
+ zzznotanarg # an argument name no RouterOS command has — `/system/script add zzznotanarg beep` is `bad parameter zzznotanarg` (7.24.5) or `expected end of command` (7.22.1), which proves the device pairs a spaced `name value` against per-command schema (#350)
804
+ zzzscript # a `…script`-shaped name no command has — `zzzscript={ :beep }` is a `syntax error` on CHR 7.22.1/7.24.5, the control for the script-body brace exemption (#350)
778
805
  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)
779
806
 
780
807
  # #361 absent-menu controls. Deliberate NON-words: each names a path no RouterOS
@@ -789,3 +816,8 @@ nochr # a CLI-Reference syscap that excludes CHR — `/system/license/output` is
789
816
  # #417 CLI Reference refresh (7.26beta1).
790
817
  cmr # MikroTik's optional `cmr` controller package and the `/cmr` menu tree it adds (7.26beta1); the catalog carries `package="cmr"` as its published gate, e.g. `/cmr/device/reboot` (#417)
791
818
  apptraffic # two paths new in 7.26beta1 with different published gates: the `/tool/apptraffic` settings menu (conditions `!mmips, !powerpc, !smips`) and the `/cmr/device/apptraffic` command (package `cmr`, no conditions) (#417)
819
+
820
+ # #498 schema-5 corpus repin.
821
+ rle # the corpus's `highlight-rle-v1` response encoding: each highlight stream stored as run-length `[class, count]` pairs, decoded by `decodeHighlightRuns` in scripts/corpus-db.ts (#498)
822
+ removenumbers # the IL head `/ip/firewall/address-list/removenumbers=…` that `remove [find …]` renders as, quoted verbatim in the bracket-`=` corroboration test (#498)
823
+ vscod # the last segment of a corpus path the forum importer truncated at its slug limit (`…-editors-like-vscod/post-0014-snippet-02.rsc`); cited as the path, so the spelling must match the corpus row (#498)
package/README.md CHANGED
@@ -11,13 +11,15 @@ installing centrs or connecting to a router:
11
11
  bunx @tikoci/centrs@next explain '/ip/address print' --json
12
12
  ```
13
13
 
14
- Use `bunx @tikoci/centrs@next --help` to inspect the full preview CLI. Since
15
- 0.1.6 the dist-tag follows standard SemVer semantics rather than the old
16
- even/odd-minor rule: a plain version (`0.1.6`) publishes to `latest` and carries
17
- `next` forward with it, while a pre-release suffix (`0.1.7-next.0`) publishes to
18
- `next` only and never moves `latest`. So `@next` is the leading edge and can
19
- equal `@latest` between cuts — `npm view @tikoci/centrs dist-tags` is the
20
- authority.
14
+ Use `bunx @tikoci/centrs@next --help` to inspect the full preview CLI, and
15
+ `bunx @tikoci/centrs@next --version` (`-V`; `--json` for an envelope) to print
16
+ the exact version for scripts and field reports. The dist-tag follows standard
17
+ SemVer semantics: a plain version (`0.1.8`) publishes to `latest` and carries
18
+ `next` forward with it, while a `next` build (`0.1.9-next.23`) publishes to
19
+ `next` only and never moves `latest`. A `next` build is described by the
20
+ `## 0.1.9 — next` section of [`CHANGELOG.md`](CHANGELOG.md). So `@next` is the
21
+ leading edge and can equal `@latest` between cuts —
22
+ `npm view @tikoci/centrs dist-tags` is the authority.
21
23
 
22
24
  The full flag reference for every command is generated from the CLI metadata
23
25
  into [`docs/CLI.md`](docs/CLI.md); per-command behavior lives in
@@ -211,8 +213,10 @@ GitHub issues it links are the roadmap.
211
213
 
212
214
  Requirements:
213
215
 
214
- - Bun 1.3.11 or newer (CI pinned to 1.3.14; Bun 2.x not yet validated).
216
+ - Bun 1.3.11 or newer (CI runs the `packageManager` version in `package.json`;
217
+ Bun 2.x not yet validated).
215
218
  - Git.
219
+ - `uv`, only for `bun run lint:actions` (zizmor workflow audit).
216
220
  - For integration tests: QEMU plus `@tikoci/quickchr` (handles CHR image
217
221
  download and boot).
218
222
 
package/SECURITY.md CHANGED
@@ -14,14 +14,22 @@ Please include the affected files or workflow, reproduction details, and impact.
14
14
 
15
15
  The repository's [Security tab](https://github.com/tikoci/centrs/security) is the live source of current alerts and advisories. This section describes *what* checks run and *why*, so the doc stays meaningful even when the badge is at 0.
16
16
 
17
- - **CodeQL** — repo-managed workflow at [`.github/workflows/codeql.yaml`](.github/workflows/codeql.yaml) with config [`.github/codeql-config.yml`](.github/codeql-config.yml). Query suite: `security-and-quality` (security-extended + code-quality) plus a small custom pack, [`.github/codeql-house-rules/`](.github/codeql-house-rules/), that structurally checks a handful of repo-specific invariants (currently: `apiFanout` must call its named protocol/validation gates) — see `.github/instructions/github-security-quality.instructions.md` for scope and the verify-before-promote discipline for adding more rules there. Languages: `javascript-typescript`, `actions`. Schedule: push to `main`, pull requests to `main`, weekly cron, and manual dispatch.
18
- - **Code Quality (AI findings, preview)** — confirmed active in GitHub as of 2026-07-03 (not just "intended"; see [issue #166](https://github.com/tikoci/centrs/issues/166)). [`.github/workflows/codeql.yaml`](.github/workflows/codeql.yaml) includes a non-failing forward-compat probe for a future AI-findings API. AI findings are noisy and self-contradicting; we accept the noise because the second-opinion catches real issues that the static suite misses. Steady-state goal is 0 open findings. False positives are dismissed via the GitHub UI with a written justification — that text is the audit-log contract. Side effect: GitHub's own Code Quality scan runs in parallel with our Advanced Setup and posts a same-named `Analyze (javascript-typescript)` check, so PR checks lists show it twice — cosmetic, tracked in #166, not a required check either way (see issue #118).
17
+ - **CodeQL** — repo-managed workflow at [`.github/workflows/codeql.yaml`](.github/workflows/codeql.yaml) with config [`.github/codeql-config.yml`](.github/codeql-config.yml). Query suite: `security-and-quality` (security-extended + code-quality) plus a small custom pack, [`.github/codeql-house-rules/`](.github/codeql-house-rules/), that structurally checks a handful of repo-specific invariants (currently: `apiFanout` must call its named protocol/validation gates) — see `.github/instructions/github-security-quality.instructions.md` for scope and the verify-before-promote discipline for adding more rules there. Languages: `javascript-typescript`, `actions`. `test/` is excluded (`paths-ignore`); it is not shipped, and its CLI argv credentials over-tainted every CLI render path. Schedule: push to `main`, pull requests to `main`, weekly cron, and manual dispatch.
18
+ - **Code Quality** — GitHub's own scan, enabled in repository settings (not a workflow here). It runs as the separate "CodeQL - Code Quality" run on every PR and push and reports different findings from the CodeQL workflow above. Steady-state goal is 0 open findings. False positives are dismissed via the GitHub UI with a written justification — that text is the audit-log contract. Side effect: it posts a same-named `Analyze (javascript-typescript)` check, so PR checks lists show it twice — cosmetic (see [issue #166](https://github.com/tikoci/centrs/issues/166)), not a required check either way (see issue #118).
19
+ - **Code Quality AI findings** — not used: a paid feature, not enabled for this repository.
20
+ - **zizmor** — GitHub Actions static analysis, as the `Analyze (zizmor)` job in [`.github/workflows/codeql.yaml`](.github/workflows/codeql.yaml). Findings go to the Security tab and do not fail the job. [`.github/zizmor.yml`](.github/zizmor.yml) accepts tag refs (see GitHub Actions below) and turns off the Dependabot cooldown audit. `bun run lint:actions` runs it locally (needs `uv`); it is not part of `lint:ci`.
19
21
  - **Dependency review** — not enabled. GitHub dependency review follows dependency-graph ecosystem support, and this Bun-only repository currently does not have a supported lockfile for that gate.
20
22
  - **Dependabot security updates** — unsupported for Bun in GitHub's [ecosystem support table](https://docs.github.com/en/code-security/reference/supply-chain-security/supported-ecosystems-and-repositories). [`.github/dependabot.yaml`](.github/dependabot.yaml) configures weekly Bun **version updates** and GitHub Actions updates. Dependency security cleanup also requires `bun audit` and reviewed lockfile upgrades.
21
23
  - **Secret scanning** — enabled in GitHub, with push protection.
22
24
  - **Private vulnerability reporting** — enabled via GitHub Security Advisories.
23
25
 
24
- Some GitHub Security tab data, especially Code Quality AI findings, is not reachable through the default workflow `GITHUB_TOKEN` (`repos/{owner}/{repo}/code-quality/findings` 403s there even with `security-events: read`) — it does respond with an elevated/owner-scoped token, but no findings have been recorded yet to confirm the response shape end-to-end (see #166). Human review in the GitHub UI remains part of the security workflow until that's wired up.
26
+ Human review of the Security tab in the GitHub UI is part of the security workflow; no workflow gates on Code Quality findings.
27
+
28
+ ## GitHub Actions
29
+
30
+ - **Action refs are tags, not commit SHAs** (`actions/checkout@v7`, `oven-sh/setup-bun@v2`), for first-party and third-party actions alike. Use the major tag; an action that publishes none (`zizmorcore/zizmor-action`) gets its release tag. This is a decision ([#266](https://github.com/tikoci/centrs/issues/266)), not an oversight, so a scanner's unpinned-action finding is not a defect here: CodeQL's `actions/unpinned-tag` is excluded in [`.github/codeql-config.yml`](.github/codeql-config.yml), and zizmor's `unpinned-uses` accepts tags in [`.github/zizmor.yml`](.github/zizmor.yml). A tag picks up an action's fixes, including security fixes, on the next run. A SHA pin goes stale silently until a bot bumps it. We accept the risk of a moved tag or a compromised release in exchange. Dependabot's weekly `github-actions` updates move major and release tags.
31
+ - **The Bun version lives in one place.** Every `setup-bun` step reads `packageManager` from `package.json` (`bun-version-file`); the Dockerfile base image is the only other copy.
32
+ - **Checkouts drop git credentials** (`persist-credentials: false`) unless the job pushes. The only one that pushes is the QA accumulate job in `qa.yaml`, which commits to the `qa-history` branch.
25
33
 
26
34
  ## Supported versions
27
35
 
@@ -170,14 +170,28 @@ the suspect.
170
170
  ## Write confirmation
171
171
 
172
172
  Read-only **iff** the method is `GET`, or the endpoint terminal verb is
173
- `print`/`get`, or it is `listen`. Everything else is treated as a write and needs
174
- confirmation (`--yes` non-interactively, or a TTY prompt) → otherwise
175
- `usage/confirmation-required`. Two consequences:
173
+ `print`/`get`, or it is `listen`, or it is a POST to exactly `/console/inspect`
174
+ (it describes and parses commands, never runs them; #458). Everything else is
175
+ treated as a write and needs confirmation (`--yes` non-interactively, or a TTY
176
+ prompt) → otherwise `usage/confirmation-required`. Consequences:
176
177
 
177
178
  - A `POST …/print` paged read does **not** prompt (keyed on the verb, not the
178
179
  wire HTTP method).
179
180
  - Streaming does **not** imply read-only: an async-but-mutating command like
180
181
  `/system/license/renew --stream` still confirms.
182
+ - A controller's CMR fleet commands (`/cmr/device/{run-script,reboot,upgrade,pair}`,
183
+ `/cmr/upgrade/trigger`, `/cmr/upgrade/job/run-next`) are writes whatever
184
+ RouterOS policy the login has: on 7.26beta1 `run-script` needs only `read` +
185
+ `api` on the controller (#456).
186
+ Before the gate, `api` reads the controller's `/cmr/device` inventory and
187
+ names the devices the request reaches, the controller's own row included,
188
+ with the same warnings as plain `execute`. Where the reach cannot be
189
+ resolved (for example `/cmr/upgrade/job/run-next`), the gate says so and
190
+ why (#453; see
191
+ [execute → Fleet commands run directly](../execute/README.md#fleet-commands-run-directly-453)).
192
+ `PUT`, `PATCH` and `DELETE` on `/cmr/vlan`, `/cmr/wifi`, `/cmr/wifi/radio`
193
+ and `/cmr/upgrade` are the menus' `add`, `set` and `remove`, and the `*id`
194
+ is the rule they act on.
181
195
 
182
196
  `--raw` does not bypass this gate.
183
197
 
@@ -209,7 +223,7 @@ centrs never substitutes polling.
209
223
  `ip/address/*1/listen` follows one row (`?.id=*1`).
210
224
  - **GET menu + `--stream`** streams one literal print and completes; it does
211
225
  not follow. Its summary carries `tip/stream-print` naming the `/listen`
212
- form (see the notice below). A streamed GET (or a `/listen`) rejects `-f` fields rather than
226
+ form (see `started` below). A streamed GET (or a `/listen`) rejects `-f` fields rather than
213
227
  silently dropping them.
214
228
  - **POST command + `--stream`** runs the literal command with its attributes:
215
229
  `api $R /tool/ping -X POST -f address=10.0.2.2 -f count=3 --stream --yes`.
@@ -219,14 +233,22 @@ centrs never substitutes polling.
219
233
  `usage/conflicting-flags` instead of being ignored. `/execute` retains
220
234
  synchronous `as-string` script semantics. Streaming never makes a command
221
235
  read-only.
222
- - Each wire reply emits one envelope with `meta.operation.stream.kind=frame`
223
- and `reply`: **`re`** (a row; `data` is the row, device `.section`,
236
+ - A stream that passed its preflight opens with one `started` line
237
+ (`meta.operation.stream.kind=started`, `data: null`). It carries the full
238
+ `meta` (target, settings, validation, request, auth) and any advice in
239
+ `tips`, and is not counted in `frames`/`rows`.
240
+ - Each wire reply emits one envelope with `meta.operation.stream.kind=frame`,
241
+ `observedAt` (when centrs received it, host clock, ISO 8601; a slow reader
242
+ does not move it) and `reply`: **`re`** (a row; `data` is the row, device `.section`,
224
243
  `.dead`, `.nextid` and `.about` verbatim) or **`empty`** (RouterOS `!empty`:
225
244
  zero rows; `data` is `null`). RouterOS sends `!empty` for an empty finite
226
245
  read, for **each zero-row tick** of `print interval=`, and for a listen
227
246
  cancelled before it sent anything. A frame received after centrs stopped the
228
247
  stream locally carries `afterStop: true` — that cancellation `!empty` is not
229
- evidence of an empty table. A terminal `!done` is not a frame: its
248
+ evidence of an empty table. A frame carries only `meta.operation` (`kind`,
249
+ `objectCount`, `stream`); the rest is on `started` and the summary
250
+ ([constitution](../../docs/CONSTITUTION.md#result-envelope)). A terminal
251
+ `!done` is not a frame: its
230
252
  attributes, when received, appear in successful summary `data.done`,
231
253
  including a terminal result received during local cancellation. The stop
232
254
  reason still reports the first local bound.
@@ -245,17 +267,19 @@ centrs never substitutes polling.
245
267
  one error envelope without starting a stream.
246
268
  - Structured streams put **all** envelopes, including errors, on stdout as
247
269
  NDJSON and exit nonzero for any failure, even after successful rows. Text
248
- mode uses the same outcome/exit rules. `--raw` retains bare payloads and
249
- stderr errors; it does not provide the structured stream contract.
270
+ mode uses the same exit rules but puts only frame rows on stdout; the
271
+ `started` tips, the summary, warnings and errors go to stderr. `--raw`
272
+ retains bare payloads and stderr errors; it does not provide the
273
+ structured stream contract.
250
274
 
251
275
  `api` never edits a change feed to make it safer; it says what RouterOS will
252
276
  not report as tips (CHR 7.23.7 + 7.24.5). Because that advice depends only on
253
- the request, it arrives **first**: a leading envelope with
254
- `meta.operation.stream.kind=notice`, `data: null` and the `tips`, before any
255
- reply — so a filtered watch that never gets a reply still shows it. A notice is
256
- not a reply and is not counted in `frames`/`rows`; a stream with no advice has
257
- no notice. The same tips are repeated on the terminal summary, successful or
258
- failed. `--raw` omits the notice.
277
+ the request, it arrives **first**, in the `tips` of the leading `started`
278
+ line, before any reply — so a filtered watch that never gets a reply still
279
+ shows it. The same tips are repeated on the terminal summary, successful or
280
+ failed. Advice from the live gate (such as `tip/rest-verb-mapping`) rides the
281
+ same two lines, as it does on a one-shot call. `--raw` omits the `started`
282
+ line.
259
283
 
260
284
  - `tip/filtered-follow` — a `listen` (or `follow`) with a query reports
261
285
  neither rows leaving the filter nor deletes. Follow unfiltered and filter
@@ -277,6 +277,18 @@ key. Contrast example 13, where bare `--raw` skips the preflight entirely. This
277
277
  pair is how you tell a RouterOS rejection from a centrs bug while staying on the
278
278
  bare-passthrough surface.
279
279
 
280
+ ### 23. POST `/console/inspect` is a read: no `--yes` needed (#458)
281
+
282
+ ```bash
283
+ centrs api $R console/inspect -X POST -f request=child -f path=system,identity --username $U --password $P </dev/null
284
+ ```
285
+
286
+ Envelope: `ok: true`, `data` lists the children of `/system/identity`
287
+ (including `print`). Inspect describes and parses commands but never runs them,
288
+ so a POST to exactly `console/inspect` skips the write gate, non-TTY and
289
+ without `--yes`. PUT/PATCH/DELETE to it (add/set/remove) and abbreviations
290
+ such as `con/insp` are still gated like example 15.
291
+
280
292
  ### H1. `--host` stands in for `<router>` (#428)
281
293
 
282
294
  ```bash
@@ -484,8 +496,8 @@ centrs api $A ip/address/listen --proplist address,.id,.dead --duration 3s --via
484
496
  ```
485
497
 
486
498
  The harness deletes a seeded address during each. With `address` alone no
487
- frame carries `.dead` for it, and the leading notice and the summary carry
488
- `tip/follow-proplist`.
499
+ frame carries `.dead` for it, and the leading `started` line and the summary
500
+ carry `tip/follow-proplist`.
489
501
  With `address,.id,.dead` the delete frame is `{ ".id", ".dead": "true" }` and
490
502
  there is no tip.
491
503
 
@@ -497,7 +509,7 @@ centrs api $A ip/address/listen --query interface=ether1 --duration 3s --via nat
497
509
 
498
510
  The harness deletes a seeded `ether1` address while listening. RouterOS sends
499
511
  nothing for it: the only frame is the cancellation `!empty` (`afterStop`).
500
- The first line is a `notice` with `tip/filtered-follow`; the summary is `ok`
512
+ The first line, `started`, carries `tip/filtered-follow`; the summary is `ok`
501
513
  and repeats it. `--raw-query` is sent
502
514
  the same way.
503
515
 
@@ -570,8 +582,8 @@ centrs api $A ip/address --stream --via native-api --port $API_PORT --username $
570
582
  ```
571
583
 
572
584
  The print completes on its own (`stopReason=completed`) with one `re` frame
573
- per row (`rows` equals a one-shot GET's row count); a leading notice and the
574
- summary carry `tip/stream-print` naming `ip/address/listen`. It never becomes a listen.
585
+ per row (`rows` equals a one-shot GET's row count); the leading `started` line
586
+ and the summary carry `tip/stream-print` naming `ip/address/listen`. It never becomes a listen.
575
587
 
576
588
  ### L18. Zero-row ticks are `!empty` frames and do not use up `--count`
577
589
 
@@ -621,7 +633,7 @@ centrs api --where board=chr ip/address --cdb-file $CDB --json
621
633
  The device-class selector matches only record 0, so `data.summary = { total: 1,
622
634
  ok: 1, failed: 0 }`, one inner success. Exit code 0.
623
635
 
624
- ### F3. An empty selection is `ok:true` with summary 0/0/0
636
+ ### F3. An empty selection is `ok:true` with summary 0/0/0 for a read
625
637
 
626
638
  ```bash
627
639
  centrs api --group no-such-group ip/address --cdb-file $CDB --json
@@ -630,6 +642,15 @@ centrs api --group no-such-group ip/address --cdb-file $CDB --json
630
642
  `ok: true`, `data.summary = { total: 0, ok: 0, failed: 0 }`, `data.targets = []`,
631
643
  and a `cdb/empty-group` warning. Exit code 0.
632
644
 
645
+ A mutating method over the same group is an outer error, with or without
646
+ `--yes`, so a misspelled group cannot pass for a successful bulk write (#460):
647
+
648
+ ```bash
649
+ centrs api -X PUT --group no-such-group ip/address -f address=10.99.0.9/32 -f interface=ether1 --yes --cdb-file $CDB --json
650
+ ```
651
+
652
+ `ok: false`, `error.code` `cdb/empty-group`. Exit code 1.
653
+
633
654
  ### F4. A mutating fan-out without `--yes` is rejected, naming the blast radius
634
655
 
635
656
  ```bash
@@ -691,6 +712,23 @@ target fails deterministically: inner `ok:false` with
691
712
  `error.code=target/unresolved` (never a `transport/dns` attempt on `"__default__"`).
692
713
  `data.summary = { total: 1, ok: 0, failed: 1 }`. Exit code 1 (every target failed).
693
714
 
715
+ ## CMR fleet commands (#453)
716
+
717
+ `test/integration/execute-cmr.test.ts` runs these on the CMR lab described in
718
+ [execute → CMR](../execute/examples.md#cmr-clients-through-a-controller-450),
719
+ over both `--via native-api` and `--via rest-api`.
720
+
721
+ ### CMR1. `POST /cmr/device/reboot` names the devices it reaches
722
+
723
+ ```bash
724
+ centrs api --quickchr $CTL -X POST /cmr/device/reboot -f labels=<client>
725
+ ```
726
+
727
+ Without `--yes` (no TTY) this is `usage/confirmation-required`. Its summary
728
+ names `1 CMR device(s)`, and `error.context.cmr` is
729
+ `{devices: 1, controllerIncluded: false, identities: ["<client>"]}`. Nothing
730
+ reboots.
731
+
694
732
  ## `--query` grammar (#397)
695
733
 
696
734
  `--query` takes the `print where` grammar shared with `retrieve --query`. The