Skip to content
Merged
Show file tree
Hide file tree
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
97 changes: 97 additions & 0 deletions .dev/features/archetype-file-tree-scan/GRILL.md
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`.
166 changes: 166 additions & 0 deletions .dev/features/archetype-file-tree-scan/PLAN.md
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).
Comment on lines +45 to +47

Copy link
Copy Markdown

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.ts should count, but the classifier only sees entry names. In practice, any route.{ts,tsx,js,mjs} file anywhere in the tree will trip backend. If that broader heuristic is intentional, say so; otherwise the scanner needs path-aware matching.

Suggested wording change
-  - 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`.
+  - a directory named `api` (covers top-level `api/` and `pages/api`), **or** any `route.{ts,tsx,js,mjs}` file
+  anywhere in the tree → `backend` signal → `backend`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- 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).
- a directory named `api` (covers top-level `api/` and `pages/api`), **or** any `route.{ts,tsx,js,mjs}` file
anywhere in the tree`backend` signal → `backend`.
- `.sql` / `migrations/`**not scanned** (decision `#2`).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.dev/features/archetype-file-tree-scan/PLAN.md around lines 45 - 47, Clarify
the route-handler rule in the archetype scan plan: the current heuristic text in
PLAN.md makes it sound like only App Router files under app/**/route.ts should
trigger backend, but the classifier matches any route.{ts,tsx,js,mjs} entry name
anywhere in the tree. Update the wording to explicitly state the broader
filename-based behavior, or change the scan logic to use path-aware matching if
that narrower scope is intended, referencing the route-handler/backend signal
guidance in the plan.


## 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 readdirSync(dir) at the directory boundary. There is no per-entry stat recovery path here, so the current wording overstates how granular the fallback is.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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).
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`
`lib` handling — **not** a blanket swallow).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.dev/features/archetype-file-tree-scan/PLAN.md around lines 80 - 82, The
current plan text overstates the fallback in the directory walk; `walkDirectory`
only handles `readdirSync(dir)` failures at the directory level and does not
recover per-entry `stat` errors. Update the wording in this section to describe
the actual behavior of `walkDirectory` and its short-circuiting logic without
claiming narrowly caught per-entry recovery.

- **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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 (packageJsonFound:false + .tsxspa). It also leaves several literal branches unpinned (next.config.ts/cjs, route.tsx/js/mjs). Please add one representative fixture for each family.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **`.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.
- **`.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`.
- **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.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.dev/features/archetype-file-tree-scan/PLAN.md around lines 98 - 114, The
eval plan is missing coverage for the manifest-less frontend path and the
literal-file variants, so add representative fixtures to lock those branches
down. In the archetype scan test plan, include a case for packageJsonFound:false
with a .tsx signal resolving to spa, and add one fixture each for
next.config.ts/cjs and route.tsx/js/mjs so those literal branches are explicitly
exercised. Keep the additions aligned with the existing detect-archetype.test.ts
/ archetype.test.ts coverage style so the new cases validate the same
signal-merge behavior.


## 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.
Loading