devmethod-ai 0.1.0-rc.2 → 0.2.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 (228) hide show
  1. package/.agents/skills/decision-architecture/SKILL.md +15 -13
  2. package/.agents/skills/decision-architecture/assets/ADR.md +18 -17
  3. package/.agents/skills/decision-architecture/references/api-contracts.md +21 -16
  4. package/.agents/skills/decision-architecture/references/backend-boundaries.md +21 -18
  5. package/.agents/skills/decision-architecture/references/product-decisions.md +14 -17
  6. package/.agents/skills/design-to-code/SKILL.md +18 -11
  7. package/.agents/skills/design-to-code/assets/UI_ACCEPTANCE.md +13 -12
  8. package/.agents/skills/design-to-code/references/ux-contract.md +20 -15
  9. package/.agents/skills/design-to-code/references/visual-creation.md +37 -0
  10. package/.agents/skills/project-foundation/SKILL.md +39 -30
  11. package/.agents/skills/project-foundation/assets/AGENTS.foundation.md +16 -15
  12. package/.agents/skills/project-foundation/assets/PROJECT_PROFILE.md +24 -23
  13. package/.agents/skills/project-foundation/assets/START_HERE.md +17 -14
  14. package/.agents/skills/project-foundation/references/mission-context.md +30 -0
  15. package/.agents/skills/project-foundation/references/operating-commands.md +49 -34
  16. package/.agents/skills/project-foundation/references/profiles/README.md +18 -0
  17. package/.agents/skills/project-foundation/references/profiles/ci.md +11 -0
  18. package/.agents/skills/project-foundation/references/profiles/cloud-delivery.md +13 -0
  19. package/.agents/skills/project-foundation/references/profiles/messaging.md +11 -0
  20. package/.agents/skills/project-foundation/references/profiles/mongodb.md +11 -0
  21. package/.agents/skills/project-foundation/references/profiles/node-nest.md +11 -0
  22. package/.agents/skills/project-foundation/references/profiles/postgres-drizzle.md +11 -0
  23. package/.agents/skills/project-foundation/references/profiles/react-next.md +11 -0
  24. package/.agents/skills/project-foundation/references/profiles/typescript.md +11 -0
  25. package/.agents/skills/project-foundation/references/work-sizing.md +31 -0
  26. package/.agents/skills/react-feature-engineering/SKILL.md +26 -22
  27. package/.agents/skills/react-feature-engineering/references/review-and-sources.md +41 -37
  28. package/.agents/skills/reliable-ai-integration/SKILL.md +13 -10
  29. package/.agents/skills/reliable-ai-integration/assets/AI_EVALUATION.md +19 -18
  30. package/.agents/skills/reliable-ai-integration/references/evidence-and-media.md +22 -18
  31. package/.agents/skills/reliable-ai-integration/references/jobs-and-costs.md +20 -15
  32. package/.agents/skills/scoped-delivery/SKILL.md +25 -18
  33. package/.agents/skills/scoped-delivery/assets/CHECKPOINT.md +22 -11
  34. package/.agents/skills/scoped-delivery/assets/MISSION.md +35 -0
  35. package/.agents/skills/scoped-delivery/assets/SLICE.md +15 -14
  36. package/.agents/skills/scoped-delivery/assets/VERIFICATION.md +19 -0
  37. package/.agents/skills/scoped-delivery/references/verification-and-cost.md +21 -18
  38. package/COMPATIBILITY.md +3 -3
  39. package/CONTRIBUTING.md +13 -0
  40. package/README.md +94 -9
  41. package/START_HERE.md +24 -0
  42. package/VALIDATION.md +37 -0
  43. package/dist/checkpoint.js +171 -0
  44. package/dist/cli.js +118 -17
  45. package/dist/doctor.js +113 -0
  46. package/dist/filesystem.js +32 -0
  47. package/dist/init.js +38 -34
  48. package/dist/mission.js +85 -0
  49. package/dist/planner.js +65 -0
  50. package/dist/records.js +76 -0
  51. package/dist/update.js +57 -0
  52. package/docs/ADR-001-distribution.md +13 -0
  53. package/docs/ADR-002-read-only-diagnostics.md +25 -0
  54. package/docs/ADR-003-installation-provenance.md +15 -0
  55. package/docs/ADR-004-checkpoint-resumption.md +23 -0
  56. package/docs/ADR-005-mission-context.md +11 -0
  57. package/docs/ADR-006-opt-in-native-pilot.md +15 -0
  58. package/docs/BMAD-COMPARISON.md +34 -0
  59. package/docs/CODEX-ADAPTER-VALIDATION.md +20 -0
  60. package/docs/COMPARISON-ITERATION.md +25 -0
  61. package/docs/DOCTOR.md +23 -0
  62. package/docs/EVALUATION.md +39 -0
  63. package/docs/EVOLUTION.md +34 -0
  64. package/docs/GREENFIELD-VALIDATION.md +36 -0
  65. package/docs/MILESTONES.md +39 -0
  66. package/docs/MISSIONS.md +47 -0
  67. package/docs/NATIVE-PILOT-RESULTS.md +22 -0
  68. package/docs/NATIVE-PILOT.md +17 -0
  69. package/docs/ORCHESTRATION.md +11 -0
  70. package/docs/RC2-VALIDATION.md +50 -0
  71. package/docs/RELEASE-0.1.0.md +60 -0
  72. package/docs/RELEASE-0.2.0.md +37 -0
  73. package/docs/RELEASE-CHECKLIST.md +13 -0
  74. package/docs/RELEASE-NOTES-0.1.0-rc.2.md +32 -0
  75. package/docs/RELEASE-NOTES-0.1.0.md +11 -0
  76. package/docs/RESUMPTION.md +87 -0
  77. package/docs/ROADMAP.md +39 -0
  78. package/docs/STACK-PROFILES.md +9 -0
  79. package/docs/TROUBLESHOOTING.md +16 -0
  80. package/docs/UPDATES.md +30 -0
  81. package/docs/VISUAL-CREATION-PROPOSAL.md +21 -0
  82. package/docs/VISUAL-WORKFLOW.md +75 -0
  83. package/docs/images/devmethod-delivery.svg +10 -0
  84. package/docs/images/devmethod-flow.svg +13 -0
  85. package/docs/media/from-zero/README.md +17 -0
  86. package/docs/media/from-zero/devmethod-demo.fr.srt +59 -0
  87. package/docs/media/from-zero/scenes.json +50 -0
  88. package/docs/media/full-chain-4k/README.md +37 -0
  89. package/docs/media/full-chain-4k/devmethod-chaine-complete.fr.srt +435 -0
  90. package/docs/media/full-chain-4k/scenes.json +173 -0
  91. package/docs/media/full-chain-4k/transcripts.fr.md +315 -0
  92. package/docs/media/visual-chain/README.md +44 -0
  93. package/docs/media/visual-chain/assets/add-book-v1.png +0 -0
  94. package/docs/media/visual-chain/assets/completed-v1.png +0 -0
  95. package/docs/media/visual-chain/devmethod-du-besoin-au-produit.fr.srt +147 -0
  96. package/docs/media/visual-chain/execution.fr.md +30 -0
  97. package/docs/media/visual-chain/image-prompts.json +9 -0
  98. package/docs/media/visual-chain/reference-hashes.json +5 -0
  99. package/docs/media/visual-chain/scenes.json +232 -0
  100. package/docs/media/visual-chain/video-preview.jpg +0 -0
  101. package/docs/missions/visual-workflow.md +46 -0
  102. package/evaluation/COMPARISONS.md +33 -0
  103. package/evaluation/README.md +35 -0
  104. package/evaluation/candidate-setup.json +33 -0
  105. package/evaluation/comparison-v2-readiness.json +18 -0
  106. package/evaluation/comparison-v2-results.json +58 -0
  107. package/evaluation/fixtures/dependency/CONTRIBUTING.md +3 -0
  108. package/evaluation/fixtures/dependency/TICKET.md +3 -0
  109. package/evaluation/fixtures/dependency/acceptance.test.mjs +4 -0
  110. package/evaluation/fixtures/dependency/api.mjs +1 -0
  111. package/evaluation/fixtures/failing-verification/CONTRIBUTING.md +3 -0
  112. package/evaluation/fixtures/failing-verification/acceptance.test.mjs +4 -0
  113. package/evaluation/fixtures/failing-verification/total.mjs +1 -0
  114. package/evaluation/fixtures/stale-handoff/CHECKPOINT.md +3 -0
  115. package/evaluation/fixtures/stale-handoff/CONTRACT.md +3 -0
  116. package/evaluation/fixtures/stale-handoff/CONTRIBUTING.md +3 -0
  117. package/evaluation/fixtures/stale-handoff/acceptance.test.mjs +4 -0
  118. package/evaluation/fixtures/stale-handoff/label.mjs +1 -0
  119. package/evaluation/fixtures/update-customization/README.md +13 -0
  120. package/evaluation/fixtures.json +85 -0
  121. package/evaluation/greenfield/acceptance.test.mjs +57 -0
  122. package/evaluation/greenfield/security.test.mjs +28 -0
  123. package/evaluation/native-pilot-results.json +155 -0
  124. package/examples/README.md +36 -0
  125. package/examples/bugfix/acceptance.test.mjs +17 -0
  126. package/examples/bugfix/page-size.mjs +4 -0
  127. package/examples/clair-from-zero/AGENT-EVALUATION.md +15 -0
  128. package/examples/clair-from-zero/MISSION.md +41 -0
  129. package/examples/clair-from-zero/README.md +24 -0
  130. package/examples/clair-from-zero/app/app.mjs +69 -0
  131. package/examples/clair-from-zero/app/domain.mjs +30 -0
  132. package/examples/clair-from-zero/app/index.html +20 -0
  133. package/examples/clair-from-zero/app/storage.mjs +4 -0
  134. package/examples/clair-from-zero/app/styles.css +128 -0
  135. package/examples/clair-from-zero/browser-check.cjs +2 -0
  136. package/examples/clair-from-zero/tests/domain.test.mjs +37 -0
  137. package/examples/fullstack/CONTRACT.md +23 -0
  138. package/examples/fullstack/DEPENDENCIES.md +18 -0
  139. package/examples/fullstack/README.md +100 -0
  140. package/examples/fullstack/api/application/tasks.ts +13 -0
  141. package/examples/fullstack/api/domain/task.ts +12 -0
  142. package/examples/fullstack/api/http.ts +26 -0
  143. package/examples/fullstack/api/infrastructure/migrate.ts +15 -0
  144. package/examples/fullstack/api/infrastructure/postgres.ts +18 -0
  145. package/examples/fullstack/api/infrastructure/schema.ts +6 -0
  146. package/examples/fullstack/api/main.ts +17 -0
  147. package/examples/fullstack/api/tsconfig.json +17 -0
  148. package/examples/fullstack/compose.yaml +14 -0
  149. package/examples/fullstack/migrations/0000_tasks.sql +5 -0
  150. package/examples/fullstack/migrations/meta/_journal.json +13 -0
  151. package/examples/fullstack/mission.json +34 -0
  152. package/examples/fullstack/package-lock.json +2661 -0
  153. package/examples/fullstack/package.json +45 -0
  154. package/examples/fullstack/tests/e2e.test.cjs +54 -0
  155. package/examples/fullstack/tests/http.test.cjs +33 -0
  156. package/examples/fullstack/tests/postgres.test.cjs +29 -0
  157. package/examples/fullstack/tests/unit.test.cjs +20 -0
  158. package/examples/fullstack/tests/web-model.test.cjs +23 -0
  159. package/examples/fullstack/web/app/layout.tsx +4 -0
  160. package/examples/fullstack/web/app/page.tsx +11 -0
  161. package/examples/fullstack/web/features/tasks/components/task-list.tsx +11 -0
  162. package/examples/fullstack/web/features/tasks/hooks/use-task-filter.ts +7 -0
  163. package/examples/fullstack/web/features/tasks/model/tasks.ts +12 -0
  164. package/examples/fullstack/web/features/tasks/server/list-tasks.ts +8 -0
  165. package/examples/fullstack/web/next.config.mjs +2 -0
  166. package/examples/fullstack/web/tsconfig.json +36 -0
  167. package/examples/mission/CONTRACT.md +3 -0
  168. package/examples/mission/mission.json +18 -0
  169. package/examples/mission/plan.json +11 -0
  170. package/examples/pocket-tasks/BRIEF.md +21 -0
  171. package/examples/pocket-tasks/PROJECT_PROFILE.md +19 -0
  172. package/examples/pocket-tasks/README.md +43 -0
  173. package/examples/pocket-tasks/docs/ADR-001.md +7 -0
  174. package/examples/pocket-tasks/docs/HANDOFF.md +16 -0
  175. package/examples/pocket-tasks/docs/MISSION.md +15 -0
  176. package/examples/pocket-tasks/docs/VERIFICATION.md +58 -0
  177. package/examples/pocket-tasks/package.json +1 -0
  178. package/examples/pocket-tasks/public/app.js +84 -0
  179. package/examples/pocket-tasks/public/index.html +12 -0
  180. package/examples/pocket-tasks/public/styles.css +1 -0
  181. package/examples/pocket-tasks/server.mjs +79 -0
  182. package/examples/pocket-tasks/src/domain.mjs +31 -0
  183. package/examples/pocket-tasks/src/store.mjs +49 -0
  184. package/examples/pocket-tasks/tests/app.test.mjs +132 -0
  185. package/examples/pocket-tasks/tests/ui-errors.test.mjs +49 -0
  186. package/examples/visual-pilot/README.md +11 -0
  187. package/examples/visual-pilot/app/app.js +10 -0
  188. package/examples/visual-pilot/app/index.html +1 -0
  189. package/examples/visual-pilot/app/reference.png +0 -0
  190. package/examples/visual-pilot/app/style.css +3 -0
  191. package/examples/visual-pilot/browser-check.cjs +3 -0
  192. package/examples/visual-pilot/desktop-actual.png +0 -0
  193. package/examples/visual-pilot/directions-prompt.txt +1 -0
  194. package/examples/visual-pilot/directions-v1.png +0 -0
  195. package/examples/visual-pilot/editorial-mockup-prompt.txt +1 -0
  196. package/examples/visual-pilot/editorial-mockup-v1.png +0 -0
  197. package/examples/visual-pilot/mismatch-probe.png +0 -0
  198. package/examples/visual-pilot/mobile-actual.png +0 -0
  199. package/examples/visual-pilot/quick-filter/AGENT-RESULT.md +25 -0
  200. package/examples/visual-pilot/quick-filter/baseline.log +42 -0
  201. package/examples/visual-pilot/quick-filter/filter.mjs +3 -0
  202. package/examples/visual-pilot/quick-filter/filter.test.mjs +9 -0
  203. package/package.json +15 -3
  204. package/scripts/check-codex-git.mjs +20 -0
  205. package/scripts/check-docs.mjs +14 -0
  206. package/scripts/check-native-sandbox.mjs +11 -0
  207. package/scripts/comparison-readiness.mjs +26 -0
  208. package/scripts/comparison.mjs +42 -0
  209. package/scripts/evaluation.mjs +64 -0
  210. package/scripts/fullstack-mission.mjs +30 -0
  211. package/scripts/hosts/checks.mjs +13 -0
  212. package/scripts/hosts/codex-meter.mjs +17 -0
  213. package/scripts/hosts/codex-rpc.mjs +44 -0
  214. package/scripts/hosts/codex-task.mjs +29 -0
  215. package/scripts/hosts/codex.mjs +28 -0
  216. package/scripts/media/demo-actions.cjs +16 -0
  217. package/scripts/media/encode-demo.py +22 -0
  218. package/scripts/media/record-demo.cjs +30 -0
  219. package/scripts/media/visual-short/check.cjs +1 -0
  220. package/scripts/media/visual-short/encode.py +16 -0
  221. package/scripts/media/visual-short/record.cjs +4 -0
  222. package/scripts/media/visual-short/story.py +34 -0
  223. package/scripts/native-host.mjs +55 -0
  224. package/scripts/package-smoke.mjs +64 -0
  225. package/scripts/probe-codex-children.mjs +41 -0
  226. package/scripts/run-comparison-v2.mjs +45 -0
  227. package/scripts/run-native-pilot.mjs +76 -0
  228. package/scripts/summarize-native-pilot.mjs +7 -0
@@ -0,0 +1,128 @@
1
+ :root{--paper:#f7f3eb;--ink:#292923;--muted:#726e63;--rust:#97472f;--line:#d9d3c7;--panel:#eee9df;--serif:Georgia,'Times New Roman',serif;--sans:Arial,Helvetica,sans-serif}
2
+ *{box-sizing:border-box}
3
+ body{margin:0;background:var(--paper);color:var(--ink);font-family:var(--sans);font-size:14px}
4
+ button,input,textarea{font:inherit}
5
+ button,a,input,textarea{-webkit-tap-highlight-color:transparent}
6
+ button{cursor:pointer}
7
+ button{color:inherit}
8
+ button:focus-visible,a:focus-visible,input:focus-visible,textarea:focus-visible,[tabindex]:focus-visible{outline:3px solid var(--rust);outline-offset:5px}
9
+ button:hover{filter:brightness(.86)}
10
+ [hidden]{display:none!important}
11
+ .page{max-width:1480px;padding:0 64px;margin:auto}
12
+ .masthead{height:100px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--line);gap:24px}
13
+ .brand{font-family:var(--serif);font-size:44px;letter-spacing:-3px;color:var(--ink);text-decoration:none}
14
+ .brand-dot{color:var(--rust);font-size:28px;display:inline-block;vertical-align:top;margin:5px 0 0 7px}
15
+ .masthead-note,.edition,.eyebrow,.hero-aside>span,.footer-note{font-size:10px;letter-spacing:1.8px;font-weight:600}
16
+ .edition{color:var(--muted)}
17
+ .hero{display:flex;align-items:center;justify-content:space-between;padding:57px 0 50px}
18
+ .eyebrow{color:var(--rust);margin:0 0 23px}
19
+ .hero h1{font:normal clamp(58px,5.7vw,82px)/1.04 var(--serif);letter-spacing:-3.3px;margin:0}
20
+ .hero h1 em{font-weight:normal;color:var(--rust)}
21
+ .intro{font-size:15px;line-height:1.7;color:var(--muted);margin:23px 0 0}
22
+ .hero-aside{width:28%;text-align:center;padding-top:8px}
23
+ .hero-aside p{font:italic 24px/1.4 var(--serif);margin:18px 0;color:var(--muted)}
24
+ .hero-aside>span{font-size:9px;color:var(--muted)}
25
+ .sun{position:relative;width:90px;height:90px;margin:0 auto}
26
+ .sun i{position:absolute;top:44px;left:0;width:90px;height:1px;background:var(--rust)}
27
+ .sun i:nth-child(2){transform:rotate(45deg)}
28
+ .sun i:nth-child(3){transform:rotate(90deg)}
29
+ .sun i:nth-child(4){transform:rotate(135deg)}
30
+ .sun:after{content:'';position:absolute;inset:30px;border:1px solid var(--rust);border-radius:50%;background:var(--paper)}
31
+ .workspace{display:grid;grid-template-columns:minmax(0,1.8fr) minmax(290px,1fr);gap:48px;border-top:1px solid var(--ink);padding-top:32px;padding-bottom:55px}
32
+ .section-heading{display:flex;justify-content:space-between;align-items:center;margin-bottom:25px}
33
+ .section-heading h2{font:normal 30px var(--serif);letter-spacing:-.6px;margin:0}
34
+ .count{font:normal 27px var(--serif);color:var(--muted)}
35
+ .list-toolbar{display:flex;justify-content:space-between;align-items:center;gap:16px}
36
+ .filters{display:flex;gap:19px}
37
+ .filters button{font-size:12px;border:0;background:transparent;padding:9px 0;color:var(--muted);border-bottom:2px solid transparent;white-space:nowrap}
38
+ .filters button[aria-pressed=true]{color:var(--rust);border-bottom-color:var(--rust);font-weight:bold}
39
+ .filters span{font-size:10px;margin-left:3px}
40
+ .progress-label{font-size:10px;color:var(--muted);white-space:nowrap}
41
+ .progress-track{height:2px;background:var(--line);margin:10px 0 0}
42
+ .progress-track div{height:100%;background:var(--rust);transition:width .2s ease}
43
+ .empty-state{min-height:285px;display:flex;align-items:center;justify-content:center;flex-direction:column;text-align:center;padding:32px 10px}
44
+ .empty-mark{font-size:43px;color:var(--rust);margin:0 0 15px}
45
+ .empty-state h3{font:normal 25px var(--serif);margin:0 0 12px;letter-spacing:-.3px}
46
+ .empty-state p{line-height:1.7;color:var(--muted);font-size:13px;margin:0}
47
+ .text-button{border:0;background:none;color:var(--rust);font-size:12px;border-bottom:1px solid var(--rust);padding:0 0 5px;margin-top:22px}
48
+ .text-button span{margin-left:14px}
49
+ .list-foot{display:flex;justify-content:space-between;gap:15px;border-top:1px solid var(--line);padding-top:19px;font-size:10px;color:var(--muted)}
50
+ .list-foot button{border:0;padding:0 0 2px;background:transparent;color:var(--rust);font-size:10px;border-bottom:1px solid var(--rust)}
51
+ .composer{background:var(--panel);padding:29px 29px 23px;align-self:start}
52
+ .composer .eyebrow{font-size:9px;margin-bottom:16px}
53
+ .composer h2{font:normal 29px/1.15 var(--serif);letter-spacing:-.7px;margin:0 0 27px}
54
+ .composer h2 em{color:var(--rust)}
55
+ label{display:block;font-size:11px;font-weight:bold;margin:0 0 10px}
56
+ label>span:not(.optional){color:var(--rust)}
57
+ .optional{font-weight:normal;color:var(--muted)}
58
+ input,textarea{width:100%;border:1px solid #c9c1b2;border-radius:0;color:var(--ink);background:#faf7f1;padding:13px 12px;font-size:12px;line-height:1.5}
59
+ input{margin-bottom:20px}
60
+ textarea{resize:vertical;min-height:91px}
61
+ input::placeholder,textarea::placeholder{color:#817b6f}
62
+ input[aria-invalid=true]{border-color:var(--rust)}
63
+ .add-button{margin-top:17px;display:flex;align-items:center;justify-content:space-between;background:var(--rust);color:white;border:0;padding:14px 17px;width:100%;font-size:12px}
64
+ .add-button span{font-size:21px;line-height:1}
65
+ .form-tip{font-size:11px;line-height:1.6;color:var(--muted);margin:20px 0 0}
66
+ .form-error{color:#8a2c20;font-size:12px;line-height:1.5;margin:12px 0 0}
67
+ #task-list{list-style:none;padding:0;margin:0}
68
+ .task{display:grid;grid-template-columns:24px minmax(0,1fr) 30px;align-items:start;gap:14px;border-bottom:1px solid var(--line);padding:23px 0}
69
+ .task:last-child{border-bottom:0}
70
+ .task-check{width:22px;height:22px;padding:0;border:1px solid #9c9586;background:transparent;margin-top:2px;display:grid;place-items:center}
71
+ .task.done .task-check{background:var(--rust);border-color:var(--rust);color:white}
72
+ .task h3{font:normal 21px/1.3 var(--serif);margin:0;overflow-wrap:anywhere}
73
+ .task p{margin:8px 0 0;color:var(--muted);font-size:12px;line-height:1.6;white-space:pre-wrap;overflow-wrap:anywhere}
74
+ .task.done h3{text-decoration:line-through;color:var(--muted)}
75
+ .task-delete{background:none;border:0;padding:3px;color:var(--muted);width:30px;height:30px}
76
+ .task-delete svg{width:16px;height:18px;fill:none;stroke:currentColor;stroke-width:1.4}
77
+ .task-state{font-size:9px;color:var(--rust);letter-spacing:1px;display:block;margin-top:9px}
78
+ .undo-bar{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:25px;max-width:calc(100% - 32px);width:max-content;background:var(--ink);color:var(--paper);padding:15px 20px;box-shadow:0 8px 25px #29292325;font-size:12px}
79
+ .undo-bar button{background:none;color:var(--paper);border:0;padding:5px;text-decoration:underline}
80
+ .undo-bar #dismiss-undo{text-decoration:none;font-size:22px;padding:0 3px}
81
+ .storage-alert{border:1px solid var(--rust);padding:18px;margin-bottom:24px;color:var(--rust);font-size:13px;line-height:1.5}
82
+ .storage-alert p{margin:0 0 12px}
83
+ .storage-alert button{background:none;border:1px solid var(--rust);color:var(--rust);padding:8px 12px}
84
+ footer{border-top:1px solid var(--line);display:flex;align-items:center;gap:24px;padding:25px 0 30px;color:var(--muted)}
85
+ .footer-brand{font:normal 27px var(--serif);letter-spacing:-1.5px}
86
+ footer p{font-size:10px;line-height:1.6;margin:0}
87
+ .footer-note{margin-left:auto;font-size:9px}
88
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap}
89
+ .skip{position:fixed;top:-100px;left:20px;background:var(--ink);color:white;padding:12px;z-index:10}
90
+ .skip:focus{top:12px}
91
+ .noscript{padding:20px;color:var(--rust)}
92
+ @media(min-width:1500px){.hero{padding-top:70px;padding-bottom:65px}
93
+ }
94
+ @media(max-width:1000px){.page{padding:0 35px}
95
+ .workspace{gap:28px;grid-template-columns:minmax(0,1.4fr) minmax(275px,1fr)}
96
+ .progress-label{display:none}
97
+ .composer{padding:25px 22px}
98
+ .masthead-note{display:none}
99
+ }
100
+ @media(max-width:700px){.page{padding:0 22px}
101
+ .masthead{height:80px}
102
+ .brand{font-size:39px}
103
+ .edition{font-size:8px;letter-spacing:1px}
104
+ .hero{padding:35px 0}
105
+ .hero h1{font-size:55px;letter-spacing:-2.5px}
106
+ .hero .eyebrow{font-size:9px;margin-bottom:19px}
107
+ .intro{font-size:13px;line-height:1.7;margin-top:20px}
108
+ .hero-aside{display:none}
109
+ .workspace{display:flex;flex-direction:column;gap:32px;padding-top:24px;padding-bottom:35px}
110
+ .composer{order:-1;width:100%;padding:24px}
111
+ .composer h2{font-size:27px}
112
+ .composer h2 br{display:none}
113
+ .composer .eyebrow{margin-bottom:10px}
114
+ .composer h2{margin-bottom:24px}
115
+ .form-tip{display:none}
116
+ .section-heading{margin-bottom:16px}
117
+ .section-heading h2{font-size:28px}
118
+ .empty-state{min-height:280px}
119
+ .empty-state h3{font-size:23px}
120
+ .list-foot{font-size:9px;line-height:1.7}
121
+ .list-foot button{font-size:10px;align-self:start}
122
+ .footer-note{display:none}
123
+ footer{gap:20px;padding-bottom:90px}
124
+ .task h3{font-size:20px}
125
+ .undo-bar{gap:13px;font-size:11px;padding:12px 15px}
126
+ }
127
+ @media(prefers-reduced-motion:reduce){*{transition:none!important;scroll-behavior:auto!important}
128
+ }
@@ -0,0 +1,2 @@
1
+ const {chromium}=require(process.env.PLAYWRIGHT_MODULE || 'playwright');const assert=require('node:assert/strict');const fs=require('node:fs');
2
+ (async()=>{const browser=await chromium.launch({channel:'chrome',headless:true});const context=await browser.newContext({viewport:{width:1280,height:900},reducedMotion:'reduce'});const p=await context.newPage();const errors=[];p.on('pageerror',e=>errors.push(e.message));await p.goto('http://127.0.0.1:8766');assert.equal(await p.locator('.task').count(),0);await p.locator('.add-button').click();assert.equal(await p.locator('#form-error').isVisible(),true);await p.locator('#task-title').fill('Vérifier le parcours');await p.locator('#task-note').fill('Une note de démonstration.');await p.locator('.add-button').click();assert.equal(await p.locator('.task').count(),1);await p.locator('.task-check').first().click();await p.locator('[data-filter="active"]').click();assert.equal(await p.locator('.task').count(),0);await p.locator('[data-filter="done"]').click();assert.equal(await p.locator('.task').count(),1);await p.reload();await p.locator('[data-filter="all"]').click();assert.equal(await p.locator('.task').count(),1);await p.locator('.task-delete').first().click();assert.equal(await p.locator('.task').count(),0);await p.locator('#undo-button').click();assert.equal(await p.locator('.task').count(),1);await p.locator('#demo-button').click();assert.ok(await p.locator('.task').count()>=1);fs.mkdirSync('docs/media/from-zero/captures',{recursive:true});await p.screenshot({path:'docs/media/from-zero/captures/clair-desktop.png',fullPage:true});await p.setViewportSize({width:390,height:844});await p.screenshot({path:'docs/media/from-zero/captures/clair-mobile.png',fullPage:true});assert.equal(await p.evaluate(()=>document.documentElement.scrollWidth<=innerWidth),true);await p.locator('#task-title').focus();await p.keyboard.type('Ajout clavier');await p.keyboard.press('Tab');await p.keyboard.press('Tab');assert.equal(await p.locator('.add-button').evaluate(e=>e===document.activeElement),true);await p.keyboard.press('Enter');assert.ok(await p.locator('.task').filter({hasText:'Ajout clavier'}).count());const blocked=await browser.newContext();await blocked.addInitScript(()=>{Storage.prototype.setItem=()=>{throw new DOMException('Blocked','QuotaExceededError');};});const b=await blocked.newPage();await b.goto('http://127.0.0.1:8766');await b.locator('#task-title').fill('Conserver ma saisie');await b.locator('.add-button').click();assert.equal(await b.locator('#storage-alert').isVisible(),true);assert.equal(await b.locator('.task').count(),1);assert.deepEqual(errors,[]);console.log('PASS: empty state, blank validation, add/note, complete/filter, reload persistence, delete/undo, demo, keyboard submit, mobile overflow, storage failure retains task and alerts.');await browser.close();})();
@@ -0,0 +1,37 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import {createTask,toggleTask,removeTask,restoreTask,filterTasks,decodeTasks,encodeTasks} from '../app/domain.mjs';
4
+ import {readTasks,writeTasks,STORAGE_KEY} from '../app/storage.mjs';
5
+ const a = createTask('Lire','Une note','a'), b = createTask('Marcher','','b');
6
+ test('creation trims inputs; rejects blank and over-limit values', () => {
7
+ assert.deepEqual(createTask(' Titre ',' Note ','id'),{id:'id',title:'Titre',note:'Note',done:false});
8
+ for (const title of ['',' ','\n\t','x'.repeat(161)]) assert.throws(() => createTask(title,'','id'));
9
+ assert.throws(() => createTask('Titre','x'.repeat(501),'id'));
10
+ assert.equal(createTask('x'.repeat(160),'x'.repeat(500),'id').note.length,500);
11
+ });
12
+ test('toggle is immutable and filters preserve order', () => {
13
+ const initial = [a,b], changed = toggleTask(initial,'a');
14
+ assert.equal(a.done,false); assert.equal(changed[0].done,true);
15
+ assert.deepEqual(filterTasks(changed,'done'),[changed[0]]); assert.deepEqual(filterTasks(changed,'active'),[b]);
16
+ assert.deepEqual(filterTasks(changed,'all'),changed); assert.deepEqual(toggleTask(changed,'a'),initial);
17
+ });
18
+ test('delete and undo restore original position even after adding another task', () => {
19
+ const initial=[a,b], {tasks,removed}=removeTask(initial,'a'), c=createTask('Écrire','','c');
20
+ assert.deepEqual(tasks,[b]); assert.deepEqual(restoreTask([...tasks,c],removed),[a,b,c]);
21
+ assert.deepEqual(restoreTask(initial,removed),initial); assert.equal(removeTask(initial,'missing').removed,null);
22
+ assert.deepEqual(restoreTask([],null),[]);
23
+ });
24
+ test('empty persistence and Unicode round-trip; HTML remains literal text', () => {
25
+ assert.deepEqual(decodeTasks(null),[]);
26
+ const task=createTask('<img src=x onerror=alert(1)>','Été\n日本語','x');
27
+ assert.deepEqual(decodeTasks(encodeTasks([task])),[task]);
28
+ });
29
+ test('untrusted storage schema rejects invalid data and duplicate IDs', () => {
30
+ for(const raw of ['not JSON','null','{}','[]',JSON.stringify({version:2,tasks:[]}),JSON.stringify({version:1,tasks:[a,a]}),JSON.stringify({version:1,tasks:[{...a,done:'true'}]}),JSON.stringify({version:1,tasks:[{...a,title:' '}]}),JSON.stringify({version:1,tasks:[{...a,note:null}]})]) assert.throws(()=>decodeTasks(raw));
31
+ });
32
+ test('storage adapter uses versioned key; failures propagate rather than fake success', () => {
33
+ const map=new Map(), storage={getItem:key=>map.get(key)??null,setItem:(key,value)=>map.set(key,value)};
34
+ writeTasks(storage,[a,b]); assert.ok(map.has(STORAGE_KEY)); assert.deepEqual(readTasks(storage),[a,b]);
35
+ assert.throws(()=>writeTasks({setItem(){throw new Error('quota');}},[a]),/quota/);
36
+ assert.throws(()=>readTasks({getItem(){throw new Error('blocked');}}),/blocked/);
37
+ });
@@ -0,0 +1,23 @@
1
+ # Task slice contract
2
+
3
+ Status: implemented fixture contract, owned by the fullstack slice. This fictional local example demonstrates one feature; it is not a production service architecture recommendation.
4
+
5
+ | Boundary | Observable contract |
6
+ |---|---|
7
+ | `POST /tasks` | JSON object with only `title`; normalize with JavaScript `trim`; require 1–120 UTF-16 code units and reject U+0000 anywhere in the title; generate UUID; return `201` and `{ id, title }` after insertion |
8
+ | Invalid creation | `400` for missing/non-string/empty/too-long title or U+0000, array body or unknown fields; no persistence call |
9
+ | `GET /tasks` | `200` with at most 100 `{ id, title }` rows sorted by title then ID using PostgreSQL ordering; no cursor/pagination |
10
+ | Infrastructure failure | `500` with generic Nest response; driver connection details are not exposed to callers |
11
+ | Next initial read | Server-only HTTP request with no cache and 3-second timeout; validate shape and omit unknown fields before sending props to the client |
12
+ | UI filter | Case-insensitive substring search in the currently loaded rows; trims search text; no mutation or fetch |
13
+ | API unavailable | Render a readable alert; this demonstration page returns HTML successfully and is not a health endpoint |
14
+
15
+ No authentication, authorization, tenant separation, updates, deletion, business uniqueness, idempotency keys, events or production deployment are in scope. The API, web commands and database bind to loopback. A repeated POST creates another task; do not automatically retry an ambiguous mutation. There is no third-party call or paid operation.
16
+
17
+ The domain owns title normalization and validation, including rejection of U+0000 before persistence because PostgreSQL text cannot store that character. `Tasks` is the use case and owns the consumer-defined `TaskStore` port. Nest handles transport, `PostgresTasks` handles Drizzle, and composition assembles them. The client model is a separately validated HTTP representation; no client import reaches API internals. `useTaskFilter` owns only query state and calls a pure transformation.
18
+
19
+ Persistence uses one SQL INSERT per creation, already atomic in PostgreSQL. UUID primary-key uniqueness, SQL nonempty-space checks and a varchar length limit provide database safeguards. JavaScript trims more whitespace and counts UTF-16 code units differently from PostgreSQL character length; the application remains authoritative for the full title rule. No cross-row invariant requires a multi-statement transaction in this scope.
20
+
21
+ Migration `0000_tasks.sql` creates the table; Drizzle's journal records application. Apply with `npm run db:migrate` against an isolated database. Replaying the migrator must not recreate the table. Do not edit an applied migration; add a forward migration. There is no tested production rollback/backup/restore policy. Destroying the local Compose fixture is the documented reset, never a production recovery procedure.
22
+
23
+ Acceptance evidence is mapped in [README](README.md). Code, schema/migration, versions, environment or this contract changing invalidates the affected result. A passing HTTP test with an in-memory store does not prove PostgreSQL behavior; the real database and end-to-end suites establish their own narrower claims.
@@ -0,0 +1,18 @@
1
+ # Fixture dependency security decision
2
+
3
+ On 2026-09-13, `npm audit --json` reported three high-severity dependency entries: `multer`, `@nestjs/platform-express`, and `@nestjs/core`. The Nest entries propagated the vulnerable transitive multer dependency; they were not three independent application flaws. Nest platform-express 12.0.1 pins multer 2.2.0 in its published npm metadata.
4
+
5
+ The following maintainer advisories identify multer 2.3.0 as the patched release:
6
+
7
+ - [Crafted multipart field names](https://github.com/expressjs/multer/security/advisories/GHSA-wc9g-mqfw-jrwm).
8
+ - [File descriptor leak on aborted uploads](https://github.com/expressjs/multer/security/advisories/GHSA-qfvm-cv95-jqjf).
9
+ - [Async fileFilter size-limit bypass](https://github.com/expressjs/multer/security/advisories/GHSA-qvfw-j98x-7q72).
10
+ - [Oversized array indices in field names](https://github.com/expressjs/multer/security/advisories/GHSA-535w-7cp7-47q4).
11
+
12
+ Sources were consulted on 2026-09-13, together with `npm view @nestjs/platform-express@12.0.1 dependencies --json` and `npm view multer@2.3.0 engines version --json`. The published patched package supports this fixture's runtime range.
13
+
14
+ Applicability assessment: the fixture installs the adapter but configures no Multer middleware or Nest file-upload interceptor. Its task controller expects JSON; the HTTP suite checks that a multipart request is rejected before persistence. The current routes therefore do not activate the affected upload parser, based on source inspection and this boundary check. This is not a claim that the original dependency was safe for an application adding uploads.
15
+
16
+ The fixture applies a narrow npm override from `@nestjs/platform-express` to `multer: 2.3.0`, preserving the Nest/React/Next major versions. The lockfile records the patched resolution. Do not use an automatic forced framework downgrade suggested by audit. Remove the override when the deliberately selected Nest adapter version natively resolves a patched compatible multer; then regenerate the lockfile and rerun the relevant checks.
17
+
18
+ Validation for this change: a clean lockfile install, npm audit, domain/HTTP/web-model tests and the production Next/Nest/PostgreSQL end-to-end suite. Their actual results belong to the commit review and mission evidence. This fixture has no upload feature, so it does not claim validation of file upload behavior or a complete security audit. Audit results are time-bound and do not prove absence of all vulnerabilities.
@@ -0,0 +1,100 @@
1
+ # Next.js / NestJS / PostgreSQL fixture
2
+
3
+ A fictional local task list demonstrates a complete server-rendered read path and a validated creation API. It includes an actual Next application, Nest HTTP server, strict TypeScript, a Drizzle adapter and a PostgreSQL migration. Dependencies and tests are isolated from DevMethod's offline CLI.
4
+
5
+ Read [the contract](CONTRACT.md) before changing the slice. The installed dependency versions are pinned in [package.json](package.json) and [package-lock.json](package-lock.json); PostgreSQL's image is pinned by digest in [compose.yaml](compose.yaml). Use the existing versions when evaluating a mission rather than installing current releases.
6
+
7
+ ## Run from a clean checkout
8
+
9
+ Prerequisites: Node.js 22+ and npm, Docker with Compose and permission to bind local ports. The recorded run used Node 23.10.0, npm 10.9.2 and Docker 27.5.1 on macOS. That is observed fixture compatibility, not a recommendation to deploy that Node version. Install with a supported project runtime and record your own result.
10
+
11
+ From the DevMethod repository:
12
+
13
+ ```sh
14
+ cd examples/fullstack
15
+ npm ci --ignore-scripts
16
+ export NEXT_TELEMETRY_DISABLED=1
17
+ npm test
18
+ docker compose -p devmethod-fixture up -d --wait
19
+ export DATABASE_URL=postgresql://fixture:fixture-local-only@127.0.0.1:55439/tasks
20
+ npm run test:db
21
+ npm run test:e2e
22
+ ```
23
+
24
+ The password above is a deliberately public, local-only fixture credential, not a secret or production configuration. Use only a disposable database: these suites apply migrations and insert/delete their own test rows. `test:db` and `test:e2e` fail when `DATABASE_URL` is missing, rather than reporting a skipped test as passed.
25
+
26
+ `npm test` compiles the API and runs six domain/use-case/HTTP/web-model tests. `test:db` verifies real migrations, replay, durability and database constraints. `test:e2e` builds API and Next production output, starts both HTTP servers on ephemeral loopback ports, creates a task through Nest, verifies its title in Next HTML, and verifies the API-unavailable rendering. It cleans up its task and HTTP processes.
27
+
28
+ To explore the feature after the tests, run in one terminal:
29
+
30
+ ```sh
31
+ npm run db:migrate
32
+ npm run start:api
33
+ ```
34
+
35
+ In a second terminal, still in this directory:
36
+
37
+ ```sh
38
+ export NEXT_TELEMETRY_DISABLED=1
39
+ npm run start:web
40
+ ```
41
+
42
+ In a third terminal create a fictional task and open the page:
43
+
44
+ ```sh
45
+ curl --fail-with-body -X POST http://127.0.0.1:3101/tasks \
46
+ -H 'content-type: application/json' -d '{"title":"Review the mission contract"}'
47
+ curl --fail http://127.0.0.1:3101/tasks
48
+ ```
49
+
50
+ Open [the local task list](http://127.0.0.1:3100), type `review` into the labeled filter and verify only matching tasks remain. Clear the input and verify the list returns. This browser/hydration protocol is **not run** in the recorded evidence. Initial HTML and the pure filter function are tested separately. Creation is deliberately through HTTP; there is no creation form.
51
+
52
+ Stop the foreground API/web processes with Ctrl-C. Remove only this disposable Compose project's container, anonymous data volume and network when finished:
53
+
54
+ ```sh
55
+ docker compose -p devmethod-fixture down -v
56
+ ```
57
+
58
+ ## Evidence and limits
59
+
60
+ Observed on 2026-09-13 for the candidate source tree committed with this example; use `git log -1 -- examples/fullstack` to resolve its introducing revision. These are automated fixture checks, not native Codex/Claude/Cursor behavioral evaluations or an independent design review.
61
+
62
+ | Acceptance criterion | Change | Executed verification | Result |
63
+ |---|---|---|---|
64
+ | Reject invalid titles before persistence | Domain and use case | `npm test` | passed |
65
+ | Preserve HTTP status/shape and hide infrastructure details | Nest controller | `npm test` | passed |
66
+ | Normalize filter and validate server data | Web model | `npm test` | passed |
67
+ | Apply migration once, replay safely, enforce SQL constraints and persist across connections | Drizzle adapter / SQL migration | `npm run test:db` | passed |
68
+ | Render a task created through the real API and database | Next server boundary | `npm run test:e2e` including production build | passed |
69
+ | Show a useful unavailable state | Next page | `npm run test:e2e` | passed |
70
+ | Browser hydration, keyboard filtering and visual quality | Client view/hook | Manual protocol above | not run |
71
+ | Production auth, deployment, transaction concurrency, restore | Outside slice | None | not run |
72
+
73
+ The first sandboxed HTTP run was blocked by loopback permissions and was rerun with local server access. An occupied initial database port was changed to 55439 without touching the existing service. Two test-harness issues (nested driver error comparison and cleanup order) were corrected before the final passing run; they were not product compatibility failures. Commands need network access for initial npm/image installation; the DevMethod CLI's offline contract does not extend to fetching example dependencies.
74
+
75
+ For a revision-specific evaluation, record the exact commit, command, environment, exit code and output in your mission evidence outside this source fixture. Re-run affected checks when source, contract, lockfile or environment changes. Never turn this historical table into fresh evidence for a later edit.
76
+
77
+ ## Troubleshooting
78
+
79
+ - Port 55439 occupied: set `FIXTURE_DB_PORT` to an unused local port before Compose, and change the port in `DATABASE_URL` to match. Do not stop another project's service.
80
+ - Database test blocked: verify Docker is running, Compose is healthy and the URL points to this disposable database. An unavailable database is not a passing persistence check.
81
+ - Page unavailable: verify API/database startup and `TASKS_API_URL` (defaults to `http://127.0.0.1:3101`). The web server never needs database credentials.
82
+ - Migration/schema mismatch: inspect migration history and create a forward migration; do not erase a real database to satisfy the fixture.
83
+ - Missing package dependencies: run `npm ci --ignore-scripts` here, not at the DevMethod root. `npm run build` emits ignored runtime files; neither installation nor tests deploy anything.
84
+
85
+ Official technical provenance is recorded in the [optional stack profiles](../../docs/STACK-PROFILES.md), consulted 2026-09-13. A linked third-party skill is not installed or validated by this example.
86
+
87
+ ## Mission, context, evidence and resumption
88
+
89
+ After installing this fixture's dependencies, run from the DevMethod repository root:
90
+
91
+ ```sh
92
+ node dist/cli.js mission --dest examples/fullstack --mission mission.json --json
93
+ node scripts/fullstack-mission.mjs
94
+ node dist/cli.js context-check --dest examples/fullstack --context evidence/context.json --json
95
+ node dist/cli.js resume --dest examples/fullstack --checkpoint evidence/checkpoint.json --json
96
+ ```
97
+
98
+ The walkthrough actually runs `npm test`, records its output and pins its inputs, and saves ignored metadata/evidence. A successful unchanged run reports ready. It records AC-TITLE only; database/e2e checks remain separate evidence in the table above. Tests are deterministic fixture checks, not a native agent transcript. The script never deploys, runs models or modifies a contract.
99
+
100
+ To exercise stale resumption in a disposable checkout, edit CONTRACT.md after the walkthrough and inspect resume again: source-changed and reverify must appear. Restore the exact bytes to return to the pinned content, or inspect the new contract and rerun affected verification. A branch change also requires reassessment; an unresolved dependency should remain in blockers until explicitly resolved. Completed scope sets nextAction to null and supplies no new task. Root mission/checkpoint tests reproduce these cases without mutating this example. If saving evidence itself changes Git status, restore the fixture's evidence/ ignore entry before recapturing context.
@@ -0,0 +1,13 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { Task, taskTitle } from '../domain/task';
3
+ export interface TaskStore {
4
+ list(): Promise<Task[]>;
5
+ insert(task: Task): Promise<Task>;
6
+ }
7
+ export class Tasks {
8
+ constructor(private readonly store: TaskStore) {}
9
+ list(): Promise<Task[]> { return this.store.list(); }
10
+ create(title: unknown): Promise<Task> {
11
+ return this.store.insert({ id: randomUUID(), title: taskTitle(title) });
12
+ }
13
+ }
@@ -0,0 +1,12 @@
1
+ export const MAX_TITLE_LENGTH = 120;
2
+ export interface Task { id: string; title: string }
3
+ export class InvalidTitle extends Error {}
4
+ export function taskTitle(value: unknown): string {
5
+ if (typeof value !== 'string') throw new InvalidTitle('Title must be a string.');
6
+ if (value.includes('\u0000')) throw new InvalidTitle('Title must not contain a null character.');
7
+ const title = value.trim();
8
+ if (!title || title.length > MAX_TITLE_LENGTH) {
9
+ throw new InvalidTitle(`Title must contain 1–${MAX_TITLE_LENGTH} characters after trimming.`);
10
+ }
11
+ return title;
12
+ }
@@ -0,0 +1,26 @@
1
+ import 'reflect-metadata';
2
+ import { BadRequestException, Body, Controller, Get, Inject, Module, Post } from '@nestjs/common';
3
+ import { NestFactory } from '@nestjs/core';
4
+ import { Tasks, TaskStore } from './application/tasks';
5
+ import { InvalidTitle } from './domain/task';
6
+ @Controller('tasks')
7
+ class TasksController {
8
+ constructor(@Inject(Tasks) private readonly tasks: Tasks) {}
9
+ @Get() list() { return this.tasks.list(); }
10
+ @Post() async create(@Body() body: unknown) {
11
+ if (!body || typeof body !== 'object' || Array.isArray(body) ||
12
+ Object.keys(body).some((key) => key !== 'title')) {
13
+ throw new BadRequestException('Expected an object containing only title.');
14
+ }
15
+ try { return await this.tasks.create((body as { title?: unknown }).title); }
16
+ catch (error) {
17
+ if (error instanceof InvalidTitle) throw new BadRequestException(error.message);
18
+ throw error;
19
+ }
20
+ }
21
+ }
22
+ export async function createApp(store: TaskStore) {
23
+ @Module({ controllers: [TasksController], providers: [{ provide: Tasks, useValue: new Tasks(store) }] })
24
+ class AppModule {}
25
+ return NestFactory.create(AppModule, { logger: false });
26
+ }
@@ -0,0 +1,15 @@
1
+ import { resolve } from 'node:path';
2
+ import { Pool } from 'pg';
3
+ import { drizzle } from 'drizzle-orm/node-postgres';
4
+ import { migrate } from 'drizzle-orm/node-postgres/migrator';
5
+ export async function migrateDatabase(pool: Pool): Promise<void> {
6
+ await migrate(drizzle(pool), { migrationsFolder: resolve(__dirname, '../../migrations') });
7
+ }
8
+ if (require.main === module) {
9
+ if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is required.');
10
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
11
+ migrateDatabase(pool).finally(() => pool.end()).catch(() => {
12
+ console.error('Migration failed. Inspect the database locally; connection details are not logged.');
13
+ process.exitCode = 1;
14
+ });
15
+ }
@@ -0,0 +1,18 @@
1
+ import { asc } from 'drizzle-orm';
2
+ import { drizzle } from 'drizzle-orm/node-postgres';
3
+ import { Pool } from 'pg';
4
+ import { TaskStore } from '../application/tasks';
5
+ import { Task } from '../domain/task';
6
+ import { tasks } from './schema';
7
+ export class PostgresTasks implements TaskStore {
8
+ private readonly db;
9
+ constructor(pool: Pool) { this.db = drizzle(pool); }
10
+ list(): Promise<Task[]> {
11
+ return this.db.select().from(tasks).orderBy(asc(tasks.title), asc(tasks.id)).limit(100);
12
+ }
13
+ async insert(task: Task): Promise<Task> {
14
+ const [created] = await this.db.insert(tasks).values(task).returning();
15
+ if (!created) throw new Error('Insert returned no task.');
16
+ return created;
17
+ }
18
+ }
@@ -0,0 +1,6 @@
1
+ import { check, pgTable, uuid, varchar } from 'drizzle-orm/pg-core';
2
+ import { sql } from 'drizzle-orm';
3
+ export const tasks = pgTable('tasks', {
4
+ id: uuid('id').primaryKey(),
5
+ title: varchar('title', { length: 120 }).notNull(),
6
+ }, (table) => [check('tasks_title_nonempty', sql`length(trim(${table.title})) > 0`)]);
@@ -0,0 +1,17 @@
1
+ import { Pool } from 'pg';
2
+ import { createApp } from './http';
3
+ import { PostgresTasks } from './infrastructure/postgres';
4
+ async function main() {
5
+ if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is required.');
6
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 3000 });
7
+ try {
8
+ await pool.query('select 1');
9
+ const app = await createApp(new PostgresTasks(pool));
10
+ try { await app.listen(Number(process.env.PORT ?? 3101), '127.0.0.1'); }
11
+ catch (error) { await app.close(); throw error; }
12
+ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
13
+ process.once(signal, () => { void app.close().finally(() => pool.end()); });
14
+ }
15
+ } catch (error) { await pool.end(); throw error; }
16
+ }
17
+ main().catch(() => { console.error('API startup failed. Check local database configuration.'); process.exitCode = 1; });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "CommonJS",
5
+ "moduleResolution": "Node",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "experimentalDecorators": true,
9
+ "emitDecoratorMetadata": true,
10
+ "outDir": "../build",
11
+ "rootDir": ".",
12
+ "skipLibCheck": true
13
+ },
14
+ "include": [
15
+ "**/*.ts"
16
+ ]
17
+ }
@@ -0,0 +1,14 @@
1
+ services:
2
+ db:
3
+ image: postgres:17.6-alpine@sha256:ef257d85f76e48da1c64832459b59fcaba1a4dac97bf5d7450c77753542eee94
4
+ environment:
5
+ POSTGRES_USER: fixture
6
+ POSTGRES_PASSWORD: fixture-local-only
7
+ POSTGRES_DB: tasks
8
+ ports:
9
+ - "127.0.0.1:${FIXTURE_DB_PORT:-55439}:5432"
10
+ healthcheck:
11
+ test: ["CMD-SHELL", "pg_isready -U fixture -d tasks"]
12
+ interval: 2s
13
+ timeout: 2s
14
+ retries: 20
@@ -0,0 +1,5 @@
1
+ CREATE TABLE "tasks" (
2
+ "id" uuid PRIMARY KEY,
3
+ "title" varchar(120) NOT NULL,
4
+ CONSTRAINT "tasks_title_nonempty" CHECK (length(trim("title")) > 0)
5
+ );
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1789257600000,
9
+ "tag": "0000_tasks",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "format": 1,
3
+ "id": "TASK-LIST",
4
+ "path": "standard",
5
+ "outcome": "Create validated tasks through Nest and render them in Next using PostgreSQL persistence.",
6
+ "scope": ["Task title validation", "Nest HTTP boundary", "Drizzle persistence", "Next read and filter boundaries"],
7
+ "exclusions": ["Authentication", "Deployment", "Creation form", "Broker integration"],
8
+ "invariants": ["Web cannot access database credentials", "Business validation precedes persistence", "Preserve the pinned framework versions"],
9
+ "uncertainties": ["Browser hydration requires a separate manual or browser check"],
10
+ "owner": "fixture-maintainer",
11
+ "status": "active",
12
+ "nextAction": "Review recorded unit evidence and run database/e2e checks under the documented local fixture contract.",
13
+ "stopConditions": ["Acceptance checks are recorded with current evidence", "A required dependency is unavailable"],
14
+ "acceptance": [
15
+ {"id": "AC-TITLE", "description": "Invalid titles fail before persistence", "changes": ["api/domain/task.ts", "api/http.ts"], "verification": "npm test", "kind": "automated"},
16
+ {"id": "AC-STORE", "description": "Tasks persist across PostgreSQL connections", "changes": ["api/infrastructure/postgres.ts", "migrations/0000_tasks.sql"], "verification": "npm run test:db", "kind": "automated"},
17
+ {"id": "AC-WEB", "description": "Next renders a task created through the real API and reports unavailability", "changes": ["web/app/page.tsx", "web/features/tasks/server/list-tasks.ts"], "verification": "npm run test:e2e", "kind": "automated"}
18
+ ],
19
+ "sources": [
20
+ {"id": "runtime", "path": "package.json", "level": "project", "reason": "Pins versions and commands", "authority": "Executable fixture configuration", "kind": "fact", "revision": "working-tree"},
21
+ {"id": "lock", "path": "package-lock.json", "level": "project", "reason": "Pins resolved dependencies", "authority": "Dependency resolution for this fixture", "kind": "fact", "revision": "working-tree"},
22
+ {"id": "contract", "path": "CONTRACT.md", "level": "domain", "reason": "Defines title and HTTP semantics", "authority": "Accepted fictional fixture contract", "kind": "accepted-decision", "revision": "working-tree"},
23
+ {"id": "domain", "path": "api/domain/task.ts", "level": "domain", "reason": "Implements title validation", "authority": "Observed implementation, subordinate to accepted contract", "kind": "fact", "revision": "working-tree"},
24
+ {"id": "application", "path": "api/application/tasks.ts", "level": "domain", "reason": "Enforces use-case boundaries", "authority": "Observed implementation", "kind": "fact", "revision": "working-tree"},
25
+ {"id": "http", "path": "api/http.ts", "level": "mission", "reason": "Implements the HTTP acceptance boundary", "authority": "Observed HTTP implementation", "kind": "fact", "revision": "working-tree"},
26
+ {"id": "api-types", "path": "api/tsconfig.json", "level": "project", "reason": "Controls compiled runtime semantics", "authority": "Compiler configuration", "kind": "fact", "revision": "working-tree"},
27
+ {"id": "web-model", "path": "web/features/tasks/model/tasks.ts", "level": "mission", "reason": "Validates and filters task data", "authority": "Observed pure model", "kind": "fact", "revision": "working-tree"},
28
+ {"id": "unit-tests", "path": "tests/unit.test.cjs", "level": "execution", "reason": "Defines title acceptance checks", "authority": "Executable check, not a recorded outcome", "kind": "fact", "revision": "working-tree"},
29
+ {"id": "http-tests", "path": "tests/http.test.cjs", "level": "execution", "reason": "Defines HTTP acceptance checks", "authority": "Executable check, not a recorded outcome", "kind": "fact", "revision": "working-tree"},
30
+ {"id": "web-tests", "path": "tests/web-model.test.cjs", "level": "execution", "reason": "Defines pure model checks", "authority": "Executable check, not a recorded outcome", "kind": "fact", "revision": "working-tree"}
31
+ ],
32
+ "dependencies": [],
33
+ "contradictions": []
34
+ }