feat(wiring): library composition — LibraryGroup + closure-as-membership with cross-package type travel#333
feat(wiring): library composition — LibraryGroup + closure-as-membership with cross-package type travel#333zoe-codez wants to merge 6 commits into
Conversation
Codecov Report❌ Patch coverage is
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. 🚀 New features to boost your workflow:
|
hfesel-rvoh
left a comment
There was a problem hiding this comment.
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< |
There was a problem hiding this comment.
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.)
| * Distinct from `depends`: `depends` is ordering + validation; `implies` is membership. | ||
| * Rollups are accepted here and flattened recursively. | ||
| */ | ||
| implies?: readonly RollupMember[]; |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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.
📬 Changes
Library composition for
@digital-alchemy/core: compose an application's library list as groups, letdependsresolve 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:
dependsimpliesoptionalDependsWhat's included
LibraryGroup({ name?, members, registry? })— compose N libraries as a unit. Nameless = an anonymous bundle nobody injects; named = earns aLoadedModuleskey + a reservedconfig.<group>namespace.registry: truegenerates apriorityInitregistry-service (register()/list()) — the canonical plugin-registry ("trench-coat") pattern — with members self-registering vialifecycle.onPreInit(() => parent.<registry>.register(...))and ordered through their owndependsedge.dependsinto membership (identity-deduped).MISSING_DEPENDENCYnarrows 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-bearingRollupProvenance, so the listed set vs. the wired set stays legible.impliesanddependscapture aconsttuple, so the carrier's emitted.d.tsreferences each member viatypeof import(...). That edge activates the member'sLoadedModulesaugmentation in any consumer that imports only the carrier —params.<member>is typed and wired with noLoadedRollupsblock. Requires members be namedfunctiondeclarations (arrow/const members serialize structurally with no edge → enforced by theservice-factory-must-be-declarationlint).LoadedModules) types always win over hoisted ones:TServiceParams = GlobalParams & Omit<RollupApis, keyof LoadedModules>. TheLoadedRollupsfallback channel can only add keys, never override a directly-loaded module.warn+ provenancemultiPath);COMPOSITION_CYCLEon a self-containing rollup / mutualimplies.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 atyarn dedupe.Tests & CI
testing/wiring.spec.mts—LibraryGroup, closure-as-membership, multi-path-warning coverage.testing/type-travel.spec.mts(new, underisolatedDeclarations) — four type-level proofs: (1) a closure-pulleddependslib is typed onparams; (2) a direct listing beats a divergent hoisted shape (theOmitpriority); (3) aconst/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.tsgrowth linear with no TS2456/TS2589. All four verified to go red when the guarded property is broken.examples/implies-propagation/+ thetest-implies-propagationCI job — the real cross-package.d.tstravel proof (retained).--allow-sys(confreadsuid, not justhomedir).yarn build && yarn lint && yarn test && yarn type-check— green (430 tests).Checklist
wiring.speccomposition/closure + thetype-travelproofs + the CI regression harnessdocumentationrepo (LibraryGroup, depends type travel, registry, the ordering-edge-bit model)