@tikoci/centrs 0.1.2 → 0.1.4

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 (134) hide show
  1. package/CHANGELOG.md +98 -0
  2. package/GLOSSARY.txt +284 -0
  3. package/LICENSE +21 -0
  4. package/README.md +55 -11
  5. package/SECURITY.md +3 -3
  6. package/commands/AGENTS.md +7 -1
  7. package/commands/api/README.md +46 -27
  8. package/commands/api/examples.md +124 -6
  9. package/commands/btest/README.md +21 -32
  10. package/commands/check/README.md +376 -53
  11. package/commands/check/examples.md +390 -0
  12. package/commands/devices/README.md +177 -37
  13. package/commands/devices/examples.md +148 -0
  14. package/commands/discover/README.md +5 -11
  15. package/commands/execute/README.md +73 -26
  16. package/commands/execute/examples.md +131 -9
  17. package/commands/explain/README.md +1716 -0
  18. package/commands/explain/examples.md +606 -0
  19. package/commands/retrieve/README.md +55 -71
  20. package/commands/retrieve/examples.md +56 -9
  21. package/commands/settings/README.md +286 -22
  22. package/commands/settings/examples.md +408 -0
  23. package/commands/terminal/README.md +6 -0
  24. package/commands/transfer/README.md +57 -19
  25. package/commands/transfer/examples.md +95 -0
  26. package/docs/CLI.md +353 -0
  27. package/docs/CONSTITUTION.md +102 -26
  28. package/docs/MATRIX.md +106 -173
  29. package/docs/errors/README.md +5 -1
  30. package/docs/errors/cdb/empty-selection.md +19 -0
  31. package/docs/errors/input/incomplete-gps.md +9 -0
  32. package/docs/errors/input/invalid-altitude.md +9 -0
  33. package/docs/errors/input/invalid-bbox.md +9 -0
  34. package/docs/errors/input/invalid-coordinate.md +9 -0
  35. package/docs/errors/input/invalid-radius.md +9 -0
  36. package/docs/errors/internal/settings-failed.md +9 -0
  37. package/docs/errors/quickchr/machine-not-found.md +22 -0
  38. package/docs/errors/quickchr/machine-stopped.md +20 -0
  39. package/docs/errors/quickchr/package-unavailable.md +27 -0
  40. package/docs/errors/quickchr/unsupported-via.md +35 -0
  41. package/docs/errors/quickchr/unsupported.md +35 -0
  42. package/docs/errors/routeros/version-unsupported.md +26 -0
  43. package/docs/errors/settings/consequential-value.md +26 -0
  44. package/docs/errors/settings/invalid-boolean.md +21 -3
  45. package/docs/errors/settings/invalid-format.md +18 -3
  46. package/docs/errors/settings/invalid-integer.md +18 -3
  47. package/docs/errors/settings/invalid-timeout.md +20 -3
  48. package/docs/errors/settings/invalid-via.md +22 -3
  49. package/docs/errors/settings/reserved-key.md +29 -0
  50. package/docs/errors/settings/skip-env-file-active.md +25 -0
  51. package/docs/errors/settings/unknown-key.md +25 -0
  52. package/docs/errors/transport/provider-trust.md +29 -0
  53. package/docs/errors/usage/fanout-not-supported.md +16 -0
  54. package/docs/errors/usage/stdin-ignored.md +28 -0
  55. package/docs/errors/validation/unknown-attribute.md +37 -4
  56. package/package.json +49 -14
  57. package/src/api-fanout.ts +469 -0
  58. package/src/api.ts +171 -56
  59. package/src/cli/api.ts +156 -13
  60. package/src/cli/btest.ts +38 -4
  61. package/src/cli/common.ts +11 -0
  62. package/src/cli/devices.ts +235 -6
  63. package/src/cli/discover.ts +3 -1
  64. package/src/cli/execute.ts +129 -9
  65. package/src/cli/explain.ts +503 -0
  66. package/src/cli/mcp.ts +8 -1
  67. package/src/cli/retrieve.ts +167 -90
  68. package/src/cli/selection.ts +260 -0
  69. package/src/cli/settings.ts +286 -0
  70. package/src/cli/terminal.ts +22 -7
  71. package/src/cli/transfer.ts +192 -51
  72. package/src/cli.ts +22 -2
  73. package/src/core/envelope.ts +5 -2
  74. package/src/core/error-catalog.ts +95 -0
  75. package/src/core/fanout.ts +143 -2
  76. package/src/core/preflight.ts +306 -0
  77. package/src/core/routeros-string.ts +24 -0
  78. package/src/devices.ts +212 -20
  79. package/src/discover.ts +7 -2
  80. package/src/errors.ts +1 -0
  81. package/src/execute-fanout.ts +453 -0
  82. package/src/execute.ts +216 -68
  83. package/src/explain/arg-tokens.ts +114 -0
  84. package/src/explain/args.ts +1403 -0
  85. package/src/explain/blocks.ts +132 -0
  86. package/src/explain/brace-slots.ts +143 -0
  87. package/src/explain/brace-tokens.ts +38 -0
  88. package/src/explain/catalog.ts +1345 -0
  89. package/src/explain/coordinates.ts +329 -0
  90. package/src/explain/defects.ts +217 -0
  91. package/src/explain/is-known-menu.ts +17 -0
  92. package/src/explain/menus.ts +664 -0
  93. package/src/explain/operator-tokens.ts +310 -0
  94. package/src/explain/operators.ts +458 -0
  95. package/src/explain/path-tokens.ts +150 -0
  96. package/src/explain/pathresolve.ts +1324 -0
  97. package/src/explain/quoted-string.ts +241 -0
  98. package/src/explain/scope-brace.ts +308 -0
  99. package/src/explain/segment.ts +757 -0
  100. package/src/explain/string-tokens.ts +67 -0
  101. package/src/explain/symbol-values.ts +520 -0
  102. package/src/explain/symbols.ts +1597 -0
  103. package/src/explain/token-ranges.ts +72 -0
  104. package/src/explain/transport.ts +497 -0
  105. package/src/explain/value-tokens.ts +80 -0
  106. package/src/explain/values.ts +357 -0
  107. package/src/explain/verbs.ts +64 -0
  108. package/src/explain/verbsplit.ts +813 -0
  109. package/src/explain/write.ts +815 -0
  110. package/src/explain.ts +2152 -0
  111. package/src/index.ts +307 -1
  112. package/src/mcp/config.ts +12 -2
  113. package/src/mcp/tools.ts +137 -2
  114. package/src/protocols/adapter.ts +38 -18
  115. package/src/protocols/index.ts +11 -5
  116. package/src/protocols/mac-telnet-console.ts +61 -17
  117. package/src/resolver/cdb.ts +10 -2
  118. package/src/resolver/comment-kv.ts +58 -4
  119. package/src/resolver/config-file.ts +136 -0
  120. package/src/resolver/facts.ts +78 -0
  121. package/src/resolver/geo.ts +453 -0
  122. package/src/resolver/index.ts +65 -0
  123. package/src/resolver/mac.ts +12 -3
  124. package/src/resolver/quickchr-provider.ts +597 -0
  125. package/src/resolver/selection.ts +391 -0
  126. package/src/resolver/service-endpoint.ts +155 -0
  127. package/src/resolver/settings.ts +54 -5
  128. package/src/resolver/target.ts +21 -3
  129. package/src/retrieve-fanout.ts +239 -133
  130. package/src/retrieve.ts +120 -25
  131. package/src/settings.ts +1078 -0
  132. package/src/terminal.ts +11 -1
  133. package/src/transfer-fanout.ts +544 -0
  134. package/src/transfer.ts +133 -40
