Skip to content

Deepen the Plan module and seal planning internals #180

Description

@lloydrichards

What problem are we solving?

The public Plan seam is surrounded by a large, externally visible implementation pipeline. Understanding how one contribution becomes a planned outcome currently requires following several modules and an intermediate representation that permits many invalid combinations.

flowchart LR
  B[Blueprint] --> R[ContributionResolver service]
  R --> N[NormalizedContributions]
  N --> C[PlanningIntentCompiler service]
  C --> W[wide PlanningIntentPath]
  W --> A[PlanAssessor service]
  A --> P[Plan]
Loading

PlanningIntentPath holds numerous independent arrays and optional fields:

type PlanningIntentPath = {
  path: string;
  authoritative?: /* ... */;
  jsonExports: ReadonlyArray</* ... */>;
  jsonDependencies: ReadonlyArray</* ... */>;
  jsonScripts: ReadonlyArray</* ... */>;
  tsImports: ReadonlyArray</* ... */>;
  tsReexports: ReadonlyArray</* ... */>;
  tsCallArgs: ReadonlyArray</* ... */>;
  tsObjectFields: ReadonlyArray</* ... */>;
  jsxSlots: ReadonlyArray</* ... */>;
};

The compiler must then maintain a family compatibility matrix, while the assessor rediscovers which combination it received. The JSX-slot defect in #178 demonstrates how easily a contribution family can be accepted by one part of the pipeline and omitted by another.

Two pure transformations are represented as injectable Effect services with no external dependencies:

export class PlanningIntentCompiler extends Context.Service(/* ... */) {}
export class PlanAssessor extends Context.Service(/* ... */) {}

This forces Plan tests to construct and override a multi-layer service graph without providing a real production adapter seam.

Planner internals are also exported from @repo/scaffold:

export { ContributionResolver } from "./service/plan/ContributionResolver";
export { PlanAssessor } from "./service/plan/PlanAssessor";

CLI catalog/debug behavior consumes ContributionResolver directly, bypassing PlanService. Meanwhile, NormalizedContributions lives in the domain package even though it is an internal handoff, while other planning-intent types remain private. The public seam is therefore less clear than the canonical Blueprint -> Plan flow suggests.

Contribution token handling adds another manifestation of the same problem: resolution is distributed by contribution tag, and several string-bearing structural fields are treated differently from paths, contents, and package fields. The contract does not make clear which fields may contain tokens.

Why does this matter?

  • Changes to one contribution kind require coordinated edits across several shallow modules.
  • Intermediate invalid states are representable and must be rejected later.
  • Test setup reflects internal module topology instead of the public Plan behavior.
  • CLI consumers can couple themselves to transitional planning representations.
  • Token behavior depends on which internal branch processes a contribution.
  • The package interface implies supported substitution seams that the current CLI does not need.

The objective is a deeper Plan module: a small stable interface with enough internal implementation to absorb planning complexity. This issue intentionally does not prescribe the exact internal types, number of files, or whether every existing service must disappear.

Scope

  • Public versus internal planning interfaces in @repo/scaffold.
  • The representation used to group compatible contribution intent per path.
  • Invalid combinations and exhaustive handling of contribution kinds.
  • Whether stateless planning transformations earn injectable service seams.
  • A deliberate query interface for CLI catalog inspection if that use case must remain.
  • An explicit, exhaustive token-resolution contract for contribution fields.
  • Tests organized around public Plan behavior plus focused internal pure behavior where valuable.

Acceptance criteria

  • Consumers can use the supported Blueprint -> Plan behavior without importing planner implementation modules.
  • Every supported contribution kind maps exhaustively to one valid planning behavior or an explicit incompatibility.
  • The per-path planning representation does not permit arbitrary incompatible family combinations without immediate validation.
  • Adding or changing one contribution kind has one clear locality for compilation and assessment behavior.
  • Dependency injection is retained only where the module provides meaningful substitution or side-effect isolation.
  • CLI catalog inspection uses an intentional public interface rather than a Plan implementation helper.
  • Token-capable fields and token resolution behavior are explicitly defined and tested for every contribution tag.
  • The package export surface distinguishes lifecycle use cases from implementation details.
  • Plan tests no longer require replacing pure implementation helpers merely to exercise the public interface.

Blocked by

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    afkAutonomous slice that can be completed without human interactionenhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions