@tikoci/centrs 0.1.2 → 0.1.3

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 (82) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/GLOSSARY.txt +71 -0
  3. package/README.md +24 -8
  4. package/SECURITY.md +3 -3
  5. package/commands/api/README.md +27 -9
  6. package/commands/api/examples.md +109 -6
  7. package/commands/check/README.md +371 -53
  8. package/commands/check/examples.md +390 -0
  9. package/commands/devices/README.md +177 -37
  10. package/commands/devices/examples.md +148 -0
  11. package/commands/execute/README.md +39 -4
  12. package/commands/execute/examples.md +90 -6
  13. package/commands/retrieve/README.md +36 -50
  14. package/commands/retrieve/examples.md +40 -9
  15. package/commands/settings/README.md +286 -22
  16. package/commands/settings/examples.md +408 -0
  17. package/commands/terminal/README.md +6 -0
  18. package/commands/transfer/README.md +32 -2
  19. package/commands/transfer/examples.md +66 -0
  20. package/docs/CONSTITUTION.md +36 -11
  21. package/docs/MATRIX.md +68 -172
  22. package/docs/errors/README.md +2 -1
  23. package/docs/errors/cdb/empty-selection.md +19 -0
  24. package/docs/errors/input/incomplete-gps.md +9 -0
  25. package/docs/errors/input/invalid-altitude.md +9 -0
  26. package/docs/errors/input/invalid-bbox.md +9 -0
  27. package/docs/errors/input/invalid-coordinate.md +9 -0
  28. package/docs/errors/input/invalid-radius.md +9 -0
  29. package/docs/errors/internal/settings-failed.md +9 -0
  30. package/docs/errors/settings/consequential-value.md +26 -0
  31. package/docs/errors/settings/invalid-boolean.md +21 -3
  32. package/docs/errors/settings/invalid-format.md +18 -3
  33. package/docs/errors/settings/invalid-integer.md +18 -3
  34. package/docs/errors/settings/invalid-timeout.md +20 -3
  35. package/docs/errors/settings/invalid-via.md +22 -3
  36. package/docs/errors/settings/reserved-key.md +29 -0
  37. package/docs/errors/settings/skip-env-file-active.md +25 -0
  38. package/docs/errors/settings/unknown-key.md +25 -0
  39. package/docs/errors/usage/fanout-not-supported.md +16 -0
  40. package/package.json +6 -6
  41. package/src/api-fanout.ts +454 -0
  42. package/src/api.ts +91 -19
  43. package/src/cli/api.ts +135 -11
  44. package/src/cli/btest.ts +20 -2
  45. package/src/cli/common.ts +11 -0
  46. package/src/cli/devices.ts +235 -6
  47. package/src/cli/discover.ts +3 -1
  48. package/src/cli/execute.ts +107 -6
  49. package/src/cli/mcp.ts +8 -1
  50. package/src/cli/retrieve.ts +135 -90
  51. package/src/cli/selection.ts +200 -0
  52. package/src/cli/settings.ts +286 -0
  53. package/src/cli/terminal.ts +22 -7
  54. package/src/cli/transfer.ts +171 -47
  55. package/src/cli.ts +7 -0
  56. package/src/core/error-catalog.ts +55 -0
  57. package/src/core/fanout.ts +143 -2
  58. package/src/devices.ts +212 -20
  59. package/src/discover.ts +7 -2
  60. package/src/execute-fanout.ts +438 -0
  61. package/src/execute.ts +120 -23
  62. package/src/index.ts +105 -1
  63. package/src/mcp/config.ts +12 -2
  64. package/src/mcp/tools.ts +137 -2
  65. package/src/protocols/adapter.ts +38 -18
  66. package/src/protocols/index.ts +11 -5
  67. package/src/resolver/cdb.ts +10 -2
  68. package/src/resolver/comment-kv.ts +58 -4
  69. package/src/resolver/config-file.ts +136 -0
  70. package/src/resolver/facts.ts +78 -0
  71. package/src/resolver/geo.ts +453 -0
  72. package/src/resolver/index.ts +46 -0
  73. package/src/resolver/mac.ts +12 -3
  74. package/src/resolver/selection.ts +304 -0
  75. package/src/resolver/settings.ts +54 -5
  76. package/src/resolver/target.ts +20 -2
  77. package/src/retrieve-fanout.ts +225 -133
  78. package/src/retrieve.ts +39 -7
  79. package/src/settings.ts +1078 -0
  80. package/src/terminal.ts +11 -1
  81. package/src/transfer-fanout.ts +527 -0
  82. package/src/transfer.ts +61 -11