@@ -486,3 +486,151 @@ Each alias produces the same envelope as its canonical verb — `meta.operation`
486
486
  reports the canonical command (`list`/`show`/`remove`), never the alias. An
487
487
  unknown verb still errors with `input/invalid-command`, and the error lists the
488
488
  canonical verbs plus the accepted aliases.
489
+
490
+ ## Location / GPS
491
+
492
+ Slice 1 of issue #146: `lat`/`lon`/`altitude`/`altitude-type` are recognized
493
+ comment-kv facts (see `README.md`, Location / GPS) — stored, queryable via
494
+ `--where`, and returned as a `location` block in `devices show`/`list` output.
495
+ Coordinate order is **lat-first everywhere**: `--gps <lat>,<lon>[,…]`.
496
+
497
+ ### 42. `add --gps` sets lat/lon/altitude; `show` returns a `location` block
498
+
499
+ ```bash
500
+ centrs devices add 198.51.100.40 --user admin --password p \
501
+ --gps 37.7749,-122.4194,16 --cdb-file $CDB
502
+ ```
503
+
504
+ `ok: true`. Re-reading via `show` returns
505
+ `data.entry.location = { lat: 37.7749, lon: -122.4194, altitude: 16, altitudeType: "MSL" }`
506
+ (the 3-part `--gps` form defaults the missing altitude-type to `MSL`).
507
+
508
+ ### 43. Discrete `--lat`/`--lon` round-trip the same way as `--gps`
509
+
510
+ ```bash
511
+ centrs devices set 192.0.2.5 --lat 51.5072 --lon -0.1276 --cdb-file $CDB
512
+ ```
513
+
514
+ `ok: true`. `devices show 192.0.2.5` returns
515
+ `data.entry.location = { lat: 51.5072, lon: -0.1276 }` (no `altitude` key —
516
+ altitude was never set on this record).
517
+
518
+ ### 44. `--altitude` alone defaults `altitudeType` to `MSL` in the envelope
519
+
520
+ ```bash
521
+ centrs devices set 192.0.2.6 --lat 48.8566 --lon 2.3522 --altitude 35 \
522
+ --cdb-file $CDB
523
+ ```
524
+
525
+ `ok: true`. `altitude-type` was never written to the comment (only
526
+ `--altitude` was passed), but `devices show 192.0.2.6` still returns
527
+ `data.entry.location.altitudeType = "MSL"` — the metadata-only default applied
528
+ at read time (`deviceLocation` in `src/resolver/geo.ts`).
529
+
530
+ ### 45. Setting `lat` without `lon` errors with `input/incomplete-gps`
531
+
532
+ ```bash
533
+ centrs devices set 198.51.100.1 --lat 10 --cdb-file $CDB
534
+ ```
535
+
536
+ Errors with `input/incomplete-gps` (target `198.51.100.1` carries no prior
537
+ `lon`). No write; no backup written.
538
+
539
+ ### 46. An out-of-range `--lat` errors with `input/invalid-coordinate`
540
+
541
+ ```bash
542
+ centrs devices set 192.0.2.5 --lat 200 --lon 0 --cdb-file $CDB
543
+ ```
544
+
545
+ Errors with `input/invalid-coordinate`. No write; no backup written.
546
+
547
+ ### 47. An invalid `--altitude-type` errors with `input/invalid-altitude`
548
+
549
+ ```bash
550
+ centrs devices set 192.0.2.5 --lat 10 --lon 10 --altitude-type wgs84 \
551
+ --cdb-file $CDB
552
+ ```
553
+
554
+ Errors with `input/invalid-altitude`. No write; no backup written.
555
+
556
+ ### 48. Alias flags canonicalize to the stored key; no more `cdb/unknown-option`
557
+
558
+ ```bash
559
+ centrs devices set 192.0.2.5 --latitude 10 --lng 20 --cdb-file $CDB
560
+ ```
561
+
562
+ `ok: true`, **no** `cdb/unknown-option` warning — `--latitude`/`--lng` are
563
+ canonicalized to `lat`/`lon` in one place (`canonicalizeGeoKey`,
564
+ `src/resolver/geo.ts`) before storage. `devices show 192.0.2.5` returns
565
+ `data.entry.comment` containing `lat=10` and `lon=20` (the canonical keys), and
566
+ `data.entry.location = { lat: 10, lon: 20 }`.
567
+
568
+ ### 49. `list --where lat=<verbatim>` matches the exact stored string
569
+
570
+ ```bash
571
+ centrs devices list --where lat=37.774900 --cdb-file $CDB
572
+ ```
573
+
574
+ `ok: true`; matches only the record whose stored `lat` fact is the exact
575
+ string `37.774900` (values are stored verbatim as typed, never reformatted —
576
+ see `README.md`, Location / GPS). `--where lat=37.7749` (numerically equal,
577
+ reformatted) matches nothing.
578
+
579
+ ## Geo query (`list --near` / `list --bbox`)
580
+
581
+ `--near`/`--bbox` filter `devices list` by device GPS (great-circle radius or a
582
+ lat-first bounding box). Coordinates are lat-first everywhere (see `README.md`,
583
+ Location / GPS); a geo-less device carries no location and is silently excluded.
584
+ The same predicates ride the shared fan-out grammar on every non-terminal
585
+ command (`execute`/`retrieve`/`api`/`transfer`) — see `docs/CONSTITUTION.md`
586
+ (Target selection).
587
+
588
+ ### 50. `list --near <lat>,<lon>,<radius>` selects in-radius devices
589
+
590
+ `192.0.2.5` is in San Francisco and `192.0.2.6` in New York (set via `--gps`);
591
+ the other records carry no GPS.
592
+
593
+ ```bash
594
+ centrs devices list --near 37.7749,-122.4194,50km --cdb-file $CDB
595
+ ```
596
+
597
+ `ok: true`; `data` holds only `192.0.2.5`. New York is ~4100 km away (outside the
598
+ 50 km radius) and the geo-less records never match. The radius unit is one of
599
+ `m`/`km`/`mi`/`ft`; a bare number is kilometers.
600
+
601
+ ### 51. `list --bbox <south>,<west>,<north>,<east>` selects devices in the box
602
+
603
+ ```bash
604
+ centrs devices list --bbox 37.70,-122.52,37.83,-122.35 --cdb-file $CDB
605
+ ```
606
+
607
+ `ok: true`; `data` holds only the device whose GPS falls inside the lat-first
608
+ box (edges inclusive). The box must satisfy `south <= north` and `west <= east`
609
+ (no antimeridian wrap), else `input/invalid-bbox`.
610
+
611
+ ### 52. `--near`/`--bbox` are `list`-only
612
+
613
+ ```bash
614
+ centrs devices set 192.0.2.5 --near 37.7749,-122.4194,50km --cdb-file $CDB
615
+ ```
616
+
617
+ Errors with `input/invalid-command`: `--where`/`--near`/`--bbox` *select*
618
+ records for `list`; `add`/`set` name the `<target>` directly. Store GPS with
619
+ `--gps`/`--lat`/`--lon` instead.
620
+
621
+ ### 53. An unrelated `set` on a record with a lone `lat` does not fail
622
+
623
+ The lat/lon pairing check fires only when the write actually touches a
624
+ coordinate. A record that already carries a lone `lat` (e.g. a hand-edited or
625
+ imported comment) can still take unrelated updates.
626
+
627
+ ```bash
628
+ centrs devices add 203.0.113.7 --user u --password p \
629
+ --comment "lat=37.7749" --cdb-file $CDB
630
+ centrs devices set 203.0.113.7 timeout=5000 --cdb-file $CDB
631
+ ```
632
+
633
+ Both succeed: the `add` stores the freeform comment verbatim, and the later
634
+ `set timeout=5000` does **not** raise `input/incomplete-gps` even though the
635
+ record's GPS pair is incomplete. Touching `lat`/`lon` (e.g. `set lat=…`) would
636
+ re-enable the pairing check.
@@ -53,17 +53,11 @@ tested without a router (crafted packet fixtures + a loopback socket).
53
53
 
