Skip to content

feat(dx): idiomatic adopt output, dynamic sugar, located errors, quiet state - #61

Merged
2000game merged 5 commits into
mainfrom
feat/authorable-configs-52
Jul 9, 2026
Merged

2000game merged 5 commits into
mainfrom
feat/authorable-configs-52

Conversation

@2000game

@2000game 2000game commented Jul 9, 2026

Copy link
Copy Markdown
Member

Addresses the tool-side items (A–D) of #52 (human-authorable configs). Item E (the blueprint rewrite of the real config) remains for ct-structure/#23; the cut items (ct fmt, defaults engine) are out of scope.

What's here (commits in D→C→B→A order)

D — State is machine-only. upsert now deep-compares managed fields and bumps updatedAt only when they actually change (keeping the existing fields object so serialization is byte-identical); adoptedAt is set once. No more state-file churn on a no-op apply.

C — Located validation errors. Eval-time config errors and the #51 unknown-field warnings are now prefixed file:line — <type> "<key>": message (e.g. ct.config.ts:42 — group "kids": unknown field "shortName" (ignored)). The call site comes from new Error().stack; jiti maps transpiled TS frames back to the original file+line. No identifiable frame → bare message, never a crash.

B — Dynamic sugar (pure eval-time desugaring, engine untouched):

  • dynamic: true → { status: "active", ruleset: { ref: "./rulesets/<key>.json" } }
  • dynamic: "<path>.json" → { status: "active", ruleset: { ref: "<path>" } }
  • explicit object form unchanged; invalid values throw.

A — ct adopt emits idiomatic config. configSnippet is now multi-line, prettier-compatible (2-space indent, trailing commas, one field per line). A new ReverseResolver (mirror of the forward resolver) turns numeric campusId/groupTypeId/groupStatusId into logical campus/groupType/status sugar via the live catalogs; unresolvable ids stay numeric with a trailing // TODO: no logical match. dynamic: blocks emit the item-B sugar. Bulk adopt keeps its type-grouped // group header.

Test evidence

  • npm run typecheck clean; npm run lint (eslint) clean.
  • npm test: 466 passed, 4 skipped (skipped = live/gated files, untouched).
  • Locked acceptance (A): a group adopted through the real command → the VERBATIM printed snippet pasted into a config → buildPlan all no-op, zero hand edits (tests/adopt-group-command.test.ts).
  • Located errors (C) exercised through the REAL jiti loader + graceful fallback (tests/located-errors.test.ts).

Notes

  • The repo baseline isn't prettier-conformant (format:check fails on pre-existing files); the enforced linter is eslint. The diff is deliberately scoped to 6 changed files + 1 new (src/resolve/reverse.ts) so parallel branches rebase cleanly. No live instance was contacted; personFields/customFields/securityLevels and live/gated tests were left untouched.

@2000game
2000game force-pushed the feat/authorable-configs-52 branch from a2fb86c to dc392dc Compare July 9, 2026 12:50
managedFields for group-type/age-group/target-group/relationship-type/
group-role yields undefined-valued optional keys (nameTranslated,
sortKey, degreeNameA/B) when the API omits them, but JSON.stringify
drops those keys on save. Re-adopting compared the persisted {name}
against a fresh {name, nameTranslated: undefined} by raw key count,
saw a mismatch, and spuriously bumped updatedAt.

fieldsEqual now filters undefined-valued keys before comparing, both
top-level and recursively, mirroring JSON round-trip semantics.
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.

1 participant