devmethod-ai 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/decision-architecture/SKILL.md +10 -0
- package/.agents/skills/decision-architecture/assets/ADR.md +6 -3
- package/.agents/skills/project-foundation/SKILL.md +2 -0
- package/.agents/skills/project-foundation/assets/CADRAGE.md +11 -0
- package/.agents/skills/project-foundation/assets/EXISTANT.md +14 -0
- package/.agents/skills/project-foundation/assets/OPPORTUNITES.md +10 -0
- package/.agents/skills/project-foundation/assets/PROJECT_PROFILE.md +3 -2
- package/.agents/skills/project-foundation/assets/REGLES.md +6 -0
- package/.agents/skills/project-foundation/assets/START_HERE.md +2 -0
- package/.agents/skills/project-foundation/references/delivery-planning.md +11 -0
- package/.agents/skills/project-foundation/references/exploration.md +11 -0
- package/.agents/skills/project-foundation/references/mission-context.md +31 -4
- package/.agents/skills/project-foundation/references/operating-commands.md +12 -4
- package/.agents/skills/project-foundation/references/work-sizing.md +1 -1
- package/.agents/skills/scoped-delivery/SKILL.md +5 -1
- package/.agents/skills/scoped-delivery/assets/MISSION.md +1 -1
- package/.agents/skills/scoped-delivery/assets/PLAN.md +20 -0
- package/.agents/skills/scoped-delivery/assets/REPRISE.md +10 -0
- package/.agents/skills/scoped-delivery/assets/REVIEW.md +34 -0
- package/.agents/skills/scoped-delivery/assets/SLICE.md +5 -0
- package/.agents/skills/scoped-delivery/assets/TICKET.md +21 -0
- package/.agents/skills/scoped-delivery/references/review-workflow.md +29 -0
- package/COMPATIBILITY.md +1 -1
- package/README.md +35 -14
- package/START_HERE.md +2 -0
- package/dist/cli.js +122 -90
- package/dist/review-app.js +462 -0
- package/dist/review-browser.js +567 -0
- package/dist/review-cli.js +68 -0
- package/dist/review-model.js +101 -0
- package/dist/review-open.js +19 -0
- package/dist/review-ui.css +782 -0
- package/dist/review.js +13 -0
- package/docs/ADR-007-conversation-and-mission-ownership.md +13 -0
- package/docs/ADR-008-review-presentation.md +13 -0
- package/docs/MISSIONS.md +1 -1
- package/docs/RELEASE-0.2.0.md +14 -3
- package/docs/RELEASE-0.3.0.md +24 -0
- package/docs/RELEASE-0.3.1.md +15 -0
- package/docs/REVIEW-GUIDE.md +68 -0
- package/docs/REVIEW-SOURCES.md +13 -0
- package/docs/REVIEW-VALIDATION.md +34 -0
- package/docs/REVIEWS.md +75 -0
- package/docs/VISUAL-WORKFLOW.md +1 -1
- package/docs/WORKFLOW-0.3-VALIDATION.md +32 -0
- package/docs/WORKFLOW-0.3.md +34 -0
- package/docs/images/devmethod-delivery.svg +1 -1
- package/docs/images/devmethod-flow.svg +1 -1
- package/docs/images/review-correction.jpg +0 -0
- package/docs/images/review-coverage.jpg +0 -0
- package/docs/images/review-interface-desktop.jpg +0 -0
- package/docs/images/review-interface-mobile.jpg +0 -0
- package/docs/media/review-extension/README.md +21 -0
- package/docs/media/review-extension/scenes.json +70 -0
- package/docs/media/visual-chain/README.md +8 -2
- package/docs/media/visual-chain/devmethod-du-besoin-au-produit.fr.srt +65 -1
- package/docs/media/visual-chain/video-preview.jpg +0 -0
- package/docs/missions/review-media-0.3.1.md +11 -0
- package/docs/missions/workflow-0.3-reviews/interface/REVIEW.md +109 -0
- package/docs/missions/workflow-0.3-reviews/interface/review.json +255 -0
- package/docs/missions/workflow-0.3.md +19 -0
- package/examples/mission-dialogue/PROJECT_PROFILE.md +9 -0
- package/examples/mission-dialogue/architecture/decisions/001-storage.md +7 -0
- package/examples/mission-dialogue/docs/missions/first-save/PLAN.md +13 -0
- package/examples/mission-dialogue/docs/missions/first-save/REPRISE.md +6 -0
- package/examples/mission-dialogue/docs/missions/first-save/tickets/SAVE-1.md +14 -0
- package/examples/mission-dialogue/docs/missions/legacy-copy.md +9 -0
- package/examples/mission-dialogue/docs/produit/REGLES.md +5 -0
- package/examples/review/README.md +14 -0
- package/examples/review/REVIEW.md +98 -0
- package/examples/review/review-demo.html +1351 -0
- package/examples/review/review.json +199 -0
- package/package.json +2 -2
- package/scripts/build-review.mjs +8 -0
- package/scripts/media/review-extension/extend.py +100 -0
- package/scripts/package-smoke.mjs +20 -5
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
{
|
|
2
|
+
"format": 1,
|
|
3
|
+
"id": "DEMO-REVIEW",
|
|
4
|
+
"title": "Review de la bibliothèque",
|
|
5
|
+
"project": "Atelier de lecture",
|
|
6
|
+
"mission": "Première sauvegarde",
|
|
7
|
+
"tickets": [
|
|
8
|
+
{
|
|
9
|
+
"id": "DEMO-12",
|
|
10
|
+
"title": "PR de l’interface (lien de démonstration)",
|
|
11
|
+
"url": "https://github.com/montassarkhalloufi/DevMethod/pull/19"
|
|
12
|
+
}
|
|
13
|
+
],
|
|
14
|
+
"date": "2026-09-13",
|
|
15
|
+
"scope": [
|
|
16
|
+
"Démonstration fictive de consultation, sans audit du projet illustré."
|
|
17
|
+
],
|
|
18
|
+
"exclusions": [
|
|
19
|
+
"Aucun résultat réel de sécurité ou de performance."
|
|
20
|
+
],
|
|
21
|
+
"revision": {
|
|
22
|
+
"commit": "demo-revision-1",
|
|
23
|
+
"dirty": []
|
|
24
|
+
},
|
|
25
|
+
"technologies": [
|
|
26
|
+
{
|
|
27
|
+
"name": "HTML / DOM",
|
|
28
|
+
"version": "Living Standard",
|
|
29
|
+
"detectedFrom": "Fixture authored for the viewer"
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"sources": [
|
|
33
|
+
{
|
|
34
|
+
"id": "WAI",
|
|
35
|
+
"title": "Messages de statut accessibles",
|
|
36
|
+
"kind": "documentation",
|
|
37
|
+
"publisher": "W3C WAI",
|
|
38
|
+
"technology": "Web accessibility",
|
|
39
|
+
"version": "WCAG 2.2",
|
|
40
|
+
"url": "https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html",
|
|
41
|
+
"consultedAt": null,
|
|
42
|
+
"access": "unverified",
|
|
43
|
+
"usage": "Référence proposée pour cette démonstration, non consultée dans une review réelle.",
|
|
44
|
+
"compatibility": "À vérifier pour le parcours cible.",
|
|
45
|
+
"provenance": "Domaine w3.org ; cette fixture ne constitue pas une vérification de provenance."
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"checks": [
|
|
49
|
+
{
|
|
50
|
+
"id": "C-01",
|
|
51
|
+
"title": "Annonce après sauvegarde",
|
|
52
|
+
"domain": "Accessibilité",
|
|
53
|
+
"kind": "manual",
|
|
54
|
+
"status": "failed",
|
|
55
|
+
"result": "Exemple fictif : le changement de statut ne serait pas annoncé.",
|
|
56
|
+
"reason": null,
|
|
57
|
+
"evidenceIds": [
|
|
58
|
+
"E-01"
|
|
59
|
+
],
|
|
60
|
+
"revision": "demo-revision-1",
|
|
61
|
+
"targets": [
|
|
62
|
+
"save-status"
|
|
63
|
+
]
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"id": "C-02",
|
|
67
|
+
"title": "Conservation du titre",
|
|
68
|
+
"domain": "Données",
|
|
69
|
+
"kind": "automated",
|
|
70
|
+
"status": "passed",
|
|
71
|
+
"result": "Exemple fictif : la valeur enregistrée correspond à la saisie.",
|
|
72
|
+
"reason": null,
|
|
73
|
+
"evidenceIds": [
|
|
74
|
+
"E-02"
|
|
75
|
+
],
|
|
76
|
+
"revision": "demo-revision-1",
|
|
77
|
+
"targets": [
|
|
78
|
+
"save-title"
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "C-03",
|
|
83
|
+
"title": "Temps de réponse sur liste longue",
|
|
84
|
+
"domain": "Performance",
|
|
85
|
+
"kind": "automated",
|
|
86
|
+
"status": "not-run",
|
|
87
|
+
"result": "Aucune mesure disponible dans cette fixture.",
|
|
88
|
+
"reason": "Jeu de données représentatif indisponible.",
|
|
89
|
+
"evidenceIds": [],
|
|
90
|
+
"revision": "demo-revision-1",
|
|
91
|
+
"targets": [
|
|
92
|
+
"list-view"
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"findings": [
|
|
97
|
+
{
|
|
98
|
+
"id": "R-01",
|
|
99
|
+
"title": "Le retour de sauvegarde reste silencieux",
|
|
100
|
+
"domain": "Accessibilité",
|
|
101
|
+
"severity": "moderate",
|
|
102
|
+
"severityReason": "Exemple fictif : le lecteur pourrait ignorer que son action a réussi.",
|
|
103
|
+
"confidence": "confirmed",
|
|
104
|
+
"resolution": "open",
|
|
105
|
+
"location": {
|
|
106
|
+
"path": "save-status (composant fictif)",
|
|
107
|
+
"line": null,
|
|
108
|
+
"component": "Confirmation de sauvegarde"
|
|
109
|
+
},
|
|
110
|
+
"trigger": "Sauvegarder un titre en utilisant un lecteur d’écran.",
|
|
111
|
+
"expected": "Le succès est annoncé sans déplacer le focus.",
|
|
112
|
+
"observed": "Dans ce scénario fictif, seul le texte visuel change.",
|
|
113
|
+
"impact": "Le lecteur ne sait pas si son titre a été conservé.",
|
|
114
|
+
"reproduction": [
|
|
115
|
+
"Scénario illustratif, non exécuté.",
|
|
116
|
+
"Saisir un titre, activer Sauvegarder et écouter le retour."
|
|
117
|
+
],
|
|
118
|
+
"evidenceIds": [
|
|
119
|
+
"E-01"
|
|
120
|
+
],
|
|
121
|
+
"correction": "Exposer le message de succès comme statut accessible, puis vérifier son annonce.",
|
|
122
|
+
"tradeoffs": "Éviter les annonces répétées ; conserver le focus dans le parcours.",
|
|
123
|
+
"sourceIds": [
|
|
124
|
+
"WAI"
|
|
125
|
+
],
|
|
126
|
+
"ticketIds": [
|
|
127
|
+
"DEMO-12"
|
|
128
|
+
],
|
|
129
|
+
"verification": "Inspecter avec un lecteur d’écran réel et vérifier le maintien du focus.",
|
|
130
|
+
"resolutionEvidenceIds": [],
|
|
131
|
+
"targets": [
|
|
132
|
+
"save-status"
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"id": "R-02",
|
|
137
|
+
"title": "Une grande liste pourrait ralentir la recherche",
|
|
138
|
+
"domain": "Performance",
|
|
139
|
+
"severity": "minor",
|
|
140
|
+
"severityReason": "Impact supposé limité aux collections volumineuses.",
|
|
141
|
+
"confidence": "suspected",
|
|
142
|
+
"resolution": "open",
|
|
143
|
+
"location": {
|
|
144
|
+
"path": "list-view (composant fictif)",
|
|
145
|
+
"line": null,
|
|
146
|
+
"component": "Liste de titres"
|
|
147
|
+
},
|
|
148
|
+
"trigger": "Rechercher dans une collection volumineuse.",
|
|
149
|
+
"expected": "La saisie reste réactive.",
|
|
150
|
+
"observed": "Aucune mesure ; risque hypothétique seulement.",
|
|
151
|
+
"impact": "Un délai pourrait gêner la saisie ; cela reste à vérifier.",
|
|
152
|
+
"reproduction": [
|
|
153
|
+
"Préparer un jeu représentatif puis mesurer ; non exécuté."
|
|
154
|
+
],
|
|
155
|
+
"evidenceIds": [],
|
|
156
|
+
"correction": "Mesurer avant de décider d’une optimisation.",
|
|
157
|
+
"tradeoffs": "Éviter une complexité non justifiée par des mesures.",
|
|
158
|
+
"sourceIds": [],
|
|
159
|
+
"ticketIds": [],
|
|
160
|
+
"verification": "Mesurer latence et réactivité sur le matériel cible.",
|
|
161
|
+
"resolutionEvidenceIds": [],
|
|
162
|
+
"targets": [
|
|
163
|
+
"list-view"
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
],
|
|
167
|
+
"evidence": [
|
|
168
|
+
{
|
|
169
|
+
"id": "E-01",
|
|
170
|
+
"title": "Flux du retour utilisateur — schéma explicatif",
|
|
171
|
+
"kind": "diagram",
|
|
172
|
+
"content": "Saisie du titre → Sauvegarder → Titre conservé → Message visuel\n ↳ Annonce vocale manquante (hypothèse de fixture)\nSchéma explicatif fictif, pas une capture ni un journal d’exécution.",
|
|
173
|
+
"url": null,
|
|
174
|
+
"image": null
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"id": "E-02",
|
|
178
|
+
"title": "Résultat illustratif de conservation",
|
|
179
|
+
"kind": "log",
|
|
180
|
+
"content": "DÉMONSTRATION — journal rédigé, test non exécuté.\nEntrée « Un titre » → valeur conservée « Un titre ».",
|
|
181
|
+
"url": null,
|
|
182
|
+
"image": null
|
|
183
|
+
}
|
|
184
|
+
],
|
|
185
|
+
"limits": [
|
|
186
|
+
"DÉMONSTRATION : données fictives, sans validation d’un produit réel.",
|
|
187
|
+
"Le contrôle de performance n’a pas été exécuté."
|
|
188
|
+
],
|
|
189
|
+
"policy": {
|
|
190
|
+
"blockingSeverities": [
|
|
191
|
+
"critical",
|
|
192
|
+
"major",
|
|
193
|
+
"moderate"
|
|
194
|
+
],
|
|
195
|
+
"requireAllChecks": true,
|
|
196
|
+
"rationale": "Politique illustrative : corriger les problèmes confirmés modérés ou plus graves avant intégration."
|
|
197
|
+
},
|
|
198
|
+
"summary": "Démonstration de l’interface. Les constats et contrôles ci-dessous sont fictifs."
|
|
199
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devmethod-ai",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "From idea to delivery with your AI coding agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"url": "git+https://github.com/montassarkhalloufi/DevMethod.git"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"build": "tsc",
|
|
31
|
+
"build": "tsc && node scripts/build-review.mjs",
|
|
32
32
|
"test": "npm run build && node --test tests/*.test.mjs",
|
|
33
33
|
"test:fixtures": "node --test tests/evaluation.test.mjs",
|
|
34
34
|
"check:docs": "node scripts/check-docs.mjs",
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The two browser modules have no runtime dependencies. Keep this bundle explicit.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
const model = fs.readFileSync('dist/review-model.js', 'utf8').replace(/\r\n/g, '\n');
|
|
4
|
+
const app = fs.readFileSync('dist/review-app.js', 'utf8').replace(/\r\n/g, '\n');
|
|
5
|
+
if (/^import /m.test(model) || (app.match(/^import /gm) || []).length !== 1 || !app.includes("from './review-model.js'")) throw new Error('Review browser dependencies changed; inspect bundling.');
|
|
6
|
+
const code = `(function () {\n'use strict';\n${model.replace(/^export /gm, '')}\n${app.replace(/^import .*;\n/m, '')}\n})();\n`;
|
|
7
|
+
fs.writeFileSync('dist/review-browser.js', code);
|
|
8
|
+
fs.copyFileSync('src/review-ui.css', 'dist/review-ui.css');
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Insert real review screenshots into the existing 0.3.0 film; never rebuild its story.
|
|
2
|
+
Requires ffmpeg, ffprobe, macOS say/Thomas and Pillow. Run from repository root.
|
|
3
|
+
"""
|
|
4
|
+
import json, math, pathlib, subprocess, re, hashlib
|
|
5
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
6
|
+
ROOT=pathlib.Path.cwd(); TEMP=pathlib.Path('/private/tmp/devmethod-review-film'); TEMP.mkdir(exist_ok=True)
|
|
7
|
+
MEDIA=ROOT/'docs/media/visual-chain'; EXT=ROOT/'docs/media/review-extension'
|
|
8
|
+
MOVIE='docs/media/visual-chain/devmethod-du-besoin-au-produit-4k.fr.mp4'
|
|
9
|
+
CUT=171.52
|
|
10
|
+
|
|
11
|
+
def run(args):
|
|
12
|
+
subprocess.run(args,check=True,stdout=subprocess.DEVNULL,stderr=open(TEMP/'encode.log','a'))
|
|
13
|
+
def duration(file):
|
|
14
|
+
return float(subprocess.check_output(['ffprobe','-v','error','-show_entries','format=duration','-of','csv=p=0',str(file)]))
|
|
15
|
+
def font(size,serif=False,mono=False):
|
|
16
|
+
return ImageFont.truetype('/System/Library/Fonts/Menlo.ttc' if mono else '/System/Library/Fonts/Supplemental/'+('Georgia.ttf' if serif else 'Arial.ttf'),size)
|
|
17
|
+
def wrap(draw,text,f,width):
|
|
18
|
+
lines=[]; line=''
|
|
19
|
+
for word in text.split():
|
|
20
|
+
candidate=(line+' '+word).strip()
|
|
21
|
+
if draw.textlength(candidate,font=f)>width and line: lines.append(line);line=word
|
|
22
|
+
else:line=candidate
|
|
23
|
+
return lines+[line]
|
|
24
|
+
def frame(i,s):
|
|
25
|
+
im=Image.new('RGB',(3840,2160),'#0e1d21'); d=ImageDraw.Draw(im)
|
|
26
|
+
d.text((160,76),'DevMethod / Review',font=font(40),fill='#b0c6bf')
|
|
27
|
+
d.text((3020,76),f'COMPLÉMENT {i+1:02} / 06',font=font(32),fill='#b0c6bf')
|
|
28
|
+
d.text((160,210),s['title'],font=font(110,True),fill='#f6f0e5')
|
|
29
|
+
d.rounded_rectangle((160,440,1220,1850),radius=28,fill='#183137',outline='#45625f',width=2)
|
|
30
|
+
y=510
|
|
31
|
+
d.text((210,y),'DEPUIS LE PACKAGE NPM',font=font(30),fill='#e9ae82'); y+=95
|
|
32
|
+
for line in s['command'].split('\n'):
|
|
33
|
+
d.text((210,y),line,font=font(34,mono=True),fill='#f1b98e');y+=58
|
|
34
|
+
y+=90
|
|
35
|
+
for line in wrap(d,s['explanation'],font(49),950):
|
|
36
|
+
d.text((210,y),line,font=font(49),fill='#ecf1e8');y+=68
|
|
37
|
+
y+=75
|
|
38
|
+
for line in wrap(d,s['note'],font(33),950):
|
|
39
|
+
d.text((210,y),line,font=font(33),fill='#b5d0c2');y+=48
|
|
40
|
+
shot=Image.open(EXT/'captures'/s['capture']).convert('RGB');scale=min(2360/shot.width,1470/shot.height);shot=shot.resize((round(shot.width*scale),round(shot.height*scale)),Image.Resampling.LANCZOS)
|
|
41
|
+
# Screenshot pixels are preserved apart from proportional display scaling.
|
|
42
|
+
im.paste(shot,(1320+(2360-shot.width)//2,460+(1470-shot.height)//2))
|
|
43
|
+
d.text((160,2040),'Captures réelles de l’interface · Données de démonstration fictives · Montage commenté',font=font(33),fill='#b0c6bf')
|
|
44
|
+
im.save(TEMP/f'frame-{i}.png')
|
|
45
|
+
|
|
46
|
+
scenes=[
|
|
47
|
+
dict(title='La review devient consultable',capture='findings.jpg',command='npx devmethod-ai@0.3.1 '+chr(92)+'\n review --demo '+chr(92)+'\n --output review.html --open',explanation='Une interface locale pour relier constats, preuves et corrections.',note='HTML autonome hors ligne. La commande présente des résultats enregistrés ; elle ne réalise pas la review.',voice='Dev Method ajoute maintenant une interface de review. Voici sa démonstration fictive, distincte des résultats réels du pilote Lisière. Cette commande ouvre un rapport local.'),
|
|
48
|
+
dict(title='Retrouver le constat utile',capture='filtered.jpg',command='Constats → Rechercher\n« silencieux »',explanation='Rechercher, filtrer et sélectionner sans confondre gravité et confiance.',note='Un constat confirmé et un risque à vérifier restent distincts. Les compteurs décrivent toute la review.',voice='La recherche isole un constat. Les filtres distinguent domaine, gravité, confiance et résolution. Un risque à vérifier reste une hypothèse, pas un défaut confirmé.'),
|
|
49
|
+
dict(title='Comprendre, puis corriger',capture='correction.jpg',command='Preuve → Correction\n→ Vérification attendue',explanation='Du scénario observé à la correction et à sa nouvelle vérification.',note='Le ticket complète le constat. Fermer le panneau ne résout jamais le problème.',voice='Chaque constat relie scénario, preuve, impact et correction. La vérification attendue et le ticket restent accessibles. Masquer un résultat ne le marque pas comme résolu.'),
|
|
50
|
+
dict(title='Voir la couverture réelle',capture='coverage.jpg',command='Couverture\nRéussi · Échec · Non exécuté',explanation='Les contrôles ont leur propre statut, indépendant du nombre de constats.',note='Une zone non inspectée ne devient pas verte. La révision et les limites restent visibles.',voice='La couverture sépare les contrôles réussis, en échec et non exécutés. L’absence de constat ne prouve pas que tout a été vérifié.'),
|
|
51
|
+
dict(title='Vérifier les références',capture='sources.jpg',command='Sources\nVersion → Provenance → Usage',explanation='Une référence connue n’est pas automatiquement une source consultée.',note='La démonstration signale sa référence non vérifiée. Une vraie review consigne sa consultation et ses limites.',voice='Les sources indiquent technologie, version, provenance et usage. Ici, la référence est clairement non vérifiée : cette démonstration ne prétend pas l’avoir consultée.'),
|
|
52
|
+
dict(title='Exporter et reprendre',capture='export.jpg',command='Exporter le rapport\nHTML · Markdown · JSON',explanation='Une source structurée commune pour l’interface et les rapports.',note='Conserver la révision, les preuves et les limites. Le HTML exporté conserve aussi l’état de consultation.',voice='Exportez la même review en HTML, Markdown ou JSON. Les résultats partagent une source commune. Le rapport conserve la révision, les preuves et les limites pour la reprise.'),
|
|
53
|
+
]
|
|
54
|
+
source=TEMP/'original-0.3.0.mp4'
|
|
55
|
+
if not source.exists(): source.write_bytes(subprocess.check_output(['git','show',f'v0.3.0:{MOVIE}']))
|
|
56
|
+
# Existing footage and narration are retained. Only the six-second ending is moved.
|
|
57
|
+
enc=['-c:v','libx264','-profile:v','high','-level:v','5.1','-pix_fmt','yuv420p','-r','25','-threads','4','-preset','veryfast','-crf','20','-c:a','aac','-ar','48000','-ac','1','-b:a','192k','-video_track_timescale','12800']
|
|
58
|
+
for i,s in enumerate(scenes):
|
|
59
|
+
frame(i,s)
|
|
60
|
+
(TEMP/f'voice-{i}.txt').write_text(s['voice'])
|
|
61
|
+
run(['say','-v','Thomas','-r','165','-f',str(TEMP/f'voice-{i}.txt'),'-o',str(TEMP/f'voice-{i}.aiff')])
|
|
62
|
+
s['duration']=math.ceil((duration(TEMP/f'voice-{i}.aiff')+0.8)*25)/25
|
|
63
|
+
clip=TEMP/f'clip-{i}.mp4'
|
|
64
|
+
fingerprint=hashlib.sha256((TEMP/f'frame-{i}.png').read_bytes()+(TEMP/f'voice-{i}.aiff').read_bytes()+json.dumps(enc).encode()).hexdigest()
|
|
65
|
+
cache=TEMP/f'clip-{i}.sha256'
|
|
66
|
+
if not clip.exists() or not cache.exists() or cache.read_text()!=fingerprint:
|
|
67
|
+
run(['ffmpeg','-y','-loop','1','-framerate','25','-i',str(TEMP/f'frame-{i}.png'),'-i',str(TEMP/f'voice-{i}.aiff'),'-vf',f"fade=t=in:st=0:d=0.3,fade=t=out:st={s['duration']-0.3}:d=0.3",'-af','apad','-t',str(s['duration']),*enc,str(clip)])
|
|
68
|
+
cache.write_text(fingerprint)
|
|
69
|
+
print('Review scene encoded',i+1,flush=True)
|
|
70
|
+
# Encode only the splice tracks once, leaving the original scene design and timing intact.
|
|
71
|
+
if not (TEMP/'before.mp4').exists():
|
|
72
|
+
run(['ffmpeg','-y','-i',str(source),'-t',str(CUT),'-map','0:v:0','-map','0:a:0',*enc,str(TEMP/'before.mp4')])
|
|
73
|
+
if not (TEMP/'after.mp4').exists():
|
|
74
|
+
run(['ffmpeg','-y','-ss',str(CUT),'-i',str(source),'-map','0:v:0','-map','0:a:0',*enc,str(TEMP/'after.mp4')])
|
|
75
|
+
clips=[TEMP/'before.mp4']+[TEMP/f'clip-{i}.mp4' for i in range(len(scenes))]+[TEMP/'after.mp4']
|
|
76
|
+
(TEMP/'concat.txt').write_text('\n'.join(f"file '{p}'" for p in clips))
|
|
77
|
+
# Subtitles follow measured segment durations, not estimated spoken word timing.
|
|
78
|
+
def seconds(st):
|
|
79
|
+
h,m,sec=st.replace(',','.').split(':');return int(h)*3600+int(m)*60+float(sec)
|
|
80
|
+
def stamp(sec):
|
|
81
|
+
n=round(sec*1000);return f'{n//3600000:02}:{n//60000%60:02}:{n//1000%60:02},{n%1000:03}'
|
|
82
|
+
original_srt=subprocess.check_output(['git','show','v0.3.0:docs/media/visual-chain/devmethod-du-besoin-au-produit.fr.srt']).decode()
|
|
83
|
+
added=sum(duration(p) for p in clips[1:-1]); prefix=duration(clips[0]); items=[]
|
|
84
|
+
for block in original_srt.strip().split('\n\n'):
|
|
85
|
+
lines=block.splitlines();a,b=map(seconds,lines[1].split(' --> ')); shift=added+(prefix-CUT) if a>=171.5 else 0
|
|
86
|
+
items.append((a+shift,b+shift,'\n'.join(lines[2:])))
|
|
87
|
+
cursor=prefix
|
|
88
|
+
for i,s in enumerate(scenes):
|
|
89
|
+
s['start']=cursor
|
|
90
|
+
parts=re.split(r'(?<=[.!?])\s+',s['voice']); spoken=duration(TEMP/f'voice-{i}.aiff');t=cursor
|
|
91
|
+
for part in parts:
|
|
92
|
+
length=spoken*len(part)/sum(map(len,parts));items.append((t,t+length,part));t+=length
|
|
93
|
+
cursor+=duration(TEMP/f'clip-{i}.mp4')
|
|
94
|
+
items.sort(); srt=MEDIA/'devmethod-du-besoin-au-produit.fr.srt'
|
|
95
|
+
srt.write_text('\n\n'.join(f'{i+1}\n{stamp(a)} --> {stamp(b)}\n{text}' for i,(a,b,text) in enumerate(items))+'\n')
|
|
96
|
+
run(['ffmpeg','-y','-f','concat','-safe','0','-i',str(TEMP/'concat.txt'),'-i',str(srt),'-map','0:v:0','-map','0:a:0','-map','1:0','-c:v','copy','-c:a','copy','-c:s','mov_text','-metadata:s:a:0','language=fra','-metadata:s:s:0','language=fra','-movflags','+faststart',str(ROOT/MOVIE)])
|
|
97
|
+
manifest=dict(baseTag='v0.3.0',baseSHA256=hashlib.sha256(source.read_bytes()).hexdigest(),insertionSeconds=CUT,baseDuration=duration(source),duration=duration(ROOT/MOVIE),method='Original footage and narration retained; review screenshot chapter inserted before original ending. Re-encoded splice tracks; no original scene regenerated.',scenes=scenes)
|
|
98
|
+
(EXT/'scenes.json').write_text(json.dumps(manifest,ensure_ascii=False,indent=2)+'\n')
|
|
99
|
+
run(['ffmpeg','-y','-ss',str(prefix+3),'-i',str(ROOT/MOVIE),'-frames:v','1','-vf','scale=1920:-1',str(MEDIA/'video-preview.jpg')])
|
|
100
|
+
print('Final duration',manifest['duration'],flush=True)
|
|
@@ -15,8 +15,21 @@ try {
|
|
|
15
15
|
run('tar', ['-xzf', '-'], root, 0, fs.readFileSync(path.resolve(archive)));
|
|
16
16
|
const pkg = path.join(root, 'package'); const cli = path.join(pkg, 'dist/cli.js');
|
|
17
17
|
const call = (args, expected = 0) => run(process.execPath, [cli, ...args], root, expected);
|
|
18
|
-
assert.equal(JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'))).version, '0.
|
|
18
|
+
assert.equal(JSON.parse(fs.readFileSync(path.join(pkg, 'package.json'))).version, '0.3.1');
|
|
19
19
|
run(process.execPath, ['scripts/check-docs.mjs'], pkg);
|
|
20
|
+
assert.match(call(['--help']), /Markdown PLAN\/tickets and legacy missions/);
|
|
21
|
+
for (const resource of ['project-foundation/references/exploration.md', 'project-foundation/references/delivery-planning.md', 'project-foundation/assets/EXISTANT.md', 'project-foundation/assets/OPPORTUNITES.md', 'project-foundation/assets/CADRAGE.md', 'project-foundation/assets/REGLES.md', 'scoped-delivery/assets/PLAN.md', 'scoped-delivery/assets/TICKET.md', 'scoped-delivery/assets/REPRISE.md', 'scoped-delivery/assets/MISSION.md', 'scoped-delivery/assets/REVIEW.md', 'scoped-delivery/references/review-workflow.md']) {
|
|
22
|
+
assert.ok(fs.statSync(path.join(pkg, '.agents/skills', resource)).size > 0, resource);
|
|
23
|
+
}
|
|
24
|
+
assert.ok(fs.existsSync(path.join(pkg, 'examples/mission-dialogue/docs/missions/first-save/tickets/SAVE-1.md')));
|
|
25
|
+
for (const file of ['review.js', 'review-cli.js', 'review-model.js', 'review-browser.js', 'review-ui.css']) assert.ok(fs.statSync(path.join(pkg, 'dist', file)).size > 0, file);
|
|
26
|
+
const reviewRoot = path.join(root, 'review-output');
|
|
27
|
+
const reviewResult = JSON.parse(call(['review', '--demo', '--dest', reviewRoot, '--output', 'review.html', '--markdown', 'REVIEW.md', '--json']));
|
|
28
|
+
assert.equal(reviewResult.status, 'corrections');
|
|
29
|
+
assert.ok(fs.readFileSync(path.join(reviewRoot, 'review.html'), 'utf8').includes('Content-Security-Policy'));
|
|
30
|
+
assert.ok(fs.readFileSync(path.join(reviewRoot, 'REVIEW.md'), 'utf8').includes('R-01'));
|
|
31
|
+
call(['review', '--demo', '--dest', reviewRoot, '--output', 'review.html'], 2);
|
|
32
|
+
|
|
20
33
|
for (const testFile of [...fs.readdirSync(path.join(pkg, 'examples/pocket-tasks/tests')).filter(f => f.endsWith('.test.mjs')).map(f => `examples/pocket-tasks/tests/${f}`), 'evaluation/greenfield/acceptance.test.mjs', 'evaluation/greenfield/security.test.mjs']) {
|
|
21
34
|
run(process.execPath, ['--test', testFile], pkg);
|
|
22
35
|
}
|
|
@@ -54,11 +67,13 @@ try {
|
|
|
54
67
|
fs.appendFileSync(customized, '\nLocal workflow policy.\n'); fs.appendFileSync(profile, '\nFilled project context.\n');
|
|
55
68
|
const before = [fs.readFileSync(customized), fs.readFileSync(profile)];
|
|
56
69
|
const preview = JSON.parse(call(['update-preview', '--dest', adopted, '--json']));
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
const skillPath = '.agents/skills/project-foundation/SKILL.md';
|
|
71
|
+
const upstreamChanged = !fs.readFileSync(path.join(legacy, skillPath)).equals(fs.readFileSync(path.join(pkg, skillPath)));
|
|
72
|
+
assert.equal(preview.entries.find(e => e.path === skillPath).classification, upstreamChanged ? 'conflict' : 'customized');
|
|
73
|
+
assert.equal(preview.provenance, JSON.parse(fs.readFileSync(path.join(adopted, 'kit-manifest.json'))).provenance ? 'recorded' : 'unknown');
|
|
59
74
|
call(['init', '--tool', 'codex', '--dest', adopted], 2);
|
|
60
75
|
assert.deepEqual([fs.readFileSync(customized), fs.readFileSync(profile)], before);
|
|
61
|
-
console.log(
|
|
76
|
+
console.log(`Actual legacy tarball: ${upstreamChanged ? 'local/upstream conflict' : 'unchanged upstream with local customization'} detected; filled profile/custom skill preserved.`);
|
|
62
77
|
}
|
|
63
|
-
console.log('Packed 0.
|
|
78
|
+
console.log('Packed 0.3.1: three host installs, subset, customization preservation, mission/context/staleness/planning and documentation links passed. No native host execution.');
|
|
64
79
|
} finally { fs.rmSync(root, { recursive: true, force: true }); }
|