Skip to content

feat: detect archetypes from file-tree structural signals - #21

Merged
PrzemekGalarowicz merged 2 commits into
mainfrom
feat/archetype-file-tree-scan
Jul 7, 2026
Merged

feat: detect archetypes from file-tree structural signals#21
PrzemekGalarowicz merged 2 commits into
mainfrom
feat/archetype-file-tree-scan

Conversation

@PrzemekGalarowicz

@PrzemekGalarowicz PrzemekGalarowicz commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Refactors archetype detection into mergeable ArchetypeSignals (packageSignals, mergeSignals, archetypesFromSignals) so classification runs once over combined facts.
  • Extends detect-archetype.ts with a bounded file-tree scan (.tsx/.jsx, next.config.*, api/ dirs, app/**/route.ts, etc.) and merges those signals with package.json dependency names.
  • Preserves backward compatibility for package.json-only callers while enabling framework detection when package.json is missing or incomplete (e.g. .tsx alone → spa).

Test plan

  • npm run check (format, lint, typecheck, test)
  • npx vitest run tests/archetype.test.ts tests/detect-archetype.test.ts
  • File-tree cases: spa from .tsx without package.json, backend from api/, ssr from next.config.js
  • Merge correctness: react dep + next.configssr only (spa suppressed)
  • Skip-list: signals under node_modules/ and dist/ ignored
  • Symlinks not followed or classified

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Archetype detection now also looks at project file-tree signals, so projects can be classified even without a package.json.
    • Added deterministic handling for common app patterns like UI files, backend routes, and SSR config.
  • Bug Fixes

    • Improved consistency in archetype results with clearer signal merging and ordering.
    • Added coverage for edge cases like skipped directories, symlinks, case-insensitive matching, and truncation limits.
  • Tests

    • Expanded automated checks to cover the new detection behavior and signal combinations.

Split archetype classification into mergeable signals and a single rule pass, then scan the project tree alongside package.json so frameworkless repos can still be classified.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PrzemekGalarowicz, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ab615c1-b95a-4c5d-af09-1926133a6fda

📥 Commits

Reviewing files that changed from the base of the PR and between bfc8350 and 3434a35.

📒 Files selected for processing (5)
  • .dev/features/archetype-file-tree-scan/SHIP.md
  • ARCHITECTURE.draft.md
  • ARCHITECTURE.md
  • CONSTITUTION.draft.md
  • CONSTITUTION.md
📝 Walkthrough

Walkthrough

Archetype detection is refactored into pure, composable signal functions (packageSignals, mergeSignals, archetypesFromSignals) and extended with a bounded, symlink-safe file-tree scan (scanFileTreeSignals, classifyEntry) merged with package.json signals in detectArchetypesFromProject. Tests are expanded accordingly, and governance/process markdown and JSON artifacts document the change.

Changes

Archetype signal-based detection

Layer / File(s) Summary
Signal contract and pure logic
src/lib/archetype.ts
Adds ArchetypeSignals interface and packageSignals, mergeSignals, archetypesFromSignals functions; detectArchetypes now delegates to this pipeline.
File-tree scanning and merged detection
src/lib/detect-archetype.ts, src/types.ts
Adds scanFileTreeSignals, classifyEntry, readPackageSignals with traversal caps/skip-lists/symlink safety; detectArchetypesFromProject merges package.json and file-tree signals; doc comment updated.
Signal and detection tests
tests/archetype.test.ts, tests/detect-archetype.test.ts
Adds tests for signal helpers and filesystem-driven scenarios covering skip lists, case-insensitivity, symlinks, and determinism.
Governance and process docs
.dev/features/archetype-file-tree-scan/*, .pharn/pharn-dev-verify/results.json, .pharn/writes-scope.json
Adds PLAN, GRILL, REVIEW, REGRESSION, SHIP, VERIFY reports and JSON verification/regression artifacts, plus scope/results config updates.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant detectArchetypesFromProject
  participant readPackageSignals
  participant scanFileTreeSignals
  participant mergeSignals
  participant archetypesFromSignals

  Caller->>detectArchetypesFromProject: detectArchetypesFromProject(cwd)
  detectArchetypesFromProject->>readPackageSignals: readPackageSignals(cwd)
  readPackageSignals-->>detectArchetypesFromProject: pkgSig, packageJsonFound
  detectArchetypesFromProject->>scanFileTreeSignals: scanFileTreeSignals(cwd)
  scanFileTreeSignals-->>detectArchetypesFromProject: fileSignals
  detectArchetypesFromProject->>mergeSignals: mergeSignals(pkgSig, fileSignals)
  mergeSignals-->>detectArchetypesFromProject: merged signals
  detectArchetypesFromProject->>archetypesFromSignals: archetypesFromSignals(merged)
  archetypesFromSignals-->>detectArchetypesFromProject: archetypes
  detectArchetypesFromProject-->>Caller: {archetypes, packageJsonFound}
Loading

Related Issues: None found in the provided context.

Related PRs: None found in the provided context.

Suggested labels: enhancement, tests, documentation

Suggested reviewers: None specified in the provided context.

🐰 A rabbit hopped through trees of code,
Merging signals down the road,
Package.json meets the file-tree scan,
Bounded, safe, deterministic plan,
SPA suppressed when SSR's found —
Advisory notes all safe and sound.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding archetype detection from file-tree structural signals.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/archetype-file-tree-scan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Reframe CONSTITUTION.md for the installer’s threat model and expand ARCHITECTURE.md §5 to describe merged package.json and file-tree archetype signals; remove promoted draft files.

Co-authored-by: Cursor <cursoragent@cursor.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (2)
tests/detect-archetype.test.ts (1)

160-166: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

This test doesn't verify app/-scoping of route.ts detection.

app/users/route.ts → backend confirms the positive case, but there's no companion test for a route.ts placed outside app/ (e.g. top-level route.ts) to pin that it should (per the documented app/**/route.ts contract) NOT trigger backend. Given the scope gap flagged in detect-archetype.ts's classifyEntry, such a test would currently fail to catch the over-broad match since the implementation doesn't check ancestry.

