# Comparison Map: SmartStack.app ↔ CLI Templates

> This file defines the **exact mapping** between SmartStack.app (source of truth)
> and CLI templates. Used by `/cli-app-sync` to detect drift.

## Paths

| Alias | Path |
|---|---|
| `APP_API` | `D:/01 - projets/SmartStack.app/02-Develop/src/SmartStack.Api` |
| `APP_WEB` | `D:/01 - projets/SmartStack.app/02-Develop/web/smartstack-web` |
| `CLI_TPL` | `D:/01 - projets/SmartStack.cli/02-Develop/templates/project` |
| `CLI_INIT` | `D:/01 - projets/SmartStack.cli/02-Develop/src/commands/init.ts` |
| `CLI_PWA_GEN` | `D:/01 - projets/SmartStack.cli/02-Develop/templates/skills/development/frontend/pwa/cli/scaffold-pwa/generate.ts` |

> `02-Develop` is the ACTIVE CLI worktree (the previous `features/version-5-optimisation`
> path no longer exists). The `APP_*` aliases point at the `02-Develop`
> worktree (develop, the source of truth since the 3.65 rework branch merged) — override
> with `--app-path=<worktree>` when comparing against another branch. It carries the
> mobile shell AND the PWA service-worker modules,
> so every section below runs against the default path; a "missing in app" finding
> there is a real finding, not a wrong-branch artefact. Verify the worktree exists
> before trusting a run — a stale alias makes every comparison silently vacuous.

---

## Backend Comparisons

### `program-cs` — Program.cs startup pipeline

| | Path |
|---|---|
| **Reference** | `APP_API/Program.cs` |
| **Template** | `CLI_TPL/Program.cs.template` |

**Patterns to verify (ordered by startup sequence):**

| # | Pattern | Grep/search | Critical |
|---|---|---|---|
| 1 | Serilog usings | `using Serilog;` + `using Serilog.Events;` | YES |
| 2 | Bootstrap logger | `Log.Logger = new LoggerConfiguration()` | YES |
| 3 | try/catch/finally | `try {` ... `catch (Exception` ... `finally` | YES |
| 4 | Kestrel HTTP/1.1 | `ConfigureKestrel` + `HttpProtocols.Http1` | YES |
| 5 | appsettings.Local.json | `AddJsonFile("appsettings.Local.json"` | YES |
| 6 | Serilog from config | `UseSmartStackSerilog()` | YES |
| 7 | Application Insights | `AddApplicationInsightsTelemetry` | YES |
| 8 | AddSmartStack | `AddSmartStack(builder.Configuration` | YES |
| 9 | InitializeSmartStackAsync | `InitializeSmartStackAsync()` | YES |
| 10 | Swagger | `UseSmartStackSwagger()` | YES |
| 11 | Middleware pipeline | `UseSmartStack()` then `MapSmartStack()` | YES |
| 12 | Success log | `Log.Information(` ... `started successfully` | NO |
| 13 | Fatal log | `Log.Fatal(ex,` | YES |
| 14 | CloseAndFlush | `Log.CloseAndFlush()` | YES |

**Template-specific additions (not in app):**
- `using Microsoft.EntityFrameworkCore;` (for ExtensionsDbContext migration)
- `Add{{ProjectName}}Infrastructure` / `Add{{ProjectName}}Application` (client DI)
- ExtensionsDbContext migration block (after InitializeSmartStackAsync)

---

### `di-app` — DependencyInjection.Application.cs

| | Path |
|---|---|
| **Reference** | *(no app equivalent — client-only pattern)* |
| **Template** | `CLI_TPL/DependencyInjection.Application.cs.template` |

**Patterns to verify:**

| # | Pattern | Search | Critical |
|---|---|---|---|
| 1 | MediatR warning | `MediatR is already registered` or `do NOT register it again` | YES |
| 2 | No AddMediatR example | must NOT contain `services.AddMediatR(cfg =>` | YES |

