@fontdata/ai-harness 2.0.0 → 2.1.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +9 -2
- package/LICENSE +21 -0
- package/README.md +174 -140
- package/{src → dist}/catalog.js +5 -12
- package/dist/cli.js +174 -0
- package/dist/comentarios/command.js +74 -0
- package/dist/comentarios/instalar.js +33 -0
- package/dist/config/config.js +98 -0
- package/dist/contratos/artefatos.js +284 -0
- package/dist/contratos/eventos.js +103 -0
- package/dist/doctor/checks.js +424 -0
- package/dist/init.js +240 -0
- package/dist/install/atomic.js +51 -0
- package/dist/install/flags.js +99 -0
- package/dist/install/hooks.js +47 -0
- package/dist/install/lock.js +39 -0
- package/dist/install/plan.js +209 -0
- package/dist/install/selection.js +77 -0
- package/dist/install/settings.js +108 -0
- package/dist/memoria/indice.js +195 -0
- package/dist/migrar/migrar.js +426 -0
- package/dist/rastro/command.js +46 -0
- package/dist/run.js +86 -0
- package/dist/types.js +1 -0
- package/dist/update/legado.js +74 -0
- package/dist/update/update.js +284 -0
- package/package.json +17 -8
- package/templates/CLAUDE.md +59 -30
- package/templates/agents/auditor-plano.md +42 -0
- package/templates/agents/investigador.md +32 -0
- package/templates/agents/qa.md +44 -0
- package/templates/agents/revisor-testes.md +40 -0
- package/templates/catalog.json +24 -61
- package/templates/commands/atualizar-deps.md +43 -9
- package/templates/commands/diagrama.md +35 -10
- package/templates/commands/gerar-env.md +39 -10
- package/templates/commands/gerar-testes.md +42 -10
- package/templates/commands/git-commit-push.md +0 -0
- package/templates/commands/onboarding.md +44 -15
- package/templates/commands/refatorar.md +50 -10
- package/templates/commands/revisar-pr.md +56 -12
- package/templates/commands/sincronizar-claude-md.md +177 -9
- package/templates/commands/versao.md +0 -0
- package/templates/hooks/comentarios/check.mjs +210 -0
- package/templates/hooks/hooks.json +34 -0
- package/templates/hooks/lembrete.mjs +29 -0
- package/templates/hooks/lib/config.mjs +40 -0
- package/templates/hooks/lib/hook.mjs +85 -0
- package/templates/hooks/lib/rastro.mjs +130 -0
- package/templates/hooks/lib/segredos.mjs +91 -0
- package/templates/hooks/lib/temas.mjs +111 -0
- package/templates/hooks/lib/trabalho.mjs +94 -0
- package/templates/hooks/proteger-contratos.mjs +56 -0
- package/templates/hooks/segredo-no-commit.mjs +76 -0
- package/templates/hooks/sem-jargao.mjs +58 -0
- package/templates/hooks/task-so-fecha-verde.mjs +33 -0
- package/templates/legado/manifesto.json +1267 -0
- package/templates/rules/comentarios.md +166 -0
- package/templates/rules/harness-projeto.md +41 -0
- package/templates/skills/auditar-performance/SKILL.md +53 -0
- package/templates/skills/auditar-performance/references/pontos-quentes.md +35 -0
- package/templates/skills/auditar-performance/templates/relatorios/CLIENTE.md +26 -0
- package/templates/skills/auditar-performance/templates/relatorios/TECNICO.md +27 -0
- package/templates/skills/auditar-seguranca/SKILL.md +67 -0
- package/templates/skills/auditar-seguranca/references/seguranca-tecnica.md +23 -0
- package/templates/skills/auditar-seguranca/templates/relatorios/CLIENTE.md +30 -0
- package/templates/skills/auditar-seguranca/templates/relatorios/TECNICO.md +38 -0
- package/templates/skills/auditar-ui/SKILL.md +64 -0
- package/templates/skills/auditar-ui/references/lentes.md +33 -0
- package/templates/skills/auditar-ui/templates/relatorios/CLIENTE.md +26 -0
- package/templates/skills/auditar-ui/templates/relatorios/TECNICO.md +31 -0
- package/templates/skills/evoluir-harness/SKILL.md +81 -0
- package/templates/skills/evoluir-harness/references/agent.md +83 -0
- package/templates/skills/evoluir-harness/references/auditoria.md +107 -0
- package/templates/skills/evoluir-harness/references/code-reviewer.md +68 -0
- package/templates/skills/evoluir-harness/references/design-system.md +56 -0
- package/templates/skills/evoluir-harness/references/hook.md +95 -0
- package/templates/skills/evoluir-harness/references/rule.md +67 -0
- package/templates/skills/evoluir-harness/references/skill.md +63 -0
- package/templates/skills/executar-plan/SKILL.md +114 -0
- package/templates/skills/executar-plan/references/delegacao.md +43 -0
- package/templates/skills/executar-plan/references/retomada.md +33 -0
- package/templates/skills/executar-plan/templates/BLOQUEIOS.md +30 -0
- package/templates/skills/harness/SKILL.md +75 -0
- package/templates/skills/harness/references/auto-tag.md +118 -0
- package/templates/skills/harness/references/ignores.md +40 -0
- package/templates/skills/harness/references/scaffold.md +29 -0
- package/templates/skills/harness/references/validar-ideia.md +42 -0
- package/templates/skills/memoria/SKILL.md +52 -0
- package/templates/skills/nova-tarefa/SKILL.md +97 -0
- package/templates/skills/nova-tarefa/references/isso-ja-existe.md +24 -0
- package/templates/skills/nova-tarefa/references/regras-do-plano.md +33 -0
- package/templates/skills/nova-tarefa/templates/DECISOES.md +20 -0
- package/templates/skills/nova-tarefa/templates/PLAN.md +58 -0
- package/templates/skills/nova-tarefa/templates/SPEC.md +37 -0
- package/templates/skills/resolver-bug/SKILL.md +104 -0
- package/templates/skills/resolver-bug/references/relatorio-para-cliente.md +26 -0
- package/templates/skills/resolver-bug/templates/PLAN.md +62 -0
- package/templates/skills/resolver-bug/templates/relatorios/CLIENTE.md +29 -0
- package/templates/skills/resolver-bug/templates/relatorios/TECNICO.md +32 -0
- package/bin/cli.js +0 -122
- package/src/init.js +0 -317
- package/src/installer.js +0 -92
- package/src/run.js +0 -99
- package/templates/commands/auditar-performance.md +0 -27
- package/templates/commands/auditar-seguranca.md +0 -64
- package/templates/commands/auditar-ui.md +0 -38
- package/templates/commands/codigo-morto.md +0 -22
- package/templates/commands/configurar-auto-tag.md +0 -185
- package/templates/commands/configurar-comentarios.md +0 -543
- package/templates/commands/configurar-orquestrador.md +0 -13
- package/templates/commands/corrigir-review.md +0 -11
- package/templates/commands/criar-agents.md +0 -42
- package/templates/commands/criar-code-reviewer.md +0 -30
- package/templates/commands/criar-hooks.md +0 -32
- package/templates/commands/criar-rag.md +0 -25
- package/templates/commands/criar-rules.md +0 -31
- package/templates/commands/criar-skill.md +0 -22
- package/templates/commands/design-system.md +0 -27
- package/templates/commands/executar-plan.md +0 -36
- package/templates/commands/gerar-claude-md.md +0 -66
- package/templates/commands/gerar-ignore.md +0 -37
- package/templates/commands/gerar-spec.md +0 -33
- package/templates/commands/harness.md +0 -34
- package/templates/commands/nova-tarefa.md +0 -60
- package/templates/commands/resolver-bug.md +0 -25
- package/templates/commands/review-por-task.md +0 -16
- package/templates/commands/scaffold.md +0 -25
- package/templates/commands/validar-commands.md +0 -24
- package/templates/commands/validar-ideia.md +0 -36
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, posix, resolve, sep } from 'node:path';
|
|
4
|
+
import { parse, stringify } from 'yaml';
|
|
5
|
+
import { SCHEMA_VERSION, WORK_ID } from '../contratos/artefatos.js';
|
|
6
|
+
import { suggestFlag } from '../install/flags.js';
|
|
7
|
+
/** Layout antigo (até a 2.0) → artefato do contrato de pastas por trabalho. */
|
|
8
|
+
const SOURCES = {
|
|
9
|
+
specs: { tipo: 'spec', target: 'SPEC.md' },
|
|
10
|
+
plans: { tipo: 'plano', target: 'PLAN.md' },
|
|
11
|
+
bugs: { tipo: 'plano', target: 'PLAN.md' },
|
|
12
|
+
investigacoes: { tipo: 'relatorio_tecnico', target: 'relatorios/TECNICO.md' },
|
|
13
|
+
};
|
|
14
|
+
const SOURCE_DIRS = Object.keys(SOURCES);
|
|
15
|
+
const IGNORED_NAME = /^(readme|index)\.md$/i;
|
|
16
|
+
/** Sufixos de documentos que acompanham outro de mesmo nome base. */
|
|
17
|
+
const SATELLITE_SUFFIXES = ['-plano', '-design', '-contrato-api', '-validacao'];
|
|
18
|
+
const LEGACY_PATH = /\b[Dd]ocs\/(specs|plans|bugs|investigacoes)\b/;
|
|
19
|
+
const OPEN_BOX = /^\s*[-*]\s+\[ \]/m;
|
|
20
|
+
function slugOf(fileName) {
|
|
21
|
+
const slug = fileName
|
|
22
|
+
.replace(/\.md$/i, '')
|
|
23
|
+
.normalize('NFD')
|
|
24
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
27
|
+
.replace(/^-+|-+$/g, '');
|
|
28
|
+
return slug || 'documento';
|
|
29
|
+
}
|
|
30
|
+
function isGitRepo(root) {
|
|
31
|
+
return existsSync(join(root, '.git'));
|
|
32
|
+
}
|
|
33
|
+
function isTracked(root, path) {
|
|
34
|
+
try {
|
|
35
|
+
execFileSync('git', ['ls-files', '--error-unmatch', path], { cwd: root, stdio: 'ignore' });
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** `docs` quando existe com esse nome; senão a pasta com outra caixa (como `Docs`), se houver. */
|
|
43
|
+
function realDocsDir(root) {
|
|
44
|
+
const entries = readdirSync(root);
|
|
45
|
+
if (entries.includes('docs'))
|
|
46
|
+
return 'docs';
|
|
47
|
+
return entries.find((name) => name.toLowerCase() === 'docs' && statSync(join(root, name)).isDirectory()) ?? null;
|
|
48
|
+
}
|
|
49
|
+
/** Data do commit que criou o arquivo; fora do git, a data de modificação. */
|
|
50
|
+
function creationDate(root, realPath, git) {
|
|
51
|
+
if (git) {
|
|
52
|
+
try {
|
|
53
|
+
const dates = execFileSync('git', ['log', '--diff-filter=A', '--follow', '--format=%cs', '--', realPath], {
|
|
54
|
+
cwd: root,
|
|
55
|
+
encoding: 'utf8',
|
|
56
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
57
|
+
})
|
|
58
|
+
.split('\n')
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
const first = dates.at(-1);
|
|
61
|
+
if (first)
|
|
62
|
+
return first;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Fora do índice: vale a data do arquivo.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return statSync(join(root, realPath)).mtime.toISOString().slice(0, 10);
|
|
69
|
+
}
|
|
70
|
+
function markdownFiles(dir) {
|
|
71
|
+
if (!existsSync(dir))
|
|
72
|
+
return [];
|
|
73
|
+
return readdirSync(dir, { recursive: true, encoding: 'utf8' })
|
|
74
|
+
.filter((rel) => rel.endsWith('.md') && statSync(join(dir, rel)).isFile())
|
|
75
|
+
.map((rel) => rel.split(sep).join('/'))
|
|
76
|
+
.sort();
|
|
77
|
+
}
|
|
78
|
+
function legacyReferences(root) {
|
|
79
|
+
const files = [
|
|
80
|
+
'CLAUDE.md',
|
|
81
|
+
...['rules', 'agents', 'skills', 'commands'].flatMap((dir) => markdownFiles(join(root, '.claude', dir)).map((rel) => `.claude/${dir}/${rel}`)),
|
|
82
|
+
];
|
|
83
|
+
return files
|
|
84
|
+
.filter((rel) => existsSync(join(root, rel)))
|
|
85
|
+
.flatMap((rel) => readFileSync(join(root, rel), 'utf8')
|
|
86
|
+
.split('\n')
|
|
87
|
+
.flatMap((line, index) => (LEGACY_PATH.test(line) ? [`${rel}:${index + 1}`] : [])));
|
|
88
|
+
}
|
|
89
|
+
/** Destino de cada documento de um trabalho. Satélite sem lugar próprio vai para `anexos/`, sem frontmatter. */
|
|
90
|
+
function assignTargets(members) {
|
|
91
|
+
const assigned = members.map(({ doc, suffix }) => {
|
|
92
|
+
if (suffix === '-plano')
|
|
93
|
+
return { doc, suffix, target: 'PLAN.md', tipo: 'plano' };
|
|
94
|
+
if (suffix === '-design')
|
|
95
|
+
return { doc, suffix, target: 'SPEC.md', tipo: 'spec' };
|
|
96
|
+
if (suffix)
|
|
97
|
+
return { doc, suffix, target: `anexos/${doc.name}`, tipo: 'anexo' };
|
|
98
|
+
return { doc, suffix, ...SOURCES[doc.dir] };
|
|
99
|
+
});
|
|
100
|
+
for (const entry of assigned.filter((candidate) => candidate.suffix === '-plano' || candidate.suffix === '-design')) {
|
|
101
|
+
const rival = assigned.find((other) => other !== entry && !other.suffix && other.target === entry.target);
|
|
102
|
+
if (!rival)
|
|
103
|
+
continue;
|
|
104
|
+
// Relatório do bug + plano de correção: o relatório vira o relatório técnico do trabalho.
|
|
105
|
+
if (entry.suffix === '-plano' && rival.doc.dir === 'bugs' && !assigned.some((other) => other.target === 'relatorios/TECNICO.md')) {
|
|
106
|
+
rival.target = 'relatorios/TECNICO.md';
|
|
107
|
+
rival.tipo = 'relatorio_tecnico';
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
entry.target = `anexos/${entry.doc.name}`;
|
|
111
|
+
entry.tipo = 'anexo';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return assigned;
|
|
115
|
+
}
|
|
116
|
+
export function planMigration(root, options = {}) {
|
|
117
|
+
const today = options.today ?? new Date().toISOString().slice(0, 10);
|
|
118
|
+
const docsDir = realDocsDir(root);
|
|
119
|
+
const plan = { moves: [], ignored: [], conflicts: [], docsDir: docsDir ?? 'docs', references: [], today };
|
|
120
|
+
if (!docsDir)
|
|
121
|
+
return plan;
|
|
122
|
+
const git = isGitRepo(root);
|
|
123
|
+
const docs = [];
|
|
124
|
+
for (const dir of SOURCE_DIRS) {
|
|
125
|
+
const abs = join(root, docsDir, dir);
|
|
126
|
+
if (!existsSync(abs) || !statSync(abs).isDirectory())
|
|
127
|
+
continue;
|
|
128
|
+
const names = readdirSync(abs)
|
|
129
|
+
.filter((name) => name.toLowerCase().endsWith('.md') && statSync(join(abs, name)).isFile())
|
|
130
|
+
.sort();
|
|
131
|
+
for (const name of names) {
|
|
132
|
+
const from = `docs/${dir}/${name}`;
|
|
133
|
+
if (IGNORED_NAME.test(name))
|
|
134
|
+
plan.ignored.push(from);
|
|
135
|
+
else
|
|
136
|
+
docs.push({ dir, name, realPath: `${docsDir}/${dir}/${name}`, from, slug: slugOf(name) });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const slugs = new Set(docs.map((doc) => doc.slug));
|
|
140
|
+
const groups = new Map();
|
|
141
|
+
for (const doc of docs) {
|
|
142
|
+
const suffix = SATELLITE_SUFFIXES.find((candidate) => doc.slug.endsWith(candidate) && slugs.has(doc.slug.slice(0, -candidate.length))) ?? null;
|
|
143
|
+
const key = suffix ? doc.slug.slice(0, -suffix.length) : doc.slug;
|
|
144
|
+
groups.set(key, [...(groups.get(key) ?? []), { doc, suffix }]);
|
|
145
|
+
}
|
|
146
|
+
for (const [key, members] of groups) {
|
|
147
|
+
const firstDate = members.map(({ doc }) => creationDate(root, doc.realPath, git)).sort()[0] ?? today;
|
|
148
|
+
const id = WORK_ID.test(key) ? key : `${firstDate}-${key}`;
|
|
149
|
+
const trabalhoTipo = members.some(({ doc }) => doc.dir === 'bugs')
|
|
150
|
+
? 'bug'
|
|
151
|
+
: members.some(({ doc }) => doc.dir === 'specs' || doc.dir === 'plans')
|
|
152
|
+
? 'feature'
|
|
153
|
+
: 'investigacao';
|
|
154
|
+
const byTarget = new Map();
|
|
155
|
+
for (const entry of assignTargets(members))
|
|
156
|
+
byTarget.set(entry.target, [...(byTarget.get(entry.target) ?? []), entry]);
|
|
157
|
+
for (const [target, entries] of byTarget) {
|
|
158
|
+
const to = `docs/trabalhos/${id}/${target}`;
|
|
159
|
+
const [entry] = entries;
|
|
160
|
+
if (!entry || entries.length > 1 || existsSync(join(root, to))) {
|
|
161
|
+
plan.conflicts.push({ destino: to, origens: entries.map((candidate) => candidate.doc.from) });
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
plan.moves.push({ from: entry.doc.from, to, id, trabalhoTipo, tipo: entry.tipo });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const order = new Map(docs.map((doc, index) => [doc.from, index]));
|
|
168
|
+
plan.moves.sort((a, b) => (order.get(a.from) ?? 0) - (order.get(b.from) ?? 0));
|
|
169
|
+
if (plan.moves.length)
|
|
170
|
+
plan.references = legacyReferences(root);
|
|
171
|
+
return plan;
|
|
172
|
+
}
|
|
173
|
+
const ROOT_CAUSE_LABEL = /^(?:#{1,6}\s*(?:\d+[.)]\s*)?|\s*[-*]?\s*\*{0,2})causa[\s-]*raiz/im;
|
|
174
|
+
const UNCONFIRMED = /n[ãa]o\s+(?:foi\s+)?(?:confirmad|comprovad)|hip[óo]tese|a\s+confirmar|suspeita/i;
|
|
175
|
+
/** Com seção ou rótulo de causa raiz, a causa é comprovada, a não ser que o trecho diga que ainda é hipótese. */
|
|
176
|
+
function rootCauseOf(body) {
|
|
177
|
+
const label = ROOT_CAUSE_LABEL.exec(body);
|
|
178
|
+
if (!label)
|
|
179
|
+
return 'hipotese';
|
|
180
|
+
return UNCONFIRMED.test(body.slice(label.index, label.index + 600)) ? 'hipotese' : 'comprovada';
|
|
181
|
+
}
|
|
182
|
+
const TASK_HEADING = /^(#{2,6})\s*(?:(?:task|tarefa)\s+|T-?)(\d+)\.(\d+)\b\s*[—–:.-]*\s*(.*)$/i;
|
|
183
|
+
const TASK_ITEM = /^\s*[-*]\s+\[([ xX])\]\s+\*{0,2}(?:(?:task|tarefa)\s+|T-?)(\d+)\.(\d+)\b\s*[—–:.-]*\s*(.*)$/i;
|
|
184
|
+
const ANY_HEADING = /^(#{1,6})\s/;
|
|
185
|
+
const BOX = /^\s*[-*]\s+\[([ xX])\]/;
|
|
186
|
+
const DONE_MARK = /✅|\bconclu[ií]d[ao]\b/i;
|
|
187
|
+
function cleanTitle(raw, sprint, number) {
|
|
188
|
+
const title = raw
|
|
189
|
+
.replace(/[[(][^\])]*conclu[^\])]*[\])]/gi, '')
|
|
190
|
+
.replace(/✅/g, '')
|
|
191
|
+
.replace(/\bconclu[ií]d[ao]\b/gi, '')
|
|
192
|
+
.replace(/\*+/g, '')
|
|
193
|
+
.replace(/[\s—–:.-]+$/, '')
|
|
194
|
+
.trim()
|
|
195
|
+
.slice(0, 120);
|
|
196
|
+
return title || `Task ${sprint}.${number}`;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Tasks do plano antigo nos formatos encontrados nos projetos: `#### Task 1.1 — Título`, `### T1.1 — Título`
|
|
200
|
+
* e `- [x] **T1.1 — Título**`. Concluída quando marcada (✅, "concluída", `[x]`), quando a seção só tem caixas
|
|
201
|
+
* marcadas ou quando o plano inteiro não tem caixa aberta.
|
|
202
|
+
*/
|
|
203
|
+
function parseTasks(body) {
|
|
204
|
+
const found = [];
|
|
205
|
+
let current = null;
|
|
206
|
+
for (const line of body.split('\n')) {
|
|
207
|
+
const heading = TASK_HEADING.exec(line);
|
|
208
|
+
if (heading) {
|
|
209
|
+
const titulo = heading[4] ?? '';
|
|
210
|
+
current = { level: (heading[1] ?? '').length, sprint: Number(heading[2]), number: Number(heading[3]), titulo, marked: DONE_MARK.test(titulo), open: 0, closed: 0 };
|
|
211
|
+
found.push(current);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const item = TASK_ITEM.exec(line);
|
|
215
|
+
if (item) {
|
|
216
|
+
const titulo = item[4] ?? '';
|
|
217
|
+
found.push({ level: 7, sprint: Number(item[2]), number: Number(item[3]), titulo, marked: item[1] !== ' ' || DONE_MARK.test(titulo), open: 0, closed: 0 });
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const other = ANY_HEADING.exec(line);
|
|
221
|
+
if (other && current && (other[1] ?? '').length <= current.level)
|
|
222
|
+
current = null;
|
|
223
|
+
const box = BOX.exec(line);
|
|
224
|
+
if (box && current) {
|
|
225
|
+
if (box[1] === ' ')
|
|
226
|
+
current.open += 1;
|
|
227
|
+
else
|
|
228
|
+
current.closed += 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const planDone = !OPEN_BOX.test(body);
|
|
232
|
+
const ids = new Set();
|
|
233
|
+
const tasks = [];
|
|
234
|
+
for (const task of found) {
|
|
235
|
+
let number = task.number;
|
|
236
|
+
const idFor = (n) => `T-${String(task.sprint).padStart(2, '0')}.${String(n).padStart(2, '0')}`;
|
|
237
|
+
while (ids.has(idFor(number)))
|
|
238
|
+
number += 1;
|
|
239
|
+
if (task.sprint > 99 || number > 99)
|
|
240
|
+
continue;
|
|
241
|
+
ids.add(idFor(number));
|
|
242
|
+
tasks.push({
|
|
243
|
+
id: idFor(number),
|
|
244
|
+
titulo: cleanTitle(task.titulo, task.sprint, task.number),
|
|
245
|
+
status: task.marked || planDone || (task.open === 0 && task.closed > 0) ? 'concluida' : 'pendente',
|
|
246
|
+
depende_de: [],
|
|
247
|
+
paralelizavel: false,
|
|
248
|
+
testes_criticos: [],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return tasks;
|
|
252
|
+
}
|
|
253
|
+
function contractFrontmatter(content, move, today) {
|
|
254
|
+
const match = /^\s*---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(content);
|
|
255
|
+
const parsed = match ? parse(match[1] ?? '') : null;
|
|
256
|
+
const existing = typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
257
|
+
const body = match ? content.slice(match[0].length) : content;
|
|
258
|
+
const heading = /^#\s+(.+)$/m.exec(body)?.[1]?.trim();
|
|
259
|
+
const status = move.tipo === 'spec' ? 'aprovado' : move.tipo === 'plano' ? (OPEN_BOX.test(body) ? 'em-execucao' : 'concluido') : 'final';
|
|
260
|
+
const data = {
|
|
261
|
+
schema_versao: SCHEMA_VERSION,
|
|
262
|
+
tipo: move.tipo,
|
|
263
|
+
id: move.id,
|
|
264
|
+
trabalho_tipo: move.trabalhoTipo,
|
|
265
|
+
titulo: heading ?? move.id,
|
|
266
|
+
status,
|
|
267
|
+
criado_em: move.id.slice(0, 10),
|
|
268
|
+
atualizado_em: today,
|
|
269
|
+
arquivos: [],
|
|
270
|
+
migrado: true,
|
|
271
|
+
...(move.tipo === 'plano'
|
|
272
|
+
? {
|
|
273
|
+
condensado: false,
|
|
274
|
+
auditoria_plano: 'pendente',
|
|
275
|
+
causa_raiz: move.trabalhoTipo === 'bug' ? rootCauseOf(body) : null,
|
|
276
|
+
tasks: parseTasks(body),
|
|
277
|
+
}
|
|
278
|
+
: {}),
|
|
279
|
+
};
|
|
280
|
+
// O que já existia prevalece, exceto a identidade do artefato, que o contrato define.
|
|
281
|
+
const merged = { ...data, ...existing, schema_versao: SCHEMA_VERSION, tipo: move.tipo, id: move.id, trabalho_tipo: move.trabalhoTipo, migrado: true };
|
|
282
|
+
return `---\n${stringify(merged)}---\n${body.startsWith('\n') ? '' : '\n'}${body}`;
|
|
283
|
+
}
|
|
284
|
+
function safeDecode(path) {
|
|
285
|
+
try {
|
|
286
|
+
return decodeURI(path);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return path;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Links do documento migrado (D-36): o relativo é resolvido a partir do lugar antigo e reescrito a partir do
|
|
294
|
+
* novo, e o caminho de uma pasta antiga passa a apontar para o destino do documento citado.
|
|
295
|
+
*/
|
|
296
|
+
function rewriteLinks(content, move, destinations) {
|
|
297
|
+
const fromDir = posix.dirname(move.from);
|
|
298
|
+
const toDir = posix.dirname(move.to);
|
|
299
|
+
const destinationOf = (path) => destinations.get(path.replace(/^Docs\//, 'docs/'));
|
|
300
|
+
const relinked = content.replace(/\]\(([^)\s#]+)(#[^)\s]*)?\)/g, (whole, target, anchor) => {
|
|
301
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith('/'))
|
|
302
|
+
return whole;
|
|
303
|
+
const decoded = safeDecode(target);
|
|
304
|
+
if (/^[Dd]ocs\//.test(decoded)) {
|
|
305
|
+
const moved = destinationOf(decoded);
|
|
306
|
+
return moved ? `](${moved}${anchor ?? ''})` : whole;
|
|
307
|
+
}
|
|
308
|
+
const resolved = posix.normalize(posix.join(fromDir, decoded));
|
|
309
|
+
const destination = destinationOf(resolved) ?? resolved;
|
|
310
|
+
const relative = posix.relative(toDir, destination) || posix.basename(destination);
|
|
311
|
+
return `](${target.includes('%') ? encodeURI(relative) : relative}${anchor ?? ''})`;
|
|
312
|
+
});
|
|
313
|
+
return relinked.replace(/\b[Dd]ocs\/(?:specs|plans|bugs|investigacoes)\/[^\s)`'"\]<>]+?\.md\b/g, (path) => destinationOf(path) ?? path);
|
|
314
|
+
}
|
|
315
|
+
/** `Docs/` → `docs/` em dois passos: num disco que não distingue caixa, a troca direta não tem efeito. */
|
|
316
|
+
function renameDocsDir(root, from, git) {
|
|
317
|
+
const temp = `${from}-ai-harness-migrar`;
|
|
318
|
+
const move = (source, target) => {
|
|
319
|
+
if (git) {
|
|
320
|
+
try {
|
|
321
|
+
execFileSync('git', ['mv', source, target], { cwd: root, stdio: 'ignore' });
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
// Pasta sem arquivo versionado: o git recusa, e a troca vai direto no disco.
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
renameSync(join(root, source), join(root, target));
|
|
329
|
+
};
|
|
330
|
+
move(from, temp);
|
|
331
|
+
move(temp, 'docs');
|
|
332
|
+
}
|
|
333
|
+
/** Pasta antiga vazia, ou só com `.gitkeep`, deixa de existir. */
|
|
334
|
+
function removeLegacyDir(root, rel, git) {
|
|
335
|
+
const dir = join(root, rel);
|
|
336
|
+
if (!existsSync(dir))
|
|
337
|
+
return;
|
|
338
|
+
const entries = readdirSync(dir).filter((name) => name !== '.DS_Store');
|
|
339
|
+
if (entries.some((name) => name !== '.gitkeep'))
|
|
340
|
+
return;
|
|
341
|
+
const keep = `${rel}/.gitkeep`;
|
|
342
|
+
if (entries.includes('.gitkeep') && git && isTracked(root, keep))
|
|
343
|
+
execFileSync('git', ['rm', '-q', keep], { cwd: root, stdio: 'ignore' });
|
|
344
|
+
rmSync(dir, { recursive: true, force: true });
|
|
345
|
+
}
|
|
346
|
+
/** Move os documentos e aplica o frontmatter; arquivos versionados vão com `git mv` para manter o histórico. */
|
|
347
|
+
export function applyMigration(root, plan) {
|
|
348
|
+
const git = isGitRepo(root);
|
|
349
|
+
if (plan.moves.length && plan.docsDir !== 'docs')
|
|
350
|
+
renameDocsDir(root, plan.docsDir, git);
|
|
351
|
+
for (const move of plan.moves) {
|
|
352
|
+
const target = join(root, move.to);
|
|
353
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
354
|
+
if (git && isTracked(root, move.from))
|
|
355
|
+
execFileSync('git', ['mv', move.from, move.to], { cwd: root, stdio: 'ignore' });
|
|
356
|
+
else
|
|
357
|
+
renameSync(join(root, move.from), target);
|
|
358
|
+
}
|
|
359
|
+
// Links e frontmatter só depois de mover tudo: cada link precisa do destino final do documento citado.
|
|
360
|
+
const destinations = new Map(plan.moves.map((move) => [move.from, move.to]));
|
|
361
|
+
for (const move of plan.moves) {
|
|
362
|
+
const target = join(root, move.to);
|
|
363
|
+
const content = rewriteLinks(readFileSync(target, 'utf8'), move, destinations);
|
|
364
|
+
writeFileSync(target, move.tipo === 'anexo' ? content : contractFrontmatter(content, move, plan.today));
|
|
365
|
+
}
|
|
366
|
+
for (const dir of SOURCE_DIRS)
|
|
367
|
+
removeLegacyDir(root, `docs/${dir}`, git);
|
|
368
|
+
}
|
|
369
|
+
const MIGRATION_FLAGS = ['--yes', '-y', '--sim'];
|
|
370
|
+
const REFERENCES_SHOWN = 40;
|
|
371
|
+
/** `ai-harness migrar [caminho] [--yes]`; sem `--yes` só simula. */
|
|
372
|
+
export function runMigrationCommand(argv) {
|
|
373
|
+
let path;
|
|
374
|
+
let yes = false;
|
|
375
|
+
for (const token of argv) {
|
|
376
|
+
if (!token.startsWith('-')) {
|
|
377
|
+
if (path !== undefined)
|
|
378
|
+
throw new Error(`Argumento inesperado: ${token}`);
|
|
379
|
+
path = token;
|
|
380
|
+
}
|
|
381
|
+
else if (MIGRATION_FLAGS.includes(token)) {
|
|
382
|
+
yes = true;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
const suggestion = suggestFlag(token, MIGRATION_FLAGS);
|
|
386
|
+
throw new Error(`Flag desconhecida: ${token}.${suggestion ? ` Você quis dizer ${suggestion}?` : ''}`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const root = resolve(process.cwd(), path ?? '.');
|
|
390
|
+
if (!existsSync(root))
|
|
391
|
+
throw new Error(`Pasta não encontrada: ${root}`);
|
|
392
|
+
const plan = planMigration(root);
|
|
393
|
+
if (!plan.moves.length && !plan.conflicts.length) {
|
|
394
|
+
const ignored = plan.ignored.length ? `\nIgnorados: ${plan.ignored.join(', ')}` : '';
|
|
395
|
+
console.log(`Nada a migrar: o projeto não tem documentos em docs/specs, docs/plans, docs/bugs nem docs/investigacoes.${ignored}`);
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
398
|
+
const lines = [`Documentos a migrar (${plan.moves.length}):`, ...plan.moves.map((move) => ` ${move.from} → ${move.to}`)];
|
|
399
|
+
if (plan.docsDir !== 'docs') {
|
|
400
|
+
lines.push('', `A pasta ${plan.docsDir}/ será renomeada para docs/ antes de migrar, com git mv quando versionada.`);
|
|
401
|
+
}
|
|
402
|
+
if (plan.ignored.length) {
|
|
403
|
+
lines.push('', `Ignorados, ficam onde estão (${plan.ignored.length}):`, ...plan.ignored.map((ignored) => ` - ${ignored}`));
|
|
404
|
+
}
|
|
405
|
+
if (plan.conflicts.length) {
|
|
406
|
+
lines.push('', `Conflitos, ficam onde estão e o restante migra (${plan.conflicts.length}):`, ...plan.conflicts.map((conflict) => ` ! ${conflict.destino} ← ${conflict.origens.join(', ')}`));
|
|
407
|
+
}
|
|
408
|
+
if (plan.references.length) {
|
|
409
|
+
lines.push('', `Citações às pastas antigas no CLAUDE.md e em .claude/, para revisar à mão (${plan.references.length}):`, ...plan.references.slice(0, REFERENCES_SHOWN).map((reference) => ` ${reference}`));
|
|
410
|
+
if (plan.references.length > REFERENCES_SHOWN)
|
|
411
|
+
lines.push(` … e mais ${plan.references.length - REFERENCES_SHOWN}`);
|
|
412
|
+
}
|
|
413
|
+
console.log(lines.join('\n'));
|
|
414
|
+
if (!plan.moves.length) {
|
|
415
|
+
console.error('\nNenhum documento pode ser migrado: resolva os conflitos acima.');
|
|
416
|
+
return 1;
|
|
417
|
+
}
|
|
418
|
+
if (!yes) {
|
|
419
|
+
console.log('\nSimulação — nada foi movido. Rode de novo com --yes para migrar.');
|
|
420
|
+
return 0;
|
|
421
|
+
}
|
|
422
|
+
applyMigration(root, plan);
|
|
423
|
+
console.log(`\nMigração concluída: ${plan.moves.length} documento(s) em docs/trabalhos/. ` +
|
|
424
|
+
'Rode `npx @fontdata/ai-harness doctor` para conferir o contrato.');
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { templatesDir } from '../catalog.js';
|
|
4
|
+
import { suggestFlag } from '../install/flags.js';
|
|
5
|
+
/** O CLI grava pela mesma biblioteca dos hooks, para as duas formas produzirem o mesmo formato. */
|
|
6
|
+
export async function loadRastroLib() {
|
|
7
|
+
return (await import(pathToFileURL(join(templatesDir, 'hooks', 'lib', 'rastro.mjs')).href));
|
|
8
|
+
}
|
|
9
|
+
const FLAGS = ['--trabalho', '--task', '--nome', '--resultado', '--dados'];
|
|
10
|
+
/** `ai-harness evento <evento> [--trabalho id] [--task T-SS.NN] [--nome n] [--resultado r] [--dados json]` */
|
|
11
|
+
export async function runEventCommand(argv) {
|
|
12
|
+
const [evento, ...rest] = argv;
|
|
13
|
+
if (!evento || evento.startsWith('-')) {
|
|
14
|
+
throw new Error('Informe o evento. Ex.: npx @fontdata/ai-harness evento suite_verde --task T-01.02');
|
|
15
|
+
}
|
|
16
|
+
const fields = { evento, origem: 'cli', nome: 'ai-harness' };
|
|
17
|
+
for (let i = 0; i < rest.length; i++) {
|
|
18
|
+
const flag = rest[i] ?? '';
|
|
19
|
+
if (!FLAGS.includes(flag)) {
|
|
20
|
+
const suggestion = suggestFlag(flag, FLAGS);
|
|
21
|
+
throw new Error(`Flag desconhecida: ${flag}.${suggestion ? ` Você quis dizer ${suggestion}?` : ''}`);
|
|
22
|
+
}
|
|
23
|
+
const value = rest[i + 1];
|
|
24
|
+
if (value === undefined)
|
|
25
|
+
throw new Error(`${flag} precisa de um valor.`);
|
|
26
|
+
i++;
|
|
27
|
+
if (flag === '--trabalho')
|
|
28
|
+
fields.trabalhoId = value;
|
|
29
|
+
else if (flag === '--task')
|
|
30
|
+
fields.taskId = value;
|
|
31
|
+
else if (flag === '--nome')
|
|
32
|
+
fields.nome = value;
|
|
33
|
+
else if (flag === '--resultado')
|
|
34
|
+
fields.resultado = value;
|
|
35
|
+
else {
|
|
36
|
+
try {
|
|
37
|
+
fields.dados = JSON.parse(value);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error('--dados precisa ser um JSON válido.');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
(await loadRastroLib()).recordEvent(process.cwd(), fields);
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { allCommands, loadCatalog } from './catalog.js';
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
/** Raiz do pacote — é ela que vira o plugin carregado via `claude --plugin-dir`. */
|
|
8
|
+
export const pluginRoot = join(__dirname, '..');
|
|
9
|
+
/** Nome do plugin, lido do manifesto para não duplicar a string. */
|
|
10
|
+
export function pluginName() {
|
|
11
|
+
const manifest = JSON.parse(readFileSync(join(pluginRoot, '.claude-plugin', 'plugin.json'), 'utf8'));
|
|
12
|
+
return manifest.name;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Ids executáveis: os comandos dos grupos mais o comando-mestre, que fica fora
|
|
16
|
+
* de `groups` no catálogo.
|
|
17
|
+
*/
|
|
18
|
+
export function runnableIds(catalog = loadCatalog()) {
|
|
19
|
+
const ids = allCommands(catalog).map((cmd) => cmd.id);
|
|
20
|
+
if (catalog.master?.id)
|
|
21
|
+
ids.unshift(catalog.master.id);
|
|
22
|
+
return [...new Set(ids)];
|
|
23
|
+
}
|
|
24
|
+
/** Ids mais parecidos com o que foi digitado, para sugerir em caso de erro. */
|
|
25
|
+
export function suggestIds(input, ids) {
|
|
26
|
+
const needle = input.toLowerCase();
|
|
27
|
+
return ids
|
|
28
|
+
.filter((id) => id.includes(needle) || needle.includes(id) || id.startsWith(needle.slice(0, 4)))
|
|
29
|
+
.slice(0, 5);
|
|
30
|
+
}
|
|
31
|
+
const SYSTEM_NOTE = 'Os slash commands do harness Fontdata estão disponíveis nesta sessão com o prefixo ' +
|
|
32
|
+
'`/PLUGIN:` (ex.: `/PLUGIN:nova-tarefa`). Quando uma instrução referenciar um comando ' +
|
|
33
|
+
'como `/nome`, entenda que se trata de `/PLUGIN:nome`.';
|
|
34
|
+
/**
|
|
35
|
+
* Executa um comando do catálogo abrindo o Claude Code na pasta atual, com o
|
|
36
|
+
* pacote carregado como plugin da sessão — sem instalar nada no projeto.
|
|
37
|
+
*/
|
|
38
|
+
export function runCommand(id, args = [], claudeFlags = []) {
|
|
39
|
+
const catalog = loadCatalog();
|
|
40
|
+
const ids = runnableIds(catalog);
|
|
41
|
+
const wanted = id.replace(/^\//, '');
|
|
42
|
+
if (!ids.includes(wanted)) {
|
|
43
|
+
const near = suggestIds(wanted, ids);
|
|
44
|
+
console.error(`Comando desconhecido: ${wanted}`);
|
|
45
|
+
if (near.length)
|
|
46
|
+
console.error(`\nVocê quis dizer: ${near.join(', ')}`);
|
|
47
|
+
console.error('\nRode `npx @fontdata/ai-harness list` para ver o catálogo completo.');
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const name = pluginName();
|
|
51
|
+
const prompt = [`/${name}:${wanted}`, ...args].join(' ').trim();
|
|
52
|
+
const argv = [
|
|
53
|
+
'--plugin-dir',
|
|
54
|
+
pluginRoot,
|
|
55
|
+
'--append-system-prompt',
|
|
56
|
+
SYSTEM_NOTE.replaceAll('PLUGIN', name),
|
|
57
|
+
...claudeFlags,
|
|
58
|
+
'--',
|
|
59
|
+
prompt,
|
|
60
|
+
];
|
|
61
|
+
const result = spawn(argv);
|
|
62
|
+
const error = result.error;
|
|
63
|
+
if (error?.code === 'ENOENT') {
|
|
64
|
+
console.error('Claude Code não encontrado no PATH.\n' +
|
|
65
|
+
'Instale com: npm install -g @anthropic-ai/claude-code');
|
|
66
|
+
return 127;
|
|
67
|
+
}
|
|
68
|
+
if (error) {
|
|
69
|
+
console.error('Erro ao executar o Claude Code:', error.message);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
return result.status ?? 0;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* No Windows o `claude` costuma ser um `.cmd`, que o spawn sem shell não
|
|
76
|
+
* resolve; nesse caso repetimos a chamada pelo shell, com os argumentos citados.
|
|
77
|
+
*/
|
|
78
|
+
function spawn(argv) {
|
|
79
|
+
const direct = spawnSync('claude', argv, { stdio: 'inherit', cwd: process.cwd() });
|
|
80
|
+
const error = direct.error;
|
|
81
|
+
if (error?.code === 'ENOENT' && process.platform === 'win32') {
|
|
82
|
+
const quoted = argv.map((arg) => `"${String(arg).replaceAll('"', '\\"')}"`).join(' ');
|
|
83
|
+
return spawnSync(`claude ${quoted}`, { stdio: 'inherit', cwd: process.cwd(), shell: true });
|
|
84
|
+
}
|
|
85
|
+
return direct;
|
|
86
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { templatesDir } from '../catalog.js';
|
|
4
|
+
import { hashContent, LOCK_VERSION } from '../install/lock.js';
|
|
5
|
+
/**
|
|
6
|
+
* Rules eram transcritas pelo modelo a partir do bloco do comando e, em vários projetos, passaram depois por um
|
|
7
|
+
* formatador. Por isso o manifesto guarda, para elas, o hash do texto normalizado: sem linhas em branco, sem espaço
|
|
8
|
+
* no fim das linhas e com as tabelas reduzidas ao conteúdo das células.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeText(text) {
|
|
11
|
+
const lines = text
|
|
12
|
+
.replace(/\r\n/g, '\n')
|
|
13
|
+
.split('\n')
|
|
14
|
+
.map((line) => line.replace(/\s+$/, ''))
|
|
15
|
+
.filter((line) => line !== '')
|
|
16
|
+
.map((line) => {
|
|
17
|
+
if (!line.trimStart().startsWith('|'))
|
|
18
|
+
return line;
|
|
19
|
+
const cells = line.split('|').map((cell) => cell.trim());
|
|
20
|
+
return cells.map((cell) => (/^:?-{3,}:?$/.test(cell) ? cell.replace(/-+/, '---') : cell)).join(' | ').trim();
|
|
21
|
+
});
|
|
22
|
+
return `${lines.join('\n')}\n`;
|
|
23
|
+
}
|
|
24
|
+
export function loadLegacyManifest() {
|
|
25
|
+
return JSON.parse(readFileSync(join(templatesDir, 'legado', 'manifesto.json'), 'utf8'));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reconhece uma instalação anterior à 2.1, que não gravava lock, pelos caminhos que as versões
|
|
29
|
+
* publicadas instalavam, e monta o lock que o update compara em três pontas. Arquivo igual a
|
|
30
|
+
* qualquer versão publicada é intacto e entra com o próprio hash; arquivo editado entra com o
|
|
31
|
+
* hash da versão mais recente, para o update enxergar a alteração local. O CLAUDE.md só conta
|
|
32
|
+
* quando é igual a uma versão publicada, porque o nome não é exclusivo do harness.
|
|
33
|
+
*/
|
|
34
|
+
export function adoptLegacyInstall(root, manifest = loadLegacyManifest()) {
|
|
35
|
+
const order = (version) => manifest.versoes.indexOf(version);
|
|
36
|
+
const itens = [];
|
|
37
|
+
let newest = -1;
|
|
38
|
+
for (const item of manifest.itens) {
|
|
39
|
+
const arquivos = [];
|
|
40
|
+
for (const entry of item.arquivos) {
|
|
41
|
+
const local = join(root, entry.caminho);
|
|
42
|
+
if (!existsSync(local))
|
|
43
|
+
continue;
|
|
44
|
+
const content = readFileSync(local);
|
|
45
|
+
const hash = hashContent(content);
|
|
46
|
+
const versions = entry.hashes[hash] ?? (item.tipo === 'rule' ? entry.hashes[hashContent(normalizeText(content.toString('utf8')))] : undefined);
|
|
47
|
+
if (versions) {
|
|
48
|
+
newest = Math.max(newest, ...versions.map(order));
|
|
49
|
+
arquivos.push({ caminho: entry.caminho, sha256: hash });
|
|
50
|
+
}
|
|
51
|
+
else if (item.tipo !== 'claude-md') {
|
|
52
|
+
arquivos.push({ caminho: entry.caminho, sha256: latestHash(entry.hashes, order) });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (arquivos.length)
|
|
56
|
+
itens.push({ tipo: item.tipo, id: item.id, arquivos });
|
|
57
|
+
}
|
|
58
|
+
if (!itens.length)
|
|
59
|
+
return null;
|
|
60
|
+
const versao = manifest.versoes[newest] ?? manifest.versoes.at(-1) ?? '2.0.0';
|
|
61
|
+
return { lock_versao: LOCK_VERSION, pacote: { nome: '@fontdata/ai-harness', versao }, gerado_em: '', itens };
|
|
62
|
+
}
|
|
63
|
+
function latestHash(hashes, order) {
|
|
64
|
+
let latest = '';
|
|
65
|
+
let latestOrder = -Infinity;
|
|
66
|
+
for (const [hash, versions] of Object.entries(hashes)) {
|
|
67
|
+
const top = Math.max(...versions.map(order));
|
|
68
|
+
if (top > latestOrder) {
|
|
69
|
+
latest = hash;
|
|
70
|
+
latestOrder = top;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return latest;
|
|
74
|
+
}
|