create-agent-rig 0.9.1 → 0.10.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 (234) hide show
  1. package/CHANGELOG.md +419 -3
  2. package/README.md +267 -305
  3. package/package.json +8 -14
  4. package/packages/cli/dist/commands/create.js +56 -88
  5. package/packages/cli/dist/commands/doctor.js +213 -0
  6. package/packages/cli/dist/commands/init.js +111 -50
  7. package/packages/cli/dist/commands/integrations.js +468 -0
  8. package/packages/cli/dist/commands/setup-wizard.js +61 -0
  9. package/packages/cli/dist/commands/uninstall.js +1223 -0
  10. package/packages/cli/dist/commands/upgrade.js +472 -90
  11. package/packages/cli/dist/index.js +651 -46
  12. package/packages/cli/dist/integrations/declaration.js +158 -0
  13. package/packages/cli/dist/integrations/doctor-guards.js +126 -0
  14. package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
  15. package/packages/cli/dist/integrations/mcp-json.js +107 -0
  16. package/packages/cli/dist/integrations/memory-doctor.js +124 -0
  17. package/packages/cli/dist/integrations/registry.js +22 -0
  18. package/packages/cli/dist/integrations/spawn.js +228 -0
  19. package/packages/cli/dist/integrations/spec-kit.js +280 -0
  20. package/packages/cli/dist/integrations/verify.js +193 -0
  21. package/packages/cli/dist/integrations/windows-job.js +275 -0
  22. package/packages/cli/dist/lib/elevated-paths.js +74 -0
  23. package/packages/cli/dist/lib/install-set.js +1 -44
  24. package/packages/cli/dist/lib/manifest.js +64 -5
  25. package/packages/cli/dist/lib/prompts.js +0 -23
  26. package/packages/cli/dist/lib/safe-path.js +193 -2
  27. package/packages/cli/dist/lib/safe-text.js +78 -0
  28. package/packages/cli/dist/lib/substitute.js +1 -52
  29. package/packages/cli/dist/lib/summary.js +5 -5
  30. package/packages/cli/dist/templates.js +4 -14
  31. package/scripts/prepare.mjs +28 -12
  32. package/templates/agent-os/subagent-routing.json +4 -4
  33. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
  34. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
  35. package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
  36. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
  37. package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
  38. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
  39. package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
  40. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
  41. package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
  42. package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
  43. package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
  44. package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
  45. package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
  46. package/templates/agent-os/universal/.claude/rules/workflow.md +47 -22
  47. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
  48. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
  49. package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
  50. package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
  51. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
  52. package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
  53. package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
  54. package/templates/agent-os/universal/.claude/settings.json +0 -8
  55. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
  56. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
  57. package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
  58. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
  59. package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
  60. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
  61. package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
  62. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
  63. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
  64. package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
  65. package/templates/agent-os/universal/.codex/hooks.json +0 -10
  66. package/templates/agent-os/universal/AGENTS.md +208 -107
  67. package/templates/agent-os/universal/CLAUDE.md +15 -162
  68. package/templates/agent-os/universal/PLAN.md +17 -10
  69. package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
  70. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
  71. package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
  72. package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
  73. package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
  74. package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
  75. package/templates/agent-os/universal/layers.json +25 -30
  76. package/templates/hash-history.json +8 -4
  77. package/templates/release-ledger.json +2 -1
  78. package/packages/cli/dist/lib/composition.js +0 -20
  79. package/packages/cli/dist/lib/targets.js +0 -28
  80. package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
  81. package/packages/cli/dist/policy/core/adapter.js +0 -18
  82. package/packages/cli/dist/policy/core/coverage.js +0 -253
  83. package/packages/cli/dist/policy/core/decision-record.js +0 -287
  84. package/packages/cli/dist/policy/core/declaration.js +0 -127
  85. package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
  86. package/packages/cli/dist/policy/core/probe.js +0 -442
  87. package/packages/cli/dist/policy/core/registry.js +0 -115
  88. package/packages/cli/dist/policy/core/validation.js +0 -275
  89. package/packages/cli/dist/policy/core/vocabulary.js +0 -123
  90. package/packages/cli/dist/policy/harness/claude.js +0 -47
  91. package/packages/cli/dist/policy/harness/codex.js +0 -87
  92. package/packages/cli/dist/policy/harness/index.js +0 -15
  93. package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
  94. package/packages/cli/dist/policy/index.js +0 -17
  95. package/templates/agent-os/init/AGENTS.md +0 -201
  96. package/templates/agent-os/init/CLAUDE.md +0 -201
  97. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
  98. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
  99. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
  100. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
  101. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
  102. package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
  103. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
  104. package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
  105. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
  106. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
  107. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
  108. package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
  109. package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
  110. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
  111. package/templates/skeleton/aws-serverless/README.md +0 -181
  112. package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
  113. package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
  114. package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
  115. package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
  116. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
  117. package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
  118. package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
  119. package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
  120. package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
  121. package/templates/skeleton/aws-serverless/gitignore +0 -56
  122. package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
  123. package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
  124. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
  125. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
  126. package/templates/skeleton/aws-serverless/infra/package.json +0 -18
  127. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
  128. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
  129. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
  130. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
  131. package/templates/skeleton/aws-serverless/package.json +0 -30
  132. package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
  133. package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
  134. package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
  135. package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
  136. package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
  137. package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
  138. package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
  139. package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
  140. package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
  141. package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
  142. package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
  143. package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
  144. package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
  145. package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
  146. package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
  147. package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
  148. package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
  149. package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
  150. package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
  151. package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
  152. package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
  153. package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
  154. package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
  155. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
  156. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
  157. package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
  158. package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
  159. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
  160. package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
  161. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
  162. package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
  163. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
  164. package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
  165. package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
  166. package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
  167. package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
  168. package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
  169. package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
  170. package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
  171. package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
  172. package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
  173. package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
  174. package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
  175. package/templates/skeleton/node-service/README.md +0 -104
  176. package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
  177. package/templates/skeleton/node-service/apps/web/package.json +0 -19
  178. package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
  179. package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
  180. package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
  181. package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
  182. package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
  183. package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
  184. package/templates/skeleton/node-service/eslint.config.mjs +0 -20
  185. package/templates/skeleton/node-service/gitignore +0 -54
  186. package/templates/skeleton/node-service/package.json +0 -30
  187. package/templates/skeleton/node-service/packages/core/package.json +0 -11
  188. package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
  189. package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
  190. package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
  191. package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
  192. package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
  193. package/templates/skeleton/node-service/packages/db/package.json +0 -12
  194. package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
  195. package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
  196. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
  197. package/templates/skeleton/node-service/packages/shared/package.json +0 -11
  198. package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
  199. package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
  200. package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
  201. package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
  202. package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
  203. package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
  204. package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
  205. package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
  206. package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
  207. package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
  208. package/templates/skeleton/node-service/services/api/package.json +0 -17
  209. package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
  210. package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
  211. package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
  212. package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
  213. package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
  214. package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
  215. package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
  216. package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
  217. package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
  218. package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
  219. package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
  220. package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
  221. package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
  222. package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
  223. package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
  224. package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
  225. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
  226. package/templates/skeleton/node-service/services/worker/package.json +0 -16
  227. package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
  228. package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
  229. package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
  230. package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
  231. package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
  232. package/templates/skeleton/node-service/tsconfig.base.json +0 -15
  233. package/templates/skeleton/node-service/tsconfig.json +0 -13
  234. package/templates/skeleton/node-service/vitest.config.ts +0 -12
