Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.30.0",
"version": "2.63.0",
"author": {
"name": "FOS Computer Services"
},
Expand Down
4 changes: 4 additions & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Attribution Moratorium

No AI attribution anywhere in this project's artifacts: no AI names, model names, or AI-authorship statements in documents, specifications, code, code comments, commit messages, PR/issue text, or generated files. No `Co-Authored-By`, `Claude-Session`, "Generated with", or similar trailers/banners — in any message or file, ever. Harness-required filenames and paths (`CLAUDE.md`, `.claude/`) are not attribution and are unaffected.

## Build & Test Commands

```bash
Expand Down
17 changes: 17 additions & 0 deletions .claude/docs/FOSMVVMArchitecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,23 @@ If a type is needed by both client and server, it belongs in the shared module.
- Anything used by `MVVMEnvironment`
- Anything that must be consistent across all targets

### The SPMLibraries umbrella — the Xcode-side twin

The shared module solves agreement between targets that are *compiled together*. An Xcode project has a second version of the same problem: several Xcode targets (app, unit tests, UI tests, frameworks) each consuming the same external SPM package products.

**When more than one Xcode target consumes SPM package products, vend them through a single `SPMLibraries` umbrella framework that every target depends on — never link the SPM products directly into each target.** `SPMLibraries` is a thin framework whose dependencies list every external package product (`FOSFoundation`, `FOSMVVM`, …); every other target depends on it.

**Why — a generic Xcode + SPM bug, not FOS-specific.** Linking an SPM library statically into multiple targets compiles a *separate copy of its types into each target*, and Swift's mangled type name carries the linking context. The "same" type then has a different runtime identity per target, so an instance crossing a target boundary fails `is` / `as?` / `==` / `===` against the same type on the other side: **`TypeA != TypeA`**. It compiles clean and breaks at runtime, far from the cause. One umbrella *dynamic* framework means one canonical copy and one shared type identity everywhere.

**Why it matters especially here.** FOSMVVM leans hard on comparing types — type-derived request paths, ViewModel/Request resolution, versioning. An app that skips the umbrella breaks exactly where those comparisons happen. The umbrella looks like redundant re-vending to a mainstream Xcode eye, which is why it has to be stated rather than left implicit.

Two carve-outs, both deliberate:

- **Testing products** (`FOSTesting`, `FOSTestingUI`, `FOSTestingVapor`) stay *out* of the umbrella and link directly into test targets. The umbrella embeds in the shipping app, and testing products must not ride along; their types are never shared across target boundaries, so the identity rule does not apply to them. (Ruled 2026-08-19.)
- **Single-embed.** The app embeds the umbrella and every local framework with sign-on-copy; every other target links without embedding, because the test host already carries the embedded copy. Embedding twice puts two copies in one bundle — the identity failure the umbrella exists to prevent, reintroduced.

This is enforced in three places, and they must agree: the scaffolder's `project.yml` templates emit it, `fosmvvm-doctor` audits an existing project for it (rules R4a/R4b/R5), and generated projects ship a `memory/spm-libraries-settled.md` carrying the argument for the app's own future sessions.

---

## File Organization Conventions
Expand Down
2 changes: 2 additions & 0 deletions .claude/skills/fosmvvm-fields-generator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ public protocol {Name}Fields: ValidatableModel, Codable, Sendable {

> **Overridable-with-a-default member? Declare it as a *requirement* AND provide the default.** If you want a Fields member to have a zero-config default that a conformer can still override (a validation policy, a message source), it must be a protocol **requirement** with a default in an extension. A member defined *only* in an extension is statically dispatched — a conformer's "override" merely **shadows** it and calls through the protocol/a generic `some {Name}Fields` still hit the default. That's a silent OCP failure. See [Architecture Patterns → Requirement + Default = a Real Override](../shared/architecture-patterns.md).

> **The `{name}FieldsValidateModel(validations:fields:)` composition helper lives in the extension deliberately — it is not an override point** (ratified 2026-08-25). Its protocol-derived prefix is the point: a type adopting two Fields protocols writes one `validate(fields:validations:)` that calls `documentFieldsValidateModel(…)` *and* `otherFieldsValidateModel(…)` — a composition seam, so the requirement-plus-default rule above does not apply to it. (`ValidatableModel.validate(fields:validations:)` itself *is* a real requirement, so a `validate` default in a Fields extension is dynamically dispatched and correctly overridable.)

### FormField Definition

```swift
Expand Down
34 changes: 30 additions & 4 deletions .claude/skills/fosmvvm-fluent-datamodel-generator/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,9 @@ final class Idea: DataModel, IdeaFields, Hashable, @unchecked Sendable {
In schema: `.field("created_by", .uuid, .required, .references(User.schema, "id", onDelete: .cascade))`

**When to use each pattern:**
- **Associated type** (`associatedtype User: UserFields`): Required relationships
- **Optional associated type**: Not supported - use `ModelIdType?` for optional FKs
- **Plain `ModelIdType`**: Optional FKs, external system references
- **Associated type** (`associatedtype User: UserFields`): required relationships — `@Parent` satisfies it directly.
- **Optional FK to a table in this database**: `@OptionalParent(key:)` on the model, over a nullable `.references(...)` column. (Optional associated types are not supported on the protocol side; the optional relationship lives on the DataModel only.)
- **Plain `ModelIdType`/`UUID` field**: ONLY for references *outside* this database — an external system's id, a token minted elsewhere — and only with express approval documented at the declaration site, per the firm `ModelIdType Requires Junction Tables Except for @ID` principle. The documentation names its authority (a decision, an issue, an approver) or the condition under which the exception ends — a note that merely describes the reference is not approval. A same-database reference as a raw UUID has no wall: nothing constrains what is written, Fluent cannot load the relation, and the reference dangles silently when the target row goes.

### Migrations

Expand Down Expand Up @@ -310,6 +310,13 @@ Key points:
- Test validation with `@Test(arguments:)`
- Create private test struct implementing the Fields protocol

**Database-backed tests bind an ephemeral database — never an inherited one.** A migration or
schema test uses `app.databases.use(.sqlite(.memory), as: .sqlite)` (or an equally ephemeral,
test-constructed binding). Never bind a DSN read from the ambient environment (`DATABASE_URL`) —
the test's target then becomes whatever the shell says, and *Tests Must Never Modify Production
Data* is a firm principle (repo `CLAUDE.md`): tests SHALL NOT modify, delete, or corrupt
production data; isolation is constructed, not inherited.

**Test structs with associated types:**

```swift
Expand Down Expand Up @@ -350,10 +357,28 @@ private struct TestUser: UserFields {
| `Bool` | `.bool` | `BOOLEAN` |
| `Date` | `.datetime` | `TIMESTAMPTZ` |
| `UUID` | `.uuid` | `UUID` |
| `[UUID]` | `.array(of: .uuid)` | `UUID[]` |
| Custom Enum | `.string` | `VARCHAR` (stored as raw value) |
| `JSONB` | `.json` | `JSONB` |

> **Decode stored enums honestly.** A raw value read back with a coalescing fallback — `SomeEnum(rawValue: stored) ?? .someCase` — silently rewrites every historical row the current enum no longer names. Decode throwing or into an explicit `.unknown` case; never coalesce into a meaning-bearing category.

> **No identity arrays.** `[UUID]` (`.array(of: .uuid)`) flattens a relation into a column nothing can constrain — element-level foreign keys do not exist on array columns. A many-to-many is a junction table + `@Siblings`; a design that genuinely wants the array (a subset pointer into an already-related aggregate, say) must clear the same express-approval bar as any raw identity field, naming the integrity cost it accepts.

> **Identities hide in JSON too.** A `Codable` struct stored as `.json` whose members reference this database's tables by `UUID` carries the same integrity risk as a raw identity column, one struct-level down. Keep same-database references out of JSONB payloads; relate with wrappers and join.

---

## Framework Surface Since v2.1

This skill's patterns predate several FOSMVVMVapor releases. Before hand-writing container loading, sorting, filtering, guarded writes, or live-refresh plumbing, check the catalog — these already exist:

- **`ContainerDataModel` + `ContainmentRelation`** — declare a container's authorization-bearing relations from its own Fluent `@Children`/`@Siblings`/`@Parent` KeyPaths; cardinality and joins come from Fluent, never restated.
- **`SortableDataModel` + `SortMapping`**, **`FilterableDataModel`** — published sort meanings mapped to database ordering, and query-driven narrowing of container loads.
- **`DataModelWriter` + `WriteTargetProviding`** — the guarded write path the CRUD request doors (`CreateRequest`/`UpdateRequest`/`DeleteRequest` registration) require.
- **Live invalidation** — a Fluent-persisted model's committed saves already nudge `.live` clients with no model-side code; non-Fluent sources pair `registerDependency(on:)` / `invalidateProjections(of:)`.

See [`../shared/api-catalog/FOSMVVMVapor.md`](../shared/api-catalog/FOSMVVMVapor.md) for each one's reach-for entry.

---

## See Also
Expand All @@ -376,3 +401,4 @@ private struct TestUser: UserFields {
| 1.3 | 2025-12-24 | Factored out Fields layer to fields-generator skill |
| 2.0 | 2025-12-26 | Renamed to fosmvvm-fluent-datamodel-generator, added Scope Guard, generalized from Kairos-specific to FOSMVVM patterns, added architecture context |
| 2.1 | 2026-01-24 | Update to context-aware approach (remove file-parsing/Q&A). Skill references conversation context instead of asking questions or accepting file paths. |
| 2.2 | 2026-08-25 | Raw-identity rules aligned with the junction-table principle: `@OptionalParent` for same-database optional FKs, no `[UUID]` arrays, no same-database ids in JSONB, express-approval documentation for external references, honest enum decodes. Post-2.1 framework surface pointer (Container/Sortable/Filterable DataModel, DataModelWriter, live invalidation). |
Loading
Loading