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,3 @@
1
+ export function filterBooks(books, status) {
2
+ return books.filter(book => status === 'all' || book.status === status);
3
+ }
@@ -0,0 +1,9 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { filterBooks } from './filter.mjs';
4
+ const books = [{ title: 'A', status: 'reading' }, { title: 'B', status: 'planned' }];
5
+ test('all retains order and every book', () => assert.deepEqual(filterBooks(books, 'all'), books));
6
+ test('status filters without mutating source', () => {
7
+ assert.deepEqual(filterBooks(books, 'reading'), [books[0]]);
8
+ assert.equal(books.length, 2);
9
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devmethod-ai",
3
- "version": "0.1.0-rc.2",
3
+ "version": "0.2.0",
4
4
  "description": "From idea to delivery with your AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,8 +13,15 @@
13
13
  "files": [
14
14
  "dist/",
15
15
  ".agents/skills/",
16
+ "docs/",
17
+ "examples/",
18
+ "evaluation/",
19
+ "scripts/",
16
20
  "LICENSE",
17
- "COMPATIBILITY.md"
21
+ "COMPATIBILITY.md",
22
+ "CONTRIBUTING.md",
23
+ "VALIDATION.md",
24
+ "START_HERE.md"
18
25
  ],
19
26
  "repository": {
20
27
  "type": "git",
@@ -22,7 +29,12 @@
22
29
  },
23
30
  "scripts": {
24
31
  "build": "tsc",
25
- "test": "npm run build && node --test tests/*.test.mjs"
32
+ "test": "npm run build && node --test tests/*.test.mjs",
33
+ "test:fixtures": "node --test tests/evaluation.test.mjs",
34
+ "check:docs": "node scripts/check-docs.mjs",
35
+ "test:cli": "node --test tests/install.test.mjs tests/doctor.test.mjs tests/update.test.mjs tests/checkpoint.test.mjs tests/resume-cli.test.mjs tests/mission.test.mjs tests/planner.test.mjs",
36
+ "test:protocols": "node --test tests/comparison.test.mjs",
37
+ "test:greenfield": "node --test examples/pocket-tasks/tests/*.test.mjs evaluation/greenfield/*.test.mjs examples/clair-from-zero/tests/*.test.mjs"
26
38
  },
27
39
  "devDependencies": {
28
40
  "@types/node": "^22.0.0",
@@ -0,0 +1,20 @@
1
+ // Native sandbox preflight, no model invocation. Run explicitly on macOS.
2
+ import fs from 'node:fs';
3
+ import { execFileSync } from 'node:child_process';
4
+ import { startCodexRpc } from './hosts/codex-rpc.mjs';
5
+ export async function checkCodexGit() {
6
+ if(process.platform!=='darwin')throw new Error('macOS preflight only');
7
+ const cwd=fs.mkdtempSync('/private/tmp/devmethod-git-sandbox-');
8
+ execFileSync('git',['init',cwd]);fs.writeFileSync(cwd+'/sentinel.txt','fictional\n');
9
+ const rpc=startCodexRpc({cwd,timeoutMs:20000});const results=[];
10
+ try {
11
+ await rpc.call('initialize',{clientInfo:{name:'devmethod_probe',version:'0.1.0'},capabilities:{experimentalApi:true}});rpc.notify({method:'initialized'});
12
+ for(const includeGit of [false,true]) {
13
+ const result=await rpc.call('command/exec',{command:['git','add','sentinel.txt'],cwd,timeoutMs:5000,sandboxPolicy:{type:'workspaceWrite',writableRoots:includeGit?[cwd,cwd+'/.git']:[cwd],networkAccess:false,excludeTmpdirEnvVar:true,excludeSlashTmp:true}});
14
+ results.push({includeGit,exitCode:result.exitCode});
15
+ }
16
+ if(results[0].exitCode===0||results[1].exitCode!==0)throw new Error('Unexpected Git sandbox result');
17
+ return {gitMetadataWritable:true,results};
18
+ }finally{rpc.close();fs.rmSync(cwd,{recursive:true,force:true});}
19
+ }
20
+ if(process.argv[1]?.endsWith('/check-codex-git.mjs')) console.log(JSON.stringify(await checkCodexGit()));
@@ -0,0 +1,14 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const skip = new Set(['.git', 'node_modules', '.next', 'dist', 'evaluation-private']);
4
+ function files(root) { return fs.readdirSync(root, { withFileTypes: true }).flatMap(e => skip.has(e.name) || e.isSymbolicLink() ? [] : e.isDirectory() ? files(path.join(root, e.name)) : e.name.endsWith('.md') ? [path.join(root, e.name)] : []); }
5
+ const failures = [];
6
+ for (const file of files('.')) {
7
+ for (const [, target] of fs.readFileSync(file, 'utf8').matchAll(/\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) {
8
+ if (/^[a-z]+:|^#|^<|\$|\{/.test(target)) continue;
9
+ const relative = decodeURIComponent(target.split('#')[0]);
10
+ if (relative && !fs.existsSync(path.resolve(path.dirname(file), relative))) failures.push(`${file}: ${target}`);
11
+ }
12
+ }
13
+ if (failures.length) { console.error(failures.join('\n')); process.exitCode = 1; }
14
+ else console.log('Local Markdown file links resolve (external URLs and anchors are not checked).');
@@ -0,0 +1,11 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import assert from 'node:assert/strict';
5
+ import { verifyFixture } from './hosts/checks.mjs';
6
+ const root=fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()),'devmethod-sandbox-probe-'));
7
+ const worker=path.join(root,'worker');fs.mkdirSync(worker);const sentinel=path.join(root,'sentinel');fs.writeFileSync(sentinel,'fictional sentinel');
8
+ try {
9
+ const code=`const fs=require('node:fs'),net=require('node:net'),assert=require('node:assert/strict');assert.throws(()=>fs.readFileSync(${JSON.stringify(sentinel)}));assert.throws(()=>fs.writeFileSync('forbidden.txt','x'));const socket=net.connect({host:'127.0.0.1',port:9});socket.on('error',e=>{assert.equal(e.code,'EPERM');console.log('read/write/network denied');});`;
10
+ const result=verifyFixture(worker,['-e',code]); assert.equal(result.status,0,result.stdout+result.stderr);console.log(result.stdout.trim());
11
+ }finally{fs.rmSync(root,{recursive:true,force:true});}
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ /** Admission requirements for an unattended BMAD/DevMethod comparison.
6
+ * Evidence is operator supplied; this check does not certify a host.
7
+ */
8
+ export function comparisonReadiness(evidence) {
9
+ const required = [
10
+ 'bmadInitializationPassed', 'taskLocalTemporaryDirectory',
11
+ 'methodArtifactsAllowed', 'gitMetadataWritable',
12
+ 'synchronousSubagentsAvailable', 'descendantUsageMeasured',
13
+ 'descendantCancellationVerified', 'equalHostPermissions',
14
+ 'frozenMethodRevisions', 'independentAcceptanceChecks',
15
+ ];
16
+ const missing = required.filter(key => evidence[key] !== true);
17
+ return { ready: missing.length === 0, missing,
18
+ limitation: 'Requires reviewed native evidence. A true label alone proves no capability.' };
19
+ }
20
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
21
+ const file = process.argv[2];
22
+ if (!file) throw new Error('Usage: node scripts/comparison-readiness.mjs EVIDENCE_JSON');
23
+ const result = comparisonReadiness(JSON.parse(fs.readFileSync(file, 'utf8')));
24
+ console.log(JSON.stringify(result, null, 2));
25
+ if (!result.ready) process.exitCode = 2;
26
+ }
@@ -0,0 +1,42 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ const arms = ['none', 'devmethod', 'bmad'];
5
+ const nonempty = value => typeof value === 'string' && value.trim().length > 0;
6
+ const finite = value => typeof value === 'number' && Number.isFinite(value) && value >= 0;
7
+ /** Validate a complete matched batch, including blocked/failed runs. Never dispatch models. */
8
+ export function summarize(batch) {
9
+ if (!batch || batch.format !== 1 || !Array.isArray(batch.runs) || !batch.runs.length) throw new Error('Expected format 1 nonempty batch');
10
+ const { budget } = batch;
11
+ if (!budget || !Number.isSafeInteger(budget.runs) || budget.runs < 1 || !Number.isSafeInteger(budget.tokens) || budget.tokens < 1 || !finite(budget.costUSD) || !nonempty(budget.authorization)) throw new Error('Explicit run/token/cost budget and authorization required');
12
+ if (batch.runs.length > budget.runs) throw new Error('Run budget exceeded');
13
+ const groups = new Map();
14
+ const matchedKeys = ['fixtureDigest', 'promptDigest', 'hostVersion', 'model', 'toolsDigest', 'permissionsDigest', 'perRunTokens', 'perRunCostUSD', 'timeoutSeconds'];
15
+ const digests = ['fixtureDigest', 'promptDigest', 'toolsDigest', 'permissionsDigest'];
16
+ for (const run of batch.runs) {
17
+ if (!run || !nonempty(run.caseId) || !Number.isSafeInteger(run.repetition) || run.repetition < 1 || !arms.includes(run.arm) || !['passed', 'failed', 'blocked', 'timeout'].includes(run.status)) throw new Error('Invalid run identity or status');
18
+ if (!matchedKeys.every(key => ['perRunTokens', 'perRunCostUSD', 'timeoutSeconds'].includes(key) ? finite(run[key]) && (key === 'perRunCostUSD' || run[key] > 0) : nonempty(run[key])) || !digests.every(key => /^[a-f0-9]{64}$/.test(run[key]))) throw new Error('Missing pinned matched conditions');
19
+ if (!nonempty(run.methodRevision) || !nonempty(run.wrapper) || (run.arm === 'none' && run.methodRevision !== 'none')) throw new Error('Pin method revision and invocation wrapper');
20
+ if (!nonempty(run.evidence) || !nonempty(run.review)) throw new Error('Evidence reference and review or blocked reason required');
21
+ for (const metric of ['tokens', 'costUSD', 'elapsedSeconds']) if (run[metric] !== null && !finite(run[metric])) throw new Error('Usage must be measured nonnegative values or null (unavailable)');
22
+ if ((run.tokens !== null && run.tokens > run.perRunTokens) || (run.costUSD !== null && run.costUSD > run.perRunCostUSD)) throw new Error('Per-run budget exceeded');
23
+ const key = `${run.caseId}:${run.repetition}`;
24
+ const group = groups.get(key) ?? [];
25
+ if (group.some(other => other.arm === run.arm)) throw new Error('Duplicate arm');
26
+ if (group.length && matchedKeys.some(key => run[key] !== group[0][key])) throw new Error('Unmatched conditions');
27
+ group.push(run); groups.set(key, group);
28
+ }
29
+ for (const group of groups.values()) if (group.length !== 3) throw new Error('Each case/repetition requires all three arms, including blocked records');
30
+ for (const metric of ['tokens', 'costUSD']) if (batch.runs.reduce((sum, run) => sum + (run[metric] ?? 0), 0) > budget[metric]) throw new Error('Total budget exceeded');
31
+ const cells = [];
32
+ for (const caseId of new Set(batch.runs.map(run => run.caseId))) for (const arm of arms) {
33
+ const runs = batch.runs.filter(run => run.caseId === caseId && run.arm === arm);
34
+ cells.push({ caseId, arm, denominator: runs.length, ...Object.fromEntries(['passed', 'failed', 'blocked', 'timeout'].map(status => [status, runs.filter(run => run.status === status).length])) });
35
+ }
36
+ return { format: 1, cells, usageComplete: batch.runs.every(run => run.tokens !== null && run.costUSD !== null),
37
+ limitation: 'Validates record consistency only. Evidence authenticity and behavioral claims require independent inspection; no aggregate superiority score.' };
38
+ }
39
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
40
+ try { if (process.argv.length !== 3) throw new Error('Usage: node scripts/comparison.mjs BATCH_JSON'); console.log(JSON.stringify(summarize(JSON.parse(fs.readFileSync(process.argv[2]))), null, 2)); }
41
+ catch (error) { console.error(error.message); process.exitCode = 2; }
42
+ }
@@ -0,0 +1,64 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { checkPath } from '../dist/filesystem.js';
7
+
8
+ const root = fileURLToPath(new URL('../', import.meta.url));
9
+ export const fixtures = JSON.parse(fs.readFileSync(path.join(root, 'evaluation/fixtures.json'))).cases;
10
+ const hash = data => createHash('sha256').update(data).digest('hex');
11
+ export function snapshot(directory) {
12
+ checkPath(path.resolve(directory));
13
+ const files = {};
14
+ function walk(dir, prefix = '') {
15
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true }).sort((a,b) => a.name.localeCompare(b.name))) {
16
+ if (entry.name === '.git') continue;
17
+ const relative = prefix + entry.name;
18
+ if (entry.isSymbolicLink()) throw new Error(`Symbolic evidence path: ${relative}`);
19
+ if (entry.isDirectory()) walk(path.join(dir, entry.name), relative + '/');
20
+ else if (entry.isFile()) files[relative] = hash(fs.readFileSync(path.join(dir, entry.name)));
21
+ else throw new Error(`Non-regular evidence path: ${relative}`);
22
+ }
23
+ }
24
+ walk(directory);
25
+ return files;
26
+ }
27
+ export function prepare(caseId, destination) {
28
+ const fixture = fixtures.find(item => item.id === caseId);
29
+ if (!fixture) throw new Error('Unknown case');
30
+ destination = path.resolve(destination);
31
+ checkPath(destination);
32
+ if (fs.existsSync(destination)) throw new Error('Use a fresh destination');
33
+ const source = path.join(root, fixture.directory);
34
+ if (JSON.stringify(Object.entries(snapshot(source)).sort()) !== JSON.stringify(Object.entries(fixture.files).sort())) throw new Error('Fixture differs from pinned manifest');
35
+ fs.cpSync(source, destination, { recursive: true, errorOnExist: true, force: false });
36
+ return { format: 1, caseId, destination, baseline: snapshot(destination), prompt: fixture.prompt };
37
+ }
38
+ /** Objective local checks only. This function never invokes or certifies a model. */
39
+ export function collect(caseId, destination, baseline, verifier) {
40
+ const fixture = fixtures.find(item => item.id === caseId);
41
+ if (!fixture) throw new Error('Unknown case');
42
+ const current = snapshot(destination);
43
+ for (const [name, digest] of Object.entries(fixture.files)) {
44
+ if (baseline[name] !== digest) throw new Error('Baseline does not contain the pinned fixture');
45
+ }
46
+ const changed = [...new Set([...Object.keys(baseline), ...Object.keys(current)])].filter(name => baseline[name] !== current[name]);
47
+ const unauthorizedChanges = changed.filter(name => !fixture.allowedChanges.includes(name));
48
+ const testsIntact = current['acceptance.test.mjs'] === fixture.files['acceptance.test.mjs'];
49
+ // Do not run replaced tests. A native evaluation must separately review model claims.
50
+ const environment = { ...process.env };
51
+ delete environment.NODE_TEST_CONTEXT;
52
+ const check = testsIntact ? verifier ? verifier(destination, fixture.check.slice(1)) : spawnSync(process.execPath, fixture.check.slice(1), { cwd: destination, env: environment, encoding: 'utf8', timeout: 30000, maxBuffer: 1024 * 1024 }) : undefined;
53
+ return { format: 1, caseId, current, changed, unauthorizedChanges, testsIntact,
54
+ check: check ? { exit: check.status, signal: check.signal, stdout: check.stdout, stderr: check.stderr, error: check.error?.message } : { skipped: 'Acceptance tests changed' },
55
+ behavioralReview: 'pending', nativeHostEvidence: 'not established by this collector' };
56
+ }
57
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
58
+ try {
59
+ const [command, caseId, destination, baselineFile] = process.argv.slice(2);
60
+ if (!caseId || !destination || !['prepare', 'collect'].includes(command) || (command === 'collect' && !baselineFile)) throw new Error('Usage: node scripts/evaluation.mjs prepare CASE FRESH_DIR | collect CASE DIR BASELINE_JSON');
61
+ const result = command === 'prepare' ? prepare(caseId, destination) : collect(caseId, destination, JSON.parse(fs.readFileSync(baselineFile)).baseline);
62
+ console.log(JSON.stringify(result, null, 2));
63
+ } catch (error) { console.error(error.message); process.exitCode = 2; }
64
+ }
@@ -0,0 +1,30 @@
1
+ // Deterministic fixture walkthrough, not a native agent evaluation.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { captureContext } from '../dist/mission.js';
7
+ import { inspectCheckpoint } from '../dist/checkpoint.js';
8
+ import { digest } from '../dist/records.js';
9
+ const root = fileURLToPath(new URL('../examples/fullstack/', import.meta.url));
10
+ const mission = JSON.parse(fs.readFileSync(path.join(root, 'mission.json'), 'utf8'));
11
+ const before = captureContext(root, mission);
12
+ const result = spawnSync(process.platform === 'win32' ? 'npm.cmd' : 'npm', ['test'], {
13
+ cwd: root, encoding: 'utf8', timeout: 120000, maxBuffer: 1024 * 1024,
14
+ env: { ...process.env, NEXT_TELEMETRY_DISABLED: '1' }
15
+ });
16
+ const log = `${result.stdout ?? ''}\n${result.stderr ?? ''}\nExit: ${result.status}; error: ${result.error?.message ?? 'none'}\n`;
17
+ fs.mkdirSync(path.join(root, 'evidence'), { recursive: true });
18
+ fs.writeFileSync(path.join(root, 'evidence/unit.log'), log);
19
+ const outcome = result.status === 0 ? 'passed' : 'failed';
20
+ const checkpoint = { format: 1, scope: 'AC-TITLE unit/HTTP/model verification only. AC-STORE and AC-WEB remain separate checks.',
21
+ status: 'active', nextAction: mission.nextAction, git: before.git,
22
+ sources: before.sources.map(({ id, path, sha256 }) => ({ id, path, sha256 })),
23
+ evidence: [{ id: 'unit', path: 'evidence/unit.log', sha256: digest(log), sourceIds: before.sources.map(s => s.id), dependsOn: [],
24
+ outcome, criterionIds: ['AC-TITLE'], kind: 'automated', revision: `${before.git.commit}; working-content ${before.git.diffSha256}` }] };
25
+ fs.writeFileSync(path.join(root, 'evidence/context.json'), JSON.stringify(before, null, 2));
26
+ fs.writeFileSync(path.join(root, 'evidence/checkpoint.json'), JSON.stringify(checkpoint, null, 2));
27
+ const report = inspectCheckpoint(root, checkpoint);
28
+ console.log(JSON.stringify({ kind: 'deterministic-fixture', command: 'npm test', outcome, checkpoint: 'examples/fullstack/evidence/checkpoint.json', report,
29
+ limitation: 'This invocation checks unit/HTTP/model only. Database, production e2e and native host behavior are separate evidence.' }, null, 2));
30
+ if (result.status !== 0 || report.status !== 'ready') process.exitCode = 1;
@@ -0,0 +1,13 @@
1
+ import path from 'node:path';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { checkPath } from '../../dist/filesystem.js';
4
+ /** macOS-only verified fixture runner. No network or writable filesystem. */
5
+ export function verifyFixture(directory, args) {
6
+ if (process.platform !== 'darwin') return { status:null, error:new Error('Native verification adapter currently supports macOS only.'),stdout:'',stderr:'' };
7
+ directory=path.resolve(directory);checkPath(directory);
8
+ const roots=[directory,path.dirname(process.execPath),'/System','/usr','/bin','/Library','/opt/homebrew','/dev'];
9
+ const profile=`(version 1) (allow default) (deny network*) (deny file-write*) (deny file-read* (subpath "/Users") (subpath "/private/tmp") (subpath "/private/var/folders")) (allow file-read-metadata) (allow file-read* ${roots.map(p=>`(subpath ${JSON.stringify(p)})`).join(' ')}) (allow file-write* (literal "/dev/null"))`;
10
+ return spawnSync('/usr/bin/sandbox-exec',['-p',profile,process.execPath,...args],{
11
+ cwd:directory,env:{PATH:process.env.PATH,LANG:'en_US.UTF-8'},encoding:'utf8',timeout:30000,maxBuffer:1024*1024
12
+ });
13
+ }
@@ -0,0 +1,17 @@
1
+ /** Reconcile a complete private app-server stream; cumulative totals are not deltas. */
2
+ export function meterCodexTree(events, root) {
3
+ const ids=new Set([root]), totals=new Map(), active=new Map(), ended=new Set();
4
+ for(const e of events) {
5
+ const p=e.params ?? {}, item=p.item;
6
+ if(item?.type==='subAgentActivity' && ids.has(p.threadId)) ids.add(item.agentThreadId);
7
+ if(e.method==='thread/started' && ids.has(p.thread?.parentThreadId))ids.add(p.thread.id);
8
+ if(e.method==='thread/tokenUsage/updated')totals.set(p.threadId,p.tokenUsage?.total);
9
+ if(e.method==='turn/started'){active.set(p.threadId,p.turn.id);ended.delete(p.threadId);}
10
+ if(e.method==='turn/completed'){active.delete(p.threadId);ended.add(p.threadId);}
11
+ }
12
+ const missing=[...ids].filter(id=>!id || !ended.has(id) || active.has(id) || !['inputTokens','outputTokens'].every(k=>Number.isSafeInteger(totals.get(id)?.[k]) && totals.get(id)[k]>=0));
13
+ // Usage for an undiscovered thread means attribution is incomplete.
14
+ for(const id of totals.keys())if(!ids.has(id))missing.push(id);
15
+ const usage=missing.length?null:{inputTokens:[...ids].reduce((n,id)=>n+totals.get(id).inputTokens,0),outputTokens:[...ids].reduce((n,id)=>n+totals.get(id).outputTokens,0),costUSD:null};
16
+ return {threads:[...ids],missing,usage};
17
+ }
@@ -0,0 +1,44 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { codexEnvironment } from './codex.mjs';
3
+
4
+ /** Private stdio server, bounded lifetime. Never attaches to the desktop daemon. */
5
+ export function startCodexRpc({ cwd, timeoutMs = 120000, onEvent = () => {}, command = 'codex', args = ['app-server', '--stdio', '--disable', 'apps', '--disable', 'plugins', '--disable', 'remote_plugin'] }) {
6
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 120000) throw new Error('Invalid RPC lifetime');
7
+ const child = spawn(command, args,
8
+ { cwd, env: codexEnvironment(), detached: process.platform !== 'win32', stdio: ['pipe', 'pipe', 'pipe'] });
9
+ const pending = new Map(); let next = 0, buffer = '', bytes = 0, stopped = false;
10
+ const kill = () => { try { process.platform === 'win32' ? child.kill('SIGKILL') : process.kill(-child.pid, 'SIGKILL'); } catch {} };
11
+ function close(error = new Error('RPC server closed')) {
12
+ if (stopped) return; stopped = true; clearTimeout(timer); process.removeListener('exit',kill); kill();
13
+ for (const { reject } of pending.values()) reject(error); pending.clear();
14
+ }
15
+ process.once('exit',kill);
16
+ const timer = setTimeout(() => close(new Error('RPC lifetime exceeded')), timeoutMs);
17
+ function send(message) { if (stopped) throw new Error('RPC closed'); child.stdin.write(JSON.stringify(message)+'\n'); }
18
+ child.stdout.setEncoding('utf8');
19
+ child.stdout.on('data', chunk => {
20
+ bytes += Buffer.byteLength(chunk); if (bytes > 2*1024*1024) return close(new Error('RPC output limit'));
21
+ buffer += chunk;
22
+ let end;
23
+ while ((end = buffer.indexOf('\n')) >= 0) {
24
+ const line = buffer.slice(0,end); buffer = buffer.slice(end+1); if (!line.trim()) continue;
25
+ try {
26
+ const message = JSON.parse(line);
27
+ if ('id' in message && !message.method) {
28
+ const call = pending.get(message.id); if (!call) continue; pending.delete(message.id);
29
+ message.error ? call.reject(new Error(JSON.stringify(message.error))) : call.resolve(message.result);
30
+ } else if ('id' in message) {
31
+ // Unattended probes never approve tool execution or fulfill external requests.
32
+ send({ id: message.id, error: { code: -32601, message: 'Client requests disabled in probe' } });
33
+ } else onEvent(message);
34
+ } catch (error) { close(error); }
35
+ }
36
+ });
37
+ child.stderr.on('data', chunk => { bytes += chunk.length; if (bytes > 2*1024*1024) close(new Error('RPC output limit')); });
38
+ child.on('error', close); child.on('close', () => close()); child.stdin.on('error', close);
39
+ return { close, notify: send, call(method, params = {}) {
40
+ if (stopped) return Promise.reject(new Error('RPC closed'));
41
+ const id = ++next;
42
+ return new Promise((resolve,reject) => { pending.set(id,{resolve,reject}); send({ id, method, params }); });
43
+ } };
44
+ }
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {startCodexRpc} from './codex-rpc.mjs';
4
+ import {meterCodexTree} from './codex-meter.mjs';
5
+ export async function runCodexTask({cwd,prompt,logFile,rpcFactory=startCodexRpc}) {
6
+ const events=[],turns=new Map(),drained=[];let root,finish;const done=new Promise(resolve=>{finish=resolve;});
7
+ const log=fs.openSync(logFile,'wx');
8
+ const rpc=rpcFactory({cwd,onEvent(e){
9
+ if(e.method==='account/rateLimits/updated')return;
10
+ events.push(e);fs.writeSync(log,JSON.stringify(e)+'\n');const p=e.params;
11
+ if(e.method==='turn/started')turns.set(p.threadId,p.turn.id);
12
+ if(e.method==='turn/completed'){turns.delete(p.threadId);if(p.threadId===root)finish(p.turn.status);if(!turns.size)drained.splice(0).forEach(resolve=>resolve());}
13
+ }});
14
+ let cancelled=false;const cancel=()=>{cancelled=true;finish('cancelled');};process.on('SIGINT',cancel);process.on('SIGTERM',cancel);
15
+ let status='failed',error,timer;
16
+ try{
17
+ await rpc.call('initialize',{clientInfo:{name:'devmethod_comparison',version:'0.1.0'},capabilities:{experimentalApi:true}});if(cancelled){status='cancelled';throw new Error('Cancelled before thread start');}rpc.notify({method:'initialized'});
18
+ const tmp=path.join(cwd,'.runtime/tmp');fs.mkdirSync(tmp,{recursive:true});
19
+ const start=await rpc.call('thread/start',{cwd,model:'gpt-5.6-sol',ephemeral:true,approvalPolicy:'never',sandbox:'workspace-write',config:{'model_reasoning_effort':'low','features.multi_agent':true,'agents.max_threads':4,'agents.max_depth':1,'web_search':'disabled','features.apps':false,'features.plugins':false,'features.remote_plugin':false,'sandbox_workspace_write.writable_roots':[cwd,path.join(cwd,'.git')],'sandbox_workspace_write.network_access':false,'sandbox_workspace_write.exclude_tmpdir_env_var':true,'sandbox_workspace_write.exclude_slash_tmp':true,'shell_environment_policy.inherit':'core','shell_environment_policy.set':{TMPDIR:tmp,UV_OFFLINE:'1',UV_PYTHON_DOWNLOADS:'never'}}});root=start.thread.id;if(cancelled){status='cancelled';throw new Error('Cancelled before model dispatch');}
20
+ await rpc.call('turn/start',{threadId:root,effort:'low',input:[{type:'text',text:prompt,text_elements:[]}]});
21
+ status=await Promise.race([done,new Promise(resolve=>{timer=setTimeout(()=>resolve('timeout'),110000);})]);
22
+ if(status==='completed'&&turns.size)status='incomplete';
23
+ if(status==='timeout'||status==='cancelled'||turns.size){
24
+ await Promise.allSettled([...turns].map(([threadId,turnId])=>rpc.call('turn/interrupt',{threadId,turnId})));
25
+ if(turns.size){let drainTimer;try{await Promise.race([new Promise(resolve=>drained.push(resolve)),new Promise(resolve=>{drainTimer=setTimeout(resolve,1500);})]);}finally{clearTimeout(drainTimer);}}
26
+ }
27
+ }catch(e){error=e.message;}finally{clearTimeout(timer);process.removeListener('SIGINT',cancel);process.removeListener('SIGTERM',cancel);rpc.close();fs.closeSync(log);}
28
+ return {status:status==='completed'?'exited':status,root,error,...meterCodexTree(events,root)};
29
+ }
@@ -0,0 +1,28 @@
1
+ /** Codex CLI 0.147.0 adapter. Optional tooling, never part of offline init. */
2
+ export const codexVersion = 'codex-cli 0.147.0';
3
+ export function codexArguments({ directory, model }) {
4
+ if (model !== 'gpt-5.6-sol') throw new Error('This pilot pins gpt-5.6-sol.');
5
+ return ['exec', '--ignore-user-config', '--ephemeral', '--json', '--color', 'never',
6
+ '--disable', 'apps', '--disable', 'plugins', '--disable', 'remote_plugin',
7
+ '--sandbox', 'workspace-write', '-C', directory, '--model', model,
8
+ '-c', 'model_reasoning_effort="low"', '-c', 'approval_policy="never"',
9
+ '-c', 'web_search="disabled"', '-c', 'sandbox_workspace_write.network_access=false',
10
+ '-c', 'sandbox_workspace_write.exclude_tmpdir_env_var=true',
11
+ '-c', 'sandbox_workspace_write.exclude_slash_tmp=true',
12
+ '-c', 'features.multi_agent=false', '-c', 'shell_environment_policy.inherit="core"',
13
+ '-c', 'shell_environment_policy.ignore_default_excludes=false', '-'];
14
+ }
15
+ export function codexUsage(lines) {
16
+ const turns = lines.filter(e => e.type === 'turn.completed');
17
+ if (!turns.length) return null;
18
+ const fields = ['input_tokens', 'output_tokens'];
19
+ if (turns.some(t => fields.some(k => !Number.isSafeInteger(t.usage?.[k]) || t.usage[k] < 0))) return null;
20
+ return { inputTokens: turns.reduce((n,t) => n+t.usage.input_tokens,0), outputTokens: turns.reduce((n,t) => n+t.usage.output_tokens,0),
21
+ cachedInputTokens: turns.every(t => Number.isSafeInteger(t.usage.cached_input_tokens)) ? turns.reduce((n,t) => n+t.usage.cached_input_tokens,0) : null,
22
+ costUSD: null };
23
+ }
24
+
25
+ export function codexEnvironment(source = process.env) {
26
+ const allowed = ['PATH','HOME','USER','LOGNAME','TMPDIR','TEMP','TMP','SystemRoot','SYSTEMROOT','WINDIR','COMSPEC','PATHEXT','LANG','LC_ALL','CODEX_HOME'];
27
+ return Object.fromEntries(allowed.filter(k => source[k] !== undefined).map(k => [k,source[k]]));
28
+ }
@@ -0,0 +1,16 @@
1
+ const pause=ms=>new Promise(r=>setTimeout(r,ms));
2
+ exports.prepare=async page=>{
3
+ await page.evaluate(()=>localStorage.clear());await page.reload();
4
+ await page.locator('#demo-button').click();
5
+ await page.locator('.workspace').scrollIntoViewIfNeeded();
6
+ };
7
+ exports.perform=async page=>{
8
+ await pause(1000);
9
+ await page.locator('#task-title').pressSequentially('Préparer ma prochaine idée',{delay:42});
10
+ await page.locator('#task-note').fill('Un premier pas concret, aujourd’hui.');
11
+ await page.locator('.add-button').click();await pause(1000);
12
+ await page.locator('.task-check').first().click();await pause(850);
13
+ await page.locator('[data-filter="done"]').click();await pause(1000);
14
+ await page.locator('[data-filter="all"]').click();await pause(850);
15
+ await page.reload();await page.locator('.workspace').scrollIntoViewIfNeeded();
16
+ };
@@ -0,0 +1,22 @@
1
+ """Encode recorded scenes with French system speech, MP4 subtitles, and an SRT sidecar.
2
+ Requires ffmpeg, ffprobe and macOS say with the Thomas voice. Run after record-demo.cjs.
3
+ """
4
+ import json, pathlib, subprocess
5
+ root=pathlib.Path('docs/media/from-zero');tmp=pathlib.Path('/private/tmp/devmethod-video');tmp.mkdir(exist_ok=True)
6
+ scenes=json.loads((root/'scenes.json').read_text())
7
+ def run(args): subprocess.run(args,check=True,stdout=subprocess.DEVNULL,stderr=subprocess.PIPE)
8
+ def stamp(t):
9
+ ms=round(t*1000);return f'{ms//3600000:02}:{ms//60000%60:02}:{ms//1000%60:02},{ms%1000:03}'
10
+ subs=[];elapsed=0
11
+ for i,s in enumerate(scenes):
12
+ text=tmp/f'{i}.txt';text.write_text(s['voice'])
13
+ if not (tmp/f'{i}.aiff').exists(): run(['say','-v','Thomas','-r','155','-f',str(text),'-o',str(tmp/f'{i}.aiff')])
14
+ run(['ffmpeg','-y','-i',str(tmp/f'scene-{i}.webm'),'-i',str(tmp/f'{i}.aiff'),'-vf','fps=30,scale=1280:720,format=yuv420p','-af','apad','-t',str(s['duration']),'-c:v','libx264','-preset','medium','-crf','21','-c:a','aac','-b:a','160k',str(tmp/f'{i}.mp4')])
15
+ sentences=[v.strip()+'.' for v in s['voice'].split('.') if v.strip()];voice_duration=s['duration']-1.2;cursor=elapsed
16
+ for sentence in sentences:
17
+ duration=voice_duration*len(sentence)/sum(map(len,sentences));subs.append(f'{len(subs)+1}\n{stamp(cursor)} --> {stamp(cursor+duration)}\n{sentence}\n');cursor+=duration
18
+ elapsed+=s['duration']
19
+ (root/'devmethod-demo.fr.srt').write_text('\n'.join(subs))
20
+ (tmp/'concat.txt').write_text(''.join(f"file '{i}.mp4'\n" for i in range(len(scenes))))
21
+ run(['ffmpeg','-y','-f','concat','-safe','0','-i',str(tmp/'concat.txt'),'-i',str(root/'devmethod-demo.fr.srt'),'-map','0:v','-map','0:a','-map','1:0','-c:v','copy','-c:a','copy','-c:s','mov_text','-metadata:s:s:0','language=fra','-movflags','+faststart',str(root/'devmethod-demo.fr.mp4')])
22
+ print(f'Encoded {elapsed:.1f}s video with French narration and subtitles.')
@@ -0,0 +1,30 @@
1
+ // Requires Playwright and Chrome; both demo servers must be running on loopback.
2
+ const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const root = process.cwd();
6
+ const output = process.env.DEMO_OUTPUT || '/private/tmp/devmethod-video';
7
+ const scenes = JSON.parse(fs.readFileSync('docs/media/from-zero/scenes.json','utf8'));
8
+ const esc = s => s.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;');
9
+ const sleep = ms => new Promise(r=>setTimeout(r,ms));
10
+ function card(s,i){
11
+ const visual=i===3?`<img class="visual" src="data:image/png;base64,${fs.readFileSync('examples/visual-pilot/directions-v1.png').toString('base64')}" alt="Trois directions du pilote Lisière">`:'';
12
+ return `<!doctype html><html lang="fr"><meta charset="utf-8"><style>*{box-sizing:border-box}body{margin:0;background:#142c29;color:#fbf5e8;font-family:Arial,sans-serif;width:1280px;height:720px;overflow:hidden}.frame{padding:65px 76px;height:100%;position:relative}.brand{font-size:23px;letter-spacing:2px}.eyebrow{color:#f4b793;letter-spacing:3px;font-size:15px;margin:70px 0 25px}h1{font:normal 74px/1.05 Georgia,serif;letter-spacing:-2px;max-width:920px;margin:0 0 25px}p{font-size:25px;line-height:1.5;white-space:pre-line;max-width:920px;color:#dce5da;margin:0}.line{height:2px;background:#c4633b;width:90px;margin-bottom:26px}.foot{position:absolute;bottom:32px;left:76px;right:76px;display:flex;justify-content:space-between;color:#b5c6bd;font-size:15px}.visual{position:absolute;right:45px;top:150px;width:700px;border-radius:6px}.with-visual h1{font-size:49px;max-width:390px}.with-visual p{font-size:21px;max-width:360px}.with-visual .eyebrow{margin-top:62px}.num{position:absolute;right:72px;top:55px;font:italic 46px Georgia;color:#54736c}</style><div class="frame ${visual?'with-visual':''}"><div class="brand">DevMethod<span class="num">0${i+1}</span></div><div class="eyebrow">${esc(s.eyebrow)}</div><div class="line"></div><h1>${esc(s.title)}</h1><p>${esc(s.text)}</p>${visual}<div class="foot"><span>Du besoin aux preuves.</span><span>Essais réels encadrés · Septembre 2026</span></div></div></html>`;
13
+ }
14
+ (async()=>{
15
+ fs.mkdirSync(output,{recursive:true});
16
+ const browser=await chromium.launch({channel:'chrome',headless:true});
17
+ for(let i=0;i<scenes.length;i++){
18
+ const s=scenes[i];const context=await browser.newContext({viewport:{width:1280,height:720},recordVideo:{dir:output,size:{width:1280,height:720}}});const page=await context.newPage();
19
+ if(s.kind==='app'){
20
+ await page.goto('http://127.0.0.1:8766');
21
+ // Action sequence lives separately, grounded in the finished app's controls.
22
+ const actions=require(path.join(root,'scripts/media/demo-actions.cjs'));
23
+ await actions.prepare(page);
24
+ await page.screenshot({path:path.join(output,`scene-${i}.png`)});
25
+ const start=Date.now();await actions.perform(page);await sleep(Math.max(0,s.duration*1000-(Date.now()-start)));
26
+ }else{await page.setContent(card(s,i));await page.screenshot({path:path.join(output,`scene-${i}.png`)});await sleep(s.duration*1000);}
27
+ const video=page.video();await context.close();fs.copyFileSync(await video.path(),path.join(output,`scene-${i}.webm`));console.log(`Scene ${i+1}/${scenes.length} recorded`);
28
+ }
29
+ await browser.close();
30
+ })().catch(e=>{console.error(e);process.exit(1)});
@@ -0,0 +1 @@
1
+ const fs=require('fs'),assert=require('node:assert/strict');const{chromium}=require(process.env.PLAYWRIGHT_MODULE || 'playwright');const r='/private/tmp/devmethod-shortfilm';(async()=>{const b=await chromium.launch({channel:'chrome',headless:true});const p=await b.newPage({viewport:{width:1440,height:1000}}),errs=[];p.on('pageerror',e=>errs.push(e.message));await p.goto('http://localhost:8768/app/');assert.equal(await p.locator('.book').count(),3);assert.equal(await p.locator('#storage-alert').isVisible(),false);await p.locator('.book').first().getByRole('button',{name:'Marquer terminé'}).click();assert.equal(await p.evaluate(()=>document.activeElement.textContent),'Rouvrir');await p.getByRole('button',{name:'Terminés',exact:true}).click();await p.screenshot({path:r+'/desktop.png'});await p.getByRole('button',{name:'Rouvrir'}).click();assert.equal(await p.locator('#empty').isVisible(),true);await p.getByRole('button',{name:'Tous',exact:true}).click();await p.getByRole('button',{name:/Ajouter un livre/}).click();await p.getByLabel('Titre',{exact:true}).fill('Les heures claires');await p.getByLabel('Auteur',{exact:true}).fill('Camille Laurent');await p.screenshot({path:r+'/modal.png'});await p.getByRole('button',{name:'Ajouter',exact:true}).click();assert.equal(await p.locator('.book').count(),4);await p.reload();assert.equal(await p.locator('.book').count(),4);await p.setViewportSize({width:390,height:844});await p.screenshot({path:r+'/mobile.png',fullPage:true});assert(await p.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));assert.equal(errs.length,0);await p.evaluate(()=>localStorage.setItem('lisiere.books.v1','broken'));await p.reload();assert(await p.locator('#storage-alert').isVisible());await p.locator('.book').first().getByRole('button').click();assert.equal(await p.evaluate(()=>localStorage.getItem('lisiere.books.v1')),'broken');console.log('PASS: initial UI, filter, status/focus, empty state, modal, add, reload, mobile, malformed storage preserved; zero page errors.');await b.close()})();
@@ -0,0 +1,16 @@
1
+ import pathlib,json,subprocess,re
2
+ r=pathlib.Path('/private/tmp/devmethod-shortfilm');o=pathlib.Path('docs/media/visual-chain');s=json.loads((r/'scenes.json').read_text());t=json.loads((r/'times.json').read_text());base=t[0]['start'];total=t[-1]['start']-base
3
+ subs=[];concat=[]
4
+ def run(a):
5
+ p=subprocess.run(a,stdout=subprocess.DEVNULL,stderr=subprocess.PIPE)
6
+ if p.returncode:raise RuntimeError(p.stderr.decode()[-1500:])
7
+ def stamp(v):
8
+ m=round(v*1000);return f'{m//3600000:02}:{m//60000%60:02}:{m//1000%60:02},{m%1000:03}'
9
+ for i,scene in enumerate(s):
10
+ duration=t[i+1]['start']-t[i]['start'];run(['ffmpeg','-y','-i',str(r/f'audio/{i}.aiff'),'-af','apad','-t',str(duration),'-ar','48000','-c:a','pcm_s16le',str(r/f'audio/{i}.wav')]);concat.append(f"file '{r}/audio/{i}.wav'")
11
+ parts=re.split(r'(?<=[.!?])\s+',scene['voice']);cursor=t[i]['start']-base;spoken=float(subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration','-of','csv=p=0',str(r/f'audio/{i}.aiff')]))
12
+ for part in parts:
13
+ length=spoken*len(part)/sum(map(len,parts));subs.append(f'{len(subs)+1}\n{stamp(cursor)} --> {stamp(cursor+length)}\n{part}\n');cursor+=length
14
+ (r/'audio-concat.txt').write_text('\n'.join(concat));(o/'devmethod-du-besoin-au-produit.fr.srt').write_text('\n'.join(subs))
15
+ vf=f"[0:v]trim=start={base}:end={t[-1]['start']},setpts=PTS-STARTPTS,fps=25,format=yuv420p[v]"
16
+ run(['ffmpeg','-y','-i',str(r/'film.webm'),'-f','concat','-safe','0','-i',str(r/'audio-concat.txt'),'-i',str(o/'devmethod-du-besoin-au-produit.fr.srt'),'-filter_complex',vf,'-map','[v]','-map','1:a','-map','2:0','-t',str(total),'-c:v','libx264','-threads','4','-preset','veryfast','-crf','20','-c:a','aac','-b:a','192k','-c:s','mov_text','-metadata:s:a:0','language=fra','-metadata:s:s:0','language=fra','-movflags','+faststart',str(o/'devmethod-du-besoin-au-produit-4k.fr.mp4')]);print('FINAL',total,flush=True)
@@ -0,0 +1,4 @@
1
+ const fs=require('fs');const{chromium}=require(process.env.PLAYWRIGHT_MODULE || 'playwright');const r='/private/tmp/devmethod-shortfilm',repo=process.cwd();const onlyApp=process.argv.includes('--only-app');const scenes=JSON.parse(fs.readFileSync(r+'/scenes.json'));const esc=s=>s.replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;');const assetPaths={directions:repo+'/examples/visual-pilot/directions-v1.png',master:repo+'/examples/visual-pilot/editorial-mockup-v1.png',add:repo+'/docs/media/visual-chain/assets/add-book-v1.png',completed:repo+'/docs/media/visual-chain/assets/completed-v1.png',desktop:r+'/desktop.png',mobile:r+'/mobile.png'};const images={};for(const[k,v]of Object.entries(assetPaths))if(fs.existsSync(v))images[k]='data:image/png;base64,'+fs.readFileSync(v).toString('base64');
2
+ const css=`*{box-sizing:border-box}html,body{margin:0;background:#0e1d21;overflow:hidden;color:#f6f0e5;font-family:Arial}.stage{position:relative;width:1920px;height:1080px;transform:scale(2);transform-origin:0 0;overflow:hidden;background:radial-gradient(ellipse at 90% 100%,#25433f,#0e1d21 70%)}header{position:absolute;left:80px;right:80px;top:38px;display:flex;justify-content:space-between;color:#b0c6bf;font-size:20px;letter-spacing:2px}h1{position:absolute;left:80px;top:85px;font:64px/1.05 Georgia;font-weight:400;letter-spacing:-1.5px;margin:0}footer{position:absolute;left:80px;bottom:28px;font-size:16px;color:#a0b8b1}.counter{position:absolute;right:80px;bottom:28px;font-size:17px;color:#a0b8b1}.progress{position:absolute;bottom:0;left:0;height:4px;width:100%;transform:scaleX(0);transform-origin:left;background:#dc9972;animation:progress var(--duration) linear forwards}@keyframes progress{to{transform:scaleX(1)}}.command{background:#183137;border:1px solid #45625f;border-radius:16px;padding:28px;box-shadow:0 20px 60px #0002}.command small{font-size:16px;color:#cfad8d;letter-spacing:2px}.cmd{font:25px/1.45 Menlo,monospace;color:#f1b98e;white-space:pre-wrap;min-height:72px;margin:18px 0}.request{font-size:25px;line-height:1.4;color:#ecf1e8}.side{position:absolute;left:80px;top:250px;width:500px}.out{margin-top:28px;font-size:27px;line-height:1.5;color:#c6d7cc;animation:enter .5s 1.2s both}.visual{position:absolute;left:650px;top:215px;width:1190px;height:780px;display:flex;align-items:center;justify-content:center;overflow:hidden;border-radius:10px}.visual img{width:100%;height:100%;object-fit:contain;animation:enter .6s .7s both,drift var(--duration) ease-in-out both}.full{left:150px;width:1620px}.full img{max-width:1550px}.mobile img{max-width:440px}.choice{position:absolute;right:70px;bottom:30px;background:#a13a20;border:1px solid #ffd3ae;border-radius:12px;padding:18px 26px;font:30px Georgia;animation:enter .5s 1.8s both}.history{position:absolute;left:150px;bottom:82px;font-size:16px;color:#9fbab2}.central{position:absolute;left:210px;top:280px;width:1500px}.central .cmd{font-size:40px}.central .request{font-size:35px}.central .out{font-size:44px}.hero{position:absolute;left:140px;top:310px;font:110px/1.13 Georgia;max-width:1580px}.hero b{font-weight:400;color:#e9ae82}.sub{font:34px/1.6 Arial;color:#b5d0c2;animation:enter .6s 1s both}.mini{position:absolute;width:480px;height:650px;top:310px;background:#233b3b;border-radius:15px;padding:12px;animation:enter .55s both}.mini img{width:100%;height:100%;object-fit:contain}.mini.one{left:130px}.mini.two{left:720px;animation-delay:1.8s}.mini.three{left:1310px;animation-delay:3.5s}.arrow{position:absolute;top:600px;color:#e8a776;font-size:60px}.derivecmd{position:absolute;left:130px;right:130px;top:200px;font:24px Menlo;color:#f1b98e}.pair{position:absolute;top:245px;left:90px;right:90px;display:flex;gap:35px;height:700px}.pair figure{width:50%;margin:0;border-radius:12px;overflow:hidden;background:#213838;padding:15px;animation:enter .7s both}.pair img{width:100%;height:610px;object-fit:contain;animation:drift var(--duration) ease-in-out}.pair figcaption{font-size:23px;padding-top:20px}.pair figure+figure{animation-delay:1s}.options{position:absolute;left:370px;right:370px;bottom:70px;display:flex;justify-content:space-between;gap:20px}.options span{font:25px Georgia;background:#1e393b;padding:16px 30px;border-radius:8px;animation:enter .5s both}.options span:nth-child(2){animation-delay:1.6s}.options span:nth-child(3){animation-delay:3.2s}iframe{position:absolute;left:80px;top:230px;width:1440px;height:1000px;transform:scale(.77);transform-origin:0 0;border:0;border-radius:10px}aside{position:absolute;left:1290px;top:390px;width:510px}aside small{font-size:20px;color:#e6ae82}aside h2{font:57px/1.12 Georgia}aside p{font:27px/1.5 Arial;color:#c0d7c9}#cursor{position:absolute;width:22px;height:22px;border:3px solid #fff;background:#b95829;border-radius:50%;box-shadow:0 1px 10px #0009;z-index:20;transition:left .35s,top .35s;pointer-events:none;left:100px;top:300px}@keyframes enter{from{opacity:0;transform:translateY(24px)}to{opacity:1;transform:translateY(0)}}@keyframes drift{from{scale:.985}to{scale:1.035}}`;
3
+ function content(s,i){let x='';const command=`<div class="command"><small>COMMANDE DANS CODEX</small><div class="cmd" data-type="${esc(s.command)}"></div><div class="request">${esc(s.request)}</div></div>`;if(s.kind==='intro'||s.kind==='end')x=`<div class="hero">${s.kind==='intro'?'Une idée.<br><b>Un design. Un produit.</b>':'Votre projet,<br><b>étape après étape.</b>'}<p class="sub">${s.kind==='intro'?'Cadrage → design → architecture → réalisation → vérification':'github.com/montassarkhalloufi/DevMethod'}</p></div>`;else if(s.kind==='text')x=`<div class="central">${command}<div class="out">${esc(s.output)}</div></div>`;else if(s.kind==='derive')x=`<div class="derivecmd">${esc(s.command)} — ${esc(s.request)}</div><div class="mini one"><img src="${images.master}"></div><span class="arrow" style="left:640px">→</span><div class="mini two"><img src="${images.add}"></div><div class="mini three"><img src="${images.completed}"></div>`;else if(s.kind==='compare')x=`<div class="pair"><figure><img src="${images.completed}"><figcaption>Image dérivée du master</figcaption></figure><figure><img src="${images.desktop}"><figcaption>Écran réalisé et vérifié</figcaption></figure></div>`;else if(s.kind==='app')x='<iframe src="http://localhost:8768/app/"></iframe><aside><small>APPLICATION RÉELLE</small><h2 id="action">Votre bibliothèque</h2><p id="detail">Les contrôles agissent vraiment.</p></aside><div id="cursor"></div>';else x=(s.command?`<div class="side">${command}</div>`:'')+`<div class="visual ${s.command?'':'full'} ${s.asset==='mobile'?'mobile':''}"><img src="${images[s.asset]}">${s.kind==='choice'?'<div class="choice">Choix réel : A ✓</div>':''}</div>`+(s.kind==='options'?'<div class="options"><span>A · Éditorial</span><span>B · Atelier</span><span>C · Botanique</span></div>':'')+(['options','choice'].includes(s.kind)?'<div class="history">Choix conservé de la session préparatoire</div>':'');return`<style>${css}</style><div class="stage" style="--duration:${s.duration}s"><header><span>DevMethod / Lisière</span><span>${String(i+1).padStart(2,'0')} / ${scenes.length}</span></header><h1>${s.title}</h1>${x}<footer>Commandes types · résultats réels · montage accéléré</footer><span class="counter">4K / FR</span><div class="progress"></div></div>`;}
4
+ (async()=>{const b=await chromium.launch({channel:'chrome',headless:true});const c=await b.newContext({viewport:{width:3840,height:2160},recordVideo:{dir:r+'/raw',size:{width:3840,height:2160}}});const begun=Date.now(),p=await c.newPage(),times=[];await p.goto('http://localhost:8768/app/');await p.evaluate(()=>localStorage.removeItem('lisiere.books.v1'));for(let i=0;i<scenes.length;i++){if(onlyApp&&scenes[i].kind!=='app')continue;const s=scenes[i];await p.setContent(content(s,i));const start=Date.now();times.push({i,start:(start-begun)/1000});console.log('scene',i);await p.evaluate(()=>{document.querySelectorAll('[data-type]').forEach(e=>{const t=e.dataset.type;let n=0;const id=setInterval(()=>{e.textContent=t.slice(0,++n);if(n>=t.length)clearInterval(id)},18)})});if(s.kind==='app'){const f=p.frameLocator('iframe');const click=async loc=>{await loc.hover();await p.waitForTimeout(250);await loc.click();};const mark=async(t,d)=>{await p.locator('#action').evaluate((e,v)=>e.textContent=v,t);await p.locator('#detail').evaluate((e,v)=>e.textContent=v,d)};await f.locator('h1').waitFor();await p.locator('#cursor').evaluate(e=>e.remove());await p.frames().find(frame=>frame.url().includes(':8768/app/')).evaluate(()=>{const dot=document.createElement('div');dot.style.cssText='position:fixed;width:18px;height:18px;border:2px solid white;border-radius:50%;background:#b95829;z-index:999999;pointer-events:none;transform:translate(-50%,-50%);display:none';document.body.append(dot);document.addEventListener('pointermove',e=>{dot.style.display='block';dot.style.left=e.clientX+'px';dot.style.top=e.clientY+'px'})});await p.waitForTimeout(1500);await mark('Filtrer','Retrouver les livres à commencer.');await click(f.getByRole('button',{name:'À lire',exact:true}));await p.waitForTimeout(1800);await click(f.getByRole('button',{name:'Tous',exact:true}));await mark('Ajouter','Un vrai formulaire, une nouvelle lecture.');await click(f.getByRole('button',{name:/Ajouter un livre/}).first());await f.getByLabel('Titre',{exact:true}).fill('Les heures claires');await f.getByLabel('Auteur',{exact:true}).fill('Camille Laurent');await p.waitForTimeout(1600);await click(f.getByRole('button',{name:'Ajouter',exact:true}));await mark('Faire évoluer','À lire → En cours → Terminé');await click(f.locator('.book').filter({hasText:'Les heures claires'}).getByRole('button',{name:'Commencer',exact:true}));await p.waitForTimeout(1500);await click(f.locator('.book').filter({hasText:'Les heures claires'}).getByRole('button',{name:'Marquer terminé',exact:true}));await click(f.getByRole('button',{name:'Terminés',exact:true}));await p.waitForTimeout(1500);await mark('Retrouver','Les données restent après rechargement.');await p.locator('iframe').evaluate(e=>e.setAttribute("src",e.getAttribute("src")));await f.locator('h1').waitFor();if(await f.locator('.book').count()!==4||await f.locator('#storage-alert').isVisible())throw Error('Persistence failed in recorded scene');await click(f.getByRole('button',{name:'Terminés',exact:true}));await f.getByRole('heading',{name:'Les heures claires',exact:true}).waitFor();}await p.waitForTimeout(Math.max(0,s.duration*1000-(Date.now()-start)-300));if(['Choisir les technologies','Organiser le code','Relire et corriger'].includes(s.title)||['app','derive','compare'].includes(s.kind))await p.screenshot({path:r+`/qa-${i}.png`});await p.waitForTimeout(Math.max(0,s.duration*1000-(Date.now()-start)));}times.push({i:scenes.length,start:(Date.now()-begun)/1000});const v=p.video();await c.close();fs.copyFileSync(await v.path(),r+(onlyApp?'/app-final.webm':'/film.webm'));fs.writeFileSync(r+(onlyApp?'/app-final-times.json':'/times.json'),JSON.stringify(times));await b.close();})();
@@ -0,0 +1,34 @@
1
+ import json,pathlib,subprocess
2
+ r=pathlib.Path('/private/tmp/devmethod-shortfilm');(r/'audio').mkdir(exist_ok=True)
3
+ S=[]
4
+ def add(kind,title,voice,seconds,command='',request='',asset='',output=''):
5
+ S.append(dict(kind=kind,title=title,voice=voice,duration=seconds,command=command,request=request,asset=asset,output=output))
6
+ add('intro','De l’idée à l’application','Du besoin au design, des choix techniques au produit vérifié. Voici Dev Method sur un projet concret.',6)
7
+ add('text','Décrire le produit','D’abord, décrivez le produit et ce que son utilisateur doit pouvoir faire.',6,'$project-foundation explore','Une bibliothèque personnelle pour suivre mes lectures.',output='Ajouter un livre · Suivre sa lecture · Retrouver ses données')
8
+ add('text','Cadrer une première version','Frame fixe le périmètre : une bibliothèque personnelle, trois statuts, et une sauvegarde sur cet appareil.',7,'$project-foundation frame','Définis une première version utile et ses limites.',output='Ajouter · Filtrer · Changer de statut · Conserver les livres localement')
9
+ add('image','Demander trois directions','Avec design, demandez trois directions pour le même écran.',6,'$project-foundation design','Propose trois directions visuelles comparables.','directions')
10
+ add('options','Comparer, puis choisir','Éditorial, Atelier ou Botanique. Même contenu, trois personnalités visuelles.',6,asset='directions')
11
+ add('choice','Choisir A — Éditorial','Ici, le choix est A. Codex développe cette direction en master screen.',7,'$project-foundation design','Je choisis A. Finalise le master screen.','master')
12
+ add('image','Le master fixe la référence','Le master précise la typographie, les couleurs, les cartes et les boutons. Il est approuvé avant la suite.',8,asset='master')
13
+ add('derive','Décliner le master','La commande suivante reste design : génère les écrans à partir de ce master.',7,'$project-foundation design','Décline ce master : bibliothèque, ajout et lectures terminées.','master')
14
+ add('image','Écran 02 — Ajouter un livre','Voici le formulaire d’ajout, généré avec le master comme référence visuelle.',6,asset='add')
15
+ add('image','Écran 03 — Lectures terminées','Puis la vue des lectures terminées. La même direction accompagne un autre état du produit.',7,asset='completed')
16
+ add('text','Choisir les technologies','Architecture compare les options selon les besoins, le coût et la maintenance. Pour ce petit prototype local, HTML, CSS et JavaScript suffisent.',9,'$project-foundation architecture','Compare les options techniques adaptées à ce périmètre.',output='Prototype retenu : HTML + CSS + JavaScript · Stockage local au navigateur')
17
+ add('text','Organiser le code','Les règles des livres restent séparées de l’interface. Les données sauvegardées sont validées. Une synchronisation entre appareils demanderait une nouvelle décision.',9,'$project-foundation architecture','Définis les responsabilités, les risques et les compromis.',output='Interface ↔ Règles métier ↔ Sauvegarde locale · Limite : un appareil')
18
+ add('text','Préparer la réalisation','Le plan relie ces écrans aux actions à réaliser et aux vérifications attendues.',6,'$project-foundation plan','Prépare une première tranche à partir des écrans.',output='Bibliothèque + filtres → Formulaire → Statuts + sauvegarde')
19
+ add('text','Vérifier que la tranche est prête','Ready vérifie le périmètre, les dépendances et les critères de réussite avant de coder.',6,'$project-foundation ready VISUAL-1','Vérifie que cette tranche est prête.',output='Références disponibles · Interactions définies · Tests attendus')
20
+ add('text','Transformer les images en code','Implement construit l’interface et ses vrais comportements à partir des références.',6,'$project-foundation implement VISUAL-1','Implémente les écrans et leurs interactions.',output='Nouvelle implémentation à partir du master et des images dérivées')
21
+ add('app','L’application fonctionne','Passons au résultat. On filtre la bibliothèque. On ajoute un livre. On commence sa lecture, puis on le marque terminé. Après rechargement, les données sont toujours présentes.',20)
22
+ add('image','Aussi sur mobile','Sur mobile, les mêmes actions restent accessibles dans une disposition adaptée.',6,asset='mobile')
23
+ add('text','Relire et corriger','Review examine le code et les risques. Ici, la revue a conduit à corriger une alerte masquée et le maintien du focus.',8,'$project-foundation review VISUAL-1','Relis la tranche et signale les défauts à corriger.',output='Corrections réelles : affichage de l’alerte · Focus après changement de statut')
24
+ add('text','Vérifier le résultat','Verify contrôle les parcours utiles et les résultats des tests.',6,'$project-foundation verify VISUAL-1','Vérifie les parcours, les erreurs et le rendu.',output='6 tests Node réussis · Parcours Chrome · Sauvegarde · Mobile')
25
+ add('compare','Comparer au design','On compare ensuite le navigateur aux références, puis on corrige les écarts importants.',7,asset='desktop')
26
+ add('text','Livrer une tranche terminée','La livraison locale conserve le résultat et son état de reprise.',6,'$project-foundation integrate VISUAL-1','Consigne la livraison locale et la reprise.',output='Prototype local · Parcours vérifiés · Références conservées')
27
+ add('text','Reprendre sans repartir de zéro','Handoff conserve les décisions, les preuves et les limites. Next retrouve ensuite la suite pertinente, ou constate que le périmètre est terminé.',8,'$project-foundation handoff','Prépare un point de reprise fidèle à la livraison.',output='Décisions + résultat + limites → $project-foundation next')
28
+ add('end','Votre idée. Une chaîne visible.','Découvrez Dev Method sur GitHub et essayez cette chaîne sur votre projet.',6)
29
+ for i,s in enumerate(S):
30
+ p=r/'audio'/f'{i}.txt';p.write_text(s['voice'].replace('GitHub','guit-hub'))
31
+ subprocess.run(['say','-v','Thomas','-r','165','-f',str(p),'-o',str(r/'audio'/f'{i}.aiff')],check=True)
32
+ d=float(subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration','-of','csv=p=0',str(r/'audio'/f'{i}.aiff')]))
33
+ s['duration']=max(s['duration'],d+0.7)
34
+ (r/'scenes.json').write_text(json.dumps(S,ensure_ascii=False,indent=2));print('duration',sum(s['duration'] for s in S))