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,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,14 +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
- "@aws-sdk/client-dynamodb": "^3.1092.0",
12
- "@aws-sdk/lib-dynamodb": "^3.1092.0"
13
- }
14
- }
@@ -1,17 +0,0 @@
1
- // The ONLY file in the project that constructs the storage SDK client.
2
- import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
- import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
4
-
5
- /**
6
- * Structural view of the document client — models depend on this, tests stub it.
7
- * (Method parameters are bivariant, so the real client satisfies it.)
8
- */
9
- export interface DocumentClient {
10
- send(command: unknown): Promise<unknown>;
11
- }
12
-
13
- export function createDocumentClient(): DocumentClient {
14
- return DynamoDBDocumentClient.from(new DynamoDBClient({}), {
15
- marshallOptions: { removeUndefinedValues: true },
16
- }) as DocumentClient;
17
- }
@@ -1,2 +0,0 @@
1
- export { createDocumentClient, type DocumentClient } from './client.js';
2
- export { NoteModel } from './note-model.js';
@@ -1,52 +0,0 @@
1
- // Single-table layout: pk = NOTE#<id>, sk = META. One model, one table, one
2
- // place that knows the key schema.
3
- import { GetCommand, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
4
- import { NoteSchema, type Note } from '@app/core';
5
- import { NotFoundError } from '@app/shared';
6
- import type { DocumentClient } from './client.js';
7
-
8
- const noteKey = (id: string) => ({ pk: `NOTE#${id}`, sk: 'META' });
9
-
10
- export class NoteModel {
11
- constructor(
12
- private readonly client: DocumentClient,
13
- private readonly tableName: string,
14
- ) {}
15
-
16
- async put(note: Note): Promise<void> {
17
- await this.client.send(
18
- new PutCommand({
19
- TableName: this.tableName,
20
- Item: { ...noteKey(note.id), ...note },
21
- ConditionExpression: 'attribute_not_exists(pk)',
22
- }),
23
- );
24
- }
25
-
26
- async get(id: string): Promise<Note> {
27
- const result = (await this.client.send(
28
- new GetCommand({ TableName: this.tableName, Key: noteKey(id) }),
29
- )) as { Item?: Record<string, unknown> };
30
- if (!result.Item) {
31
- throw new NotFoundError(`note ${id} not found`);
32
- }
33
- // Validate on the way out too: the database is an external system.
34
- return NoteSchema.parse(result.Item);
35
- }
36
-
37
- async list(): Promise<Note[]> {
38
- // A filtered Scan is fine at skeleton scale; swap for a GSI Query when
39
- // real data volume arrives (a Tier-2 change — it touches the schema).
40
- const result = (await this.client.send(
41
- new ScanCommand({
42
- TableName: this.tableName,
43
- FilterExpression: 'sk = :meta',
44
- ExpressionAttributeValues: { ':meta': 'META' },
45
- }),
46
- )) as { Items?: Array<Record<string, unknown>> };
47
- // Validate every entry — silently skipping corruption would hide data loss.
48
- return (result.Items ?? [])
49
- .map((item) => NoteSchema.parse(item))
50
- .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
51
- }
52
- }
@@ -1,91 +0,0 @@
1
- import { GetCommand, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb';
2
- import { NotFoundError } from '@app/shared';
3
- import { describe, expect, it } from 'vitest';
4
- import { NoteModel } from '../src/note-model.js';
5
- import type { DocumentClient } from '../src/client.js';
6
-
7
- const note = {
8
- id: 'n1',
9
- title: 'Hello',
10
- slug: 'hello',
11
- tags: ['x'],
12
- createdAt: '2024-01-01T00:00:00.000Z',
13
- };
14
-
15
- function stubClient(response: unknown = {}) {
16
- const sent: unknown[] = [];
17
- const client: DocumentClient = {
18
- send: (command) => {
19
- sent.push(command);
20
- return Promise.resolve(response);
21
- },
22
- };
23
- return { client, sent };
24
- }
25
-
26
- describe('NoteModel.put', () => {
27
- it('writes the note under the single-table key, refusing overwrites', async () => {
28
- const { client, sent } = stubClient();
29
- await new NoteModel(client, 'notes-table').put(note);
30
- expect(sent).toHaveLength(1);
31
- const command = sent[0] as PutCommand;
32
- expect(command).toBeInstanceOf(PutCommand);
33
- expect(command.input).toMatchObject({
34
- TableName: 'notes-table',
35
- Item: { pk: 'NOTE#n1', sk: 'META', ...note },
36
- ConditionExpression: 'attribute_not_exists(pk)',
37
- });
38
- });
39
- });
40
-
41
- describe('NoteModel.get', () => {
42
- it('reads by key and validates the stored shape', async () => {
43
- const { client, sent } = stubClient({ Item: { pk: 'NOTE#n1', sk: 'META', ...note } });
44
- const loaded = await new NoteModel(client, 'notes-table').get('n1');
45
- expect(loaded).toEqual(note);
46
- const command = sent[0] as GetCommand;
47
- expect(command).toBeInstanceOf(GetCommand);
48
- expect(command.input).toMatchObject({
49
- TableName: 'notes-table',
50
- Key: { pk: 'NOTE#n1', sk: 'META' },
51
- });
52
- });
53
-
54
- it('throws NotFoundError on a miss', async () => {
55
- const { client } = stubClient({});
56
- await expect(new NoteModel(client, 'notes-table').get('nope')).rejects.toThrow(NotFoundError);
57
- });
58
-
59
- it('refuses corrupt stored data instead of returning it', async () => {
60
- const { client } = stubClient({ Item: { pk: 'NOTE#n1', sk: 'META', id: 'n1' } });
61
- await expect(new NoteModel(client, 'notes-table').get('n1')).rejects.toThrow();
62
- });
63
- });
64
-
65
- describe('NoteModel.list', () => {
66
- it('scans meta items, validates them, and returns newest-first', async () => {
67
- const older = { ...note, id: 'a', createdAt: '2024-01-01T00:00:00.000Z' };
68
- const newer = { ...note, id: 'b', createdAt: '2024-02-01T00:00:00.000Z' };
69
- const { client, sent } = stubClient({
70
- Items: [
71
- { pk: 'NOTE#a', sk: 'META', ...older },
72
- { pk: 'NOTE#b', sk: 'META', ...newer },
73
- ],
74
- });
75
- const listed = await new NoteModel(client, 'notes-table').list();
76
- expect(listed.map((n) => n.id)).toEqual(['b', 'a']);
77
- const command = sent[0] as ScanCommand;
78
- expect(command).toBeInstanceOf(ScanCommand);
79
- expect(command.input.TableName).toBe('notes-table');
80
- });
81
-
82
- it('returns [] for an empty table', async () => {
83
- const { client } = stubClient({ Items: [] });
84
- expect(await new NoteModel(client, 'notes-table').list()).toEqual([]);
85
- });
86
-
87
- it('refuses corrupt stored entries instead of skipping them', async () => {
88
- const { client } = stubClient({ Items: [{ pk: 'NOTE#x', sk: 'META', id: 'x' }] });
89
- await expect(new NoteModel(client, 'notes-table').list()).rejects.toThrow();
90
- });
91
- });
@@ -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
- });
@@ -1,28 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { AppError, NotFoundError, ValidationError } from '../src/errors.js';
3
-
4
- describe('typed errors', () => {
5
- it('AppError defaults to INTERNAL/500', () => {
6
- const error = new AppError('boom');
7
- expect(error.code).toBe('INTERNAL');
8
- expect(error.statusCode).toBe(500);
9
- expect(error.name).toBe('AppError');
10
- });
11
-
12
- it('preserves an explicit cause', () => {
13
- const cause = new Error('root');
14
- expect(new AppError('boom', { cause }).cause).toBe(cause);
15
- });
16
-
17
- it('ValidationError is 400 and carries issues', () => {
18
- const error = new ValidationError('bad input', ['title: required']);
19
- expect(error.statusCode).toBe(400);
20
- expect(error.code).toBe('VALIDATION');
21
- expect(error.issues).toEqual(['title: required']);
22
- expect(error).toBeInstanceOf(AppError);
23
- });
24
-
25
- it('NotFoundError is 404', () => {
26
- expect(new NotFoundError('nope').statusCode).toBe(404);
27
- });
28
- });
@@ -1,19 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { createLogger } from '../src/logger.js';
3
-
4
- describe('createLogger', () => {
5
- it('emits one JSON line per call with level, message, base and call fields', () => {
6
- const lines: string[] = [];
7
- const log = createLogger({ service: 'api' }, (line) => lines.push(line));
8
- log.info('started', { port: 3000 });
9
- log.error('failed');
10
- expect(lines).toHaveLength(2);
11
- expect(JSON.parse(lines[0]!)).toEqual({
12
- level: 'info',
13
- message: 'started',
14
- service: 'api',
15
- port: 3000,
16
- });
17
- expect(JSON.parse(lines[1]!)).toMatchObject({ level: 'error', message: 'failed' });
18
- });
19
- });