@tikoci/centrs 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +348 -0
  2. package/GLOSSARY.txt +252 -0
  3. package/LICENSE +21 -0
  4. package/README.md +44 -12
  5. package/commands/AGENTS.md +14 -5
  6. package/commands/api/AGENTS.md +13 -1
  7. package/commands/api/README.md +56 -26
  8. package/commands/api/examples.md +46 -1
  9. package/commands/btest/README.md +21 -32
  10. package/commands/check/README.md +6 -1
  11. package/commands/discover/README.md +5 -11
  12. package/commands/execute/README.md +77 -36
  13. package/commands/execute/examples.md +120 -10
  14. package/commands/explain/README.md +2756 -0
  15. package/commands/explain/examples.md +702 -0
  16. package/commands/mcp/README.md +7 -4
  17. package/commands/mcp/examples.md +2 -2
  18. package/commands/retrieve/README.md +25 -27
  19. package/commands/retrieve/examples.md +16 -0
  20. package/commands/terminal/examples.md +7 -1
  21. package/commands/transfer/README.md +28 -20
  22. package/commands/transfer/examples.md +29 -0
  23. package/docs/CLI.md +353 -0
  24. package/docs/CONSTITUTION.md +138 -30
  25. package/docs/MATRIX.md +92 -18
  26. package/docs/errors/README.md +3 -0
  27. package/docs/errors/quickchr/machine-not-found.md +22 -0
  28. package/docs/errors/quickchr/machine-stopped.md +20 -0
  29. package/docs/errors/quickchr/package-unavailable.md +27 -0
  30. package/docs/errors/quickchr/unsupported-via.md +35 -0
  31. package/docs/errors/quickchr/unsupported.md +35 -0
  32. package/docs/errors/routeros/version-unsupported.md +26 -0
  33. package/docs/errors/transport/provider-trust.md +29 -0
  34. package/docs/errors/usage/stdin-ignored.md +28 -0
  35. package/docs/errors/validation/syntax.md +29 -4
  36. package/docs/errors/validation/unknown-attribute.md +37 -4
  37. package/package.json +56 -10
  38. package/src/api-fanout.ts +46 -18
  39. package/src/api.ts +351 -121
  40. package/src/cli/api.ts +26 -7
  41. package/src/cli/btest.ts +19 -3
  42. package/src/cli/execute.ts +25 -6
  43. package/src/cli/explain.ts +503 -0
  44. package/src/cli/retrieve.ts +33 -1
  45. package/src/cli/selection.ts +71 -11
  46. package/src/cli/settings.ts +2 -2
  47. package/src/cli/terminal.ts +1 -1
  48. package/src/cli/transfer.ts +23 -6
  49. package/src/cli.ts +15 -2
  50. package/src/core/envelope.ts +58 -2
  51. package/src/core/error-catalog.ts +42 -1
  52. package/src/core/execute-command.ts +148 -0
  53. package/src/core/inspect.ts +36 -4
  54. package/src/core/preflight.ts +306 -0
  55. package/src/core/routeros-string.ts +24 -0
  56. package/src/core/version.ts +4 -0
  57. package/src/core/yaml.ts +86 -0
  58. package/src/errors.ts +1 -0
  59. package/src/execute-fanout.ts +47 -18
  60. package/src/execute.ts +339 -307
  61. package/src/explain/arg-tokens.ts +151 -0
  62. package/src/explain/args.ts +1749 -0
  63. package/src/explain/blocks.ts +175 -0
  64. package/src/explain/brace-slots.ts +143 -0
  65. package/src/explain/brace-tokens.ts +39 -0
  66. package/src/explain/catalog.ts +1343 -0
  67. package/src/explain/coordinates.ts +488 -0
  68. package/src/explain/defects.ts +218 -0
  69. package/src/explain/escape-tokens.ts +77 -0
  70. package/src/explain/highlight-projection.ts +308 -0
  71. package/src/explain/is-known-menu.ts +17 -0
  72. package/src/explain/menus.ts +668 -0
  73. package/src/explain/operator-tokens.ts +316 -0
  74. package/src/explain/operators.ts +465 -0
  75. package/src/explain/path-tokens.ts +150 -0
  76. package/src/explain/pathresolve.ts +1569 -0
  77. package/src/explain/quoted-string.ts +331 -0
  78. package/src/explain/scope-brace.ts +461 -0
  79. package/src/explain/segment.ts +1165 -0
  80. package/src/explain/separator.ts +187 -0
  81. package/src/explain/split-owner.ts +194 -0
  82. package/src/explain/string-tokens.ts +77 -0
  83. package/src/explain/symbol-values.ts +556 -0
  84. package/src/explain/symbols.ts +1617 -0
  85. package/src/explain/token-ranges.ts +72 -0
  86. package/src/explain/transport.ts +514 -0
  87. package/src/explain/value-tokens.ts +83 -0
  88. package/src/explain/values.ts +357 -0
  89. package/src/explain/verbs.ts +64 -0
  90. package/src/explain/verbsplit.ts +955 -0
  91. package/src/explain/write.ts +856 -0
  92. package/src/explain.ts +2602 -0
  93. package/src/index.ts +225 -6
  94. package/src/mcp/server.ts +2 -1
  95. package/src/offline-gate.ts +148 -0
  96. package/src/protocols/mac-telnet-console.ts +61 -17
  97. package/src/resolver/facts.ts +1 -1
  98. package/src/resolver/index.ts +23 -4
  99. package/src/resolver/quickchr-provider.ts +597 -0
  100. package/src/resolver/selection.ts +99 -12
  101. package/src/resolver/service-endpoint.ts +155 -0
  102. package/src/resolver/target.ts +1 -1
  103. package/src/retrieve-fanout.ts +27 -13
  104. package/src/retrieve.ts +82 -77
  105. package/src/settings.ts +2 -1
  106. package/src/transfer-fanout.ts +43 -26
  107. package/src/transfer.ts +88 -45
