create-agent-rig 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +462 -3
- package/README.md +267 -305
- package/package.json +8 -14
- package/packages/cli/dist/commands/create.js +56 -88
- package/packages/cli/dist/commands/doctor.js +213 -0
- package/packages/cli/dist/commands/init.js +111 -50
- package/packages/cli/dist/commands/integrations.js +468 -0
- package/packages/cli/dist/commands/setup-wizard.js +61 -0
- package/packages/cli/dist/commands/uninstall.js +1223 -0
- package/packages/cli/dist/commands/upgrade.js +508 -101
- package/packages/cli/dist/index.js +658 -47
- package/packages/cli/dist/integrations/declaration.js +158 -0
- package/packages/cli/dist/integrations/doctor-guards.js +126 -0
- package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
- package/packages/cli/dist/integrations/mcp-json.js +107 -0
- package/packages/cli/dist/integrations/memory-doctor.js +124 -0
- package/packages/cli/dist/integrations/registry.js +22 -0
- package/packages/cli/dist/integrations/spawn.js +228 -0
- package/packages/cli/dist/integrations/spec-kit.js +280 -0
- package/packages/cli/dist/integrations/verify.js +193 -0
- package/packages/cli/dist/integrations/windows-job.js +275 -0
- package/packages/cli/dist/lib/elevated-paths.js +74 -0
- package/packages/cli/dist/lib/install-set.js +1 -44
- package/packages/cli/dist/lib/manifest.js +64 -5
- package/packages/cli/dist/lib/prompts.js +0 -23
- package/packages/cli/dist/lib/safe-path.js +193 -2
- package/packages/cli/dist/lib/safe-text.js +78 -0
- package/packages/cli/dist/lib/substitute.js +1 -52
- package/packages/cli/dist/lib/summary.js +5 -5
- package/packages/cli/dist/templates.js +4 -14
- package/scripts/prepare.mjs +28 -12
- package/templates/agent-os/subagent-routing.json +4 -4
- package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +45 -12
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
- package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +4 -2
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
- package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
- package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
- package/templates/agent-os/universal/.claude/rules/workflow.md +59 -22
- package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
- package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
- package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
- package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
- package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
- package/templates/agent-os/universal/.claude/settings.json +0 -8
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +45 -12
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
- package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
- package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
- package/templates/agent-os/universal/.codex/hooks.json +0 -10
- package/templates/agent-os/universal/AGENTS.md +208 -107
- package/templates/agent-os/universal/CLAUDE.md +15 -162
- package/templates/agent-os/universal/PLAN.md +17 -10
- package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
- package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
- package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
- package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
- package/templates/agent-os/universal/layers.json +25 -30
- package/templates/hash-history.json +112 -40
- package/templates/release-ledger.json +3 -1
- package/packages/cli/dist/lib/composition.js +0 -20
- package/packages/cli/dist/lib/targets.js +0 -28
- package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
- package/packages/cli/dist/policy/core/adapter.js +0 -18
- package/packages/cli/dist/policy/core/coverage.js +0 -253
- package/packages/cli/dist/policy/core/decision-record.js +0 -287
- package/packages/cli/dist/policy/core/declaration.js +0 -127
- package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
- package/packages/cli/dist/policy/core/probe.js +0 -442
- package/packages/cli/dist/policy/core/registry.js +0 -115
- package/packages/cli/dist/policy/core/validation.js +0 -275
- package/packages/cli/dist/policy/core/vocabulary.js +0 -123
- package/packages/cli/dist/policy/harness/claude.js +0 -47
- package/packages/cli/dist/policy/harness/codex.js +0 -87
- package/packages/cli/dist/policy/harness/index.js +0 -15
- package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
- package/packages/cli/dist/policy/index.js +0 -17
- package/templates/agent-os/init/AGENTS.md +0 -201
- package/templates/agent-os/init/CLAUDE.md +0 -201
- package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
- package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
- package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
- package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
- package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
- package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
- package/templates/skeleton/aws-serverless/README.md +0 -181
- package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
- package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
- package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
- package/templates/skeleton/aws-serverless/gitignore +0 -56
- package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
- package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
- package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
- package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
- package/templates/skeleton/aws-serverless/infra/package.json +0 -18
- package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
- package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
- package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
- package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
- package/templates/skeleton/aws-serverless/package.json +0 -30
- package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
- package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
- package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
- package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
- package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
- package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
- package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
- package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
- package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
- package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
- package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
- package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
- package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
- package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
- package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
- package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
- package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
- package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
- package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
- package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
- package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
- package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
- package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
- package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
- package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
- package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
- package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
- package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
- package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
- package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
- package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
- package/templates/skeleton/node-service/README.md +0 -104
- package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
- package/templates/skeleton/node-service/apps/web/package.json +0 -19
- package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
- package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
- package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
- package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
- package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
- package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
- package/templates/skeleton/node-service/eslint.config.mjs +0 -20
- package/templates/skeleton/node-service/gitignore +0 -54
- package/templates/skeleton/node-service/package.json +0 -30
- package/templates/skeleton/node-service/packages/core/package.json +0 -11
- package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
- package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
- package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
- package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
- package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
- package/templates/skeleton/node-service/packages/db/package.json +0 -12
- package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
- package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
- package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
- package/templates/skeleton/node-service/packages/shared/package.json +0 -11
- package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
- package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
- package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
- package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
- package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
- package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
- package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
- package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
- package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
- package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
- package/templates/skeleton/node-service/services/api/package.json +0 -17
- package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
- package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
- package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
- package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
- package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
- package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
- package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
- package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
- package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
- package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
- package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
- package/templates/skeleton/node-service/services/worker/package.json +0 -16
- package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
- package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
- package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
- package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
- package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
- package/templates/skeleton/node-service/tsconfig.base.json +0 -15
- package/templates/skeleton/node-service/tsconfig.json +0 -13
- 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,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 +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,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,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
|
-
});
|