🤖 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 `@tests/detect-archetype.test.ts` around lines 160 - 166, The current
`app/**/route.ts` test only covers the positive `detectArchetypesFromProject`
case and misses the required `app/` scoping check. Add a companion assertion in
`tests/detect-archetype.test.ts` that creates a top-level `route.ts` (outside
`app/`) and verifies `detectArchetypesFromProject` does not return `backend`.
This should pin the intended behavior of `classifyEntry` and catch any
over-broad `route.ts` matching that ignores ancestry.
src/lib/detect-archetype.ts (1)

47-49: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider extending SKIP_DIRS to common generated/build directories.

.next, coverage, .turbo, out, .cache are common in JS/TS repos and add scan cost (and potential noise) without any classification value, similar to the already-skipped dist/build.

♻️ Proposed addition
-const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build']);
+const SKIP_DIRS = new Set([
+  'node_modules',
+  '.git',
+  'dist',
+  'build',
+  '.next',
+  'coverage',
+  '.turbo',
+  'out',
+  '.cache',
+]);
🤖 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 `@src/lib/detect-archetype.ts` around lines 47 - 49, Extend the SKIP_DIRS set
in detect-archetype.ts to include common generated/build directories such as
.next, coverage, .turbo, out, and .cache. Update the directory-skipping logic
used by the archetype detection flow so these paths are treated the same as
node_modules, .git, dist, and build, keeping the check case-insensitive and
avoiding recursion into them.
🤖 Prompt for all review comments with 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.

Inline comments:
In @.dev/features/archetype-file-tree-scan/PLAN.md:
- Around line 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.
- Around line 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.
- Around line 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.

In @.dev/features/archetype-file-tree-scan/REVIEW.md:
- Around line 19-21: The coverage summary in the archetype-file-tree-scan review
is overstating the test coverage: it says every new function/behavior is tested,
but the same review notes gaps for route/next.config variants and the truncation
path. Update the summary text to match the actual coverage level, keeping the
wording consistent with the later coverage notes in REVIEW.md and the overall
regression-spec assessment.