---

### `di-infra` — DependencyInjection.Infrastructure.cs

| | Path |
|---|---|
| **Reference** | `APP_API/../SmartStack.Infrastructure/DependencyInjection.cs` |
| **Template** | `CLI_TPL/DependencyInjection.Infrastructure.cs.template` |

**Patterns to verify:**

| # | Pattern | Search | Critical |
|---|---|---|---|
| 1 | AddDbContext pattern | `AddDbContext<ExtensionsDbContext>` | YES |
| 2 | SQL Server | `UseSqlServer` | YES |
| 3 | Connection string | `GetConnectionString` | YES |

---

### `appsettings` — appsettings.json

| | Path |
|---|---|
| **Reference** | `APP_API/appsettings.json` |
| **Template** | `CLI_TPL/appsettings.json.template` |

**Method — STRUCTURAL DIFF, not pattern grep.**

Grepping for a handful of section names cannot detect drift: it reports `[OK]` while
a dozen bound sections are missing from the template, and it happily demands sections
the app no longer reads. Compare the two files as KEY SETS instead.

1. Parse both files as JSON (resolve template placeholders first — see
   `resolveTemplatePlaceholders` in `src/lib/config-sync.ts`).
2. Flatten each to a set of leaf key paths joined by `:` — `Email:RecipientGuard:Mode`.
   An array is a LEAF (`Authentication:Microsoft:AllowedTenants`), never recursed into.
3. Report the two set differences, minus the intentional differences below.

```bash
flatten() { node -e '
  const j = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
  const out = [];
  (function walk(o, p) {
    for (const k of Object.keys(o)) {
      const path = p ? p + ":" + k : k, v = o[k];
      if (v && typeof v === "object" && !Array.isArray(v)) walk(v, path); else out.push(path);
    }
  })(j, "");
  console.log(out.sort().join("\n"));
' "$1"; }

flatten "$APP_API/appsettings.json" > /tmp/app.keys
sed -e 's/{{[A-Za-z]*}}//g' "$CLI_TPL/appsettings.json.template" > /tmp/tpl.json
flatten /tmp/tpl.json > /tmp/tpl.keys

echo "=== (a) In app, MISSING from template — client cannot configure these ==="
comm -23 /tmp/app.keys /tmp/tpl.keys
echo "=== (b) In template, ABSENT from app — dead config shipped to clients ==="
comm -13 /tmp/app.keys /tmp/tpl.keys
```

**Findings:**

| Direction | Meaning | Criticality |
|---|---|---|
| (a) app-only path | A bound section a generated project cannot configure. If the backing options class has **no default** (e.g. `LocalContainerOptions.MaxFileSizeMB` → 0 MB, every upload rejected), this is a broken generated app. | BLOCKING |
| (b) template-only path | Config shipped to every client that the API reads nowhere. Remove from the template AND add to `OBSOLETE_PATHS` in `src/lib/config-sync.ts` so `ss upgrade` prunes it from existing projects. | WARNING |

**Intentional differences — exclude from BOTH directions:**

| Path / value | Why |
|---|---|
| `SmartStack:FrontendUrl`, `Logging:Sinks:*:ServerUrl`, `Email:Development:*`, any port | The CLI targets 7074/7042; the app targets 6173/5142. Same KEYS, different values — compare key sets, never values. |
| `ConnectionStrings:DefaultConnection` | Per-environment by definition. |
| `{{ProjectName}}`-style placeholders | Resolved at `ss init`. |
| `Jwt:Secret` | Empty in the template; `init.ts` injects a per-project `randomBytes(64)` after `JSON.parse`. A non-empty secret in the template would ship the SAME signing key to every client. |

⚠ A path in direction (b) is only dead once you have confirmed the API binds it
NOWHERE — check for `GetSection("X")`, a `SectionName = "X"` constant, and
`GetValue<T>("X:Y")` before deleting. `LicenseServer` was wrongly declared dead
on a missing-from-appsettings signal alone while `DependencyInjection.cs` binds
it with `.ValidateOnStart()`; `ss upgrade` then deleted it from client projects.

