Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@ Planned and considered features for Packkit. Not commitments — a backlog to pu
- Package managers: npm · pnpm · yarn · bun

**Surfaces & DX**
- CLI wizard + 18 presets + short aliases + full non-interactive flag parity
- CLI wizard + 17 presets + short aliases + full non-interactive flag parity
- One-command scaffold (`--recommended`; flags imply non-interactive) + `--from` profiles
- Web configurator: client-side zip download + reproducible one-shot command + preset gists
- Agent-native: `--schema` (JSON), `llms.txt`, and an **MCP server** (`packkit-mcp`)

**Staying honest**
- **Template-dependency freshness** — weekly Action flags any version we write into generated projects that falls a major behind (what Dependabot can't see) and opens/auto-closes an issue
- **End-to-end integration matrix** — generates every preset + tooling variant, installs, and runs its real checks (build/test/lint, starts services) on any change to generation logic or deps
- **All template deps refreshed to current** (Vite 8, Vitest 4, ESLint 10, Storybook 10, React 19, jsdom 29, Biome 2, Jest 30, …) — verified green by the matrix. Intentional holds: `typescript` (5.9, pending typescript-eslint TS 7 support) and `knip` (5, oxc-parser native-binding crash in 6).
- **All template deps refreshed to current** (Vite 8, Vitest 4, ESLint 10, Storybook 10, React 19, jsdom 29, Biome 2, Jest 30, …) — verified green by the matrix, and since 3.1 they live in one catalog (`src/core/versions.js`). Intentional holds (`HELD` in that file): `typescript` (5.9, pending typescript-eslint TS 7 support), `knip` (5, oxc-parser native-binding crash in 6), and `lint-staged` (16, v17 needs Node >=22.22.1, above the Node 22 floor).

**On defaults:** tsup + ESLint/Prettier + Vitest + Changesets stay the conservative, best-supported defaults; tsdown/Biome/oxlint are one click away.

Expand All @@ -53,7 +53,7 @@ Planned and considered features for Packkit. Not commitments — a backlog to pu
### Layering (from the 3.0 approval review)
_3.0 turned the embedded API into Packkit's public programmatic surface. Keeping the layers distinct is now an explicit design rule, not an accident of the current code._

**Architecture principles** — worth writing into a short `ARCHITECTURE.md`:
**Architecture principles** — now written down in [ARCHITECTURE.md](ARCHITECTURE.md) (shipped 3.1):
1. The core never performs side effects.
2. The embedded API is the only supported programmatic surface.
3. The CLI is implemented on top of the embedded API.
Expand Down Expand Up @@ -93,19 +93,19 @@ _First unsolicited review from an agent that used Packkit to build a real intern
## From architecture review (2026)
_Second external review, this one of the codebase rather than the experience. Ratings: architecture 8.5, engineering 8, product 7, OSS readiness 8, maintainability 7. Every claim below was checked against the code before being written down._

**The theme worth acting on: Packkit doesn't eat its own cooking.** It generates `publint` + are-the-types-wrong checks, strict TypeScript, and lint gates for other people's packages, then ships without any of them. `npm run check:pkg` is something Packkit writes for you, not something it runs on itself — and its own package would fail it.
**The theme: Packkit didn't eat its own cooking.** It generates `publint` + are-the-types-wrong checks, strict TypeScript, and lint gates for other people's packages, then shipped without any of them. Largely addressed in 3.0/3.1 — it now ships type declarations and lints its own source (below).

- [ ] **Type declarations for the public API** — `exports` exposes `.`, `./core`, `./cli`, `./scaffold`, with **no `types`**. Programmatic consumers and the MCP server get nothing. JSDoc + `tsc --declaration` is enough; a rewrite isn't needed. Types wanted for `PackkitConfig`, the resolved config, `GenerateResult`, the file map, preset names, and scaffold results. **v3 requirement.**
- [x] ~~**Type declarations for the public API**~~**Shipped (3.0).** `types/` (`index`/`core`/`embedded`/`writer`), a `types` entry, and per-export `types`. Covers `PackkitConfig`, `ResolvedPackkitConfig`, `GeneratedProject`, the file map, diagnostics, the deployment contract, definitions, and the writer result. Verified resolvable from a packed install.
- [x] ~~**Self-enforcement**~~ — **Shipped (3.1).** Packkit now lints its own source with the stack it generates for users (eslint flat config + `@eslint/js`): `npm run lint`, a `check` script (lint + test), and a CI lint job. It immediately earned its keep — caught a real duplicate `minify` key in the CLI arg parser plus several dead vars/imports, all fixed. Full `tsc --checkJs` was assessed and deferred: 95 errors, nearly all the incremental-object-literal pattern (noise), not worth annotating around right now — though it did surface the dead `reactEntry` reference removed in the CLI-on-embedded PR.
- [ ] **Validate generated paths at the writer** — `writeProject` joins each relative path onto the target and writes it. Fine while every feature is trusted first-party code; a security boundary the moment third-party features, community recipes, or MCP-supplied data can contribute paths. Reject absolute paths, `..` escapes, and post-normalization collisions **in the writer**, not per feature — before extensibility ships, not after.
- [ ] **Collision diagnostics** — `deepMerge` ends in `return source`, so when two features set the same `scripts.build` or `exports` key the last one silently wins. Additive fields (dependencies) should keep merging; `scripts`, `exports`, `bin`, `files` should report which features collided. Same for two features emitting the same file path.
- [ ] **Validate generated paths at the writer** — **done for the embedded writer (3.0/3.1):** `writeGeneratedProject` rejects absolute paths, `..` escapes, null bytes, Windows-reserved names, case-insensitive collisions, and (3.1) paths that resolve through a symlink — validated both when a project is assembled and again at the write boundary; the CLI now writes through it. What's still on the older, looser path: `writeProject` in `src/cli/write.js`, which the **MCP server** uses directly, doesn't run these checks. Route MCP's writes through the embedded writer (or apply the same validation there) before any third-party/MCP-supplied paths are trusted.
- [x] ~~**Collision diagnostics**~~**Shipped (3.0 detection + 3.1 enforcement).** The embedded API surfaces `FEATURE_FILE_COLLISION` and `PACKAGE_FIELD_CONFLICT` diagnostics (with each feature named), dependency conflicts are keyed per section, and `deepMerge` now refuses prototype-pollution keys. 3.1's `test/feature-registry.test.js` turns it into an enforced invariant — no two features may collide across the matrix — which caught and fixed a real `scripts.dev` collision on `node-service`.
- [ ] **Structured subprocess errors** — `run()` returns a boolean and discards stdout/stderr when quiet, so "install failed" can't distinguish a missing executable from a network error, an unconfigured git identity, or a rejected push. Return `{ ok, command, exitCode, stdout, stderr, category }`; the CLI can still print something friendly, while MCP callers get something actionable.
- [ ] **Windows command-execution tests** — `run()` and `capture()` both set `shell: true` on win32, where quoting and metacharacter handling change. Test repo slugs, descriptions, remote URLs, and paths containing spaces, `&`, quotes, pipes, parens, and Unicode. Prefer resolving the executable (or `.cmd`) over enabling the shell globally.
- [ ] **Test the packed tarball** — `npm pack`, install the tgz, then check every export path resolves, the shebang and exec bit survive, only intended files ship, and the core stays browser-safe. The `./scaffold` export added in 2.8 makes this sharper: it pulls `node:child_process` into the published surface.

### Architecture, once the gates are in

- [ ] **Staged resolution with diagnostics** — `normalizeConfig` now applies defaults, coerces conflicts, derives helpers, and encodes framework/target policy in one pass, and **silently** disables what it can't support. Split into ordered stages returning `{ config, changes[], warnings[], errors[] }` so "we turned Storybook off because this isn't a component library" is reported rather than inferred.
- [ ] **Staged resolution with diagnostics** — the **diagnostics half shipped (3.0)**: `normalizeConfig` takes a collector and records every coercion it makes (previous → resolved value, with a code), which the embedded API and CLI surface — "Storybook was disabled because this isn't a component library" is now reported, not inferred. What's left is the structural half: splitting the single pass into ordered, named stages (defaults → validate → target rules → framework rules → …) each returning its own diagnostics. Lower priority now that the reporting exists.
- [x] ~~**A resolved domain model**~~ — **Shipped (3.1).** `normalizeConfig` now attaches a grouped `cfg.resolved` view (`targets` / `language` / `framework` / `build` / `module` / `package`) so consumers can read structured state (`cfg.resolved.targets.app`) instead of re-interpreting raw selections; the deployment contract already does. The reviewer's real worry — "flags can in principle contradict" — is now **proven impossible**: `test/invariants.test.js` asserts across a broad matrix that the grouped view never disagrees with the flat flags and that the derived state is internally consistent (publishable ⟺ its definition, an app is always ESM and never publishable, at most one framework, gated options off when their precondition fails, …). A future `normalizeConfig` change that introduces a contradiction fails there. Deliberately *not* done: a mechanical rewrite of every `cfg.hasApp` reference to `cfg.resolved.targets.app` — it's byte-safe but pure cosmetic churn; the flat flags stay supported and the invariants test guarantees they can't drift.
- [x] ~~**Make feature ordering explicit**~~ — **Shipped (3.1).** Rather than build speculative phase/requires/conflictsWith machinery, the real risk — order silently deciding a collision — is now an enforced invariant: `test/feature-registry.test.js` asserts every feature has a unique id and that **no two features collide** on a file or package.json field across the full config matrix (1683 configs). That immediately surfaced a real one — `bundler` and `service` both set `scripts.dev` on `node-service`, resolved only by array position — now fixed by making the service the explicit owner of its `dev` script. Ordering is no longer load-bearing for correctness; a colliding new feature fails the test instead of shipping.
- [ ] **Pairwise combination testing** — the integration matrix covers presets, which are the *common* paths. The risk is cross-feature interaction (dual + Rollup + Jest + Biome). Generate a matrix where every option pair appears at least once.
Expand Down