Skip to content

feat(api,core): multi-target fan-out — shared foundation + api consumer - #114

Merged
mobileskyfi merged 4 commits into
mainfrom
fanout-first-class
Jun 30, 2026
Merged

feat(api,core): multi-target fan-out — shared foundation + api consumer#114
mobileskyfi merged 4 commits into
mainfrom
fanout-first-class

Conversation

@mobileskyfi

@mobileskyfi mobileskyfi commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Why

Fan-out (running a command across many CDB-selected routers) has been the recurring "last item" in command implementations and lived in exactly one place — retrieve, wired only to a single --group. This makes it a first-class, uniform capability: a shared selection grammar + generic orchestrator that every command can adopt. Phase 1 extracts the foundation; Phase 2 makes api its first new consumer — the multi-target fan-out deferred from the centrs api work (#91 follow-ups).

This PR is the api part. Phases 3–5 (retrieve full grammar, execute, transfer) follow in a later PR on the same plan.

Decisions (locked with the maintainer)

  • --where <attr>=<value> matches raw comment facts + core fields (target/identity/group/mac; core wins — no spoofing).
  • Write fan-out is --yes-only (no --force), confirmed once up front, with a blast-radius message naming the router count when missing. --force stays scoped to devices CDB mutations.
  • Granular exit code 0/2/1 (all-ok / partial / all-failed-or-orchestration-error), uniform via fanoutExitCode.
  • Fan-out mode is keyed on intent (any selector flag OR >1 positional target), so a single-positional call keeps the single-target envelope.

What's in it

Foundationsrc/resolver/selection.ts (expandCdbSelection: full grammar, de-dupe by record index, --all excludes __default__, allowAdhoc MCP boundary, absent default CDB → empty registry never created), parseRawCommentFacts, src/cli/selection.ts (shared parser + isFanoutMode + strict --concurrency + shared help rows), src/core/fanout.ts (runFanout, fanoutExitCode, promoted envelope helpers), new usage/fanout-not-supported + cdb/empty-selection codes/pages. retrieve-fanout refactored onto the shared core (retrieve now exits 0/2/1).

api fan-outsrc/api-fanout.ts (apiFanout), resolveApiRequest pre-resolved-CDB override (load once, avoids the __default__ synthetic-target collision), api positional boundary, fan-out dispatch, request-shape validation before expansion, __default__ deterministic target/unresolved guard, --listen/--raw fan-out guards.

Docsdocs/CONSTITUTION.md Target-selection grammar updated to the implemented contract (resolves the prior conflict: FanoutData, 0/2/1, --yes for RouterOS writes); api README/examples (F1–F9); MATRIX; devices README reconciled to the locked envelope.

Validation (CHR 7.23.1)

  • test/integration/api-fanout.test.ts F1–F9 (38 assertions): --group, --where, empty selection, write-reject, write, --all (excludes __default__), positional+group union de-dupe, --concurrency, --default guard.
  • test/integration/fanout-retrieve.test.ts regression (exit 2 on partial).
  • Hermetic cli-smoke guards (--raw/--listen + fan-out).
  • Unit: selection, fanout, api-fanout. Full suite 892 pass / 0 fail; lint + lint:ci + build green.

Incorporates two review rounds

Locked-envelope-only, per-command parser boundaries, --where raw facts + core precedence, __default__ guard, no-resolveCdb collision, fan-out-mode dispatch, MCP allowAdhoc; plus the second round: request-shape validation in fan-out, no default-CDB file creation on read, strict --concurrency, constitution updated in this phase, MATRIX claim backed by F6–F9, selection flags in CLI help, examples header refreshed.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added multi-target “fan-out” support for API and retrieval commands, including grouped target selection, concurrency control, and per-target result reporting.
    • Added clearer handling for empty selections and reserved default-target behavior.
  • Bug Fixes

    • Improved exit codes and error messages for partial failures, unsupported multi-target modes, and conflicting flags.
    • Tightened validation for write operations and streaming/listen usage in multi-target scenarios.
  • Documentation

    • Expanded command and error documentation with updated examples and selection rules.
  • Tests

    • Added integration and unit coverage for fan-out behavior, selection parsing, and error handling.

Make fan-out a first-class, uniform capability. Phase 1 extracts the shared
foundation; Phase 2 makes `api` its first new consumer (the deferred item from
the `centrs api` work). Both CHR-passed on 7.23.1.

Foundation (resolver/selection.ts, cli/selection.ts, core/fanout.ts):
- expandCdbSelection: the full target-selection grammar (positionals + repeatable
  --group/--where + --all + --default), de-duped by record index, ordered by
  record index; --where matches raw comment facts + core fields (core wins);
  --all excludes __default__; allowAdhoc surface policy (MCP keeps the CDB
  allowlist); an absent default CDB is an empty registry, never created.
- parseRawCommentFacts (allowlist-free fact reader).
- shared CLI selection parser + isFanoutMode (keyed on intent) + strict
  --concurrency; selectionCommandOptions for command help.
- generic runFanout + fanoutExitCode (granular 0/2/1) + promoted envelope helpers.
- usage/fanout-not-supported + cdb/empty-selection codes + pages.
- retrieve-fanout refactored onto the shared core; retrieve exit code now 0/2/1.

api fan-out (api-fanout.ts, cli/api.ts):
- apiFanout on runFanout + expandCdbSelection; resolveApiRequest gained a
  pre-resolved-CDB override (load once, no __default__ synthetic collision).
- api positional boundary (final positional = endpoint), fan-out-mode dispatch,
  request-shape validation before expansion.
- --yes-once write confirm naming the blast radius; __default__ deterministic
  target/unresolved guard; --listen/--stream → usage/fanout-not-supported and
  --raw → usage/conflicting-flags in fan-out mode.

Docs: constitution Target-selection grammar updated to the implemented contract
(FanoutData, 0/2/1, --yes for RouterOS writes / --force scoped to devices CDB);
api README + examples (F1-F9) + MATRIX; devices README reconciled.

CHR-passed 7.23.1: test/integration/api-fanout.test.ts (F1-F9) + fanout-retrieve
regression; hermetic cli-smoke guards. Unit: selection, fanout, api-fanout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 30, 2026 17:16
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4156e4ed-35b7-45a7-b16d-46c24f3f19db

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds multi-target fan-out orchestration to the api CLI command. Introduces a CDB selection expansion engine (--group/--where/--all/--default/positionals), shared fanout core utilities (runFanout, buildFanoutEnvelope, fanoutExitCode), a new src/api-fanout.ts orchestration module, CLI wiring for fan-out dispatch, refactored retrieve-fanout to use shared primitives, updated public exports, new error catalog entries, and comprehensive unit/integration tests plus documentation.

Changes

API multi-target fan-out

Layer / File(s) Summary
Selection grammar types and CDB expansion engine
src/resolver/selection.ts, src/resolver/comment-kv.ts, src/resolver/cdb.ts, src/resolver/index.ts
Defines TargetSelection, SelectionWhereClause, CdbSelectionMember, CdbSelectionExpansion; implements expandCdbSelection with --all/--group/--where/--default/positional resolution, single CDB load, de-duplication by record index, and empty-selection warning semantics; exports parseRawCommentFacts, resolutionFromEntry, and selection symbols.
Shared fanout orchestration core and error catalog
src/core/fanout.ts, src/core/error-catalog.ts
Adds runFanout (generic bounded-concurrency resolve+execute loop with retry), fanoutExitCode (0/1/2 mapping), commonVia (protocol collapse), buildFanoutEnvelope (locked success envelope), buildFanoutResolveFailure (inner error envelope); adds cdb/empty-selection and usage/fanout-not-supported to the error catalog.
CLI selection flag parsing helpers
src/cli/selection.ts, src/cli/common.ts
Adds selectionCommandOptions, SelectionFlags accumulator, emptySelectionFlags, consumeSelectionFlag (validates --where format and --concurrency as strict integer via new parseStrictInteger), buildTargetSelection, and isFanoutMode.
api.ts extensions and api-fanout orchestration module
src/api.ts, src/api-fanout.ts
Exports parseApiMethod and adds apiRequestSummaryFromRequest; adds override.cdbResolution bypass in resolveApiRequest; introduces full apiFanout orchestration (single-validation, expand-once, write confirmation via assertFanoutWriteConfirmed, runFanout dispatch), buildApiFanoutErrorEnvelope (pre-flight failure normalization), and renderApiFanoutEnvelope (json/yaml/text rendering).
CLI api fan-out dispatch and retrieve exit code fix
src/cli/api.ts, src/cli/retrieve.ts
Wires selectionCommandOptions into apiCommand; extends ApiCliArgs with selectionFlags/targetPositionals; updates parseApiCliArgs to consume selection flags; adds fan-out branch in runApiCli routing to runApiFanoutCli (with --raw and listen-endpoint guards); updates runRetrieveCli to return fanoutExitCode for group requests.
retrieve-fanout refactored to shared primitives
src/retrieve-fanout.ts
Replaces inlined runBoundedPool+runWithRetry+manual try-catch with a single runFanout call; removes local buildFanoutEnvelope, commonVia, buildResolveFailureEnvelope in favor of shared imports; adds retrieveFanoutEnvelope wrapper.
Public re-exports
src/index.ts
Expands export surface with apiRequestSummaryFromRequest, parseApiMethod, all ApiFanout* types/functions, additional fanout core helpers, and resolver selection types/functions.
Unit tests
test/unit/selection.test.ts, test/unit/fanout.test.ts, test/unit/api-fanout.test.ts
Covers parseRawCommentFacts, isDefaultRecordTarget, expandCdbSelection (group/all/default/where/positional/union/empty/no-CDB); fanoutExitCode, commonVia, buildFanoutEnvelope, buildFanoutResolveFailure, runFanout; CLI positional parsing and apiFanout orchestration (partial failure, empty selection, write confirmation, __default__ resolution).
Integration tests and documentation
test/integration/api-fanout.test.ts, test/integration/cli-smoke.test.ts, test/integration/fanout-retrieve.test.ts, docs/..., commands/...
Adds nine F1–F9 CHR scenarios; smoke tests for --listen/--group and --raw/--group flag rejections; updates retrieve exit code expectation to 2; updates CONSTITUTION.md, MATRIX.md, commands/api/README.md, commands/api/examples.md, commands/devices/README.md; adds docs/errors/cdb/empty-selection.md and docs/errors/usage/fanout-not-supported.md.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • tikoci/centrs#42: Uses the same subprocess smoke/envelope parsing harness in test/integration/cli-smoke.test.ts that this PR extends with new api flag-validation cases.
  • tikoci/centrs#109: Established the src/core/fanout.ts groundwork that this PR extends with runFanout, buildFanoutEnvelope, and related helpers.
  • tikoci/centrs#110: Introduced the centrs api command flows in src/api.ts/src/cli/api.ts that this PR extends with multi-target fan-out orchestration.

Poem

🐇 Hop hop, many routers in a row,
Fan-out sends requests, watch the packets flow!
--group, --where, --all — I'll pick them right,
One confirmation gates each write.
Per-target envelopes, exit codes so clean,
The fluffiest fleet runner you have ever seen! 🌟

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the PR, but it does not follow the required template sections for Links, Change type, and Notes. Rewrite it into the repository template with Links, Change type, and Notes sections, including validation run and RouterOS/protocol assumptions.
Docstring Coverage ⚠️ Warning Docstring coverage is 46.30% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the main change: multi-target fan-out and its shared/api scope.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fanout-first-class

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements the shared multi-target fan-out foundation (selection grammar + core orchestration helpers) and makes centrs api the first new consumer, including the locked FanoutData envelope shape and the granular 0/2/1 exit-code contract.

Changes:

  • Added shared target-selection parsing + CDB selection expansion (--group/--where/--all/--default) and generic fan-out runner helpers.
  • Implemented api fan-out mode (CLI boundary, multi-target dispatch, write confirmation via --yes, and fan-out guards for --raw/--listen).
  • Updated docs + tests to reflect/validate the uniform fan-out contract (including retrieve’s exit code on partial failure).

Reviewed changes

Copilot reviewed 27 out of 27 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/unit/selection.test.ts Unit coverage for selection expansion (--group, --where, --all, literals, empty selection, absent default CDB).
test/unit/fanout.test.ts Unit coverage for new core fan-out helpers (runFanout, fanoutExitCode, buildFanoutEnvelope, etc.).
test/unit/api-fanout.test.ts Unit tests for api fan-out behavior + CLI positional boundary + concurrency parsing strictness.
test/integration/fanout-retrieve.test.ts Updates retrieve fan-out integration expectations to use exit code 2 on partial failure.
test/integration/cli-smoke.test.ts Adds hermetic CLI guards for incompatible fan-out flags (api + --listen / --raw).
test/integration/api-fanout.test.ts New CHR-backed integration coverage for api fan-out scenarios (F1–F9).
src/retrieve-fanout.ts Refactors retrieve group fan-out to the shared runFanout + shared envelope helpers.
src/resolver/selection.ts New shared selection expansion engine (expandCdbSelection) implementing the fan-out grammar.
src/resolver/index.ts Re-exports new selection and raw-comment-facts utilities.
src/resolver/comment-kv.ts Adds parseRawCommentFacts for --where device-class selection.
src/resolver/cdb.ts Exports resolutionFromEntry for reuse by selection expansion.
src/index.ts Public exports updated to include new fan-out/selection APIs.
src/core/fanout.ts Adds generic runFanout, fanoutExitCode, commonVia, and shared envelope builders.
src/core/error-catalog.ts Registers cdb/empty-selection and usage/fanout-not-supported.
src/cli/selection.ts New shared CLI selection flag parsing (consumeSelectionFlag, isFanoutMode, help rows).
src/cli/retrieve.ts Uses fanoutExitCode for retrieve group fan-out.
src/cli/common.ts Adds parseStrictInteger utility for strict numeric flag parsing.
src/cli/api.ts Adds selection parsing, fan-out dispatch path, and fan-out output + exit code behavior.
src/api.ts Adds resolveApiRequest override seam + exports request-summary builder for fan-out meta.
src/api-fanout.ts New apiFanout orchestrator (selection expansion, write confirmation, per-target envelopes).
docs/MATRIX.md Updates api cell status narrative to include fan-out + associated tests.
docs/errors/usage/fanout-not-supported.md New error page for fan-out-incompatible modes (single-session surfaces).
docs/errors/cdb/empty-selection.md New error page explaining empty selection warning semantics.
docs/CONSTITUTION.md Updates the authoritative target-selection + fan-out envelope/exit-code/write-confirmation rules.
commands/devices/README.md Aligns devices README fan-out description with the locked FanoutData contract.
commands/api/README.md Documents api fan-out behavior, guards, and exit-code semantics.
commands/api/examples.md Updates examples header + adds fan-out examples section (F1–F9) aligned with tests.

Comment thread src/cli/selection.ts
Comment thread src/api-fanout.ts
{ cdbResolution: member.resolution },
);
}
return resolveApiRequest({ ...request, targetInput: member.input }, env);

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
commands/devices/README.md (1)

