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,54 +0,0 @@
1
- node_modules/
2
- dist/
3
- coverage/
4
- *.tsbuildinfo
5
- .DS_Store
6
- var/
7
- .next/
8
- out/
9
- next-env.d.ts
10
- dist/
11
- .claude/worktrees/
12
- # per-checkout queue state: the tier the last close recorded. The queue CONFIG
13
- # is composed and tracked; this is runtime state and must never be committed.
14
- .claude/queue.state.json
15
- # per-checkout board selector (`queue/index.mjs board <name>`); the boards are in the config
16
- .claude/queue.board
17
- # gate rounds, one count per branch
18
- .claude/gate-rounds.json
19
- # per-run machine trace (.claude/scripts/run-journal.mjs): forensics that churn
20
- # on every gate and collide across concurrent sessions. Unlike a committed
21
- # evidence directory, this one is per-run and stays out of the repository.
22
- .claude/runs/
23
-
24
- # secrets — never commit these
25
- *.env
26
- .env
27
- .env.*
28
- !.env.example
29
- .envrc
30
- .npmrc
31
- .netrc
32
- .pgpass
33
- id_rsa
34
- id_ed25519
35
- *.pem
36
- *.key
37
- *.p12
38
- *.pfx
39
- *.keystore
40
- *.jks
41
- *.secret
42
- *.secrets
43
- *.token
44
- *.tokens
45
- *.password
46
- *.passwords
47
- *.credential
48
- *.credentials
49
- *.apikey
50
- *.apikeys
51
- *.passwd
52
- *.creds
53
- *.jwt
54
- *.bearer
@@ -1,30 +0,0 @@
1
- {
2
- "name": "@app/root",
3
- "version": "0.2.0",
4
- "private": true,
5
- "type": "module",
6
- "engines": {
7
- "node": ">=20"
8
- },
9
- "scripts": {
10
- "test": "vitest run",
11
- "lint": "eslint .",
12
- "typecheck": "tsc -p tsconfig.json && tsc -p apps/web/tsconfig.json",
13
- "build:web": "pnpm --filter @app/web build",
14
- "build:artifact": "pnpm build:web && node scripts/build-artifact.mjs",
15
- "start:api": "pnpm --filter @app/api start",
16
- "start:worker": "pnpm --filter @app/worker start",
17
- "check": "pnpm lint && pnpm typecheck && pnpm build:artifact && pnpm test"
18
- },
19
- "devDependencies": {
20
- "@eslint/js": "^10.0.1",
21
- "@types/node": "^26.1.1",
22
- "esbuild": "^0.28.1",
23
- "eslint": "^10.7.0",
24
- "globals": "^17.7.0",
25
- "tsx": "^4.23.1",
26
- "typescript": "^6.0.3",
27
- "typescript-eslint": "^8.65.0",
28
- "vitest": "^4.1.10"
29
- }
30
- }
@@ -1,11 +0,0 @@
1
- {
2
- "name": "@app/core",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "main": "src/index.ts",
7
- "types": "src/index.ts",
8
- "dependencies": {
9
- "zod": "^4.4.3"
10
- }
11
- }
@@ -1,14 +0,0 @@
1
- // Domain event vocabulary. The schema is the contract between the API (producer)
2
- // and the worker (consumer) — both sides validate against it.
3
- import { z } from 'zod';
4
- import { NoteSchema, type Note } from './note.js';
5
-
6
- export const NoteCreatedEventSchema = z.object({
7
- type: z.literal('note.created'),
8
- note: NoteSchema,
9
- });
10
- export type NoteCreatedEvent = z.infer<typeof NoteCreatedEventSchema>;
11
-
12
- export function makeNoteCreatedEvent(note: Note): NoteCreatedEvent {
13
- return { type: 'note.created', note };
14
- }
@@ -1,15 +0,0 @@
1
- export {
2
- NewNoteSchema,
3
- NoteSchema,
4
- InvalidNoteError,
5
- createNote,
6
- slugify,
7
- type NewNote,
8
- type Note,
9
- type NoteIdentity,
10
- } from './note.js';
11
- export {
12
- NoteCreatedEventSchema,
13
- makeNoteCreatedEvent,
14
- type NoteCreatedEvent,
15
- } from './events.js';
@@ -1,69 +0,0 @@
1
- // The domain core is pure: no I/O, no clock, no randomness, no environment.
2
- // "Now" and "a new id" are decided by the caller (the usecase layer) and enter
3
- // as arguments. The guard-core-purity hook enforces this at the tool layer.
4
- import { z } from 'zod';
5
-
6
- export const NewNoteSchema = z.object({
7
- title: z.string().trim().min(1, 'title must not be empty').max(200, 'title too long'),
8
- tags: z.array(z.string().trim().min(1).max(40)).max(10).default([]),
9
- });
10
- export type NewNote = z.infer<typeof NewNoteSchema>;
11
-
12
- export const NoteSchema = z.object({
13
- id: z.string().min(1),
14
- title: z.string().min(1),
15
- slug: z.string().min(1),
16
- tags: z.array(z.string()),
17
- createdAt: z.string().min(1),
18
- });
19
- export type Note = z.infer<typeof NoteSchema>;
20
-
21
- /** Thrown when input fails domain validation. The core owns its own error type. */
22
- export class InvalidNoteError extends Error {
23
- readonly issues: readonly string[];
24
-
25
- constructor(issues: readonly string[]) {
26
- super(`invalid note: ${issues.join('; ')}`);
27
- this.name = 'InvalidNoteError';
28
- this.issues = issues;
29
- }
30
- }
31
-
32
- export interface NoteIdentity {
33
- /** Generated by the caller — the core never invents ids. */
34
- id: string;
35
- /** ISO-8601 timestamp decided by the caller — the core never reads the clock. */
36
- createdAt: string;
37
- }
38
-
39
- /**
40
- * The one domain function: validate raw input and construct a Note.
41
- * Deterministic — identical arguments always yield an identical note.
42
- */
43
- export function createNote(input: unknown, identity: NoteIdentity): Note {
44
- const parsed = NewNoteSchema.safeParse(input);
45
- if (!parsed.success) {
46
- throw new InvalidNoteError(
47
- parsed.error.issues.map((issue) => `${issue.path.join('.') || 'input'}: ${issue.message}`),
48
- );
49
- }
50
- const { title, tags } = parsed.data;
51
- return {
52
- id: identity.id,
53
- title,
54
- slug: slugify(title),
55
- tags: [...new Set(tags)],
56
- createdAt: identity.createdAt,
57
- };
58
- }
59
-
60
- /** Lowercased, hyphen-separated, ascii-ish slug. Pure string work. */
61
- export function slugify(title: string): string {
62
- const slug = title
63
- .toLowerCase()
64
- .normalize('NFKD')
65
- .replace(/[\u0300-\u036f]/g, '') // strip combining marks left by NFKD
66
- .replace(/[^a-z0-9]+/g, '-')
67
- .replace(/^-+|-+$/g, '');
68
- return slug.length > 0 ? slug : 'note';
69
- }
@@ -1,23 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { NoteCreatedEventSchema, makeNoteCreatedEvent } from '../src/events.js';
3
- import { createNote } from '../src/note.js';
4
-
5
- const note = createNote({ title: 'Hello' }, { id: 'n1', createdAt: '2024-01-01T00:00:00.000Z' });
6
-
7
- describe('note.created event', () => {
8
- it('wraps a note and round-trips through its own schema', () => {
9
- const event = makeNoteCreatedEvent(note);
10
- expect(event.type).toBe('note.created');
11
- expect(NoteCreatedEventSchema.parse(JSON.parse(JSON.stringify(event)))).toEqual(event);
12
- });
13
-
14
- it('rejects a payload with the wrong type tag', () => {
15
- expect(NoteCreatedEventSchema.safeParse({ type: 'other', note }).success).toBe(false);
16
- });
17
-
18
- it('rejects a payload with a malformed note', () => {
19
- expect(
20
- NoteCreatedEventSchema.safeParse({ type: 'note.created', note: { id: '' } }).success,
21
- ).toBe(false);
22
- });
23
- });
@@ -1,101 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { InvalidNoteError, createNote, slugify } from '../src/note.js';
3
-
4
- const identity = { id: 'note-1', createdAt: '2024-01-01T00:00:00.000Z' };
5
-
6
- describe('createNote', () => {
7
- it('builds a note from valid input', () => {
8
- const note = createNote({ title: 'Hello World', tags: ['a', 'b'] }, identity);
9
- expect(note).toEqual({
10
- id: 'note-1',
11
- title: 'Hello World',
12
- slug: 'hello-world',
13
- tags: ['a', 'b'],
14
- createdAt: '2024-01-01T00:00:00.000Z',
15
- });
16
- });
17
-
18
- it('is deterministic: identical arguments yield an identical note', () => {
19
- const a = createNote({ title: 'Same' }, identity);
20
- const b = createNote({ title: 'Same' }, identity);
21
- expect(a).toEqual(b);
22
- });
23
-
24
- it('defaults tags to an empty array', () => {
25
- expect(createNote({ title: 'No tags' }, identity).tags).toEqual([]);
26
- });
27
-
28
- it('trims the title', () => {
29
- expect(createNote({ title: ' padded ' }, identity).title).toBe('padded');
30
- });
31
-
32
- it('deduplicates tags', () => {
33
- expect(createNote({ title: 'T', tags: ['x', 'x', 'y'] }, identity).tags).toEqual(['x', 'y']);
34
- });
35
-
36
- it('refuses an empty or whitespace-only title', () => {
37
- expect(() => createNote({ title: '' }, identity)).toThrow(InvalidNoteError);
38
- expect(() => createNote({ title: ' ' }, identity)).toThrow(InvalidNoteError);
39
- });
40
-
41
- it('refuses a title over 200 characters', () => {
42
- expect(() => createNote({ title: 'x'.repeat(201) }, identity)).toThrow(InvalidNoteError);
43
- expect(() => createNote({ title: 'x'.repeat(200) }, identity)).not.toThrow();
44
- });
45
-
46
- it('refuses more than 10 tags', () => {
47
- const tags = Array.from({ length: 11 }, (_, i) => `t${i}`);
48
- expect(() => createNote({ title: 'T', tags }, identity)).toThrow(InvalidNoteError);
49
- });
50
-
51
- it('refuses empty or oversized tags', () => {
52
- expect(() => createNote({ title: 'T', tags: [''] }, identity)).toThrow(InvalidNoteError);
53
- expect(() => createNote({ title: 'T', tags: ['x'.repeat(41)] }, identity)).toThrow(
54
- InvalidNoteError,
55
- );
56
- });
57
-
58
- it('refuses non-object input', () => {
59
- for (const bad of [null, undefined, 42, 'title', []]) {
60
- expect(() => createNote(bad, identity)).toThrow(InvalidNoteError);
61
- }
62
- });
63
-
64
- it('refuses wrongly-typed fields', () => {
65
- expect(() => createNote({ title: 42 }, identity)).toThrow(InvalidNoteError);
66
- expect(() => createNote({ title: 'T', tags: 'not-an-array' }, identity)).toThrow(
67
- InvalidNoteError,
68
- );
69
- });
70
-
71
- it('reports every issue with its path', () => {
72
- try {
73
- createNote({ title: '', tags: [''] }, identity);
74
- expect.unreachable('should have thrown');
75
- } catch (error) {
76
- const invalid = error as InvalidNoteError;
77
- expect(invalid.issues.length).toBeGreaterThanOrEqual(2);
78
- expect(invalid.issues.join('\n')).toMatch(/title/);
79
- expect(invalid.issues.join('\n')).toMatch(/tags/);
80
- }
81
- });
82
- });
83
-
84
- describe('slugify', () => {
85
- it('lowercases and hyphenates', () => {
86
- expect(slugify('Hello World')).toBe('hello-world');
87
- });
88
-
89
- it('collapses runs of non-alphanumerics and trims hyphens', () => {
90
- expect(slugify(' a -- b!! c ')).toBe('a-b-c');
91
- });
92
-
93
- it('strips diacritics', () => {
94
- expect(slugify('Crème Brûlée')).toBe('creme-brulee');
95
- });
96
-
97
- it('falls back to "note" when nothing survives', () => {
98
- expect(slugify('!!!')).toBe('note');
99
- expect(slugify('日本語')).toBe('note');
100
- });
101
- });
@@ -1,12 +0,0 @@
1
- {
2
- "name": "@app/db",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "main": "src/index.ts",
7
- "types": "src/index.ts",
8
- "dependencies": {
9
- "@app/core": "workspace:*",
10
- "@app/shared": "workspace:*"
11
- }
12
- }
@@ -1 +0,0 @@
1
- export { JsonFileNoteStore } from './note-store.js';
@@ -1,100 +0,0 @@
1
- // The model boundary, same contract as any other target — only the medium
2
- // differs: one JSON file. This is the ONLY module that knows how notes are
3
- // persisted; nothing else composes a path into the data file.
4
- import { randomUUID } from 'node:crypto';
5
- import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
6
- import path from 'node:path';
7
- import { NoteSchema, type Note } from '@app/core';
8
- import { AppError, NotFoundError } from '@app/shared';
9
-
10
- type NoteTable = Record<string, unknown>;
11
-
12
- export class JsonFileNoteStore {
13
- /**
14
- * Writes run one at a time — **per instance, which is the whole extent of
15
- * it**. A write is load → mutate → save, and that is not atomic: two
16
- * concurrent `put()`s would both read the same table and the second save
17
- * would drop the first note. This chain serialises the ones going through
18
- * *this* object. Two `JsonFileNoteStore`s over the same file lose notes to
19
- * each other exactly as before — in one process as readily as in two — so
20
- * share the instance, and reach for a real lock before sharing the file.
21
- */
22
- private writes: Promise<unknown> = Promise.resolve();
23
-
24
- constructor(private readonly file: string) {}
25
-
26
- async put(note: Note): Promise<void> {
27
- return this.serialised(async () => {
28
- const table = await this.load();
29
- if (note.id in table) {
30
- throw new AppError(`note ${note.id} already exists`, { code: 'CONFLICT', statusCode: 409 });
31
- }
32
- table[note.id] = note;
33
- await this.save(table);
34
- });
35
- }
36
-
37
- async get(id: string): Promise<Note> {
38
- const table = await this.load();
39
- const raw = table[id];
40
- if (raw === undefined) {
41
- throw new NotFoundError(`note ${id} not found`);
42
- }
43
- // Validate on the way out: the file is an external system.
44
- return NoteSchema.parse(raw);
45
- }
46
-
47
- async list(): Promise<Note[]> {
48
- const table = await this.load();
49
- // Validate every entry — silently skipping corruption would hide data loss.
50
- return Object.values(table)
51
- .map((raw) => NoteSchema.parse(raw))
52
- .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
53
- }
54
-
55
- /** Queues `work` behind the writes already in flight on this instance. */
56
- private serialised<T>(work: () => Promise<T>): Promise<T> {
57
- const result = this.writes.then(work);
58
- // The caller gets the rejection; the chain gets a settled promise. Without
59
- // this line one failed write (a 409, say) would reject every write queued
60
- // behind it, and this store object would stay broken for as long as it is
61
- // held — the chain is per instance, so a fresh store would still work.
62
- this.writes = result.then(
63
- () => undefined,
64
- () => undefined,
65
- );
66
- return result;
67
- }
68
-
69
- private async load(): Promise<NoteTable> {
70
- let content: string;
71
- try {
72
- content = await readFile(this.file, 'utf8');
73
- } catch (error) {
74
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') return {};
75
- throw error;
76
- }
77
- try {
78
- return JSON.parse(content) as NoteTable;
79
- } catch (cause) {
80
- throw new AppError(`data file ${this.file} is corrupt`, { code: 'DATA_CORRUPT', cause });
81
- }
82
- }
83
-
84
- private async save(table: NoteTable): Promise<void> {
85
- await mkdir(path.dirname(this.file), { recursive: true });
86
- // Write-then-rename keeps readers from ever seeing a half-written file.
87
- // The temp name is unique per write: a shared one lets two writers scribble
88
- // over each other's file and lose a rename to ENOENT.
89
- const tmp = `${this.file}.${process.pid}.${randomUUID()}.tmp`;
90
- try {
91
- await writeFile(tmp, JSON.stringify(table, null, 2));
92
- await rename(tmp, this.file);
93
- } catch (error) {
94
- // Best-effort tidy-up: a failure to remove the temp file must not
95
- // replace the failure that actually matters.
96
- await rm(tmp, { force: true }).catch(() => undefined);
97
- throw error;
98
- }
99
- }
100
- }
@@ -1,100 +0,0 @@
1
- import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
3
- import path from 'node:path';
4
- import { AppError, NotFoundError } from '@app/shared';
5
- import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
- import { JsonFileNoteStore } from '../src/note-store.js';
7
-
8
- const note = {
9
- id: 'n1',
10
- title: 'Hello',
11
- slug: 'hello',
12
- tags: ['x'],
13
- createdAt: '2024-01-01T00:00:00.000Z',
14
- };
15
-
16
- let dir: string;
17
- let file: string;
18
-
19
- beforeEach(async () => {
20
- dir = await mkdtemp(path.join(tmpdir(), 'note-store-'));
21
- file = path.join(dir, 'data', 'notes.json');
22
- });
23
-
24
- afterEach(async () => {
25
- await rm(dir, { recursive: true, force: true });
26
- });
27
-
28
- describe('JsonFileNoteStore', () => {
29
- it('round-trips a note (creating the data dir on first write)', async () => {
30
- const store = new JsonFileNoteStore(file);
31
- await store.put(note);
32
- expect(await store.get('n1')).toEqual(note);
33
- expect(JSON.parse(await readFile(file, 'utf8'))).toEqual({ n1: note });
34
- });
35
-
36
- it('refuses to overwrite an existing id', async () => {
37
- const store = new JsonFileNoteStore(file);
38
- await store.put(note);
39
- await expect(store.put(note)).rejects.toThrow(AppError);
40
- await expect(store.put(note)).rejects.toMatchObject({ statusCode: 409 });
41
- });
42
-
43
- it('a rejected write does not block the writes queued behind it', async () => {
44
- // The 409 above cannot see this: a poisoned write chain rejects the next
45
- // put() with the very same AppError the duplicate would have raised.
46
- const store = new JsonFileNoteStore(file);
47
- await store.put(note);
48
- await expect(store.put(note)).rejects.toThrow(AppError);
49
-
50
- await expect(store.put({ ...note, id: 'n2' })).resolves.toBeUndefined();
51
- expect((await store.list()).map((n) => n.id).sort()).toEqual(['n1', 'n2']);
52
- });
53
-
54
- it('throws NotFoundError on a miss', async () => {
55
- await expect(new JsonFileNoteStore(file).get('nope')).rejects.toThrow(NotFoundError);
56
- });
57
-
58
- it('refuses corrupt stored entries instead of returning them', async () => {
59
- const store = new JsonFileNoteStore(file);
60
- await store.put(note);
61
- const table = JSON.parse(await readFile(file, 'utf8'));
62
- table.n1 = { id: 'n1' };
63
- await writeFile(file, JSON.stringify(table));
64
- await expect(store.get('n1')).rejects.toThrow();
65
- });
66
-
67
- it('reports a corrupt data file clearly', async () => {
68
- const store = new JsonFileNoteStore(file);
69
- await store.put(note);
70
- await writeFile(file, '{not json');
71
- await expect(store.get('n1')).rejects.toMatchObject({ code: 'DATA_CORRUPT' });
72
- });
73
-
74
- it('lists notes newest-first, and an empty store as []', async () => {
75
- const store = new JsonFileNoteStore(file);
76
- expect(await store.list()).toEqual([]);
77
- await store.put({ ...note, id: 'a', createdAt: '2024-01-01T00:00:00.000Z' });
78
- await store.put({ ...note, id: 'b', createdAt: '2024-02-01T00:00:00.000Z' });
79
- const listed = await store.list();
80
- expect(listed.map((n) => n.id)).toEqual(['b', 'a']);
81
- });
82
-
83
- it('keeps every concurrently written note (no lost update)', async () => {
84
- const store = new JsonFileNoteStore(file);
85
- const ids = Array.from({ length: 20 }, (_, i) => `n${i}`);
86
- await Promise.all(ids.map((id) => store.put({ ...note, id })));
87
-
88
- expect((await store.list()).map((n) => n.id).sort()).toEqual([...ids].sort());
89
- expect(Object.keys(JSON.parse(await readFile(file, 'utf8'))).sort()).toEqual([...ids].sort());
90
- });
91
-
92
- it('refuses to list corrupt entries instead of skipping them silently', async () => {
93
- const store = new JsonFileNoteStore(file);
94
- await store.put(note);
95
- const table = JSON.parse(await readFile(file, 'utf8'));
96
- table.bad = { id: 'bad' };
97
- await writeFile(file, JSON.stringify(table));
98
- await expect(store.list()).rejects.toThrow();
99
- });
100
- });
@@ -1,11 +0,0 @@
1
- {
2
- "name": "@app/shared",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "main": "src/index.ts",
7
- "types": "src/index.ts",
8
- "dependencies": {
9
- "zod": "^4.4.3"
10
- }
11
- }
@@ -1,17 +0,0 @@
1
- import type { ZodType } from 'zod';
2
- import { AppError } from './errors.js';
3
-
4
- /**
5
- * Parse the environment through a zod schema, once, at startup. Configuration
6
- * is invalid → the process fails loudly at boot, not deep inside a request.
7
- */
8
- export function loadEnv<T>(schema: ZodType<T>, source: Record<string, string | undefined>): T {
9
- const parsed = schema.safeParse(source);
10
- if (!parsed.success) {
11
- const details = parsed.error.issues
12
- .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
13
- .join('; ');
14
- throw new AppError(`invalid environment: ${details}`, { code: 'ENV_INVALID' });
15
- }
16
- return parsed.data;
17
- }
@@ -1,33 +0,0 @@
1
- /**
2
- * Typed errors. Every error that crosses a layer boundary is one of these —
3
- * handlers map them to transport codes without inspecting messages.
4
- */
5
- export class AppError extends Error {
6
- readonly code: string;
7
- readonly statusCode: number;
8
-
9
- constructor(
10
- message: string,
11
- options: { code?: string; statusCode?: number; cause?: unknown } = {},
12
- ) {
13
- super(message, options.cause !== undefined ? { cause: options.cause } : undefined);
14
- this.name = new.target.name;
15
- this.code = options.code ?? 'INTERNAL';
16
- this.statusCode = options.statusCode ?? 500;
17
- }
18
- }
19
-
20
- export class ValidationError extends AppError {
21
- readonly issues: readonly string[];
22
-
23
- constructor(message: string, issues: readonly string[] = []) {
24
- super(message, { code: 'VALIDATION', statusCode: 400 });
25
- this.issues = issues;
26
- }
27
- }
28
-
29
- export class NotFoundError extends AppError {
30
- constructor(message: string) {
31
- super(message, { code: 'NOT_FOUND', statusCode: 404 });
32
- }
33
- }
@@ -1,3 +0,0 @@
1
- export { AppError, NotFoundError, ValidationError } from './errors.js';
2
- export { createLogger, type Logger } from './logger.js';
3
- export { loadEnv } from './env.js';
@@ -1,20 +0,0 @@
1
- /** Minimal structured logger: one JSON object per line on stdout. */
2
- export interface Logger {
3
- info(message: string, fields?: Record<string, unknown>): void;
4
- warn(message: string, fields?: Record<string, unknown>): void;
5
- error(message: string, fields?: Record<string, unknown>): void;
6
- }
7
-
8
- export function createLogger(
9
- base: Record<string, unknown> = {},
10
- write: (line: string) => void = (line) => process.stdout.write(line + '\n'),
11
- ): Logger {
12
- const emit = (level: string, message: string, fields?: Record<string, unknown>) => {
13
- write(JSON.stringify({ level, message, ...base, ...fields }));
14
- };
15
- return {
16
- info: (message, fields) => emit('info', message, fields),
17
- warn: (message, fields) => emit('warn', message, fields),
18
- error: (message, fields) => emit('error', message, fields),
19
- };
20
- }
@@ -1,26 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { z } from 'zod';
3
- import { AppError } from '../src/errors.js';
4
- import { loadEnv } from '../src/env.js';
5
-
6
- const schema = z.object({
7
- TABLE_NAME: z.string().min(1),
8
- QUEUE_URL: z.string().min(1),
9
- });
10
-
11
- describe('loadEnv', () => {
12
- it('returns the typed subset of the environment', () => {
13
- const env = loadEnv(schema, { TABLE_NAME: 't', QUEUE_URL: 'q', UNRELATED: 'x' });
14
- expect(env).toEqual({ TABLE_NAME: 't', QUEUE_URL: 'q' });
15
- });
16
-
17
- it('fails loudly, naming every missing key', () => {
18
- expect(() => loadEnv(schema, {})).toThrow(AppError);
19
- expect(() => loadEnv(schema, {})).toThrow(/TABLE_NAME/);
20
- expect(() => loadEnv(schema, {})).toThrow(/QUEUE_URL/);
21
- });
22
-
23
- it('rejects empty values', () => {
24
- expect(() => loadEnv(schema, { TABLE_NAME: '', QUEUE_URL: 'q' })).toThrow(/TABLE_NAME/);
25
- });
26
- });