package/CHANGELOG.md CHANGED
@@ -8,6 +8,65 @@ documenting cross-cutting shifts that affect contributors and consumers.
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 0.1.3 — 2026-07-06
12
+
13
+ Patch release. `@tikoci/centrs@0.1.3`.
14
+
15
+ ### Added
16
+
17
+ - **`api` command over rest-api + native-api is `CHR-passed`.** `centrs api`
18
+ brings a `gh api`-style structured passthrough to both transports, and folds
19
+ the old standalone `stream` command into `api --stream` (native-api only,
20
+ `--listen` alias): a `NativeApiSession.listen()` reader with `/cancel`
21
+ support emits NDJSON frames plus a run summary. Grounded against real CHR
22
+ 7.23.1.
23
+ - **Multi-target fan-out is uniform across `api`, `retrieve`, `execute`, and
24
+ `transfer`.** A shared `expandCdbSelection` + `runFanout` + `fanoutExitCode`
25
+ core (extracted once, consumed by all four commands) lets any command take a
26
+ CDB selector and run over every matching device. The positional boundary
27
+ that ends the selector differs per command (final positional for
28
+ `api`/`retrieve`, `--` for `execute`, the verb keyword for `transfer`);
29
+ `terminal` and `api --stream` explicitly reject `N > 1` selections since
30
+ neither has a sane multi-target framing.
31
+ - **`settings` command (`print` / `get` / `set` / `reset`).** A managed-key
32
+ registry for the real `CENTRS_*` environment keys backs an atomic
33
+ `centrs.env` writer: `set` validates per key, canonicalizes booleans to
34
+ `1`/`0` on disk, warns (never blocks) on consequential values
35
+ (`insecure=true`, `transfer-via=ftp`), and never partially writes on a
36
+ validation failure; `reset` deletes the managed line rather than blanking it
37
+ (a blank value would itself mean "set to empty" for a bash-sourced file) and
38
+ leaves foreign lines/comments untouched. Bare `centrs settings` behaves like
39
+ `print` regardless of TTY. Four new error/warning codes
40
+ (`settings/reserved-key`, `settings/unknown-key`,
41
+ `settings/consequential-value`, `settings/skip-env-file-active`) plus
42
+ `internal/settings-failed`, all cataloged. `CHR-passed` via 32
43
+ fixture-backed integration examples.
44
+ - **Device GPS: lat/lon/altitude storage + `--near` / `--bbox` geo
45
+ predicates.** `centrs devices` now stores GPS coordinates per device
46
+ (lat-first ordering, chosen so agents guess the field order correctly) and
47
+ supports proximity
48
+ (`--near`) and bounding-box (`--bbox`) selection predicates. `CHR-passed`
49
+ via fixture-backed integration coverage.
50
+ - **Registry⇄MATRIX drift guard + "did you mean?" flag errors.** A CI-checked
51
+ guard keeps the command/protocol registry and `docs/MATRIX.md` from
52
+ silently drifting apart; unrecognized CLI flags now suggest the closest
53
+ known flag instead of a bare rejection.
54
+ - **GitHub Pages: published API reference + error catalog.** `typedoc` output
55
+ and the generated error-code catalog now publish to GitHub Pages on push.
56
+
57
+ ### Fixed
58
+
59
+ - **Windows unit-test portability for `settings` and MNDP.** Path-separator
60
+ and related platform assumptions in the `settings` and MNDP unit suites are
61
+ now Windows-safe (#170).
62
+ - **A real CodeQL-caught ReDoS in the device GPS geo-predicate parser**, fixed
63
+ rather than suppressed.
64
+
65
+ ### Changed
66
+
67
+ - **CodeQL house-rules pack promoted to the default run**, alongside CodeRabbit
68
+ review-tone tuning and a reduced GitHub-checks timeout (15 → 7 minutes).
69
+
11
70
  ## 0.1.2 — 2026-06-26
12
71
 
13
72
  Patch release. `@tikoci/centrs@0.1.2`.
package/GLOSSARY.txt CHANGED
@@ -139,6 +139,7 @@ MNDP
139
139
  NAND # NAND flash memory; write-back caches file writes up to ~40s on some MikroTik boards
140
140
  netmiko # [Multi-vendor library to simplify Paramiko SSH connections to network devices](https://github.com/ktbyers/netmiko)
141
141
  netwatch
142
+ nullglob # bash shopt that makes an unmatched glob expand to zero words instead of the literal pattern
142
143
  npk # MikroTik package format
143
144
  nvme-over-tcp # supported by RouterOS **with** rose-storage.npk extra-package
144
145
  OID
@@ -148,6 +149,7 @@ PRGA
148
149
  proplist
149
150
  pywinauto
150
151
  QML
152
+ qll # CodeQL library file extension (.qll), e.g. CleartextLoggingCustomizations.qll
151
153
  qmljsdebugger
152
154
  quickchr
153
155
  RAML
@@ -161,12 +163,15 @@ rosetta
161
163
  routeros
162
164
  routeros-skills
163
165
  rsc # RouterOS script / export file extension (.rsc)
166
+ SARIF # Static Analysis Results Interchange Format; CodeQL's code-scanning analysis download format
164
167
  scp
165
168
  scriptability
166
169
  scriptable
170
+ shopt # bash builtin for toggling shell options (e.g. nullglob)
167
171
  Sikuli
168
172
  SikuliX
169
173
  skillcheck
174
+ startswith # jq builtin filter that tests whether a string begins with a given prefix
170
175
  SNMP
171
176
  SPI
172
177
  SRP
@@ -184,6 +189,7 @@ TIKOCI
184
189
  tiktui
185
190
  TLV
186
191
  traceroute
192
+ tostring # jq builtin filter that renders a value as its JSON string representation
187
193
  tsc
188
194
  tzsp
189
195
  TZSP
@@ -434,3 +440,68 @@ toarray
434
440
  # Logic shorthand ("if and only if") used in api write-gate doc comments; cspell's
435
441
  # markdown dictionary knows it but the TypeScript one does not.
436
442
  iff
443
+
444
+ # GitHub Pages build vocabulary (scripts/build-pages-site.ts, pages.yaml)
445
+ # typedoc/pandoc: the API-reference generator and the markdown->HTML converter
446
+ # the Pages build chains together. gfm: GitHub-Flavored Markdown, pandoc's
447
+ # input format for the error pages. larr: the "←" left-arrow HTML entity
448
+ # in the back-to-docs link. nojekyll: the marker file that disables GitHub
449
+ # Pages' Jekyll processing for an Actions-deployed artifact. extensionless:
450
+ # describes the `details_url` contract (docs/errors/README.md) — no `.html`.
451
+ typedoc
452
+ pandoc
453
+ gfm
454
+ larr
455
+ nojekyll
456
+ extensionless
457
+ # nothrow: Bun Shell's `$` method to disable throwing on a non-zero exit
458
+ # (scripts/agent-check.ts), so the caller can inspect output either way.
459
+ nothrow
460
+
461
+ # check-command spec vocabulary (commands/check/README.md, examples.md)
462
+ # TikTOML: centrs-owned readable device-inventory format (#137); check's
463
+ # metadata feeds it. routerboard: RouterBOARD firmware/hardware menu
464
+ # (/system/routerboard). rscmd: the tikbook RouterOS command-notebook file
465
+ # extension (.rscmd), the format the check spec was grounded against.
466
+ TikTOML
467
+ routerboard
468
+ rscmd
469
+ # RTT: round-trip time, the host->router connect-latency reach signal. tojson: the
470
+ # /system/script helper (:serialize to=json) the batched execute reads reuse.
471
+ # qrcode: the /ip/cloud vpn-wireguard-client-config-qrcode field token (redacted
472
+ # by the cloud check, referenced when documenting redaction).
473
+ RTT
474
+ tojson
475
+ qrcode
476
+
477
+ # Device GPS vocabulary (issue #146, commands/devices/README.md "Location /
478
+ # GPS", src/resolver/geo.ts). MSL/AGL: the two supported altitude-type vertical
479
+ # datums (mean sea level / above ground level); stored case-insensitively but
480
+ # normalized to upper case. EPSG: the geodetic coordinate-reference-system
481
+ # registry (EPSG:4326 = WGS84 lat/lon), cited for the lat-first ordering
482
+ # rationale. bbox: the --bbox geo selector (bounding box). wgs: WGS84, the
483
+ # geodetic datum GPS coordinates use; appears only as a deliberately-invalid
484
+ # --altitude-type value in tests/examples. antimeridian: the 180° meridian a
485
+ # --bbox may not cross in v1. GEOSEARCH: the Redis command whose m/km/mi/ft
486
+ # radius-unit vocabulary --near borrows. IUGG: the geodesy union whose mean
487
+ # Earth radius the haversine uses. SFO/OAK: airports used as a known-distance
488
+ # haversine fixture in tests. ele/GPX: `ele` is GPX's altitude element, kept as
489
+ # an input alias for `altitude` (GPX = the GPS Exchange Format). Geolocation:
490
+ # the W3C Geolocation API, cited (with ISO 6709 / EPSG / IETF geo:) for why
491
+ # `altitude` — not `elevation` — is the canonical field name.
492
+ MSL
493
+ msl
494
+ AGL
495
+ agl
496
+ Agl
497
+ EPSG
498
+ bbox
499
+ wgs
500
+ antimeridian
501
+ GEOSEARCH
502
+ IUGG
503
+ SFO
504
+ sfo
505
+ ele
506
+ GPX
507
+ Geolocation
package/README.md CHANGED
@@ -4,6 +4,12 @@
4
4
  CLI (with MCP, TUI, and HTTP proxy frontends planned) for talking to MikroTik
5
5
  RouterOS devices through a regularized, validated interface.
6
6
 
7
+ Preview builds are published on npm. To inspect the CLI without installing it:
8
+
9
+ ```bash
10
+ bunx @tikoci/centrs --help
11
+ ```
12
+
7
13
  It is a **friendly conduit**, not a high-level configuration abstraction. It
8
14
  helps humans and agents reach RouterOS over the right protocol, with the right
9
15
  credentials and ports, and validates RouterOS-shaped commands before running
@@ -11,9 +17,12 @@ them. It does **not** hide RouterOS behind helpers like `createVlanOnBridge()`
11
17
  — you still speak RouterOS. Validation and structured diagnostics are the
12
18
  product; without them this would just be a worse `curl`.
13
19
 
14
- > **Status:** early and in active development. The library, device registry, and
15
- > first MCP server phases are CHR-verified; the CLI is wired (`coded`); encrypted-CDB
16
- > writes are still blocked, and the TUI/proxy frontends are planned.
20
+ > **Status:** early npm preview, active development. `@tikoci/centrs` is
21
+ > published on npm, but command flags and envelopes may still change before a
22
+ > stable release. Check current dist-tags with
23
+ > `npm view @tikoci/centrs dist-tags --json`. The library, device registry, and
24
+ > first MCP server phases are CHR-verified; the CLI is wired (`coded`);
25
+ > encrypted-CDB writes are still blocked, and the TUI/proxy frontends are planned.
17
26
  > `docs/MATRIX.md` is the single source of truth for what works today — treat
18
27
  > anything not green there as not-yet-shipped.
19
28
 
@@ -104,16 +113,23 @@ CDB tags, and `devices set` refuses to put them there. Unknown keys produce a
104
113
  `cdb/unknown-option` warning and are ignored, so a stray `=` in human prose
105
114
  never breaks anything.
106
115
 
107
- Settings resolve lowest-to-highest: **built-in defaults → project config →
108
- comment-kv → `CENTRS_*` env → CLI flag / API arg.** Every result envelope
109
- reports which source won each setting under `meta.settings`, so you can always
110
- see *why* a given port or transport was chosen.
116
+ Settings resolve lowest-to-highest: **built-in defaults → user config file
117
+ (`centrs.env`) → comment-kv → `CENTRS_*` env → CLI flag / API arg.** Every
118
+ result envelope reports which source won each setting under `meta.settings`,
119
+ so you can always see *why* a given port or transport was chosen.
111
120
 
112
121
  ### Groups, discovery, and the MCP allowlist all ride on the CDB
113
122
 
114
123
  - **Groups** are CDB-native: an entry's `group` field is just a string, and
115
124
  `--group prod-edge` fans a single command out to every entry that shares it.
116
- There is no separate group-definition file.
125
+ There is no separate group-definition file. Groups are one selector in a
126
+ **uniform fan-out grammar** shared by `api`, `retrieve`, `execute`, and
127
+ `transfer`: mix `<router>` positionals, repeatable `--group`/`--where
128
+ <attr>=<value>`, `--all`, and `--default` (de-duped by CDB record index). Every
129
+ fan-out returns the locked `FanoutData` envelope (per-target results, not
130
+ metadata) with a granular exit code (`0` all ok / `2` partial / `1` all failed);
131
+ a multi-target write is gated once by `--yes` naming the blast radius. See
132
+ `docs/CONSTITUTION.md` (Target selection grammar).
117
133
  - **Discovery** writes back into the CDB: `discover --save` records MNDP
118
134
  neighbors as new entries tagged `group=discovered` with a `source=mndp`
119
135
  marker — hints to curate, not authoritative inventory.
package/SECURITY.md CHANGED
@@ -14,14 +14,14 @@ 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). Languages: `javascript-typescript`, `actions`. Schedule: push to `main`, pull requests to `main`, weekly cron, and manual dispatch.
18
- - **Code Quality (AI findings, preview)** — intended to be enabled in GitHub. [`.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.
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).
19
19
  - **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
20
  - **Dependabot security updates** — enabled and configured in [`.github/dependabot.yaml`](.github/dependabot.yaml).
21
21
  - **Secret scanning** — enabled in GitHub, with push protection.
22
22
  - **Private vulnerability reporting** — enabled via GitHub Security Advisories.
23
23
 
24
- Some GitHub Security tab data, especially Code Quality AI findings, is not currently available through stable unauthenticated APIs. Human review in the GitHub UI remains part of the security workflow.
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.
25
25
 
26
26
  ## Supported versions
27
27
 
@@ -6,17 +6,18 @@ CDB / env / flags, validates the request through `/console/inspect`, runs the
6
6
  single REST or native-API operation, and returns the result in the standard
7
7
  envelope.
8
8
 
9
- Status: `CHR-passed` over `rest-api` and `native-api` (single-target),
10
- including open-ended `--stream` follow (native-api only), per `docs/MATRIX.md`;
11
- multi-target fan-out is a later phase. This file describes intent and flags; the
12
- matrix holds the cell states. Load-bearing rules — envelope, errors, settings
13
- precedence, identity, validation, protocol selection — live in
9
+ Status: `CHR-passed` over `rest-api` and `native-api`, including multi-target
10
+ fan-out and open-ended `--stream` follow (native-api only), per
11
+ `docs/MATRIX.md`. This file describes intent and flags; the matrix holds the
12
+ cell states. Load-bearing rules — envelope, errors, settings precedence,
13
+ identity, validation, protocol selection — live in
14
14
  [`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md); the `(constitution: …)`