187-207: 📐 Maintainability & Code Quality | 🔵 Trivial

Avoid restating the constitution's fan-out envelope and exit-code contract.

Lines 193–202 repeat the generic FanoutData envelope shape and exit-code mapping that already lives in docs/CONSTITUTION.md. Per the path instructions, command READMEs should link to the constitution for envelope and target selection rather than restate them. Keep only devices-specific details here (CDB de-duplication, --force gating for CDB mutations).

Also, the list of rejected commands is incomplete: it omits api --raw, which the constitution documents as rejected with usage/conflicting-flags.

As per path instructions, commands/*/README.md should not restate the constitution for envelope, errors, settings precedence, target selection, and protocol selection; link to docs/CONSTITUTION.md instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@commands/devices/README.md` around lines 187 - 207, Remove the duplicated
FanoutData envelope and exit-code description from the devices README and
replace it with a brief pointer to docs/CONSTITUTION.md for the shared fan-out
contract, target selection, and exit-code rules. Keep only devices-specific
content like CDB de-duplication and --force gating, and update the
unsupported-commands list to include api --raw with its usage/conflicting-flags
rejection.

Source: Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/errors/cdb/empty-selection.md`:
- Around line 7-10: The empty-selection error doc is currently too broad and
incorrectly suggests that bare `--group` empties map here; update the wording to
scope this page to non-group fan-out empties from the selection flow and
explicitly note that bare `--group` is handled by `cdb/empty-group`. Use
`selection.ts` and the `cdb/empty-selection` / `cdb/empty-group` symbols to keep
the error descriptions aligned with the resolver behavior.

In `@src/api-fanout.ts`:
- Around line 383-394: The failure summary in renderApiFanoutEnvelope() hides
the original target input when identity/host are missing, so resolve failures
show as “(unknown)”. Update the label fallback in the target rendering loop to
include meta.target.input after identity and host, keeping the existing
recordIndex and error formatting intact. Use the existing memberTargetMeta()
shape and the target.meta.target fields so adhoc resolve errors clearly identify
the host/path that failed.

---

Nitpick comments:
In `@commands/devices/README.md`:
- Around line 187-207: Remove the duplicated FanoutData envelope and exit-code
description from the devices README and replace it with a brief pointer to
docs/CONSTITUTION.md for the shared fan-out contract, target selection, and
exit-code rules. Keep only devices-specific content like CDB de-duplication and
--force gating, and update the unsupported-commands list to include api --raw
with its usage/conflicting-flags rejection.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 77ab3073-b401-4520-836b-151a2a79973e

📥 Commits

Reviewing files that changed from the base of the PR and between 665f838 and 49feb5c.

📒 Files selected for processing (27)
  • commands/api/README.md
  • commands/api/examples.md
  • commands/devices/README.md
  • docs/CONSTITUTION.md
  • docs/MATRIX.md
  • docs/errors/cdb/empty-selection.md
  • docs/errors/usage/fanout-not-supported.md
  • src/api-fanout.ts
  • src/api.ts
  • src/cli/api.ts
  • src/cli/common.ts
  • src/cli/retrieve.ts
  • src/cli/selection.ts
  • src/core/error-catalog.ts
  • src/core/fanout.ts
  • src/index.ts
  • src/resolver/cdb.ts
  • src/resolver/comment-kv.ts
  • src/resolver/index.ts
  • src/resolver/selection.ts
  • src/retrieve-fanout.ts
  • test/integration/api-fanout.test.ts
  • test/integration/cli-smoke.test.ts
  • test/integration/fanout-retrieve.test.ts
  • test/unit/api-fanout.test.ts
  • test/unit/fanout.test.ts
  • test/unit/selection.test.ts

Comment thread docs/errors/cdb/empty-selection.md Outdated
Comment on lines +7 to +10
A fan-out selection (`--group` / `--where` / `--all` / `--default` / positionals)
resolved to zero targets. This is reported as a warning on an `ok: true` fan-out
envelope with `summary { total: 0, ok: 0, failed: 0 }`, not a hard failure — there
was simply nothing to run.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Exclude bare --group from this error description.

src/resolver/selection.ts:276-295 emits cdb/empty-group when a group-only selection matches nothing. This page currently implies that an empty --group selection lands on cdb/empty-selection, which points users at the wrong error doc. Please scope this page to non-group empties or explicitly call out that bare --group uses cdb/empty-group.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/errors/cdb/empty-selection.md` around lines 7 - 10, The empty-selection
error doc is currently too broad and incorrectly suggests that bare `--group`
empties map here; update the wording to scope this page to non-group fan-out
empties from the selection flow and explicitly note that bare `--group` is
handled by `cdb/empty-group`. Use `selection.ts` and the `cdb/empty-selection` /
`cdb/empty-group` symbols to keep the error descriptions aligned with the
resolver behavior.

Comment thread src/api-fanout.ts
Comment on lines +383 to +394
for (const target of envelope.data.targets) {
const meta = target.meta;
const label = meta.target.identity ?? meta.target.host ?? "(unknown)";
const index = meta.target.recordIndex ?? "-";
if (target.ok) {
lines.push(
` [${index}] ok ${label} (${target.meta.operation?.objectCount ?? 0} object(s))`,
);
} else {
lines.push(
` [${index}] FAIL ${label} [${target.error.code}] ${target.error.summary}`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the operation/target meta type to confirm `input` is a valid field.
fd -t f 'envelope.ts' src/core --exec sed -n '1,200p' {}
rg -nP --type=ts -C3 'memberTargetMeta|target:\s*\{' src/api-fanout.ts

Repository: tikoci/centrs

Length of output: 7665


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant target-meta construction and renderer around the cited lines.
sed -n '130,260p' src/api-fanout.ts
printf '\n---\n'
sed -n '360,430p' src/api-fanout.ts

Repository: tikoci/centrs

Length of output: 5579


Adhoc resolve failures should include the original input

renderApiFanoutEnvelope() falls back to (unknown) for positional targets because memberTargetMeta() only carries input and the renderer ignores it. Add meta.target.input to the label fallback so resolve errors identify which host/path failed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api-fanout.ts` around lines 383 - 394, The failure summary in
renderApiFanoutEnvelope() hides the original target input when identity/host are
missing, so resolve failures show as “(unknown)”. Update the label fallback in
the target rendering loop to include meta.target.input after identity and host,
keeping the existing recordIndex and error formatting intact. Use the existing
memberTargetMeta() shape and the target.meta.target fields so adhoc resolve
errors clearly identify the host/path that failed.

mobileskyfi and others added 2 commits June 30, 2026 10:35
# Conflicts:
#	commands/api/README.md
#	commands/api/examples.md
#	docs/CONSTITUTION.md
#	docs/MATRIX.md
#	src/cli/api.ts
- selection: --concurrency rejects 0/negative, not just non-integers (the
  error already promised "integer >= 1"). (Copilot)
- api-fanout: drop the borrowed __default__ recordIndex from ad-hoc literal
  members and stamp their input, so distinct literals don't collide on one
  index. (Copilot)
- api-fanout: renderApiFanoutEnvelope falls back to meta.target.input before
  "(unknown)", so an ad-hoc resolve failure names the host. (CodeRabbit)
- docs/errors/cdb/empty-selection: scope the page to non-group empties and
  point bare --group empties at cdb/empty-group. (CodeRabbit)
- commands/devices/README: stop restating the constitution's FanoutData
  envelope + exit-code contract; link to it (commands/CLAUDE.md rule).
  (CodeRabbit nitpick)

Unit: --concurrency < 1 rejected; literal member drops recordIndex, keeps
input, renders by input not "(unknown)".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread src/cli/api.ts Dismissed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.

Comment thread src/api-fanout.ts
Comment on lines +186 to +193
// Validate the request shape + method ONCE, before selection expansion: a
// missing endpoint or an invalid `-X` is an outer error regardless of how many
// targets the selection resolves to (so an empty selection cannot mask it).
validateApiRequestShape(request);
const method = parseApiMethod(request.method);
const requestSummary = apiRequestSummaryFromRequest(request, env);
const selectionSummary = summarizeSelection(selection);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 13a711a. A globally-pinned --via / CENTRS_VIA outranks every per-target CDB override (in resolveStringSetting, explicit/env beats comment-kv), so an invalid one fails every target identically — it is genuinely a pre-flight condition. apiFanout now validates request.via ?? env.CENTRS_VIA once up front via a shared assertApiProtocolSupported, throwing the same settings/invalid-via / routeros/protocol-not-implemented errors as single-target resolveApiProtocol, which the CLI catch-block renders as an outer ok:false. An invalid CDB-record via override (no global pin) correctly stays a per-target failure, since it is target-specific. Unit coverage added in test/unit/api-fanout.test.ts.

A globally-pinned `--via` / `CENTRS_VIA` outranks every per-target CDB
override, so an invalid one fails every target identically. Validate it
once up front in `apiFanout` (outer ok:false), matching single-target
`resolveApiProtocol`, instead of degrading into N per-target resolve
failures. Extracts the shared check as `assertApiProtocolSupported`.

Addresses PR #114 Copilot review (src/api-fanout.ts invalid --via).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants