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,57 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import {pathToFileURL} from 'node:url';
7
+ const project=path.resolve(process.env.PROJECT_DIR || 'examples/pocket-tasks');
8
+ const {createTaskServer}=await import(pathToFileURL(path.join(project,'server.mjs')));
9
+ const start=async dataFile=>{const server=createTaskServer({dataFile});await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});return {server,url:`http://127.0.0.1:${server.address().port}`};};
10
+ const close=server=>new Promise((resolve,reject)=>{server.close(error=>error?reject(error):resolve());server.closeIdleConnections?.();});
11
+
12
+ test('independent greenfield contract',async t=>{
13
+ const dir=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'pocket-acceptance-'));
14
+ const dataFile=path.join(dir,'tasks.json');let app=await start(dataFile);
15
+ t.after(async()=>{await close(app.server);fs.rmSync(dir,{recursive:true,force:true});});
16
+ const request=(route,method='GET',body)=>fetch(app.url+route,{method,...(body===undefined?{}:{headers:{'content-type':'application/json'},body:JSON.stringify(body)})});
17
+ let id;
18
+ await t.test('new app starts empty, creates trimmed task and persists fields',async()=>{
19
+ assert.deepEqual(await (await request('/api/tasks')).json(),{tasks:[]});
20
+ const response=await request('/api/tasks','POST',{title:' Plan the demo '});assert.equal(response.status,201);
21
+ const {task}=await response.json();assert.equal(task.title,'Plan the demo');assert.equal(task.done,false);assert.equal(typeof task.id,'string');assert.ok(task.id);id=task.id;
22
+ });
23
+ await t.test('edits, completes, reopens and handles missing IDs',async()=>{
24
+ let response=await request('/api/tasks/'+id,'PATCH',{title:'Ship the demo',done:true});assert.equal(response.status,200);assert.deepEqual((await response.json()).task,{id,title:'Ship the demo',done:true});
25
+ response=await request('/api/tasks/'+id,'PATCH',{done:false});assert.equal((await response.json()).task.done,false);
26
+ assert.equal((await request('/api/tasks/missing','PATCH',{done:true})).status,404);
27
+ assert.equal((await request('/api/tasks/missing','DELETE')).status,404);
28
+ });
29
+ await t.test('invalid inputs and malformed JSON are rejected without mutations',async()=>{
30
+ const before=await (await request('/api/tasks')).json();
31
+ for(const body of [null,[],{}, {title:''},{title:' '},{title:12},{title:'x'.repeat(121)}])assert.equal((await request('/api/tasks','POST',body)).status,400,JSON.stringify(body));
32
+ for(const body of [null,[],{}, {done:'yes'},{title:null}])assert.equal((await request('/api/tasks/'+id,'PATCH',body)).status,400,JSON.stringify(body));
33
+ const malformed=await fetch(app.url+'/api/tasks',{method:'POST',headers:{'content-type':'application/json'},body:'{broken'});assert.equal(malformed.status,400);assert.ok(await malformed.json());
34
+ const large=await request('/api/tasks','POST',{title:'x'.repeat(17000)});assert.equal(large.status,413);
35
+ assert.deepEqual(await (await request('/api/tasks')).json(),before);
36
+ });
37
+ await t.test('concurrent writes preserve unique tasks and literal HTML-looking text',async()=>{
38
+ const results=await Promise.all(Array.from({length:12},(_,i)=>request('/api/tasks','POST',{title:'Parallel '+i})));
39
+ assert.ok(results.every(r=>r.status===201));
40
+ const literal='<img src=x onerror=alert(1)>';const response=await request('/api/tasks','POST',{title:literal});assert.equal(response.status,201);assert.equal((await response.json()).task.title,literal);
41
+ const {tasks}=await (await request('/api/tasks')).json();assert.equal(tasks.length,14);assert.equal(new Set(tasks.map(t=>t.id)).size,14);
42
+ });
43
+ await t.test('restart restores data and delete persists',async()=>{
44
+ const before=await (await request('/api/tasks')).json();await close(app.server);app=await start(dataFile);assert.deepEqual(await (await request('/api/tasks')).json(),before);
45
+ assert.equal((await request('/api/tasks/'+id,'DELETE')).status,204);await close(app.server);app=await start(dataFile);assert.equal((await (await request('/api/tasks')).json()).tasks.length,13);
46
+ });
47
+ await t.test('serves a UI and protects nonpublic sources',async()=>{
48
+ const page=await request('/');assert.equal(page.status,200);assert.match(page.headers.get('content-type'),/text\/html/);
49
+ assert.equal((await request('/server.mjs')).status,404);assert.equal((await request('/BRIEF.md')).status,404);assert.equal((await request('/api/unknown')).status,404);
50
+ });
51
+ await t.test('malformed stored data never silently resets',async()=>{
52
+ const corrupt=path.join(dir,'corrupt.json');fs.writeFileSync(corrupt,'not-json');let bad;
53
+ try{bad=await start(corrupt);}catch(error){assert.ok(error instanceof Error);}
54
+ if(bad){try{const response=await fetch(bad.url+'/api/tasks');assert.equal(response.status,500);}finally{await close(bad.server);}}
55
+ assert.equal(fs.readFileSync(corrupt,'utf8'),'not-json');
56
+ });
57
+ });
@@ -0,0 +1,28 @@
1
+ import test from 'node:test';
2
+ import {request as rawRequest} from 'node:http';
3
+ import assert from 'node:assert/strict';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import {pathToFileURL} from 'node:url';
8
+ const project=path.resolve(process.env.PROJECT_DIR || 'examples/pocket-tasks');
9
+ const {createTaskServer}=await import(pathToFileURL(path.join(project,'server.mjs')));
10
+ test('independent origin, host and media-type non-mutation checks',async t=>{
11
+ const dir=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'pocket-trust-'));const server=createTaskServer({dataFile:path.join(dir,'tasks.json')});
12
+ await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));const url=`http://127.0.0.1:${server.address().port}`;
13
+ t.after(async()=>{await new Promise(resolve=>{server.close(resolve);server.closeIdleConnections?.();});fs.rmSync(dir,{recursive:true,force:true});});
14
+ const send=headers=>new Promise((resolve,reject)=>{
15
+ const req=rawRequest(url+'/api/tasks',{method:'POST',headers},response=>{response.resume();response.on('end',()=>resolve({status:response.statusCode}));});
16
+ req.on('error',reject);if(headers.host)assert.equal(req.getHeader('host'),headers.host);req.end(JSON.stringify({title:'Rejected mutation'}));
17
+ });
18
+ for(const headers of [
19
+ {origin:'https://untrusted.example','content-type':'text/plain'},
20
+ {origin:'https://untrusted.example','content-type':'application/json'},
21
+ {origin:'null','content-type':'application/json'},
22
+ {'sec-fetch-site':'cross-site','content-type':'application/json'},
23
+ {host:`untrusted.example:${server.address().port}`,'content-type':'application/json'}
24
+ ])assert.equal((await send(headers)).status,403,JSON.stringify(headers));
25
+ assert.equal((await send({'content-type':'text/plain'})).status,415);
26
+ assert.deepEqual(await (await fetch(url+'/api/tasks')).json(),{tasks:[]});
27
+ assert.equal((await send({origin:url,'content-type':'application/json'})).status,201);
28
+ });
@@ -0,0 +1,155 @@
1
+ {
2
+ "format": 1,
3
+ "kind": "bounded-native-pilot",
4
+ "runs": [
5
+ {
6
+ "id": "b1-1-none",
7
+ "status": "exited",
8
+ "objectiveChecksPassed": true,
9
+ "usage": {
10
+ "inputTokens": 79088,
11
+ "outputTokens": 953,
12
+ "cachedInputTokens": 69888,
13
+ "costUSD": null
14
+ },
15
+ "elapsedSeconds": 43.715,
16
+ "review": "Calibration only; excluded from comparison due to changed isolation settings. Independently rechecked.",
17
+ "evidenceSha256": {
18
+ "-setup.json": "8877dd507461b0f08a872da89bf4bc969625bcd1fc5ba8d75d8e9e8ce054fa48",
19
+ ".jsonl": "8f359c452ca399d82a10bd2d67df5eb930851c2104f5782c294f7d1035b636a2",
20
+ ".stderr": "a7886594c2ac648cf3565b765315daac7bda8ded4489c3d1d7cd92c75c941699"
21
+ }
22
+ },
23
+ {
24
+ "id": "matched-b1-1-bmad",
25
+ "status": "exited",
26
+ "objectiveChecksPassed": false,
27
+ "usage": {
28
+ "inputTokens": 36722,
29
+ "outputTokens": 435,
30
+ "cachedInputTokens": 23936,
31
+ "costUSD": null
32
+ },
33
+ "elapsedSeconds": 28.574,
34
+ "review": "Incomplete: no changes or handoff; tests fail. Worker reports UV/temp blocker, but no UV invocation is present in retained transcript; cause unconfirmed.",
35
+ "evidenceSha256": {
36
+ "-setup.json": "0be207640ca9e8d5277deb4e4d7f89726c50239defbc1d3bf7ba5df148495e7d",
37
+ ".jsonl": "37fb256063e991fb0b388e94474c181ba8524fd98ed1660388b87317cc6ecb40",
38
+ ".stderr": "45f6aa7228724ae1d9c42884f37b2b5cecc0429986b35c49550b410af857926c"
39
+ }
40
+ },
41
+ {
42
+ "id": "matched-b1-1-devmethod",
43
+ "status": "exited",
44
+ "objectiveChecksPassed": true,
45
+ "usage": {
46
+ "inputTokens": 134616,
47
+ "outputTokens": 1853,
48
+ "cachedInputTokens": 120192,
49
+ "costUSD": null
50
+ },
51
+ "elapsedSeconds": 57.01,
52
+ "review": "Independent review passed: relevant skills consulted, bounded change, preserved tests, 2/2 passed.",
53
+ "evidenceSha256": {
54
+ "-setup.json": "2bfa306ace9aebfa6a3534540962e9cfc5ec25b1c211a941078526fe03f0fa39",
55
+ ".jsonl": "6bceff4a9916e2fdd5413e2ead7630097785cc13425a989386655600a92ef80f",
56
+ ".stderr": "aa0a6e878075b2f4554c9e181ff7daadd2e8fc099a9313d34d84a14fca076690"
57
+ }
58
+ },
59
+ {
60
+ "id": "matched-b1-1-none",
61
+ "status": "exited",
62
+ "objectiveChecksPassed": true,
63
+ "usage": {
64
+ "inputTokens": 49207,
65
+ "outputTokens": 1041,
66
+ "cachedInputTokens": 35968,
67
+ "costUSD": null
68
+ },
69
+ "elapsedSeconds": 30.329,
70
+ "review": "Objective checks passed; no method arm.",
71
+ "evidenceSha256": {
72
+ "-setup.json": "131be54e9b8468155bcf2ab40a7ce7a98357f29708728f3c9c13a7cf3595bfdf",
73
+ ".jsonl": "ad384bb14637b3612910891205ba0e338af1999eaa78eb56b9d9a4c092c549b0",
74
+ ".stderr": "c95bb25b3d249e4e71dc19a5d6db0a8a08d7bbbd9e855c936496e699c04fdb25"
75
+ }
76
+ },
77
+ {
78
+ "id": "smoke-b3",
79
+ "status": "exited",
80
+ "objectiveChecksPassed": true,
81
+ "usage": {
82
+ "inputTokens": 105298,
83
+ "outputTokens": 1530,
84
+ "cachedInputTokens": 77952,
85
+ "costUSD": null
86
+ },
87
+ "elapsedSeconds": 44.517,
88
+ "review": "Independent review passed: unresolved ADR dependency recognized, no invented contract, explicit handoff, 1/1 passed.",
89
+ "evidenceSha256": {
90
+ "-setup.json": "63d899f33747045485bb1bedbf8b781806eeaf8fc2d2c2d32b0ed1f5a7f8b7ed",
91
+ ".jsonl": "2fc8aadd2ee1a8ad1728454dd24528bdb23e040ca4dde91fc2ca4e02578a6999",
92
+ ".stderr": "3f0820fbf17e94a4b1e5aebc46bd95a75ff89fc5aa60d82bf392bf66b009ba1b"
93
+ }
94
+ },
95
+ {
96
+ "id": "smoke-b4",
97
+ "status": "exited",
98
+ "objectiveChecksPassed": true,
99
+ "usage": {
100
+ "inputTokens": 136447,
101
+ "outputTokens": 2137,
102
+ "cachedInputTokens": 106880,
103
+ "costUSD": null
104
+ },
105
+ "elapsedSeconds": 57.67,
106
+ "review": "Independent review passed: initial failed test evidenced, stale checkpoint corrected against current contract, 1/1 passed. Initial test requested explicitly; supplied checkpoint, not a two-session chain.",
107
+ "evidenceSha256": {
108
+ "-setup.json": "ab2ae33e7882d08fae2c04a00ff687cebf6c4cd3a2fdfcb14574cba74559a8ce",
109
+ ".jsonl": "a102c8e1d0a3f29ffa1060db3c154803df1c45531bb5b670949d89349399a9d7",
110
+ ".stderr": "09c25279076b0b51db41952e7c4d4c622f531173ef5493f470f07e3cf6f414ae"
111
+ }
112
+ }
113
+ ],
114
+ "cells": [
115
+ {
116
+ "arm": "none",
117
+ "planned": 3,
118
+ "executed": 1,
119
+ "notRun": 2,
120
+ "objectivePassed": 1,
121
+ "objectiveFailed": 0
122
+ },
123
+ {
124
+ "arm": "devmethod",
125
+ "planned": 3,
126
+ "executed": 1,
127
+ "notRun": 2,
128
+ "objectivePassed": 1,
129
+ "objectiveFailed": 0
130
+ },
131
+ {
132
+ "arm": "bmad",
133
+ "planned": 3,
134
+ "executed": 1,
135
+ "notRun": 2,
136
+ "objectivePassed": 0,
137
+ "objectiveFailed": 1
138
+ }
139
+ ],
140
+ "totalTokens": 549327,
141
+ "costUSD": null,
142
+ "limits": {
143
+ "maxRuns": 12,
144
+ "timeoutSeconds": 120,
145
+ "observedTokenStop": 500000
146
+ },
147
+ "limitation": "Calibration excluded from matched cells. Objective success is not behavioral review or proof of superiority. Unknown cost remains unavailable.",
148
+ "date": "2026-09-13",
149
+ "host": "Codex CLI 0.147.0 / macOS",
150
+ "model": "gpt-5.6-sol",
151
+ "effort": "low",
152
+ "sourceRevision": "385c56f55691c2904d96a3360ed0d38ce3ecb535",
153
+ "stopReason": "Observed threshold crossed within sixth invocation; six remaining invocations not dispatched.",
154
+ "rawEvidence": "Retained locally, not included in package; hashes permit later verification."
155
+ }
@@ -0,0 +1,36 @@
1
+ # Starter exercises
2
+
3
+ These are fictional tasks, not recorded agent successes. Keep generated application code in a disposable repository. The public npm candidate predates the new work-sizing reference; use a reviewed source checkout to evaluate that guidance.
4
+
5
+ ## B1: runnable small bug fix
6
+
7
+ Copy `examples/bugfix/` into a new directory outside the DevMethod checkout. With Node.js 22+, run:
8
+
9
+ ```bash
10
+ node --test acceptance.test.mjs
11
+ ```
12
+
13
+ The two tests intentionally fail on the starting implementation. This is expected and separate from DevMethod's own `npm test`. Optionally initialize a local Git repository to record the baseline, then install the chosen method into that fixture directory. For DevMethod, run from the reviewed DevMethod checkout:
14
+
15
+ ```bash
16
+ node dist/cli.js init --tool codex --dest /path/to/fixture
17
+ node dist/cli.js doctor --dest /path/to/fixture
18
+ ```
19
+
20
+ Use this task in the host; select `project-foundation` with the host's invocation syntax when testing DevMethod:
21
+
22
+ > Fix parsePageSize in page-size.mjs. Accept positive safe integer numbers and strings containing only decimal digits after trimming. Cap valid values at 100. Return 20 for every other value; do not coerce objects. Preserve the export and acceptance.test.mjs. Make no dependency additions or unrelated changes. Run node --test acceptance.test.mjs, review the diff, and report the observed result. Delivery is local only. Finish when this scope is verified.
23
+
24
+ Expected artifacts: focused implementation diff, actual test output and concise outcome. No new product spec or architecture decision is needed. Keep the starting fixture unchanged in this repository so subsequent evaluations use the same input.
25
+
26
+ ## Existing React feature prompt
27
+
28
+ Requires an existing application, approved UI reference and real project commands; it is not a bundled runnable fixture.
29
+
30
+ > Use project-foundation to add an empty state to [existing screen], following [approved reference]. Reuse the installed components and existing server-state mechanism. Keep request orchestration in the existing feature hook and pure transformations outside the view. Preserve loading/error behavior and keyboard accessibility. Inspect package.json and applicable instructions to choose actual checks; verify the relevant rendered states. Scope excludes restyling other screens, dependency upgrades and deployment. Ask only about unresolved behavior that affects implementation.
31
+
32
+ ## AI integration prompt
33
+
34
+ Requires an existing application and an approved provider contract. Live provider calls need the project's existing authorization and budget.
35
+
36
+ > Use project-foundation for [ticket]: add a summary action using the already approved provider behind the existing backend boundary. Validate inputs and output shape, keep credentials server-side, and preserve the specified timeout, retry and quota policy. Treat document content as untrusted data. Do not claim generated summaries are factual without the ticket's evidence criteria. Test success, invalid output and provider failure using the existing test setup; report live-provider validation separately. Scope excludes choosing a new provider, buying services or deployment.
@@ -0,0 +1,17 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { parsePageSize } from './page-size.mjs';
4
+
5
+ test('accepts positive safe integers and caps them at 100', () => {
6
+ for (const [input, expected] of [[1, 1], [25, 25], [' 12 ', 12], ['005', 5], [100, 100], [150, 100], ['150', 100]]) {
7
+ assert.equal(parsePageSize(input), expected, String(input));
8
+ }
9
+ });
10
+
11
+ test('invalid inputs return the default 20 without coercing objects', () => {
12
+ for (const input of [undefined, null, '', ' ', 0, -1, 1.5, NaN, Infinity, true, false,
13
+ [], [2], {}, '1.5', '-2', '+2', '1e2', '0x10', 'Infinity', Number.MAX_SAFE_INTEGER + 1,
14
+ { valueOf() { throw new Error('Must not coerce objects'); } }]) {
15
+ assert.equal(parsePageSize(input), 20);
16
+ }
17
+ });
@@ -0,0 +1,4 @@
1
+ // Intentionally defective starting point for the B1 workflow exercise.
2
+ export function parsePageSize(input) {
3
+ return Number(input) || 20;
4
+ }
@@ -0,0 +1,15 @@
1
+ # Relevé de l’essai Clair
2
+
3
+ Date : 2026-09-13. Dossier initial : kit fraîchement installé, aucune application. Source du produit : brief transmis par l’agent parent, dont la sélection détaillée des écrans est déléguée. Le relevé est une observation de cette exécution, pas une comparaison de méthodes.
4
+
5
+ Les lectures initiales et décisions ont précédé le code. Exploration/cadrage/design/architecture/plan/readiness ont été groupés en une tranche standard CLAIR-1, puis implémentation, tests et revue locale. Le seul enregistrement courant est `docs/missions/clair.md`. Ce rapport est un instantané ; aucun document par étape. Aucun imagegen, réseau, dépendance ou sous-agent utilisé. Le kit est resté intact ; le profil adopté est dans la mission pour respecter cette contrainte.
6
+
7
+ Fichiers lus : START_HERE.md, AGENTS.foundation.md, PROJECT_PROFILE.md, ENGINEERING_POLICY.template.md ; SKILL.md de project-foundation, design-to-code, scoped-delivery et decision-architecture ; références work-sizing, operating-commands, mission-context, visual-creation, ux-contract, verification-and-cost ; modèle scoped-delivery/assets/MISSION.md ; kit-manifest.json. CONTRIBUTING.md et AGENTS.md étaient absents. Les autres modules ont été découverts par inventaire, sans être chargés.
8
+
9
+ Fichiers créés : app/index.html, app/styles.css, app/app.mjs, app/domain.mjs, app/storage.mjs, tests/domain.test.mjs, docs/missions/clair.md et ce rapport. Livré : page française responsive ivoire/encre/rouille, ajout titre/note, validation, toggle, trois filtres, suppression/annulation, localStorage versionné et validé, alerte d’échec persistante/réessai, réinitialisation explicite après erreur de lecture, départ vide et ajout de trois tâches de démonstration marquées fictives.
10
+
11
+ Exécuté : `node --test tests/*.test.mjs` — 6 tests passés, 0 échec ; `node --check app/app.mjs` — passé. Vérification des hashes SHA-256 des fichiers du manifeste installé — aucune différence. Revue locale du code et des limites ; aucune installation ni ressource distante.
12
+
13
+ Limites : cet agent n’a pas lancé de navigateur, capturé d’écran, testé l’interaction réelle ou vérifié le rendu mobile. Le parent en est chargé indépendamment. Les vérifications DOM/CSS effectuées sont des inspections du source. Aucune preuve d’intégration, de déploiement ou d’usage production. L’application conserve les priorités jusqu’à suppression ; aucune remise à zéro quotidienne ni synchronisation entre onglets. L’annulation porte sur la dernière suppression et disparaît après rechargement. La démo peut être ajoutée plusieurs fois volontairement.
14
+
15
+ Lancer : `python3 -m http.server 8766 --bind 127.0.0.1 --directory app`. Prochaine commande méthode : `$project-foundation verify CLAIR-1`.
@@ -0,0 +1,41 @@
1
+ # Clair — mission canonique
2
+
3
+ 2026-09-13, livraison standard, propriétaire : agent de réalisation.
4
+
5
+ ## Sources et profil adopté
6
+ Le brief transmis par le parent fait autorité : application française locale, HTML/CSS/JS sans dépendances ni comptes, direction ivoire/encre/rouille et sérif ; choix détaillés délégués. Les fichiers du kit restent intacts, notamment PROJECT_PROFILE.md : ce paragraphe porte le profil adopté conformément à la contrainte de préservation. CONTRIBUTING.md et AGENTS.md sont absents. ENGINEERING_POLICY.template.md est un modèle ; le JavaScript demandé prend priorité sur sa recommandation TypeScript.
7
+
8
+ Stack : modules JavaScript natifs, CSS, HTML ; Node pour les tests purs ; Python pour le serveur local. Commandes : `node --test tests/*.test.mjs` ; `python3 -m http.server 8766 --bind 127.0.0.1 --directory app`. Aucun déploiement, réseau, installation ou génération d’images autorisé. Données : titres, notes et état d’achèvement stockés sous clair.tasks.v1 dans localStorage, conservés jusqu’à suppression ou effacement du navigateur ; aucune synchronisation ni télémétrie.
9
+
10
+ ## Framing, design et architecture — décisions avant code
11
+ Valeur : choisir et suivre les priorités du jour sans configuration. Direction existante explicite, choix d’écran délégué ; aucune image ou maquette séparée nécessaire. Prototype HTML final choisi comme support. Grand titre éditorial, date, compteur, liste principale et formulaire latéral ; mobile en une colonne avec formulaire avant la liste. Palette #f7f3eb, #292923, #97472f ; Georgia système et sans-sérif système. États : vide, filtres vides, tâches actives/terminées, validation, suppression/annulation, stockage indisponible/corrompu.
12
+
13
+ Choix structurel dans le cadre délégué : module pur pour validation/transitions/schéma, adaptateur localStorage, contrôleur DOM. Une page monolithique serait plus courte mais rendrait les invariants difficiles à tester isolément. Aucun framework utile au périmètre. Les titres/notes ne deviennent jamais du HTML. Échec d’écriture : garder la session en mémoire et afficher une alerte persistante avec réessai ; erreur de lecture : ne pas écraser les données sans action explicite de réinitialisation. Suppression réversible de la dernière tâche, sans expiration temporelle. Pas de réinitialisation automatique à minuit.
14
+
15
+ ## Plan et readiness (avant implémentation)
16
+ Une tranche CLAIR-1 couvre AC1 ajout titre/note et validation ; AC2 toggle/filtres ; AC3 suppression/annulation ; AC4 persistance/schema/erreurs ; AC5 vide + démo explicitement fictive ; AC6 responsive/clavier/focus. Contrats prêts, dépendances résolues. Tests prévus sur invariants et stockage ; vérification navigateur déléguée au parent et non revendiquée par cet agent. Limites de titre 160 caractères et note 500, données versionnées et validées à la lecture. Fin de périmètre après réalisation locale et tests ; pas de fonctionnalités hors brief.
17
+
18
+ ## Evidence et état
19
+ État courant après vérification parent : CLAIR-1 réalisé et vérifié dans Chrome desktop/mobile. Les étapes explorations/cadrage/design/architecture/plan/readiness ont été groupées, pas de document par étape.
20
+
21
+ ## Résultat local et evidence — 2026-09-13
22
+ CLAIR-1 réalisé dans `app/`. État lors du retour de l’agent : implémentation locale terminée, acceptation navigateur alors en attente. Aucun commit, PR ou déploiement.
23
+
24
+ | Critère | Preuve exécutée | Résultat |
25
+ |---|---|---|
26
+ | AC1 | `node --test tests/*.test.mjs`, cas validation | passé : blanc, trim, bornes titre/note |
27
+ | AC2 | même commande, toggle/filtres | passé : immutabilité et ordre |
28
+ | AC3 | même commande, suppression/annulation | passé : position, ajout intermédiaire, absence et doublon |
29
+ | AC4 | même commande, schéma/adapter | passé : round-trip Unicode, JSON invalide, version, types, IDs dupliqués, exceptions lecture/écriture |
30
+ | AC5 | revue du contrôleur et HTML | démarrage vide sans données ; bouton de démo explicitement fictif, notes marquées ; navigateur non exécuté |
31
+ | AC6 | revue CSS/DOM | labels, focus visible, annonces, règles 700/1000 px et réduction de mouvement présentes ; comportement/rendu navigateur non vérifiés |
32
+
33
+ Commande `node --check app/app.mjs` passée. Six tests, zéro échec. Revue locale de ses propres fichiers : contenu utilisateur inséré via textContent, stockage traité au bord, aucune ressource distante, aucun secret, actions couvertes dans le contrôleur. Cette inspection ne démontre pas l'accessibilité complète. Vérification SHA-256 des fichiers installés contre kit-manifest.json : aucun changement. Aucun test navigateur réalisé par cet agent, aucune comparaison pixel ou audit automatique revendiqué.
34
+
35
+ Prochaine action exacte : parent, exécuter la vérification desktop/mobile et des parcours de CLAIR-1 sur le serveur local. Commande méthode recommandée : `$project-foundation verify CLAIR-1`.
36
+
37
+ ## Vérification indépendante du parent
38
+
39
+ Interactions Chrome vérifiées : vide, validation, ajout/note, toggle/filtres, rechargement, suppression/annulation, démo, saisie clavier, absence de débordement à 390px, échec de stockage avec conservation en mémoire et alerte. Captures desktop/mobile inspectées. Les animations sont désactivées pour les captures afin de ne pas photographier une barre de progression en transition. Aucun audit complet d’accessibilité.
40
+
41
+ Constat méthode : l’agent avait conservé une ancienne ligne « implémentation en cours » avant son résultat final. Le parent l’a réconciliée ici ; ce besoin de correction empêche de présenter la tenue du statut comme entièrement autonome. Le rapport agent est un instantané historique. Prochaine action : aucune implémentation restante dans ce périmètre ; intégration et vidéo suivies dans le dépôt DevMethod.
@@ -0,0 +1,24 @@
1
+ # Clair — an actual from-zero DevMethod pilot
2
+
3
+ Clair is a fictional French personal-priorities app created by a separate Codex subagent in a new directory containing only the installed DevMethod kit. The parent provided a bounded brief and delegated the detailed editorial visual choices. No application code or previous implementation was supplied. This is a same-host subagent test, not a fresh standalone Codex/Claude session or a comparative benchmark.
4
+
5
+ ## Run
6
+
7
+ From this directory:
8
+
9
+ ```sh
10
+ python3 -m http.server 8766 --bind 127.0.0.1 --directory app
11
+ node --test tests/*.test.mjs
12
+ ```
13
+
14
+ Open http://127.0.0.1:8766. Stop the server with Ctrl-C. No npm dependencies, backend, external fonts, account or provider key. Browser storage uses `clair.tasks.v1`. The application starts empty; its demo button explicitly adds fictional tasks. Delete/undo covers the last deletion and does not survive reload. There is no cross-tab synchronization or daily reset.
15
+
16
+ ## What was observed
17
+
18
+ The implementing agent grouped framing, design, architecture, planning and readiness in one mission. It created five app files, a test file, one mission and the requested evaluation snapshot; all installed kit files retained their hashes. Six logic/storage tests passed. The parent then exercised the actual Chrome UI: blank validation, add/note, completion/filters, persistence, delete/undo, demo, keyboard submission, 390px overflow and write-failure recovery.
19
+
20
+ [Agent report](AGENT-EVALUATION.md) is a historical observation. [Mission](MISSION.md) includes parent checks and disclosed limitations. A stale status sentence left by the implementing agent was reconciled by the parent; this was not perfect autonomous bookkeeping. The design was delegated, so this test does not repeat the user-choice/image steps of the separate [Lisière pilot](../visual-pilot/README.md).
21
+
22
+ Browser check from the DevMethod repository root: `node examples/clair-from-zero/browser-check.cjs`, with Playwright and Chrome available. Set PLAYWRIGHT_MODULE to an installed Playwright module path when needed. It writes captures under docs/media/from-zero/captures and uses a temporary browser profile.
23
+
24
+ [French video and transcript](../../docs/media/from-zero/README.md) show this app's actual interactions and separately identify the Lisière visual pilot. Neither demonstration establishes general code quality, production readiness, lower costs or superiority over BMAD.
@@ -0,0 +1,69 @@
1
+ import {createTask,toggleTask,removeTask,restoreTask,filterTasks} from './domain.mjs';
2
+ import {readTasks,writeTasks} from './storage.mjs';
3
+ const $ = selector => document.querySelector(selector);
4
+ let tasks = [], filter = 'all', removed = null, readBlocked = false;
5
+ const announce = message => { $('#announcement').textContent = message; };
6
+ function showStorageError(message, blocked = false) {
7
+ $('#storage-alert').hidden = false; $('#storage-message').textContent = message;
8
+ $('#retry-storage').hidden = blocked; $('#reset-storage').hidden = !blocked;
9
+ }
10
+ try { tasks = readTasks(window.localStorage); }
11
+ catch { readBlocked = true; showStorageError('Les données enregistrées ne peuvent pas être lues. Vos changements restent uniquement en mémoire. Pour protéger les données existantes, la sauvegarde est suspendue. Vous pouvez les effacer explicitement pour repartir avec cette liste.', true); }
12
+ function save() {
13
+ if (readBlocked) return;
14
+ try { writeTasks(window.localStorage, tasks); $('#storage-alert').hidden = true; }
15
+ catch { showStorageError('La sauvegarde a échoué. Vos changements restent uniquement en mémoire et peuvent être perdus à la fermeture ou au rechargement. Libérez de l’espace ou autorisez le stockage, puis réessayez.'); }
16
+ }
17
+ function commit(message) { save(); render(); announce(message); }
18
+ function setFilter(value) { filter = value; render(); }
19
+ function render() {
20
+ const done = tasks.filter(task => task.done).length;
21
+ $('#total-count').textContent = String(tasks.length).padStart(2,'0');
22
+ $('#all-count').textContent = tasks.length; $('#active-count').textContent = tasks.length-done; $('#done-count').textContent = done;
23
+ $('#progress-label').textContent = tasks.length ? `${done} sur ${tasks.length} terminée${tasks.length > 1 ? 's' : ''}` : 'Un nouveau départ';
24
+ $('#progress-bar').style.width = `${tasks.length ? done/tasks.length*100 : 0}%`;
25
+ document.querySelectorAll('[data-filter]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.filter === filter)));
26
+ const visible = filterTasks(tasks, filter), list = $('#task-list'); list.replaceChildren();
27
+ for (const task of visible) {
28
+ const row = document.createElement('li'); row.className = `task${task.done?' done':''}`; row.dataset.id = task.id;
29
+ const check = document.createElement('button'); check.type = 'button'; check.className = 'task-check'; check.setAttribute('role','checkbox'); check.setAttribute('aria-checked', String(task.done)); check.setAttribute('aria-label', `${task.done?'Marquer à faire':'Terminer'} : ${task.title}`); check.textContent = task.done ? '✓' : '';
30
+ check.addEventListener('click', () => {
31
+ const position = visible.findIndex(item => item.id === task.id);
32
+ tasks = toggleTask(tasks, task.id); commit(task.done ? 'Priorité marquée à faire.' : 'Priorité terminée.');
33
+ const newRows = [...list.children], same = newRows.find(item => item.dataset.id === task.id);
34
+ (same?.querySelector('.task-check') || newRows[Math.min(position,newRows.length-1)]?.querySelector('.task-check') || $('#empty-add')).focus();
35
+ });
36
+ const content = document.createElement('div'), title = document.createElement('h3'); title.textContent = task.title; content.append(title);
37
+ if (task.note) { const note = document.createElement('p'); note.textContent = task.note; content.append(note); }
38
+ if (task.done) { const state = document.createElement('span'); state.className = 'task-state'; state.textContent = 'TERMINÉE'; content.append(state); }
39
+ const del = document.createElement('button'); del.type = 'button'; del.className = 'task-delete'; del.setAttribute('aria-label', `Supprimer : ${task.title}`);
40
+ del.innerHTML = '<svg viewBox="0 0 20 22" aria-hidden="true"><path d="M3 6h14M7 6V3h6v3M5 6l1 14h8l1-14M8 9v8M12 9v8"/></svg>';
41
+ del.addEventListener('click', () => { const result = removeTask(tasks, task.id); tasks = result.tasks; removed = result.removed; commit('Priorité supprimée. Vous pouvez annuler.'); $('#undo-button').focus(); });
42
+ row.append(check,content,del); list.append(row);
43
+ }
44
+ $('#empty-state').hidden = visible.length > 0;
45
+ if (!tasks.length) { $('#empty-title').textContent = 'Tout commence par une chose.'; $('#empty-copy').textContent = 'Qu’aimeriez-vous faire avancer aujourd’hui ? Ajoutez votre première priorité, même petite.'; $('#empty-add').textContent = 'Écrire ma première priorité ↗'; }
46
+ else if (filter === 'done') { $('#empty-title').textContent = 'Chaque chose en son temps.'; $('#empty-copy').textContent = 'Vos priorités terminées apparaîtront ici. Un petit pas suffit pour commencer.'; $('#empty-add').textContent = 'Voir les priorités à faire ↗'; }
47
+ else { $('#empty-title').textContent = 'De la place pour souffler.'; $('#empty-copy').textContent = 'Toutes vos priorités sont terminées. Savourez ce que vous avez fait aujourd’hui.'; $('#empty-add').textContent = 'Ajouter une nouvelle priorité ↗'; }
48
+ $('#undo-bar').hidden = !removed;
49
+ }
50
+ $('#task-form').addEventListener('submit', event => {
51
+ event.preventDefault();
52
+ try {
53
+ const task = createTask($('#task-title').value, $('#task-note').value, crypto.randomUUID());
54
+ tasks = [...tasks,task]; filter = 'all'; $('#task-form').reset(); $('#form-error').hidden = true; $('#task-title').removeAttribute('aria-invalid'); commit('Priorité ajoutée.'); $('#task-title').focus();
55
+ } catch (error) { $('#form-error').textContent = error.message; $('#form-error').hidden = false; $('#task-title').setAttribute('aria-invalid','true'); $('#task-title').focus(); }
56
+ });
57
+ $('#task-title').addEventListener('input', () => { if ($('#task-title').value.trim()) { $('#form-error').hidden = true; $('#task-title').removeAttribute('aria-invalid'); } });
58
+ document.querySelectorAll('[data-filter]').forEach(button => button.addEventListener('click', () => { setFilter(button.dataset.filter); announce(`${filterTasks(tasks,filter).length} priorité(s) affichée(s).`); }));
59
+ $('#empty-add').addEventListener('click', () => { if (tasks.length && filter === 'done') { setFilter('active'); $('[data-filter="active"]').focus(); } else { $('#task-title').focus(); $('#task-title').scrollIntoView({block:'center',behavior:'auto'}); } });
60
+ $('#undo-button').addEventListener('click', () => { const id = removed?.task.id; tasks = restoreTask(tasks,removed); removed = null; filter = 'all'; commit('Suppression annulée.'); [...$('#task-list').children].find(row => row.dataset.id === id)?.querySelector('.task-check').focus(); });
61
+ $('#dismiss-undo').addEventListener('click', () => { removed = null; render(); $('#priorities').focus(); });
62
+ $('#demo-button').addEventListener('click', () => {
63
+ const examples = [['Lire quelques pages','Démo fictive · Un chapitre, un thé et le téléphone de côté.'],['Faire une promenade','Démo fictive · Vingt minutes sans itinéraire.'],['Écrire à une personne chère','Démo fictive · Juste quelques mots pour prendre des nouvelles.']];
64
+ tasks = [...tasks,...examples.map(([title,note]) => createTask(title,note,crypto.randomUUID()))]; filter = 'all'; commit('Trois priorités fictives ajoutées à la liste.');
65
+ });
66
+ $('#retry-storage').addEventListener('click', () => { save(); if ($('#storage-alert').hidden) { announce('Liste sauvegardée dans ce navigateur.'); $('#task-title').focus(); } });
67
+ $('#reset-storage').addEventListener('click', () => { readBlocked = false; save(); if ($('#storage-alert').hidden) { announce('Les anciennes données ont été remplacées par la liste actuelle.'); $('#task-title').focus(); } });
68
+ $('#today').textContent = new Intl.DateTimeFormat('fr-FR',{weekday:'long',day:'numeric',month:'long'}).format(new Date()).toLocaleUpperCase('fr-FR');
69
+ render();
@@ -0,0 +1,30 @@
1
+ export const TITLE_LIMIT = 160;
2
+ export const NOTE_LIMIT = 500;
3
+ export function createTask(title, note, id) {
4
+ if (typeof title !== 'string' || !title.trim()) throw new Error('Donnez un titre à votre priorité.');
5
+ if (title.trim().length > TITLE_LIMIT) throw new Error('Le titre est limité à 160 caractères.');
6
+ if (typeof note !== 'string' || note.trim().length > NOTE_LIMIT) throw new Error('La note est limitée à 500 caractères.');
7
+ return { id, title: title.trim(), note: note.trim(), done: false };
8
+ }
9
+ export function toggleTask(tasks, id) { return tasks.map(task => task.id === id ? {...task, done: !task.done} : task); }
10
+ export function removeTask(tasks, id) {
11
+ const index = tasks.findIndex(task => task.id === id);
12
+ return { tasks: tasks.filter(task => task.id !== id), removed: index < 0 ? null : { task: tasks[index], index } };
13
+ }
14
+ export function restoreTask(tasks, removed) {
15
+ if (!removed || tasks.some(task => task.id === removed.task.id)) return tasks;
16
+ const result = [...tasks]; result.splice(Math.min(removed.index, result.length), 0, removed.task); return result;
17
+ }
18
+ export function filterTasks(tasks, filter) { return tasks.filter(task => filter === 'done' ? task.done : filter === 'active' ? !task.done : true); }
19
+ export function decodeTasks(raw) {
20
+ if (raw === null) return [];
21
+ const data = JSON.parse(raw);
22
+ if (!data || data.version !== 1 || !Array.isArray(data.tasks)) throw new Error('Format de données inconnu.');
23
+ const ids = new Set();
24
+ for (const task of data.tasks) {
25
+ if (!task || typeof task.id !== 'string' || !task.id || ids.has(task.id) || typeof task.title !== 'string' || !task.title.trim() || task.title.length > TITLE_LIMIT || typeof task.note !== 'string' || task.note.length > NOTE_LIMIT || typeof task.done !== 'boolean') throw new Error('Données enregistrées illisibles.');
26
+ ids.add(task.id);
27
+ }
28
+ return data.tasks.map(({id, title, note, done}) => ({id, title, note, done}));
29
+ }
30
+ export function encodeTasks(tasks) { return JSON.stringify({version: 1, tasks}); }
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html lang="fr">
3
+ <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="theme-color" content="#f7f3eb"><title>Clair — Place à l’essentiel</title><link rel="stylesheet" href="styles.css"><script type="module" src="app.mjs"></script></head>
4
+ <body>
5
+ <a class="skip" href="#priorities">Aller aux priorités</a>
6
+ <div class="page">
7
+ <header class="masthead"><a class="brand" href="./" aria-label="Clair, accueil">clair<span class="brand-dot">✳</span></a><span class="masthead-note">UN PEU MOINS. UN PEU MIEUX.</span><span class="edition">VOTRE ESPACE PERSONNEL</span></header>
8
+ <main>
9
+ <section class="hero" aria-labelledby="hero-title"><div><p class="eyebrow" id="today">LE FIL DU JOUR</p><h1 id="hero-title">Faire place<br>à <em>l’essentiel.</em></h1><p class="intro">Une journée, quelques priorités.<br>Gardez de la place pour ce qui compte vraiment.</p></div><div class="hero-aside" aria-hidden="true"><div class="sun"><i></i><i></i><i></i><i></i></div><p>Moins de bruit.<br>Plus de présence.</p><span>À VOTRE RYTHME</span></div></section>
10
+ <div id="storage-alert" class="storage-alert" role="alert" hidden><p id="storage-message"></p><button type="button" id="retry-storage">Réessayer la sauvegarde</button><button type="button" id="reset-storage" hidden>Effacer les données illisibles</button></div>
11
+ <div class="workspace">
12
+ <section class="priorities" id="priorities" aria-labelledby="list-heading" tabindex="-1"><div class="section-heading"><h2 id="list-heading">Vos priorités</h2><span class="count" id="total-count">00</span></div><div class="list-toolbar"><div class="filters" role="group" aria-label="Filtrer les priorités"><button type="button" data-filter="all" aria-pressed="true">Toutes <span id="all-count">0</span></button><button type="button" data-filter="active" aria-pressed="false">À faire <span id="active-count">0</span></button><button type="button" data-filter="done" aria-pressed="false">Terminées <span id="done-count">0</span></button></div><span id="progress-label" class="progress-label">Un nouveau départ</span></div><div class="progress-track" aria-hidden="true"><div id="progress-bar"></div></div><ul id="task-list" aria-label="Liste des priorités"></ul><div id="empty-state" class="empty-state"><div class="empty-mark" aria-hidden="true">✳</div><h3 id="empty-title">Tout commence par une chose.</h3><p id="empty-copy">Qu’aimeriez-vous faire avancer aujourd’hui ?<br>Ajoutez votre première priorité, même petite.</p><button type="button" id="empty-add" class="text-button">Écrire ma première priorité <span aria-hidden="true">↗</span></button></div><div class="list-foot"><span id="list-note">Une petite liste. Une intention claire.</span><button type="button" id="demo-button">Charger une démo fictive <span aria-hidden="true">↗</span></button></div></section>
13
+ <aside class="composer" aria-labelledby="form-heading"><p class="eyebrow">UNE CHOSE À LA FOIS</p><h2 id="form-heading">Qu’est-ce qui<br>compte <em>aujourd’hui ?</em></h2><form id="task-form" novalidate><label for="task-title">Votre priorité <span aria-hidden="true">*</span></label><input id="task-title" name="title" type="text" maxlength="160" placeholder="Par exemple, prendre le temps de lire" required aria-describedby="form-error" autocomplete="off"><label for="task-note">Une note <span class="optional">— facultatif</span></label><textarea id="task-note" name="note" maxlength="500" rows="3" placeholder="Un détail, une idée, un premier pas…"></textarea><p id="form-error" class="form-error" role="alert" hidden></p><button class="add-button" type="submit">Ajouter une priorité <span aria-hidden="true">+</span></button></form><p class="form-tip">Pas besoin de tout prévoir.<br>Commencez par ce qui vous tient à cœur.</p></aside>
14
+ </div>
15
+ </main>
16
+ <footer><span class="footer-brand">clair</span><p>Vos priorités restent dans ce navigateur.<br>Aucun compte. Aucune synchronisation.</p><span class="footer-note">CHAQUE PETIT PAS COMPTE.</span></footer>
17
+ </div>
18
+ <div id="undo-bar" class="undo-bar" role="status" hidden><span>Priorité supprimée.</span><button type="button" id="undo-button">Annuler</button><button type="button" id="dismiss-undo" aria-label="Fermer la notification de suppression">×</button></div><div class="sr-only" id="announcement" role="status" aria-live="polite"></div>
19
+ <noscript><p class="noscript">Activez JavaScript pour utiliser Clair. Aucune donnée n’est envoyée à un serveur.</p></noscript>
20
+ </body></html>
@@ -0,0 +1,4 @@
1
+ import { decodeTasks, encodeTasks } from './domain.mjs';
2
+ export const STORAGE_KEY = 'clair.tasks.v1';
3
+ export function readTasks(storage) { return decodeTasks(storage.getItem(STORAGE_KEY)); }
4
+ export function writeTasks(storage, tasks) { storage.setItem(STORAGE_KEY, encodeTasks(tasks)); }