@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,11 @@
1
+ {
2
+ "id": "swift-ios",
3
+ "label": "iOS app — Swift 6.2 / SwiftUI / CloudKit",
4
+ "languages": ["swift"],
5
+ "idPrompt": "Bundle identifier",
6
+ "requirements": [
7
+ "Xcode 26+",
8
+ "XcodeGen (brew install xcodegen) — run `xcodegen generate` to create the .xcodeproj"
9
+ ],
10
+ "notes": "Packages build day one: swift test --package-path Packages/{{PROJECT_NAME}}Core"
11
+ }
@@ -0,0 +1,33 @@
1
+ # XcodeGen manifest — run `xcodegen generate` to (re)create {{PROJECT_NAME}}.xcodeproj.
2
+ # The .xcodeproj is gitignored: this file is the source of truth (merge-conflict-free).
3
+ name: {{PROJECT_NAME}}
4
+ options:
5
+ bundleIdPrefix: {{BUNDLE_ID}}
6
+ deploymentTarget:
7
+ iOS: "26.0"
8
+ packages:
9
+ {{PROJECT_NAME}}DS:
10
+ path: Packages/{{PROJECT_NAME}}DS
11
+ {{PROJECT_NAME}}Core:
12
+ path: Packages/{{PROJECT_NAME}}Core
13
+ DataLayer:
14
+ path: Packages/DataLayer
15
+ targets:
16
+ {{PROJECT_NAME}}:
17
+ type: application
18
+ platform: iOS
19
+ sources: [{{PROJECT_NAME}}]
20
+ dependencies:
21
+ - package: {{PROJECT_NAME}}DS
22
+ - package: {{PROJECT_NAME}}Core
23
+ - package: DataLayer
24
+ settings:
25
+ base:
26
+ PRODUCT_BUNDLE_IDENTIFIER: {{BUNDLE_ID}}
27
+ SWIFT_VERSION: "6.2"
28
+ SWIFT_STRICT_CONCURRENCY: complete
29
+ SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor
30
+ GENERATE_INFOPLIST_FILE: true
31
+ INFOPLIST_KEY_UILaunchScreen_Generation: true
32
+ CURRENT_PROJECT_VERSION: 1
33
+ MARKETING_VERSION: 0.1.0
@@ -0,0 +1,32 @@
1
+ import SwiftUI
2
+ import {{PROJECT_NAME}}DS
3
+
4
+ @main
5
+ struct {{PROJECT_NAME}}App: SwiftUI.App { // qualified: `App` (unqualified) is our namespace enum
6
+ var body: some Scene {
7
+ WindowGroup {
8
+ RootView()
9
+ // Dark-first: lock the scheme so system chrome (sheets, alerts, keyboard) matches
10
+ // the dark token palette — see docs-architecture/DESIGN_SYSTEM.md.
11
+ .preferredColorScheme(.dark)
12
+ }
13
+ }
14
+ }
15
+
16
+ /// Root gate — grows at kickoff into: !isReady → Splash, needsOnboarding → Welcome, else → tabs.
17
+ /// (See docs-architecture/NAVIGATION.md.)
18
+ struct RootView: View {
19
+ var body: some View {
20
+ ZStack {
21
+ Color.DS.background.ignoresSafeArea()
22
+ VStack(spacing: DS.Padding.m) {
23
+ Text("{{PROJECT_NAME}}")
24
+ .designSystem(font: .largeTitle)
25
+ .foregroundStyle(DS.Gradients.brand)
26
+ Text("Hello 👋")
27
+ .designSystem(font: .callout)
28
+ .foregroundStyle(Color.DS.textSecondary)
29
+ }
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,4 @@
1
+ /// Namespace for app screens: `extension App { struct Feed: View }` → call sites read `App.Feed`.
2
+ /// IMPORTANT: this enum collides with the `SwiftUI.App` protocol by design — the @main entry point
3
+ /// must declare `struct {{PROJECT_NAME}}App: SwiftUI.App` (fully qualified). See CONVENTIONS.md §4.
4
+ enum App {}
@@ -0,0 +1,9 @@
1
+ # Changelog — {{BUNDLE_ID}}
2
+
3
+ All notable changes, newest first. Versions are git tags (`vX.Y.Z`); clients pin them.
4
+
5
+ ## [0.1.0]
6
+
7
+ - Initial scaffold: fetch transport with normalized `ApiError`, injected storage
8
+ (`SDKContext`) and logger (`SDKLogger`) ports, `AuthClient` with single-flight token
9
+ refresh, types-only `./types` subpath, dist verification gate.
@@ -0,0 +1,72 @@
1
+ # {{PROJECT_NAME}} — Claude Code Operating Manual
2
+
3
+ This project was scaffolded by **AppForge** (pack: TypeScript SDK): a Claude-Code-first
4
+ architecture extracted from production SDKs. You (Claude) are the team lead AND the
5
+ primary developer. Follow this manual exactly — it encodes hard-won lessons, not preferences.
6
+
7
+ ## Identity
8
+ - Package: **`{{BUNDLE_ID}}`** ({{PROJECT_NAME}}) · TypeScript, strict · Node 20+ / evergreen browsers
9
+ - This is a **typed SDK**: the single client library between an API and all of its consumers
10
+ (web, SSR, CLI, CI…).
11
+
12
+ ## Multi-repo position — this repo is the CONTRACT layer
13
+ The product spans repos: `backend → SDK (this repo) → clients`. Everything here follows
14
+ `docs-architecture/MULTI_REPO_CONTRACT.md`:
15
+ - **Backend changes land here second** — never start SDK work against a backend whose tests
16
+ are red; never let clients start before this SDK builds, type-checks, tests green and is
17
+ **tagged**.
18
+ - **Clients consume tagged releases only** (`#vX.Y.Z` or a registry pin) — never a branch.
19
+ - **Breaking change ⇒ MIGRATION.md entry + version bump + tag**, in the same change.
20
+ - The public surface is the `exports` map. Anything not exported does not exist.
21
+
22
+ ## Session protocol (MANDATORY)
23
+ 1. **Session start**: run the `restore-context` skill — read `.claude/memory/*.md` before doing anything. Never invent project facts.
24
+ 2. **Empty project / new idea**: run the `kickoff` skill — it interviews the user, writes the PRD, plans slices, then builds autonomously.
25
+ 3. **After significant work**: update `.claude/memory/PROJECT_STATE.md` (and DECISIONS/NEXT_STEPS when relevant) — `save-context` skill.
26
+
27
+ ## Architecture (read the docs before coding)
28
+ The knowledge base lives in `docs-architecture/`. Read the relevant doc BEFORE touching that area:
29
+
30
+ | You are about to… | Read first |
31
+ |---|---|
32
+ | understand the layer model (stack-agnostic) | `ARCHITECTURE_PRINCIPLES.md` |
33
+ | plan/deliver slices, validate, update memory | `DELIVERY.md` |
34
+ | coordinate with the backend or a client repo | `MULTI_REPO_CONTRACT.md` |
35
+ | anything SDK-shaped (this whole repo) | `SDK_CONTRACT.md` — this pack instantiates it |
36
+ | add/move any file, add a resource client | `ARCHITECTURE.md` |
37
+ | write any TypeScript code | `CONVENTIONS_TS.md` |
38
+ | accept user-supplied URLs (webhooks…) | `SECURITY_USER_URLS.md` |
39
+ | write any documentation | `DOCS_PLACEMENT.md` |
40
+ | add caching/feature flags/auth shortcuts | `ANTI_PATTERNS.md` |
41
+
42
+ Layer summary (universal contract in ARCHITECTURE_PRINCIPLES.md, TS mapping in ARCHITECTURE.md):
43
+ L0 `src/types/` + `src/errors/` (wire types, ApiError — import nothing above) · L1 logger port ·
44
+ L2 `src/core/` (fetch transport, storage adapters) · L3 `src/clients/` (resource clients +
45
+ single-flight auth use case) · L5 `src/index.ts` (composition root, sole public surface).
46
+ From the consumer's seat, this whole package is **their L2 Data brick**.
47
+
48
+ ## Non-negotiable rules
49
+ - **Never claim done without proof**: `npm run typecheck` + `npx vitest run` + `npm run build`
50
+ (which runs the dist/types verification) all green, outputs shown.
51
+ - **The single-flight tests are the spec.** `tests/singleFlight.test.ts` must never be
52
+ weakened or skipped — the concurrency bug it pins reappears every time someone
53
+ "simplifies" the auth use case.
54
+ - **Zero `console.*` in `src/`** — logging goes through the injected `SDKLogger` port,
55
+ debug-gated, and never logs token values.
56
+ - **`.ts` sources only** — never author `.d.ts` files in `src/` (declaration generators
57
+ silently skip them; see SDK_CONTRACT.md §4). Never commit `dist/`.
58
+ - **`dependencies` stays `{}`** unless a DECISIONS.md entry justifies a runtime dep.
59
+ Build tooling lives in `devDependencies`, always.
60
+ - **Tagged releases only**; every breaking change ships its MIGRATION.md entry.
61
+
62
+ ## Build commands
63
+ ```bash
64
+ npm install
65
+ npm run typecheck # tsc --noEmit — fastest signal
66
+ npx vitest run # full suite, includes the single-flight regression test
67
+ npm run build # tsup (esm+cjs+d.ts) THEN scripts/verify-dist.mjs gate
68
+ ```
69
+
70
+ ## Git
71
+ - Never push without explicit user approval. Feature branches; commit format `add/update/fix(scope) - description`.
72
+ - No AI attribution in commits or file headers.
@@ -0,0 +1,28 @@
1
+ # Migration Guide — {{BUNDLE_ID}}
2
+
3
+ One dated section per **breaking change**, newest first, written in the same change that
4
+ breaks (CONVENTIONS_TS.md §8, MULTI_REPO_CONTRACT.md breaking-change protocol). Every code
5
+ sample must compile against the real SDK surface — invented samples are worse than no docs.
6
+
7
+ Template:
8
+
9
+ ```markdown
10
+ ## vX.Y.0 — <one-line summary> (YYYY-MM-DD)
11
+
12
+ ### What changed
13
+ Before / after code blocks, copied from real consumer usage.
14
+
15
+ ### Why
16
+ The constraint that forced the break.
17
+
18
+ ### Impact
19
+ What consumers must do, per platform if it differs.
20
+
21
+ ### Security implications
22
+ Mandatory when auth, tokens or cookies are involved — state the new tradeoff and the
23
+ required mitigations.
24
+ ```
25
+
26
+ ---
27
+
28
+ *No entries yet — v0.1.0 is the initial scaffold.*
@@ -0,0 +1,21 @@
1
+ # {{PROJECT_NAME}} — Commands
2
+
3
+ > Only commands proven to work in THIS project, with exact flags.
4
+
5
+ ## Fast loop (always in this order)
6
+ npm run typecheck # tsc --noEmit — fastest signal
7
+ npx vitest run # full suite (CI mode)
8
+ npx vitest run tests/singleFlight.test.ts # the auth concurrency regression suite alone
9
+
10
+ ## Build + dist verification
11
+ npm run build # tsup (esm+cjs+d.ts, "." and "./types" entries) THEN scripts/verify-dist.mjs
12
+ ls dist dist/types # eyeball the artifacts when in doubt
13
+
14
+ ## Release gate (before tagging — see CONVENTIONS_TS.md §8)
15
+ npm pkg get dependencies # must be the expected object (ideally {})
16
+ npm pack --dry-run # tarball must contain dist + manifest only
17
+ git tag v0.X.Y # clients pin THIS, never a branch
18
+
19
+ ## Consumer-side linking (lives in the CLIENT repo — SDK_CONTRACT.md §6)
20
+ # sdk:local / sdk:prod scripts switch between file:../ and the pinned tag;
21
+ # a file: link must never reach a shared branch.
@@ -0,0 +1,132 @@
1
+ # ARCHITECTURE — Typed TypeScript SDK (the contract package)
2
+
3
+ This package is the **contract layer** of a multi-repo product: `backend → SDK → clients`
4
+ (MULTI_REPO_CONTRACT.md). From any consumer's seat, the whole package is **their L2 Data
5
+ brick** — the network client behind their repository implementations. Internally it runs
6
+ the universal L0–L5 lattice (ARCHITECTURE_PRINCIPLES.md) in miniature, exactly as
7
+ SDK_CONTRACT.md prescribes. Read both before touching this repo; this file maps them onto
8
+ TypeScript files.
9
+
10
+ ## 1. Layer model — TS instantiation
11
+
12
+ ```
13
+ L5 src/index.ts composition root: constructs adapters, wires transport → auth →
14
+ resource clients, re-exports the ENTIRE public surface (and only it)
15
+ L3 src/clients/ one typed client per API resource + the secure-request use case
16
+ (single-flight token refresh lives in AuthClient)
17
+ L2 src/core/ adapters & ports: HttpClient (fetch transport), SDKContext
18
+ (storage port + adapters), withTimeout primitive
19
+ L1 src/core/Logger.ts ops port: SDKLogger, no-op by default, debug-gated at the root
20
+ L0 src/types/ wire types mirroring backend DTOs — import NOTHING in the package
21
+ src/errors/ ApiError + fromResponse normalization — imports types only
22
+ ```
23
+
24
+ Tests live in `tests/`, stub the ports, and exercise the **public surface**
25
+ (`import … from '../src/index'`) so they double as compile-time proof of the exports.
26
+
27
+ ## 2. Dependency direction
28
+
29
+ | Directory | may import | must NEVER import |
30
+ |---|---|---|
31
+ | `types/` | nothing | anything — a `types/` file with an import is a bug |
32
+ | `errors/` | `types/` | `core/`, `clients/`, `index.ts` |
33
+ | `core/` | `types/`, `errors/` | `clients/`, `index.ts` |
34
+ | `clients/` | core **ports** (`HttpTransport`, `SDKContext`, `SDKLogger`), `types/`, `errors/` | `fetch`/`document` directly, sibling clients, `index.ts` |
35
+ | `index.ts` | everything | — (sole assembly point) |
36
+
37
+ The import graph is the architecture — every violation is grep-visible:
38
+
39
+ ```bash
40
+ grep -rn "fetch(" src/clients/ src/errors/ src/types/ # must be empty (transport port only)
41
+ grep -rn "console\." src/ # must be empty (injected logger only)
42
+ grep -rn "from '.*clients" src/types/ src/errors/ src/core/ # must be empty (no upward imports)
43
+ grep -rln "document\.\|window\." src/ --include='*.ts' # browser APIs only inside a storage adapter
44
+ ```
45
+
46
+ > ⚠️ **Gotcha:** Symptom — a "tiny helper" in `types/` starts importing `ApiError`, then a
47
+ > client, and suddenly the types-only subpath drags the HTTP runtime into the consumer's
48
+ > pure L3 layer. Cause — runtime code creeping into `types/`. Fix — `types/` holds
49
+ > declarations only (interfaces, type aliases, string-literal unions); anything with a
50
+ > runtime body lives in `core/` or above.
51
+
52
+ ## 3. The SDK from the consumer's seat
53
+
54
+ - Consumers import **`{{BUNDLE_ID}}`** (runtime: clients, errors, adapters) or
55
+ **`{{BUNDLE_ID}}/types`** (wire types only, erased at compile time). Nothing else exists —
56
+ the `exports` map is the law (MULTI_REPO_CONTRACT.md "Never bypass the SDK").
57
+ - Consumer L3 declares repository contracts and may reference `{{BUNDLE_ID}}/types`;
58
+ consumer L2 implements those contracts by calling this SDK and mapping wire → domain.
59
+ No consumer L3+ brick instantiates the SDK; their composition root (L5) wires it.
60
+ - Clients pin a **tag**, never a branch (MULTI_REPO_CONTRACT.md). Cutting a release here is
61
+ what unblocks client work — see the release checklist in CONVENTIONS_TS.md.
62
+
63
+ ## 4. Adding a resource client — the recipe
64
+
65
+ Backend first (its tests green, DTOs frozen), then here, then clients — always in that order.
66
+
67
+ 1. **`src/types/`** — add wire types mirroring the new DTOs (plain `.ts`, no imports).
68
+ 2. **`src/clients/FooClient.ts`** — thin, typed methods; secure endpoints take the
69
+ `AuthClient`, public endpoints take the `HttpTransport` port directly:
70
+
71
+ ```ts
72
+ export class ProjectClient {
73
+ constructor(private readonly auth: AuthClient) {}
74
+ list(): Promise<Project[]> { return this.auth.request('GET', '/projects'); }
75
+ create(draft: ProjectDraft): Promise<Project> { return this.auth.request('POST', '/projects', draft); }
76
+ }
77
+ ```
78
+
79
+ No URL building beyond the path, no response re-parsing, no error catching — the
80
+ transport normalizes errors (`ApiError`), the consumer maps them to UX (SDK_CONTRACT.md §2).
81
+ 3. **`src/index.ts`** — construct it in the composition root, expose it as a readonly
82
+ field, re-export the class and its wire types.
83
+ 4. **`scripts/verify-dist.mjs`** — add the new public names to the surface list so the
84
+ build gate proves the types actually shipped.
85
+ 5. **`tests/`** — stub `HttpTransport`, assert method/path/payload and error mapping.
86
+ 6. Build green → version bump → tag (CONVENTIONS_TS.md §8).
87
+
88
+ ## 5. Auth — why `clients/AuthClient.ts` looks the way it does
89
+
90
+ The single-flight refresh contract (shared inflight promise, bounded wait, retry exactly
91
+ once, clear-tokens-on-failure) is specified in **SDK_CONTRACT.md §3** — read it before
92
+ touching `AuthClient`. Local invariants on top:
93
+
94
+ - `tests/singleFlight.test.ts` is the executable spec: N concurrent 401s ⇒ **exactly one**
95
+ refresh call against a fake API that enforces *single-use* rotating refresh tokens. It
96
+ also pins the timeout and failure paths. Never weaken or skip these tests.
97
+ - The refresh call itself goes through the raw transport, never through `request()` —
98
+ a refresh that 401s must not trigger another refresh.
99
+ - Token storage goes through the `SDKContext` port. The skeleton ships the **memory
100
+ adapter** only; adding a browser/SSR adapter is a recorded decision (token-storage
101
+ tradeoff table in SDK_CONTRACT.md §3 → `DECISIONS.md`), never a copied default.
102
+
103
+ ## 6. Build pipeline — dual output, verified types
104
+
105
+ ```
106
+ npm run build = tsup (entries: src/index.ts + src/types/index.ts → esm + cjs + .d.ts)
107
+ THEN node scripts/verify-dist.mjs
108
+ ```
109
+
110
+ `verify-dist.mjs` fails the build unless every expected `dist/` artifact exists **and**
111
+ every name on the public-surface list appears in the emitted declarations. "It built" is
112
+ not proof the types shipped (SDK_CONTRACT.md §4) — this gate makes the proof mechanical.
113
+
114
+ > ⚠️ **Gotcha:** Symptom — consumers report "module has no exported member" for types that
115
+ > visibly exist in `src/`. Cause — the types were authored as `.d.ts` files; declaration
116
+ > generators treat those as already-compiled and emit nothing for them. Fix — author all
117
+ > types as plain `.ts`; the verify-dist gate catches any regression at build time.
118
+
119
+ > ⚠️ **Gotcha:** Symptom — the SDK behaves differently in the consumer than in this repo's
120
+ > tests, and `git diff` is clean. Cause — a stale committed `dist/` shadowing fresh sources.
121
+ > Fix — `dist/` is gitignored here and must stay that way; artifacts are built at tag time,
122
+ > never reviewed, never committed.
123
+
124
+ ## 7. Why this works with AI agents
125
+
126
+ - **Seconds-fast ground truth**: `npm run typecheck` and `npx vitest run` give precise
127
+ feedback without any consumer app — iterate here before touching client repos.
128
+ - **Ports make tests cheap**: every client is testable by stubbing `HttpTransport`; no
129
+ network, no mock servers, no global-fetch patching.
130
+ - **Grep-visible boundaries** (§2): an agent can verify the architecture with four greps.
131
+ - **Mechanical release gates**: verify-dist + the single-flight suite encode the two
132
+ historical production failures as automated checks, so no session re-learns them.
@@ -0,0 +1,152 @@
1
+ # CONVENTIONS — TypeScript for a typed SDK
2
+
3
+ House rules for every line of TypeScript in this repo. They instantiate SDK_CONTRACT.md
4
+ for this package — when in doubt, that file wins. Each rule below was paid for in
5
+ production; none is style preference.
6
+
7
+ ## 1. Compiler discipline
8
+
9
+ - `strict: true`, no `any` (use `unknown` + narrowing). A cast (`as`) needs a comment
10
+ explaining why narrowing is impossible.
11
+ - `verbatimModuleSyntax: true` — type-only imports are written `import type { … }`. This
12
+ keeps the types-only subpath genuinely runtime-free.
13
+ - `npm run typecheck` (`tsc --noEmit`) is the fastest signal — run it before the test
14
+ suite, run both before any build.
15
+
16
+ ### `.ts` sources only — never author `.d.ts`
17
+
18
+ All of `src/` is plain `.ts`, **including every file in `src/types/`**. Declarations are
19
+ *generated* into `dist/` by the build, never written by hand.
20
+
21
+ > ⚠️ **Gotcha:** Symptom — consumers get "module has no exported member" for roughly half
22
+ > the public types while the source visibly contains them. Cause — types authored as
23
+ > `.d.ts` files: declaration generators treat them as already-compiled output and silently
24
+ > emit nothing, and a committed `dist/` masked the hole for months (a production team
25
+ > shipped 13 of 21 type modules missing this way). Fix — `.ts` sources only, `dist/`
26
+ > gitignored, and the `scripts/verify-dist.mjs` gate (run by `npm run build`) failing the
27
+ > build when a public name is absent from the emitted declarations.
28
+
29
+ ## 2. Exports map — the public surface, and the types-only subpath
30
+
31
+ `package.json#exports` defines what exists. This SDK exposes exactly two entries:
32
+
33
+ - `"."` — the runtime surface (composition root, clients, errors, adapters).
34
+ - `"./types"` — **types only**, so a consumer's pure L3 layer can reference wire types
35
+ without dragging the HTTP client into it (type imports erase at compile time).
36
+
37
+ Rules:
38
+ - Adding/removing/renaming a subpath is an architectural decision → `DECISIONS.md` entry
39
+ + semver impact assessed. Deep paths (`/src/*`, `/dist/internal/*`) are never exposed.
40
+ - Everything public is re-exported from `src/index.ts` (or `src/types/index.ts`); a type
41
+ reachable only through a deep import is a bug.
42
+ - `"sideEffects": false` stays true: no module-level statements with effects — module load
43
+ must be free (no env reads, no global patching, no top-level `await`).
44
+
45
+ ## 3. Dependencies policy — runtime deps target zero
46
+
47
+ - `"dependencies": {}` — the transport is built-in `fetch`; an SDK drags its runtime deps
48
+ into every consumer's tree. Adding one requires a `DECISIONS.md` entry justifying it.
49
+ - Build/test tooling (`tsup`, `typescript`, `vitest`) lives in `devDependencies`, always.
50
+ - Release audit (it's in the checklist, §8): `npm pkg get dependencies` must print the
51
+ expected object — ideally `{}`.
52
+
53
+ > ⚠️ **Gotcha:** Symptom — every consumer install pulls a bundler plugin (and its whole
54
+ > dependency tree) they never use. Cause — a declaration-generator plugin added under
55
+ > `dependencies` instead of `devDependencies`; nothing fails, so it ships for months.
56
+ > Fix — `dependencies` may contain only what `src/` actually imports at runtime; audit at
57
+ > every release (see SDK_CONTRACT.md §1).
58
+
59
+ ## 4. Logging — injected port, silent by default
60
+
61
+ - **Zero `console.*` in `src/`** — grep-enforced (`grep -rn "console\." src/` must be
62
+ empty); add `no-console: error` if you introduce a linter. Build scripts (`scripts/`)
63
+ may print — they run on developer machines, not inside consumers.
64
+ - All logging goes through the `SDKLogger` port; the default is a no-op, and `debug()`
65
+ only fires when the consumer passes `debug: true`. The *consumer* owns the switch.
66
+ - **Never log token values, `Authorization` headers, or token-presence booleans.** A
67
+ production team once logged method + URL + which auth tokens were present on every
68
+ request: unfilterable console noise for every consumer and a free map of the auth
69
+ topology in devtools (SDK_CONTRACT.md §5). Log method + path, nothing from headers.
70
+
71
+ ## 5. Errors — normalize once, never re-parse
72
+
73
+ - Every non-2xx response becomes an `ApiError` via `ApiError.fromResponse(status, text)` —
74
+ thrown by the transport, the single place that reads response bodies on failure.
75
+ - The wire error shape `{ code, name, description }` is frozen contract: renaming a field
76
+ is a **breaking change in two repos** (SDK_CONTRACT.md §2) — major bump + MIGRATION entry.
77
+ - Clients (`src/clients/`) never catch-and-rewrap, never inspect bodies; consumers map
78
+ `statusCode` → UX in one shared handler, never re-parse raw responses.
79
+ - Genuine network failures (DNS, refused connection) propagate as-is — inventing a fake
80
+ status code for them would lie to the consumer's error handler.
81
+
82
+ ## 6. Auth invariants — single-flight or nothing
83
+
84
+ The contract is SDK_CONTRACT.md §3; the local law:
85
+
86
+ - One shared inflight refresh promise; latecomers await it; the wait is **bounded** by a
87
+ timeout that rejects (never hang waiters forever).
88
+ - On 401: join/start the refresh, then retry the original request **exactly once**. Two
89
+ retries hide real auth breakage; zero retries logs users out on every token expiry.
90
+ - On any refresh failure: **fail every waiter** — a half-authenticated session is worse than
91
+ a clean error. But **clear tokens ONLY on a real auth rejection** (a 401, or a 400 carrying
92
+ `invalid_grant`): that is the server's verdict that the refresh token is dead. A transport
93
+ failure — offline, DNS, connection refused, timeout (`RefreshTimeout`/408), or 5xx — never
94
+ reached a verdict, so the tokens are **kept**; clearing them would force a gratuitous logout
95
+ on a session that may still be valid (this matches §5: network failures propagate as-is).
96
+ - The refresh call uses the raw transport, never the secure `request()` path (a 401 from
97
+ the refresh endpoint must not recurse).
98
+ - `tests/singleFlight.test.ts` pins all of this against a fake API with *single-use*
99
+ rotating refresh tokens. It is the regression test for a real production bug
100
+ (N parallel 401s → N refreshes → N−1 failures → random logouts) and is non-negotiable.
101
+
102
+ > ⚠️ **Gotcha:** Symptom — intermittent logouts return months after the fix. Cause —
103
+ > someone "simplified" the auth use case and dropped the shared inflight promise; the bug
104
+ > is invisible in manual testing because it needs concurrent expiry. Fix — the concurrency
105
+ > test stays, and any `AuthClient` refactor runs it first.
106
+
107
+ ## 7. Testing conventions
108
+
109
+ - **vitest**, plain `node` environment. `npx vitest run` (CI mode) is the gate; watch mode
110
+ is for development only.
111
+ - **Stub the `HttpTransport` port** in client/use-case tests — never patch global `fetch`
112
+ there (only a transport adapter test may). Fakes enforce realistic API behavior (e.g.
113
+ single-use refresh tokens), not just canned responses.
114
+ - Concurrency tests use `Promise.all` / `Promise.allSettled` over real async boundaries —
115
+ no fake timers for the single-flight suite (timing is the point; keep timeouts short).
116
+ - Every test asserts observable behavior (calls, payloads, thrown `ApiError` fields).
117
+ A test without a meaningful assertion is deleted, not kept for coverage.
118
+ - Tests import from `../src/index` wherever possible — they double as proof that the
119
+ public surface actually exports what consumers need.
120
+
121
+ ## 8. Releases & versioning
122
+
123
+ - **Semver, tagged.** While `0.x`: breaking ⇒ **minor** bump; from `1.0.0`: breaking ⇒
124
+ **major**. (This convention is stated here so clients can rely on it — changing it is a
125
+ DECISIONS.md entry.)
126
+ - **Never commit `dist/`** — built at tag time. Consumers pin tags or registry versions,
127
+ never a branch (MULTI_REPO_CONTRACT.md — branch pins broke a production deploy with an
128
+ empty diff).
129
+ - Every breaking change ships a dated **MIGRATION.md** section in the same change: what
130
+ changed (before/after code), why, impact, and security implications when auth/cookies
131
+ are involved. Every sample in it must compile against the real surface
132
+ (SDK_CONTRACT.md §7 — invented samples convert knowledge gaps into false confidence).
133
+
134
+ Release checklist (all outputs shown, per DELIVERY.md proof discipline):
135
+
136
+ ```bash
137
+ npm run typecheck && npx vitest run # green
138
+ npm run build # tsup + verify-dist gate green
139
+ npm pkg get dependencies # expected (ideally {})
140
+ npm pack --dry-run # tarball = dist + manifest, nothing else
141
+ # CHANGELOG entry (+ MIGRATION entry if breaking) → bump version → git tag vX.Y.Z
142
+ ```
143
+
144
+ ## 9. Naming & style
145
+
146
+ - One exported class per file, file named after it: `AuthClient.ts`, `ProjectClient.ts`.
147
+ Resource clients end in `Client`; ports are interfaces named for the capability
148
+ (`HttpTransport`, `SDKContext`, `SDKLogger`).
149
+ - Names express intent, not technology (`ProjectClient.list()`, not `getProjectsHTTP`).
150
+ - String-literal unions over `enum` (erasable, tree-shakeable, JSON-friendly).
151
+ - `async/await` over `.then()` chains; no default exports; no module-level mutable state
152
+ outside class instances (the inflight refresh promise lives on the client instance).
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ *.tsbuildinfo
5
+ *.tgz
6
+ npm-debug.log*
@@ -0,0 +1,11 @@
1
+ {
2
+ "id": "ts-sdk",
3
+ "label": "TypeScript SDK — typed client between API and front(s)",
4
+ "languages": ["typescript"],
5
+ "idPrompt": "Package name (npm-style, e.g. @org/myapp-sdk)",
6
+ "requirements": [
7
+ "Node.js 20+ (built-in fetch)",
8
+ "npm 10+"
9
+ ],
10
+ "notes": "SDK builds and tests day one: npm install && npm run build && npm test"
11
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "{{BUNDLE_ID}}",
3
+ "version": "0.1.0",
4
+ "description": "{{PROJECT_NAME}} — typed SDK between the API and its clients",
5
+ "type": "module",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "sideEffects": false,
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "typesVersions": {
14
+ "*": {
15
+ "types": ["./dist/types/index.d.ts"]
16
+ }
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ }
28
+ },
29
+ "./types": {
30
+ "import": {
31
+ "types": "./dist/types/index.d.ts",
32
+ "default": "./dist/types/index.js"
33
+ },
34
+ "require": {
35
+ "types": "./dist/types/index.d.cts",
36
+ "default": "./dist/types/index.cjs"
37
+ }
38
+ }
39
+ },
40
+ "scripts": {
41
+ "build": "tsup && node scripts/verify-dist.mjs",
42
+ "typecheck": "tsc --noEmit",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest"
45
+ },
46
+ "engines": {
47
+ "node": ">=20"
48
+ },
49
+ "dependencies": {},
50
+ "devDependencies": {
51
+ "tsup": "^8.5.0",
52
+ "typescript": "^5.8.3",
53
+ "vitest": "^3.2.4"
54
+ }
55
+ }
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build gate: "it built" is NOT proof the types shipped (SDK_CONTRACT.md §4).
4
+ * A production team once shipped 13 of 21 type modules silently missing from
5
+ * dist/ because the declaration generator skipped .d.ts sources and a committed
6
+ * dist/ masked the hole. This script makes the proof mechanical:
7
+ * 1. every expected dist/ artifact exists;
8
+ * 2. every public name is present in the emitted declarations.
9
+ *
10
+ * Runs as part of `npm run build`. Add new public names when you extend index.ts.
11
+ */
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+
14
+ const REQUIRED_FILES = [
15
+ 'dist/index.js',
16
+ 'dist/index.cjs',
17
+ 'dist/index.d.ts',
18
+ 'dist/index.d.cts',
19
+ 'dist/types/index.js',
20
+ 'dist/types/index.cjs',
21
+ 'dist/types/index.d.ts',
22
+ 'dist/types/index.d.cts',
23
+ ];
24
+
25
+ // Names that consumers import — keep in sync with src/index.ts and src/types/index.ts.
26
+ const PUBLIC_SURFACE = {
27
+ 'dist/index.d.ts': [
28
+ 'createSdk',
29
+ 'SDKOptions',
30
+ 'ApiError',
31
+ 'AuthClient',
32
+ 'HttpClient',
33
+ 'HttpTransport',
34
+ 'createMemoryContext',
35
+ 'SDKContext',
36
+ 'SDKLogger',
37
+ 'TokenPair',
38
+ ],
39
+ 'dist/types/index.d.ts': ['HttpMethod', 'TokenPair', 'ApiErrorBody'],
40
+ };
41
+
42
+ let failed = false;
43
+
44
+ for (const file of REQUIRED_FILES) {
45
+ if (!existsSync(file)) {
46
+ console.error(`✗ missing build artifact: ${file}`);
47
+ failed = true;
48
+ }
49
+ }
50
+
51
+ if (!failed) {
52
+ for (const [file, names] of Object.entries(PUBLIC_SURFACE)) {
53
+ const dts = readFileSync(file, 'utf8');
54
+ for (const name of names) {
55
+ if (!dts.includes(name)) {
56
+ console.error(`✗ ${file}: public name "${name}" not found in emitted declarations`);
57
+ failed = true;
58
+ }
59
+ }
60
+ }
61
+ }
62
+
63
+ if (failed) {
64
+ console.error('\ndist verification FAILED — do not tag this build.');
65
+ process.exit(1);
66
+ }
67
+ console.log(`✓ dist verification OK (${REQUIRED_FILES.length} artifacts, declarations contain the public surface)`);