# Dimension: Implementation Thinness

Evaluate whether the repo embodies the erp-kit principle of **thin implementations**: resolvers should delegate to module commands rather than embed logic, custom modules should be small and self-contained (`lib/` free of misplaced business logic — logic belongs in `command`/`query`), and frontend pages should rely on `@tailor-platform/app-shell` instead of bespoke layout components.

## Inputs

- `REPO_ROOT`
- `APP_ROOT` (optional) — path under `REPO_ROOT` to one app (e.g. `apps/my-shop`)
- `MODULES_ROOT` (optional) — path under `REPO_ROOT` to the modules directory (e.g. `modules`)

The agent decides which to pass to the measure CLI based on what exists in the repo. Provide both when possible.

## Procedure

### 1. Run the measurement CLI

```bash
npx erp-kit internal measure thin --app-root <APP_ROOT> --modules-root <MODULES_ROOT>
```

If neither flag is provided the result is `{}` — surface a finding and skip scoring.

The CLI returns JSON shaped like:

```json
{
  "resolver": {
    "count": 24,
    "avgLoC": 42,
    "maxLoC": 180,
    "filesOverThreshold": [{ "path": "...", "loc": 250 }]
  },
  "command": {
    "count": 38,
    "avgLoC": 28,
    "handWrittenRatio": 0.72,
    "libNonTypesFiles": ["modules/sales/lib/helpers.ts"]
  },
  "frontend": {
    "pages": { "count": 18 },
    "appShellCatalog": {
      "localVersion": "1.3.0",
      "source": "local"
    },
    "jsxComposition": {
      "appShellTags": 142,
      "replaceableTags": 22,
      "candidateTags": 9,
      "structuralTags": 64,
      "otherLibraryTags": 5,
      "unclassifiedTags": 0,
      "appShellRatio": 0.84
    },
    "appShellGaps": [
      { "tag": "select", "count": 6 },
      { "tag": "textarea", "count": 3 }
    ],
    "suspectedHandRolled": [
      { "path": "frontend/src/pages/dashboard/page.tsx", "appShellTags": 1, "structuralTags": 38, "replaceableTags": 0 }
    ],
    "rawHtmlHeavyPages": [
      { "path": "frontend/src/pages/legacy/page.tsx", "appShellRatio": 0.2, "replaceableTags": 18 }
    ],
    "customComponents": 12,
    "localComponentRisks": [
      { "path": "packages/frontend-shared/src/ui/card.tsx", "exportedNames": ["Card"], "reasons": ["shadows app-shell component: Card"], "appShellTags": 0, "replaceableTags": 0, "structuralTags": 14 }
    ]
  }
}
```

If the CLI is not available (older erp-kit), return `{ "score": null, "status": "tool-unavailable", "findings": [{ "severity": "info", "message": "measure thin command not available in this erp-kit version; dimension skipped." }] }`.

### 2. Interpret each sub-metric

For each present section, judge against erp-kit conventions.

#### Resolvers (when `resolver` is present)

- **avgLoC**: < 80 ideal, 80-150 acceptable, > 150 suggests embedded logic. Penalty grows with average.
- **filesOverThreshold**: each entry is a resolver > 200 lines (the same threshold used elsewhere in erp-kit). Each occurrence is a strong signal that logic belongs in a module command.

#### Commands (when `command` is present)

- **avgLoC**: < 60 ideal. Higher means commands are doing too much.
- **handWrittenRatio**: of `command/*.ts`, the share of hand-written impls (`foo.ts`) vs generated wiring shells (`foo.generated.ts`). In the doc-driven model each command is a pair (one generated shell + one hand-written `run`), so a healthy module sits essentially **at 0.5** — it does not drift there as a module matures. **Any drift outside ~0.4–0.6 is a smell, symmetrically on both sides:** above the band (hand-written excess, → 1.0) = commands with no generated shell → the generator was not run / commands were hand-rolled; below the band (generated excess, → 0.0) = generated shells whose hand-written impl was deleted → stale/orphaned `.generated.ts` files left behind. Treat both tails as suspect, not just the high one.
- **libNonTypesFiles**: files in `lib/` whose names fall outside the structure convention (`types.ts`, `*.generated.ts`, `_*.ts`). The CLI lists them **by filename only** — the thinness call is about content, not the name. Open each file and penalise only when it holds **business logic that belongs in a `command`/`query`** (or is dead code). Pure type declarations, shared constants, and thin helpers shared across commands (lock guards, column lists, external-service clients) are exactly what a maturing module legitimately accumulates here — do **not** penalise these, and do **not** demand a cosmetic rename just to satisfy the score. Filename conventions (e.g. the `_` prefix) are the **structure dimension's** concern, not thinness.

