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.
- package/README.md +12 -1
- package/THIRD-PARTY-NOTICES.md +6 -0
- package/package.json +5 -2
- package/skills-registry.yaml +18 -6
- package/src/bmm-skills/1-analysis/wizz-agent-analyst/customize.toml +1 -1
- package/src/bmm-skills/1-analysis/wizz-set-feature/SKILL.md +34 -12
- package/src/bmm-skills/2-plan-workflows/wizz-agent-pm/customize.toml +1 -1
- package/src/bmm-skills/2-plan-workflows/wizz-agent-ux-designer/customize.toml +1 -1
- package/src/bmm-skills/3-solutioning/wizz-agent-architect/customize.toml +1 -1
- package/src/bmm-skills/4-implementation/wizz-agent-dev/customize.toml +1 -1
- package/src/bmm-skills/module.yaml +10 -0
- package/src/core-skills/_shared/handoff-protocol.md +10 -0
- package/src/modules/wizz/_shared/encerramento.md +4 -0
- package/src/modules/wizz/_shared/organizacao.md +58 -0
- package/src/modules/wizz/_shared/video-pipeline.md +1 -0
- package/src/modules/wizz/agents/wizz-maestro/SKILL.md +9 -1
- package/src/modules/wizz/agents/wizz-maestro/customize.toml +8 -2
- package/src/skills-lib/wizz-inicio-de-projeto/SKILL.md +11 -0
- package/src/skills-lib/wizz-router/SKILL.md +3 -1
- package/src/skills-lib/wizz-router/references/routing-table-flat.md +1 -0
- package/src/skills-lib/wizz-video-styles/SKILL.md +66 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/CREDITS.md +54 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/LIBRARIES.md +31 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/kit.json +182 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/music/acoustic-lifestyle.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/music/cinematic-tense.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/music/classical-light.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/music/corporate-light.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/music/lofi-chill.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/music/meme-beat.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/music/piano-editorial.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/music/tech-pulse.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/boom-impact.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/click-mouse.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/ding-notify.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/glitch-short.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/ka-ching.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/pop-bubble.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/record-scratch.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/riser-short.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/swoosh-text.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/typing-short.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/whoosh-fast.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/audio/sfx/whoosh-soft.mp3 +0 -0
- package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/EditorialReel.tsx +111 -0
- package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/README.md +77 -0
- package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/captions.tsx +56 -0
- package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/example-words.json +285 -0
- package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/primitives.tsx +241 -0
- package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/scenes.tsx +113 -0
- package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/theme.ts +68 -0
- package/src/skills-lib/wizz-video-styles/assets/remotion/editorial/timeline.ts +65 -0
- package/src/skills-lib/wizz-video-styles/data/recipes.json +341 -0
- package/src/skills-lib/wizz-video-styles/data/references.json +3397 -0
- package/src/skills-lib/wizz-video-styles/data/styles.json +726 -0
- package/src/skills-lib/wizz-video-styles/references/audio.md +54 -0
- package/src/skills-lib/wizz-video-styles/references/measured-patterns.md +38 -0
- package/src/skills-lib/wizz-video-styles/references/research-method.md +33 -0
- package/src/skills-lib/wizz-video-styles/references/style-picker.md +51 -0
- package/src/skills-lib/wizz-video-styles/references/styles/camera-no-setup.md +60 -0
- package/src/skills-lib/wizz-video-styles/references/styles/desafio-numero.md +60 -0
- package/src/skills-lib/wizz-video-styles/references/styles/esquete.md +57 -0
- package/src/skills-lib/wizz-video-styles/references/styles/experimento.md +56 -0
- package/src/skills-lib/wizz-video-styles/references/styles/explicador-animado.md +62 -0
- package/src/skills-lib/wizz-video-styles/references/styles/macro-asmr.md +61 -0
- package/src/skills-lib/wizz-video-styles/references/styles/meme-rapido.md +65 -0
- package/src/skills-lib/wizz-video-styles/references/styles/motion-editorial.md +69 -0
- package/src/skills-lib/wizz-video-styles/references/styles/podcast-clip.md +61 -0
- package/src/skills-lib/wizz-video-styles/references/styles/pov-vlog.md +57 -0
- package/src/skills-lib/wizz-video-styles/references/styles/quadro-branco.md +60 -0
- package/src/skills-lib/wizz-video-styles/references/styles/talking-head-cards.md +62 -0
- package/src/skills-lib/wizz-video-styles/references/styles/talking-head-legenda.md +62 -0
- package/src/skills-lib/wizz-video-styles/references/styles/tour.md +59 -0
- package/src/skills-lib/wizz-video-styles/scripts/analyze.sh +66 -0
- package/src/skills-lib/wizz-video-styles/scripts/build_catalog.py +128 -0
- package/tools/hooks/README.md +14 -2
- package/tools/hooks/session-rules.js +65 -140
- package/tools/hooks/wizz-arrumar.js +418 -0
- package/tools/hooks/wizz-artifact-guard.js +160 -0
- package/tools/hooks/wizz-feature-lib.js +414 -0
- package/tools/hooks/wizz-feature.js +401 -0
- package/tools/hooks/wizz-layout-lib.js +152 -0
- package/tools/hooks/wizz-router-enforce.js +21 -1
- package/tools/installer/core/doctor.js +5 -1
- package/tools/installer/core/hooks-settings.js +10 -0
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
// wizz-arrumar.js — arrumação da raiz do projeto com manifesto e desfazer.
|
|
2
|
+
//
|
|
3
|
+
// Fonte de verdade: wizz-method/tools/hooks/. Instalada em `_wizz/hooks/`.
|
|
4
|
+
// É o padrão que deu certo no Spider (2026-10-02), agora repetível:
|
|
5
|
+
// manifesto .tsv revisável, script de desfazer, referências atualizadas em
|
|
6
|
+
// código/docs/memórias e testes rodando antes e depois. Diferença do Spider:
|
|
7
|
+
// o desfazer também restaura os arquivos cujas referências foram reescritas
|
|
8
|
+
// (lá o desfazer só revertia os moves e avisava que o resto ficava torto).
|
|
9
|
+
//
|
|
10
|
+
// layout propõe o layout de pastas (só mostra)
|
|
11
|
+
// layout --gravar [t=dir] grava `layout:` no config, cria pastas + README curto
|
|
12
|
+
// planejar monta o manifesto dos arquivos soltos (não move nada)
|
|
13
|
+
// aplicar <manifesto.tsv> move, atualiza referências, gera desfazer, roda testes
|
|
14
|
+
// mover <origem> <destino> atalho de um arquivo (manifesto de uma linha + aplicar)
|
|
15
|
+
// desfazer <pasta-da-arrumacao>
|
|
16
|
+
//
|
|
17
|
+
// Opções de aplicar/mover: --testes "<comando>", --refs <dir,dir> (ex.: pasta
|
|
18
|
+
// da auto-memória), --manter (não desfaz sozinho se os testes quebrarem).
|
|
19
|
+
// Cada execução fica em `<arquivo>/arrumacao/<data>-<hora>/`.
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
const { spawnSync } = require('node:child_process');
|
|
24
|
+
const lib = require('./wizz-feature-lib.js');
|
|
25
|
+
const layoutLib = require('./wizz-layout-lib.js');
|
|
26
|
+
|
|
27
|
+
const SKIP_DIRS = new Set([
|
|
28
|
+
'node_modules',
|
|
29
|
+
'.git',
|
|
30
|
+
'.next',
|
|
31
|
+
'.nuxt',
|
|
32
|
+
'.svelte-kit',
|
|
33
|
+
'dist',
|
|
34
|
+
'build',
|
|
35
|
+
'out',
|
|
36
|
+
'coverage',
|
|
37
|
+
'_wizz',
|
|
38
|
+
'.venv',
|
|
39
|
+
'venv',
|
|
40
|
+
'__pycache__',
|
|
41
|
+
'.turbo',
|
|
42
|
+
'.vercel',
|
|
43
|
+
'.cache',
|
|
44
|
+
]);
|
|
45
|
+
const TEXT_EXT = new Set([
|
|
46
|
+
'.md',
|
|
47
|
+
'.mdx',
|
|
48
|
+
'.txt',
|
|
49
|
+
'.js',
|
|
50
|
+
'.cjs',
|
|
51
|
+
'.mjs',
|
|
52
|
+
'.ts',
|
|
53
|
+
'.tsx',
|
|
54
|
+
'.jsx',
|
|
55
|
+
'.py',
|
|
56
|
+
'.sh',
|
|
57
|
+
'.bash',
|
|
58
|
+
'.zsh',
|
|
59
|
+
'.json',
|
|
60
|
+
'.yaml',
|
|
61
|
+
'.yml',
|
|
62
|
+
'.toml',
|
|
63
|
+
'.html',
|
|
64
|
+
'.css',
|
|
65
|
+
'.sql',
|
|
66
|
+
'.ini',
|
|
67
|
+
'.cfg',
|
|
68
|
+
'.env.example',
|
|
69
|
+
]);
|
|
70
|
+
const MAX_TEXT_BYTES = 1024 * 1024;
|
|
71
|
+
|
|
72
|
+
function parseArgs(argv) {
|
|
73
|
+
const opts = { _: [] };
|
|
74
|
+
for (let i = 0; i < argv.length; i++) {
|
|
75
|
+
const a = argv[i];
|
|
76
|
+
if (a.startsWith('--')) {
|
|
77
|
+
const key = a.slice(2);
|
|
78
|
+
const next = argv[i + 1];
|
|
79
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
80
|
+
opts[key] = next;
|
|
81
|
+
i++;
|
|
82
|
+
} else opts[key] = true;
|
|
83
|
+
} else opts._.push(a);
|
|
84
|
+
}
|
|
85
|
+
return opts;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stamp() {
|
|
89
|
+
const d = new Date();
|
|
90
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
91
|
+
return `${lib.today()}-${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function archiveDir(root, layout) {
|
|
95
|
+
const dir = layout.arquivo && layoutLib.normalizeDir(layout.arquivo) !== '.' ? layoutLib.normalizeDir(layout.arquivo) : 'arquivo';
|
|
96
|
+
return path.join(root, dir, 'arrumacao');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function isInside(child, parent) {
|
|
100
|
+
const r = path.relative(parent, child);
|
|
101
|
+
return r !== '' && !r.startsWith('..') && !path.isAbsolute(r);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── layout ──────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
const README_BODY = (type, dir) =>
|
|
107
|
+
`# ${dir}\n\nPasta de ${layoutLib.LAYOUT_TYPES[type] || type}.\n\nDefinida em \`_wizz/bmm/config.yaml\` (bloco \`layout:\`). Arquivo novo deste tipo nasce aqui, nunca solto na raiz.\n`;
|
|
108
|
+
|
|
109
|
+
function cmdLayout(root, opts) {
|
|
110
|
+
const config = lib.readConfig(root);
|
|
111
|
+
const { layout: proposed, loose } = layoutLib.proposeLayout(root);
|
|
112
|
+
if (!opts.gravar) {
|
|
113
|
+
const current = Object.keys(config.layout).length > 0 ? config.layout : null;
|
|
114
|
+
const shown = current || proposed;
|
|
115
|
+
const pairs = Object.entries(shown)
|
|
116
|
+
.map(([t, d]) => `${t}=${d}`)
|
|
117
|
+
.join(' ');
|
|
118
|
+
return {
|
|
119
|
+
ok: true,
|
|
120
|
+
line: current
|
|
121
|
+
? `Layout atual: ${pairs}.`
|
|
122
|
+
: `Proposta de layout: ${pairs}. ${loose.length} arquivo(s) solto(s) na raiz. Grave com: layout --gravar ${pairs}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const given = opts._.slice(1).filter((a) => a.includes('='));
|
|
126
|
+
const layout = given.length > 0 ? Object.fromEntries(given.map((a) => a.split('=').map((s) => s.trim()))) : proposed;
|
|
127
|
+
lib.writeLayout(root, layout);
|
|
128
|
+
const created = [];
|
|
129
|
+
for (const [type, dir] of Object.entries(layout)) {
|
|
130
|
+
if (type === 'raiz_extra' || layoutLib.normalizeDir(dir) === '.') continue;
|
|
131
|
+
const abs = path.join(root, layoutLib.normalizeDir(dir));
|
|
132
|
+
if (!isInside(abs, root)) continue;
|
|
133
|
+
fs.mkdirSync(abs, { recursive: true });
|
|
134
|
+
const readme = path.join(abs, 'README.md');
|
|
135
|
+
if (!fs.existsSync(readme)) {
|
|
136
|
+
fs.writeFileSync(readme, README_BODY(type, layoutLib.normalizeDir(dir)));
|
|
137
|
+
created.push(layoutLib.normalizeDir(dir));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
ok: true,
|
|
142
|
+
line: `Layout gravado em _wizz/bmm/config.yaml: ${Object.entries(layout)
|
|
143
|
+
.map(([t, d]) => `${t}=${d}`)
|
|
144
|
+
.join(' ')}.${created.length > 0 ? ` README curto criado em: ${created.join(', ')}.` : ''}`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── manifesto ───────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function readManifest(file) {
|
|
151
|
+
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/).filter(Boolean);
|
|
152
|
+
return lines
|
|
153
|
+
.slice(lines[0] && lines[0].startsWith('origem\t') ? 1 : 0)
|
|
154
|
+
.map((l) => l.split('\t'))
|
|
155
|
+
.filter((cols) => cols.length >= 2 && cols[0] && cols[1])
|
|
156
|
+
.map(([origem, destino]) => ({ origem, destino }));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function writeManifest(file, moves) {
|
|
160
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
161
|
+
fs.writeFileSync(file, ['origem\tdestino', ...moves.map((m) => `${m.origem}\t${m.destino}`)].join('\n') + '\n');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function planMoves(root, layout) {
|
|
165
|
+
const moves = [];
|
|
166
|
+
const semDestino = [];
|
|
167
|
+
for (const e of fs.readdirSync(root, { withFileTypes: true })) {
|
|
168
|
+
if (!e.isFile() || layoutLib.isAllowedAtRoot(e.name, layout)) continue;
|
|
169
|
+
const dest = layoutLib.suggestDestination(e.name, layout);
|
|
170
|
+
if (dest) moves.push({ origem: e.name, destino: `${dest.dir}/${e.name}` });
|
|
171
|
+
else semDestino.push(e.name);
|
|
172
|
+
}
|
|
173
|
+
return { moves, semDestino };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function cmdPlanejar(root, opts) {
|
|
177
|
+
const config = lib.readConfig(root);
|
|
178
|
+
if (Object.keys(config.layout).length === 0) return { ok: false, code: 2, line: 'Sem layout no config: rode `layout --gravar` antes.' };
|
|
179
|
+
const { moves, semDestino } = planMoves(root, config.layout);
|
|
180
|
+
if (moves.length === 0)
|
|
181
|
+
return { ok: true, line: `Raiz já está arrumada.${semDestino.length > 0 ? ` Sem destino: ${semDestino.join(', ')}.` : ''}` };
|
|
182
|
+
const file =
|
|
183
|
+
typeof opts.saida === 'string' ? path.resolve(root, opts.saida) : path.join(archiveDir(root, config.layout), `plano-${stamp()}.tsv`);
|
|
184
|
+
writeManifest(file, moves);
|
|
185
|
+
return {
|
|
186
|
+
ok: true,
|
|
187
|
+
line:
|
|
188
|
+
`Manifesto com ${moves.length} move(s): ${path.relative(root, file)}. Revise e rode: aplicar ${path.relative(root, file)}` +
|
|
189
|
+
(semDestino.length > 0 ? `. Sem destino sugerido: ${semDestino.join(', ')}.` : '.'),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── referências ─────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
function* walkText(dir, skipAbs) {
|
|
196
|
+
let entries = [];
|
|
197
|
+
try {
|
|
198
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
199
|
+
} catch {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
for (const e of entries) {
|
|
203
|
+
const abs = path.join(dir, e.name);
|
|
204
|
+
if (e.isDirectory()) {
|
|
205
|
+
if (SKIP_DIRS.has(e.name) || skipAbs.some((s) => abs === s || isInside(abs, s))) continue;
|
|
206
|
+
yield* walkText(abs, skipAbs);
|
|
207
|
+
} else if (e.isFile()) {
|
|
208
|
+
const ext = e.name.endsWith('.env.example') ? '.env.example' : path.extname(e.name).toLowerCase();
|
|
209
|
+
if (!TEXT_EXT.has(ext)) continue;
|
|
210
|
+
try {
|
|
211
|
+
if (fs.statSync(abs).size <= MAX_TEXT_BYTES) yield abs;
|
|
212
|
+
} catch {
|
|
213
|
+
// some no meio do caminho: ignora
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function escapeRe(s) {
|
|
220
|
+
return s.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Reescreve referências a `origem` (caminho relativo à raiz) para `destino`.
|
|
225
|
+
* Casa `origem` e `./origem` quando não colado a outro caminho; não toca um
|
|
226
|
+
* arquivo cuja pasta tem um irmão com o mesmo nome (a referência é local).
|
|
227
|
+
*/
|
|
228
|
+
function rewriteRefs(text, moves, fileDir) {
|
|
229
|
+
let next = text;
|
|
230
|
+
for (const { origem, destino } of moves) {
|
|
231
|
+
if (fs.existsSync(path.join(fileDir, path.basename(origem))) && fileDir !== '') continue;
|
|
232
|
+
const re = new RegExp(String.raw`(?<![\w/.-])(\./)?${escapeRe(origem)}(?![\w-])`, 'g');
|
|
233
|
+
next = next.replace(re, (_m, dot) => `${dot || ''}${destino}`);
|
|
234
|
+
}
|
|
235
|
+
return next;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── aplicar / desfazer ──────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
function runTests(root, cmd) {
|
|
241
|
+
if (!cmd || cmd === true) return null;
|
|
242
|
+
const r = spawnSync(cmd, { cwd: root, shell: true, encoding: 'utf8', timeout: 10 * 60 * 1000 });
|
|
243
|
+
return { ok: r.status === 0, code: r.status, tail: `${r.stdout || ''}${r.stderr || ''}`.trim().split('\n').slice(-5).join('\n') };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function shQuote(s) {
|
|
247
|
+
return `'${String(s).replaceAll("'", String.raw`'\''`)}'`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function writeUndoScript(runDir, root, moves, edits) {
|
|
251
|
+
const lines = [
|
|
252
|
+
'#!/usr/bin/env bash',
|
|
253
|
+
`# Desfaz a arrumação de ${path.basename(runDir)}: restaura os arquivos editados e devolve os moves.`,
|
|
254
|
+
'set -euo pipefail',
|
|
255
|
+
`cd ${shQuote(root)}`,
|
|
256
|
+
...edits.map((e) => `cp -p ${shQuote(path.join(runDir, e.backup))} ${shQuote(e.original)}`),
|
|
257
|
+
...moves
|
|
258
|
+
.toReversed()
|
|
259
|
+
.map((m) => `[[ -e ${shQuote(m.destino)} && ! -e ${shQuote(m.origem)} ]] && mv ${shQuote(m.destino)} ${shQuote(m.origem)}`),
|
|
260
|
+
'echo "Arrumação desfeita."',
|
|
261
|
+
'',
|
|
262
|
+
];
|
|
263
|
+
const file = path.join(runDir, 'desfazer.sh');
|
|
264
|
+
fs.writeFileSync(file, lines.join('\n'));
|
|
265
|
+
fs.chmodSync(file, 0o755);
|
|
266
|
+
return file;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function undo(root, runDir) {
|
|
270
|
+
const moves = readManifest(path.join(runDir, 'manifesto.tsv'));
|
|
271
|
+
const editsFile = path.join(runDir, 'edicoes.tsv');
|
|
272
|
+
const edits = fs.existsSync(editsFile)
|
|
273
|
+
? fs
|
|
274
|
+
.readFileSync(editsFile, 'utf8')
|
|
275
|
+
.split('\n')
|
|
276
|
+
.filter(Boolean)
|
|
277
|
+
.map((l) => {
|
|
278
|
+
const [original, backup] = l.split('\t');
|
|
279
|
+
return { original, backup };
|
|
280
|
+
})
|
|
281
|
+
: [];
|
|
282
|
+
for (const e of edits) fs.copyFileSync(path.join(runDir, e.backup), e.original);
|
|
283
|
+
for (const m of moves.toReversed()) {
|
|
284
|
+
const from = path.join(root, m.destino);
|
|
285
|
+
const to = path.join(root, m.origem);
|
|
286
|
+
if (fs.existsSync(from) && !fs.existsSync(to)) fs.renameSync(from, to);
|
|
287
|
+
}
|
|
288
|
+
return { moves: moves.length, edits: edits.length };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function validateMoves(root, moves) {
|
|
292
|
+
const problems = [];
|
|
293
|
+
for (const m of moves) {
|
|
294
|
+
const from = path.join(root, m.origem);
|
|
295
|
+
const to = path.join(root, m.destino);
|
|
296
|
+
if (!isInside(from, root) || !isInside(to, root)) problems.push(`${m.origem}: fora do projeto`);
|
|
297
|
+
else if (['_wizz', '.git'].includes(m.origem.split('/')[0])) problems.push(`${m.origem}: pasta protegida`);
|
|
298
|
+
else if (!fs.existsSync(from)) problems.push(`${m.origem}: não existe`);
|
|
299
|
+
else if (fs.existsSync(to)) problems.push(`${m.destino}: destino já existe`);
|
|
300
|
+
}
|
|
301
|
+
return problems;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function applyMoves(root, moves, opts) {
|
|
305
|
+
const config = lib.readConfig(root);
|
|
306
|
+
const problems = validateMoves(root, moves);
|
|
307
|
+
if (problems.length > 0) return { ok: false, code: 2, line: `Nada foi movido: ${problems.join('; ')}.` };
|
|
308
|
+
|
|
309
|
+
const runDir = path.join(archiveDir(root, config.layout), stamp());
|
|
310
|
+
fs.mkdirSync(runDir, { recursive: true });
|
|
311
|
+
writeManifest(path.join(runDir, 'manifesto.tsv'), moves);
|
|
312
|
+
|
|
313
|
+
const before = runTests(root, opts.testes);
|
|
314
|
+
|
|
315
|
+
// Referências: projeto inteiro + pastas extras (auto-memória, Cérebro).
|
|
316
|
+
const extra = typeof opts.refs === 'string' ? opts.refs.split(',').map((d) => path.resolve(root, d.trim())) : [];
|
|
317
|
+
const skip = [archiveDir(root, config.layout)];
|
|
318
|
+
const targets = new Set();
|
|
319
|
+
for (const base of [root, ...extra]) for (const f of walkText(base, skip)) targets.add(f);
|
|
320
|
+
const movedAbs = new Set(moves.map((m) => path.join(root, m.origem)));
|
|
321
|
+
|
|
322
|
+
// 1) Backup + reescrita ANTES de mover: o backup guarda o texto original.
|
|
323
|
+
const edits = [];
|
|
324
|
+
let n = 0;
|
|
325
|
+
for (const file of targets) {
|
|
326
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
327
|
+
const dir = movedAbs.has(file) ? '' : path.dirname(file) === root ? '' : path.dirname(file);
|
|
328
|
+
const next = rewriteRefs(text, moves, dir);
|
|
329
|
+
if (next === text) continue;
|
|
330
|
+
const backup = path.join('antes', String(++n).padStart(4, '0') + '-' + path.basename(file));
|
|
331
|
+
fs.mkdirSync(path.join(runDir, 'antes'), { recursive: true });
|
|
332
|
+
fs.copyFileSync(file, path.join(runDir, backup));
|
|
333
|
+
// Arquivo que também muda de lugar: o desfazer restaura no caminho NOVO e
|
|
334
|
+
// só depois devolve o move; restaurar no antigo deixaria uma cópia órfã.
|
|
335
|
+
const moved = moves.find((m) => path.join(root, m.origem) === file);
|
|
336
|
+
edits.push({ original: moved ? path.join(root, moved.destino) : file, backup });
|
|
337
|
+
fs.writeFileSync(file, next);
|
|
338
|
+
}
|
|
339
|
+
fs.writeFileSync(
|
|
340
|
+
path.join(runDir, 'edicoes.tsv'),
|
|
341
|
+
edits.map((e) => `${e.original}\t${e.backup}`).join('\n') + (edits.length > 0 ? '\n' : ''),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// 2) Moves.
|
|
345
|
+
for (const m of moves) {
|
|
346
|
+
const to = path.join(root, m.destino);
|
|
347
|
+
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
348
|
+
fs.renameSync(path.join(root, m.origem), to);
|
|
349
|
+
}
|
|
350
|
+
const undoFile = writeUndoScript(runDir, root, moves, edits);
|
|
351
|
+
const after = runTests(root, opts.testes);
|
|
352
|
+
const relRun = path.relative(root, runDir);
|
|
353
|
+
|
|
354
|
+
if (before && after && before.ok && !after.ok && !opts.manter) {
|
|
355
|
+
undo(root, runDir);
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
code: 1,
|
|
359
|
+
line: `Os testes passavam antes e quebraram depois: desfiz tudo. Últimas linhas: ${after.tail}. Registro em ${relRun}/.`,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const testes = before
|
|
363
|
+
? ` Testes: antes ${before.ok ? 'ok' : `falhando (${before.code})`}, depois ${after.ok ? 'ok' : `falhando (${after.code})`}.`
|
|
364
|
+
: ' Sem comando de testes (--testes): confira à mão.';
|
|
365
|
+
return {
|
|
366
|
+
ok: !after || after.ok || !before?.ok,
|
|
367
|
+
code: 0,
|
|
368
|
+
line: `Movi ${moves.length} arquivo(s) e atualizei referências em ${edits.length}.${testes} Desfazer: ${path.relative(root, undoFile)}.`,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function cmdAplicar(root, opts) {
|
|
373
|
+
const file = opts._[1];
|
|
374
|
+
if (!file) return { ok: false, code: 2, line: 'Uso: aplicar <manifesto.tsv>.' };
|
|
375
|
+
const moves = readManifest(path.resolve(root, file));
|
|
376
|
+
if (moves.length === 0) return { ok: false, code: 2, line: 'Manifesto vazio: nada a fazer.' };
|
|
377
|
+
return applyMoves(root, moves, opts);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function cmdMover(root, opts) {
|
|
381
|
+
const [, origem, destino] = opts._;
|
|
382
|
+
if (!origem || !destino) return { ok: false, code: 2, line: 'Uso: mover <origem> <destino>.' };
|
|
383
|
+
const relOf = (p) => path.relative(root, path.resolve(root, p));
|
|
384
|
+
return applyMoves(root, [{ origem: relOf(origem), destino: relOf(destino) }], opts);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function cmdDesfazer(root, opts) {
|
|
388
|
+
const dir = opts._[1];
|
|
389
|
+
if (!dir) return { ok: false, code: 2, line: 'Uso: desfazer <pasta-da-arrumacao>.' };
|
|
390
|
+
const runDir = path.resolve(root, dir);
|
|
391
|
+
if (!fs.existsSync(path.join(runDir, 'manifesto.tsv'))) return { ok: false, code: 2, line: `Sem manifesto.tsv em ${dir}.` };
|
|
392
|
+
const r = undo(root, runDir);
|
|
393
|
+
return { ok: true, line: `Desfeito: ${r.moves} move(s) revertido(s), ${r.edits} arquivo(s) restaurado(s).` };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const COMMANDS = { layout: cmdLayout, planejar: cmdPlanejar, aplicar: cmdAplicar, mover: cmdMover, desfazer: cmdDesfazer };
|
|
397
|
+
|
|
398
|
+
function run(argv) {
|
|
399
|
+
const opts = parseArgs(argv);
|
|
400
|
+
const command = COMMANDS[opts._[0]];
|
|
401
|
+
if (!command) return { ok: false, code: 2, line: `Comandos: ${Object.keys(COMMANDS).join(', ')}.` };
|
|
402
|
+
const cwd = typeof opts.cwd === 'string' ? path.resolve(opts.cwd) : process.cwd();
|
|
403
|
+
const root = lib.findWizzRoot(cwd);
|
|
404
|
+
if (!root) return { ok: true, code: 0, line: 'Não é projeto Wizz (sem _wizz/): nada a fazer.' };
|
|
405
|
+
try {
|
|
406
|
+
return command(root, opts);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
return { ok: false, code: 1, line: `Falhou: ${error.message}` };
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (require.main === module) {
|
|
413
|
+
const result = run(process.argv.slice(2));
|
|
414
|
+
process.stdout.write(`${result.line}\n`);
|
|
415
|
+
process.exit(result.code ?? (result.ok ? 0 : 1));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
module.exports = { run, rewriteRefs, readManifest, planMoves, undo };
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// wizz-artifact-guard.js — PostToolUse hook (matcher: Write)
|
|
2
|
+
//
|
|
3
|
+
// Fonte de verdade: wizz-method/tools/hooks/. Instalar via `npm run sync:global`
|
|
4
|
+
// ou pelo installer (registra em `.claude/settings.json` do projeto).
|
|
5
|
+
//
|
|
6
|
+
// Dois avisos, nunca bloqueio (o arquivo já foi escrito; o aviso entra como
|
|
7
|
+
// contexto para o agente corrigir na hora):
|
|
8
|
+
//
|
|
9
|
+
// 1. Artefato fora do lugar: escrito na pasta de artefatos mas fora da
|
|
10
|
+
// subpasta da feature ativa, artefato de feature sem feature ativa, ou
|
|
11
|
+
// artefato tipado (`spec-x.md`...) com frontmatter faltando/errado.
|
|
12
|
+
// 2. Arquivo solto na raiz: fora do `layout:` do config. Sugere o destino e
|
|
13
|
+
// o comando que move atualizando as referências.
|
|
14
|
+
//
|
|
15
|
+
// Fail-open: sem `_wizz/`, sem config, payload estranho ou qualquer erro =
|
|
16
|
+
// silêncio e exit 0. Caso comum (arquivo dentro de uma pasta qualquer) custa
|
|
17
|
+
// zero token.
|
|
18
|
+
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
let lib;
|
|
22
|
+
let layoutLib;
|
|
23
|
+
try {
|
|
24
|
+
lib = require('./wizz-feature-lib.js');
|
|
25
|
+
layoutLib = require('./wizz-layout-lib.js');
|
|
26
|
+
} catch {
|
|
27
|
+
lib = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const HOOK_CMD = '_wizz/hooks';
|
|
31
|
+
|
|
32
|
+
function isInside(child, parent) {
|
|
33
|
+
const r = path.relative(parent, child);
|
|
34
|
+
return r !== '' && !r.startsWith('..') && !path.isAbsolute(r);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Aviso para um arquivo dentro das pastas de artefato, ou ''.
|
|
39
|
+
* @param {string} file - caminho absoluto
|
|
40
|
+
* @param {string} content - conteúdo escrito (pode ser '')
|
|
41
|
+
* @param {ReturnType<typeof lib.resolveFeature>} state
|
|
42
|
+
*/
|
|
43
|
+
function artifactWarning(file, content, state) {
|
|
44
|
+
const dirs = lib.artifactDirs(state.root, state.config);
|
|
45
|
+
const base = [dirs.planning, dirs.implementation].find((d) => isInside(file, d));
|
|
46
|
+
if (!base) return '';
|
|
47
|
+
|
|
48
|
+
const parts = path.relative(base, file).split(path.sep);
|
|
49
|
+
const name = parts.at(-1);
|
|
50
|
+
const type = lib.artifactTypeOf(name);
|
|
51
|
+
const relFile = path.relative(state.root, file);
|
|
52
|
+
const slug = state.slug;
|
|
53
|
+
const msgs = [];
|
|
54
|
+
|
|
55
|
+
if (parts.length === 1) {
|
|
56
|
+
if (lib.PROJECT_WIDE.test(name)) return '';
|
|
57
|
+
if (slug) {
|
|
58
|
+
msgs.push(
|
|
59
|
+
`ARTEFATO FORA DA FEATURE: ${relFile} está na raiz das pastas de artefato, mas a feature ativa é "${slug}". ` +
|
|
60
|
+
`Mova para ${path.relative(state.root, path.join(base, slug, name))} agora (raiz é só para artefato de projeto inteiro: arquitetura, contexto).`,
|
|
61
|
+
);
|
|
62
|
+
} else if (type) {
|
|
63
|
+
msgs.push(
|
|
64
|
+
`ARTEFATO SEM FEATURE: ${relFile} é de feature, mas nenhuma está ativa. ` +
|
|
65
|
+
`Grave a feature a partir do pedido (node ${HOOK_CMD}/wizz-feature.js definir <slug>) e mova o arquivo para a subpasta dela.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
} else if (slug && parts[0] !== slug) {
|
|
69
|
+
msgs.push(
|
|
70
|
+
`ARTEFATO EM OUTRA FEATURE: ${relFile} está em "${parts[0]}", mas a ativa é "${slug}". ` +
|
|
71
|
+
`Se o trabalho é de "${slug}", mova; se o assunto mudou, troque a feature (wizz-feature.js definir ${parts[0]}).`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (type) {
|
|
76
|
+
const problems = lib.frontmatterProblems(content, parts.length > 1 ? parts[0] : slug);
|
|
77
|
+
if (problems.length > 0) msgs.push(`FRONTMATTER de ${relFile}: ${problems.join('; ')}.`);
|
|
78
|
+
}
|
|
79
|
+
return msgs.join(' ');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Aviso para arquivo solto na raiz do projeto, ou ''.
|
|
84
|
+
* @param {string} file
|
|
85
|
+
* @param {ReturnType<typeof lib.resolveFeature>} state
|
|
86
|
+
*/
|
|
87
|
+
function rootWarning(file, state) {
|
|
88
|
+
if (path.dirname(file) !== state.root) return '';
|
|
89
|
+
const name = path.basename(file);
|
|
90
|
+
const layout = state.config.layout;
|
|
91
|
+
if (layoutLib.isAllowedAtRoot(name, layout)) return '';
|
|
92
|
+
|
|
93
|
+
if (!state.hasLayout) {
|
|
94
|
+
return (
|
|
95
|
+
`ARQUIVO SOLTO NA RAIZ: ${name}. O projeto ainda não tem layout de pastas. ` +
|
|
96
|
+
`Proponha um em UMA linha (node ${HOOK_CMD}/wizz-arrumar.js layout) e grave com \`layout --gravar\`; depois mova o arquivo.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const dest = layoutLib.suggestDestination(name, layout);
|
|
100
|
+
if (!dest) {
|
|
101
|
+
return `ARQUIVO SOLTO NA RAIZ: ${name} fica fora do layout. Mova para a pasta do tipo dele, ou libere em \`layout.raiz_extra\` se pertence à raiz.`;
|
|
102
|
+
}
|
|
103
|
+
const target = `${dest.dir}/${name}`;
|
|
104
|
+
return (
|
|
105
|
+
`ARQUIVO SOLTO NA RAIZ: ${name} fica fora do layout (${dest.type} → ${dest.dir}/). ` +
|
|
106
|
+
`Mova agora, atualizando as referências: node ${HOOK_CMD}/wizz-arrumar.js mover ${name} ${target}`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Decide o aviso a partir do payload do PostToolUse. Pura o bastante para teste.
|
|
112
|
+
* @param {{tool_name?: string, tool_input?: {file_path?: string, content?: string}, cwd?: string}} payload
|
|
113
|
+
* @returns {string} '' quando não há o que dizer
|
|
114
|
+
*/
|
|
115
|
+
function buildWarning(payload) {
|
|
116
|
+
if (!lib || !payload || typeof payload !== 'object') return '';
|
|
117
|
+
if (payload.tool_name && payload.tool_name !== 'Write') return '';
|
|
118
|
+
const input = payload.tool_input || {};
|
|
119
|
+
if (typeof input.file_path !== 'string' || !input.file_path) return '';
|
|
120
|
+
|
|
121
|
+
const cwd = typeof payload.cwd === 'string' && payload.cwd ? payload.cwd : process.cwd();
|
|
122
|
+
const file = path.resolve(cwd, input.file_path);
|
|
123
|
+
// O cwd da sessão manda; o diretório do arquivo é a reserva (sessão aberta fora do projeto).
|
|
124
|
+
let state = lib.resolveFeature(cwd);
|
|
125
|
+
if (!state.isWizz || !isInside(file, state.root)) state = lib.resolveFeature(path.dirname(file));
|
|
126
|
+
if (!state.isWizz || !state.config.exists) return '';
|
|
127
|
+
if (!isInside(file, state.root)) return '';
|
|
128
|
+
// Nada dentro de _wizz/ (engine instalada) ou de pastas ocultas é artefato.
|
|
129
|
+
const first = path.relative(state.root, file).split(path.sep)[0];
|
|
130
|
+
if (first === '_wizz' || first.startsWith('.')) return '';
|
|
131
|
+
|
|
132
|
+
return artifactWarning(file, typeof input.content === 'string' ? input.content : '', state) || rootWarning(file, state);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (require.main === module) {
|
|
136
|
+
let raw = '';
|
|
137
|
+
const timer = setTimeout(() => finish(), 3000);
|
|
138
|
+
process.stdin.setEncoding('utf8');
|
|
139
|
+
process.stdin.on('data', (chunk) => (raw += chunk));
|
|
140
|
+
process.stdin.on('end', () => {
|
|
141
|
+
clearTimeout(timer);
|
|
142
|
+
finish();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
function finish() {
|
|
146
|
+
try {
|
|
147
|
+
const warning = buildWarning(JSON.parse(raw || '{}'));
|
|
148
|
+
if (warning) {
|
|
149
|
+
process.stdout.write(
|
|
150
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: `📁 Wizz: ${warning}` } }),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// aviso nunca derruba a ferramenta
|
|
155
|
+
}
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { buildWarning, artifactWarning, rootWarning };
|