15
15
  notes below point there rather than restating them.
16
16
 
17
17
  `api` absorbs the former `stream` command: open-ended follow is `api <router>
18
18
  <endpoint> --stream` (or the `/listen` endpoint form). `--listen` is an accepted
19
- alias of `--stream`.
19
+ alias of `--stream`. Streaming is single-session — it cannot combine with
20
+ multi-target fan-out (see below).
20
21
 
21
22
  ## Where `api` sits — the verb trichotomy
22
23
 
@@ -72,11 +73,28 @@ when a bare-collection `POST` carries create-looking fields.
72
73
  | `--validate[=false]` | Default `true`. Gate is `/console/inspect` (see Validation). |
73
74
  | `--via <protocol>` | `rest-api` (default) or `native-api`. No silent downgrade. |
74
75
  | `--format <json\|yaml\|text>` | Output format. Defaults to `json` for `api` (machine-first); `CENTRS_FORMAT` overrides. Under `--stream`, `json`/`yaml` emit one compact envelope per line (NDJSON); `text` emits a concise row per frame. |
76
+ | `--group <name>` / `--where <attr>=<value>` / `--near <lat>,<lon>,<radius>` / `--bbox <south>,<west>,<north>,<east>` / `--all` / `--default` / `--concurrency <n>` | Multi-target fan-out (see below). Repeatable `--group`/`--where`; `--near`/`--bbox` select by device GPS (lat-first; see constitution: target selection). The union de-dupes by CDB record index. |
75
77
  | target/auth | `--host`, `--port`, `--username`/`--user`/`-u`, `--password`, `--insecure`, `--timeout`, `--cdb-file`, `--cdb-password`, `--resolve <none\|arp>` — same single-target resolver as `retrieve`/`execute`. |
