ADR 0014: ProjectContext Parsed Manifests — Let Stack Packs See Real package.json Scripts

Field Value
Status Accepted
Date 2026-05-20
Deciders Project maintainers
Supersedes
Superseded by
Related ADRs ADR-0012 (Spec↔core type reconciliation — defers this), ADR-0006 (Stack-pack contract shape)

Summary

stack-pack-contract.md §13.1 says a stack pack's validation commands SHOULD use the project's actual package.json scripts, but ProjectContext exposes no parsed manifest — only an optional packageManagerLockfile path. So @aidokit/stack-pack-node-ts emits conventional <pm> run <kind> commands (npm run lint, …) without verifying those scripts exist. This ADR adds an optional parsedManifests field to ProjectContext, reusing the existing ParsedManifest type. It is Accepted and implemented: ProjectContext carries the field, @aidokit/cli's buildProjectContext populates it from the root package.json, and @aidokit/stack-pack-node-ts's suggestValidationCommands uses it to emit commands only for scripts that exist.


Context

StackPack.suggestValidationCommands(ctx: ProjectContext) returns the lint / typecheck / test / build commands a project should run. For Node/TS the right commands depend on what package.json#scripts actually defines — a project may name its test script test, test:unit, or use vitest directly.

The DetectContext passed to StackPack.detect() does expose a parsed manifest (readManifest() returns ParsedManifest). But detect() runs before user selections; suggestValidationCommands() takes the heavier ProjectContext, which carries only:

readonly packageManagerLockfile?: string;  // a path, not parsed content

Consequence in @aidokit/stack-pack-node-ts: suggestValidationCommands derives the package manager from the lockfile name and emits conventional command strings. Its own source comments this as a known limitation, and the emitted commands are flagged <REVIEW REQUIRED> for the user to correct at init time. ADR-0012 recorded the gap as a deferred follow-up.


Decision

1. Add parsedManifests to ProjectContext

export interface ProjectContext {
  // … existing fields …
  /** Manifests parsed at context-build time (e.g. the root package.json). */
  readonly parsedManifests?: readonly ParsedManifest[];
}

ParsedManifest already exists in @aidokit/core (types.ts) — { raw, parsed, format } — and is reused unchanged. The field is OPTIONAL so existing ProjectContext construction sites keep compiling.

2. The CLI populates it during init

@aidokit/cli's buildProjectContext already does a detectLockfile pass over the project root; it additionally parses the root package.json (and, later, other recognised manifests) into ParsedManifest values and sets parsedManifests.

3. Stack packs read it; behaviour degrades gracefully when absent

suggestValidationCommands consults ctx.parsedManifests to use real script names when present. When the field is absent (or the project has no package.json), it falls back to today's conventional commands — so the field is a refinement, not a hard dependency.


Consequences

Positive

Negative

Neutral


Alternatives considered

Alternative 1: Leave it — conventional commands only

Pros: no change.

Cons: validation commands may name non-existent scripts; the stack pack's contract obligation (§13.1 SHOULD) is permanently unmet.

Reason rejected: it leaves a contract SHOULD unsatisfiable and pushes correction onto every user at init time.

Alternative 2: Give stack packs filesystem access in suggestValidationCommands

Pass a DetectContext-like reader, or the raw projectRoot, so the pack reads package.json itself.

Pros: the pack gets whatever it needs.

Cons: breaks the design where ProjectContext is the single immutable carrier and stack-pack methods are pure functions of it (ARCHITECTURE §7); re-introduces filesystem access into a layer deliberately kept pure.

Reason rejected: it erodes the pure-function contract for a problem a typed field solves cleanly.

Alternative 3: Pass scripts through ctx.options

options is Record<string, unknown> — the CLI could stuff package.json#scripts there.

Pros: no schema change.

Cons: untyped; every consumer casts; options becomes a junk drawer.

Reason rejected: typed context fields exist precisely so this kind of data is not smuggled through options.


Validation

This decision is validated when:


References

Internal

External