@horka/app-forge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/LICENSE +32 -0
  2. package/README.md +99 -0
  3. package/bin/cli.js +371 -0
  4. package/bin/cli.test.js +91 -0
  5. package/package.json +43 -0
  6. package/templates/core/CLAUDE.md +36 -0
  7. package/templates/core/claude/memory/ARCHITECTURE.md +20 -0
  8. package/templates/core/claude/memory/COMMANDS.md +13 -0
  9. package/templates/core/claude/memory/DECISIONS.md +5 -0
  10. package/templates/core/claude/memory/NEXT_STEPS.md +11 -0
  11. package/templates/core/claude/memory/PROJECT_STATE.md +24 -0
  12. package/templates/core/claude/skills/kickoff/SKILL.md +84 -0
  13. package/templates/core/claude/skills/product-owner/SKILL.md +58 -0
  14. package/templates/core/claude/skills/restore-context/SKILL.md +29 -0
  15. package/templates/core/claude/skills/save-context/SKILL.md +35 -0
  16. package/templates/core/docs-architecture/ANTI_PATTERNS.md +180 -0
  17. package/templates/core/docs-architecture/ARCHITECTURE_PRINCIPLES.md +134 -0
  18. package/templates/core/docs-architecture/DELIVERY.md +68 -0
  19. package/templates/core/docs-architecture/DOCS_PLACEMENT.md +151 -0
  20. package/templates/core/docs-architecture/MULTI_REPO_CONTRACT.md +158 -0
  21. package/templates/core/docs-architecture/SDK_CONTRACT.md +214 -0
  22. package/templates/core/docs-architecture/SECURITY_USER_URLS.md +152 -0
  23. package/templates/core/gitignore +15 -0
  24. package/templates/core/mcp.json +8 -0
  25. package/templates/packs/nuxt-web/CLAUDE.md +74 -0
  26. package/templates/packs/nuxt-web/app/app.vue +5 -0
  27. package/templates/packs/nuxt-web/app/assets/css/main.css +18 -0
  28. package/templates/packs/nuxt-web/app/assets/css/tokens.css +41 -0
  29. package/templates/packs/nuxt-web/app/designSystem/DSButton/components/DSButton.vue +70 -0
  30. package/templates/packs/nuxt-web/app/designSystem/DSButton/index.ts +4 -0
  31. package/templates/packs/nuxt-web/app/designSystem/DSButton/tests/DSButton.spec.ts +34 -0
  32. package/templates/packs/nuxt-web/app/designSystem/DSButton/types/dsButton.ts +5 -0
  33. package/templates/packs/nuxt-web/app/domain/.gitkeep +0 -0
  34. package/templates/packs/nuxt-web/app/features/.gitkeep +0 -0
  35. package/templates/packs/nuxt-web/app/pages/index.vue +36 -0
  36. package/templates/packs/nuxt-web/app/utils/.gitkeep +0 -0
  37. package/templates/packs/nuxt-web/claude/memory/COMMANDS.md +21 -0
  38. package/templates/packs/nuxt-web/docs-architecture/ARCHITECTURE.md +169 -0
  39. package/templates/packs/nuxt-web/docs-architecture/CONVENTIONS.md +140 -0
  40. package/templates/packs/nuxt-web/docs-architecture/I18N.md +102 -0
  41. package/templates/packs/nuxt-web/docs-architecture/OPS_WEB.md +176 -0
  42. package/templates/packs/nuxt-web/docs-architecture/SEO_AND_ROUTING.md +118 -0
  43. package/templates/packs/nuxt-web/gitignore +18 -0
  44. package/templates/packs/nuxt-web/nuxt.config.ts +49 -0
  45. package/templates/packs/nuxt-web/pack.json +11 -0
  46. package/templates/packs/nuxt-web/package.json +31 -0
  47. package/templates/packs/nuxt-web/playwright.config.ts +39 -0
  48. package/templates/packs/nuxt-web/server/api/health.get.ts +7 -0
  49. package/templates/packs/nuxt-web/tests/e2e/home.spec.ts +19 -0
  50. package/templates/packs/nuxt-web/tsconfig.json +4 -0
  51. package/templates/packs/nuxt-web/vitest.config.ts +23 -0
  52. package/templates/packs/swift-ios/CLAUDE.md +64 -0
  53. package/templates/packs/swift-ios/Packages/DataLayer/Package.swift +21 -0
  54. package/templates/packs/swift-ios/Packages/DataLayer/Sources/DataLayer/DataLayer.swift +11 -0
  55. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Package.swift +20 -0
  56. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Domain/SampleItem.swift +15 -0
  57. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Engine/SampleEngine.swift +14 -0
  58. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Repository/SampleItemRepository.swift +27 -0
  59. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Tests/{{PROJECT_NAME}}CoreTests/SampleEngineTests.swift +32 -0
  60. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Package.swift +17 -0
  61. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Color+DS.swift +18 -0
  62. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Components/DSCard.swift +22 -0
  63. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DS.swift +36 -0
  64. package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DSFont.swift +26 -0
  65. package/templates/packs/swift-ios/claude/memory/COMMANDS.md +18 -0
  66. package/templates/packs/swift-ios/docs-architecture/ARCHITECTURE.md +246 -0
  67. package/templates/packs/swift-ios/docs-architecture/CLOUDKIT_GUIDE.md +224 -0
  68. package/templates/packs/swift-ios/docs-architecture/CONVENTIONS.md +246 -0
  69. package/templates/packs/swift-ios/docs-architecture/DESIGN_SYSTEM.md +272 -0
  70. package/templates/packs/swift-ios/docs-architecture/NAVIGATION.md +241 -0
  71. package/templates/packs/swift-ios/docs-architecture/TESTING.md +176 -0
  72. package/templates/packs/swift-ios/docs-architecture/WORKFLOW.md +165 -0
  73. package/templates/packs/swift-ios/github/workflows/ci.yml +48 -0
  74. package/templates/packs/swift-ios/gitignore +5 -0
  75. package/templates/packs/swift-ios/mcp.json +8 -0
  76. package/templates/packs/swift-ios/pack.json +11 -0
  77. package/templates/packs/swift-ios/project.yml +33 -0
  78. package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/App.swift +32 -0
  79. package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/AppNamespace.swift +4 -0
  80. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Module/.gitkeep +0 -0
  81. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Store/.gitkeep +0 -0
  82. package/templates/packs/swift-ios/{{PROJECT_NAME}}/Tools/.gitkeep +0 -0
  83. package/templates/packs/ts-sdk/CHANGELOG.md +9 -0
  84. package/templates/packs/ts-sdk/CLAUDE.md +72 -0
  85. package/templates/packs/ts-sdk/MIGRATION.md +28 -0
  86. package/templates/packs/ts-sdk/claude/memory/COMMANDS.md +21 -0
  87. package/templates/packs/ts-sdk/docs-architecture/ARCHITECTURE.md +132 -0
  88. package/templates/packs/ts-sdk/docs-architecture/CONVENTIONS_TS.md +152 -0
  89. package/templates/packs/ts-sdk/gitignore +6 -0
  90. package/templates/packs/ts-sdk/pack.json +11 -0
  91. package/templates/packs/ts-sdk/package.json +55 -0
  92. package/templates/packs/ts-sdk/scripts/verify-dist.mjs +67 -0
  93. package/templates/packs/ts-sdk/src/clients/AuthClient.ts +168 -0
  94. package/templates/packs/ts-sdk/src/core/HttpClient.ts +85 -0
  95. package/templates/packs/ts-sdk/src/core/Logger.ts +27 -0
  96. package/templates/packs/ts-sdk/src/core/SDKContext.ts +40 -0
  97. package/templates/packs/ts-sdk/src/core/withTimeout.ts +19 -0
  98. package/templates/packs/ts-sdk/src/errors/ApiError.ts +93 -0
  99. package/templates/packs/ts-sdk/src/index.ts +62 -0
  100. package/templates/packs/ts-sdk/src/types/index.ts +33 -0
  101. package/templates/packs/ts-sdk/tests/apiError.test.ts +58 -0
  102. package/templates/packs/ts-sdk/tests/httpClient.test.ts +60 -0
  103. package/templates/packs/ts-sdk/tests/singleFlight.test.ts +191 -0
  104. package/templates/packs/ts-sdk/tsconfig.json +15 -0
  105. package/templates/packs/ts-sdk/tsup.config.ts +22 -0
  106. package/templates/packs/ts-sdk/vitest.config.ts +8 -0
  107. package/templates/packs/vapor-api/CLAUDE.md +73 -0
  108. package/templates/packs/vapor-api/Dockerfile +80 -0
  109. package/templates/packs/vapor-api/Package.swift +68 -0
  110. package/templates/packs/vapor-api/Sources/App/App.swift +5 -0
  111. package/templates/packs/vapor-api/Sources/App/Configure/AppConfig.swift +108 -0
  112. package/templates/packs/vapor-api/Sources/App/Configure/configure.swift +74 -0
  113. package/templates/packs/vapor-api/Sources/App/Configure/entrypoint.swift +47 -0
  114. package/templates/packs/vapor-api/Sources/App/Configure/routes.swift +21 -0
  115. package/templates/packs/vapor-api/Sources/App/Error/Failed.swift +73 -0
  116. package/templates/packs/vapor-api/Sources/App/Error/FailedMiddleware.swift +56 -0
  117. package/templates/packs/vapor-api/Sources/App/Features/Item/AppItem.swift +38 -0
  118. package/templates/packs/vapor-api/Sources/App/Features/Item/Controllers/ItemControllersCrud.swift +41 -0
  119. package/templates/packs/vapor-api/Sources/App/Features/Item/DTO/ItemDTO.swift +22 -0
  120. package/templates/packs/vapor-api/Sources/App/Features/Item/Entities/ItemEntity.swift +30 -0
  121. package/templates/packs/vapor-api/Sources/App/Features/Item/Migrations/ItemMigrationCreate.swift +25 -0
  122. package/templates/packs/vapor-api/Sources/App/Features/Item/Repositories/ItemRepository.swift +32 -0
  123. package/templates/packs/vapor-api/Sources/App/Features/Item/Services/ItemService.swift +57 -0
  124. package/templates/packs/vapor-api/Sources/App/Registry/ControllersRegister.swift +17 -0
  125. package/templates/packs/vapor-api/Sources/App/Registry/MiddlewaresRegister.swift +15 -0
  126. package/templates/packs/vapor-api/Sources/App/Registry/MigrationsRegister.swift +18 -0
  127. package/templates/packs/vapor-api/Sources/Monitoring/Logging/JSONLogHandler.swift +59 -0
  128. package/templates/packs/vapor-api/Sources/Monitoring/Middleware/HTTPLoggingMiddleware.swift +50 -0
  129. package/templates/packs/vapor-api/Sources/Monitoring/Monitoring.swift +110 -0
  130. package/templates/packs/vapor-api/Sources/{{PROJECT_NAME}}Foundation/String+Trimmed.swift +15 -0
  131. package/templates/packs/vapor-api/Tests/AppTests/AppTests.swift +155 -0
  132. package/templates/packs/vapor-api/claude/memory/COMMANDS.md +30 -0
  133. package/templates/packs/vapor-api/docs-architecture/ARCHITECTURE.md +144 -0
  134. package/templates/packs/vapor-api/docs-architecture/CONVENTIONS.md +121 -0
  135. package/templates/packs/vapor-api/docs-architecture/GOTCHAS_LINUX_SWIFT.md +109 -0
  136. package/templates/packs/vapor-api/docs-architecture/OPS.md +102 -0
  137. package/templates/packs/vapor-api/env_dist +29 -0
  138. package/templates/packs/vapor-api/gitignore +7 -0
  139. package/templates/packs/vapor-api/pack.json +11 -0
  140. package/templates/packs/vapor-api/scripts/generate-error-codes.sh +73 -0
  141. package/templates/packs/vapor-api/scripts/validate-env-vars.sh +72 -0