In `@src/lib/detect-archetype.ts`:
- Around line 99-135: `scanFileTreeSignals` still materializes and sorts every
directory entry before `MAX_ENTRIES` or `allFound()` can stop traversal, so the
defensive cap is not actually limiting worst-case work. Update the directory
walk in `scanFileTreeSignals` (and its `readEntries`/`walk` flow) to use an
incremental directory iterator instead of `readdirSync` plus full-array
`sort()`, so processing can stop early without reading the entire listing; keep
the existing `classifyEntry`, `SKIP_DIRS`, and symlink checks in the per-entry
loop.
- Around line 68-86: The backend signal in classifyEntry() is too broad because
it marks any bare route.ts/tsx/js/mjs file as an App Router handler. Update the
detection to require app/** context before setting backend for route.* entries,
and thread the parent path or under-app flag from scanFileTreeSignals() into
classifyEntry() so only routes inside app/ are classified as backend.

---

Nitpick comments:
In `@src/lib/detect-archetype.ts`:
- Around line 47-49: Extend the SKIP_DIRS set in detect-archetype.ts to include
common generated/build directories such as .next, coverage, .turbo, out, and
.cache. Update the directory-skipping logic used by the archetype detection flow
so these paths are treated the same as node_modules, .git, dist, and build,
keeping the check case-insensitive and avoiding recursion into them.

In `@tests/detect-archetype.test.ts`:
- Around line 160-166: The current `app/**/route.ts` test only covers the
positive `detectArchetypesFromProject` case and misses the required `app/`
scoping check. Add a companion assertion in `tests/detect-archetype.test.ts`
that creates a top-level `route.ts` (outside `app/`) and verifies
`detectArchetypesFromProject` does not return `backend`. This should pin the
intended behavior of `classifyEntry` and catch any over-broad `route.ts`
matching that ignores ancestry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 840fad05-736e-498f-98a2-3a85f3c68ff3

📥 Commits

Reviewing files that changed from the base of the PR and between 509c00f and bfc8350.

📒 Files selected for processing (15)
  • .dev/features/archetype-file-tree-scan/GRILL.md
  • .dev/features/archetype-file-tree-scan/PLAN.md
  • .dev/features/archetype-file-tree-scan/REGRESSION.md
  • .dev/features/archetype-file-tree-scan/REVIEW.md
  • .dev/features/archetype-file-tree-scan/SHIP.md
  • .dev/features/archetype-file-tree-scan/VERIFY.md
  • .dev/features/archetype-file-tree-scan/regression-report.json
  • .dev/features/archetype-file-tree-scan/verify-report.json
  • .pharn/pharn-dev-verify/results.json
  • .pharn/writes-scope.json
  • src/lib/archetype.ts
  • src/lib/detect-archetype.ts
  • src/types.ts
  • tests/archetype.test.ts
  • tests/detect-archetype.test.ts

Comment on lines +45 to +47
- 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).

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.

Comment on lines +80 to +82
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).

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.

Comment on lines +98 to +114
- **`.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.

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.

Comment on lines +19 to +21
- **L-eval → P1:** Not a Capability (no `role:` frontmatter) → P1's Capability⇒eval does not bind; the
vitest suite is its regression-spec, and every new function/behavior is tested (401/401). Two minor
coverage gaps below. No missing eval-binding; floor agrees (validate GREEN).

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

Tighten the coverage summary.

The headline says every new function/behavior is tested, but this same review later calls out untested route/next.config variants and the truncation path. Rephrase the summary so it matches the actual coverage level.

🤖 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/REVIEW.md around lines 19 - 21, The
coverage summary in the archetype-file-tree-scan review is overstating the test
coverage: it says every new function/behavior is tested, but the same review
notes gaps for route/next.config variants and the truncation path. Update the
summary text to match the actual coverage level, keeping the wording consistent
with the later coverage notes in REVIEW.md and the overall regression-spec
assessment.

Comment on lines +68 to +86
function classifyEntry(name: string, isDir: boolean): ArchetypeSignals {
const lower = name.toLowerCase();
if (isDir) {
// A dir named `api` (covers a top-level `api/` and `pages/api`) → backend.
return { ssr: false, backend: lower === 'api', clientUi: false };
}
return {
// `next.config.{js,ts,mjs,cjs,…}` → an SSR meta-framework config.
ssr: lower.startsWith('next.config.'),
// App-Router route handlers (`app/**/route.ts`) → a backend surface.
backend:
lower === 'route.ts' ||
lower === 'route.tsx' ||
lower === 'route.js' ||
lower === 'route.mjs',
// A `.tsx` / `.jsx` file anywhere → a client-UI (frontend) signal.
clientUi: lower.endsWith('.tsx') || lower.endsWith('.jsx'),
};
}

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 | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files 'src/lib/detect-archetype.ts' 'src/types.ts' 'src/**/*.test.ts' 'src/**/*.spec.ts' | sed 's#^`#-` #'

printf '\n== outline: src/lib/detect-archetype.ts ==\n'
ast-grep outline src/lib/detect-archetype.ts --view expanded || true

printf '\n== outline: src/types.ts ==\n'
ast-grep outline src/types.ts --view expanded || true

printf '\n== relevant text search ==\n'
rg -n "route\\.(ts|tsx|js|mjs)|app/\\*\\*/route|App-Router|backend surface|classifyEntry|detect-archetype" src -S

Repository: pharn-dev/pharn-cli