---

## Frontend Comparisons

### `package-json` — package.json dependencies and scripts

| | Path |
|---|---|
| **Reference** | `APP_WEB/package.json` |
| **Template** | `CLI_INIT` → variable `packageJson` (search: `const packageJson = {`) |

**Dependency version checks:**

| Package | Category | Compare rule |
|---|---|---|
| `@types/node` | devDependencies | Major range must match app (e.g., `^24` vs `^26`) |
| `typescript` | devDependencies | Range strategy and major must match (`~5.9` vs `~5.9`) |
| `eslint-plugin-react-refresh` | devDependencies | Major range must match |
| `react` | dependencies | Major range must match |
| `react-dom` | dependencies | Major range must match |
| `vite` | devDependencies | Major range must match |
| `tailwindcss` | dependencies | Major range must match |

**Testing stack (must be present in template):**

| Package | Required |
|---|---|
| `vitest` | YES |
| `@vitest/coverage-v8` | YES |
| `@testing-library/react` | YES |
| `@testing-library/jest-dom` | YES |
| `@testing-library/user-event` | YES |
| `jsdom` | YES |

**Scripts (must be present in template):**

| Script | Command |
|---|---|
| `test` | `vitest run` |
| `test:watch` | `vitest` |
| `test:coverage` | `vitest run --coverage` |

---

### `vite-config` — vite.config.ts proxy and plugins

