Skip to content
Merged
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "fosmvvm-generators",
"description": "FOSMVVM architecture generators for ViewModels, Fields, DataModels, ServerRequests, Leaf Views, and ViewModel Tests",
"version": "2.64.0",
"version": "2.66.0",
"author": {
"name": "FOS Computer Services"
},
Expand Down
14 changes: 10 additions & 4 deletions .claude/docs/FOSMVVMArchitecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -747,21 +747,21 @@ en:
invalidCategory: "The category %{category} is not valid."
```

**2. Simple Errors (String-Based Codes)**
**2. Simple Errors (Case-Keyed Codes)**

For simpler errors without associated values, use a `String` raw value enum:
For simpler errors without associated values, use a plain enum — no raw value — and localize each case by the case itself:

```swift
struct SimpleError: ServerRequestError {
let code: ErrorCode
let message: LocalizableString

enum ErrorCode: String, Codable, Sendable {
enum ErrorCode: Codable, Sendable {
case serverFailed
case applicationFailed

var message: LocalizableString {
.localized(for: Self.self, parentType: SimpleError.self, propertyName: rawValue)
.localized(case: self, parentType: SimpleError.self)
}
}

Expand All @@ -780,6 +780,12 @@ en:
applicationFailed: "The application failed"
```

**Enums never take a `String` raw value** (ruled 2026-09-02)

An enum never takes a `String` raw value. A raw value opens a public string door — `Reason(rawValue: "invalid")` — that anyone can mint or parse, and it makes the case's spelling the user-facing text, which cannot localize. Cases localize through the YAML tree keyed by type and case; the wire carries the case, not a string the type published.

The shipped form is the plain enum above: Swift synthesizes its `Codable`, the wire carries the case name, and `LocalizableString.localized(case:parentType:)` derives the YAML key from the case. `enum X: String` is a review blocker (`no-string-backed-enums`).

**3. Type-Safe Client Handling**

```swift
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/fosmvvm-fields-generator/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,15 @@ public protocol IdeaFields: ValidatableModel, Codable, Sendable {
var ideaValidationMessages: IdeaFieldsMessages { get }
}

public enum Department: String, CaseIterable, Equatable, Codable, Sendable {
public enum Department: CaseIterable, Equatable, Codable, Sendable {
case strategic
case product
case content
case consulting
case operations
}

public enum IdeaStatus: String, CaseIterable, Equatable, Codable, Sendable {
public enum IdeaStatus: CaseIterable, Equatable, Codable, Sendable {
case queued
case exploring
case parking
Expand Down
18 changes: 15 additions & 3 deletions .claude/skills/fosmvvm-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ Areas with no matched files (other than `cross-cutting`) are skipped.

For each area in the dispatch list, dispatch a Task tool subagent (general-purpose) with the prompt template below. Run up to **4 subagents in parallel** (cap chosen to balance throughput against token usage; tune in a future plan if needed).

**Partitioning a large area.** One subagent must read every file it is given in full. When an area's file list exceeds about 100 files, split it into partitions by top-level module directory (`Sources/<Module>`, `Tests/<Module>`), keeping each partition under that size, and dispatch each partition as its own subagent for the same area with the same checks. A partition is a unit of reading, not a unit of judgment: the area's report is the union of its partitions.

**Project-scope clauses run once.** Some checks, or clauses within them, answer a question about the whole project rather than about a site — "no behavioral suite exists", "no `.VersionedTestJSON` directory is committed", "the boot path never installs the error middleware". A check file marks these with a `**Scope:** project` line (see `reference.md`); everything unmarked is site scope. When an area is partitioned, exactly one partition — the first — is the **project-scope dispatch** and evaluates those clauses; every other partition is told to skip them. Otherwise each partition re-discovers the same absence and the report carries one fact eight times.

#### Subagent Prompt Template

```
Expand All @@ -124,6 +128,9 @@ The "right way" lives in the `{generator_skill}` skill. Treat its SKILL.md as th
## Checks to run
{full_check_section_text}

## Project-scope clauses
{project_scope_instruction}

## Instructions
1. For each file in scope, evaluate every check against every relevant code construct in the file.
2. For each finding, report: file:line, severity, check name, the offending code snippet, and a one-sentence explanation citing the generator skill.
Expand All @@ -149,12 +156,17 @@ Format each finding as:
Prevention: {generator-skill}
```

Substitute `{area}`, `{file_list}`, `{reviewer_guidance_section_or_"(none)"}`, `{generator_skill}`, and `{full_check_section_text}` from the loaded check file before dispatching.
Substitute `{area}`, `{file_list}`, `{reviewer_guidance_section_or_"(none)"}`, `{generator_skill}`, and `{full_check_section_text}` from the loaded check file before dispatching. Substitute `{project_scope_instruction}` with one of:

- Unpartitioned area, or the project-scope dispatch of a partitioned one: "Evaluate the checks and clauses marked `**Scope:** project` once, for the whole project, and report each at most once."
- Any other partition: "Skip every check and clause marked `**Scope:** project` — another dispatch of this area holds them. Report site-scope findings only."

### Step 6: Aggregate Findings

Collect each subagent's findings. Parse them into structured records: `{severity, area, file, line, check, message, prevention}`.

**Collapse duplicates across partitions.** Two records with the same `check`, `file`, and `line` are one finding — keep the first. A project-scope finding (its check or clause is marked `**Scope:** project`) reported by more than one dispatch collapses to one record regardless of `file`; if a partition reported it despite the skip instruction, that is the partition's error, not a second finding.

If a subagent returned an error or timeout, record the area as `ERROR` with the failure message; do not abort other areas.

### Step 7: Emit Report
Expand All @@ -167,7 +179,7 @@ If a subagent returned an error or timeout, record the area as `ERROR` with the
**Scope:** {scope description} ({N} files)
**Tier 1 (doctor):** {ran | unavailable — reason and the enabling route}
**Tier 2:** (only when halted) halted — doctor reported structural errors; fix structure first, then re-run
**Areas triaged:** {comma-separated areas, or "none — tier 2 halted"}
**Areas triaged:** {comma-separated areas, each followed by " ({N} partitions)" when it was partitioned, or "none — tier 2 halted"}
**Fail-on threshold:** {threshold}
**Configuration applied:** (omit line if no config) disabled checks: {names}; severity overrides: {name=severity, ...}; excluded paths: {N}; doctor disabled rules: {N} ({M} unmatched: {rule@target, ...})

Expand Down Expand Up @@ -340,7 +352,7 @@ Block scope:

The coverage ledger's register is closed — every gap it identified has a shipped check (most recently G22–G26 in 2.62.0). As of plugin 2.62.0:

- **Covered:** `cross-cutting` (20 checks), `viewmodel` (13), `view` (9 — multi-surface: SwiftUI/Leaf/React), `serverrequest` (9), `swiftui-app-setup` (6), `datamodel` (5), `viewmodel-test` (5), `ui-tests` (4), `fields` (4), `serverrequest-test` (2).
- **Covered:** `cross-cutting` (21 checks), `viewmodel` (13), `view` (9 — multi-surface: SwiftUI/Leaf/React), `serverrequest` (9), `swiftui-app-setup` (6), `datamodel` (5), `viewmodel-test` (5), `ui-tests` (4), `fields` (4), `serverrequest-test` (2).
- **Retired:** `viewmodelrequest`. The rule set names `ServerRequest`, not `ViewModelRequest` — the latter is a `ShowRequest` specialization, so its wire contract is `serverrequest`'s and the VM↔Request pairing is `viewmodel`'s (`viewmodel-request-pairing`).

Violations no check covers still surface as `uncovered-{area}` findings rather than under invented names, so any remaining gap shows up in every report instead of hiding behind official-looking labels — an `uncovered-*` finding is now also a signal that the coverage ledger (`coverage-ledger.md`, beside the checks) may need a new entry.
Expand Down
16 changes: 16 additions & 0 deletions .claude/skills/fosmvvm-review/checks/cross-cutting.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ static var modelNamespace: ModelNamespace { .init(stringLiteral: "User") }
```
**Detection:** Flag: (a) a `public`/`internal` `var`/`func` on a sealed identity/namespace/token type that returns `String`/`UUID` of its private storage; (b) a raw `String`/`UUID` parameter or stored property used as an identity/route/key/token where a typed value exists; (c) constructing an identity/namespace from a string literal rather than a type. Exempt: the single owner-scoped computed that *consumes* the string to build a typed value and never returns it.

## Check: no-string-backed-enums
**Severity:** blocker
**What:** An enum never takes a `String` raw value. A raw value opens a public string door — `Reason(rawValue: "invalid")` — that anyone can mint or parse, and it makes the case's spelling the user-facing text, which cannot localize. Cases localize through the YAML tree keyed by type and case; the wire carries the case, not a string the type published.
**Anti-pattern:**
```swift
enum ErrorCode: String, Codable, Sendable { // a public string door + an unlocalizable spelling
case serverFailed

var message: LocalizableString {
.localized(for: Self.self, parentType: SimpleError.self, propertyName: rawValue) // the raw value IS the key
}
}
```
**Detection:** Flag every `enum … : String` (and `: Int` when the raw value is anything but an ordinal the type itself consumes) — public or internal, wire-crossing or not. The remedy is the plain enum with synthesized `Codable`; a case's localized text comes from `LocalizableString.localized(case:parentType:)`, which derives the YAML key from the case with no string in user code. Exempt: `CodingKeys` (Swift's own coding contract); a `String`-backed enum whose raw value is consumed only by a system API that demands `RawRepresentable<String>` — name the API in the finding when this exemption is claimed, and treat a `rawValue` read anywhere else as the hole; and **tools, not libraries** (ruled 2026-09-02) — a CLI's argument and config-file tokens (`--shape clientServer`, a bootstrap config's `"shape"`) are typed by a human at a prompt, not carried on a wire or shown to a user, so the rule does not reach them. When the framework pin predates `localized(case:parentType:)` (shipped 0.16.0), report as correct at time of writing, now fixable.

## Check: status-interpreted-as-result
**Severity:** blocker
**What:** Client code reading an HTTP status to interpret an operation's *result*. Statuses govern transport consequences only (logging, caching, retry/backoff); result semantics ride the typed error path — the server `throw`s a `ServerRequestError` and the client catches the typed case. Branching business behavior on a status number is the stringly-typed break applied to errors: any failure can wear a 401, so the client learns nothing typed. See [Architecture Patterns → Typed Errors Are the Operation's Throw](../../shared/architecture-patterns.md).
Expand Down Expand Up @@ -188,6 +203,7 @@ Pairs with `deployment-urls-distinguish-environments` (`swiftui-app-setup`) —

## Check: behavioral-suite-standing
**Severity:** warning
**Scope:** project (clause 1, standing); site (clause 2, isolation)
**What:** The behavioral-test channel's **standing and isolation** — and nothing else. Behavioral suites project from requirements + ratified design in a context that never saw the implementation (execution-model's dedicated second channel; the `fosmvvm-behavioral-test-generator` skill). Review verifies that the suite exists and that its isolation held; **review NEVER judges a behavioral suite's assertions against the implementation** (ruled 2026-08-25) — a reviewer proposing to "fix" a behavioral assertion to match the code is committing exactly the contamination the channel exists to prevent. When a behavioral assertion and the code disagree, that is channel disagreement, classified upward (code defect / payload defect / ambiguous requirement) — never a review finding against the test.
**Anti-pattern:** A `*BehavioralTests.swift` suite with `@testable import` of the module under test; a behavioral suite importing an app or server target; a project whose truth layer carries requirements while no behavioral suite exists.
**Detection:** Behavioral suites are identified by the generator's conventions — `{Name}BehavioralTests.swift`, suites named `"{Name} — {REQ} behavioral"`, per-test `// REQ-nn:` traceability comments. Two clauses:
Expand Down
3 changes: 2 additions & 1 deletion .claude/skills/fosmvvm-review/checks/serverrequest.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ struct MyError: ServerRequestError {
let reason: String // free-text only — string-puns with any abort body
}

enum ErrorCode: String, Codable, Sendable {
enum ErrorCode: Codable, Sendable {
case unauthorized401 // status-named — transport leaked into semantics
case badRequest
}
Expand Down Expand Up @@ -173,6 +173,7 @@ This is the same guarantee `registration-uses-the-request-door` protects — cli

## Check: server-installs-the-error-middleware
**Severity:** blocker
**Scope:** project
**What:** The server's boot path installs FOSMVVMVapor's `ErrorMiddleware` (ruled 2026-08-25; the api-catalog's entry is the statement — "Don't keep Vapor's stock ErrorMiddleware"). Without it, Vapor's stock middleware flattens every typed rejection — a validation failure, a thrown `ResponseError` — into a bare 500 with prose, and no client can branch on what happened. The field case: a Fields-contract validation ran correctly on the server and the client saw only `500 Internal Server Error`.
**Anti-pattern:** A `configure(_:)`/`registerServices(_:)` that registers requests and enables localization but never touches `app.middleware` — the stock middleware is silently in charge of every error.
**Detection:** In the server target's boot path, find `app.middleware.use(FOSMVVMVapor.ErrorMiddleware.default(environment:))`. **The module qualification matters**: Vapor declares its own `ErrorMiddleware`, the bare name is ambiguous beside it, and `Vapor.ErrorMiddleware.default` type-checks while installing the wrong one — verify which module's middleware is named, not merely that the line exists. Absence in any server target that declares `ServerRequest`s is the blocker; the remedy is the catalog's one-liner. A project-authored middleware demonstrably serving encodable errors typed-and-localized is a judgment call, not an automatic hit — say what it covers and what the FOS middleware would add.
1 change: 1 addition & 0 deletions .claude/skills/fosmvvm-review/checks/viewmodel-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Say which locale, which fixture, and which keys — the failure mode is a test t

## Check: versioned-baseline-committed
**Severity:** warning
**Scope:** project
**What:** Every non-client-hosted ViewModel under versioned test has at least one **committed** `.VersionedTestJSON` baseline (ruled 2026-08-25; the generator's Conceptual Foundation states the committed-artifact rule). Without one, `expectVersionedViewModel` takes its write-once branch on every clean checkout and then re-decodes only the baseline it just wrote — the wire-shape canary can never fire.
**Anti-pattern:** A test target calling `expectFullViewModelTests(SomeViewModel.self)` with no `.VersionedTestJSON` directory anywhere in its tree.
**Detection:** For each test target exercising `expectFullViewModelTests`/`expectVersionedViewModel`, check that a `.VersionedTestJSON` directory exists in the target's tree, is tracked (not ignored), and holds at least one baseline per non-client-hosted ViewModel under test. `clientHostedFactory` ViewModels are exempt — they carry no server wire contract to pin. Destroying or regenerating existing baselines is `versioned-baselines-not-regenerated`'s finding; this check fires on never having had them.
2 changes: 2 additions & 0 deletions .claude/skills/fosmvvm-review/coverage-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,8 @@ Ordered by damage; this is the work queue for the coming check-authoring stages.

**G26 · SHIPPED 2026-08-25** as `views-dont-mint-prose` (`view`, SwiftUI surface, plugin 2.62.0). Warning, the narrowed ruled form with its three carve-outs (user-authored input, typed values, machine text) plus the `#Preview` exemption. The view generator's Hardcoding Text section learned the operation-argument extension in the same stage. Falsifier clean (no prose literals flow into ops); the true positive is banked from the scaffold's "New Card" literal (which itself remains the queued G26-shaped template follow-up: the default title belongs on the VM, localized).

**G27 · SHIPPED 2026-09-02** as `no-string-backed-enums` (`cross-cutting`, blocker, plugin 2.66.0). The truth statement was authored the same day into FOSMVVMArchitecture.md (§ Enums never take a `String` raw value) — check-may-lead in reverse: the rule had been stated in review many times and never written down, and the architecture doc's own "Simple Errors" section taught the anti-pattern. Surfaced by a review of the framework's own `CredentialRejectedError`. The same stage swept the framework's five String-backed FOSMVVM enums, the seven teaching sites (architecture doc, three generator references, the serverrequest generator, the serverrequest check's example, two DocC articles), and shipped `LocalizableString.localized(case:parentType:)` so a case localizes without a string in user code.

## Minor uncovered clauses

Recorded for completeness; none warrants its own stage — fold each into the nearest stage's authoring:
Expand Down
5 changes: 4 additions & 1 deletion .claude/skills/fosmvvm-review/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ Free-form meta-instructions read by the subagent BEFORE running checks. Use for:

### `## Check: <name>` (zero or more)

Each check has four required fields:
Each check has four required fields, and one optional:

- `**Scope:** project` (optional; default is site) — the check, or the named clause of it, answers a question about the whole project rather than about a site, so it is evaluated once per run by the project-scope dispatch and skipped by every other partition of the area (SKILL.md Step 5). Write `**Scope:** project (clause 1); site (clause 2)` when a check mixes the two.


- **Severity:** `blocker` | `warning` | `nit`
- **What:** one-sentence description of the rule.
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/fosmvvm-serverrequest-generator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -719,12 +719,12 @@ public final class IdeaMoveRequest: UpdateRequest, @unchecked Sendable {
public let code: ErrorCode
public let message: LocalizableString

public enum ErrorCode: String, Codable, Sendable {
public enum ErrorCode: Codable, Sendable { // never `: String` — a raw value is a public string door and cannot localize
case ideaNotFound
case invalidTransition

var message: LocalizableString {
.localized(for: Self.self, parentType: ResponseError.self, propertyName: rawValue)
.localized(case: self, parentType: ResponseError.self)
}
}

Expand Down
8 changes: 4 additions & 4 deletions .claude/skills/fosmvvm-serverrequest-generator/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,12 +471,12 @@ public struct Simple{Entity}Error: ServerRequestError {
public let code: ErrorCode
public let message: LocalizableString

public enum ErrorCode: String, Codable, Sendable {
public enum ErrorCode: Codable, Sendable {
case notFound
case permissionDenied

var message: LocalizableString {
.localized(for: Self.self, parentType: Simple{Entity}Error.self, propertyName: rawValue)
.localized(case: self, parentType: Simple{Entity}Error.self)
}
}

Expand Down Expand Up @@ -664,12 +664,12 @@ public struct PermissionError: ServerRequestError {
public let code: ErrorCode
public let message: LocalizableString

public enum ErrorCode: String, Codable, Sendable {
public enum ErrorCode: Codable, Sendable {
case insufficientRole
case accountSuspended

var message: LocalizableString {
.localized(for: Self.self, parentType: PermissionError.self, propertyName: rawValue)
.localized(case: self, parentType: PermissionError.self)
}
}

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/fosmvvm-viewmodel-generator/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ public extension SettingsViewModel {
}
}

public enum Theme: String, Codable, Sendable {
public enum Theme: Codable, Sendable {
case light, dark, system
}
```
Expand Down
Loading
Loading