@@ -1,68 +0,0 @@
1
- // PreToolUse hook: the web app is a consumer of the domain, not the backend.
2
- // `apps/web` may import the pure core and shared utilities — never the storage
3
- // layer (`…/db`) and never the services. Enforced at the tool layer, same as
4
- // core purity: best-effort text scan, failing safe toward a false block.
5
- //
6
- // Contract (Claude Code and Codex): JSON on stdin; exit 0 = allow, exit 2 = block, and
7
- // stderr is shown to the agent as the reason.
8
- // Generator-owned coverage for the neutral bounded-inspection refusal lives upstream in
9
- // codex.test.ts (absent in a generated rig) › "$guard blocks with a neutral,
10
- // actionable size-limit refusal" — a downstream edit requires a local replacement test.
11
- import { editFragments } from './lib/edit-input.mjs';
12
- import { readHookInput } from './lib/hook-input.mjs';
13
-
14
- const WEB_PATH = /(^|\/)apps\/web\//;
15
- const CODE_FILE = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
16
-
17
- // Workspace package names arrive rewritten to the project scope, so match the
18
- // package *suffix* under any scope: @<anything>/db, @<anything>/api, …
19
- const FORBIDDEN_WORKSPACE = /^@[^/]+\/(db|api|worker)$/;
20
- const FORBIDDEN_RELATIVE = /(^|\/)(packages\/db|services)(\/|$)/;
21
-
22
- function main() {
23
- const input = readHookInput();
24
- if (input === null) return 0; // unparseable payload: not ours to judge
25
- const violations = [];
26
- const importRe =
27
- /(?:\bfrom\s*|\bimport\s*\(\s*|\brequire\s*\(\s*|^\s*import\s+)['"]([^'"]+)['"]/gm;
28
- const fragments = editFragments(input);
29
- const blocked = fragments.find(
30
- ({ inspectionRefusal, appliesToAll }) => appliesToAll && inspectionRefusal,
31
- );
32
- const globalRefusal = blocked?.inspectionRefusal;
33
- if (globalRefusal) {
34
- process.stderr.write(
35
- `BLOCKED — cannot safely inspect this edit: ${globalRefusal}\n` +
36
- // The remedy has to match the refusal: splitting cannot change a
37
- // container shape, and a fixed line sent the agent into a retry loop
38
- // on the one path it could not retry out of.
39
- `${blocked.remedy ?? 'Split it into a smaller patch and retry.'}\n`,
40
- );
41
- return 2;
42
- }
43
-
44
- for (const { filePath, fragment, inspectionRefusal } of fragments) {
45
- if (!WEB_PATH.test(filePath) || !CODE_FILE.test(filePath)) continue;
46
- if (inspectionRefusal) {
47
- violations.push(`cannot safely inspect this move — ${inspectionRefusal}`);
48
- continue;
49
- }
50
- for (const match of fragment.matchAll(importRe)) {
51
- const spec = match[1];
52
- if (FORBIDDEN_WORKSPACE.test(spec) || FORBIDDEN_RELATIVE.test(spec)) {
53
- violations.push(spec);
54
- }
55
- }
56
- }
57
- if (violations.length === 0) return 0;
58
-
59
- process.stderr.write(
60
- `BLOCKED — apps/web imports the domain (core, shared), never the backend:\n` +
61
- violations.map((v) => ` - "${v}" crosses the web boundary`).join('\n') +
62
- `\nThe web talks to services over HTTP only; storage stays behind the API ` +
63
- `(see .claude/rules/architecture.md).\n`,
64
- );
65
- return 2;
66
- }
67
-
68
- process.exit(main());
@@ -1,81 +0,0 @@
1
- # Architecture — layers and boundaries
2
-
3
- The layout is the same in every target; only the adapters at the edges change.
4
-
5
- ## Layers
6
-
7
- | Layer | Location | May depend on | Never contains |
8
- | --- | --- | --- | --- |
9
- | **core** | `packages/core/` | nothing (plus its schema library) | I/O, clock, randomness, environment, SDKs |
10
- | **shared** | `packages/shared/` | nothing external of note | domain logic |
11
- | **db** | `packages/db/` | core, shared | HTTP handling, business decisions |
12
- | **services** | `services/*` | core, db, shared | direct SDK/storage access |
13
- | **web** | `apps/web/` | core, shared | db, services, any storage or backend SDK |
14
-
15
- Dependency direction is one-way: `services → (core, db, shared)`, `db → (core, shared)`,
16
- `core → nothing`. A dependency pointing the other way is a defect, not a style choice.
17
-
18
- ## The web boundary is mechanical too
19
-
20
- `apps/web` is a consumer of the domain, not the backend: it imports the pure
21
- core (schemas, domain functions — the same validation the server trusts) and
22
- shared utilities, and talks to services **over HTTP only**. It never imports
23
- the storage layer or a service module — the `guard-web-boundary` hook refuses
24
- such an edit at the tool layer, exactly like core purity. The payoff is that
25
- one shared domain function validates on both sides of the wire: client-side
26
- for instant feedback, server-side for trust.
27
-
28
- **The web layer's test contract** (so "TDD, without exception" is read
29
- correctly here): its tests are the **shared-validation suite** — the same core
30
- function must accept/reject identically on both sides — plus the production
31
- build and typecheck. Component-level testing (DOM rendering, interaction
32
- frameworks) is deliberately out of scope; do **not** introduce a component
33
- testing apparatus unless real work in this project shows the need. New web
34
- *logic* still starts with a failing test — put the logic in a plain module
35
- (like `lib/validate`) and test it directly.
36
-
37
- ## The request path is fixed
38
-
39
- Every operation travels the same route, with no shortcuts:
40
-
41
- ```
42
- payload → handler → usecase → model
43
- ```
44
-
45
- - **Handlers** translate transport into schema-validated, typed input and back.
46
- Nothing else.
47
- - **The usecase layer is mandatory.** Every business operation has exactly one
48
- usecase function. Handlers never call models or SDKs directly — even for a
49
- "trivial" read. The uniformity is the point: it is what makes the codebase
50
- predictable for both humans and agents.
51
- - **Usecases receive their dependencies** (models, publishers, clock, id
52
- generation) as arguments, and invoke the core's pure domain functions
53
- themselves. That is what keeps the core pure and the tests fast.
54
- - A dedicated *service* layer between usecase and model is **deliberately
55
- absent** from the minimal skeleton: today a usecase is one domain function
56
- plus one model call. Introduce a service only when a usecase outgrows that —
57
- and then state it in this file, so the chain stays written down in one place.
58
-
59
- ## The core is pure — and the rule is mechanical
60
-
61
- `packages/core/src/` contains domain logic only: pure functions and schemas.
62
- No I/O, no clock, no randomness, no environment, no SDK. Values like "now" and
63
- "a new id" enter as arguments from the usecase layer.
64
-
65
- This is not a convention you are trusted to follow; the
66
- `guard-core-purity` hook refuses the edit at the tool layer. If the hook blocks
67
- you, the answer is to move the impure part out — never to look for a way around
68
- the hook.
69
-
70
- Precision about the claim, so it is never overstated: the guard is a
71
- **best-effort text scan of each edit fragment before it lands** — an `Edit`
72
- shows the hook its new text, not the whole resulting file. It stops the normal
73
- path cold; a determined evasion is caught by the layers behind it (review and
74
- tests), not by the hook. A rulebook that sells enforcement must describe its
75
- enforcement exactly.
76
-
77
- ## Storage has exactly one owner
78
-
79
- `packages/db/` is the only place that touches the storage SDK/driver. Every
80
- other external SDK likewise gets exactly one owning module. If you need a second
81
- place, you actually need a function exported from the first place.
@@ -1,28 +0,0 @@
1
- name: CI
2
-
3
- # The default token is read-only for this job: CI needs nothing else, and a
4
- # workflow that can write is a workflow a compromised dependency can write with.
5
- permissions:
6
- contents: read
7
-
8
- on:
9
- push:
10
- pull_request:
11
-
12
- jobs:
13
- ci:
14
- runs-on: ubuntu-latest
15
- steps:
16
- - uses: actions/checkout@v4
17
- - uses: pnpm/action-setup@v4
18
- with:
19
- version: 11
20
- - uses: actions/setup-node@v4
21
- with:
22
- node-version: 22
23
- cache: pnpm
24
- - run: pnpm install --frozen-lockfile
25
- - run: pnpm lint
26
- - run: pnpm typecheck
27
- - run: pnpm test
28
- - run: pnpm synth
@@ -1,90 +0,0 @@
1
- name: Deploy (dev)
2
-
3
- # DEV deployment only. Production is a HUMAN step (see README) — the Never tier
4
- # (.claude/rules/autonomy.md) forbids an agent from triggering a production
5
- # deploy, so no production path exists in this workflow at all.
6
- on:
7
- push:
8
- branches: [main, master]
9
- workflow_dispatch:
10
-
11
- permissions:
12
- # OIDC: assume a short-lived federated role. No long-lived keys in secrets —
13
- # a starter multiplies whatever it ships, so it ships zero static credentials.
14
- id-token: write
15
- contents: read
16
-
17
- jobs:
18
- deploy:
19
- runs-on: ubuntu-latest
20
- steps:
21
- - uses: actions/checkout@v4
22
-
23
- # Degrade cleanly: with no configured role this SKIPS with a message and
24
- # the job stays green — never a red X on a fresh project with no cloud
25
- # account. Enabling deployment is "add the AWS_DEPLOY_ROLE_ARN secret",
26
- # not "edit this file".
27
- - id: guard
28
- env:
29
- ROLE: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
30
- run: |
31
- if [ -z "$ROLE" ]; then
32
- echo "deploy skipped: no credentials configured — see README (Deploy)."
33
- echo "skip=true" >> "$GITHUB_OUTPUT"
34
- fi
35
-
36
- - if: steps.guard.outputs.skip != 'true'
37
- uses: pnpm/action-setup@v4
38
- - if: steps.guard.outputs.skip != 'true'
39
- uses: actions/setup-node@v4
40
- with:
41
- node-version: 22
42
- cache: pnpm
43
- - if: steps.guard.outputs.skip != 'true'
44
- run: pnpm install --frozen-lockfile
45
- - if: steps.guard.outputs.skip != 'true'
46
- run: pnpm build:web
47
- env:
48
- NEXT_PUBLIC_API_URL: ${{ vars.API_URL }}
49
-
50
- - if: steps.guard.outputs.skip != 'true'
51
- uses: aws-actions/configure-aws-credentials@v4
52
- with:
53
- role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
54
- aws-region: ${{ vars.AWS_REGION || '__REGION__' }}
55
-
56
- # `--require-approval never` is deliberate and dev-only: this workflow has
57
- # no production path at all (see the header), so the approval prompt would
58
- # only ever block an unattended dev deploy on a change nobody is watching.
59
- - if: steps.guard.outputs.skip != 'true'
60
- run: >
61
- pnpm --filter @app/infra exec cdk deploy AppStack WebStack
62
- --require-approval never --outputs-file cdk-outputs.json
63
-
64
- # The bundle is built above and the bucket is created by the deploy — but
65
- # nothing puts one into the other, so without this step CloudFront keeps
66
- # serving whatever was uploaded by hand, last time somebody remembered.
67
- # `web-stack.ts` deliberately does not use BucketDeployment (synth must
68
- # never depend on `next build` having run), which is why the upload lives
69
- # here rather than in the stack.
70
- - if: steps.guard.outputs.skip != 'true'
71
- name: Upload the web bundle and invalidate the edge cache
72
- run: |
73
- # `jq -r` prints the string "null" and exits 0 for a missing key, and
74
- # GitHub's shell is `bash -e` with no `-u` and no pipefail — so a
75
- # renamed stack or a half-finished deploy would sync the bundle into
76
- # `s3://null`, a real bucket in a global namespace someone else owns,
77
- # and issue --delete against it. `-e` makes jq exit non-zero instead.
78
- set -euo pipefail
79
- BUCKET=$(jq -er '.WebStack.WebBucketName' infra/cdk-outputs.json)
80
- DISTRIBUTION=$(jq -er '.WebStack.WebDistributionId' infra/cdk-outputs.json)
81
- # An existing-but-empty bundle syncs zero objects, and --delete then
82
- # empties the live site with a green checkmark.
83
- [ -n "$(ls -A apps/web/out 2>/dev/null)" ] || {
84
- echo "apps/web/out is empty — refusing to sync"; exit 1;
85
- }
86
- # --delete makes the bucket the bundle's territory alone: anything not
87
- # in this build is removed from it.
88
- aws s3 sync apps/web/out "s3://$BUCKET" --delete
89
- aws cloudfront create-invalidation \
90
- --distribution-id "$DISTRIBUTION" --paths '/*'
@@ -1,181 +0,0 @@
1
- # __PROJECT_NAME__
2
-
3
- A serverless notes service generated by `create-agent-rig`, target
4
- `aws-serverless`. It is deliberately the **smallest project that proves the
5
- architecture** — one HTTP route, one queue consumer — with every layer visible
6
- and every gate wired. This project is yours: **delete what you don't need.**
7
-
8
- ## Layout
9
-
10
- ```
11
- packages/core/ pure domain: zod schemas + createNote() (no I/O — hook-enforced)
12
- packages/shared/ logger, loadEnv(zod), typed errors
13
- packages/db/ NoteModel — the only code touching the DynamoDB SDK
14
- services/api/ POST /notes + GET /notes: payload → handler → usecase → model
15
- services/worker/ note.created consumer (batchSize 1 → DLQ after 3 strikes)
16
- apps/web/ static Next export; validates with the SAME core schema the
17
- server trusts (imports core+shared only — hook-enforced)
18
- infra/ CDK stacks: table, queue+DLQ+alarm, three lambdas, HTTP API,
19
- and S3+CloudFront for the web bundle
20
- .claude/ the agent operating system: rules, gates, blocking hooks
21
- ```
22
-
23
- The agent-facing map is `CLAUDE.md`; the rules live in `.claude/rules/`.
24
-
25
- ## Run it
26
-
27
- ```sh
28
- pnpm install
29
- pnpm test # every layer, core exhaustively
30
- pnpm lint
31
- pnpm typecheck
32
- pnpm build:web # static Next export → apps/web/out
33
- pnpm synth # CloudFormation synth (no AWS credentials needed)
34
- pnpm check # all of the above
35
- ```
36
-
37
- ## Deploy it
38
-
39
- ### Dev — automated, on push (OIDC, no static keys)
40
-
41
- `.github/workflows/deploy.yml` runs a **dev** deploy on every push to the
42
- default branch. It ships with **no credentials**, so on a fresh repo it skips
43
- cleanly with a message — never a red X. To enable it, you *add a secret*, you
44
- do not edit the workflow:
45
-
46
- 1. In AWS, create an IAM role your repo can assume via GitHub's OIDC provider
47
- (`token.actions.githubusercontent.com`) — a short-lived federated role, no
48
- long-lived access keys anywhere. Beyond what `cdk deploy` needs, the
49
- workflow uploads the bundle itself, so the role also needs
50
- **`s3:ListBucket`, `s3:PutObject`, `s3:DeleteObject`** on the web bucket and
51
- **`cloudfront:CreateInvalidation`** on the distribution. A role scoped only
52
- to CDK's bootstrap roles gets through the deploy and fails on the upload —
53
- after both stacks are already up.
54
- 2. Add its ARN as the repository secret **`AWS_DEPLOY_ROLE_ARN`**, and set the
55
- repo variable **`API_URL`** to the deployed API's URL (the `ApiUrl` output
56
- of a first deploy). It becomes `NEXT_PUBLIC_API_URL`, which Next **inlines
57
- into the bundle at build time** — unset, the site calls `/notes` on its own
58
- CloudFront domain, where nothing answers. `AWS_REGION` is genuinely
59
- optional. The first deploy is therefore two passes: deploy, read `ApiUrl`
60
- from the job's `cdk deploy` Outputs, set the variable, then re-run the
61
- workflow (push, or **Run workflow** — it is `workflow_dispatch`-enabled).
62
-
63
- The workflow then assumes the role, builds the web bundle, runs
64
- `cdk deploy AppStack WebStack --outputs-file`, then **uploads `apps/web/out` to
65
- the web bucket (`aws s3 sync --delete`) and invalidates the CloudFront cache**,
66
- reading both destinations from the stack outputs. You do not sync anything by
67
- hand on this path. The site is served from S3 + CloudFront (the `WebUrl`
68
- output).
69
-
70
- ### Local / manual
71
-
72
- ```sh
73
- # needs AWS credentials; region comes from your profile (generator default: __REGION__)
74
- cd infra
75
- npx cdk bootstrap # first time per account/region
76
- npx cdk deploy AppStack WebStack --outputs-file cdk-outputs.json
77
- # --delete makes the bucket the bundle's territory alone, so a stale or missing
78
- # `out/` would empty the live site — or restore last month's. The workflow
79
- # builds two steps before its sync; by hand, build here.
80
- # Read the outputs into variables FIRST. `jq -er` exits non-zero on a missing
81
- # key, but a command substitution inside an assignment or an argument throws
82
- # that status away — which is how a stale outputs file becomes a bundle built
83
- # against `null` and an `aws s3 sync … s3://null --delete`.
84
- API=$(jq -er '.AppStack.ApiUrl' cdk-outputs.json) || { echo "no ApiUrl"; exit 1; }
85
- BUCKET=$(jq -er '.WebStack.WebBucketName' cdk-outputs.json) || { echo "no WebBucketName"; exit 1; }
86
- DIST=$(jq -er '.WebStack.WebDistributionId' cdk-outputs.json) || { echo "no WebDistributionId"; exit 1; }
87
-
88
- # NEXT_PUBLIC_API_URL is inlined at build time: without it the bundle calls its
89
- # own CloudFront domain instead of the API, and the sync below makes that live.
90
- (cd .. && NEXT_PUBLIC_API_URL="$API" pnpm build:web)
91
- [ -f ../apps/web/out/index.html ] || { echo "no web bundle — build failed"; exit 1; }
92
- aws s3 sync ../apps/web/out "s3://$BUCKET" --delete
93
- # a synced bucket whose distribution still serves the old objects has not
94
- # deployed — invalidate, or you are looking at the previous build
95
- aws cloudfront create-invalidation --paths '/*' --distribution-id "$DIST"
96
- ```
97
-
98
- ### Production — a human step, on purpose
99
-
100
- There is **no production deploy in this repo**, by design: the agent operating
101
- system's Never tier forbids an agent from triggering a production deploy, so
102
- shipping an automated prod path would contradict the rules on day one.
103
- Promote to production yourself — a separate account/role, a reviewed change,
104
- your own approval — reusing the dev workflow's OIDC pattern.
105
-
106
- ## Verify runtime health (CI-green ≠ runtime-healthy)
107
-
108
- After every deploy:
109
-
110
- ```sh
111
- # From the deploy's outputs file — run this from `infra/`, where the manual
112
- # section leaves you. Deployed through CI instead? That file is written on the
113
- # runner and never lands here: take both values from the job's `cdk deploy`
114
- # Outputs, or re-run `cdk deploy … --outputs-file cdk-outputs.json` locally.
115
- API_URL=$(jq -er '.AppStack.ApiUrl' cdk-outputs.json)
116
- WEB_URL=$(jq -er '.WebStack.WebUrl' cdk-outputs.json)
117
-
118
- curl -s -X POST "$API_URL/notes" \
119
- -H 'content-type: application/json' \
120
- -d '{"title":"smoke test","tags":["deploy"]}'
121
- # expect: HTTP 201 with { "note": { … } }
122
- ```
123
-
124
- **That checks the API, not the site.** The two fail independently: a bundle
125
- built without `NEXT_PUBLIC_API_URL` calls its own CloudFront domain, and a
126
- misnamed origin is refused by the browser — neither is visible to a `curl` that
127
- sends no `Origin` header. So also open the `WebUrl` output, create a note in
128
- the form, and reload:
129
-
130
- ```sh
131
- curl -s -I "$WEB_URL" | head -1 # the bundle is served
132
-
133
- # API Gateway answers a preflight 204 whether or not the origin matched, so the
134
- # status code proves nothing here. The browser gates on the echoed header —
135
- # that is what to look for. `content-type` is sent because the real call uses
136
- # it, and a non-safelisted header is what makes `allowHeaders` matter.
137
- curl -s -X OPTIONS "$API_URL/notes" -D - -o /dev/null \
138
- -H "Origin: $WEB_URL" \
139
- -H 'access-control-request-method: POST' \
140
- -H 'access-control-request-headers: content-type' \
141
- | grep -i '^access-control-allow-origin:'
142
- # expect: a line echoing $WEB_URL. No line = the browser will refuse the call,
143
- # whatever the status code said.
144
- ```
145
-
146
- Then confirm the pipeline: the worker logs `note.created processed`, and the
147
- **DLQ alarm stays quiet**. If the smoke test regresses: `npx cdk deploy` the
148
- previous revision (or `git revert` and redeploy) **first**, diagnose second.
149
-
150
- ## Boundaries (the short version)
151
-
152
- - Every request follows `payload → handler → usecase → model`. The usecase
153
- layer is mandatory even when it looks like ceremony.
154
- - `packages/core` is pure — the `guard-core-purity` hook refuses I/O, clock,
155
- randomness, and environment access at the tool layer.
156
- - `packages/db` is the only module that touches the storage SDK.
157
- - A failing queue message is poison: it throws, SQS retries ×3, the DLQ alarm
158
- fires. Never wrap the worker in a broad catch.
159
- - **Creating a note is a dual write, and it is not atomic.** `create-note`
160
- puts to DynamoDB and then publishes to SQS. If the publish fails, the note
161
- is stored and its event never happened — the worker never runs, and nothing
162
- compensates; the caller gets a 500 for a note that exists. That is a
163
- deliberate simplification for a starter. When it starts to matter, the two
164
- ways out are an **outbox** (write the event alongside the note, relay it
165
- afterwards) or DynamoDB Streams feeding the worker, which deletes the second
166
- write instead of coordinating it.
167
- - **CORS names who may call the API, and that half is wired for you.**
168
- `bin/app.ts` builds `WebStack` first and passes its CloudFront origin to
169
- `AppStack`, so the API allows the deployed site without you configuring it.
170
- The other half is not automatic: the bundle has to know where the API *is*,
171
- and that is `NEXT_PUBLIC_API_URL` above. For a custom domain or a second
172
- origin, deploy with `-c allowedOrigins=https://app.example.com` (comma-
173
- separated for several) — the entrypoint prefers the flag over the wired
174
- default, and the cross-stack export disappears with it. An `allowedOrigins`
175
- that parses to nothing, or an entry no browser could send, is refused at
176
- synth rather than deployed as an API nobody can call. `*` is not the default
177
- and never will be, but the flag does take it: you get a warning naming the
178
- consequence, not a refusal, because the alternative is people editing the
179
- stack.
180
-
181
- See `.claude/rules/architecture.md` for the full rules.
@@ -1,17 +0,0 @@
1
- /** @type {import('next').NextConfig} */
2
- const nextConfig = {
3
- // Static export: one build, no second runtime — the API server (or a CDN)
4
- // serves the bundle. See README for how this target serves it.
5
- output: 'export',
6
- // The core ships as TypeScript source; Next transpiles it for the browser.
7
- transpilePackages: ['@app/core'],
8
- // The core uses NodeNext-style relative imports ("./note.js" resolving to
9
- // note.ts). Turbopack cannot map that, so the build runs webpack (see the
10
- // build script) with the standard extension alias.
11
- webpack: (config) => {
12
- config.resolve.extensionAlias = { '.js': ['.ts', '.tsx', '.js'] };
13
- return config;
14
- },
15
- };
16
-
17
- export default nextConfig;
@@ -1,19 +0,0 @@
1
- {
2
- "name": "@app/web",
3
- "version": "0.1.0",
4
- "private": true,
5
- "scripts": {
6
- "dev": "next dev --port 3001 --webpack",
7
- "build": "next build --webpack"
8
- },
9
- "dependencies": {
10
- "@app/core": "workspace:*",
11
- "next": "^16.2.11",
12
- "react": "^19.2.8",
13
- "react-dom": "^19.2.8"
14
- },
15
- "devDependencies": {
16
- "@types/react": "^19.2.17",
17
- "@types/react-dom": "^19.2.3"
18
- }
19
- }
@@ -1,17 +0,0 @@
1
- import type { ReactNode } from 'react';
2
-
3
- // Deliberately unstyled: a starter's frontend is scaffolding, not a product.
4
- // Polish belongs in the mechanism (the shared validation, the boundary hook),
5
- // not in the pixels — replace all of this freely.
6
- export const metadata = {
7
- title: '__PROJECT_NAME__',
8
- description: 'Notes — generated by create-agent-rig',
9
- };
10
-
11
- export default function RootLayout({ children }: { children: ReactNode }) {
12
- return (
13
- <html lang="en">
14
- <body>{children}</body>
15
- </html>
16
- );
17
- }
@@ -1,96 +0,0 @@
1
- 'use client';
2
- // One page, one point: the SAME domain schema validates here (instant
3
- // feedback) and on the server (trust). See src/lib/validate.ts.
4
- import { useCallback, useEffect, useState } from 'react';
5
- import type { Note } from '@app/core';
6
- import { createNote, listNotes } from '../lib/api';
7
- import { validateNewNote } from '../lib/validate';
8
-
9
- export default function NotesPage() {
10
- const [title, setTitle] = useState('');
11
- const [tags, setTags] = useState('');
12
- const [issues, setIssues] = useState<string[]>([]);
13
- const [notes, setNotes] = useState<Note[]>([]);
14
- const [status, setStatus] = useState<string>('');
15
-
16
- const refresh = useCallback(async () => {
17
- try {
18
- setNotes(await listNotes());
19
- setStatus('');
20
- } catch (error) {
21
- setStatus(`could not load notes: ${String(error)}`);
22
- }
23
- }, []);
24
-
25
- useEffect(() => {
26
- void refresh();
27
- }, [refresh]);
28
-
29
- async function onSubmit(event: React.FormEvent) {
30
- event.preventDefault();
31
- const input = {
32
- title,
33
- tags: tags
34
- .split(',')
35
- .map((tag) => tag.trim())
36
- .filter(Boolean),
37
- };
38
- // Client-side: the same core schema the server will apply again.
39
- const verdict = validateNewNote(input);
40
- setIssues(verdict.issues);
41
- if (!verdict.ok) return;
42
- try {
43
- await createNote(input);
44
- setTitle('');
45
- setTags('');
46
- await refresh();
47
- } catch (error) {
48
- setIssues([String(error)]);
49
- }
50
- }
51
-
52
- return (
53
- <main>
54
- <h1>Notes</h1>
55
- <p>
56
- The form validates with the <em>same</em> core function the server
57
- trusts — one schema, both sides of the wire.
58
- </p>
59
-
60
- <form onSubmit={onSubmit}>
61
- <label>
62
- Title{' '}
63
- <input
64
- name="title"
65
- value={title}
66
- onChange={(event) => setTitle(event.target.value)}
67
- />
68
- </label>{' '}
69
- <label>
70
- Tags (comma-separated){' '}
71
- <input name="tags" value={tags} onChange={(event) => setTags(event.target.value)} />
72
- </label>{' '}
73
- <button type="submit">Create</button>
74
- </form>
75
-
76
- {issues.length > 0 && (
77
- <ul role="alert">
78
- {issues.map((issue) => (
79
- <li key={issue}>{issue}</li>
80
- ))}
81
- </ul>
82
- )}
83
- {status && <p role="status">{status}</p>}
84
-
85
- <h2>Existing</h2>
86
- <ul>
87
- {notes.map((note) => (
88
- <li key={note.id}>
89
- <strong>{note.title}</strong> <code>{note.slug}</code>{' '}
90
- <small>{note.tags.join(', ')}</small>
91
- </li>
92
- ))}
93
- </ul>
94
- </main>
95
- );
96
- }
@@ -1,36 +0,0 @@
1
- // The web talks to the backend over HTTP only — never by importing it
2
- // (the guard-web-boundary hook refuses such imports at the tool layer).
3
- import type { Note } from '@app/core';
4
-
5
- // In THIS target the API is never same-origin: the bundle is served from
6
- // CloudFront and the API is API Gateway, so `NEXT_PUBLIC_API_URL` has to be set
7
- // at build time — Next inlines it, and an unset one leaves the empty string
8
- // here, which sends every call to whatever is serving this page.
9
- //
10
- // The `?? ''` is a fallback to nothing, not to a working default: there is no
11
- // dev proxy in this project, so `pnpm --filter @app/web dev` needs the variable
12
- // too. It exists only so a missing value is a relative URL rather than the
13
- // string "undefined" in a request path.
14
- const base = process.env.NEXT_PUBLIC_API_URL ?? '';
15
-
16
- export async function createNote(input: { title: string; tags: string[] }): Promise<Note> {
17
- const response = await fetch(`${base}/notes`, {
18
- method: 'POST',
19
- headers: { 'content-type': 'application/json' },
20
- body: JSON.stringify(input),
21
- });
22
- const body = (await response.json()) as { note?: Note; error?: string; issues?: string[] };
23
- if (!response.ok || !body.note) {
24
- throw new Error(body.issues?.join('; ') ?? body.error ?? `HTTP ${response.status}`);
25
- }
26
- return body.note;
27
- }
28
-
29
- export async function listNotes(): Promise<Note[]> {
30
- const response = await fetch(`${base}/notes`);
31
- const body = (await response.json()) as { notes?: Note[]; error?: string };
32
- if (!response.ok || !body.notes) {
33
- throw new Error(body.error ?? `HTTP ${response.status}`);
34
- }
35
- return body.notes;
36
- }
@@ -1,23 +0,0 @@
1
- // The load-bearing import of this whole app: the browser validates with the
2
- // SAME schema the server trusts. Client-side for instant feedback,
3
- // server-side for trust — one function, two sides of the wire. This is what
4
- // core purity buys: `@app/core` has no I/O, so it runs anywhere.
5
- import { NewNoteSchema } from '@app/core';
6
-
7
- export interface ValidationResult {
8
- ok: boolean;
9
- issues: string[];
10
- }
11
-
12
- export function validateNewNote(input: unknown): ValidationResult {
13
- const parsed = NewNoteSchema.safeParse(input);
14
- if (parsed.success) {
15
- return { ok: true, issues: [] };
16
- }
17
- return {
18
- ok: false,
19
- issues: parsed.error.issues.map(
20
- (issue) => `${issue.path.join('.') || 'input'}: ${issue.message}`,
21
- ),
22
- };
23
- }