#### Frontend (when `frontend` is present)

**How "replaceable" is decided (no guessing).** The CLI resolves the actual `@tailor-platform/app-shell` from the measured repo's `node_modules` and reads its `.d.ts` catalog. A raw HTML tag is "replaceable" only when the catalog *proves* an equivalent exists: a same-name component (`<select>` → `Select`) or a component that declares it wraps the tag via `ComponentProps<"tag">` (`<tr>` → `Table.*`). This means a tag is never *falsely* claimed replaceable, and the set tracks whatever version the repo actually installed.

**When the catalog can't be resolved** (`appShellCatalog.source: "unresolved"` — usually because the measured repo's deps are not installed), the CLI makes **no guess**: raw lowercase tags are neither claimed replaceable nor reported as gaps; they are counted as `unclassifiedTags` and excluded from the ratio. If you see `source: "unresolved"`, the frontend numbers are partial — tell the user to install deps (`pnpm install`) and re-measure rather than reading the ratio as final.

**TS-generic noise.** The tag scanner is regex-based, so it also catches generic type arguments like `useState<string>()`. Unambiguous TS type keywords (`string`, `number`, `void`, `typeof`, …) are dropped. Two exceptions are kept because they are *also* real elements: `object` (`<object>`) and `symbol` (`<symbol>`). If either shows up in `appShellGaps`, open the page and decide whether it is a genuine `<object>`/`<symbol>` tag or just a `Record<string, object>`-style false positive before acting on it.

JSX tags found in page-tree `.tsx` files are bucketed:

