-
Notifications
You must be signed in to change notification settings - Fork 0
feat: detect archetypes from file-tree structural signals #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # GRILL — archetype file-tree scan (ADVISORY — gates nothing) | ||
|
|
||
| Interrogated plan: `.dev/features/archetype-file-tree-scan/PLAN.md`. Spec-hash check: recomputed | ||
| `sha256(ARCHITECTURE.md)` = `11cd9ad5983188623fe0931d13588c16435a5565888344e20669748947d1d969` — **matches** | ||
| the plan's `spec_content_hash` (no drift). Registered grillers (`count-grillers.mjs .`): **0** — the | ||
| `pharn-pipeline` module isn't in this repo, so no pluggable grillers ran; the inline axes below applied. | ||
|
|
||
| > The `PLAN.md` is `trust: untrusted` to this stage. `problem` / `evidence` below quote the plan as DATA. | ||
| > Enum-gated fields (`type`, `rule_id`, `severity`, `file`) are the griller's own assertions. **No finding | ||
| > here blocks `/pharn-dev-build`** (fix #3): drift blocks at `/pharn-dev-build`'s floor-gate, not here. | ||
|
|
||
| ## Findings (finding-shape objects; `pharn-contracts/finding-shape.md` — conformed, not restated) | ||
|
|
||
| ### Axis: honest scope / limits (P7, P0) | ||
|
|
||
| ```yaml | ||
| - type: FINDING | ||
| rule_id: P7 | ||
| severity: important | ||
| file: ".dev/features/archetype-file-tree-scan/PLAN.md:83" | ||
| problem: "The depth/entry caps are labeled a perf 'defensive bound', but their real failure mode is a SILENT false-negative — a signal past the cap (e.g. a monorepo .tsx at apps/web/src/…/Foo.tsx) is undetected, yielding a wrong archetype with no signal that truncation occurred." | ||
| evidence: "Bounded (advisory defensive caps, P7 — labeled, not sold as a hard guarantee): a depth cap and a total-entry cap bound a pathological tree" | ||
| ``` | ||
|
|
||
| Weigh at build: pick **generous** caps (lean on the skip-list + all-signals-true short-circuit for perf, | ||
| not a tight cap), and document the completeness-vs-bound tradeoff honestly — a truncated signal is a | ||
| determinism-preserving but completeness-sacrificing miss, not merely "perf." Determinism (same tree → same | ||
| result) is unaffected either way. | ||
|
|
||
| ### Axis: eval coverage / changed invariant (P1, P6) | ||
|
|
||
| ```yaml | ||
| - type: FINDING | ||
| rule_id: P1 | ||
| severity: important | ||
| file: ".dev/features/archetype-file-tree-scan/PLAN.md:61" | ||
| problem: "The plan says packageJsonFound 'keeps its exact current meaning', but the emergent invariant that USED to hold — packageJsonFound:false ⟹ archetypes == ['lib'] — is now broken by design (a manifest-less project with a .tsx → {archetypes:['spa'], packageJsonFound:false}). No listed eval pins the full ArchetypeDetection for a manifest-less-but-file-signalled project, so a future consumer could still assume the old coupling." | ||
| evidence: "packageJsonFound keeps its exact current meaning (was a usable manifest read) — only archetypes becomes the merged set." | ||
| ``` | ||
|
|
||
| Weigh at build: add an eval asserting the **whole** `{archetypes, packageJsonFound}` for a project with | ||
| **no** package.json + a `.tsx` (expect `{archetypes:['spa'], packageJsonFound:false}`) — the sharpest proof | ||
| of "detect from files, not just package.json," and it locks the intentionally-changed coupling. | ||
|
|
||
| ```yaml | ||
| - type: FINDING | ||
| rule_id: P1 | ||
| severity: minor | ||
| file: ".dev/features/archetype-file-tree-scan/PLAN.md:98" | ||
| problem: "Only node_modules is asserted as skipped; the .git / dist / build / .env* skips (the bounded-walk safety surface) hold by construction but are untested — one representative build-output skip test would pin them." | ||
| evidence: "Bounded walk: a .tsx under node_modules/ is skipped → no spa (proves the skip-list)." | ||
| ``` | ||
|
|
||
| ### Axis: one axis of change (P3) | ||
|
|
||
| ```yaml | ||
| - type: FINDING | ||
| rule_id: P3 | ||
| severity: minor | ||
| file: ".dev/features/archetype-file-tree-scan/PLAN.md:58" | ||
| problem: "classifyEntry is PURE file-name membership — arguably the same 'classification rules' axis as the package-name membership that lives in archetype.ts. Placing it in the I/O file mixes 'reading strategy' with 'classification', a defensible but debatable split the human should ratify (alternative: put classifyEntry beside the other pure membership in archetype.ts, leaving detect-archetype.ts purely I/O)." | ||
| evidence: "local pure classifyEntry(name, isDir): ArchetypeSignals (the file-name patterns above);" | ||
| ``` | ||
|
|
||
| ### Axis: determinism (P5) | ||
|
|
||
| ```yaml | ||
| - type: FINDING | ||
| rule_id: P5 | ||
| severity: minor | ||
| file: ".dev/features/archetype-file-tree-scan/PLAN.md:44" | ||
| problem: "The signal patterns don't state case handling. On case-insensitive filesystems (macOS/Windows) readdir returns the on-disk case, so exact-case matching would miss API/, .TSX, or Next.config.js. Specify a lowercase-normalized compare so membership is stable across case variants." | ||
| evidence: "next.config.* file → ssr signal → ssr." | ||
| ``` | ||
|
|
||
| ## Prose summary | ||
|
|
||
| The plan is **strong and unusually honest**: its guarantee audit correctly splits floor (determinism + | ||
| each pinned mapping) from advisory (mapping completeness, names-only, caps); its trust audit closes the | ||
| untrusted-file-name surface (names → membership → closed enum, symlink-safe, worst case = a flipped | ||
| boolean → an advisory mis-detection, never injection/escape); and the merge-**signals**-not-sets pivot | ||
| correctly implements the human's SSR-gating decision. The reversal of #20 and the ARCHITECTURE.md §5 | ||
| "membership over package.json" tension are **surfaced and human-owned** (approved at GATE 1), not smuggled. | ||
|
|
||
| The concerns are refinements, not defects: (1) the walk caps' true failure mode is a **silent | ||
| false-negative**, so caps should be generous + the tradeoff documented; (2) the intentionally-changed | ||
| `packageJsonFound:false ⇏ ['lib']` coupling deserves a dedicated pinning eval (a no-manifest + `.tsx` | ||
| fixture — also the headline "detect from files" proof); (3) a placement judgment call for the pure | ||
| `classifyEntry` (P3); (4) unstated case-normalization; (5) one more skip-list test. None touches a | ||
| constitution principle as a violation; none blocks build. | ||
|
|
||
| ## ADVISORY VERDICT | ||
|
|
||
| **5 concerns raised (2 important-severity, 3 minor) — all advisory, for the human to weigh before | ||
| /pharn-dev-build.** No blocking-severity findings; no spec-hash drift; no CONSTITUTION_VIOLATION. This | ||
| grill-log **does not gate** `/pharn-dev-build` (P0/fix #3) — the deterministic backstops remain | ||
| `/pharn-dev-build`'s floor-gates (spec-hash, unresolved HALTs) and `.dev/floor/validate.mjs`. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,166 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # PLAN — archetype file-tree scan (package.json ∪ file-tree signals) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - spec_content_hash: 11cd9ad5983188623fe0931d13588c16435a5565888344e20669748947d1d969 # fix #4 — sha256 of ARCHITECTURE.md this run | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - increment: Extend archetype detection to also walk the project file tree once (bounded, names-only), collect structural signals, and merge them with the package.json signals — applying the archetype rule once over the union. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - layer(s): `src/lib/` (consumer-side infra for the capability resolver — ARCHITECTURE.md §5; the frameworkless `lib` base — §4) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - constitution_refs: [P0, P2, P3, P5, P6, P7] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Context — why this scope (discovery, P6) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Discovery this run read the live target and the four trusted docs. Findings that shaped the plan: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - The pure detector `detectArchetypes(pkg)` (`src/lib/archetype.ts`) is **package.json-names-only**; the I/O | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| boundary `detectArchetypesFromProject(cwd)` (`src/lib/detect-archetype.ts`) reads only `package.json`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - The enum is `Archetype = 'ssr' | 'backend' | 'spa' | 'lib'` (`src/types.ts:188`; ARCHITECTURE.md §5:179). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| There is **no** `frontend`/`db`/`nextjs` member. The description's signal NAMES must map onto this enum. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Blast radius is small:** `detectArchetypes` / `detectArchetypesFromProject` have **no product caller yet** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (grep: only their own tests). `resolveCapabilities` consumes an `Archetype[]` but never calls detection. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **A doc/decision tension (surfaced at the discovery halt).** ARCHITECTURE.md §5 and the `types.ts:184` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| comment both scope detection to "membership over `package.json`," and the **immediately-prior increment | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (#20, `archetype-io-boundary`) deliberately DROPPED file-tree signals** to stay spec-aligned. This | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| increment **reverses** that. §5 is trusted + hook-protected (agent cannot edit it). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| **Three decisions the human resolved at the discovery halt (this run):** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 1. **Proceed — scan the file tree too.** Detection broadens to file-tree NAME signals (still a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deterministic membership test, P5-clean), merged with package.json. §5's "package.json" wording is a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| **human-owned doc-reconciliation** (see Open questions); this plan records the reversal of #20 explicitly. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 2. **Drop `.sql` / `migrations/`** (P7 — smallest coherent increment). That signal maps to no existing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| archetype; adding a `db` archetype is cross-cutting (fix #5: the archetype set drives four maps | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `validate` checks agree). Revisit `db` as its own increment when a real need triggers it. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 3. **SSR-gate the `.tsx`/`.jsx` signal.** Merge **signals** (booleans), then apply `spa = clientUI && !ssr` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| **once** over the union — NOT a union of independently-computed archetype sets. So a Next app carrying | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `.tsx` files stays `ssr`, never `ssr`+`spa`. Consistent with today's package.json suppression rule. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Design — merge SIGNALS, not sets (the correctness pivot) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| The three-boolean signal `{ ssr, backend, clientUi }` is the merge pivot. Both sources compute booleans; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| they are OR-merged; the archetype rule is applied once. This is what makes decision #3 correct: a naive | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `pkgArchetypes ∪ fileArchetypes` could yield `{ssr, spa}` where the merged-signals answer is `{ssr}`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Signal → archetype mapping (the enum has no `db`/`frontend`, so names map onto members): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - `.tsx` / `.jsx` file anywhere → `clientUi` signal → contributes `spa` **iff no `ssr`** (decision #3). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - `next.config.*` file → `ssr` signal → `ssr`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - a directory named `api` (covers top-level `api/` and `pages/api`), **or** a `route.{ts,tsx,js,mjs}` file | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (App-Router route handlers, `app/**/route.ts`) → `backend` signal → `backend`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - `.sql` / `migrations/` → **not scanned** (decision #2). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Files | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - `src/lib/archetype.ts` — **MODIFY (pure).** Add the signal pivot without changing any existing behavior: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `interface ArchetypeSignals { ssr; backend; clientUi }`; `packageSignals(pkg): ArchetypeSignals` (extract | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| today's three membership checks); `archetypesFromSignals(sig): Archetype[]` (extract today's rule — | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `spa = clientUi && !ssr`, `lib` default, `ARCHETYPE_ORDER`); `mergeSignals(a, b)` (field-wise OR). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `detectArchetypes(pkg)` becomes `archetypesFromSignals(packageSignals(pkg))` — **byte-identical output** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (existing `archetype.test.ts` stays green). — layer `src/lib/` (pure membership rules). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - `src/lib/detect-archetype.ts` — **MODIFY (I/O boundary).** Add the file-tree reading strategy + merge: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| local pure `classifyEntry(name, isDir): ArchetypeSignals` (the file-name patterns above); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `scanFileTreeSignals(cwd): ArchetypeSignals` — a **bounded, symlink-safe, deterministic** walk (below); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `detectArchetypesFromProject(cwd)` merges `packageSignals(pkg)` with `scanFileTreeSignals(cwd)` and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| delegates to `archetypesFromSignals`. `packageJsonFound` keeps its exact current meaning (was a usable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| manifest read) — only `archetypes` becomes the merged set. — layer `src/lib/` (reading strategy). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - `src/types.ts` — **MODIFY (comment only, no type change).** The `Archetype` doc comment (≈184–188) says | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| detection is "membership over package.json" — now inaccurate. Correct it to name file-tree signals and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| point at the detection module, with a one-line note that the ARCHITECTURE.md §5 wording reconciliation is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| human-owned. Keeps the code self-honest (P0/P6) without touching the hook-protected doc. — layer `src/`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - `tests/detect-archetype.test.ts` — **MODIFY.** Add file-tree fixtures (the repo's deterministic-suite | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| analog of evals, P1). — layer `tests/`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - `tests/archetype.test.ts` — **MODIFY.** Add pure unit tests for the new `packageSignals` / | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `archetypesFromSignals` / `mergeSignals` (esp. the spa-gating rule and OR-merge). — layer `tests/`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| **The bounded walk (`scanFileTreeSignals`) — determinism + safety by construction:** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Skip** (never recurse / never classify): dir names `node_modules`, `.git`, `dist`, `build`; any name | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| starting with `.env` (decision from the description). **Symlink-safe:** `readdirSync(dir, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {withFileTypes:true})`, recurse only into real `dirent.isDirectory()` (a symlink-to-dir is `false` there) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| — so the walk **cannot escape `cwd` via a symlink** (cf. the repo's `hook-symlink-escape` concern). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Deterministic (P5):** signals are booleans → OR-merge is order-independent; per-directory entries are | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| **sorted by name** before traversal, so even a cap-truncated walk visits the same set on any filesystem. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Short-circuit once all three booleans are true. A per-entry `readdir`/`stat` error is narrowly caught and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| that subtree contributes no signal (a deterministic default, mirroring the existing malformed-`package.json` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| → `lib` handling — **not** a blanket swallow). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+80
to
+82
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Match the error-handling claim to the actual fallback. The walk only catches Suggested wording change- - **Deterministic (P5):** signals are booleans → OR-merge is order-independent; per-directory entries are
- **sorted by name** before traversal, so even a cap-truncated walk visits the same set on any filesystem.
- Short-circuit once all three booleans are true. A per-entry `readdir`/`stat` error is narrowly caught and
- that subtree contributes no signal (a deterministic default, mirroring the existing malformed-`package.json`
+ - **Deterministic (P5):** signals are booleans → OR-merge is order-independent; per-directory entries are
+ **sorted by name** before traversal, so even a cap-truncated walk visits the same set on any filesystem.
+ Short-circuit once all three booleans are true. A per-directory `readdir` error is narrowly caught and
+ that subtree contributes no signal (a deterministic default, mirroring the existing malformed-`package.json`📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Bounded (advisory defensive caps, P7 — labeled, not sold as a hard guarantee):** a depth cap and a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total-entry cap bound a pathological tree, in the spirit of the repo's 256KB/8s fetch caps. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Contracts satisfied | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **N/A — no `pharn-contracts` reference.** `pharn-contracts` is a PHARN-*product* layer (ARCHITECTURE.md | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| §4); this is a pharn-cli-internal `src/lib/` boundary and satisfies no product contract (as with #20). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Spec cited, not restated (P4):** ARCHITECTURE.md §5 (`archetype ∈ {ssr, backend, spa, lib}`, deterministic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| detection) and §4 (the frameworkless `lib` base). This increment **extends the detection INPUT surface** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (adds file-tree signals) while preserving the enum and determinism; it does not restate the rule. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Evals to write (P1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Every new behavior is produced by ≥1 test (evals = this CLI's vitest suite). New cases: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **`.tsx` present, NO `react` dep → `spa`** (frontend detected from files, not package.json) — the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description's headline case. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Pure-backend tree** (`api/` dir, no `.tsx`) → `backend`, **never `spa`** — the required inverse. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **`next.config.js` in tree, no `next` dep → `ssr`.** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **`route.ts` under `app/` → `backend`; `api/` dir → `backend`.** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Merge SSR-gating (the key correctness case):** package.json `react` (→ would be `spa`) + file tree | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `next.config.js` (→ `ssr`) → merged **`['ssr']` only** — proves signals-merge-then-rule (decision #3), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| not union-of-sets. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Merge additive:** package.json `express` + file tree `.tsx` → `['backend','spa']`. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Bounded walk:** a `.tsx` under `node_modules/` is **skipped** → no `spa` (proves the skip-list). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **`.sql`/`migrations/` present → contributes nothing** (decision #2, pinned so a future `db` change is a | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deliberate edit). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Determinism:** the same tree scanned twice → equal result. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Backward-compatible:** every existing `detect-archetype.test.ts` / `archetype.test.ts` case still passes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (package.json-only fixtures have no file signals → unchanged). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Pure units:** `archetypesFromSignals({clientUi:true, ssr:true})` → `['ssr']` (gating); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `mergeSignals` ORs field-wise. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+98
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Pin the no-manifest and literal-variant cases. The eval list covers the headline spa case, but it still doesn't explicitly lock the manifest-less path the PR summary calls out ( Suggested additions - **`.tsx` present, NO `react` dep → `spa`** (frontend detected from files, not package.json) — the
description's headline case.
+- **No `package.json` + `.tsx` → `spa` and `packageJsonFound:false`.**
+- **Variant coverage:** `next.config.ts/cjs` and `route.tsx/js/mjs`.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Guarantee audit (P0) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **"Same tree → same `Archetype[]` (deterministic)."** → **FLOOR.** Reduces to: booleans (OR-merge is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| commutative + idempotent) + sorted traversal + fixed caps (deterministic even when truncated) + the pure | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `archetypesFromSignals` (membership + fixed `ARCHETYPE_ORDER`). Backstopped by the determinism tests and | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `npm run check` (typecheck + vitest) → `.dev/floor/validate.mjs` GREEN. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **"Each specific signal → archetype mapping."** → **FLOOR** per mapping — each is pinned by a dedicated | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| test (the enum output is a closed set; the test asserts exact membership). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **"The mapping SET is the right/complete set of signals."** → **ADVISORY** (a design judgment, not floor- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reducible) — labeled so; backstopped by review/grill, never sold as guaranteed. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **"Reads NAMES only — never a discovered file's body, never executes, never sends."** → **ADVISORY.** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Holds by construction (`readdirSync`/`Dirent` name+type only; the only `readFileSync` is the pre-existing | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `package.json` read; no `child_process`, no `fetch`). No dedicated hook asserts it, so per P0 it is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| **labeled advisory**, backstopped by the `.dev/floor/scan-code-*` lenses + review (as in #20). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **"Bounded walk (depth/entry caps)."** → **ADVISORY defensive bound**, not a hard guarantee (a large tree | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| within caps is still large) — labeled honestly (P7), in the spirit of the repo's fetch caps. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Trust audit (P2) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Inputs:** `<cwd>/package.json` bytes (untrusted, pre-existing) **and** the project's file/dir **NAMES** | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from the walk (untrusted project input). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Taint propagation — contained at the boundary.** The walk reads only entry **names + `isDir`**, tests | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| them for membership against fixed in-code patterns, and never reads a discovered file's body, executes, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interpolates, forwards, or logs a name. Output is a closed `ArchetypeSignals` (3 booleans) → closed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `Archetype[]` enum — **no untrusted free text escapes**. Untrusted names → membership test → enum output | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (P2-clean), the same shape as today's package.json boundary, extended to file names. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Worst case of a hostile name** (e.g. a file literally named `route.ts` in a non-backend project, or odd | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| unicode): it can only **flip a boolean** → at most a mis-detected archetype (an *advisory* capability- | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| selection input downstream), **never** injection, arbitrary read, or path escape. Symlink escape is | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| prevented structurally (recurse only into real dirs). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Determinism audit (P5) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - Every branch is a membership test: file-name/dir patterns, package-name membership, the boolean spa-gating | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rule. No LLM classification. The walk's traversal is sorted + skip-listed + capped → a deterministic | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| visited set → deterministic booleans. The terminal fallback is a **deterministic default** ("no signal ⇒ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contributes nothing"; whole-project no-signal ⇒ `lib`, §4), never a guess and never a human question — | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| appropriate because "no framework signal ⇒ frameworkless lib" is a defined spec rule, not an irreducible | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ambiguity. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Open questions (HALT) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **None blocking.** The three ambiguities were resolved by the human at the discovery halt: **Proceed — | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scan files too** · **Drop `.sql`/`migrations/`** · **SSR-gate the `.tsx` signal**. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| - **Human-owned reconciliation (surfaced, NOT agent-edited — reported per P6):** ARCHITECTURE.md §5 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ("detected deterministically (membership over `package.json`)") and the intent recorded in | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `.dev/features/archetype-io-boundary/PLAN.md` ("Drop — stay spec-aligned") both predate this reversal. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| §5 is trusted + hook-protected — the agent cannot amend it; updating §5's wording (and, if desired, the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| #20 record) is the human's call. This plan builds the code + corrects the editable `types.ts` comment; it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| leaves §5 untouched. The build pins §5's current content-hash, so nothing here depends on §5 being edited | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| first. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Clarify the route-handler heuristic.
This reads as if only
app/**/route.tsshould count, but the classifier only sees entry names. In practice, anyroute.{ts,tsx,js,mjs}file anywhere in the tree will tripbackend. If that broader heuristic is intentional, say so; otherwise the scanner needs path-aware matching.Suggested wording change
📝 Committable suggestion
🤖 Prompt for AI Agents