54
54
  ## Flags and defaults
55
55
 
56
- | Flag | Default | Meaning |
57
- | ---------------- | ------------- | ---------------------------------------------------- |
58
- | `--timeout` | `15s` | Listen window before results are returned. First refresh is sent immediately. |
59
- | `--save` | off | Persist discovered neighbors into the CDB. |
60
- | `--group` | `discovered` | First-class CDB group assigned to saved entries. |
61
- | `--port` | `5678` | UDP port to bind for MNDP. |
62
- | `--cdb-file` | resolved CDB | CDB path override for `--save`. |
63
- | `--cdb-password` | env/none | Decrypt an encrypted CDB; also used to re-encrypt on `--save`. |
64
- | `--format` | `text` | `text`, `json`, or `yaml`; renders the same envelope. |
65
-
66
- `--save` without `--timeout` uses the default 15s window.
56
+ Implemented flags (with their defaults) are generated from the CLI metadata
57
+ into [`docs/CLI.md` → discover](../../docs/CLI.md#discover); this file does
58
+ not duplicate that table. The first refresh is sent immediately, so the
59
+ `--timeout` listen window (default 15s) bounds the wait, and `--save` without
60
+ `--timeout` uses the same default window.
67
61
 
68
62
  Bare `centrs discover` is **read-only** — it returns the envelope and never
69
63
  writes the (WinBox-shared) CDB; `--save` is the explicit opt-in. When it finds
@@ -6,8 +6,9 @@ CLI-shaped commands — there is no separate `update` command.
6
6
 
7
7
  Status: `rest-api`, `native-api`, `mac-telnet`, and `ssh` are `CHR-passed` (see
8
8
  `docs/MATRIX.md` and `commands/execute/examples.md`, examples 1–11 over REST,
9
- 12–18 over the native API, 19–21 over mac-telnet, and S1–S4 over ssh, green via
10
- `bun run test:integration`). `romon` and `winbox-terminal` remain
9
+ 12–19 over the native API, 20–22 over mac-telnet, and S1–S4 over ssh, green via
10
+ `bun run test:integration`), including multi-target fan-out (see **Target
11
+ selection**, examples F1–F5). `romon` and `winbox-terminal` remain
11
12
  `not-started`. SNMP is retrieve-only and rejects `execute`.
