Skip to content

feat(adopt): bulk/filtered adoption, --with-dynamic ruleset capture, unknown-field warnings - #58

Merged
2000game merged 3 commits into
mainfrom
feat/bulk-adopt-51
Jul 9, 2026
Merged

2000game merged 3 commits into
mainfrom
feat/bulk-adopt-51

Conversation

@2000game

@2000game 2000game commented Jul 9, 2026

Copy link
Copy Markdown
Member

Summary

Implements #51 — three related asks:

1. Bulk/filtered group adoption — ct adopt group is now a dedicated subcommand supporting:

2. --with-dynamic ruleset capture — for each adopted group (single or bulk) that IS a dynamic group (a 404 on the ruleset GET means "not dynamic" — skipped silently), captures its normalized ruleset (reusing engine/dynamic.js's normalizeRuleset, the same normalizer plan/apply already use) to rulesets/<key>.json and emits the dynamic: { status, ruleset: { ref } } block in the printed config snippet — so ct plan is a no-op once pasted. Covered end-to-end via buildPlan against the same mocked client (see acceptance test below).

3. Unknown-declaration-field warning — the config DSL (src/config/context.ts) now warns (via the existing warn() UI helper, never throws) when a declaration carries a field the registry doesn't manage for that type, e.g. campus's vestigial shortName instead of the real shorty. The allowlist is derived from the registry's own managedFields (a new knownFields() export) — never hand-copied, so it can't drift. File:line locations are #52's job, not built here.

Bonus fix: while building the group subcommand I found that Commander does not merge a same-named parent+subcommand option into either level's plain .opts() — ct adopt's parent already declares -s/--state/-e/--env, and any subcommand redeclaring them (both grants and the new group) silently had those flags dropped. Fixed by reading command.optsWithGlobals() instead, with a regression test on adopt grants proving --state is now actually honoured.

Test plan

  • npm test — 435 passed, 4 skipped (live-only), 0 failed
  • npm run typecheck — clean
  • npm run lint — clean
  • New tests/adopt-group-command.test.ts (20 tests): multi-id list, --type (numeric + logical key), --children-of (nested subtree, cycle guard, state-key resolution, empty subtree), --with-dynamic (capture, skip-silently for non-dynamic, dry-run previews without writing, mixed bulk selection), and an end-to-end acceptance test reconstructing the pasted config from the printed snippet and asserting buildPlan returns an all-no-op plan.
  • tests/registry.test.ts / tests/context.test.ts: knownFields() allowlist derivation + unknown-field warning (including the exact shortName vestigial-field scenario from the issue).
  • tests/adopt-grants-command.test.ts: regression test for the optsWithGlobals() fix.
  • Never hits a live instance — every test uses a mocked/fake CtClient.

Closes #51

2000game added 3 commits July 9, 2026 14:13
Derive a per-resource-type allowlist from the registry's own managedFields
(registry.knownFields) — never hand-copied — and warn (not throw) in the
config DSL when a declaration carries a field the registry doesn't manage
for that type, e.g. campus's vestigial `shortName` instead of the real
`shorty`. The field still passes through unchanged; this only surfaces the
mistake instead of leaving it silently un-diffed forever. File:line
locations are #52's job, not built here.
…ing them (#51)

`ct adopt`'s subcommands (grants, and the new group added in this PR) each
redeclare -s/--state and -e/--env for their own --help text. Commander does
not merge a same-named parent+subcommand option into either level's plain
.opts() — both come up empty for it — so the subcommand silently fell back
to the default state path / no env, ignoring the flag the user passed.
Read merged options via command.optsWithGlobals() instead, which walks the
whole command chain correctly. Adds a regression test proving --state is
actually honoured by resolving a scoped grant's dataId via a non-default
state file.
…pture (#51)

Add `ct adopt group` as a dedicated subcommand (mirroring adopt-grants.ts)
covering:

- Multiple positional ids: `ct adopt group <id> <id> <id>`.
- `--type <groupTypeIdOrKey>`: adopt every group of a group type, resolved
  as a numeric id or a logical key against the live /group/grouptypes
  catalog (client.getAll for listing, per #50).
- `--children-of <idOrKey>`: recursively adopt a group's full hierarchy
  subtree via /groups/{id}/children, parent-before-child, excluding the
  root; guards against a cyclic hierarchy with a visited-id set.
- `--with-dynamic`: for each adopted group that IS a dynamic group (a 404
  on the ruleset GET means "not dynamic" — skipped silently), capture its
  normalized ruleset (engine/dynamic.js's normalizeRuleset, the same
  normalizer plan/apply already use) to rulesets/<key>.json and emit the
  `dynamic: { status, ruleset: { ref } }` block in the printed config
  snippet, so `ct plan` is a no-op once pasted (covered by an end-to-end
  test using buildPlan against the same mocked client).

Output stays per-resource (state entries + config snippets), plus one
grouped `// group` config block for bulk selections — configSnippet's own
per-line FORMAT is untouched (#52 reworks that later).

Selection filters compose with each other via mutual exclusivity (exactly
one of: ids / --type / --children-of); duplicate resolved ids are deduped;
--key is rejected whenever more than one group resolves.
@2000game
2000game force-pushed the feat/bulk-adopt-51 branch from 1755ce7 to 0b65f57 Compare July 9, 2026 12:13
@2000game
2000game merged commit 8fc16be into main Jul 9, 2026
1 check passed
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.

feat(adopt): bulk/filtered adoption + auto-capture dynamic rulesets + warn on unknown declaration fields

1 participant