@@ -0,0 +1,28 @@
1
+ # `usage/stdin-ignored`
2
+
3
+ stdin was redirected but the input came from the positional argument, so
4
+ anything piped in was not analyzed.
5
+
6
+ This is a **warning**, not an error: the analysis ran and its result is exactly
7
+ what the arguments asked for. What the warning reports is that a second possible
8
+ input source was present and lost.
9
+
10
+ ## Fix
11
+
12
+ Name the source you meant:
13
+
14
+ ```bash
15
+ cat script.rsc | centrs explain --file - # analyze stdin
16
+ centrs explain '/ip/route print' # analyze the positional
17
+ ```
18
+
19
+ The live form spells the same thing (`… | centrs explain edge1 --file -`) and is
20
+ already parsed, but it is refused with
21
+ [`usage/not-implemented`](not-implemented.md) until the phase-2 live probes
22
+ land — so it is not a fix you can apply today.
23
+
24
+ `explain` reads ambient stdin only when no positional could be the input, so a
25
+ positional always wins. It cannot check whether the pipe actually carries bytes
26
+ without consuming fd 0, which is why the collision is reported rather than
27
+ resolved. See [`commands/explain/README.md`](https://github.com/tikoci/centrs/blob/main/commands/explain/README.md)
28
+ for the positional grammar.
@@ -1,9 +1,34 @@
1
1
  # `validation/syntax`
2
2
 
3
- RouterOS rejected the command syntax during the :parse gate.
3
+ The command syntax was rejected by the validation gate — offline analysis, or
4
+ the RouterOS `:parse` probe.
5
+
6
+ The gate has two stages
7
+ ([`docs/CONSTITUTION.md` → Validation is the product](../../CONSTITUTION.md#validation-is-the-product)),
8
+ and `error.context.validationStage` says which one spoke:
9
+
10
+ - **`"offline"`** — centrs rejected the input before any connection was opened,
11
+ and `meta.validation.stages[]` shows the device stage `skipped`: the router was
12
+ never asked. The **corpus-gated analyzer** is the whole of this stage, and
13
+ `error.context.validationSource` names it. `error.context.span` carries the
14
+ offending byte range (end-exclusive) and `error.context.diagnostics[]` the
15
+ analyzer's own `explain/<analyzer>/<slug>` codes.
16
+
17
+ A quote-balance supplement used to sit beside it for the unterminated `"` or
18
+ `'` the analyzer passed, reporting no span. It is gone (GH#355): the analyzer
19
+ rejects a bare `'` itself now, with the byte span, and the supplement's own
20
+ model of `'` as a delimiter was false-rejecting input RouterOS accepts.
21
+ - **absent** — RouterOS itself rejected it, through `:put [:parse "…"]`. When the
22
+ device reported a `(line N column M)`, it is on `error.position`, which is
23
+ RouterOS's authoritative 1-based **byte** column and is never synthesized by
24
+ centrs. An offline rejection never carries `error.position`.
4
25
 
5
26
  ## Fix
6
27
 
7
- See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) for the centrs error
8
- contract. This stub will be expanded with the typical trigger and remediation
9
- for `validation/syntax`.
28
+ Run `centrs explain '<command>'` to see the offending bytes in context, fix the
29
+ RouterOS CLI syntax — quotes and bracketed expressions most often — and retry.
30
+
31
+ `--validate=false` bypasses **both** stages. Use it only to probe a RouterOS edge
32
+ centrs reads wrong, never to work around a rejection you believe is correct: if
33
+ the validator rejects something a real router accepts, the validator is the bug
34
+ (constitution: validation is the product).
@@ -1,9 +1,42 @@
1
1
  # `validation/unknown-attribute`
2
2
 
3
- An attribute is not valid for the path/verb per /console/inspect.
3
+ An attribute or argument is not valid for the requested RouterOS operation.
4
+ The validator rejects the request before RouterOS executes it.
5
+
6
+ ## Context keys
7
+
8
+ Context keys vary by validation mechanism. Callers must not assume every key
9
+ below is present on every `validation/unknown-attribute` rejection.
10
+
11
+ | Key | Type | Notes |
12
+ | --- | ---- | ----- |
13
+ | `parameter` | `string` | First offending name. Present on retrieve and inspect-gated rejections, and on console `:parse` when RouterOS reports `bad parameter <name>`. |
14
+ | `requestedAttributes` | `string[]` | Full requested name list. Retrieve and inspect-gated API/execute validation only. |
15
+ | `availableAttributes` | `string[]` | Names exposed for the path/verb. Retrieve and inspect-gated API/execute validation only. |
16
+ | `path` | `string` | Slash-prefixed RouterOS path (for example `/ip/address`). Retrieve and inspect-gated API/execute validation only. |
17
+ | `verb` | `string` | Verb when applicable (`print`, `add`, `set`, …). Inspect-gated API/execute validation only; retrieve is path-only. |
18
+ | `validationSource` | `string` | Validation provenance. Inspect-gated API/execute and console `:parse` only; retrieve does not currently emit it. |
19
+ | `command` | `string` | Original CLI command. Console `:parse` only. |
20
+ | `detail` | `string` | Raw RouterOS parse diagnostic. Console `:parse` only. |
21
+
22
+ Console `:parse` may also put RouterOS' byte location in the top-level
23
+ `error.position` field. On older RouterOS versions, an unknown name can produce
24
+ only a generic syntax diagnostic; that is reported as `validation/syntax`
25
+ because no parameter identity is available.
26
+
27
+ Provenance:
28
+
29
+ - `src/api.ts:988,1033` — REST/native `validateApiRequest` (print vs add/set branches)
30
+ - `src/execute.ts:742` — REST `validateRestExecute`
31
+ - `src/retrieve.ts:233` — REST `retrieve` attribute projection
32
+ - `src/protocols/mac-telnet-console.ts:674` — console `:parse` (`bad parameter <name>`) over mac-telnet/ssh
33
+ - `src/core/routeros-errors.ts:128` — `!trap`/`detail` `unknown parameter <name>` (routeros/unknown-attribute sibling)
4
34
 
5
35
  ## Fix
6
36
 
7
- See [`docs/CONSTITUTION.md`](../../CONSTITUTION.md) for the centrs error
8
- contract. This stub will be expanded with the typical trigger and remediation
9
- for `validation/unknown-attribute`.
37
+ For `retrieve` and inspect-gated API/execute validation, check the name against
38
+ `/console/inspect`; `retrieve --list-attributes` lists the available properties.
39
+ For console `:parse`, compare `context.parameter` with the RouterOS parameters
40
+ for `context.command`; `context.detail` preserves the device's raw diagnostic.
41
+ Use `--validate=false` only when intentionally probing an undocumented RouterOS
42
+ edge.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tikoci/centrs",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "main": "src/index.ts",
5
5
  "module": "src/index.ts",
6
6
  "repository": {
@@ -9,11 +9,11 @@
9
9
  },
10
10
  "homepage": "https://github.com/tikoci/centrs#readme",
11
11
  "bugs": "https://github.com/tikoci/centrs/issues",
12
- "packageManager": "bun@1.3.13",
12
+ "packageManager": "bun@1.3.14",
13
+ "license": "MIT",
13
14
  "devDependencies": {
14
- "@biomejs/biome": "2.5.2",
15
+ "@biomejs/biome": "2.5.7",
15
16
  "@secretlint/secretlint-rule-preset-recommend": "^13.0.2",
16
- "@tikoci/quickchr": "^0.4.2",
17
17
  "@types/bun": "^1.3.13",
18
18
  "cloc": "^2.6.0-cloc",
19
19
  "cspell": "^10.0.0",
@@ -22,13 +22,16 @@
22
22
  "secretlint": "^13.0.2",
23
23
  "typedoc": "^0.28.19"
24
24
  },
25
- "optionalDependencies": {},
25
+ "optionalDependencies": {
26
+ "@tikoci/quickchr": "^0.4.7"
27
+ },
26
28
  "peerDependencies": {
27
29
  "typescript": "^6.0.3"
28
30
  },
29
31
  "exports": {
30
32
  ".": "./src/index.ts",
31
33
  "./cli": "./src/cli.ts",
34
+ "./explain": "./src/explain.ts",
32
35
  "./mcp": "./src/mcp.ts",
33
36
  "./tui": "./src/tui.ts",
34
37
  "./webproxy": "./src/webproxy.ts",
@@ -45,22 +48,24 @@
45
48
  "files": [
46
49
  "src/",
47
50
  "commands/",
51
+ "docs/CLI.md",
48
52
  "docs/CONSTITUTION.md",
49
53
  "docs/MATRIX.md",
50
54
  "docs/errors/",
51
55
  "README.md",
52
56
  "CHANGELOG.md",
53
57
  "SECURITY.md",
58
+ "LICENSE",
54
59
  "GLOSSARY.txt"
55
60
  ],
56
61
  "scripts": {
57
- "ci": "bun run lint && bun run test && bun run build",
58
- "git:pre-commit": "bun run lint",
62
+ "ci": "bun run lint:ci && bun run test && bun run build",
63
+ "git:pre-commit": "bun run lint:git:commit",
59
64
  "git:pre-push": "bun run lint:git:push",
60
65
  "lint": "bun run lint:biome && bun run lint:typecheck",
61
- "lint:git:commit": "bun run lint",
62
- "lint:git:push": "bun run lint:ci && bun run test && bun run build",
63
- "lint:ci": "bun run lint && bun run lint:markdownlint && bun run lint:cspell && bun run lint:secretlint",
66
+ "lint:git:commit": "bun run lint:biome",
67
+ "lint:git:push": "bun run lint:ci",
68
+ "lint:ci": "bun run lint && bun run docs:cli:check && bun run explain:value-census:readme:check && bun run explain:token-census:readme:check && bun run explain:highlight-agreement:readme:check && bun run explain:operator-readme:check && bun run explain:bracket-equals:readme:check && bun run lint:markdownlint && bun run lint:cspell && bun run lint:secretlint",
64
69
  "lint:cspell": "cspell lint . --gitignore",
65
70
  "lint:cspell:all": "cspell lint . --no-gitignore",
66
71
  "lint:markdownlint": "markdownlint-cli2 \"**/*.md\" \"!.scratch/**\"",
@@ -82,9 +87,50 @@
82
87
  "test:ci": "bun test --coverage",
83
88
  "test:all": "bun run test:ci",
84
89
  "check:agent": "bun run scripts/agent-check.ts",
90
+ "corpus:fetch": "bun run scripts/corpus-fetch.ts",
85
91
  "build": "bun build src/index.ts src/cli.ts src/mcp.ts src/tui.ts src/webproxy.ts src/protocols/index.ts --outdir dist --target bun",
86
92
  "build:doc:api": "typedoc --out docs/api src/index.ts",
93
+ "docs:cli": "bun run scripts/gen-cli-docs.ts",
94
+ "docs:cli:check": "bun run scripts/gen-cli-docs.ts --check",
87
95
  "docs:errors": "bun run scripts/gen-error-pages.ts",
96
+ "explain:bracket-equals": "bun run scripts/explain-bracket-equals.ts",
97
+ "explain:bracket-equals:check": "bun run scripts/explain-bracket-equals.ts --check",
98
+ "explain:bracket-equals:readme": "bun run scripts/explain-bracket-equals.ts --readme",
99
+ "explain:bracket-equals:readme:check": "bun run scripts/explain-bracket-equals.ts --readme --check",
100
+ "explain:browser-consumer": "bun run scripts/explain-browser-consumer.ts",
101
+ "explain:catalog": "bun run scripts/gen-explain-catalog.ts",
102
+ "explain:catalog:check": "bun run scripts/gen-explain-catalog.ts --check",
103
+ "explain:corpus-census": "bun run scripts/explain-corpus-census.ts",
104
+ "explain:highlight-agreement": "bun run scripts/explain-highlight-agreement.ts",
105
+ "explain:highlight-agreement:check": "bun run scripts/explain-highlight-agreement.ts --check",
106
+ "explain:highlight-agreement:readme": "bun run scripts/explain-highlight-agreement.ts --readme",
107
+ "explain:highlight-agreement:readme:check": "bun run scripts/explain-highlight-agreement.ts --readme --check",
108
+ "explain:highlight-slice": "bun run scripts/explain-highlight-slice.ts",
109
+ "explain:menus": "bun run scripts/gen-explain-menus.ts",
110
+ "explain:operator-census": "bun run scripts/explain-operator-census.ts",
111
+ "explain:operator-census:check": "bun run scripts/explain-operator-census.ts --check",
112
+ "explain:operator-readme": "bun run scripts/explain-operator-slice.ts --readme",
113
+ "explain:operator-readme:check": "bun run scripts/explain-operator-slice.ts --readme --check",
114
+ "explain:operator-slice": "bun run scripts/explain-operator-slice.ts",
115
+ "explain:probe:brace-slots": "bun run scripts/probes/explain-brace-slot-sweep.ts",
116
+ "explain:probe:escapes": "bun run scripts/probes/explain-escape-sweep.ts",
117
+ "explain:probe:highlight-recapture": "bun run scripts/probes/explain-highlight-recapture.ts",
118
+ "explain:probe:operators": "bun run scripts/probes/explain-operator-sweep.ts",
119
+ "explain:probe:symbol-anchors": "bun run scripts/probes/explain-symbol-anchors.ts",
120
+ "explain:probe:symbol-arms-score": "bun run scripts/probes/explain-symbol-arms-score.ts",
121
+ "explain:probe:symbol-bad-sigil-replay": "bun run scripts/probes/explain-symbol-bad-sigil-replay.ts",
122
+ "explain:probe:symbol-classes-replay": "bun run scripts/probes/explain-symbol-classes-replay.ts",
123
+ "explain:probe:version-floors": "bun run scripts/probes/explain-version-floors.ts",
124
+ "explain:menus:check": "bun run scripts/gen-explain-menus.ts --check",
125
+ "explain:value-census": "bun run scripts/explain-value-census.ts",
126
+ "explain:value-census:check": "bun run scripts/explain-value-census.ts --check",
127
+ "explain:value-census:readme": "bun run scripts/explain-value-census.ts --readme",
128
+ "explain:value-census:readme:check": "bun run scripts/explain-value-census.ts --readme --check",
129
+ "explain:arg-reach": "bun run scripts/explain-arg-reach-sweep.ts",
130
+ "explain:token-census": "bun run scripts/explain-token-census.ts",
131
+ "explain:token-census:check": "bun run scripts/explain-token-census.ts --check",
132
+ "explain:token-census:readme": "bun run scripts/explain-token-census.ts --readme",
133
+ "explain:token-census:readme:check": "bun run scripts/explain-token-census.ts --readme --check",
88
134
  "security": "bun run lint:secretlint && bun run lint:markdownlint"
89
135
  },
90
136
  "type": "module",
package/src/api-fanout.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * `ApiEnvelope`s into the locked `FanoutData` shape (`src/core/envelope.ts`).
8
8
  *
9
9
  * Boundaries (see `commands/api/README.md`, `docs/CONSTITUTION.md`):
10
- * - The CDB is loaded ONCE (`expandCdbSelection`); a cdb member is resolved
10
+ * - The CDB is loaded ONCE (`expandSelection`); a cdb member is resolved
11
11
  * with its pre-resolved record, so `resolveCdb`'s `__default__` synthetic
12
12
  * fallback never collides ad-hoc literals.
13
13
  * - A `--default` / `__default__` member is guarded from being dialed as the
@@ -39,6 +39,7 @@ import type {
39
39
  CentrsSuccessEnvelope,
40
40
  FanoutData,
41
41
  FanoutSummary,
42
+ ValidationTrace,
42
43
  } from "./core/envelope.ts";
43
44
  import {
44
45
  buildFanoutEnvelope,
@@ -49,19 +50,19 @@ import {
49
50
  runFanout,
50
51
  summarizeFanout,
51
52
  } from "./core/fanout.ts";
53
+ import { toYaml } from "./core/yaml.ts";
52
54
  import { CentrsError, serializeCentrsError } from "./errors.ts";
53
55
  import { promptForWriteConfirmation } from "./execute.ts";
54
56
  import { plannedProtocols, type RouterOsProtocol } from "./protocols/index.ts";
55
57
  import {
56
- type CdbSelectionExpansion,
57
- type CdbSelectionMember,
58
- type CdbSelectionResolveInput,
59
- expandCdbSelection,
58
+ expandSelection,
60
59
  isDefaultRecordTarget,
61
60
  loadEnvFileDefaults,
61
+ type SelectionExpansion,
62
+ type SelectionMember,
63
+ type SelectionResolveInput,
62
64
  type TargetSelection,
63
65
  } from "./resolver/index.ts";
64
- import { toYaml } from "./retrieve.ts";
65
66
 
66
67
  /** Compact description of the selector that produced the fan-out. */
67
68
  export interface ApiSelectionSummary {
@@ -70,6 +71,8 @@ export interface ApiSelectionSummary {
70
71
  all: boolean;
71
72
  default: boolean;
72
73
  positionals: readonly string[];
74
+ /** quickchr machine names (`--quickchr`, repeatable; exclusive of the rest). */
75
+ quickchr?: readonly string[];
73
76
  }
74
77
 
75
78
  export interface ApiFanoutOperationMeta {
@@ -92,10 +95,10 @@ export type ApiFanoutErrorEnvelope =
92
95
  export interface ApiFanoutInternals {
93
96
  expand?: (
94
97
  selection: TargetSelection,
95
- input: CdbSelectionResolveInput,
98
+ input: SelectionResolveInput,
96
99
  env: Record<string, string | undefined>,
97
100
  config?: Record<string, string | undefined>,
98
- ) => Promise<CdbSelectionExpansion>;
101
+ ) => Promise<SelectionExpansion>;
99
102
  execute?: (resolved: ResolvedApiRequest) => Promise<ApiSuccessEnvelope>;
100
103
  sleep?: (ms: number) => Promise<void>;
101
104
  }
@@ -113,6 +116,7 @@ function summarizeSelection(selection: TargetSelection): ApiSelectionSummary {
113
116
  all: selection.all,
114
117
  default: selection.default,
115
118
  positionals: selection.positionals,
119
+ quickchr: selection.quickchr,
116
120
  };
117
121
  }
118
122
 
@@ -120,7 +124,7 @@ function summarizeSelection(selection: TargetSelection): ApiSelectionSummary {
120
124
  function fanoutVia(
121
125
  request: ApiRequest,
122
126
  env: Record<string, string | undefined>,
123
- expansion: CdbSelectionExpansion,
127
+ expansion: SelectionExpansion,
124
128
  ): RouterOsProtocol {
125
129
  // Deliberately CLI/env only, not `config` — see the note above this
126
130
  // function's caller.
@@ -139,7 +143,7 @@ function fanoutVia(
139
143
  return anyNative ? "native-api" : "rest-api";
140
144
  }
141
145
 
142
- function memberTargetMeta(member: CdbSelectionMember): {
146
+ function memberTargetMeta(member: SelectionMember): {
143
147
  input?: string;
144
148
  identity?: string;
145
149
  recordIndex?: number;
@@ -151,6 +155,9 @@ function memberTargetMeta(member: CdbSelectionMember): {
151
155
  recordIndex: member.recordIndex,
152
156
  };
153
157
  }
158
+ if (member.kind === "quickchr") {
159
+ return { input: member.name, identity: member.name };
160
+ }
154
161
  return { input: member.input };
155
162
  }
156
163
 
@@ -163,7 +170,7 @@ function memberTargetMeta(member: CdbSelectionMember): {
163
170
  */
164
171
  function relabelAdhocMember(
165
172
  envelope: ApiEnvelope,
166
- member: CdbSelectionMember,
173
+ member: SelectionMember,
167
174
  ): ApiEnvelope {
168
175
  if (member.kind !== "literal") {
169
176
  return envelope;
@@ -209,7 +216,7 @@ export async function apiFanout(
209
216
  assertApiProtocolSupported(pinnedVia);
210
217
  }
211
218
 
212
- const expand = internals.expand ?? expandCdbSelection;
219
+ const expand = internals.expand ?? expandSelection;
213
220
  const expansion = await expand(
214
221
  selection,
215
222
  {
@@ -240,10 +247,15 @@ export async function apiFanout(
240
247
  await assertFanoutWriteConfirmed(request, requestSummary, method, expansion);
241
248
 
242
249
  const execute = internals.execute ?? runResolvedApi;
250
+ // Per-target validation trace. `execute` and `onExecuteError` are separate
251
+ // callbacks that share only the resolved request, so the request object is
252
+ // the key: without it a per-target error envelope would reconstruct
253
+ // `meta.validation` instead of reporting the gate that actually ran (GH#354).
254
+ const traces = new WeakMap<ResolvedApiRequest, ValidationTrace>();
243
255
  const sleep = internals.sleep ?? defaultFanoutSleep;
244
256
 
245
257
  const targets = await runFanout<
246
- CdbSelectionMember,
258
+ SelectionMember,
247
259
  ResolvedApiRequest,
248
260
  ApiEnvelope
249
261
  >({
@@ -267,6 +279,15 @@ export async function apiFanout(
267
279
  { cdbResolution: member.resolution, config },
268
280
  );
269
281
  }
282
+ if (member.kind === "quickchr") {
283
+ // The live descriptor resolves here, per member — a stopped/unknown
284
+ // machine is an inner per-target failure, not an expansion error.
285
+ return resolveApiRequest(
286
+ { ...request, targetInput: undefined, quickchr: member.name },
287
+ env,
288
+ { config },
289
+ );
290
+ }
270
291
  return resolveApiRequest({ ...request, targetInput: member.input }, env, {
271
292
  config,
272
293
  });
@@ -277,11 +298,18 @@ export async function apiFanout(
277
298
  target: memberTargetMeta(member),
278
299
  warnings: member.kind === "cdb" ? [...member.resolution.warnings] : [],
279
300
  }),
280
- execute: async (resolved, member) =>
281
- relabelAdhocMember(await execute(resolved), member),
301
+ execute: async (resolved, member) => {
302
+ const trace: ValidationTrace = {};
303
+ traces.set(resolved, trace);
304
+ return relabelAdhocMember(await execute(resolved, trace), member);
305
+ },
282
306
  onExecuteError: (resolved, member, error) =>
283
307
  relabelAdhocMember(
284
- buildApiErrorEnvelopeFromResolved(resolved, error),
308
+ buildApiErrorEnvelopeFromResolved(
309
+ resolved,
310
+ error,
311
+ traces.get(resolved),
312
+ ),
285
313
  member,
286
314
  ),
287
315
  sleep,
@@ -302,7 +330,7 @@ async function assertFanoutWriteConfirmed(
302
330
  request: ApiRequest,
303
331
  summary: ApiRequestSummary,
304
332
  method: string,
305
- expansion: CdbSelectionExpansion,
333
+ expansion: SelectionExpansion,
306
334
  ): Promise<void> {
307
335
  if (!summary.write || request.yes) {
308
336
  return;
@@ -329,7 +357,7 @@ async function assertFanoutWriteConfirmed(
329
357
  interface ApiFanoutSuccessInput {
330
358
  summary: FanoutSummary;
331
359
  targets: readonly ApiEnvelope[];
332
- warnings: CdbSelectionExpansion["warnings"];
360
+ warnings: SelectionExpansion["warnings"];
333
361
  via: RouterOsProtocol | null;
334
362
  concurrency: number;
335
363
  requestSummary: ApiRequestSummary;