| | Path |
|---|---|
| **Reference** | `APP_WEB/vite.config.ts` |
| **Template** | `CLI_INIT` → variable `viteConfig` (search: `` const viteConfig = ` ``) |

**Patterns to verify:**

| # | Pattern | Search | Critical |
|---|---|---|---|
| 1 | React plugin | `react()` | YES |
| 2 | Tailwind plugin | `tailwindcss()` | YES |
| 3 | API proxy | `'/api':` | YES |
| 4 | Proxy bypass | `bypass(req` or `bypass:` | YES |
| 5 | HTML accept check | `accept?.includes('text/html')` | YES |
| 6 | SignalR proxy | `'/hubs':` | YES |
| 7 | WebSocket flag | `ws: true` | YES |
| 8 | Path alias | `'@': '/src'` or `'@/': './src'` | YES |

---

### `tsconfig` — tsconfig.json compiler options

| | Path |
|---|---|
| **Reference** | `APP_WEB/tsconfig.json` or `APP_WEB/tsconfig.app.json` |
| **Template** | `CLI_INIT` → variable `tsConfig` (search: `const tsConfig = {`) |

**Options to verify:**

| Option | Expected value | Critical |
|---|---|---|
| `target` | `ES2022` | YES |
| `lib` | includes `ES2022` | YES |
| `verbatimModuleSyntax` | `true` | YES |
| `erasableSyntaxOnly` | `true` | YES |
| `isolatedModules` | must NOT be present (deprecated) | YES |
| `moduleResolution` | `bundler` | YES |
| `jsx` | `react-jsx` | YES |
| `strict` | `true` | YES |

---

### `index-css` — index.css Tailwind directives

| | Path |
|---|---|
| **Reference** | `APP_WEB/src/index.css` |
| **Template** | `CLI_INIT` → variable `indexCss` (search: `` const indexCss = ` ``) |

**Patterns to verify:**

| # | Pattern | Search | Critical |
|---|---|---|---|
| 1 | Tailwind import | `@import "tailwindcss"` | YES |
| 2 | Source directive | `@source "../node_modules/@atlashub/smartstack` | YES |
| 3 | Dark mode variant | `@custom-variant dark` | YES |

---

## Intentional Exclusions (SKIP)

These comparisons should **always** return `[SKIP]` — differences are by design.

| Item | Reason |
|---|---|
| `App.tsx` | Client uses `PageRegistry`+`DynamicRouter` (v3.7+); app uses DynamicRouter exclusively (mergeRoutes removed in v3.7) |
| `api.ts` | Client re-exports SmartStack client — wrapper pattern is correct |
| `ExtensionsDbContext` | Intentionally simplified vs `CoreDbContext` |
| `GlobalUsings.cs` | App has platform-wide usings; client has minimal set |
| Skills/Agents/Hooks | Installed globally via `ss install`, not by `ss init` |
| Platform-specific skills | `bugfix-issue`, `feature-test`, `sonar`, `version-bump` |
| CI/CD files | `azure-pipelines.yml` is platform-specific |
| `CLAUDE.md` | Each project has its own instructions |

---

## v3.46+ Comparison Points

Added with the v3.46 alignment of CLI templates. These verify client projects honor the new architecture.

| ID | Comparison | Reference path | Patterns to check | Criticality |
|---|---|---|---|---|
| `nav-application-flags` | Seeds use `IsPersonal`/`IsOpen`, NOT `Zone = ApplicationZone.X` | `*/Seeding/Data/*NavigationApplicationSeedData.cs` and `*/Seeding/Data/*NavigationSeedData.cs` | Must match `IsPersonal\s*=` AND `IsOpen\s*=`. Must NOT match `ApplicationZone\.\w+`. | BLOCKING (with C66 grace period — currently WARNING) |
| `frontend-layouts-count` | Exactly 4 layouts present | `web/.../src/layouts/*.tsx` | Must contain `AppLayout.tsx`, `AuthLayout.tsx`, `DocsLayout.tsx`, `PublicLayout.tsx`. Must NOT contain `AdminLayout`, `BusinessLayout`, `UserLayout`, `HRLayout`, `SalesLayout`. | BLOCKING (C67 grace period — WARNING) |
| `provider-pattern` | Client root uses `<SmartStackProvider>` | `web/.../src/main.tsx` or `web/.../src/App.tsx` | Must contain `<SmartStackProvider config={` AND `<DynamicRouter />`. Must NOT contain manual `<AuthProvider>`/`<ThemeProvider>` re-wrap. | WARNING |
| `ai-agent-modes` | If AI activated, enums `AiAgentMode` / `AiAgentStepRole` are present | `src/.../Domain/AI/Agents/*.cs` | If `AiAgent.cs` exists, must reference `AiAgentMode` enum (Sequential/ReAct/PlanAndExecute/SelfCorrection). | WARNING |
| `lookup-endpoint-gate` | Integration `/lookup` endpoints carry the v3.62 dual gate | `src/.../Api/Controllers/**/*Controller.cs` | Every `[HttpGet("lookup")]` must be gated `[RequirePermission(X.Lookup, X.Read)]` (TWO args, ANY semantics). A single-arg `[RequirePermission(X.Read)]` on a lookup endpoint = pre-3.62 scaffold — re-run scaffold-controller. | WARNING (grace period — becomes BLOCKING once the project's platform floor is ≥ 3.62) |
| `export-datasets-seam` | The export-datasets seam is documented and anchored | `CLI_TPL/DependencyInjection.Infrastructure.cs.template` + `CLI_SKILLS/development/backend/data-layer/references/export-datasets.md` | Template must contain `<<< EXPORT-DATASETS-DI BEGIN >>>`; the reference must exist and must state the v3.66 filter semantics (an EMPTIED filter yields an empty collection, not the provider's `DefaultValues`), that `ctx.DepartmentIds` is informational (the engine folds it into `ctx.UserIds`), and the v3.68 document layout — `GroupBy` is the ROW AXIS of a MATRIX (not stacked sections), `RowLayout` says how it is written, a formatter draws it with `ExportSplitView.Build` + `ExportMatrix.Build`, and a line's label goes at its `LabelIndex` with its own `LabelType`. Reference contracts in the app: `src/SmartStack.Application/Common/Exports/IExportDatasetProvider.cs`, `.../ExportStructure.cs`, `.../ExportMatrix.cs`. | WARNING (a stale reference produces fail-OPEN client providers, but nothing fails to compile) |

---

## Mobile-shell Comparison Points

The mobile shell ("descente par paliers") lives **entirely in the package**: a client app
inherits it at `npm install` and switches it on with `mobile: { enabled: true }`. So these
points do NOT compare app files against template files — they verify that the socle still
EXPORTS what the CLI's docs and gates depend on. A rename in SmartStack.app that is not
mirrored here breaks `scaffold-pwa`'s capability probe and every doc claim about the shell.

The default `APP_WEB` worktree carries the shell, so these run as-is. Only pass
`--app-path` when comparing against a branch that predates it.

| ID | Comparison | Reference path | Patterns to check | Criticality |
|---|---|---|---|---|
| `mobile-shell-layouts` | The shell layout set is intact | `APP_WEB/src/layouts/mobile/*.tsx` | Must contain `MobileShell.tsx`, `MobileHeader.tsx`, `MobileBottomNav.tsx`, `MobileBreadcrumbs.tsx`, `MobileTenantSheet.tsx`. | BLOCKING |
| `mobile-shell-pages` | The palier + bottom-bar pages are intact | `APP_WEB/src/pages/mobile/*.tsx` | Must contain `MobileHomePage.tsx` (Applications), `MobileLevelPage.tsx` (Modules → Sections), `MobileTasksPage.tsx`, `MobileActivityPage.tsx`, `MobileAccountPage.tsx`. | BLOCKING |
| `mobile-bottom-nav-4` | The bottom bar still has its FOUR transverse entries | `APP_WEB/src/layouts/mobile/MobileBottomNav.tsx` | Applications / Tâches / Activité / Compte — one route target each. A 3- or 5-entry bar means the CLI docs (pwa SKILL.md, web.CLAUDE.md.template, project README) are stale. | WARNING |
| `mobile-kit-components` | The kit `scaffold-component` emits against exists | `APP_WEB/src/components/mobile/*.tsx` | Must contain `MobileEmptyState.tsx` and `MobileFab.tsx` (mounted on `adapted` LIST pages), plus `MobileFilterBar.tsx`, `MobileDetailTabs.tsx`, `MobileNavList.tsx`, `MobilePageHeading.tsx` (documented, hand-mounted). | BLOCKING for EmptyState/Fab, WARNING for the rest |
| `mobile-nav-context` | The palier navigation hook exists | `APP_WEB/src/hooks/useMobileNavContext.ts` | Also `useViewportMode.ts` (the FAB's viewport gate) and `useRecentPages.ts` ("Reprendre"). | BLOCKING |
| `mobile-package-exports` | Everything above is EXPORTED from the package barrel | `APP_WEB/src/index.ts` | Must export `MobileShell`, `useMobileNavContext` (the two `scaffold-pwa` probes for), `MobileEmptyState`, `MobileFab`, `useViewportMode` (the two `scaffold-component` imports). An unexported component is invisible to every client app. | BLOCKING |
| `me-aggregate-endpoints` | The bottom-bar aggregates are served | `APP_API/Controllers/**` + `src/SmartStack.Application/Common/{Tasks,Activity}` | `GET /api/me/tasks`, `GET /api/me/tasks/count`, `GET /api/me/activity`; seams `ITaskProvider` / `IActivityProvider` with `AddSmartStackTaskProvider<T>()`, `AddExtensionTasks<TContext>(…)`, `AddSmartStackActivityProvider<T>()`. Tâches/Activité render empty without them. | BLOCKING |

---

## PWA Service-Worker Template Comparison Points

A THIRD kind of CLI-side location: the CLI side is a **literal embedded in a
skill scaffolder's `generate.ts`** (neither a `templates/project/*.template`
file nor an `init.ts` variable). `scaffold-pwa` embeds faithful copies of the
socle's `src/pwa/{sw,cacheKey,swMessages}.ts` as template strings and emits
them into every client app — exactly the drift class this skill exists to
catch, historically invisible because the map stopped at `.template` files and
`init.ts` (the a6b9f2a5 per-account cache-key hardening reached no client for
that reason).

To compare: extract the literal (grep hints in the SKILL.md
`<extraction_patterns>`), STRIP the leading `AUTO-GENERATED` banner block,
un-escape the template string (`\`` → `` ` ``, `\\` → `\` inside regex
literals, and for `swSource` re-substitute `${title}` → `SmartStack`), then
diff verbatim against the socle file. Expected residual diff: NONE below the
banner (sw.ts: the banner REPLACES the socle's own doc header; cacheKey.ts /
swMessages.ts: the banner is PREPENDED above the intact socle header).

The default `APP_WEB` worktree carries the split cache-key module
(`src/pwa/{sw,cacheKey,swMessages}.ts`), so these run as-is. Only pass
`--app-path` when comparing against a branch that predates the split.

| ID | Comparison | Reference path | CLI-side literal in `CLI_PWA_GEN` | Patterns to check | Criticality |
|---|---|---|---|---|---|
| `pwa-sw` | The emitted service worker matches the socle SW | `APP_WEB/src/pwa/sw.ts` | `export function swSource(` (template literal) | Byte-faithful modulo the 2 allowed deviations (banner; push fallback title `'SmartStack'` → `${title}`). Must import `{ API_CACHE_NAME, PURGE_API_CACHE_MESSAGE, buildApiCacheKey } from './cacheKey'` and carry the `SACRED_SW_MARKERS` of audit-dev-pwa (12, on the sw+cacheKey concatenation). | BLOCKING |
| `pwa-cachekey` | The emitted cache-key module matches the socle | `APP_WEB/src/pwa/cacheKey.ts` | `export const CACHE_KEY_SOURCE = \`` | Byte-faithful below the banner: `API_CACHE_NAME`, `PURGE_API_CACHE_MESSAGE`, `buildApiCacheKey` folding `X-Tenant-Slug`/`Accept-Language`/`X-User-Id` → `__ss_tenant`/`__ss_lang`/`__ss_user`. | BLOCKING |
| `pwa-swmessages` | The emitted SW-messaging helper matches the socle | `APP_WEB/src/pwa/swMessages.ts` | `export const SW_MESSAGES_SOURCE = \`` | Byte-faithful below the banner: `purgeApiCache()` posting `PURGE_API_CACHE_MESSAGE`, swallowed try/catch. | BLOCKING |

A vitest drift-lock complements (but does not replace) these points:
`scaffold-pwa/__tests__/generate.test.ts` imports `SACRED_SW_MARKERS` from
audit-dev-pwa and asserts every marker against the emitted concatenation — it
locks emitter ↔ auditor, while `pwa-*` above locks emitter ↔ socle.

---

## List-representation & field-caption Comparison Points

A FOURTH location class, and the one that bit hardest: the client app renders a
list through components **nobody compares** — `PageTemplate` (owned by
`scaffold-layout`), `DataTable` / `ResponsiveDataTable` / `FilterBar` /
`tableRepresentation` (owned by `scaffold-ui-primitives`) — while relying on CSS
that lives in the **socle package** (`.btn` in `base.css`). A change on either
side alone is invisible: the socle's own screens stay correct, the generated
app's do not, and no test in either repo crosses the boundary.

That is exactly how a tablet ended up showing button labels broken in two, a
filter panel with two label typographies, and a seven-column table crushed to
~100px per column. Each half was defensible; only their combination was wrong.

⚠ App reference: the socle side of these points landed on the 3.67 rework branch —
pass `--app-path` accordingly until it reaches `02-Develop`.

| ID | Comparison | Reference path | Patterns to check | Criticality |
|---|---|---|---|---|
| `btn-nowrap` | A button label never breaks in two | `APP_WEB/src/base.css` | `.btn` must carry `white-space: nowrap`. Generated pages use RAW `.btn` buttons (`scaffold-component` header actions), so this single socle declaration is what keeps every client app's header readable. Dropping it re-breaks "Ouvrir une / grille" on any narrow header. | BLOCKING |
| `page-header-wrap` | The page header gives way by WRAPPING, never by crushing its actions | `APP_WEB/src/components/ui/PageHeader.tsx` ↔ `scaffold-layout/generate.ts` → `pageTemplate()` | Both must: wrap the row (`flex flex-wrap items-start justify-between`), give the title block a REAL flex-basis (`basis-80` — a bare `flex-1` is basis 0, so the row never wraps and the actions block is crushed instead), and leave the actions block with NEITHER `min-w-0` (shrinks below one button) NOR `shrink-0` (pins at max-content → page-level horizontal scrollbar). | BLOCKING |
| `table-representation` | The width budget module is emitted and is the ONLY owner of width knowledge | `APP_WEB/src/components/ui/tableRepresentation.ts` ↔ `scaffold-ui-primitives/generate.ts` → `tableRepresentationModule()` | Emitted at `src/components/ui/tableRepresentation.ts`; exports `COLUMN_MIN_WIDTH`, `tableMinWidth`, `decideRepresentation`, `useContainerWidth`, `survivingColumns`, `BREAKPOINTS`, `ALWAYS_KEY`. `ResponsiveDataTable` must IMPORT `BREAKPOINTS` (no local copy) and must NOT reference `window.innerWidth` — the pane can be 256-320px narrower than the window, and measuring the window keeps columns that do not fit. | BLOCKING |
| `table-min-width` | `overflow-x-auto` is not inert | `APP_WEB/src/components/ui/DataTable.tsx` ↔ `scaffold-ui-primitives/generate.ts` → `dataTableComponent()` | The `<table>` must carry a real `min-width` (the columns' budget). A `w-full` table in `table-layout: auto` COMPRESSES instead of overflowing, so the wrapper's `overflow-x-auto` never fires and the columns are simply crushed. | BLOCKING |
| `filter-grid` | ONE track definition for the whole filter panel | `scaffold-ui-primitives/generate.ts` → `filterBarComponent()` + `scaffold-component/render/list.ts` | `FILTER_GRID` declared ONCE in `FilterBar` and used by BOTH the primary row and the advanced panel. The page must hand `advanced` over as a bare fragment (`advanced={<>`), never wrapped in its own grid, and must emit uniform `min-w-0` filter cells — no `min-w-[180px]` / `min-w-[220px]` / `min-w-[240px]`. Two grids in two files is how the column origins drifted apart. | BLOCKING |
| `field-caption-signature` | One typography per role | `APP_WEB/scripts/audit-field-labels.mjs` ↔ `scaffold-component/render/list.ts` | The socle guard forbids a caption that is BOTH smaller and dimmer than `text-sm font-medium text-[var(--text-primary)]`; the generator must not emit one either (historically `text-xs font-medium text-[var(--text-muted)]` for text/select/date filters while FK filters delegated to a primitive using the DS signature). If the socle grows the guard, the generator must follow — otherwise every scaffolded page re-imports the drift. | BLOCKING |
| `cards-fallback` | A list always has a narrow representation | `scaffold-component/render/list.ts` | The cards branch is emitted UNCONDITIONALLY; `viewModes` governs only whether the MANUAL toggle is offered. A generator that gates the branch on `viewModes` again means narrow screens fall back to a crushed table — the amputation this whole point exists to prevent. | WARNING |

---

## Maintenance

When adding a new comparison point:
1. Add an entry to the appropriate section above
2. Define the reference path and template location
3. List the key patterns to check with criticality
4. Update the SKILL.md `<comparison_categories>` table
