wizz-method 2.15.0 → 2.17.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 (85) hide show
  1. package/README.md +12 -1
  2. package/THIRD-PARTY-NOTICES.md +6 -0
  3. package/package.json +5 -2
  4. package/skills-registry.yaml +18 -6
  5. package/src/bmm-skills/1-analysis/wizz-agent-analyst/customize.toml +1 -1
  6. package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +34 -12
  7. package/src/bmm-skills/2-plan-workflows/wizz-agent-pm/customize.toml +1 -1
  8. package/src/bmm-skills/2-plan-workflows/wizz-agent-ux-designer/customize.toml +1 -1
  9. package/src/bmm-skills/3-solutioning/wizz-agent-architect/customize.toml +1 -1
  10. package/src/bmm-skills/4-implementation/wizz-agent-dev/customize.toml +1 -1
  11. package/src/bmm-skills/module.yaml +10 -0
  12. package/src/core-skills/_shared/handoff-protocol.md +10 -0
  13. package/src/modules/wizz/_shared/encerramento.md +4 -0
  14. package/src/modules/wizz/_shared/organizacao.md +58 -0
  15. package/src/modules/wizz/_shared/video-pipeline.md +1 -0
  16. package/src/modules/wizz/agents/wizz-maestro/SKILL.md +9 -1
  17. package/src/modules/wizz/agents/wizz-maestro/customize.toml +8 -2
  18. package/src/skills-lib/wizz-inicio-de-projeto/SKILL.md +11 -0
  19. package/src/skills-lib/wizz-router/SKILL.md +3 -1
  20. package/src/skills-lib/wizz-router/references/routing-table-flat.md +1 -0
  21. package/src/skills-lib/wizz-video-styles/SKILL.md +66 -0
  22. package/src/skills-lib/wizz-video-styles/assets/audio/CREDITS.md +54 -0
  23. package/src/skills-lib/wizz-video-styles/assets/audio/LIBRARIES.md +31 -0
  24. package/src/skills-lib/wizz-video-styles/assets/audio/kit.json +182 -0
  25. package/src/skills-lib/wizz-video-styles/assets/audio/music/acoustic-lifestyle.mp3 +0 -0
  26. package/src/skills-lib/wizz-video-styles/assets/audio/music/cinematic-tense.mp3 +0 -0
  27. package/src/skills-lib/wizz-video-styles/assets/audio/music/classical-light.mp3 +0 -0
  28. package/src/skills-lib/wizz-video-styles/assets/audio/music/corporate-light.mp3 +0 -0
  29. package/src/skills-lib/wizz-video-styles/assets/audio/music/lofi-chill.mp3 +0 -0
  30. package/src/skills-lib/wizz-video-styles/assets/audio/music/meme-beat.mp3 +0 -0
  31. package/src/skills-lib/wizz-video-styles/assets/audio/music/piano-editorial.mp3 +0 -0
  32. package/src/skills-lib/wizz-video-styles/assets/audio/music/tech-pulse.mp3 +0 -0
  33. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/boom-impact.mp3 +0 -0
  34. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/click-mouse.mp3 +0 -0
  35. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/ding-notify.mp3 +0 -0
  36. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/glitch-short.mp3 +0 -0
  37. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/ka-ching.mp3 +0 -0
  38. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/pop-bubble.mp3 +0 -0
  39. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/record-scratch.mp3 +0 -0
  40. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/riser-short.mp3 +0 -0
  41. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/swoosh-text.mp3 +0 -0
  42. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/typing-short.mp3 +0 -0
  43. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/whoosh-fast.mp3 +0 -0
  44. package/src/skills-lib/wizz-video-styles/assets/audio/sfx/whoosh-soft.mp3 +0 -0
  45. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/EditorialReel.tsx +111 -0
  46. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/README.md +77 -0
  47. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/captions.tsx +56 -0
  48. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/example-words.json +285 -0
  49. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/primitives.tsx +241 -0
  50. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/scenes.tsx +113 -0
  51. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/theme.ts +68 -0
  52. package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/timeline.ts +65 -0
  53. package/src/skills-lib/wizz-video-styles/data/recipes.json +341 -0
  54. package/src/skills-lib/wizz-video-styles/data/references.json +3397 -0
  55. package/src/skills-lib/wizz-video-styles/data/styles.json +726 -0
  56. package/src/skills-lib/wizz-video-styles/references/audio.md +54 -0
  57. package/src/skills-lib/wizz-video-styles/references/measured-patterns.md +38 -0
  58. package/src/skills-lib/wizz-video-styles/references/research-method.md +33 -0
  59. package/src/skills-lib/wizz-video-styles/references/style-picker.md +51 -0
  60. package/src/skills-lib/wizz-video-styles/references/styles/camera-no-setup.md +60 -0
  61. package/src/skills-lib/wizz-video-styles/references/styles/desafio-numero.md +60 -0
  62. package/src/skills-lib/wizz-video-styles/references/styles/esquete.md +57 -0
  63. package/src/skills-lib/wizz-video-styles/references/styles/experimento.md +56 -0
  64. package/src/skills-lib/wizz-video-styles/references/styles/explicador-animado.md +62 -0
  65. package/src/skills-lib/wizz-video-styles/references/styles/macro-asmr.md +61 -0
  66. package/src/skills-lib/wizz-video-styles/references/styles/meme-rapido.md +65 -0
  67. package/src/skills-lib/wizz-video-styles/references/styles/motion-editorial.md +69 -0
  68. package/src/skills-lib/wizz-video-styles/references/styles/podcast-clip.md +61 -0
  69. package/src/skills-lib/wizz-video-styles/references/styles/pov-vlog.md +57 -0
  70. package/src/skills-lib/wizz-video-styles/references/styles/quadro-branco.md +60 -0
  71. package/src/skills-lib/wizz-video-styles/references/styles/talking-head-cards.md +62 -0
  72. package/src/skills-lib/wizz-video-styles/references/styles/talking-head-legenda.md +62 -0
  73. package/src/skills-lib/wizz-video-styles/references/styles/tour.md +59 -0
  74. package/src/skills-lib/wizz-video-styles/scripts/analyze.sh +66 -0
  75. package/src/skills-lib/wizz-video-styles/scripts/build_catalog.py +128 -0
  76. package/tools/hooks/README.md +14 -2
  77. package/tools/hooks/session-rules.js +65 -140
  78. package/tools/hooks/wizz-arrumar.js +418 -0
  79. package/tools/hooks/wizz-artifact-guard.js +160 -0
  80. package/tools/hooks/wizz-feature-lib.js +414 -0
  81. package/tools/hooks/wizz-feature.js +401 -0
  82. package/tools/hooks/wizz-layout-lib.js +152 -0
  83. package/tools/hooks/wizz-router-enforce.js +21 -1
  84. package/tools/installer/core/doctor.js +5 -1
  85. package/tools/installer/core/hooks-settings.js +10 -0
