Skip to content

feat(extensions): extension loader with topo sort and lifecycle - #1035

Merged
ariskemper merged 6 commits into
mainfrom
feat/extensions-loader
Apr 17, 2026
Merged

ariskemper merged 6 commits into
mainfrom
feat/extensions-loader

Conversation

@ariskemper

@ariskemper ariskemper commented Apr 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • ExtensionLoader.flattenPresets() — recursively expand preset extends arrays (supports nested presets); cycle-guarded so malformed graphs raise a controlled error instead of stack-overflowing
  • ExtensionLoader.topologicalSort() — Kahn's algorithm, contract dependency ordering, tolerant of duplicate extension names
  • ExtensionLoader.setupAll() — validate, audit, register provides, call setup(); idempotent (auto-teardown on re-entry); rolls back on setup failure; honors source-priority at register() time
  • ExtensionLoader.teardownAll() — reverse-order teardown with error isolation

Review feedback addressed

  • P1 (topo sort dedup): cycle detection now compares against the set of unique extension names rather than the input array length, so duplicate entries with the same name no longer trigger false CIRCULAR_DEPENDENCY_ERROR.
  • P2 (recursive preset flattening): a preset whose children are themselves presets now resolves down to leaf extensions in one call.
  • P1 (source priority on register()): selectContractProviders() helper in validation.ts returns the priority-winning ResolvedExtension per contract; setupAll() gates static register() calls on that map, so a lower-priority extension later in iteration order no longer overwrites a higher-priority winner.
  • P1 (rollback on setup failure): ext.setup() wrapped in try/catch; on throw, best-effort teardown of the failing extension (error-isolated), then teardownAll() unwinds prior extensions and resets the registry, then rethrows. Previously-loaded extensions no longer leak on mid-setup failures.
  • P2 (preset cycle guard): flattenPresetsInner() threads a DFS path-set through recursion and raises EXTENSION_VALIDATION_ERROR on a back edge. A diamond (shared leaf reachable via two parents) is still accepted — the set tracks path membership, not global visits.
  • Simplification: removed redundant reset() / setupOrder = [] after teardownAll() — teardown is already idempotent and handles both clears.

Test plan

  • 27 loader test steps: deno task test src/extensions/loader.test.ts
  • Full extensions suite (10 files, 101 steps): deno task test src/extensions/
  • deno check clean under noUncheckedIndexedAccess on loader.ts, validation.ts, loader.test.ts
  • Pre-push suite: 1359 tests pass
  • /simplify, /review, /security-review clean — no issues flagged at threshold

Closes #1017

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c62dafccd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/extensions/discovery.ts Outdated
Comment thread src/extensions/loader.ts Outdated
Comment thread src/extensions/loader.ts Outdated
@ariskemper
ariskemper marked this pull request as draft April 14, 2026 16:06
- flattenPresets now recursively expands nested presets (extends-of-extends)
  so a preset that extends another preset resolves to its leaf extensions
  rather than intermediate preset markers.
- topologicalSort cycle-detection compares sorted.length against the count
  of unique extension names rather than the input array length. Duplicate
  entries with the same name collapse to one node in the graph, so the old
  check produced false-positive CIRCULAR_DEPENDENCY_ERROR whenever the same
  extension appeared twice in the input (e.g. config + package source).
- Add tests for both: recursive preset flattening and duplicate-name
  tolerance.
teardownAll is already idempotent and unconditionally clears setupOrder
and the contract registry. Calling reset()/setupOrder=[] after it was a
noop on the hot path and obscured the intent. Replace with an
unconditional teardownAll() call.
@ariskemper
ariskemper force-pushed the feat/extensions-loader branch from 2ca06c2 to 281e2ac Compare April 17, 2026 15:15
@ariskemper
ariskemper marked this pull request as ready for review April 17, 2026 15:21

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 281e2ac55c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/extensions/loader.ts Outdated
Comment thread src/extensions/loader.ts Outdated
Comment thread src/extensions/loader.ts Outdated
Three Codex findings on the current loader:

P1 — source-priority winner on register()
  setupAll iterated extensions in order and called register() for every
  provider, so a lower-priority provider later in the list silently
  overwrote a higher-priority one via contracts.set(). detectConflicts
  permits such pairs when one source strictly dominates (config beats
  package), so the runtime inverted the documented priority model.

  Added selectContractProviders() helper in validation.ts that returns
  the priority-winning ResolvedExtension per contract. setupAll now
  consults that map and skips register() for losers.

P1 — rollback on setup() failure
  If ext.setup(ctx) threw, the method unwound without calling the
  failing extension's teardown (it was never pushed to setupOrder) and
  without clearing earlier extensions. Resources opened mid-setup
  leaked; previous extensions stayed "loaded" in an inconsistent state.

  Wrapped setup() in try/catch: on failure, best-effort call the
  failing extension's teardown (guarded, logged on error), then
  teardownAll() to roll back prior extensions and reset the registry,
  then re-throw.

P2 — preset cycle guard
  flattenPresets recursed on extends children with no visited set, so
  A → B → A stack-overflowed. Now threads a DFS path-set through the
  recursion and raises EXTENSION_VALIDATION_ERROR on a back edge. A
  diamond (preset with the same leaf listed twice) is still accepted —
  the set tracks path membership, not global visit state.

Tests (27 steps, up from 16):
  - source priority: two-source case, order-insensitive winner
  - rollback: prior-extension teardown, failing-ext teardown, registry
    cleared, no crash when failing ext has no teardown hook
  - cycle guard: A→B→A, self-reference, diamond-is-not-cycle
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(extensions): PR 8 — Extension loader with topo sort and lifecycle

1 participant