@@ -0,0 +1,68 @@
1
+ # Delivery Method — vertical slices, proof over claims (platform-agnostic)
2
+
3
+ How work ships, regardless of stack. This file is the method. The concrete build/run/proof
4
+ commands come from the platform pack if it provides them (e.g. a `WORKFLOW.md` and a proof
5
+ ladder); if the pack ships none, establish that loop yourself and record it in
6
+ `.claude/memory/COMMANDS.md`.
7
+
8
+ ## Vertical slices
9
+
10
+ Never build horizontally ("all the models, then all the screens"). Ship **vertical slices**:
11
+ thin end-to-end features that a user can see working.
12
+
13
+ - **Slice 0 — skeleton runs**: empty app boots on the target (simulator/emulator/browser/server
14
+ responds). Proof: screenshot or curl output.
15
+ - **Slice 1 — domain heart**: core entities + the main engine, exhaustively tested, plus the ONE
16
+ main screen reading from InMemory data. Proof: tests green + screenshot.
17
+ - **Slice N**: one feature each, always end-to-end (L3 logic → L2 data → L3/L4 UI bricks → L5
18
+ screen), always leaving the app shippable.
19
+
20
+ Each slice gets a short blueprint before code: goal, files per layer, test plan, demo criterion
21
+ ("what the user sees"). Blueprints live in `docs/`.
22
+
23
+ ## The build loop (per slice)
24
+
25
+ 1. **L3 Core Logic first**: models/engines + contracts + their tests. Layer tests green before moving on.
26
+ 2. **L2 Data**: InMemory impl of the contracts (real backend impl only when the slice demands it).
27
+ 3. **L3 Core UI / L4 bricks**: reusable components — L0 tokens only, callbacks as boundaries.
28
+ 4. **L5 Feature**: assemble screen + state + navigation.
29
+ 5. **Full build** of the app target; fix until green.
30
+ 6. **Eyes-on validation**: run it, navigate to the feature, capture proof (screenshot/recording/
31
+ response), and actually inspect it — layout, empty states, error states.
32
+ 7. **Memory update**: PROJECT_STATE.md (done/todo/gotchas), DECISIONS.md if a choice was made.
33
+
34
+ ## Validation etiquette (the trust contract)
35
+
36
+ - **Executed-output only**: any value presented as produced by the code (durations, counts, demo
37
+ strings) must come from actually executed output. Didn't run it? Label it an estimate.
38
+ - **Commit at every gate and slice completion** (`add/update/fix(scope) - description`). Gate
39
+ discipline must be provable from git history — two delivered slices sitting untracked on main
40
+ is an audit failure.
41
+
42
+ - **Never claim done without proof.** Tests green + build green + visual/behavioral proof.
43
+ - **Failures are reported with output**, not narrated away. A skipped step is stated as skipped.
44
+ - **Layer tests before app builds** — they're orders of magnitude faster and more precise.
45
+ - **Every new domain rule ships with its test in the same change.** No test, no rule.
46
+ - **Gotchas get written down** the moment they're solved: symptom → cause → fix, in
47
+ PROJECT_STATE.md. The next session must not pay for them again.
48
+
49
+ ## Memory protocol
50
+
51
+ `.claude/memory/` is the project's long-term brain:
52
+
53
+ | File | Contains |
54
+ |---|---|
55
+ | PROJECT_STATE.md | Current slice, done/in-progress/todo, gotchas log, launch blockers |
56
+ | ARCHITECTURE.md | How THIS project instantiates the layers; deviations + why |
57
+ | DECISIONS.md | Dated one-liners a future session must not re-litigate |
58
+ | NEXT_STEPS.md | Ordered backlog, blockers waiting on the user |
59
+ | COMMANDS.md | Commands proven to work here, exact flags |
60
+
61
+ Restore at session start; save after significant work. On conflict, **code wins over memory** —
62
+ then fix the memory file.
63
+
64
+ ## When to stop and ask the user
65
+
66
+ Only for: genuine scope ambiguity, paid/external dependencies, irreversible actions
67
+ (deleting data, publishing), or actions only they can perform (store consoles, certificates,
68
+ real-device tests). Everything else: decide, note it in DECISIONS.md, keep moving.
@@ -0,0 +1,151 @@
1
+ # Docs Placement — where knowledge lives (the two-tier rule)
2
+
3
+ Documentation here is not for humans browsing a wiki — it is **loaded into AI agent context**
4
+ and treated as ground truth. A wrong doc is worse than no doc: the agent acts on it.
5
+ This file defines where each kind of knowledge must live so it stays true.
6
+
7
+ ## The two-tier rule
8
+
9
+ **Tier 1 — umbrella / root docs** (`docs-architecture/`, root `CLAUDE.md`):
10
+ hold ONLY knowledge that survives refactors —
11
+
12
+ - **Contracts**: what each component promises the others (API shapes, SDK boundaries,
13
+ event formats). The L3 repository contracts of ARCHITECTURE_PRINCIPLES.md are the model.
14
+ - **Ordering**: in what sequence work flows across components
15
+ (e.g. "API first, then SDK, then clients" — the *rule*, never the file list).
16
+ - **Invariants**: the rules that make the system safe to change
17
+ ("L3 Core Logic is pure", "tokens are the only source of visual values").
18
+ - **Infra topology**: what runs where, what talks to what, deployment shape.
19
+
20
+ **Tier 2 — colocated docs** (per-repo/per-module `CLAUDE.md` or `AGENTS.md`, `docs/`
21
+ folders next to code, doc comments): hold **every file-path fact** — module layout,
22
+ where a feature lives, which command builds this package, local conventions.
23
+
24
+ **The placement test:** *if a refactor could move a file and silently make the sentence
25
+ false, the sentence belongs next to that file — Tier 2.* A root doc must never name a
26
+ path inside a child component. Point at the component's own docs and stop.
27
+
28
+ ## Why — a two-year natural experiment
29
+
30
+ > 📖 **War story:** A production team ran a multi-component product (API, web app,
31
+ > mobile app, JS SDK) for two years with both doc styles side by side.
32
+ > **Symptom:** agents kept editing the wrong files, "fixing" code that didn't exist, and
33
+ > citing a frontend stack two major versions out of date.
34
+ > **Cause:** the umbrella docs were path inventories. A root "component mapping" file —
35
+ > tables mapping every feature to exact file paths across four repos — was audited and
36
+ > found **~90% fiction**: the API's `Controllers/` directory had been dissolved into use
37
+ > cases and multi-target packages; the SDK had moved under another repo's umbrella and its
38
+ > service files renamed; the web framework's major upgrade had relocated the entire pages
39
+ > tree. The root "architecture" doc still claimed a static-CDN frontend (it had become
40
+ > SSR), the previous framework major version, and an HTTP library that was no longer a
41
+ > dependency. Nobody updates a doc they don't see while editing the code it describes.
42
+ > **Fix:** the team deleted the mapping tables outright. The colocated rule-style docs —
43
+ > per-repo conventions, invariants, gotcha logs — had stayed accurate the whole time,
44
+ > because *rules don't move when files do*. That asymmetry is this file's entire thesis.
45
+
46
+ ## What goes where
47
+
48
+ | Knowledge | Lives in | Tier |
49
+ |---|---|---|
50
+ | Layer rules, dependency arrows (L0–L5) | `docs-architecture/ARCHITECTURE_PRINCIPLES.md` | 1 |
51
+ | Delivery method, memory protocol | `docs-architecture/DELIVERY.md` | 1 |
52
+ | Cross-component contracts & work ordering | `docs-architecture/` | 1 |
53
+ | Infra/deploy topology (what runs where) | `docs-architecture/` or `deploy/docs/` | 1 |
54
+ | "Component X handles Y — see `X/CLAUDE.md`" | root `CLAUDE.md` (pointer only) | 1 |
55
+ | Module layout, where features live | that module's `CLAUDE.md` / `docs/` | 2 |
56
+ | Build/test/run commands for a component | that component's docs + `.claude/memory/COMMANDS.md` | 2 |
57
+ | Per-repo conventions, naming, local rules | that repo's `CLAUDE.md` | 2 |
58
+ | Feature debriefs, gotchas | next to the code + PROJECT_STATE.md gotcha log | 2 |
59
+ | Slice blueprints | `docs/` of the component being sliced | 2 |
60
+
61
+ ## Anti-pattern: the feature→file mapping table
62
+
63
+ **Docs that restate code rot the fastest.** A table mapping features to file paths is a
64
+ cache of the file tree with no invalidation — every refactor, rename, or framework upgrade
65
+ silently corrupts it, and nothing fails when it does.
66
+
67
+ Never write:
68
+
69
+ ```markdown
70
+ | Feature | Backend | Frontend |
71
+ |---|---|---|
72
+ | Login | /api/src/controllers/AuthController.ts | /web/pages/login.vue |
73
+ ```
74
+
75
+ The codebase already IS this table, always up to date. Agents have grep, glob, and LSP;
76
+ a stale map is strictly worse than a 2-second search. Write instead:
77
+
78
+ ```markdown
79
+ Auth lives in the api component (L4 shared feature). Conventions and entry points:
80
+ see `api/CLAUDE.md`. Invariant: clients never mint tokens — only the API issues them
81
+ via the auth provider.
82
+ ```
83
+
84
+ The same ban covers prose inventories ("the engine lives in `src/core/engine.ts`"),
85
+ directory-tree ASCII art for *other* components, and root docs listing another repo's
86
+ commands.
87
+
88
+ ## Write invariants and war stories — never inventories
89
+
90
+ The durable doc sentences are the ones a refactor cannot falsify:
91
+
92
+ - **Invariant:** "L2 never imports L3 services — contracts and models only." True in any
93
+ file layout. ✅
94
+ - **Contract:** "The SDK is the only sanctioned HTTP path for web clients; bypassing it
95
+ to call the API directly is a bug." ✅
96
+ - **War story:** symptom → cause → fix (format below). Pays rent every time someone
97
+ hits the same wall. ✅
98
+ - **Inventory:** "Controllers are in `Sources/App/Controllers/`." One restructure away
99
+ from fiction. ❌ — move it to that package's own doc, where the person moving the
100
+ files is staring at it.
101
+
102
+ > ⚠️ **Gotcha:** "But the inventory helps the agent find things faster."
103
+ > **Symptom:** it does — for the first month. **Cause:** after that it actively misleads:
104
+ > in the experiment above the agent confidently edited paths that had not existed for a
105
+ > year, and burned sessions reconciling doc vs reality. **Fix:** ship the *search recipe*
106
+ > instead ("routes are registered in one file per component — grep for the route
107
+ > registrar"), which stays true across renames.
108
+
109
+ ## The staleness loop — code wins
110
+
111
+ Any doc line that claims a **path, command, version, or flag** is a liability with a
112
+ maintenance contract:
113
+
114
+ 1. **On touch, spot-check.** Whenever a session reads such a claim before acting,
115
+ verify it against the code first (one `ls`/grep). Never act on the claim directly.
116
+ 2. **On conflict, code wins.** Same rule as DELIVERY.md's memory protocol: the doc is
117
+ wrong, not the code. No exceptions.
118
+ 3. **Fix in the same change.** Correct the line — or better, demote it to Tier 2 or
119
+ delete it — in the very commit that revealed the rot. Stale claims found and left
120
+ in place are bugs you chose to ship.
121
+ 4. **On refactor, sweep.** Moving or renaming files? Grep the docs of *that component*
122
+ for the old paths. The two-tier rule makes this tractable: only colocated docs can
123
+ name paths, so the sweep never leaves the component.
124
+
125
+ ## The feature debrief
126
+
127
+ After a hairy feature — anything that fought back — write a **short colocated debrief**
128
+ before moving on: 10–20 lines, next to the code it concerns (the module's `CLAUDE.md`
129
+ or `docs/`), with the durable parts mirrored into the memory files (DELIVERY.md):
130
+
131
+ - **What broke** — the 2–3 real obstacles, as gotcha blocks (symptom → cause → fix).
132
+ - **What to know** — the non-obvious decisions and the invariants they created.
133
+ - **What NOT to try** — dead ends, so the next session doesn't pay for them again.
134
+
135
+ A short debrief next to the code beats a wiki page every time: it loads into context
136
+ exactly when an agent works on that code, and it dies with the code when the code is
137
+ deleted — which is correct. Wiki pages outlive their subject and become Tier-1 fiction.
138
+
139
+ Wire it into the memory system: gotchas land in `PROJECT_STATE.md`'s gotcha log,
140
+ decisions in `DECISIONS.md`, newly-proven commands in `COMMANDS.md`. Memory files obey
141
+ the same staleness loop — they claim paths and commands, so they get spot-checked and
142
+ code wins.
143
+
144
+ ## Summary card
145
+
146
+ 1. Root docs: contracts, ordering, invariants, infra. **Zero child-component paths.**
147
+ 2. Every file-path fact lives next to the code it describes.
148
+ 3. No feature→file mapping tables — docs that restate code rot the fastest.
149
+ 4. Write invariants and war stories; the codebase is its own inventory.
150
+ 5. Path/command claims get spot-checked on touch; code wins; fix in the same change.
151
+ 6. Hairy feature done → short colocated debrief + memory update, not a wiki page.
@@ -0,0 +1,158 @@
1
+ # Multi-Repo Contract — backend → SDK → clients, one order, no exceptions
2
+
3
+ When {{PROJECT_NAME}} spans several repos (API, SDK, web, mobile…), the repos form a
4
+ dependency graph exactly like layers inside one codebase. This file is the cross-repo
5
+ analog of ARCHITECTURE_PRINCIPLES.md: **imports point downward — between repos too.**
6
+
7
+ ## The repo lattice
8
+
9
+ ```
10
+ CLIENTS web app, mobile app, CLI, integrations consume the SDK's public surface only
11
+ ↓ depends on
12
+ SDK typed client library: services, DTOs, the API contract, made importable
13
+ auth/session handling, error normalization
14
+ ↓ depends on (wire protocol only — HTTP/GraphQL/gRPC)
15
+ BACKEND the API: routes, domain, persistence knows nothing about SDK or clients
16
+ ```
17
+
18
+ Mapping to the 6-layer model:
19
+ - Each repo internally runs its **own full L0–L5 lattice** (the API too — see
20
+ "This applies to EVERY stack" in ARCHITECTURE_PRINCIPLES.md).
21
+ - From a client's point of view, **the SDK is its L2 Data brick**: network client +
22
+ DTO ↔ domain mapping. The in-repo rule "a layer imports only layers strictly below it"
23
+ becomes "a client imports only the SDK's public surface."
24
+ - The backend is *below* everything: it never imports the SDK, never special-cases a
25
+ client. Upward knowledge is a violation, same as L2 calling L5.
26
+
27
+ ## Implementation order — backend first, SDK second, clients last
28
+
29
+ Every cross-repo change follows dependency order. No parallel starts, no "I'll stub it":
30
+
31
+ | Step | Repo | Work | Gate before the next step starts |
32
+ |---|---|---|---|
33
+ | 1 | **Backend** | model/migration, use case, route, DTOs, tests | backend tests green; endpoint reachable (curl proof); DTOs frozen for this slice |
34
+ | 2 | **SDK** | types mirroring the DTOs, service method, error mapping, tests | SDK builds + type-checks; tests green; version bumped; new surface exported from the public entry point; **tagged** |
35
+ | 3 | **Clients** | upgrade pinned SDK tag, wire UI/state, integrate | client builds against the tag; feature verified eyes-on (DELIVERY.md validation etiquette) |
36
+
37
+ Rules around the order:
38
+ - **A gate failing blocks the next layer.** Never start SDK work against a backend whose
39
+ tests are red; never start client work against an SDK that doesn't build or type-check.
40
+ - **Bug fixes follow the same arrow.** Locate the most upstream repo that owns the defect,
41
+ fix it there with a regression test, then propagate downstream. Patching around a backend
42
+ bug inside a client creates two bugs.
43
+ - **Dependency upgrades propagate in the same order**: backend → SDK → clients, validating
44
+ each repo before touching the next.
45
+ - **Business rules live in the backend, once.** The SDK transports them, clients render
46
+ them. Client-side validation is UX sugar; the backend is the source of truth. Duplicated
47
+ domain logic across repos *will* diverge.
48
+
49
+ ## Never bypass the SDK
50
+
51
+ The SDK's public surface **is** the contract. Two absolute prohibitions for clients:
52
+
53
+ 1. **No direct API calls.** A `fetch`/HTTP call to the backend from client code is a
54
+ violation, even "just for this one endpoint." If the SDK lacks the method, the SDK is
55
+ behind — fix step 2 before step 3, never around it.
56
+ 2. **No deep imports.** Clients import the SDK's published entry point only — never
57
+ `sdk/src/*`, never `sdk/dist/internal/*`. Enforce mechanically: the SDK's package
58
+ manifest declares an `exports` map with a single public entry; anything not exported
59
+ does not exist. Deep-importing internals is the cross-repo version of L5 reaching into
60
+ L2's private parts.
61
+
62
+ Both violations are grep-visible (rule 2 of "Physical mapping" in
63
+ ARCHITECTURE_PRINCIPLES.md): `grep` clients for the raw API base URL and for deep-import
64
+ paths in CI.
65
+
66
+ > ⚠️ **Gotcha:** Symptom — a client's build explodes after an SDK *patch* release, errors
67
+ > deep inside `node_modules`. Cause — the client deep-imported an internal module the SDK
68
+ > moved during a refactor; internals carry no compatibility promise. Fix — consume only the
69
+ > public entry point; if you need something internal, promote it to the public surface via
70
+ > an SDK release.
71
+
72
+ **One SDK per language ecosystem.** A client platform with no SDK in its language (e.g. a
73
+ native mobile app next to a TypeScript SDK) builds a dedicated API-client module in its own
74
+ L2 — a mini-SDK. Same rules apply to it: single API surface in one place, typed DTOs,
75
+ normalized errors, no scattered HTTP calls from feature code.
76
+
77
+ ## Breaking-change protocol
78
+
79
+ Repos deploy independently — there is **no atomic cross-repo merge**. Design for the window
80
+ where old clients talk to the new backend:
81
+
82
+ 1. **Backend lands the change first.** Prefer expand/contract: add the new field/endpoint,
83
+ keep the old one serving until all clients have migrated, remove it later. Version the
84
+ route (`/v2/...`) when the shape change can't be additive. Backend tests green.
85
+ 2. **SDK adapts**: types and services updated, error mapping verified, **semver bump**
86
+ matching the impact (breaking → major, or minor below 1.0 — pick one convention and
87
+ write it in the SDK README), plus a dated **MIGRATION.md entry**: what broke,
88
+ before/after snippets, why. Tag the release.
89
+ 3. **Each client upgrades deliberately**: bump the pinned tag in its own PR, follow the
90
+ MIGRATION.md entry, run the client's full gate. Clients migrate at their own pace —
91
+ that's the point of the contract.
92
+
93
+ Never: merging a backend breaking change and "fixing the clients later today." Later today
94
+ is when production traffic from yesterday's mobile build hits the new shape.
95
+
96
+ > 📖 **War story:** Symptom — users logged out every time they returned from an external
97
+ > payment page; no client code had changed. Cause — a session-cookie policy change shipped
98
+ > from the SDK's auth layer without a migration note, so client teams couldn't connect the
99
+ > regression to the upgrade. Fix — the change was re-released with a MIGRATION.md entry
100
+ > (before/after config, the cross-domain redirect rationale), and "breaking change ⇒
101
+ > MIGRATION entry + version bump" became a hard gate on SDK releases.
102
+
103
+ ## Pin SDK versions by tag — never branch HEAD
104
+
105
+ Clients pin the SDK to an **immutable reference**: a published registry version or a git
106
+ tag (`#v0.12.0`). Never a branch, never an implicit HEAD.
107
+
108
+ - A branch reference makes every fresh install (CI, deploy server, new laptop) resolve to
109
+ *whatever the branch is that day*. Your client's behavior changes with zero diff in its
110
+ own repo — unreproducible builds, undebuggable regressions.
111
+ - Upgrading the SDK is always an **explicit, reviewable diff** in the client repo: one line,
112
+ one PR, one changelog entry to read.
113
+ - Lockfiles help but don't save you: they're bypassed by fresh resolution paths and they
114
+ hide *intent* — the manifest must state the exact version you mean.
115
+
116
+ > 📖 **War story:** Symptom — a production frontend deploy broke on a Friday with an empty
117
+ > diff; the same commit had deployed fine on Monday. Cause — the SDK dependency was a git
118
+ > URL pointing at a branch with no tag; a breaking SDK change merged mid-week, and the
119
+ > deploy's fresh `install` silently pulled the new HEAD. Fix — pin by tag, upgrade via
120
+ > explicit PRs; the team also added a CI check rejecting any git dependency without a
121
+ > pinned ref.
122
+
123
+ **Local development linking.** While building an SDK change you need the client to consume
124
+ your working copy. Use your package manager's link/workspace mechanism or a `file:` path,
125
+ behind two scripts in the client (`sdk:local` switches to the local path, `sdk:prod`
126
+ restores the pinned tag). The local link **never gets committed** — CI should fail on a
127
+ `file:` or `link:` SDK dependency reaching the main branch.
128
+
129
+ ## How this composes with vertical slices (DELIVERY.md)
130
+
131
+ A vertical slice doesn't stop at a repo boundary — **a slice crosses all repos, in
132
+ contract order**:
133
+
134
+ ```
135
+ Slice N: BACKEND (its own L3 → L2 → L5 loop, tests + curl proof)
136
+ → SDK (types + service + tests, build, bump, tag)
137
+ → CLIENT (upgrade pin, L3/L4 bricks, L5 screen, eyes-on proof)
138
+ ```
139
+
140
+ - The slice blueprint (DELIVERY.md) lists **files per repo per layer**, not just per layer.
141
+ - A cross-repo slice typically means one PR per repo, merged in dependency order, **each
142
+ leaving its repo shippable** — the backend with the new endpoint live is shippable even
143
+ before any client uses it (that's expand/contract working for you).
144
+ - The slice is **done** only on end-to-end proof: the feature visible in at least one real
145
+ client, against the deployed backend — not when the backend tests pass.
146
+ - InMemory caveat: inside a client, early slices may run on an InMemory L2 implementation
147
+ (DELIVERY.md build loop step 2). That's fine *within* the client repo — but the moment
148
+ the slice goes live, the real L2 is the SDK, under all the rules above.
149
+
150
+ ## Checklist (per cross-repo change)
151
+
152
+ - [ ] Backend tests green + endpoint proven before SDK work started
153
+ - [ ] SDK builds, type-checks, tests green; version bumped; release tagged
154
+ - [ ] Breaking change ⇒ MIGRATION.md entry with before/after
155
+ - [ ] Clients consume the public SDK surface only (no raw API calls, no deep imports)
156
+ - [ ] Client SDK pins are immutable tags; upgrade is an explicit PR
157
+ - [ ] No `file:`/`link:`/branch SDK reference on the main branch
158
+ - [ ] Slice proven end-to-end in a real client before being called done
@@ -0,0 +1,214 @@
1
+ # SDK Contract — shipping a typed SDK between your API and your clients
2
+
3
+ When several frontends (web, SSR, CLI, CI) consume one API, the API client becomes its own
4
+ package: a typed SDK. This file is the contract for building and distributing it. It assumes
5
+ the layer model from ARCHITECTURE_PRINCIPLES.md and the proof discipline from DELIVERY.md.
6
+
7
+ **Where the SDK sits**: inside the consumer app, the SDK is an **L2 Data brick** — it is the
8
+ network client behind the repository implementations. L3 Core Logic declares repository
9
+ contracts; L2 implements them by calling the SDK and mapping wire types → domain models.
10
+ No L3+ brick ever instantiates the SDK directly; only the composition root (L5) wires it.
11
+
12
+ ## 1. Package layout
13
+
14
+ ```
15
+ sdk/
16
+ src/
17
+ types/ # wire types, enums, type guards — imports NOTHING else in the package
18
+ errors/ # ApiError + normalization (fromResponse)
19
+ core/ # http transport, session manager, token service, secure-request use case
20
+ ports/ # storage port (SDKContext) + adapters: browser, SSR
21
+ clients/ # one client per resource (ProjectClient, UserClient…) — thin, typed methods
22
+ index.ts # composition root: wires transport → auth → clients; re-exports types/errors
23
+ tests/ # transport, token, concurrency tests (see §3)
24
+ MIGRATION.md # one section per breaking change (see §7)
25
+ CHANGELOG.md
26
+ ```
27
+
28
+ Internally the SDK obeys the same lattice in miniature: `types` ≈ L0, `errors` ≈ L0,
29
+ `core`/`clients` ≈ L2, `index.ts` ≈ L5. `types/` importing from `clients/` is a bug.
30
+
31
+ **Exports map — a types-only subpath is mandatory** so consumer L3 code can reference wire
32
+ types without dragging the HTTP client into the pure layer (type imports erase at compile
33
+ time; runtime client imports stay in L2/L5):
34
+
35
+ ```jsonc
36
+ {
37
+ "name": "@{{PROJECT_NAME}}/sdk",
38
+ "type": "module",
39
+ "files": ["dist"],
40
+ "exports": {
41
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" },
42
+ "./types": { "types": "./dist/types/index.d.ts", "import": "./dist/types/index.js" }
43
+ },
44
+ "dependencies": {}, // target: zero — fetch-based transport
45
+ "devDependencies": { "typescript": "…", "vite": "…", "vite-plugin-dts": "…" }
46
+ }
47
+ ```
48
+
49
+ > ⚠️ **Gotcha:** build tooling in `dependencies`. Symptom: every consumer `npm install`
50
+ > pulls the SDK's bundler plugin into their tree. Cause: `vite-plugin-dts` (or similar)
51
+ > added under `dependencies` instead of `devDependencies`. Fix: an SDK's `dependencies`
52
+ > must contain only what its *runtime* imports — audit it at every release; ideally empty.
53
+
54
+ ## 2. Error normalization — the contract crosses repos
55
+
56
+ Errors are part of the API contract, and the contract spans two repositories. Freeze the
57
+ wire shape, normalize once in the SDK, map to UX once in the consumer.
58
+
59
+ **SDK side** — every non-2xx response becomes one typed error, never a raw fetch error:
60
+
61
+ ```ts
62
+ class ApiError extends Error {
63
+ readonly code: number; readonly errorName: string;
64
+ readonly statusCode: number; readonly rawMessage?: string;
65
+ static fromResponse(statusCode: number, responseText: string): ApiError {
66
+ // 1. try JSON.parse → expect { code, name, description } (the wire contract)
67
+ // 2. shape mismatch or parse failure → standard error built from statusCode
68
+ }
69
+ }
70
+ ```
71
+
72
+ **Consumer side** — one shared handler maps `statusCode` → severity + i18n message, with
73
+ per-call overrides. UI code catches and delegates; it never inspects response bodies:
74
+
75
+ ```ts
76
+ const { handleError } = useApiErrorHandler()
77
+ try { await repo.createTeam(input) }
78
+ catch (e) { handleError(e, { 409: t('team.alreadyExists') }) } // 409 → warning, rest → error
79
+ ```
80
+
81
+ Rules: renaming a wire error field is a **major** version bump in both repos. The SDK never
82
+ swallows status codes; the consumer never re-parses raw responses.
83
+
84
+ ## 3. Auth tokens — single-flight refresh, injected storage
85
+
86
+ ### Single-flight refresh
87
+
88
+ With rotating, **single-use** refresh tokens, two concurrent 401s must produce exactly one
89
+ refresh call. Keep one inflight promise; latecomers await it; always bound the wait:
90
+
91
+ ```ts
92
+ private refreshing: Promise<void> | null = null;
93
+
94
+ // on 401 inside the secure-request use case:
95
+ if (!this.refreshing) {
96
+ this.refreshing = this.doRefresh().finally(() => { this.refreshing = null; });
97
+ }
98
+ await withTimeout(this.refreshing, REFRESH_TIMEOUT_MS); // reject, don't hang forever
99
+ return retryOriginalRequest(); // retry ONCE with the new token
100
+ ```
101
+
102
+ On refresh failure: clear tokens, fail all waiters — a half-authenticated session is worse
103
+ than a logout.
104
+
105
+ > 📖 **War story:** a production team shipped refresh-on-401 without synchronization. Any
106
+ > page firing parallel requests after token expiry sent N simultaneous refreshes; the
107
+ > rotating refresh token was single-use, so N−1 calls failed and randomly logged users out.
108
+ > Fix: the shared inflight promise above — plus the regression test below, because this bug
109
+ > reappears every time someone "simplifies" the use case.
110
+
111
+ **The concurrency regression test is non-negotiable** (DELIVERY.md: every rule ships with
112
+ its test):
113
+
114
+ ```ts
115
+ test('concurrent 401s trigger exactly ONE refresh', async () => {
116
+ // arrange: two requests that 401 first, succeed after refresh
117
+ await Promise.all([sdk.projects.list(), sdk.users.me()]);
118
+ expect(tokenService.refresh).toHaveBeenCalledTimes(1);
119
+ });
120
+ // also test: refresh timeout rejects waiters; refresh failure clears tokens
121
+ ```
122
+
123
+ ### Storage is an injected port
124
+
125
+ The SDK never touches `document.cookie` or framework internals directly. It receives a
126
+ storage port; adapters live beside it:
127
+
128
+ ```ts
129
+ interface SDKContext {
130
+ getCookie(name: string): string | null;
131
+ setCookie(name: string, value: string, opts?: CookieOptions): void;
132
+ removeCookie(name: string): void;
133
+ }
134
+ createBrowserContext(): SDKContext // document.cookie adapter
135
+ createSSRContext(get, set, remove): SDKContext // delegate to framework cookie utils
136
+ ```
137
+
138
+ This is the ports & adapters arrow from ARCHITECTURE_PRINCIPLES.md applied across a package
139
+ boundary — and it is what makes the SDK testable and SSR-safe.
140
+
141
+ ### Token-storage tradeoff — decide, write it down, never copy a default
142
+
143
+ | Option | Pros | Cons |
144
+ |---|---|---|
145
+ | httpOnly cookies + BFF proxy | JS can never read tokens (XSS-resistant) | needs a server tier; SDK stops managing tokens |
146
+ | JS-readable cookie (browser adapter) | pure SPA works; survives cross-site redirects | XSS can exfiltrate; demands short-lived access + rotating single-use refresh tokens |
147
+
148
+ The browser adapter above is the second option. If it also sets `SameSite=None` (cross-site
149
+ payment/auth redirects), CSRF protection moves to the API (origin validation, CSRF tokens).
150
+ Record the choice and its mitigations in `DECISIONS.md` — inheriting this template's default
151
+ silently is a finding in review.
152
+
153
+ ## 4. Distribution — tags, no dist/ in git, verify the types
154
+
155
+ - **Tagged releases only.** Semver tag per release; consumers pin the tag:
156
+ `"@{{PROJECT_NAME}}/sdk": "git+ssh://git@HOST/ORG/sdk.git#v1.4.2"` (or a registry pin).
157
+ A consumer pointing at a branch is a build that changes under your feet.
158
+ - **Never commit `dist/`.** Gitignore it; build in CI at tag time. Committed artifacts go
159
+ stale, hide build breakage, and poison code review.
160
+ - **Verify the published types after every build.** CI step: `npm pack`, install the tarball
161
+ in a fixture project, `tsc --noEmit` on a file importing every public type (or run
162
+ `@arethetypeswrong/cli`). "It built" is not proof the types shipped — see DELIVERY.md.
163
+
164
+ > ⚠️ **Gotcha:** declaration plugins skip `.d.ts` source files. Symptom: consumers hit
165
+ > "module has no exported member" for roughly half the public types. Cause: types were
166
+ > authored as `.d.ts` files; the dts generator treats those as already-compiled and emits
167
+ > nothing — 13 of 21 type modules silently missing from `dist/`, masked for months because
168
+ > a stale `dist/` was committed to git. Fix: author all types as plain `.ts`, never commit
169
+ > `dist/`, and gate releases on the type-verification step above.
170
+
171
+ ## 5. Logging — injected, silent by default
172
+
173
+ Zero `console.*` in `src/`. The SDK accepts a logger port with a debug flag; default is a
174
+ no-op:
175
+
176
+ ```ts
177
+ interface SDKLogger { debug(msg: string, meta?: object): void; error(msg: string, meta?: object): void; }
178
+ new SDK({ baseURL, sdkContext, logger: myLogger, debug: false })
179
+ ```
180
+
181
+ Enforce with lint: `no-console: error` on `src/`.
182
+
183
+ > ⚠️ **Gotcha:** a production team left 9 `console.*` calls in the request hot path — every
184
+ > single API call logged method, URL and which auth tokens were present, into every
185
+ > consumer's production console. Unfilterable by consumers, noisy in tests, and a free map
186
+ > of the auth topology for anyone opening devtools. Fix: the injected logger above; debug
187
+ > output exists, but the *consumer* owns the switch.
188
+
189
+ ## 6. Local-dev loop — link scripts in the consumer
190
+
191
+ Iterating on SDK + app simultaneously must be one command, not manual `package.json` edits:
192
+
193
+ ```jsonc
194
+ // consumer package.json scripts
195
+ "sdk:local": "npm pkg set dependencies.@{{PROJECT_NAME}}/sdk='file:../sdk' && npm install",
196
+ "sdk:prod": "npm pkg set dependencies.@{{PROJECT_NAME}}/sdk='git+ssh://git@HOST/ORG/sdk.git#vX.Y.Z' && npm install",
197
+ "sdk:status": "npm pkg get dependencies.@{{PROJECT_NAME}}/sdk"
198
+ ```
199
+
200
+ Guard rail: CI fails the consumer build if the SDK dependency is a `file:` path — local
201
+ links must never reach a commit on a shared branch.
202
+
203
+ ## 7. Docs discipline — MIGRATION.md, and samples must compile
204
+
205
+ - Every breaking change gets a `MIGRATION.md` section: what changed (before/after code),
206
+ why, impact, and the security implications if auth/cookies are involved.
207
+ - **Every code sample must compile against the real SDK surface.** Keep samples as actual
208
+ `.ts` files type-checked in CI, or extract fenced blocks and run `tsc` over them.
209
+
210
+ > 📖 **War story:** a migration guide illustrated CSRF mitigation with a sample passing
211
+ > `headers` inside a resource-creation payload — an option no client method ever accepted.
212
+ > The sample was invented, it type-checked nowhere, and teams who pasted it shipped a no-op
213
+ > "security measure" believing they were protected. Docs with invented samples are worse
214
+ > than no docs: they convert a knowledge gap into false confidence.