Length of output: 4961


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== src/lib/detect-archetype.ts (lines 1-220) ==\n'
nl -ba src/lib/detect-archetype.ts | sed -n '1,220p'

printf '\n== src/types.ts (relevant range) ==\n'
nl -ba src/types.ts | sed -n '1,220p'

Repository: pharn-dev/pharn-cli

Length of output: 246


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== src/lib/detect-archetype.ts (selected lines) ==\n'
sed -n '60,140p' src/lib/detect-archetype.ts | cat -n

printf '\n== src/types.ts (around archetype comments) ==\n'
sed -n '178,194p' src/types.ts | cat -n

printf '\n== docs/tests mentioning route handler scope ==\n'
rg -n "route\\.ts|route\\.tsx|route\\.js|route\\.mjs|app/\\*\\*/route|App-Router route handlers|backend surface" src README.md . -S

Repository: pharn-dev/pharn-cli

Length of output: 6643


Scope route.* detection to app/**

classifyEntry() currently treats any bare route.ts/tsx/js/mjs file as backend, but this detector is meant to catch App Router handlers under app/**. Since scanFileTreeSignals() only passes the entry name, an unrelated route.* elsewhere in the tree will be misclassified. Thread the path/under-app context into the check before merging.

🤖 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 `@src/lib/detect-archetype.ts` around lines 68 - 86, The backend signal in
classifyEntry() is too broad because it marks any bare route.ts/tsx/js/mjs file
as an App Router handler. Update the detection to require app/** context before
setting backend for route.* entries, and thread the parent path or under-app
flag from scanFileTreeSignals() into classifyEntry() so only routes inside app/
are classified as backend.

Comment on lines +99 to +135
export function scanFileTreeSignals(root: string): ArchetypeSignals {
let acc: ArchetypeSignals = { ssr: false, backend: false, clientUi: false };
let budget = MAX_ENTRIES;

const allFound = (): boolean => acc.ssr && acc.backend && acc.clientUi;

const readEntries = (dir: string) => {
try {
return readdirSync(dir, { withFileTypes: true });
} catch {
return []; // unreadable subtree → no signal (deterministic default)
}
};

const walk = (dir: string, depth: number): void => {
if (depth > MAX_DEPTH || budget <= 0 || allFound()) return;
const entries = readEntries(dir).sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
);
for (const entry of entries) {
if (budget <= 0 || allFound()) return;
const name = entry.name;
// Never follow or classify a symlink → no escape past `root` (P2).
if (entry.isSymbolicLink()) continue;
const isDir = entry.isDirectory();
if (isDir && SKIP_DIRS.has(name.toLowerCase())) continue;
if (!isDir && name.toLowerCase().startsWith('.env')) continue;
if (!isDir && !entry.isFile()) continue; // sockets/fifos/etc.: not signals
budget -= 1;
acc = mergeSignals(acc, classifyEntry(name, isDir));
if (isDir) walk(join(dir, name), depth + 1);
}
};

walk(root, 0);
return acc;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

readdirSync + full-array sort() run before the entry-level cap can short-circuit.

MAX_ENTRIES/allFound() are only checked once per entry inside the for loop (lines 118-119), but readEntries(dir) (line 107) and .sort() (lines 115-117) always process the entire directory listing first. SKIP_DIRS protects node_modules-style trees since the skip check happens before recursion, but any other single directory with a very large number of direct entries (not on the skip list) still pays full read+sort cost before the cap engages — defeating the doc's stated intent that these caps are "a DEFENSIVE bound on a pathological tree, NOT a perf-only knob" over untrusted project input (P2).

A true per-directory bound would require a streaming read (e.g. fs.opendirSync + iterative readSync) that can stop before materializing/sorting the full listing.

🤖 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 `@src/lib/detect-archetype.ts` around lines 99 - 135, `scanFileTreeSignals`
still materializes and sorts every directory entry before `MAX_ENTRIES` or
`allFound()` can stop traversal, so the defensive cap is not actually limiting
worst-case work. Update the directory walk in `scanFileTreeSignals` (and its
`readEntries`/`walk` flow) to use an incremental directory iterator instead of
`readdirSync` plus full-array `sort()`, so processing can stop early without
reading the entire listing; keep the existing `classifyEntry`, `SKIP_DIRS`, and
symlink checks in the per-entry loop.

@PrzemekGalarowicz
PrzemekGalarowicz merged commit 80d7a33 into main Jul 7, 2026
6 checks passed
@PrzemekGalarowicz
PrzemekGalarowicz deleted the feat/archetype-file-tree-scan branch July 7, 2026 12:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant