# ioc β€” Implementation Plan & Progress Tracker **What this document is:** The authoritative execution roadmap and living progress tracker for the `ioc` monorepo (`fnioc/ioc`, npm scope `@fnioc`). It encodes the full dependency DAG, phase-by-phase checklists, subagent decomposition, and parallelism constraints. Design ground truth lives in `PRD.md` β€” this document is about *execution and tracking*, not design prose. **How to use it:** As each task lands, tick the checkbox and update the dashboard row's Status and Notes columns. Append to the Progress Log on any significant state change. If a track gets stuck, append to `blockers.md` (see Β§Risks). --- ## Status Legend | Symbol | Meaning | |--------|---------| | ⬜ | Not started | | 🟑 | In progress | | βœ… | Done | | πŸ”΄ | Blocked | --- ## Status Dashboard | Phase / Track | Status | Notes | |---|---|---| | **Phase 0** β€” Scaffold (main checkout) | βœ… | Green on `main`; repo + branch protection (`verify`) + squash auto-merge configured | **Phase 1** β€” `@fnioc/core` (ABI + WeakMap + authoring surfaces) | βœ… | Merged (PR #1) β€” 22 tests; ABI, global WeakMap, `defineDeps`/`getDeps`, `@signature`, `forCtor` | | **Phase 2A** β€” `@fnioc/di` runtime (base; no factories) | βœ… | Merged (PR #3) β€” 59 tests; DiBuilder, scopes, Β§5.4 captive-dep rule, greedy selection, cycle detection, `useFactory`/`useValue`, native disposal, async-as-values; factory injection deferred to 2D | | **Phase 2B** β€” `@fnioc/transformer` (base; no factory detection) | βœ… | Merged (PR #4) β€” 28 tests + `tspc` ESM e2e; rebased onto current `main` + fixed `transformer:test` missing build-dep | | **Phase 2C** β€” Docs (README + API reference) | βœ… | Merged (PR #2) β€” root + 3 package advertisement READMEs | | **Phase 2D** β€” Factories (coordinated: core ABI + di + transformer) | βœ… | 2D.1 FactoryRef ABI (PR #5) Β· 2D.2 di injection (PR #6, +10 testsβ†’69) Β· 2D.3 transformer detection + Β§4.5/async/overload diagnostics (PR #7, +17β†’45). All merged. | | **Phase 3** β€” Integration & verification | βœ… | Merged (PR #10) β€” e2e transformerβ†’di, progressive-enhancement parity, factory e2e, correctness suite all landed. | | **Phase 4** β€” Packaging & publish | βœ… | All three packages (`@fnioc/core`, `@fnioc/di`, `@fnioc/transformer`) live on npm at v1.0.0 with provenance; OIDC trusted publishing; two-stage @next/@latest publish flow. | | **Phase 5** β€” Dir rename + session transfer | βœ… | Rename `ioc@rhombus-toolkit`β†’`ioc@fnioc` done early (mid-build recovery); session re-anchored | | **Standing** β€” `rhombus-toolkit/ts` issue filing | ⬜ | Ongoing; high bar for filing | --- ## Dependency DAG ``` Phase 0 Scaffold (MAIN CHECKOUT β€” no worktree; repo doesn't exist yet) β”‚ git init Β· toolchain mirror Β· pkg skeletons Β· tsconfigs Β· CI workflows β”‚ THE BUILD DEVIATION (tsc β†’ dist + .d.ts) Β· first commit Β· gh repo create β”‚ fnioc/ioc (SSH) Β· push Β· arm CI monitor β”‚ Phase 1 @fnioc/core β”‚ ABI + global-symbol WeakMap + defineDeps + @signature + forCtor β”‚ Token + hole + ABI_VERSION β”‚ [CRITICAL PATH BARRIER β€” di AND transformer both block here] β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Phase 2A Phase 2B Phase 2C ALL THREE CONCURRENT @fnioc/di runtime @fnioc/transformer Docs: README + worktree-per-subagent (hand-fed, no (ts-patch) API reference NO isolation:"worktree" plugin needed) (sonnet-shaped) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ Phase 3 Integration & verification β”‚ [BARRIER: needs di + transformer green] β”‚ Β· compile sample WITH transformer, run against di (ABI contract) β”‚ Β· progressive-enhancement e2e (with + without plugin) β”‚ Β· captive-dep Β· cycle Β· disposal sync+async Β· async-as-values Β· overloads β”‚ Phase 4 Packaging & publish β”‚ [sequential after Phase 3] β”‚ Β· real build output verification Β· ts-patch/TS compat Β· @fnioc scope claim β”‚ Β· OIDC trusted-publisher config Β· first release-please release β”‚ Β· MONITOR full chain to installable β”‚ Phase 5 Dir rename (ioc@rhombus-toolkit β†’ ioc@fnioc) + session transfer ``` **Barriers:** Phase 1 is the hard synchronization point β€” nothing in 2A or 2B can start until `core` is merged and green on `main`. Phase 3 is the second barrier β€” it blocks on both `di` and `transformer` being independently green. Phase 4 is sequential after 3. **Concurrent fan-out:** Phases 2A, 2B, and 2C run fully in parallel after Phase 1. `di` and `transformer` share only the ABI and token format from `core`; they are otherwise independent. `di` is developed and tested hand-fed (hand-written tokens + `defineDeps` calls, no transformer involved) β€” this is the design property that makes the parallelism valid and matches the locked design's build-order intent. --- ## Phase 0 β€” Scaffold (main checkout) **Dependencies:** none β€” repo does not exist yet; work happens in the main checkout. **Acceptance criteria:** CI runs and goes green on first push; all three package skeletons build (`tsc` β†’ `dist/`); Moon task graph resolves; release-please config parses. ### Checklist - [ ] `git init` in `~/src/ioc@rhombus-toolkit` (rename to `ioc@fnioc` deferred to Phase 5) - [ ] `mise.toml` β€” pin Bun + Moon (mirror `fnclaude@fnclaude` versions) - [ ] `bunfig.toml` - [ ] `.moon/workspace.yml` β€” define `packages/*` glob - [ ] `.moon/toolchain.yml` β€” Bun runtime - [ ] Root `package.json` (workspace root, private) - [ ] Three package skeletons: `packages/core/`, `packages/di/`, `packages/transformer/` - [ ] `packages/core/package.json` β€” name `@fnioc/core`, `main`/`types`/`exports` β†’ `dist/` - [ ] `packages/di/package.json` β€” name `@fnioc/di`, dep on `@fnioc/core` - [ ] `packages/transformer/package.json` β€” name `@fnioc/transformer`, dep on `@fnioc/core` - [ ] `tsconfig.base.json` β€” `lib: ["ES2022", "ESNext.Disposable"]`, target ES2022, NodeNext module/moduleResolution, strict, `noUncheckedIndexedAccess`, `isolatedModules`, `declaration`, `declarationMap`, `sourceMap` - [ ] Per-package `tsconfig.json` extending base; `outDir: "dist"`, `rootDir: "src"` - [ ] Per-package `moon.yml` β€” `build` task: `bun tsc -p tsconfig.json`, `outputs: ['dist']`; `test` task; `lint` task - [ ] **THE BUILD DEVIATION:** confirm Moon `build` task uses `tsc` (not `bun run` passthrough) β€” `ioc` is a library shipping to node/webpack/vite/tsc consumers, not a Bun app. `main`/`types`/`exports` point at `dist/`, not `src/`. - [ ] `.github/workflows/ci.yml` β€” `verify` job (checkout w/ 3Γ— retry + `fetch-depth: 0`, `mise-action`, `bun install --frozen-lockfile`, `moon run :lint/:test/:build`) + `publish` job (release-please-action + AUTOMERGE_PAT, OIDC trusted-publishing, `workspace:*`β†’concrete-version rewrite, topo-sort dependents-last, verify-deps-resolve guard, `npm publish --provenance`). Workflow file MUST be named `ci.yml` (OIDC trusted-publisher config references it by name). - [ ] `.github/workflows/auto-merge.yml` β€” squash auto-merge via AUTOMERGE_PAT - [ ] `.github/FUNDING.yml` β€” `github: fnrhombus`, `buy_me_a_coffee: fnrhombus` - [ ] `.release-please-manifest.json` + `release-please-config.json` β€” `separate-pull-requests: true`, `include-component-in-tag: true`, one entry per package - [ ] `.gitignore` β€” `dist/`, `node_modules/`, `*.tsbuildinfo` - [ ] Pre-commit hooks via mise hooksPath (mirror fnclaude pattern) - [ ] `bun install` β€” verify lockfile generates cleanly - [ ] `moon run :build` β€” verify all three packages produce `dist/` (empty/stub passes) - [ ] First commit - [ ] `gh repo create fnioc/ioc --public` (SSH remote) - [ ] Push `main` - [ ] Arm CI Monitor (see Β§Standing Tasks β€” CI monitoring discipline) --- ## Phase 1 β€” `@fnioc/core` **Dependencies:** Phase 0 green. **Acceptance criteria:** All items below pass `bun test`. `core` builds to `dist/`. The ABI shape (`DepRecord`), `ABI_VERSION`, global-symbol WeakMap, `defineDeps`, `@signature`, `forCtor`, and `Token` are all exported and typed correctly. Two copies of `core` at the same `ABI_VERSION` share one WeakMap via `Symbol.for`. [superseded: `hole` sentinel removed; `DepRecord.signatures` is `ReadonlyArray>`; unregistered tokens are runtime misses, not null holes] **Branch:** `feat-core` in a worktree. ### Checklist - [ ] `Token` type alias (`= string`, no branding, no literal types) - [ ] `hole` sentinel β€” exported `null` alias; wire value is `null` (JSON-friendly) [superseded: hole removed; primitives tokenize by name; unregistered = runtime miss] - [ ] `ABI_VERSION` β€” exported integer constant (`1`) - [ ] `DepRecord` shape β€” `{ abi: number, signatures: ReadonlyArray> }` [superseded: `abi` field removed; shape is `{ signatures: ReadonlyArray> }`; slots are `Token | FactoryRef | ScopeRef | Union | LiteralRef`, no null] - [ ] Global-symbol WeakMap β€” `globalThis[Symbol.for(\`@fnioc/core:deps@${ABI_VERSION}\`)] ??= new WeakMap()` at module init; all reads/writes through this key. `Symbol.for` only (never unique symbol); version-suffixed. - [ ] `defineDeps(ctor, signatures)` β€” merges (appends unique) signatures into the ctor's `DepRecord`; creates the record if absent; writes through the global-symbol WeakMap - [ ] `@signature(...tokens)` β€” TC39 class decorator (NOT legacy); on apply calls `defineDeps(ctor, [[...tokens]])`; stacking = overloads - [ ] `forCtor(ctor).signature(...tokens)` β€” fluent free-function; chaining = overloads; same `signature` verb as the ABI field and the decorator - [ ] Unit tests: `defineDeps` append-unique semantics; WeakMap key isolation by ABI version; two-copies-share-one-map scenario; `@signature` stacking; `forCtor` chaining; `hole` wire value [superseded: hole removed; test coverage updated accordingly] - [ ] `packages/core/src/index.ts` exports all of the above - [ ] `moon run core:build` green (`dist/` generated with `.d.ts`) - [ ] `moon run core:test` green - [ ] PR opened β†’ CI green β†’ merge to `main` --- ## Phase 2A β€” `@fnioc/di` Runtime **Dependencies:** Phase 1 merged to `main`. **Acceptance criteria:** Full di test suite green with hand-fed tokens (no transformer). Progressive-enhancement property holds: hand-fed tokens work identically to transformer-lowered tokens. All correctness rules from the locked design pass. **Subagent decomposition:** One subagent per internal stage (stages are sequential within 2A, but 2A itself runs concurrently with 2B and 2C). Create worktrees with `git worktree add` before dispatching; agent `cd`s into the worktree as its first action. No `isolation:"worktree"` flag. **Wall-clock note:** Stages 2A.3–2A.6 are largely parallel after 2A.2 lands β€” evaluate at dispatch time whether each is large enough to warrant a separate subagent or whether one agent serializing them is faster (see Β§Parallelism Notes). ### Internal stage sequence #### 2A.1 β€” Engine core, hand-fed **Branch:** `feat-di-core` - [ ] `DiBuilder` class skeleton - [ ] `RegistrationMap` internal structure β€” keyed by token string - [ ] `services.add(MyConcrete)` β€” one type param; concrete typed `new (...args: any[]) => IFoo`; plain `new`, NOT `abstract new` - [ ] `.as()` fluent β€” attaches lifetime tag; compile-time `S extends Scopes` check - [ ] WeakMap read via `@fnioc/core`'s global-symbol key to retrieve `DepRecord` - [ ] Positional resolution: for each signature token, resolve registered dep or treat as `hole`; `new Ctor(...resolvedArgs)` [superseded: hole removed; unregistered token = runtime miss (UnregisteredTokenError)] - [ ] Hand-fed token tests: register `"my:IFoo"` β†’ `MyFoo`; resolve; assert correct instance - [ ] `moon run di:test` green #### 2A.2 β€” Scope chain + tagged lifetimes **Branch:** `feat-di-scopes` - [ ] `services.createScope(tag)` β†’ returns a child `Scope` with parent reference - [ ] Registration lookup: child scope shadows parent (Angular-style hierarchical walk up) - [ ] Instance ownership: lifetime tag walks UP the parent chain to find the nearest ancestor scope with a matching tag; cache instance there - [ ] **Throw if no ancestor scope matches the tag** β€” this is the captive-dependency protection; never auto-create a scope - [ ] Transient (no tag): fresh instance every resolve, never cached - [ ] Root/singleton scope must be an explicit app-lifetime object; no lazy mint per-resolve - [ ] Tests: singleton cached; request scoped per-request scope; transient fresh; throw on mismatched tag #### 2A.3 β€” Correctness: captive-dep, cycles, disposal **Branch:** `feat-di-correctness` - [ ] **Captive-dep rule (Β§5.4):** resolve constructor deps relative to the scope that will OWN the instance (not the scope that triggered the resolve); singleton depending on request-scoped dep throws - [ ] **Cycle detection:** maintain resolution stack; token reappearance β†’ throw with full resolution path in message - [ ] **Disposal β€” sync:** `scope.dispose()` disposes owned instances in reverse construction order; only instances implementing `Disposable` / `Symbol.dispose`; `sync dispose()` THROWS if scope owns a `Promise`-valued disposable (directs to `disposeAsync()`) - [ ] **Disposal β€” async:** `scope.disposeAsync()` handles `AsyncDisposable` / `Symbol.asyncDispose`; uses `await using` semantics (TS 5.2+); reverse construction order - [ ] **Native disposal only:** `Disposable`/`AsyncDisposable` from TS lib β€” NOT a `@rhombus-toolkit` Disposable - [ ] Tests: captive-dep throw; cycle throw with path; sync dispose order; async dispose order; sync-dispose-on-async-disposable throws #### 2A.4 β€” Greedy overload selection **Branch:** `feat-di-overloads` - [ ] When a ctor has multiple registered signatures (`DepRecord.signatures.length > 1`): scan longest β†’ shortest; pick first where every param is satisfiable (token registered ∨ `hole` ∨ factory-typed) [superseded: hole removed; LiteralRef slots are always satisfiable; Union slots resolved first-match] - [ ] Equal-arity ties β†’ registration order - [ ] Tests: longest wins; shorter fallback when longest unsatisfiable; equal-arity registration-order tie #### 2A.5 β€” Factories **Branch:** `feat-di-factories` - [ ] Bare factory injection: param annotation is a literal arrow/function type returning a registered type β†’ inject as factory, not instance - [ ] Named function-interface (e.g. `interface IFooThunk { (): IFoo }`) β†’ resolves as a normal service by its own token (the explicit opt-out) - [ ] Partial/positional factories: factory params are the **unregistered** ctor params in relative order; holes filled positionally by caller args at call time - [ ] Runtime partition against live registration map (no whole-program analysis); positional collapse of unregistered params - [ ] Tests: bare factory injection; partial factory param ordering; named-interface opt-out; `(a: A, b: B) => IFoo` where A is registered, B is not #### 2A.6 β€” Override paths **Branch:** `feat-di-overrides` - [ ] `container.register(token, { useFactory: (c) => ... })` β€” no dep array, no decorator, closure wires deps - [ ] `container.register(token, { useValue: instance })` β€” static instance - [ ] Re-export `@signature`, `forCtor`, `hole` from `@fnioc/core` (one-import ergonomics for consumers) [superseded: hole removed; re-exports are `@signature`, `forCtor`, `union`, `defineDeps`, etc.] - [ ] Tests: `useFactory` wires correctly; `useValue` returns same instance; override shadows parent registration - [ ] `moon run di:build` green (full package) - [ ] `moon run di:test` green (full suite) - [ ] PR opened β†’ CI green β†’ merge to `main` --- ## Phase 2B β€” `@fnioc/transformer` **Dependencies:** Phase 1 merged to `main`. Runs concurrently with 2A and 2C. **Acceptance criteria:** `ts-patch` harness compiles; token generation is stable across the publish boundary; `defineDeps` emission matches the ABI; registration lowering round-trips correctly; all Β§4.5 diagnostics fire on fixture inputs. TDD throughout with fixture compilations. **Subagent decomposition:** One subagent handling the sequential internal stages (2B.1–2B.4 are themselves sequential; the whole track is independent of 2A). **Branch:** `feat-transformer` in a worktree. ### Internal stage sequence #### 2B.1 β€” ts-patch harness - [ ] `ts-patch` dev dependency in `packages/transformer/package.json`; `patchTs` step in Moon `build` task - [ ] Transformer entry point skeleton β€” `TransformerFactory` signature compatible with ts-patch - [ ] Fixture compilation test harness: input `.ts` source β†’ run transformer β†’ compare output `.ts` snapshot - [ ] Smoke test: identity transform compiles cleanly #### 2B.2 β€” Token generation - [ ] Walk up to nearest `package.json` to determine owning package + whether a symbol is publicly exported - [ ] Public export token: `packageName + publicExportSubpath + symbolName` (e.g. `your-lib:contracts/IFoo`) β€” stable across publish boundary; version EXCLUDED from token - [ ] App-internal (non-exported) type token: source-relative path token - [ ] Document multiple-install / version-skew caveat in generated output comments or README - [ ] Tests: public-export token stability; internal-type token; cross-package token matches what a consumer would import #### 2B.3 β€” Dep extraction + `defineDeps` emission + registration lowering - [ ] At each `services.add(MyConcrete)` site: read ctor param types via TypeChecker; compute token per param; emit `null` only for types that can never be tokens (primitives) [superseded: primitives now tokenize by name; null/hole removed; anonymous structural types β†’ hard error 990006] - [ ] Emit `defineDeps(MyConcrete, [[...tokens]])` call before the registration site - [ ] Emit `services.add("pkg:IFoo", MyConcrete).as("tag")` (lowered form) - [ ] `Promise` unwrap: param typed `Promise` β†’ same token as `X` - [ ] Lowered output format is the ABI contract β€” must match `DepRecord.signatures` shape exactly - [ ] Tests: dep extraction round-trips; `null` for primitives; `Promise` unwrap; multi-param class; registration lowering output snapshot [superseded: primitives produce named tokens, not null; test coverage updated] #### 2B.4 β€” Diagnostics + edge cases - [ ] **Β§4.5 diagnostic:** validate factory signatures against the target ctor's unregistered params in order; warn on equal-arity overload ambiguity; warn where consumer declared `IDb` but service is async-registered (should be `Promise`) - [ ] **Already-annotated class:** manual annotation (`@signature`/`forCtor`) is authoritative β†’ transformer SKIPS emission + emits an info diagnostic (never silent, never double-writes) - [ ] **Fully-dynamic class** (ctor transformer cannot statically see): no dep array emitted; runtime will throw with guidance if ctor has params but no metadata - [ ] Tests: factory-sig diagnostic fires; already-annotated skip + info diagnostic; dynamic-class produces no emission; equal-arity ambiguity warning - [ ] `moon run transformer:build` green - [ ] `moon run transformer:test` green - [ ] PR opened β†’ CI green β†’ merge to `main` --- ## Phase 2C β€” Docs **Dependencies:** Phase 1 merged (API shape is firm). Runs concurrently with 2A and 2B. **Subagent:** Dispatch a Sonnet subagent (prose-shaped work) into the `feat-docs` worktree. **Branch:** `feat-docs` in a worktree. ### Checklist - [ ] `packages/core/README.md` β€” API reference: `Token`, `hole`, `ABI_VERSION`, `DepRecord`, `defineDeps`, `@signature`, `forCtor` - [ ] `packages/di/README.md` β€” advertisement-oriented: the "lowering" analogy, progressive-enhancement model (with + without transformer), `DiBuilder`, scope model, `createScope`, `.add().as()`, `useFactory`/`useValue`, disposal, factories, greedy overload selection - [ ] `packages/transformer/README.md` β€” ts-patch setup, token derivation rule + version-skew caveat, `Promise` unwrap, `@signature`/`forCtor` opt-out, Β§4.5 diagnostic description - [ ] Root `README.md` β€” monorepo overview, links to package READMEs, the three-package dependency diagram, quick-start (install + transformer config + first registration) - [ ] PR opened β†’ merged (no CI gate on docs; merge at will) --- ## Phase 3 β€” Integration & Verification **Dependencies:** Phase 2A (di) AND Phase 2B (transformer) both merged to `main`. **Acceptance criteria:** All integration tests pass. The progressive-enhancement property holds end-to-end. ABI contract test passes (transformer output β†’ di resolves correctly). No regressions on hand-fed token paths. **Branch:** `feat-integration` in a worktree. ### Checklist - [ ] Sample app under `examples/` or `test/integration/`: classes authored type-driven against the sample scope union - [ ] Compile sample WITH transformer (`ts-patch`): verify lowered output matches expected ABI shape - [ ] Run lowered sample against `@fnioc/di`: resolve the full dependency graph; assert correct instances - [ ] **ABI contract test:** transformer-emitted `defineDeps` calls produce a `DepRecord` that `di` reads identically to a hand-fed one - [ ] **Progressive-enhancement e2e:** same sample compiles and runs WITHOUT transformer (hand-fed tokens + `@signature`/`forCtor`); assert behavioral parity - [ ] Captive-dep test: singleton depending on request-scoped service β†’ throws with informative message - [ ] Cycle detection test: `A β†’ B β†’ A` β†’ throws with full resolution path - [ ] Disposal sync test: close scope β†’ `dispose()` called in reverse construction order - [ ] Disposal async test: `disposeAsync()` on `AsyncDisposable` instances; correct order - [ ] Async-as-values test: `useFactory` returning `Promise`; consumer declares dep as `Promise`; singleton caches the Promise; multiple awaits resolve to same instance - [ ] Greedy overload selection test: transformer-emitted multi-signature class; di selects longest satisfiable signature - [ ] `moon run :test` across all packages green - [ ] PR opened β†’ CI green β†’ merge to `main` --- ## Phase 4 β€” Packaging & Publish **Dependencies:** Phase 3 merged to `main`. **Acceptance criteria:** `@fnioc/core`, `@fnioc/di`, `@fnioc/transformer` published to npm under the `@fnioc` scope with provenance; OIDC trusted-publishing flow works end-to-end; release-please generates correct version PRs. ### Checklist - [ ] **Real build output verification:** `moon run :build` produces `dist/` with correct `.js` + `.d.ts` + `.d.ts.map` + `.js.map`; `package.json` `main`/`types`/`exports` resolve correctly under `node` - [ ] **ts-patch ↔ TS version compat check:** verify the installed `ts-patch` version supports the TypeScript version pinned in `mise.toml`; document the tested pair (see Β§Standing Tasks) - [ ] **`@fnioc` scope claim:** retrieve the `rhombulus` god token from Bitwarden; run equivalent of `claim-npm.ps1` to register the `@fnioc` org on npmjs.com - [ ] **OIDC trusted-publisher config on npmjs.com:** provider=GitHub Actions, repo=`fnioc/ioc`, workflow=`ci.yml` (exact filename β€” do not rename the workflow) - [ ] **`AUTOMERGE_PAT` secret:** confirm set on `fnioc/ioc` repo (needed for release-please auto-merge) - [ ] **First release:** merge a release-please PR (or trigger manually); verify it produces correct version tags with `include-component-in-tag` format - [ ] **Monitor the full publish chain:** PR test β†’ auto-merge β†’ release-please PR β†’ release-please merge β†’ `release.yml` β†’ npm publish with provenance β†’ installable. Arm Monitor before the first publish attempt; PushNotification on terminal states. See Β§Standing Tasks β€” CI monitoring discipline. - [ ] **Verify install from npm:** `bun add @fnioc/core @fnioc/di` in a scratch project; import and run the basic usage path - [ ] All three packages installable and functional --- ## Phase 5 β€” Dir Rename + Session Transfer **Dependencies:** Phase 4 complete. Deferred to the END to keep one stable `cwd` throughout the full build. **Approach:** `mv ~/src/ioc@rhombus-toolkit ~/src/ioc@fnioc`, then `fnc_switch_project` / `fnc_spawn_session` to re-anchor Claude Code at the new path. If a copy+transfer approach is used instead, DELETE the old folder afterward β€” no lingering duplicates. ### Checklist - [ ] Confirm Phase 4 complete and all work pushed - [ ] `mv ~/src/ioc@rhombus-toolkit ~/src/ioc@fnioc` - [ ] Re-anchor Claude Code session at `~/src/ioc@fnioc` - [ ] Verify git remote, Moon, and Bun all resolve correctly from the new path - [ ] Delete old path if it still exists (copy+transfer path only) --- ## Parallelism Notes **What runs concurrently:** - Phases 2A, 2B, 2C start in the same dispatch turn after Phase 1 is merged. Three subagents, three worktrees, three independent branch names. - Within 2A: stages 2A.3–2A.6 can overlap after 2A.2 lands β€” evaluate at dispatch time. If each stage is β‰₯20 minutes of real work, fan out. If all four together would fit in one subagent in similar wall-clock, serialize them. **Barriers (hard synchronization points):** 1. Phase 1 must be merged before dispatching 2A or 2B. 2. Both 2A AND 2B must be merged before starting Phase 3. 3. Phase 3 must be merged before Phase 4. **Wall-clock parity rule:** do not fan out tasks where the parallel cold-start framing overhead (~50K tokens each) exceeds the serial speedup. For tasks estimated under ~5 minutes each, one subagent serializing them is often faster. Phase 2C (docs) is Sonnet-shaped and cheap to dispatch in parallel regardless of size. **`di` ↔ `transformer` independence:** `@fnioc/di` is built and tested entirely with hand-fed tokens. The transformer is never invoked during 2A testing. This is the design property (from locked design Β§2) that makes the parallel tracks valid β€” they share only the ABI format defined in `core`, which is complete after Phase 1. --- ## Standing Tasks ### File issues to `rhombus-toolkit/ts` During the `@rhombus-toolkit/*` reuse audit (per-use, not wholesale), file GitHub issues to `rhombus-toolkit/ts` for anything that is a genuine, well-justified problem found during the audit. **High bar:** the issue must be specific, actionable, and something that would benefit `rhombus-toolkit` consumers broadly β€” not a "nice to have" or something ioc can work around trivially. Write issues as the project author (no AI attribution); first-person, technical, direct. All `@rhombus-toolkit/*` packages are published except `fniterate`; verify publication before depending on any of them. ### ts-patch ↔ TS version compatibility Verify the `ts-patch` version in `packages/transformer/package.json` supports the TypeScript version pinned in `mise.toml`. Document the tested pair. If a TS patch version upgrade is needed during development, update the pin in `mise.toml` (not system-wide) and re-run the compat check. ### CI monitoring discipline Per user prefs: whenever a branch is pushed, a PR is opened, or a workflow is triggered that gates downstream automation (release-please, AUR publish, deploys), arm a Monitor in the same turn. Cover the full chain through the terminal state. Emit on every terminal state (success, failure, cancellation, `timed_out`) β€” not just success. Add in-script wedge detection (`last_change` timestamp; emit `WEDGE` and exit if no state change in 10–15 minutes). `PushNotification` on outcomes that change what needs to happen next. --- ## Progress Log *(Append-only. Newest entries at the bottom.)* - **2026-05-30** β€” Design locked (see `ioc-locked-design.md`). PRD drafted to `PRD.md`. PLAN drafted to `PLAN.md`. Repo directory exists at `~/src/ioc@rhombus-toolkit` (empty; no git init yet). All phases at ⬜. - **2026-05-30** β€” Phase 1 (`@fnioc/core`) merged via PR #1 (22 tests). **Restructured factories into a coordinated Phase 2D** (core ABI element extension for a factory descriptor + di injection + transformer detection + the Β§4.5 factory-signature diagnostic), to run AFTER the base di + transformer land β€” factory injection couples all three packages and shouldn't be crammed into the first di PR. The ABI `signatures` element type stays `Token | null` for v1 base; 2D extends it to `Token | null | FactoryRef` (still ABI v1 β€” pre-release, no published consumers). Dispatched Phase 2 in parallel: di base engine, transformer base, docs. - **2026-05-30** β€” Phase 0 scaffold complete locally: Bun + Moon + release-please, three package skeletons, real `tsc -b` β†’ `dist` build, smoke tests. `moon run :lint :test :build` green locally. Repo created at `fnioc/ioc`, pushed to `main`; squash auto-merge + branch protection (require `verify`) enabled; publish job no-ops cleanly until `AUTOMERGE_PAT` is set. First CI run hit moon's genesis single-commit `HEAD~1` baseline error (no parent commit) β€” resolved by this second commit; every future push sits on existing history so it won't recur. **Blocker:** bw Vault server `hass4150.duckdns.org` unreachable β†’ npm god token + `AUTOMERGE_PAT` retrieval deferred (only needed at Phase 4 publish). - **2026-05-30** β€” **Recovery + Phase 2 close-out.** Repo rename to `ioc@fnioc` confirmed; `bun install` re-linked the workspace; `main` green. **Phase 2B fixed & merged (PR #4, squash β†’ `518e586`):** rebased `feat-transformer` (cut from stale pre-di `main`) onto current `main` β€” conflict-free (disjoint files) β€” and fixed a real `transformer:test` task-graph bug (no build dep, so on a cold checkout `tspc`'s e2e exited 1 resolving `@fnioc/core`'s `dist`; added `deps: [build, core:build]`). 28 transformer tests + `tspc` ESM e2e green; branch + worktree cleaned, remote branch deleted. **Env gotcha:** git SSH commit-signing/push runs through the Bitwarden Desktop agent β€” prefix git with `SSH_AUTH_SOCK=$HOME/.bitwarden-ssh-agent.sock` (agent was down briefly mid-session; came back). **Phase 2D launched:** 2D.1 (`feat-factory-abi`, in flight) extends the ABI slot `Token|null`β†’`Token|null|FactoryRef` (`{factory: Token}`) + di type-adaptation (skips factory/hole sigs, no injection yet) + simplified the `DiBuilder.add` plugin-required error message (user: no "lowering" jargon β€” "requires the @fnioc/transformer plugin"). After 2D.1 merges: **2D.2 di factory-injection βˆ₯ 2D.3 transformer factory-detection** fan out (both need 2D.1's `FactoryRef`; 2D.3 also needs PR #4, now merged). - **2026-05-30** β€” **Wave 2 (Phase 2D factories) complete.** 2D.2 (PR #6 β†’ `ef5509e`): `@fnioc/di` `scope.ts` now resolves `FactoryRef` and `null`-hole slots β€” bare zero-arg factories route through the normal resolve path (respecting the target's registered lifetime), parameterized factories partition the target ctor against the live registration map and construct fresh every call; added `FactoryTargetError`; +10 di tests (β†’69). 2D.3 (PR #7 β†’ `cb50268`): `@fnioc/transformer` detects inline function-type ctor params (`()=>IFoo`, `(a,b)=>IFoo`) and emits `{factory:'token'}` slots keyed on the Promise-unwrapped return type; named callable interfaces opt out (resolve as normal services); shipped the Β§4.5 factory-signature diagnostic plus async-mismatch and equal-arity-overload warnings; tightened the `nameof` plugin-required message; +17 transformer tests (β†’45). The factory-vs-interface discriminator is purely syntactic (the parameter's `FunctionTypeNode` node-kind, never the resolved `Type` β€” an inline arrow and a named callable interface are structurally identical). Both PRs auto-merged on `verify`-green; the `enable-auto-merge` CI job still fails because `AUTOMERGE_PAT` is unset (it does not block `gh pr merge --auto`). Worktrees + local/remote branches cleaned (remote branches lingered β€” the failed enable-auto-merge job skipped auto-delete β€” and were removed explicitly). **Phase 3** dispatched into `feat-integration` (integration e2e). **Phase 4 bootstrap** started in parallel: the Bitwarden vault β€” the prior blocker (server `hass4150.duckdns.org` was unreachable) β€” is now reachable, merely `locked`, and unlockable; repo currently has zero secrets (no `AUTOMERGE_PAT`). - **2026-05-30** β€” **Phases 3 & 4 complete; two-stage publish + merge queue live.** Phase 3 (integration & verification) merged via PR #10 (`feat-integration`): full e2e correctness suite β€” transformerβ†’di pipeline, progressive-enhancement parity (hand-fed vs. transformer-lowered), factory e2e, captive-dep/cycle/disposal/async-as-values/greedy-overload tests β€” all green. Also merged PR #9 (two-copies-share-one-WeakMap coverage). Phase 4 first release: release-please cut per-package tags + GitHub Releases for `core-v1.0.0`, `transformer-v1.0.0`, and `di-v1.0.0`; all three packages published to npm at v1.0.0 with provenance. **Premature-release reset:** an earlier mis-versioned release-please attempt produced stale tags; PR #14 reset the release-please manifest state to 0.0.0 for a clean v1.0.0 republish. PR #19 added OIDC bootstrap token auth for the first publish. PR #15 fixed hand-declared factory diagnostics and publish prep. **Two-stage publish conversion (PR #21):** `@next` publishes automatically on every release-please release via OIDC trusted publishing in `ci.yml` (`npm publish --provenance --tag next`, no token; added `npm install -g npm@latest` because OIDC requires npm β‰₯ 11.5.1 and setup-node ships 10.x). `@latest` is promoted manually via `promote.yml` (`workflow_dispatch`) using `npm dist-tag add … latest` with the `NPM_TOKEN` secret (the scoped fnioc token β€” `npm dist-tag` is not OIDC-covered). `auto-merge.yml` excludes `release-please--*` PRs so release PRs accumulate and are merged deliberately. **Merge queue:** went live via a GitHub ruleset (required check `verify` + `merge_queue` SQUASH strategy); classic branch protection was deleted in favor of the ruleset β€” rulesets are the only API-automatable path to a merge queue on GitHub. --- ## Risks & Blockers **Blocker convention:** if any track gets stuck, append to `blockers.md` at the repo root β€” what was tried, what was learned, what is still unknown. Stop and surface it. Do not spin. ### Known risk points | Risk | Phase | Mitigation | |---|---|---| | Phase 5 rename breaks Claude Code session anchor | 5 | Defer rename to the very end; use `fnc_switch_project` / `fnc_spawn_session` to re-anchor; delete old path if copy+transfer approach used | | ts-patch ↔ TS version incompatibility | 2B / 4 | Pin both in `mise.toml`; verify the tested pair in Phase 2B.1; re-verify in Phase 4 before first publish | | OIDC trusted-publisher bootstrap (first publish requires the config to exist before any publish attempt) | 4 | Set up trusted-publisher config on npmjs.com BEFORE triggering the first release-please merge; verify AUTOMERGE_PAT is set | | `@fnioc` scope not yet claimed β€” another party could claim it before Phase 4 | 4 | Claim the scope (via Bitwarden rhombulus god token) at the START of Phase 4, before any publish attempt | | Greedy overload selection + factory heuristic interaction (syntactic factory detection is at the transformer level, but the runtime partition is at resolve time) | 3 | Integration test explicitly covers the combined path in Phase 3 | | Global-symbol WeakMap shared by two copies of `core` at the same ABI β€” correct by design, but subtle | 1 | Explicit two-copies-share-one-map unit test in Phase 1 checklist |