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
- Accurate validation commands: emitted commands reference scripts that
actually exist, removing a class of
<REVIEW REQUIRED>churn. - Reuses an existing type: no new shape —
ParsedManifestis already defined and used byDetectContext. - Backwards compatible: optional field; absent → current behaviour.
Negative
@aidokit/corechange:ProjectContext(and its schema, if validated) gains a field — a minor bump pre-1.0. Touches@aidokit/cli's context builder and@aidokit/stack-pack-node-ts.- The CLI now parses manifests: a small amount of new IO + error handling
in
buildProjectContext(a malformedpackage.jsonmust not crash init).
Neutral
- Other manifest types (
pyproject.toml,Cargo.toml, …) can be added toparsedManifestsas non-Node stack packs land; v0.1/v0.5 only needspackage.json.
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:
- [ ]
ProjectContexthas an optionalparsedManifestsfield typed asreadonly ParsedManifest[]. - [ ]
@aidokit/cli'sbuildProjectContextpopulates it from the rootpackage.json, tolerating a malformed file. - [ ]
@aidokit/stack-pack-node-ts'ssuggestValidationCommandsuses real script names when the field is present and falls back cleanly when absent.
References
Internal
- ADR 0012 — Spec↔core type reconciliation (defers this gap)
- ADR 0006 — Stack-pack contract shape
docs/specs/stack-pack-contract.md§13.1 — validation commands SHOULD use real scriptspackages/core/src/types.ts—ProjectContext,ParsedManifest
External
- npm
package.jsonscripts— https://docs.npmjs.com/cli/v10/configuring-npm/package-json#scripts