12
13
 
13
14
  ## Intent
@@ -15,11 +16,17 @@ Status: `rest-api`, `native-api`, `mac-telnet`, and `ssh` are `CHR-passed` (see
15
16
  - Mirror RouterOS `execute` semantics. Input is a CLI string; output is
16
17
  console-shaped text (or structured records for path+verb writes) wrapped in
17
18
  the standard envelope. This includes write-shaped add/set/remove.
18
- - Validation is two-stage (see constitution: validation): `:put [:parse
19
- "<cmd>"]` is a **syntax** gate only — it does not catch unknown attributes or
20
- bad values. Semantic validation needs `/console/inspect` or the server's own
21
- re-validation on the write round-trip. A clean parse is necessary, not
22
- sufficient.
19
+ - Validation is two-stage (see constitution: validation; grounded on CHR 7.23.3,
20
+ GH#230): a single `:put [:parse "<cmd>"]` covers both the syntax and the
21
+ unknown-attribute (name-level) gate on **every** transport — `:parse` never
22
+ throws; its return value's printed form (`(evl …)` / `(evl bad parameter …)` /
23
+ `(<%% bad command name …)`) is read from the REST/native `ret` *and* the
24
+ console output. That check is necessary, not sufficient: value-type errors pass
25
+ it, and the `/console/inspect` / server re-validation semantic checks are
26
+ separate.
27
+ The outer RouterOS string literal escapes backslash, quote, and dollar so
28
+ validation parses the caller's exact command rather than expanding variables
29
+ in the wrapper.
23
30
  - centrs owns the **script-vs-structured gate** (`canonicalizeExecuteCommand` +
24
31
  `isWriteShaped` in `src/execute.ts`): it decides which validation runs and
25
32
  whether the write-confirm prompt fires. The shared `rosetta`/`lsp-routeros-ts`
@@ -101,24 +108,31 @@ Status: `rest-api`, `native-api`, `mac-telnet`, and `ssh` are `CHR-passed` (see
101
108
 
102
109
  ## Flags
103
110
 
104
- | Flag | Behavior |
111
+ Implemented flags are generated from the CLI metadata into
112
+ [`docs/CLI.md` → execute](../../docs/CLI.md#execute); this file does not
113
+ duplicate that table. Behavior notes the generated reference cannot carry:
114
+
115
+ - `--validate` is transport-appropriate but now one gate on every transport —
116
+ a single `:put [:parse ...]` that is read as a value (GH#230): REST/native
117
+ read the `ret` text (HTTP 200 / `as-string`); mac-telnet and ssh read the
118
+ console's printed value.
119
+ - `--format` defaults to `text`; `CENTRS_FORMAT` sets the default.
120
+ - `--quickchr` resolves host/port/auth from the live `@tikoci/quickchr`
121
+ (0.4.5+, optional dependency) descriptor, bypassing CDB/env resolution for
122
+ those fields.
123
+ - `--` — use it when the command contains tokens that would otherwise be
124
+ claimed as centrs flags; otherwise the command must be quoted as a single
125
+ argument. In fan-out, positional targets come BEFORE `--` and the command
126
+ AFTER it (see **Target selection**).
127
+
128
+ ### Designed, not implemented
129
+
130
+ Spec-tier flags with no implementation yet — today they fail as unknown flags:
131
+
132
+ | Flag | Designed behavior |
105
133
  | ------------------------------- | ------------------------------------------------------------------------------------- |
106
- | `--via <protocol>` | Pin the transport (`native-api`, `rest-api`, `ssh`, or `mac-telnet`). No silent downgrade. A bare MAC target defaults to `mac-telnet`. |
107
- | `--host <host\|url>` | Override the resolved host or base URL for the target. |
108
- | `--port <n>` | Override the resolved management port. |
109
- | `--username` (alias `--user`, `-u`) / `--password` | RouterOS credentials; fall back to `CENTRS_USERNAME` / `CENTRS_PASSWORD`. |
110
- | `--ssh-key <path>` | `--via ssh`: explicit private-key path. Falls back to `CENTRS_SSH_KEY` / the ssh-agent. |
111
- | `--insecure` | Accept a new SSH host key (`--via ssh`) or a self-signed `api-ssl` TLS cert. Default verifies. |
112
- | `--cdb-file` / `--cdb-password` | Read target credentials from (and decrypt) a WinBox CDB file. |
113
- | `--resolve <none\|arp>` | Resolve a MAC-address target to an IP via the host ARP cache. Default `none`. |
114
- | `--timeout <duration>` | Per-request timeout. REST: ≤ 60s; other transports may allow longer. |
115
- | `--validate[=false]` | Run transport-appropriate preflight validation before execution (default `true`): REST/native use `:parse` + `/console/inspect`; mac-telnet and ssh use a single console `:parse` (covers syntax + unknown-attribute). |
116
134
  | `--strict` | Treat any warning on a successful run as an error (`ok: false`, nonzero exit), like `-Werror`. Default is lenient (`ok: true` + `warnings`). |
117
- | `--yes` | Confirm write-shaped add/set/remove commands in non-interactive runs. |
118
- | `--max-bytes <n>` | Byte budget for the rendered envelope; excess output is truncated with a warning + `meta.truncated` (not an error), matching `retrieve`. (Renamed from `--max-results`.) |
119
- | `--format <text\|json\|yaml>` | Output format (alias `--json`). Defaults to `text`; `CENTRS_FORMAT` sets the default. |
120
- | `--where <attr>=<value>` | Device-class selector — run across every CDB record whose stored fact/comment-kv matches (e.g. `--where board=RB5009 /system/package/check-for-updates`). Repeatable (AND). See constitution: target selection. |
121
- | `--` | End-of-options marker. Every token after `--` is taken as the literal RouterOS command, even flag-shaped ones — e.g. `centrs execute $R -- /interface print where disabled=yes`. Use it when the command contains tokens that would otherwise be claimed as centrs flags; otherwise the command must be quoted as a single argument. |
135
+ | `--max-bytes <n>` | Byte budget for the rendered envelope; excess output is truncated with a warning + `meta.truncated` (not an error), matching `retrieve`. (Will replace the implemented `--max-results`, which fails instead of truncating.) |
122
136
 
123
137
  ## SSH key selection
124
138
 
@@ -147,6 +161,39 @@ contents, and any inline key material are always sensitive and must be listed in
147
161
  redacted only if the caller marks them sensitive or the error couples them with
148
162
  key material.
149
163
 
164
+ ## Target selection
165
+
166
+ `execute` runs the **same** RouterOS command across a multi-target selection over
167
+ the shared grammar (multiple `<target>` positionals, repeatable `--group`/
168
+ `--where`, the geo selectors `--near`/`--bbox`, `--all`, `--default`). The
169
+ grammar, the locked `FanoutData` envelope,
170
+ the record-order reassembly, and the granular **0/2/1 exit code** are normative in
171
+ [`docs/CONSTITUTION.md` → Target selection grammar](../../docs/CONSTITUTION.md#target-selection-grammar).
172
+
173
+ execute's positional **boundary is `--`** (it differs from `api`/`retrieve`,
174
+ whose last positional is the operation arg, because an execute command is itself
175
+ many tokens):
176
+
177
+ - `execute r1 r2 -- /system/reboot` — targets `[r1, r2]` before `--`, command
178
+ after. **More than one positional target requires `--`.**
179
+ - `execute --group prod /system/resource/print` — selector-only: targets come
180
+ from the selector and every positional is the command (no `--` needed).
181
+ - `execute r1 /system/resource/print` — a single positional target with no
182
+ selector stays **single-target** (legacy split: first positional is the target,
183
+ the rest is the command), never `FanoutData`.
184
+ - `execute --quickchr lab /system/resource/print` — the quickchr
185
+ named-live-provider claims the target slots like a selector (positionals are
186
+ the command; no `--` needed). One `--quickchr` stays single-target; repeating
187
+ it fans out; mixing it with positionals or CDB selectors is
188
+ `usage/conflicting-flags` (constitution: resolution providers).
189
+
190
+ Because `execute` is the primary **write** path, a write-shaped fan-out
191
+ (add/set/remove) is gated by **`--yes`**, confirmed **once** up front; without it
192
+ the error names the blast radius (how many routers). Write-ness is determined from
193
+ the command alone (`canonicalizeExecuteCommand` + `isWriteShaped`), so the gate
194
+ fires before any target is dialed. Per-target validation still runs because
195
+ RouterOS schemas can differ by version.
196
+
150
197
  ## mac-telnet L2 validation
151
198
 
152
199
  Decided (2026-06-06): the real-L2 integration path is `@tikoci/quickchr`'s
@@ -162,7 +209,7 @@ macOS needs `SO_REUSEPORT`; mcast works on Linux/CI). Grounding: quickchr
162
209
 
163
210
  `execute / mac-telnet` is now **`CHR-passed`** end to end over real L2 against
164
211
  stock CHR 7.23.1 (`test/integration/mac-telnet-console.test.ts` +
165
- `mactelnet-l2-bridge.ts`, examples 19–21): `executeEnvelope` resolves a MAC
212
+ `mactelnet-l2-bridge.ts`, examples 20–22): `executeEnvelope` resolves a MAC
166
213
  target, the `MacTelnetAdapter` opens a `MacTelnetConsole` over a UDP datagram
167
214
  transport, and reads, writes (REST-verified), and validation-rejects all work.
168
215
 
@@ -178,7 +225,7 @@ How it is wired:
178
225
  `transport/capability-unsupported` (mac-telnet is execute/terminal only).
179
226
  - **Validation is one console `:parse`.** Over the console, `:put [:parse "<cmd>"]`
180
227
  prints both `syntax error` and `bad parameter <name>`, so a single gate covers
181
- syntax **and** the unknown-attribute (semantic) check — no `/console/inspect`
228
+ syntax **and** the unknown-attribute (name-level) check — no `/console/inspect`
182
229
  table parsing. Maps to `validation/syntax` / `validation/unknown-attribute`,
183
230
  same codes as REST/native (see `docs/CONSTITUTION.md`, validation).
184
231
  - **Auth (grounded):** MTWEI is required; a stock device offers a 16-byte MD5 salt
@@ -208,7 +255,7 @@ Protocol selection: an unresolved MAC target auto-selects mac-telnet (covered by
208
255
  **decided:** `--strict` promotes any warning on an otherwise-successful run to
209
256
  `ok: false` (nonzero exit), like `-Werror`; the default stays lenient
210
257
  (`ok: true` with the warning in the `warnings` channel). Tracked code
211
- follow-up; the flag is in the table above.
258
+ follow-up; the flag is under **Designed, not implemented** above.
212
259
 
213
260
  These are deferred refinements to the already-grounded single-shot envelope,
214
261
  not blockers for the `rest-api`/`native-api` cells.
@@ -2,8 +2,8 @@
2
2
 
3
3
  Each numbered example is an executable spec, run against a CHR 7.23 router booted
4
4
  by `@tikoci/quickchr`: `test/integration/execute.test.ts` covers examples 1–11
5
- over REST and 12–18 over the native API; `test/integration/mac-telnet-console.test.ts`
6
- covers examples 19–21 over mac-telnet (real L2 via the `socket-connect` bridge).
5
+ over REST and 12–19 over the native API; `test/integration/mac-telnet-console.test.ts`
6
+ covers examples 20–22 over mac-telnet (real L2 via the `socket-connect` bridge).
7
7
  If a line here is not exercised by a test, the test file is wrong; if a line
8
8
  passes only with `validate=false`, the **implementation** is wrong (see
9
9
  `docs/CONSTITUTION.md`).
@@ -62,14 +62,16 @@ The command is a script-shaped console expression, not a slash path plus verb,
62
62
  so REST must use the `/rest/execute` fallback.
63
63
 
64
64
  ```bash
65
- centrs execute $R ':put [/system/identity/get name]' --via rest-api --username $U --password $P
65
+ centrs execute $R ':local a true; :if ($a = true) do={:put [/system/identity/get name]}' --via rest-api --username $U --password $P
66
66
  ```
67
67
 
68
68
  Envelope: `ok: true`, `data` is string-shaped (`data.output` or `data.ret`) and
69
69
  contains the CHR identity name, `meta.via=rest-api`, and
70
70
  `meta.validation.syntax=true`. `meta.validation.semantic` is absent or marked
71
71
  `not-applicable` because `/console/inspect` validates path-shaped commands, not
72
- arbitrary script expressions.
72
+ arbitrary script expressions. The `$a` variable must survive the validation
73
+ wrapper: if that outer string expands it before `:parse`, this example fails even
74
+ though RouterOS accepts the original command.
73
75
 
74
76
  ### 5. Syntax reject from `:put [:parse ...]`
75
77
 
@@ -95,7 +97,7 @@ centrs execute $R '/ip/address/add address=198.51.100.11/32 interface=ether1 no-
95
97
  ```
96
98
 
97
99
  Envelope: `ok: false`, `error.code=validation/unknown-attribute`,
98
- `error.cause.attribute="no-such-arg"` (or equivalent structured field),
100
+ `error.context.parameter="no-such-arg"`,
99
101
  `meta.validation.syntax=true`, `meta.validation.source=/console/inspect`, and
100
102
  no address is added. This example is the product claim: syntax validation and
101
103
  semantic validation are separate gates.
@@ -242,6 +244,23 @@ centrs execute $A '/ip/address/add address=198.51.100.23/32 interface=ether1 com
242
244
  Envelope: `ok: false`, `error.code=usage/confirmation-required`,
243
245
  `meta.via=native-api`, no validation command is sent, and no address is added.
244
246
 
247
+ ### 19. Script-mode command runs through `/execute` with `as-string` (parity with REST)
248
+
249
+ A non-path-shaped console expression over native-api falls through to
250
+ `/execute` the same way REST does (example 4), instead of being refused.
251
+ `as-string` makes native `/execute` block and return the captured output
252
+ rather than scheduling a background job and returning only its id.
253
+
254
+ ```bash
255
+ centrs execute $A ':put [/system/identity/get name]' --via native-api --port $API_PORT --username $U --password $P
256
+ ```
257
+
258
+ Envelope: `ok: true`, `data.ret` is a string containing the CHR identity (same
259
+ shape and cross-transport value as example 4 over REST), `meta.via=native-api`,
260
+ and `meta.validation.syntax=true`. Regression coverage for the bug where the
261
+ native adapter sent `/execute` without `as-string`, silently discarding
262
+ captured output.
263
+
245
264
  ## mac-telnet (`--via mac-telnet`)
246
265
 
247
266
  Layer-2 execute over the RouterOS interactive console (UDP/20561). `$MAC` is the
@@ -252,11 +271,11 @@ is a **single console `:put [:parse …]`** — it reports both `syntax error` a
252
271
  at once (no `/console/inspect` table parsing). The console opens through a
253
272
  first-login license screen (auto-answered) and a ~10s terminal-negotiation stall;
254
273
  **a successful write prints nothing** over the console (no `.id`/`ret`, unlike
255
- REST/native). Examples 19–21 are green via `bun run test:integration`
274
+ REST/native). Examples 20–22 are green via `bun run test:integration`
256
275
  (`test/integration/mac-telnet-console.test.ts`, CHR 7.23.1) over the real L2
257
276
  bridge; the console reader is also exercised directly in the same test.
258
277
 
259
- ### 19. Read a command over mac-telnet
278
+ ### 20. Read a command over mac-telnet
260
279
 
261
280
  ```bash
262
281
  centrs execute $MAC '/system/identity/print' --via mac-telnet --host $MT_HOST --port $MT_PORT --username $U --password $P
@@ -266,7 +285,7 @@ Envelope: `ok: true`, `meta.via=mac-telnet`, `data.ret` is the console output an
266
285
  contains the device identity (cross-checked against REST). `meta.validation.source`
267
286
  is `:put [:parse ...] over mac-telnet`.
268
287
 
269
- ### 20. Write (add) over mac-telnet
288
+ ### 21. Write (add) over mac-telnet
270
289
 
271
290
  ```bash
272
291
  centrs execute $MAC '/ip/address/add address=198.51.100.40/32 interface=ether1' --via mac-telnet --host $MT_HOST --port $MT_PORT --username $U --password $P --yes
@@ -276,7 +295,7 @@ Envelope: `ok: true`, `meta.via=mac-telnet`, `data.ret` is empty (a successful
276
295
  console write prints nothing — there is no `.id` to return). A subsequent REST
277
296
  read of `/ip/address` shows `198.51.100.40`.
278
297
 
279
- ### 21. Validation rejects an unknown attribute over mac-telnet
298
+ ### 22. Validation rejects an unknown attribute over mac-telnet
280
299
 
281
300
  ```bash
282
301
  centrs execute $MAC '/ip/address/add address=198.51.100.41/32 interface=ether1 no-such-arg=x' --via mac-telnet --host $MT_HOST --port $MT_PORT --username $U --password $P --yes
@@ -337,6 +356,73 @@ centrs execute 127.0.0.1 '/ip/address/add address=198.51.100.51/32 interface=eth
337
356
  Envelope: `ok: false`, `error.code=validation/unknown-attribute`, `meta.via=ssh`,
338
357
  and no address is added — the console `:parse` gate catches `bogus` pre-mutation.
339
358
 
359
+ ## Target selection (fan-out)
360
+
361
+ These exercise the shared target-selection grammar
362
+ ([`docs/CONSTITUTION.md` → Target selection grammar](../../docs/CONSTITUTION.md#target-selection-grammar)),
363
+ run by `test/integration/execute-fanout.test.ts` when CHR integration is enabled.
364
+ `$CDB` has two records in group `fanout-chr`: record 0 is the live CHR (comment
365
+ fact `role=edge`) and record 1 is an unreachable REST URL (`role=core`). Note the
366
+ execute `--` boundary: targets/selectors come before it, the command after.
367
+
368
+ ### F1. Group fan-out of a read command
369
+
370
+ ```bash
371
+ centrs execute --group fanout-chr --via rest-api --cdb-file $CDB --json -- /system/resource/print
372
+ ```
373
+
374
+ `ok: true`, `data.summary = { total: 2, ok: 1, failed: 1 }`, targets ordered by
375
+ `recordIndex`, `meta.operation.kind = fanout`; the unreachable target is an inner
376
+ `ok: false` (`transport/connection-refused`). Process exit `2`.
377
+
378
+ ### F2. Empty / unknown group
379
+
380
+ ```bash
381
+ centrs execute --group no-such-group --via rest-api --cdb-file $CDB --json -- /system/resource/print
382
+ ```
383
+
384
+ `ok: true`, `data.summary = { total: 0, ok: 0, failed: 0 }`,
385
+ `warnings` include `cdb/empty-group`. Exit `0`.
386
+
387
+ ### F3. `--where` device-class selector (subset)
388
+
389
+ ```bash
390
+ centrs execute --where role=edge --via rest-api --cdb-file $CDB --json -- /system/resource/print
391
+ ```
392
+
393
+ Selects only record 0: `data.summary = { total: 1, ok: 1, failed: 0 }`,
394
+ `meta.operation.selection.where = ["role=edge"]`, exit `0`.
395
+
396
+ ### F4. `--all` (every CDB record)
397
+
398
+ ```bash
399
+ centrs execute --all --via rest-api --cdb-file $CDB --json -- /system/resource/print
400
+ ```
401
+
402
+ `data.summary = { total: 2, ok: 1, failed: 1 }`,
403
+ `meta.operation.selection.all = true`, exit `2`.
404
+
405
+ ### F5. Write-shaped fan-out without `--yes` is refused (blast radius)
406
+
407
+ ```bash
408
+ centrs execute --group fanout-chr --via rest-api --cdb-file $CDB --json -- /system/identity/set name=fanout-blast
409
+ ```
410
+
411
+ A write-shaped fan-out is gated once up front: `ok: false`,
412
+ `error.code = usage/confirmation-required`, the summary names the blast radius
413
+ (`2 router(s)`), exit `1`. Nothing is mutated. Add `--yes` to fan the write out.
414
+
415
+ ### F6. Multiple positional targets (ad-hoc literals) before `--`
416
+
417
+ ```bash
418
+ centrs execute $REACHABLE_URL $UNREACHABLE_URL --via rest-api --username $U --password $P --json -- /system/resource/print
419
+ ```
420
+
421
+ More than one positional target is fan-out mode (the `--` separates them from the
422
+ command). With no `--cdb-file`, both are ad-hoc literals, labeled by
423
+ `meta.target.input` with no `recordIndex`: `data.summary = { total: 2, ok: 1,
424
+ failed: 1 }`, exit `2`.
425
+
340
426
  ## Protocol selection notes
341
427
 
342
428
  For an unresolved MAC target, execute auto-selection defaults to mac-telnet
@@ -351,3 +437,39 @@ Envelope: `meta.via=mac-telnet` when `$MAC` is not resolved from CDB and no
351
437
  `--host`/`--port`). If the caller wants IP-level REST or native API execution,
352
438
  they must opt into MAC-to-IP resolution (`--resolve arp` + `--via …`) before
353
439
  protocol selection.
440
+
441
+ ## quickchr targets (#134)
442
+
443
+ `$NAME` is the machine name of a running quickchr-managed CHR. Host/port/auth
444
+ come from the live descriptor — the CDB, `CENTRS_HOST`-style env, and the
445
+ `__default__` ladder are bypassed for those fields. Covered by
446
+ `test/integration/quickchr-target.test.ts`.
447
+
448
+ ### Q1. Run a command on a quickchr target (native API default)
449
+
450
+ ```bash
451
+ centrs execute --quickchr $NAME ':put [/system/identity/get name]' --json
452
+ ```
453
+
454
+ Envelope: `ok: true`, `meta.via=native-api` (execute's normal default),
455
+ `meta.target.source.kind=provider`, `meta.target.identity=$NAME`.
456
+
457
+ ### Q2. A `--via` the descriptor does not forward → `quickchr/unsupported-via`
458
+
459
+ ```bash
460
+ centrs execute --quickchr $NAME ':put 1' --via mac-telnet --json
461
+ ```
462
+
463
+ Envelope: `ok: false`, `error.code=quickchr/unsupported-via` — quickchr v1
464
+ forwards `rest-api`/`native-api`/`ssh` only; centrs never falls back to another
465
+ transport.
466
+
467
+ ### Q3. Repeated `--quickchr` fans out; an unknown machine is an inner failure
468
+
469
+ ```bash
470
+ centrs execute --quickchr $NAME --quickchr no-such-machine -- ':put 1' --json
471
+ ```
472
+
473
+ Envelope: outer `ok: true`, `data.summary = { total: 2, ok: 1, failed: 1 }`;
474
+ the unknown machine's inner error is `quickchr/machine-not-found`. Exit code 2
475
+ (partial).