- **app-shell tag** — imported from `@tailor-platform/app-shell` (e.g. `<Layout>`, `<Button>`) → numerator
- **replaceable tag** — lowercase opener with a proven catalog equivalent → in the denominator (penalty: should have been an app-shell component)
- **candidate tag** — lowercase opener with NO app-shell equivalent and NOT pure layout/text (`<select>`, `<textarea>`, `<form>` when absent from the catalog, etc.) → **neutral** (excluded from the denominator — can't blame a page for using a primitive app-shell doesn't offer). Surfaced as `appShellGaps` (see below).
- **structural tag** — pure layout/text (`<div>`, `<span>`, `<p>`, `<h1>`-`<h6>`, lists, landmarks) → **neutral**, excluded from the denominator. Not because app-shell has no alternative (its `Card`/`Layout`/etc. are div-based containers — the catalog even declares `ComponentProps<"div">`), but because a single structural tag is too generic to mechanically judge and these are ubiquitous, so penalising them per-occurrence would be pure noise. The hand-rolled-composite case (div soup that should be a `Card`/`Tabs`) is caught separately by `suspectedHandRolled`, not here.
- **other-library tag** — imported from a genuinely external package, excluding `react`/`react-dom` (e.g. `<DataGrid>` from MUI) → in the denominator + flat penalty for mere presence. **Repo-internal imports do NOT land here**: a tsconfig `paths` alias (`@/...`) or a workspace-package name is resolved to local code and treated as a local component (neutral), not third-party UI.
- **unclassified tag** — a raw lowercase tag that is **not** pure layout/text (structural is still bucketed as such), seen while the catalog is `unresolved` → **neutral**, excluded from the denominator (can't prove replaceable without a catalog). Only ever non-zero when `source: "unresolved"`.
- **local component** — imported via relative path, a tsconfig path alias, or a workspace package → **not counted (neutral)**. A `<OrderForm/>` usage says nothing about whether `OrderForm` is a thin app-shell composite or a re-built `Card`, so it's excluded from the ratio. **Neutral ≠ approved**: whether the *definition* re-implements app-shell is judged separately via `localComponentRisks` (see below).

Metrics surfaced from this bucketing:

- **appShellCatalog**: `{ localVersion, source }`. `source: "unresolved"` means the catalog couldn't be read from the repo's `node_modules` (raw tags fall to `unclassifiedTags`, frontend numbers are partial — tell the user to install deps and re-measure).
- **jsxComposition.appShellRatio**: app-shell tags vs (app-shell + replaceable + other-library) across all page-tree `.tsx` files. Candidate, structural, and unclassified tags are excluded from the denominator. > 0.85 ideal, < 0.6 suggests pages are built with raw HTML or third-party UI kits where app-shell would have served. Note the scoring table only *deducts* below **0.55**, not 0.6: the 0.55–0.6 band is a deliberate grace zone still surfaced as not-yet-ideal in findings. Under this denominator the scaffold-bundled reference app measures ~0.84 (user-management), comfortably clear of the penalty line — so 0.55 is an initial floor to be re-tuned once real-world data accumulates, not a threshold hugging the reference baseline.
- **jsxComposition.replaceableTags**: the actionable count — each is a place where an app-shell component would have been a better choice.
- **jsxComposition.candidateTags / structuralTags / unclassifiedTags**: neutral counts, surfaced for transparency (`unclassifiedTags` is only non-zero when `source: "unresolved"`).
- **jsxComposition.otherLibraryTags**: tags from genuinely external packages (repo-internal aliases/workspace packages are excluded). Any non-zero value is a smell — erp-kit assumes app-shell is the UI vocabulary. Before treating it as actionable, check **what** the library is: an icon set (`lucide-react`) or a charting lib (`recharts`) fills a gap app-shell doesn't cover (treat like an `appShellGaps` feature-request), whereas a competing component kit (MUI, antd) is the real smell.
- **appShellGaps**: candidate tags aggregated by frequency (`{ tag, count }`) — raw HTML the app uses that the installed app-shell has no component for. This is the **share-with-app-shell-devs list**: report each as a **feature request candidate for app-shell** (surface as `info`, do **not** penalise the repo for it). The CLI matches only against the *installed* version, so an entry may already exist in a newer app-shell — when recommending, suggest checking whether upgrading covers it before treating it as truly missing.
- **suspectedHandRolled**: pages with ≥ 20 structural tags (a noise floor, not a discriminator — most real pages clear it) where app-shell tags are a **minority of the structural mass** (app-shell share < 0.4). In plain terms: a structural-heavy page that barely leans on app-shell — a likely-un-migrated or hand-rolled page. **Known blind spot:** this is a page-level ratio, so a single hand-rolled `Card` buried in an otherwise app-shell-rich page won't trip it (and a page that uses a lot of app-shell but also a lot of layout `div`s sits above the threshold — that's intended, it isn't hand-rolled). The CLI does **not** assert these are violations — tag names can't tell a re-implemented `Card`/`Tabs`/`Dialog` from legitimate layout. **Open each flagged page and sort it into one of three:**
  - **Re-implements something app-shell already provides** → `warning`. **List *every* app-shell component the page should adopt** (e.g. `Field`, `Select`, `Form`) so the user sees the full scope and doesn't think fixing one line is enough — then illustrate **one** of them concretely so they know what the swap looks like, e.g. `Field`: `<div><label/><input/></div>` → `<Field>`.
  - **A reusable pattern app-shell *lacks*** (e.g. a hero banner, wizard, kanban board) → do **not** penalise the repo, but surface it as an **app-shell feature-request candidate** (`info`), naming the composite. This is the composite-level twin of `appShellGaps` — it's how "they hand-rolled a Hero because app-shell has none" gets fed back to app-shell devs instead of silently passing.
  - **A genuine one-off bespoke layout** → no penalty, no request.

  Do not deduct points purely on the CLI numbers.
- **rawHtmlHeavyPages**: pages with `appShellRatio < 0.5` and `replaceableTags >= 5`. These are the worst offenders (raw button/table where app-shell fits) — list them individually in findings.
- **customComponents**: number of `.tsx` files under `frontend/src/components/`. A small number is fine. Larger numbers (e.g. > 20) hint that the app is rebuilding things app-shell already provides.
- **localComponentRisks**: local/workspace component *definitions* (the app's `frontend/src/**/*.tsx` outside `pages/`, plus the repo's `packages/*/src`) that look like app-shell re-implementations. This is where the "neutral" local components from the ratio get scrutinised — a `frontend-shared/ui/Card` or `src/features/orders/OrderForm` that the ratio ignored shows up here. Each entry carries `reasons`: `shadows app-shell component: <names>` (an export name collides with an app-shell component), `uses N replaceable raw tags` (the body rebuilds primitives from raw `<button>`/`<input>`/etc.), or `structural-heavy with little app-shell use`. The CLI does **not** assert these are violations — **open each flagged file and sort it like `suspectedHandRolled`:**
  - **Re-implements an app-shell component** (esp. a name shadow like a local `Card`/`Form`/`Sheet` built on raw tags or radix/shadcn) → `warning`; name the app-shell component it duplicates and recommend migrating to it.
  - **A reusable pattern app-shell lacks** (kanban, hero, chart wrapper) → `info` feature-request, no penalty.
  - **A genuine business composite** that just happens to trip a heuristic → no penalty.

  Do not deduct purely on the CLI numbers; the reasons are evidence, not a verdict.

### 3. Score

Start at 100 and subtract. Thresholds below are anchored to the scaffold-bundled apps' actual measurements (= roughly: resolver avg 65-68 LoC, command avg 90 LoC in built-in modules, JSX appShellRatio ~0.83). Pure-noise tuning will likely happen after a release cycle of real-world data.

| Signal | Subtract |
| ------ | -------- |
| Resolver `avgLoC` > 100 | 2 per 10 LoC above 100, cap 15 |
| Each resolver in `filesOverThreshold` (= over 200 LoC) | 5, cap 25 |
| Command `avgLoC` > 120 | 2 per 10 LoC above 120, cap 10 |
| `command.handWrittenRatio` outside 0.4–0.6 with `command.count` > 5 | 5 |
| Each `libNonTypesFiles` entry that holds misplaced business logic / dead code (judge content; skip pure types, constants, and shared thin helpers) | 5, cap 20 |
| `frontend.jsxComposition.appShellRatio` < 0.55 | 3 per 0.05 below, cap 15 |
| Each `frontend.rawHtmlHeavyPages` entry | 3, cap 15 |
| `frontend.jsxComposition.otherLibraryTags` > 0 from a competing component kit (not an icon/chart gap-filler) | 5 flat (different UI kit smell) |
| `frontend.customComponents` > 20 | 5 (+5 if > 40) |
| Each `frontend.localComponentRisks` entry confirmed (on inspection) to re-implement an app-shell component | 5, cap 20 |

Clamp to `[0, 100]`.

Each subtraction must include a finding so the user understands why points were deducted.

**When `appShellCatalog.source` is `unresolved`:** do **not** apply the `appShellRatio` row — with raw tags excluded the ratio is partial and skews high, so a penalty would be misleading and a pass would be a false negative. Emit an `info` finding telling the user to install deps and re-measure instead. (`rawHtmlHeavyPages` is empty in this state anyway, since `replaceableTags` is 0.) The resolver, command, `lib`, `otherLibraryTags`, and `customComponents` rows are catalog-independent and still apply.

> **Note:** All numeric thresholds in this table are initial values calibrated against the scaffold-bundled apps. They are expected to be re-tuned once enough real-world data accumulates. Report findings even when a threshold is borderline — the human reviewer is the final judge.

### 4. Output

Return JSON with the standard dimension shape:

```json
{
  "dimension": "thinness",
  "score": 78,
  "findings": [
    {
      "severity": "info",
      "message": "Resolvers: 24 files, avg 42 LoC, max 180 — within ideal range."
    },
    {
      "severity": "warning",
      "message": "Command avgLoC 78 exceeds the 60 ideal; consider extracting helpers or splitting commands."
    },
    {
      "severity": "warning",
      "message": "modules/sales/lib/pricing.ts embeds discount-calculation logic that belongs in a command — lib/ should hold types/constants/thin helpers, not business logic."
    },
    {
      "severity": "info",
      "message": "AppShell tag ratio 0.94 (app-shell vs replaceable + other-library tags) — healthy."
    }
  ]
}
```

`severity` mapping in the report:

- positive observations and within-range metrics → `info` (collapsed)
- thresholds exceeded but workable → `warning`
- structural violations (lib/, very long resolvers) → `warning` or `error` depending on magnitude
