Skip to content

Docs: document SafeDI cycle mechanics, erasure matrix, and selected init#270

Merged
dfed merged 1 commit intomainfrom
claude/docs-safedi-conventions
Apr 20, 2026
Merged

Docs: document SafeDI cycle mechanics, erasure matrix, and selected init#270
dfed merged 1 commit intomainfrom
claude/docs-safedi-conventions

Conversation

@dfed
Copy link
Copy Markdown
Owner

@dfed dfed commented Apr 20, 2026

Summary

Capture convention/mechanism knowledge that wasn't load-bearing in the code but has tripped up recent work. All additions are docs + one small helper extraction — no behavior change.

What's documented

Cycle mechanisms table (CLAUDE.md). Four related things — isPropertyCycle, cycleEdges, throwIfInvalidCycle, validateMockRootScopeForCycles — each doing a different job with overlapping names. Table names them, says where each is set, and what it does. Also explicit: if throwIfInvalidCycle accepts a cycle, every hop is non-constant.

erasedToConcreteExistential × property-type matrix (CLAUDE.md Common Pitfalls). The flag doesn't imply a constant property type — it's orthogonal. Four rows cover all combinations and whether each can appear in an accepted cycle. This mistake cost a round trip in #259 when defensive branches were removed on a wrong "erased = constant" assumption.

Two-layer mock return type (CLAUDE.md Mock generation specifics). Inner closure type (builderClosureType, #251) vs outer mock() return type (#266) are distinct concerns when a customMock returns a type other than Self. Both layers have to agree.

"Selected construction initializer" (CLAUDE.md). SafeDI picks a single canonical initializer per type. Validation that only matters for the path SafeDI uses (e.g., Self.*-in-default-expression check) should gate on this, not walk every initializer. Was #268's P1 bug class.

"Structurally unreachable" claims need a failing test (CLAUDE.md Testing Philosophy). Codifies the discipline we're already trying to hold — structural arguments are easy to get wrong.

Inline /// comments:

  • MockParameterNode.isPropertyCycle cross-references cycleEdges (per-root syntactic vs global feedback arc set).
  • MockContext.cycleEdges cross-references isPropertyCycle (opposite direction).
  • throwIfInvalidCycle docstring expanded with explicit rejection classes and acceptance condition.

Code change

Add InstantiableVisitor.canonicalConstructionInitializer computed property. Route the two existing initializers.first(where: { $0.isValid(forFulfilling: dependencies) }) call sites through it:

  • InstantiableVisitor.swift:377 (building Instantiable.initializer)
  • InstantiableMacro.swift:259 (the hasMemberwiseInitializerForInjectableProperties guard)

Removes drift risk — previously the macro recomputed the selector separately.

Test plan

  • swift test --traits sourceBuild — 885 tests pass
  • ./CLI/lint.sh

🤖 Generated with Claude Code

Capture convention/mechanism knowledge that wasn't load-bearing in the
code but tripped up recent work:

**CLAUDE.md additions**
- Cycle mechanisms table (`isPropertyCycle`, `cycleEdges`,
  `throwIfInvalidCycle`, `validateMockRootScopeForCycles`) naming scope
  and role of each so future work doesn't conflate them.
- `erasedToConcreteExistential` × property-type matrix under Common
  Pitfalls. The flag is orthogonal to `Property.PropertyType.isConstant`;
  reasoning about cycle reachability has to check the property type.
- Two-layer mock return type note (inner `builderClosureType` vs outer
  `mock()` return) with PR refs.
- "Selected construction initializer" named + pointed at the new
  `canonicalConstructionInitializer` helper.
- Testing Philosophy: "structurally unreachable" claims need a failing
  test, not a structural argument. Past session burned a round trip on a
  wrong argument.

**Code changes**
- Add `InstantiableVisitor.canonicalConstructionInitializer` and route
  the two existing `initializers.first(where: isValid(forFulfilling:))`
  callers through it. Removes drift risk — the macro used to recompute
  the selector separately from the visitor.
- Add /// comments on `MockParameterNode.isPropertyCycle` and
  `MockContext.cycleEdges` cross-referencing each other so the
  per-root-vs-global distinction is readable from the definitions.
- Expand `throwIfInvalidCycle` docstring: explicit rejection classes,
  explicit acceptance condition, pointer to the erasure matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (8f3ba53) to head (db767b5).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##              main      #270   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           41        41           
  Lines         6796      6796           
=========================================
  Hits          6796      6796           
Files with missing lines Coverage Δ
...afeDICore/Generators/DependencyTreeGenerator.swift 100.00% <ø> (ø)
Sources/SafeDICore/Generators/ScopeGenerator.swift 100.00% <ø> (ø)
...rces/SafeDICore/Visitors/InstantiableVisitor.swift 100.00% <100.00%> (ø)
...ources/SafeDIMacros/Macros/InstantiableMacro.swift 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dfed dfed merged commit e91633b into main Apr 20, 2026
17 checks passed
@dfed dfed deleted the claude/docs-safedi-conventions branch April 20, 2026 06:27
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