@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.
- package/LICENSE +32 -0
- package/README.md +99 -0
- package/bin/cli.js +371 -0
- package/bin/cli.test.js +91 -0
- package/package.json +43 -0
- package/templates/core/CLAUDE.md +36 -0
- package/templates/core/claude/memory/ARCHITECTURE.md +20 -0
- package/templates/core/claude/memory/COMMANDS.md +13 -0
- package/templates/core/claude/memory/DECISIONS.md +5 -0
- package/templates/core/claude/memory/NEXT_STEPS.md +11 -0
- package/templates/core/claude/memory/PROJECT_STATE.md +24 -0
- package/templates/core/claude/skills/kickoff/SKILL.md +84 -0
- package/templates/core/claude/skills/product-owner/SKILL.md +58 -0
- package/templates/core/claude/skills/restore-context/SKILL.md +29 -0
- package/templates/core/claude/skills/save-context/SKILL.md +35 -0
- package/templates/core/docs-architecture/ANTI_PATTERNS.md +180 -0
- package/templates/core/docs-architecture/ARCHITECTURE_PRINCIPLES.md +134 -0
- package/templates/core/docs-architecture/DELIVERY.md +68 -0
- package/templates/core/docs-architecture/DOCS_PLACEMENT.md +151 -0
- package/templates/core/docs-architecture/MULTI_REPO_CONTRACT.md +158 -0
- package/templates/core/docs-architecture/SDK_CONTRACT.md +214 -0
- package/templates/core/docs-architecture/SECURITY_USER_URLS.md +152 -0
- package/templates/core/gitignore +15 -0
- package/templates/core/mcp.json +8 -0
- package/templates/packs/nuxt-web/CLAUDE.md +74 -0
- package/templates/packs/nuxt-web/app/app.vue +5 -0
- package/templates/packs/nuxt-web/app/assets/css/main.css +18 -0
- package/templates/packs/nuxt-web/app/assets/css/tokens.css +41 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/components/DSButton.vue +70 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/index.ts +4 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/tests/DSButton.spec.ts +34 -0
- package/templates/packs/nuxt-web/app/designSystem/DSButton/types/dsButton.ts +5 -0
- package/templates/packs/nuxt-web/app/domain/.gitkeep +0 -0
- package/templates/packs/nuxt-web/app/features/.gitkeep +0 -0
- package/templates/packs/nuxt-web/app/pages/index.vue +36 -0
- package/templates/packs/nuxt-web/app/utils/.gitkeep +0 -0
- package/templates/packs/nuxt-web/claude/memory/COMMANDS.md +21 -0
- package/templates/packs/nuxt-web/docs-architecture/ARCHITECTURE.md +169 -0
- package/templates/packs/nuxt-web/docs-architecture/CONVENTIONS.md +140 -0
- package/templates/packs/nuxt-web/docs-architecture/I18N.md +102 -0
- package/templates/packs/nuxt-web/docs-architecture/OPS_WEB.md +176 -0
- package/templates/packs/nuxt-web/docs-architecture/SEO_AND_ROUTING.md +118 -0
- package/templates/packs/nuxt-web/gitignore +18 -0
- package/templates/packs/nuxt-web/nuxt.config.ts +49 -0
- package/templates/packs/nuxt-web/pack.json +11 -0
- package/templates/packs/nuxt-web/package.json +31 -0
- package/templates/packs/nuxt-web/playwright.config.ts +39 -0
- package/templates/packs/nuxt-web/server/api/health.get.ts +7 -0
- package/templates/packs/nuxt-web/tests/e2e/home.spec.ts +19 -0
- package/templates/packs/nuxt-web/tsconfig.json +4 -0
- package/templates/packs/nuxt-web/vitest.config.ts +23 -0
- package/templates/packs/swift-ios/CLAUDE.md +64 -0
- package/templates/packs/swift-ios/Packages/DataLayer/Package.swift +21 -0
- package/templates/packs/swift-ios/Packages/DataLayer/Sources/DataLayer/DataLayer.swift +11 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Package.swift +20 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Domain/SampleItem.swift +15 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Engine/SampleEngine.swift +14 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Sources/{{PROJECT_NAME}}Core/Repository/SampleItemRepository.swift +27 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}Core/Tests/{{PROJECT_NAME}}CoreTests/SampleEngineTests.swift +32 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Package.swift +17 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Color+DS.swift +18 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/Components/DSCard.swift +22 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DS.swift +36 -0
- package/templates/packs/swift-ios/Packages/{{PROJECT_NAME}}DS/Sources/{{PROJECT_NAME}}DS/DSFont.swift +26 -0
- package/templates/packs/swift-ios/claude/memory/COMMANDS.md +18 -0
- package/templates/packs/swift-ios/docs-architecture/ARCHITECTURE.md +246 -0
- package/templates/packs/swift-ios/docs-architecture/CLOUDKIT_GUIDE.md +224 -0
- package/templates/packs/swift-ios/docs-architecture/CONVENTIONS.md +246 -0
- package/templates/packs/swift-ios/docs-architecture/DESIGN_SYSTEM.md +272 -0
- package/templates/packs/swift-ios/docs-architecture/NAVIGATION.md +241 -0
- package/templates/packs/swift-ios/docs-architecture/TESTING.md +176 -0
- package/templates/packs/swift-ios/docs-architecture/WORKFLOW.md +165 -0
- package/templates/packs/swift-ios/github/workflows/ci.yml +48 -0
- package/templates/packs/swift-ios/gitignore +5 -0
- package/templates/packs/swift-ios/mcp.json +8 -0
- package/templates/packs/swift-ios/pack.json +11 -0
- package/templates/packs/swift-ios/project.yml +33 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/App.swift +32 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/App/AppNamespace.swift +4 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Module/.gitkeep +0 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Store/.gitkeep +0 -0
- package/templates/packs/swift-ios/{{PROJECT_NAME}}/Tools/.gitkeep +0 -0
- package/templates/packs/ts-sdk/CHANGELOG.md +9 -0
- package/templates/packs/ts-sdk/CLAUDE.md +72 -0
- package/templates/packs/ts-sdk/MIGRATION.md +28 -0
- package/templates/packs/ts-sdk/claude/memory/COMMANDS.md +21 -0
- package/templates/packs/ts-sdk/docs-architecture/ARCHITECTURE.md +132 -0
- package/templates/packs/ts-sdk/docs-architecture/CONVENTIONS_TS.md +152 -0
- package/templates/packs/ts-sdk/gitignore +6 -0
- package/templates/packs/ts-sdk/pack.json +11 -0
- package/templates/packs/ts-sdk/package.json +55 -0
- package/templates/packs/ts-sdk/scripts/verify-dist.mjs +67 -0
- package/templates/packs/ts-sdk/src/clients/AuthClient.ts +168 -0
- package/templates/packs/ts-sdk/src/core/HttpClient.ts +85 -0
- package/templates/packs/ts-sdk/src/core/Logger.ts +27 -0
- package/templates/packs/ts-sdk/src/core/SDKContext.ts +40 -0
- package/templates/packs/ts-sdk/src/core/withTimeout.ts +19 -0
- package/templates/packs/ts-sdk/src/errors/ApiError.ts +93 -0
- package/templates/packs/ts-sdk/src/index.ts +62 -0
- package/templates/packs/ts-sdk/src/types/index.ts +33 -0
- package/templates/packs/ts-sdk/tests/apiError.test.ts +58 -0
- package/templates/packs/ts-sdk/tests/httpClient.test.ts +60 -0
- package/templates/packs/ts-sdk/tests/singleFlight.test.ts +191 -0
- package/templates/packs/ts-sdk/tsconfig.json +15 -0
- package/templates/packs/ts-sdk/tsup.config.ts +22 -0
- package/templates/packs/ts-sdk/vitest.config.ts +8 -0
- package/templates/packs/vapor-api/CLAUDE.md +73 -0
- package/templates/packs/vapor-api/Dockerfile +80 -0
- package/templates/packs/vapor-api/Package.swift +68 -0
- package/templates/packs/vapor-api/Sources/App/App.swift +5 -0
- package/templates/packs/vapor-api/Sources/App/Configure/AppConfig.swift +108 -0
- package/templates/packs/vapor-api/Sources/App/Configure/configure.swift +74 -0
- package/templates/packs/vapor-api/Sources/App/Configure/entrypoint.swift +47 -0
- package/templates/packs/vapor-api/Sources/App/Configure/routes.swift +21 -0
- package/templates/packs/vapor-api/Sources/App/Error/Failed.swift +73 -0
- package/templates/packs/vapor-api/Sources/App/Error/FailedMiddleware.swift +56 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/AppItem.swift +38 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Controllers/ItemControllersCrud.swift +41 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/DTO/ItemDTO.swift +22 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Entities/ItemEntity.swift +30 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Migrations/ItemMigrationCreate.swift +25 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Repositories/ItemRepository.swift +32 -0
- package/templates/packs/vapor-api/Sources/App/Features/Item/Services/ItemService.swift +57 -0
- package/templates/packs/vapor-api/Sources/App/Registry/ControllersRegister.swift +17 -0
- package/templates/packs/vapor-api/Sources/App/Registry/MiddlewaresRegister.swift +15 -0
- package/templates/packs/vapor-api/Sources/App/Registry/MigrationsRegister.swift +18 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Logging/JSONLogHandler.swift +59 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Middleware/HTTPLoggingMiddleware.swift +50 -0
- package/templates/packs/vapor-api/Sources/Monitoring/Monitoring.swift +110 -0
- package/templates/packs/vapor-api/Sources/{{PROJECT_NAME}}Foundation/String+Trimmed.swift +15 -0
- package/templates/packs/vapor-api/Tests/AppTests/AppTests.swift +155 -0
- package/templates/packs/vapor-api/claude/memory/COMMANDS.md +30 -0
- package/templates/packs/vapor-api/docs-architecture/ARCHITECTURE.md +144 -0
- package/templates/packs/vapor-api/docs-architecture/CONVENTIONS.md +121 -0
- package/templates/packs/vapor-api/docs-architecture/GOTCHAS_LINUX_SWIFT.md +109 -0
- package/templates/packs/vapor-api/docs-architecture/OPS.md +102 -0
- package/templates/packs/vapor-api/env_dist +29 -0
- package/templates/packs/vapor-api/gitignore +7 -0
- package/templates/packs/vapor-api/pack.json +11 -0
- package/templates/packs/vapor-api/scripts/generate-error-codes.sh +73 -0
- 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.
|