Skip to content

feat(wiring): library composition — LibraryGroup + closure-as-membership with cross-package type travel#333

Open
zoe-codez wants to merge 6 commits into
mainfrom
library-composition
Open

feat(wiring): library composition — LibraryGroup + closure-as-membership with cross-package type travel#333
zoe-codez wants to merge 6 commits into
mainfrom
library-composition

Conversation

@zoe-codez

@zoe-codez zoe-codez commented Jun 16, 2026

Copy link
Copy Markdown
Member

📬 Changes

Library composition for @digital-alchemy/core: compose an application's library list as groups, let depends resolve transitive membership automatically, and carry library types across package boundaries with no manual re-export.

The model

Membership and type travel now follow one consistent rule, with the three relationship fields differing by a single axis — does it add an ordering edge:

Field Pulls membership Carries types Ordering edge
depends ✅ (closure)
implies
optionalDepends ❌ (untyped-travel) order-if-present

What's included

  • LibraryGroup({ name?, members, registry? }) — compose N libraries as a unit. Nameless = an anonymous bundle nobody injects; named = earns a LoadedModules key + a reserved config.<group> namespace. registry: true generates a priorityInit registry-service (register()/list()) — the canonical plugin-registry ("trench-coat") pattern — with members self-registering via lifecycle.onPreInit(() => parent.<registry>.register(...)) and ordered through their own depends edge.
  • Closure-as-membership — listing a library auto-pulls its transitive depends into membership (identity-deduped). MISSING_DEPENDENCY narrows to "could not resolve to any object at all." Every auto-pulled library is narrated in the boot log ([X] auto-pulled into membership by [Y]) via load-bearing RollupProvenance, so the listed set vs. the wired set stays legible.
  • Automatic cross-package types — both implies and depends capture a const tuple, so the carrier's emitted .d.ts references each member via typeof import(...). That edge activates the member's LoadedModules augmentation in any consumer that imports only the carrier — params.<member> is typed and wired with no LoadedRollups block. Requires members be named function declarations (arrow/const members serialize structurally with no edge → enforced by the service-factory-must-be-declaration lint).
  • Type priority — directly-listed (LoadedModules) types always win over hoisted ones: TServiceParams = GlobalParams & Omit<RollupApis, keyof LoadedModules>. The LoadedRollups fallback channel can only add keys, never override a directly-loaded module.
  • Diamonds & cycles — shared bases are deduped (hygiene warn + provenance multiPath); COMPOSITION_CYCLE on a self-containing rollup / mutual implies.
  • Legible DUPLICATE_LIBRARY — two physical copies of a same-named library is a broken install (the framework holds each library as a global singleton and deliberately does not arbitrate versions — the package manager owns that). The error now names both resolved copies and points at yarn dedupe.

Tests & CI

  • testing/wiring.spec.mtsLibraryGroup, closure-as-membership, multi-path-warning coverage.
  • testing/type-travel.spec.mts (new, under isolatedDeclarations) — four type-level proofs: (1) a closure-pulled depends lib is typed on params; (2) a direct listing beats a divergent hoisted shape (the Omit priority); (3) a const/arrow member is not typed via the edge (proves the lint is load-bearing); (4) fan-out — a base depended-on by many keeps .d.ts growth linear with no TS2456/TS2589. All four verified to go red when the guarded property is broken.
  • examples/implies-propagation/ + the test-implies-propagation CI job — the real cross-package .d.ts travel proof (retained).
  • Deno pipelines broadened to --allow-sys (conf reads uid, not just homedir).

yarn build && yarn lint && yarn test && yarn type-check — green (430 tests).

Checklist

  • Tests — wiring.spec composition/closure + the type-travel proofs + the CI regression harness
  • Docs — companion PR in the documentation repo (LibraryGroup, depends type travel, registry, the ordering-edge-bit model)

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.08738% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.45%. Comparing base (69ca0f6) to head (2a3006c).

Files with missing lines Patch % Lines
src/helpers/wiring.mts 97.53% 2 Missing ⚠️
src/services/wiring.service.mts 95.23% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #333      +/-   ##
==========================================
- Coverage   97.57%   97.45%   -0.12%     
==========================================
  Files          24       24              
  Lines        1276     1375      +99     
  Branches      251      279      +28     
==========================================
+ Hits         1245     1340      +95     
- Misses         30       34       +4     
  Partials        1        1              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@zoe-codez zoe-codez changed the title feat(wiring): add library implies and RollupLibraries feat(wiring): library composition (RollupLibraries + implies) with automatic cross-package types Jun 16, 2026

@hfesel-rvoh hfesel-rvoh 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.

Comments with the help of Claude! Otherwise it looks good. The implies with no depends seems like the biggest risk to me

): void | never {
// rollups are nameless membership carriers — exclude them here. The flatten pass
// expands them to their (named) members, which are validated on the post-flatten list.
const definitions = libraries.filter(entry => !isRollup(entry)) as LibraryDefinition<

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

assertLibraryNames now filters rollups out before counting, but it's also called from CreateApplication (assertLibraryNames(libraries), ~line 413) on the un-flattened input. So a RollupLibraries([...]) containing a same-name/different-object collision is no longer caught at definition time — only at bootstrap after flattenLibraries expands it. That contradicts the @throws DUPLICATE_LIBRARY / "fail loudly at definition time" promise in the CreateApplication TSDoc (line 384) and assertLibraryNames's own doc. Either expand rollups in the definition-time check, or update those doc comments to note rollup contents are validated at bootstrap. (Bootstrap does catch it — the rejects same-name/different-object delivered via a rollup test confirms — so this is doc accuracy, not a hole.)

Comment thread src/helpers/wiring.mts
* Distinct from `depends`: `depends` is ordering + validation; `implies` is membership.
* Rollups are accepted here and flattened recursively.
*/
implies?: readonly RollupMember[];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Worth making the ordering hazard explicit here since this is the field's doc: implies contributes membership with no ordering edge, so a consumer that sets only implies: [X] (not depends: [X]) and reads params.X in the factory body — rather than inside a lifecycle hook — can get undefined if X wires after the implier. The README and demo cover it (the demo sets both), but a one-line @remarks pointing readers to pair implies with depends when they touch the member at wire time would prevent the obvious misuse.

Comment thread testing/wiring.spec.mts
const b = RollupLibraries([a], { label: "b" });
// force a cycle (the public API alone can't build one): a -> b -> a
(a.members as RollupMember[]).push(b);
let caught: BootstrapException;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

caught is definitely-unassigned if flattenLibraries doesn't throw, so caught?.cause reads a possibly-uninitialized let. Harmless under vitest/esbuild (types stripped, and testing/ isn't in tsconfig.lib.json), but | undefined is more honest and the assertion still works. Same fix applies at line 1809.

@zoe-codez zoe-codez changed the title feat(wiring): library composition (RollupLibraries + implies) with automatic cross-package types feat(wiring): library composition — LibraryGroup + closure-as-membership with cross-package type travel Jun 17, 2026
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.

2 participants