76
78
 
77
- Multi-target fan-out (`--group` / `--where` / `--all` / `--default` /
78
- `--concurrency`, reusing `src/core/fanout.ts`) lands in a later phase. When it
79
- does, `--stream` is single-session: N>1 targets → `usage/fanout-not-supported`.
79
+ ## Fan-out (multi-target)
80
+
81
+ Selecting more than one router — any selector flag (`--group` / `--where` /
82
+ `--near` / `--bbox` / `--all` / `--default`) or more than one positional target — switches `api` into
83
+ **fan-out mode** (`src/api-fanout.ts`, on the shared `src/core/fanout.ts` engine
84
+ and the `src/resolver/selection.ts` grammar). A plain single-positional call
85
+ stays the single-target envelope. Output is the locked `FanoutData` envelope
86
+ (`data = { summary, targets[] }`; outer `ok` = orchestration success; per-target
87
+ failures are inner `ok:false`), and the process exit code is granular: `0`
88
+ all-ok, `2` partial, `1` orchestration error or every target failed. See
89
+ `docs/CONSTITUTION.md` (Target selection) for the shared grammar and `--where`
90
+ vs `--query` distinction.
91
+
92
+ - **Writes fan out under `--yes`**, confirmed once up front (not per target).
93
+ Without `--yes`, the error names the blast radius (how many routers) and that
94
+ `--yes` is required.
95
+ - **`--listen`/`--stream` is single-session** → `usage/fanout-not-supported` in
96
+ fan-out mode. **`--raw` strips the envelope** → `usage/conflicting-flags` in
97
+ fan-out mode (per-target envelopes can't be bare).
80
98
 
81
99
  ## Validation
82
100
 
@@ -1,11 +1,13 @@
1
1
  # api — examples
2
2
 
3
- Each numbered example is an executable spec, asserted by CHR integration tests
4
- (booted by `@tikoci/quickchr`): `test/integration/api.test.ts` (rest-api
5
- examples), `test/integration/api-native.test.ts` (native-api examples, N…), and
6
- `test/integration/api-listen.test.ts` (streaming examples, L…), with example N ↔
7
- assertion N. If a line here is not exercised by a test, the test file is wrong; if
8
- a line passes only with `--validate=false`, the implementation is wrong (see
3
+ Each numbered example is an executable spec asserted by CHR integration tests
4
+ (booted by `@tikoci/quickchr`), example N ↔ assertion N:
5
+ `test/integration/api.test.ts` (rest-api examples),
6
+ `test/integration/api-native.test.ts` (native-api examples, N…),
7
+ `test/integration/api-listen.test.ts` (streaming examples, L…), and
8
+ `test/integration/api-fanout.test.ts` (fan-out examples, F…). If a line here is not
9
+ exercised by a test, the test file is wrong; if a line passes only with
10
+ `--validate=false`, the implementation is wrong (see
9
11
  [`docs/CONSTITUTION.md`](../../docs/CONSTITUTION.md)).
10
12
 
11
13
  `$R` is `<host>:<rest-port>` resolved by quickchr. `$A` is `<host>` and
@@ -323,3 +325,104 @@ centrs api $A ip/address --stream --duration 2s --via native-api --port $API_POR
323
325
 
324
326
  With no change during the window, the stream ends after ~2 s with a summary
325
327
  envelope whose `data.stopReason=duration-elapsed`. Exit code 0.
328
+
329
+ ## fanout (multi-target, F…)
330
+
331
+ These run against a CDB fixture with two records sharing group `$G`: record 0 is
332
+ the live CHR (comment `board=chr`), record 1 is an unreachable host (comment
333
+ `board=dead`). Fan-out output is the locked `FanoutData` envelope
334
+ (`data = { summary, targets[] }`; outer `ok` = orchestration success; per-target
335
+ failures are inner `ok:false`), with the granular exit code (0 all-ok / 2 partial /
336
+ 1 all-failed or orchestration error). See `test/integration/api-fanout.test.ts`.
337
+ The `--raw`/`--listen` + fan-out guards are network-free and validated in
338
+ `test/integration/cli-smoke.test.ts`.
339
+
340
+ ### F1. `--group` fans a GET out; a dead target is an inner failure
341
+
342
+ ```bash
343
+ centrs api --group $G ip/address --cdb-file $CDB --json
344
+ ```
345
+
346
+ `ok: true`, `data.summary = { total: 2, ok: 1, failed: 1 }`, `data.targets[]` in
347
+ record-index order (`[0, 1]`): target 0 is an inner success (`meta.via=rest-api`),
348
+ target 1 is inner `ok:false` with `error.code=transport/connection-refused`.
349
+ `meta.operation.kind=fanout`. Exit code 2.
350
+
351
+ ### F2. `--where` selects only the matching device
352
+
353
+ ```bash
354
+ centrs api --where board=chr ip/address --cdb-file $CDB --json
355
+ ```
356
+
357
+ The device-class selector matches only record 0, so `data.summary = { total: 1,
358
+ ok: 1, failed: 0 }`, one inner success. Exit code 0.
359
+
360
+ ### F3. An empty selection is `ok:true` with summary 0/0/0
361
+
362
+ ```bash
363
+ centrs api --group no-such-group ip/address --cdb-file $CDB --json
364
+ ```
365
+
366
+ `ok: true`, `data.summary = { total: 0, ok: 0, failed: 0 }`, `data.targets = []`,
367
+ and a `cdb/empty-group` warning. Exit code 0.
368
+
369
+ ### F4. A mutating fan-out without `--yes` is rejected, naming the blast radius
370
+
371
+ ```bash
372
+ centrs api -X PUT ip/address -f address=10.99.0.1/32 -f interface=ether1 --group $G --cdb-file $CDB --json
373
+ ```
374
+
375
+ Non-interactive and unconfirmed: outer `ok:false`,
376
+ `error.code=usage/confirmation-required`, and the message names the router count
377
+ (`2 router(s)`) and that `--yes` is required. No write is attempted. Exit code 1.
378
+
379
+ ### F5. A mutating fan-out with `--yes` writes across the selected devices
380
+
381
+ ```bash
382
+ centrs api -X PUT ip/address -f address=10.99.0.7/32 -f interface=ether1 --where board=chr --yes --cdb-file $CDB --json
383
+ ```
384
+
385
+ Confirmed once up front; the add runs on record 0 only (selected by `--where`).
386
+ `data.summary = { total: 1, ok: 1, failed: 0 }`, the inner success carries the
387
+ created `.id`. Exit code 0.
388
+
389
+ The fixture for F6–F9 adds record 2 — the reserved `__default__` credential record.
390
+
391
+ ### F6. `--all` fans across every record except `__default__`
392
+
393
+ ```bash
394
+ centrs api --all ip/address --cdb-file $CDB --json
395
+ ```
396
+
397
+ `data.summary = { total: 2, ok: 1, failed: 1 }`, `data.targets[]` record indices
398
+ `[0, 1]` (record 2 `__default__` is excluded). Exit code 2.
399
+
400
+ ### F7. A positional + `--group` union de-dupes by record index
401
+
402
+ ```bash
403
+ centrs api $R0 --group $G ip/address --cdb-file $CDB --json
404
+ ```
405
+
406
+ `$R0` is record 0's target, also a member of `$G`; the union is `{0, 1}` — record
407
+ indices `[0, 1]`, `total = 2` (not 3). Exit code 2.
408
+
409
+ ### F8. `--concurrency` bounds the worker pool
410
+
411
+ ```bash
412
+ centrs api --group $G --concurrency 1 ip/address --cdb-file $CDB --json
413
+ ```
414
+
415
+ Runs one target at a time; `meta.operation.concurrency = 1`, still
416
+ `data.summary = { total: 2, ok: 1, failed: 1 }`. (`--concurrency 2abc` / `1.5` are
417
+ rejected at parse time with `usage/invalid-concurrency`.) Exit code 2.
418
+
419
+ ### F9. `--default` selects `__default__`, which fails the connectable guard
420
+
421
+ ```bash
422
+ centrs api --default ip/address --cdb-file $CDB --json
423
+ ```
424
+
425
+ The reserved record is a credential fallback, not a connectable router, so its one
426
+ target fails deterministically: inner `ok:false` with
427
+ `error.code=target/unresolved` (never a `transport/dns` attempt on `"__default__"`).
428
+ `data.summary = { total: 1, ok: 0, failed: 1 }`. Exit code 1 (every target failed).