@@ -0,0 +1,414 @@
1
+ // wizz-feature-lib.js — funções puras de feature, artefato e layout.
2
+ //
3
+ // Fonte de verdade: wizz-method/tools/hooks/. Não é hook registrado: é a
4
+ // biblioteca que os hooks (session-rules, wizz-router-enforce,
5
+ // wizz-artifact-guard) e as CLIs (wizz-feature, wizz-arrumar) dividem. Mora
6
+ // AQUI porque tudo em tools/hooks/ viaja junto: o installer copia a pasta
7
+ // inteira para `_wizz/hooks/` e o `sync:global` para `~/.claude/hooks/`.
8
+ //
9
+ // Sem dependência de pacote de propósito (roda fora do node_modules do repo):
10
+ // o YAML do config é lido com parser de chave única, que é tudo o que precisa.
11
+ // Toda leitura é fail-open: config ausente ou ilegível vira "sem valor".
12
+
13
+ const fs = require('node:fs');
14
+ const path = require('node:path');
15
+
16
+ // Branches que NUNCA viram feature: são linha principal, não trabalho nomeado.
17
+ const TRUNK = new Set(['main', 'master', 'develop', 'dev', 'staging', 'stage', 'prod', 'production', 'trunk', 'default', 'release']);
18
+
19
+ // Branch de release/versão não é feature: nomeia um corte do produto, não um trabalho.
20
+ const RELEASE_PREFIX = /^(release|rc|v)[/-]/i;
21
+
22
+ // Prefixos de convenção que descrevem o TIPO do trabalho, não a feature.
23
+ const TYPE_PREFIX = /^(feat|feature|fix|bugfix|hotfix|chore|refactor|docs|test|perf|ci|build|style)[/-]/i;
24
+
25
+ // Tipos de artefato com nome padronizado `<tipo>-<slug>.md` e a pasta de cada um.
26
+ // `roteiro` (aplicação + rollback) vai para o conhecimento do projeto, não para
27
+ // os artefatos: é o que alguém reabre no dia do deploy, meses depois.
28
+ const ARTIFACT_TYPES = {
29
+ spec: 'planning',
30
+ plano: 'planning',
31
+ auditoria: 'planning',
32
+ story: 'implementation',
33
+ review: 'implementation',
34
+ roteiro: 'knowledge',
35
+ };
36
+
37
+ const STATUS = ['rascunho', 'aprovado', 'implementado', 'no-ar'];
38
+
39
+ // Artefato de projeto inteiro: fica na raiz das pastas de artefato, nunca numa feature.
40
+ const PROJECT_WIDE = /^(architecture|arquitetura|project-context|contexto|sprint-status|readme|index)([-._]|$)/i;
41
+
42
+ const CONFIG_REL = path.join('_wizz', 'bmm', 'config.yaml');
43
+
44
+ /**
45
+ * Normaliza texto livre para slug kebab-case: o mesmo formato que a skill
46
+ * `wizz-set-feature` grava, para branch e config nunca divergirem.
47
+ * @param {string} raw
48
+ * @returns {string} slug, ou '' se não sobrar nada utilizável
49
+ */
50
+ function toSlug(raw) {
51
+ if (typeof raw !== 'string') return '';
52
+ return raw
53
+ .normalize('NFD')
54
+ .replaceAll(/[̀-ͯ]/g, '')
55
+ .toLowerCase()
56
+ .replaceAll(/[^a-z0-9]+/g, '-')
57
+ .replaceAll(/^-+|-+$/g, '')
58
+ .slice(0, 60)
59
+ .replace(/-+$/, '');
60
+ }
61
+
62
+ /**
63
+ * Data local em YYYY-MM-DD. `toISOString()` daria o dia em UTC, que em UTC-3
64
+ * vira o dia seguinte depois das 21h. `WIZZ_TODAY` fixa a data nos testes.
65
+ * @returns {string}
66
+ */
67
+ function today() {
68
+ if (process.env.WIZZ_TODAY && /^\d{4}-\d{2}-\d{2}$/.test(process.env.WIZZ_TODAY)) return process.env.WIZZ_TODAY;
69
+ const d = new Date();
70
+ const pad = (n) => String(n).padStart(2, '0');
71
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
72
+ }
73
+
74
+ /**
75
+ * Sobe a partir de `start` procurando `name` (até 6 níveis).
76
+ * @param {string} start
77
+ * @param {string} name
78
+ * @returns {string} diretório que contém `name`, ou ''
79
+ */
80
+ function findUp(start, name) {
81
+ let dir = start;
82
+ for (let i = 0; i < 6; i++) {
83
+ try {
84
+ if (fs.existsSync(path.join(dir, name))) return dir;
85
+ } catch {
86
+ // segue subindo
87
+ }
88
+ const parent = path.dirname(dir);
89
+ if (parent === dir) break;
90
+ dir = parent;
91
+ }
92
+ return '';
93
+ }
94
+
95
+ /** Raiz do projeto Wizz (a pasta que contém `_wizz/`), ou ''. */
96
+ function findWizzRoot(cwd) {
97
+ return findUp(cwd, '_wizz');
98
+ }
99
+
100
+ /**
101
+ * Lê o estado git sem subprocesso: `.git/HEAD` já traz o ref.
102
+ * @param {string} cwd
103
+ * @returns {{isRepo: boolean, branch: string}} branch '' em detached HEAD
104
+ */
105
+ function readGitState(cwd) {
106
+ const repoDir = findUp(cwd, '.git');
107
+ if (!repoDir) return { isRepo: false, branch: '' };
108
+ try {
109
+ const gitPath = path.join(repoDir, '.git');
110
+ let headFile = path.join(gitPath, 'HEAD');
111
+ if (fs.statSync(gitPath).isFile()) {
112
+ // worktree/submódulo: `.git` é um arquivo `gitdir: <caminho>`
113
+ const m = /^gitdir:\s*(.+)$/m.exec(fs.readFileSync(gitPath, 'utf8'));
114
+ if (!m) return { isRepo: true, branch: '' };
115
+ headFile = path.join(path.resolve(repoDir, m[1].trim()), 'HEAD');
116
+ }
117
+ const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(fs.readFileSync(headFile, 'utf8').trim());
118
+ return { isRepo: true, branch: ref ? ref[1] : '' };
119
+ } catch {
120
+ return { isRepo: true, branch: '' };
121
+ }
122
+ }
123
+
124
+ /** Só o nome do branch (compatível com a API antiga do session-rules). */
125
+ function readGitBranch(cwd) {
126
+ return readGitState(cwd).branch;
127
+ }
128
+
129
+ /** Branch de tronco ou de release: linha principal, não trabalho nomeado. */
130
+ function isTrunkBranch(branch) {
131
+ return Boolean(branch) && (TRUNK.has(branch.toLowerCase()) || RELEASE_PREFIX.test(branch));
132
+ }
133
+
134
+ /**
135
+ * Deriva o slug de feature do nome do branch. Tronco e branch sem nome útil
136
+ * devolvem '' (nenhuma feature), nunca um slug inventado.
137
+ * @param {string} branch
138
+ * @returns {string}
139
+ */
140
+ function featureFromBranch(branch) {
141
+ if (!branch || isTrunkBranch(branch)) return '';
142
+ const slug = toSlug(branch.replace(TYPE_PREFIX, ''));
143
+ if (!slug || TRUNK.has(slug)) return '';
144
+ // Branch que é só um número de issue não descreve feature nenhuma.
145
+ if (/^\d+$/.test(slug)) return '';
146
+ return slug;
147
+ }
148
+
149
+ function unquote(raw) {
150
+ return String(raw)
151
+ .replace(/\s+#.*$/, '')
152
+ .trim()
153
+ .replaceAll(/^["']|["']$/g, '');
154
+ }
155
+
156
+ /**
157
+ * Lê o config do bmm num objeto plano + o bloco `layout:`. Fail-open.
158
+ * @param {string} root - raiz do projeto Wizz
159
+ * @returns {{exists: boolean, text: string, values: Record<string,string>, layout: Record<string,string>}}
160
+ */
161
+ function readConfig(root) {
162
+ const empty = { exists: false, text: '', values: {}, layout: {} };
163
+ if (!root) return empty;
164
+ let text;
165
+ try {
166
+ text = fs.readFileSync(path.join(root, CONFIG_REL), 'utf8');
167
+ } catch {
168
+ return empty;
169
+ }
170
+ const values = {};
171
+ const layout = {};
172
+ let inLayout = false;
173
+ for (const line of text.split(/\r?\n/)) {
174
+ const top = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
175
+ if (top) {
176
+ inLayout = top[1] === 'layout' && unquote(top[2]) === '';
177
+ if (!inLayout) values[top[1]] = unquote(top[2]);
178
+ continue;
179
+ }
180
+ const nested = /^\s+([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
181
+ if (inLayout && nested) layout[nested[1]] = unquote(nested[2]);
182
+ else if (line.trim() && !line.trim().startsWith('#') && !/^\s/.test(line)) inLayout = false;
183
+ }
184
+ return { exists: true, text, values, layout };
185
+ }
186
+
187
+ /** Lê só o `active_feature` (API antiga do session-rules). */
188
+ function readConfigFeature(root) {
189
+ const configPath = path.join(root || '', CONFIG_REL);
190
+ return { slug: toSlug(readConfig(root).values.active_feature || ''), configPath };
191
+ }
192
+
193
+ /**
194
+ * Resolve um caminho do config (`{project-root}/x`, relativo ou absoluto).
195
+ * @param {string|undefined} value
196
+ * @param {string} root
197
+ * @param {string} fallback - relativo à raiz
198
+ */
199
+ function resolveConfigPath(value, root, fallback) {
200
+ const raw = value && value.trim() ? value.trim() : fallback;
201
+ const replaced = raw.replace('{project-root}', root);
202
+ return path.isAbsolute(replaced) ? path.normalize(replaced) : path.join(root, replaced);
203
+ }
204
+
205
+ /**
206
+ * Pastas de artefato do projeto, já resolvidas.
207
+ * @param {string} root
208
+ * @param {{values: Record<string,string>}} [config]
209
+ */
210
+ function artifactDirs(root, config = readConfig(root)) {
211
+ const output = resolveConfigPath(config.values.output_folder, root, '_wizz-output');
212
+ return {
213
+ planning: resolveConfigPath(config.values.planning_artifacts, root, path.join(path.relative(root, output), 'planning-artifacts')),
214
+ implementation: resolveConfigPath(
215
+ config.values.implementation_artifacts,
216
+ root,
217
+ path.join(path.relative(root, output), 'implementation-artifacts'),
218
+ ),
219
+ knowledge: resolveConfigPath(config.values.project_knowledge, root, 'docs'),
220
+ };
221
+ }
222
+
223
+ /**
224
+ * Estado completo da feature: de onde veio, branch, config e opção de branch.
225
+ * Ordem: branch git > `active_feature` do config > nada.
226
+ * @param {string} cwd
227
+ */
228
+ function resolveFeature(cwd) {
229
+ const root = findWizzRoot(cwd);
230
+ const config = readConfig(root);
231
+ const git = readGitState(root || cwd);
232
+ const fromBranch = featureFromBranch(git.branch);
233
+ const fromConfig = toSlug(config.values.active_feature || '');
234
+ const slug = fromBranch || fromConfig;
235
+ let source = '';
236
+ if (fromBranch) source = 'branch';
237
+ else if (fromConfig) source = 'config';
238
+ return {
239
+ root,
240
+ isWizz: Boolean(root),
241
+ slug,
242
+ source,
243
+ fromBranch,
244
+ fromConfig,
245
+ git,
246
+ onTrunk: git.isRepo && (isTrunkBranch(git.branch) || !git.branch),
247
+ featureBranches: (config.values.feature_branches || 'off').toLowerCase() === 'auto' ? 'auto' : 'off',
248
+ hasLayout: Object.keys(config.layout).length > 0,
249
+ config,
250
+ };
251
+ }
252
+
253
+ function yamlScalar(value) {
254
+ return `"${String(value)
255
+ .replaceAll('\\', '\\\\')
256
+ .replaceAll('"', String.raw`\"`)}"`;
257
+ }
258
+
259
+ /**
260
+ * Grava uma chave escalar de topo no config, preservando o resto do arquivo.
261
+ * Escrita atômica (tmp + rename): config pela metade derrubaria todo agente.
262
+ * @returns {boolean} true se gravou
263
+ */
264
+ function writeConfigValue(root, key, value) {
265
+ const file = path.join(root, CONFIG_REL);
266
+ let text = '';
267
+ try {
268
+ text = fs.readFileSync(file, 'utf8');
269
+ } catch {
270
+ fs.mkdirSync(path.dirname(file), { recursive: true });
271
+ }
272
+ const line = `${key}: ${yamlScalar(value)}`;
273
+ const re = new RegExp(String.raw`^${key}:.*$`, 'm');
274
+ let next;
275
+ if (re.test(text)) next = text.replace(re, line);
276
+ else if (text && !text.endsWith('\n')) next = `${text}\n${line}\n`;
277
+ else next = `${text}${line}\n`;
278
+ atomicWrite(file, next);
279
+ return true;
280
+ }
281
+
282
+ /**
283
+ * Grava (substitui) o bloco `layout:` do config.
284
+ * @param {string} root
285
+ * @param {Record<string,string>} layout
286
+ */
287
+ function writeLayout(root, layout) {
288
+ const file = path.join(root, CONFIG_REL);
289
+ let text = '';
290
+ try {
291
+ text = fs.readFileSync(file, 'utf8');
292
+ } catch {
293
+ fs.mkdirSync(path.dirname(file), { recursive: true });
294
+ }
295
+ const block = ['layout:', ...Object.entries(layout).map(([k, v]) => ` ${k}: ${yamlScalar(v)}`)].join('\n');
296
+ const lines = text.split('\n');
297
+ const start = lines.findIndex((l) => /^layout:\s*$/.test(l));
298
+ if (start === -1) {
299
+ atomicWrite(file, `${text}${text && !text.endsWith('\n') ? '\n' : ''}${block}\n`);
300
+ return;
301
+ }
302
+ let end = start + 1;
303
+ while (end < lines.length && (/^\s+\S/.test(lines[end]) || lines[end].trim() === '')) end++;
304
+ // Linha em branco final pertence ao que vem depois, não ao bloco.
305
+ while (end > start + 1 && lines[end - 1].trim() === '') end--;
306
+ atomicWrite(file, [...lines.slice(0, start), block, ...lines.slice(end)].join('\n'));
307
+ }
308
+
309
+ function atomicWrite(file, content) {
310
+ const tmp = `${file}.${process.pid}.tmp`;
311
+ fs.writeFileSync(tmp, content);
312
+ fs.renameSync(tmp, file);
313
+ }
314
+
315
+ // ── Frontmatter ─────────────────────────────────────────────────────────
316
+
317
+ /**
318
+ * Lê o frontmatter YAML simples (chave: valor) do topo de um markdown.
319
+ * @param {string} text
320
+ * @returns {{has: boolean, data: Record<string,string>}}
321
+ */
322
+ function parseFrontmatter(text) {
323
+ const m = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/.exec(String(text || ''));
324
+ if (!m) return { has: false, data: {} };
325
+ const data = {};
326
+ for (const line of m[1].split(/\r?\n/)) {
327
+ const kv = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
328
+ if (kv) data[kv[1]] = unquote(kv[2]);
329
+ }
330
+ return { has: true, data };
331
+ }
332
+
333
+ /**
334
+ * Troca (ou adiciona) uma chave do frontmatter. Sem frontmatter, cria um.
335
+ * @param {string} text
336
+ * @param {string} key
337
+ * @param {string} rawValue - já formatado (ex.: `no-ar`, `[D-01, D-02]`)
338
+ */
339
+ function setFrontmatterField(text, key, rawValue) {
340
+ const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text);
341
+ if (!m) return `---\n${key}: ${rawValue}\n---\n\n${text}`;
342
+ const body = m[1];
343
+ const re = new RegExp(String.raw`^${key}:.*$`, 'm');
344
+ const nextBody = re.test(body) ? body.replace(re, `${key}: ${rawValue}`) : `${body}\n${key}: ${rawValue}`;
345
+ return `---\n${nextBody}\n---${text.slice(m[0].length)}`;
346
+ }
347
+
348
+ /**
349
+ * Valida o frontmatter de um artefato tipado. Devolve a lista de problemas
350
+ * (vazia quando está tudo certo).
351
+ * @param {string} text
352
+ * @param {string} [expectedFeature]
353
+ * @returns {string[]}
354
+ */
355
+ function frontmatterProblems(text, expectedFeature) {
356
+ const { has, data } = parseFrontmatter(text);
357
+ if (!has) return ['sem frontmatter (feature, status, atualizado_em, decisoes)'];
358
+ const problems = [];
359
+ if (!data.feature) problems.push('falta `feature:`');
360
+ else if (expectedFeature && toSlug(data.feature) !== expectedFeature)
361
+ problems.push(`\`feature: ${data.feature}\` diverge da pasta (${expectedFeature})`);
362
+ if (!STATUS.includes(data.status)) problems.push(`\`status:\` precisa ser ${STATUS.join('|')}`);
363
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(data.atualizado_em || '')) problems.push('`atualizado_em:` precisa ser YYYY-MM-DD');
364
+ if (!('decisoes' in data)) problems.push('falta `decisoes:` (use [] quando não houver)');
365
+ return problems;
366
+ }
367
+
368
+ /** Tipo do artefato pelo nome (`spec-x.md` → spec), ou ''. */
369
+ function artifactTypeOf(fileName) {
370
+ const m = /^([a-z]+)-[a-z0-9][a-z0-9-]*\.md$/.exec(fileName);
371
+ return m && ARTIFACT_TYPES[m[1]] ? m[1] : '';
372
+ }
373
+
374
+ /** Conteúdo de uma seção `## Título` (sem comentários HTML), ou null se ausente. */
375
+ function sectionContent(text, title) {
376
+ const lines = String(text).split('\n');
377
+ const start = lines.findIndex((l) => new RegExp(String.raw`^##\s+${title}\s*$`, 'i').test(l.trim()));
378
+ if (start === -1) return null;
379
+ const out = [];
380
+ for (let i = start + 1; i < lines.length && !/^#{1,2}\s/.test(lines[i]); i++) out.push(lines[i]);
381
+ return out
382
+ .join('\n')
383
+ .replaceAll(/<!--[\s\S]*?-->/g, '')
384
+ .trim();
385
+ }
386
+
387
+ module.exports = {
388
+ TRUNK,
389
+ ARTIFACT_TYPES,
390
+ STATUS,
391
+ PROJECT_WIDE,
392
+ CONFIG_REL,
393
+ toSlug,
394
+ today,
395
+ findUp,
396
+ findWizzRoot,
397
+ readGitState,
398
+ readGitBranch,
399
+ isTrunkBranch,
400
+ featureFromBranch,
401
+ readConfig,
402
+ readConfigFeature,
403
+ resolveConfigPath,
404
+ artifactDirs,
405
+ resolveFeature,
406
+ writeConfigValue,
407
+ writeLayout,
408
+ atomicWrite,
409
+ parseFrontmatter,
410
+ setFrontmatterField,
411
+ frontmatterProblems,
412
+ artifactTypeOf,
413
+ sectionContent,
414
+ };