product-runner 0.5.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 +165 -0
- package/common/agents/README.md +68 -0
- package/common/agents/agente-conceituacao.md +141 -0
- package/common/agents/agente-documentacao-funcional.md +107 -0
- package/common/agents/agente-gerador-spec.md +106 -0
- package/common/agents/agente-kickoff.md +121 -0
- package/common/agents/agente-prod-runner.md +107 -0
- package/common/agents/agente-review-code.md +97 -0
- package/common/agents/agente-review-llm.md +94 -0
- package/common/agents/agente-review-product.md +98 -0
- package/common/agents/agente-user-review.md +99 -0
- package/common/agents/protocolo-de-gates.md +51 -0
- package/common/claude-md.template.md +210 -0
- package/common/design-principles.md +229 -0
- package/common/lessons-learned.md +440 -0
- package/common/pipeline.md +143 -0
- package/common/spec-guide.md +327 -0
- package/common/specs/_open-issues.md +46 -0
- package/common/specs/_overview.md +75 -0
- package/dist/cli.js +187 -0
- package/dist/migrations.js +147 -0
- package/dist/scaffold.js +276 -0
- package/dist/update.js +400 -0
- package/migrations/0.3.0.md +54 -0
- package/migrations/0.4.0.md +76 -0
- package/migrations/0.5.0.md +55 -0
- package/migrations/README.md +68 -0
- package/package.json +41 -0
- package/profile-cli/README.md +54 -0
- package/profile-cli/claude-md.extension.md +102 -0
- package/profile-cli/code-patterns.md +363 -0
- package/profile-ssr/DESIGN-SYSTEM.md +795 -0
- package/profile-ssr/README.md +51 -0
- package/profile-ssr/api-patterns.md +70 -0
- package/profile-ssr/claude-md.extension.md +113 -0
- package/profile-ssr/code-patterns.md +175 -0
- package/profile-ssr/ui-patterns.md +97 -0
package/dist/update.js
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, access, rm, readdir } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { buildArtifacts, listFiles, sha256, writeManifest, templatesRoot, packageVersion, MANIFEST_FILENAME, } from "./scaffold.js";
|
|
6
|
+
import { discoverMigrations, migrationsInSpan, applyOps, globToRegExp, } from "./migrations.js";
|
|
7
|
+
/** Subpasta (dentro de docs/) onde vão os artefatos de handoff dos conflitos. */
|
|
8
|
+
export const HANDOFF_DIR = ".prod-runner-update";
|
|
9
|
+
/**
|
|
10
|
+
* Nome do manifesto ANTES do rename para `product-runner` (migration 0.5.0).
|
|
11
|
+
* Projetos scaffoldados em versões anteriores têm o manifesto com este nome; o
|
|
12
|
+
* `readManifest` cai pra ele pra ainda achar o cursor — e a migration 0.5.0
|
|
13
|
+
* renomeia o arquivo no disco. Literal de propósito (não derivar do novo nome).
|
|
14
|
+
*/
|
|
15
|
+
const LEGACY_MANIFEST_FILENAME = ".project-docs-blueprints.json";
|
|
16
|
+
/** Diretórios que nunca entram na varredura do projeto. */
|
|
17
|
+
const SKIP_DIRS = new Set([
|
|
18
|
+
"node_modules",
|
|
19
|
+
".git",
|
|
20
|
+
"dist",
|
|
21
|
+
".obsidian",
|
|
22
|
+
HANDOFF_DIR,
|
|
23
|
+
]);
|
|
24
|
+
async function exists(path) {
|
|
25
|
+
try {
|
|
26
|
+
await access(path, constants.F_OK);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// --- Normalização (antes de comparar) -------------------------------------
|
|
34
|
+
//
|
|
35
|
+
// O achado central da recon: sem normalizar, o formatter do projeto faz todo
|
|
36
|
+
// arquivo parecer divergente. Normalizamos AMBOS os lados só pra COMPARAR — o
|
|
37
|
+
// que vai pro disco é sempre o conteúdo bruto do template.
|
|
38
|
+
/** Caminho do Prettier LOCAL do projeto, ou null se não estiver instalado. */
|
|
39
|
+
async function prettierBin(projectDir) {
|
|
40
|
+
const bin = join(projectDir, "node_modules", ".bin", process.platform === "win32" ? "prettier.cmd" : "prettier");
|
|
41
|
+
return (await exists(bin)) ? bin : null;
|
|
42
|
+
}
|
|
43
|
+
/** Roda o Prettier LOCAL do projeto (nunca baixa nada). null se indisponível. */
|
|
44
|
+
function runPrettier(projectDir, filename, content) {
|
|
45
|
+
return new Promise(async (resolve) => {
|
|
46
|
+
const bin = await prettierBin(projectDir);
|
|
47
|
+
if (bin === null) {
|
|
48
|
+
resolve(null);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const child = spawn(bin, ["--stdin-filepath", filename], {
|
|
52
|
+
cwd: projectDir,
|
|
53
|
+
});
|
|
54
|
+
let out = "";
|
|
55
|
+
child.stdout.on("data", (d) => (out += d));
|
|
56
|
+
child.stderr.on("data", () => { });
|
|
57
|
+
child.on("error", () => resolve(null));
|
|
58
|
+
child.on("close", (code) => resolve(code === 0 ? out : null));
|
|
59
|
+
child.stdin.end(content);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Reduz links a só o texto visível, pra diferença de estilo não contar. */
|
|
63
|
+
function stripLinks(md) {
|
|
64
|
+
return md
|
|
65
|
+
.replace(/\[\[([^\]]+)\]\]/g, "$1") // [[wiki]]
|
|
66
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1"); // [texto](url)
|
|
67
|
+
}
|
|
68
|
+
async function normalizeForCompare(content, opts) {
|
|
69
|
+
let s = content.replace(/\r\n/g, "\n");
|
|
70
|
+
if (opts.formatNormalize) {
|
|
71
|
+
const pretty = await runPrettier(opts.projectDir, opts.filename, s);
|
|
72
|
+
if (pretty !== null)
|
|
73
|
+
s = pretty;
|
|
74
|
+
}
|
|
75
|
+
s = stripLinks(s);
|
|
76
|
+
s = s
|
|
77
|
+
.split("\n")
|
|
78
|
+
.map((l) => l.replace(/[ \t]+$/, ""))
|
|
79
|
+
.join("\n")
|
|
80
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
81
|
+
.trim();
|
|
82
|
+
return s + "\n";
|
|
83
|
+
}
|
|
84
|
+
/** Converte links markdown relativos pro estilo wiki `[[alvo]]` do Obsidian. */
|
|
85
|
+
function toWikiLinks(md) {
|
|
86
|
+
return md.replace(/\[([^\]]+)\]\(\.?\/?([^)]+?)(?:\.md)?\)/g, (_m, _text, target) => {
|
|
87
|
+
const base = target.split("/").pop() ?? target;
|
|
88
|
+
return `[[${base}]]`;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// --- Detecção de movidos --------------------------------------------------
|
|
92
|
+
/** basename ignorando o sufixo `.template` (ex.: _overview.template.md → _overview.md). */
|
|
93
|
+
function normBasename(path) {
|
|
94
|
+
const base = path.split("/").pop() ?? path;
|
|
95
|
+
return base.replace(/\.template(\.[^.]+)$/, "$1");
|
|
96
|
+
}
|
|
97
|
+
// --- Classificação --------------------------------------------------------
|
|
98
|
+
async function classify(art, templatePath, projectPath, manifest, opts) {
|
|
99
|
+
// 1. não existe no projeto → adicionar
|
|
100
|
+
if (projectPath === null) {
|
|
101
|
+
return { bucket: "add", reason: "novo no template; não existe no projeto" };
|
|
102
|
+
}
|
|
103
|
+
const projectRaw = await readFile(join(opts.targetDir, projectPath), "utf8");
|
|
104
|
+
const filename = projectPath.split("/").pop() ?? "x.md";
|
|
105
|
+
// 2. com manifesto: comparar hash bruto contra a base registrada
|
|
106
|
+
const base = manifest?.files[templatePath]?.sha256;
|
|
107
|
+
if (base) {
|
|
108
|
+
const untouched = sha256(projectRaw) === base;
|
|
109
|
+
const templateChanged = sha256(art.content) !== base;
|
|
110
|
+
if (untouched) {
|
|
111
|
+
return templateChanged
|
|
112
|
+
? { bucket: "automerge", reason: "você não editou; template evoluiu" }
|
|
113
|
+
: { bucket: "uptodate", reason: "em dia (nada mudou)" };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// 3. iguais após normalizar → no-op (pega diff só de formatação/links)
|
|
117
|
+
const [np, nn] = await Promise.all([
|
|
118
|
+
normalizeForCompare(projectRaw, {
|
|
119
|
+
projectDir: opts.targetDir,
|
|
120
|
+
filename,
|
|
121
|
+
formatNormalize: opts.formatNormalize,
|
|
122
|
+
}),
|
|
123
|
+
normalizeForCompare(art.content, {
|
|
124
|
+
projectDir: opts.targetDir,
|
|
125
|
+
filename,
|
|
126
|
+
formatNormalize: opts.formatNormalize,
|
|
127
|
+
}),
|
|
128
|
+
]);
|
|
129
|
+
if (np === nn) {
|
|
130
|
+
return { bucket: "uptodate", reason: "idêntico após normalizar formatação" };
|
|
131
|
+
}
|
|
132
|
+
// 4. divergência real → decisão humana (gera handoff)
|
|
133
|
+
return {
|
|
134
|
+
bucket: "review",
|
|
135
|
+
reason: base
|
|
136
|
+
? "você editou e o template também mudou"
|
|
137
|
+
: "sem manifesto; divergência precisa de classificação",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
// --- Plano + aplicação ----------------------------------------------------
|
|
141
|
+
async function readManifest(targetDir) {
|
|
142
|
+
// nome atual primeiro; cai pro legado pra ainda achar o cursor de projetos
|
|
143
|
+
// pré-rename (a migration 0.5.0 renomeia o arquivo no disco no mesmo update).
|
|
144
|
+
for (const name of [MANIFEST_FILENAME, LEGACY_MANIFEST_FILENAME]) {
|
|
145
|
+
const p = join(targetDir, "docs", name);
|
|
146
|
+
if (!(await exists(p)))
|
|
147
|
+
continue;
|
|
148
|
+
try {
|
|
149
|
+
return JSON.parse(await readFile(p, "utf8"));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
function handoffContent(item, projectRaw) {
|
|
158
|
+
return `# Handoff de update — ${item.templatePath}
|
|
159
|
+
|
|
160
|
+
> Gerado pelo \`product-runner update\`. O CLI **não** alterou o arquivo
|
|
161
|
+
> original; isto é material pra você (ou uma sessão Claude) classificar e decidir.
|
|
162
|
+
|
|
163
|
+
- **Arquivo no projeto:** \`${item.projectPath}\`${item.moved ? " (movido em relação ao template)" : ""}
|
|
164
|
+
- **Origem no template:** \`${item.art.fromTemplate}\`
|
|
165
|
+
- **Motivo:** ${item.reason}
|
|
166
|
+
|
|
167
|
+
## Tarefa
|
|
168
|
+
|
|
169
|
+
Classifique cada diferença entre as duas versões abaixo como **melhoria do
|
|
170
|
+
template** (trazer) ou **customização do projeto** (preservar), e produza a
|
|
171
|
+
versão final mesclada. Em caso de conflito real no mesmo trecho, explique o
|
|
172
|
+
tradeoff em vez de escolher sozinho.
|
|
173
|
+
|
|
174
|
+
## Versão ATUAL (no projeto)
|
|
175
|
+
|
|
176
|
+
\`\`\`\`\`markdown
|
|
177
|
+
${projectRaw.trimEnd()}
|
|
178
|
+
\`\`\`\`\`
|
|
179
|
+
|
|
180
|
+
## Versão NOVA (template)
|
|
181
|
+
|
|
182
|
+
\`\`\`\`\`markdown
|
|
183
|
+
${item.art.content.trimEnd()}
|
|
184
|
+
\`\`\`\`\`
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
function migrationHandoff(mig) {
|
|
188
|
+
return `# Migration ${mig.version} — ${mig.title}
|
|
189
|
+
|
|
190
|
+
> Migration **conduzida** (não-automática) do \`product-runner\`. O CLI
|
|
191
|
+
> não aplicou nada; conduza as instruções abaixo com o humano.
|
|
192
|
+
${mig.previous ? `\n- **De:** ${mig.previous} → **${mig.version}**` : ""}
|
|
193
|
+
${mig.risk ? `- **Risco:** ${mig.risk}` : ""}
|
|
194
|
+
${mig.affects.length ? `- **Afeta:** ${mig.affects.join(", ")}` : ""}
|
|
195
|
+
|
|
196
|
+
${mig.body}
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Calcula o plano de update e, se !dryRun, aplica: escreve ADD + AUTO-MERGE
|
|
201
|
+
* (ADD primeiro, pra refs resolverem), gera handoff dos REVIEW sem tocar o
|
|
202
|
+
* original, e reescreve o manifesto com a nova base.
|
|
203
|
+
*/
|
|
204
|
+
export async function update(opts) {
|
|
205
|
+
const manifest = await readManifest(opts.targetDir);
|
|
206
|
+
const mode = manifest ? "3way" : "legacy";
|
|
207
|
+
const profile = opts.profile ?? manifest?.profile;
|
|
208
|
+
if (!profile) {
|
|
209
|
+
throw new Error("Sem manifesto e sem --profile. Informe --profile <cli|ssr> pra um projeto legado.");
|
|
210
|
+
}
|
|
211
|
+
// nome/porta: do manifesto, ou inferidos (só afetam o CLAUDE.md, que é review)
|
|
212
|
+
let name = manifest?.projectName;
|
|
213
|
+
let port = manifest?.port ?? "3000";
|
|
214
|
+
if (!name) {
|
|
215
|
+
const claudePath = join(opts.targetDir, "CLAUDE.md");
|
|
216
|
+
if (await exists(claudePath)) {
|
|
217
|
+
const m = (await readFile(claudePath, "utf8")).match(/^#\s+(.+)$/m);
|
|
218
|
+
name = m?.[1]?.trim();
|
|
219
|
+
}
|
|
220
|
+
name = name ?? "projeto";
|
|
221
|
+
}
|
|
222
|
+
const meta = { name, profile, port };
|
|
223
|
+
// Normalização por Prettier só roda se pedida E o binário existir no projeto.
|
|
224
|
+
// Se foi pedida mas não há binário, degradamos — e sinalizamos no resultado
|
|
225
|
+
// (senão arquivos que só diferem por formatação caem em REVISAR sem aviso).
|
|
226
|
+
const prettierFound = (await prettierBin(opts.targetDir)) !== null;
|
|
227
|
+
const formatActive = opts.formatNormalize && prettierFound;
|
|
228
|
+
const effectiveOpts = { ...opts, formatNormalize: formatActive };
|
|
229
|
+
const handoffDir = join(opts.targetDir, "docs", HANDOFF_DIR);
|
|
230
|
+
// Migrations: só com manifesto (cursor conhecido). Intervalo
|
|
231
|
+
// (manifesto.version, versão-do-pacote]. Rodam ANTES do diff de estado.
|
|
232
|
+
const pkgVersion = await packageVersion(templatesRoot());
|
|
233
|
+
const migrations = manifest
|
|
234
|
+
? migrationsInSpan(await discoverMigrations(join(templatesRoot(), "migrations")), manifest.version, pkgVersion)
|
|
235
|
+
: [];
|
|
236
|
+
if (!opts.dryRun && migrations.length) {
|
|
237
|
+
// 1. ops mecânicos das migrations autoApply, em ordem (mutam o projeto)
|
|
238
|
+
for (const mig of migrations) {
|
|
239
|
+
if (mig.autoApply && mig.ops.length) {
|
|
240
|
+
await applyOps(opts.targetDir, mig.ops, await listProjectFiles(opts.targetDir));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// 2. corpo conduzido → handoff (mesmo em autoApply: parte mecânica roda
|
|
244
|
+
// sozinha, mas o que precisa de decisão humana fica em MIGRATION-<v>.md)
|
|
245
|
+
const withBody = migrations.filter((m) => m.body.trim().length > 0);
|
|
246
|
+
if (withBody.length) {
|
|
247
|
+
await mkdir(handoffDir, { recursive: true });
|
|
248
|
+
for (const mig of withBody) {
|
|
249
|
+
await writeFile(join(handoffDir, `MIGRATION-${mig.version}.md`), migrationHandoff(mig), "utf8");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const artifacts = await buildArtifacts(meta);
|
|
254
|
+
// índice de arquivos do projeto por basename normalizado (pra detectar movidos)
|
|
255
|
+
// (já reflete o resultado das migrations autoApply, aplicadas acima)
|
|
256
|
+
const projectFiles = await listProjectFiles(opts.targetDir);
|
|
257
|
+
const byBasename = new Map();
|
|
258
|
+
for (const p of projectFiles) {
|
|
259
|
+
const key = normBasename(p);
|
|
260
|
+
const list = byBasename.get(key) ?? [];
|
|
261
|
+
list.push(p);
|
|
262
|
+
byBasename.set(key, list);
|
|
263
|
+
}
|
|
264
|
+
const onlyRe = opts.only ? globToRegExp(opts.only) : null;
|
|
265
|
+
const templatePaths = new Set(artifacts.keys());
|
|
266
|
+
const plan = [];
|
|
267
|
+
for (const [templatePath, art] of artifacts) {
|
|
268
|
+
// resolve onde (e se) o arquivo existe no projeto
|
|
269
|
+
let projectPath = null;
|
|
270
|
+
let moved = false;
|
|
271
|
+
if (await exists(join(opts.targetDir, templatePath))) {
|
|
272
|
+
projectPath = templatePath;
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// candidatos a "movido": mesmo basename, mas NÃO um arquivo que já é
|
|
276
|
+
// emitido pelo template no próprio lugar dele (evita casar docs/README.md
|
|
277
|
+
// com docs/agents/README.md, p.ex.).
|
|
278
|
+
const candidates = (byBasename.get(normBasename(templatePath)) ?? []).filter((p) => !templatePaths.has(p));
|
|
279
|
+
if (candidates.length === 1) {
|
|
280
|
+
projectPath = candidates[0];
|
|
281
|
+
moved = projectPath !== templatePath;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (onlyRe && !onlyRe.test(templatePath) && !(projectPath && onlyRe.test(projectPath))) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const { bucket, reason } = await classify(art, templatePath, projectPath, manifest, effectiveOpts);
|
|
288
|
+
plan.push({
|
|
289
|
+
templatePath,
|
|
290
|
+
projectPath: projectPath ?? templatePath,
|
|
291
|
+
bucket,
|
|
292
|
+
reason,
|
|
293
|
+
moved,
|
|
294
|
+
art,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
const counts = {
|
|
298
|
+
add: 0,
|
|
299
|
+
automerge: 0,
|
|
300
|
+
uptodate: 0,
|
|
301
|
+
review: 0,
|
|
302
|
+
};
|
|
303
|
+
for (const it of plan)
|
|
304
|
+
counts[it.bucket]++;
|
|
305
|
+
if (!opts.dryRun) {
|
|
306
|
+
const projectUsesWiki = await detectWikiLinks(opts.targetDir, projectFiles);
|
|
307
|
+
// ADD primeiro (pra refs de arquivos novos resolverem nos merges)
|
|
308
|
+
for (const it of plan.filter((i) => i.bucket === "add")) {
|
|
309
|
+
let content = it.art.content;
|
|
310
|
+
if (opts.normalizeLinks && projectUsesWiki)
|
|
311
|
+
content = toWikiLinks(content);
|
|
312
|
+
const dest = join(opts.targetDir, ...it.templatePath.split("/"));
|
|
313
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
314
|
+
await writeFile(dest, content, "utf8");
|
|
315
|
+
}
|
|
316
|
+
// AUTO-MERGE: substitui no lugar atual (inclusive se movido)
|
|
317
|
+
for (const it of plan.filter((i) => i.bucket === "automerge")) {
|
|
318
|
+
let content = it.art.content;
|
|
319
|
+
if (opts.normalizeLinks && projectUsesWiki)
|
|
320
|
+
content = toWikiLinks(content);
|
|
321
|
+
const dest = join(opts.targetDir, ...it.projectPath.split("/"));
|
|
322
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
323
|
+
await writeFile(dest, content, "utf8");
|
|
324
|
+
}
|
|
325
|
+
// REVIEW: gera handoff, nunca toca o original
|
|
326
|
+
const reviews = plan.filter((i) => i.bucket === "review");
|
|
327
|
+
if (reviews.length) {
|
|
328
|
+
await mkdir(handoffDir, { recursive: true });
|
|
329
|
+
// limpa só handoffs antigos; preserva o resto (ex.: marcador .last-check)
|
|
330
|
+
for (const f of await readdir(handoffDir)) {
|
|
331
|
+
if (f.endsWith(".handoff.md")) {
|
|
332
|
+
await rm(join(handoffDir, f), { force: true });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const it of reviews) {
|
|
336
|
+
const projectRaw = await readFile(join(opts.targetDir, it.projectPath), "utf8");
|
|
337
|
+
const flat = it.templatePath.replace(/\//g, "__");
|
|
338
|
+
await writeFile(join(handoffDir, `${flat}.handoff.md`), handoffContent(it, projectRaw), "utf8");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// reescreve o manifesto: base passa a ser o template atual (vira 3-way no próximo)
|
|
342
|
+
await writeManifest(join(opts.targetDir, "docs"), meta, artifacts);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
mode,
|
|
346
|
+
plan,
|
|
347
|
+
applied: !opts.dryRun,
|
|
348
|
+
handoffDir,
|
|
349
|
+
counts,
|
|
350
|
+
formatActive,
|
|
351
|
+
prettierFound,
|
|
352
|
+
migrations,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/** Lista arquivos do projeto (POSIX, relativos a targetDir), pulando SKIP_DIRS. */
|
|
356
|
+
async function listProjectFiles(targetDir) {
|
|
357
|
+
const all = await listFiles(targetDir);
|
|
358
|
+
return all.filter((p) => !p.split("/").some((seg) => SKIP_DIRS.has(seg)));
|
|
359
|
+
}
|
|
360
|
+
/** Heurística: o projeto usa predominantemente wiki-links `[[x]]`? */
|
|
361
|
+
async function detectWikiLinks(targetDir, projectFiles) {
|
|
362
|
+
let wiki = 0;
|
|
363
|
+
for (const p of projectFiles.filter((f) => f.endsWith(".md")).slice(0, 50)) {
|
|
364
|
+
const content = await readFile(join(targetDir, p), "utf8");
|
|
365
|
+
wiki += (content.match(/\[\[[^\]]+\]\]/g) ?? []).length;
|
|
366
|
+
}
|
|
367
|
+
return wiki >= 3;
|
|
368
|
+
}
|
|
369
|
+
const BUCKET_LABEL = {
|
|
370
|
+
add: "➕ ADICIONA",
|
|
371
|
+
automerge: "✅ AUTO-MERGE",
|
|
372
|
+
uptodate: "✓ EM DIA",
|
|
373
|
+
review: "⚠️ REVISAR",
|
|
374
|
+
};
|
|
375
|
+
/** Renderiza o plano pra impressão no terminal. */
|
|
376
|
+
export function renderPlan(result) {
|
|
377
|
+
const lines = [];
|
|
378
|
+
if (result.migrations.length) {
|
|
379
|
+
lines.push(`🧭 MIGRATIONS no caminho (${result.migrations.length}) — em ordem, antes do diff:`);
|
|
380
|
+
for (const mig of result.migrations) {
|
|
381
|
+
const mode = mig.autoApply ? "automático" : "conduzir";
|
|
382
|
+
const risk = mig.risk ? ` · risco ${mig.risk}` : "";
|
|
383
|
+
lines.push(` ${mig.version} ${mig.title} [${mode}${risk}]`);
|
|
384
|
+
}
|
|
385
|
+
lines.push("");
|
|
386
|
+
}
|
|
387
|
+
const order = ["add", "automerge", "review", "uptodate"];
|
|
388
|
+
for (const bucket of order) {
|
|
389
|
+
const items = result.plan.filter((i) => i.bucket === bucket);
|
|
390
|
+
if (!items.length)
|
|
391
|
+
continue;
|
|
392
|
+
lines.push(`${BUCKET_LABEL[bucket]} (${items.length})`);
|
|
393
|
+
for (const it of items) {
|
|
394
|
+
const path = it.moved ? `${it.projectPath} ← ${it.templatePath}` : it.templatePath;
|
|
395
|
+
lines.push(` ${path} — ${it.reason}`);
|
|
396
|
+
}
|
|
397
|
+
lines.push("");
|
|
398
|
+
}
|
|
399
|
+
return lines.join("\n").trimEnd();
|
|
400
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
{
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"previous": "0.2.3",
|
|
5
|
+
"title": "agente-prod-runner como entrada; _overview/_open-issues vão pra specs/ sem .template",
|
|
6
|
+
"risk": "low",
|
|
7
|
+
"autoApply": true,
|
|
8
|
+
"affects": [
|
|
9
|
+
"docs/_overview.template.md -> specs/_overview.md",
|
|
10
|
+
"docs/_open-issues.template.md -> specs/_open-issues.md",
|
|
11
|
+
"CLAUDE.md (seção Manutenção -> agente-prod-runner)"
|
|
12
|
+
],
|
|
13
|
+
"ops": [
|
|
14
|
+
{ "type": "rename", "from": "docs/_overview.template.md", "to": "specs/_overview.md" },
|
|
15
|
+
{ "type": "rename", "from": "docs/_open-issues.template.md", "to": "specs/_open-issues.md" },
|
|
16
|
+
{ "type": "replace", "glob": "**/*.md", "find": "_overview\\.template", "replace": "_overview" },
|
|
17
|
+
{ "type": "replace", "glob": "**/*.md", "find": "_open-issues\\.template", "replace": "_open-issues" }
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## O que mudou na 0.3.0
|
|
23
|
+
|
|
24
|
+
Dois movimentos:
|
|
25
|
+
|
|
26
|
+
1. **`_overview` / `_open-issues` saem de `docs/` (com sufixo `.template`) e viram
|
|
27
|
+
seeds em `specs/`** — `specs/_overview.md` e `specs/_open-issues.md`. É onde os
|
|
28
|
+
projetos reais já os colocavam; o `pipeline.md` sempre tratou specs como
|
|
29
|
+
habitantes de `specs/`. Os `ops` mecânicos acima fazem o rename e dropam o
|
|
30
|
+
sufixo `.template` das referências.
|
|
31
|
+
|
|
32
|
+
2. **A porta de entrada agora é o `agente-prod-runner`** (roteador + ciclo de vida), e a
|
|
33
|
+
rotina de manutenção saiu do `CLAUDE.md` pro `docs/agents/agente-prod-runner.md`. O
|
|
34
|
+
`update` adiciona `agente-prod-runner.md` (e `agente-kickoff.md`, se faltar) em
|
|
35
|
+
`docs/agents/` automaticamente.
|
|
36
|
+
|
|
37
|
+
## Conduza com o humano (parte não-mecânica)
|
|
38
|
+
|
|
39
|
+
O `CLAUDE.md` é customizado, então o `update` o marca como REVISAR — não é tocado
|
|
40
|
+
automaticamente. Ao reconciliar o handoff do `CLAUDE.md`:
|
|
41
|
+
|
|
42
|
+
- **Substitua a seção `## Manutenção dos protocolos de doc` antiga** (a que trazia
|
|
43
|
+
a rotina completa de verificação/`update`) **pelo gatilho fino** da versão nova,
|
|
44
|
+
que apenas aponta para `docs/agents/agente-prod-runner.md`. **Preserve** todo o resto do
|
|
45
|
+
seu `CLAUDE.md` (stack, arquitetura, design system, tabela de estado).
|
|
46
|
+
- Se o seu `CLAUDE.md` ainda **não** tinha a seção Manutenção (projeto bem antigo),
|
|
47
|
+
apenas **adicione** o gatilho fino.
|
|
48
|
+
|
|
49
|
+
Depois, confira:
|
|
50
|
+
|
|
51
|
+
- `specs/_overview.md` e `specs/_open-issues.md` existem e mantêm o conteúdo que
|
|
52
|
+
você havia preenchido (o rename preserva o conteúdo; só muda o caminho/nome).
|
|
53
|
+
- Nenhum link aponta mais para `*_overview.template` / `*_open-issues.template`.
|
|
54
|
+
Se sobrou algum link com caminho `docs/_overview…`, ajuste para `specs/_overview.md`.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
---
|
|
2
|
+
{
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"previous": "0.3.0",
|
|
5
|
+
"title": "status content-derived + fluxos de review como estágio nomeado",
|
|
6
|
+
"risk": "low",
|
|
7
|
+
"autoApply": false,
|
|
8
|
+
"affects": [
|
|
9
|
+
"docs/pipeline.md (estágio 5 + seção 'Em que estágio estou?')",
|
|
10
|
+
"docs/agents/README.md (fluxos de review na tabela/diagrama)",
|
|
11
|
+
"docs/lessons-learned.md (nova lição)",
|
|
12
|
+
"CLAUDE.md (subseção 'Em que etapa o projeto está' + caveat na tabela de estado)",
|
|
13
|
+
"specs/_overview.md (caveat 'índice derivado')"
|
|
14
|
+
],
|
|
15
|
+
"ops": []
|
|
16
|
+
}
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## O que mudou na 0.4.0
|
|
20
|
+
|
|
21
|
+
Um eixo só: **status do projeto se descobre pelo conteúdo das specs, não pela
|
|
22
|
+
tabela de índice** — e os **fluxos de review** (Estágio 5) passam a ser estágio
|
|
23
|
+
nomeado e verificável, não um passo solto.
|
|
24
|
+
|
|
25
|
+
Motivação concreta: numa sessão de orientação, ao responder "em que etapa
|
|
26
|
+
estamos / qual o próximo passo", o agente respondeu a partir das tabelas de
|
|
27
|
+
status (`_overview`, tabela de estado do `CLAUDE.md`) — lidas truncadas — sem
|
|
28
|
+
abrir nenhuma spec. Resultado: quase repassou a divergência do índice (que
|
|
29
|
+
driftou) como verdade e confundiu "Decisões de implementação preenchidas"
|
|
30
|
+
(rastro do **estágio 4**, por M1) com "review feito" (**estágio 5**, que tem
|
|
31
|
+
rastro próprio). A correção torna o procedimento explícito.
|
|
32
|
+
|
|
33
|
+
Mudanças por arquivo:
|
|
34
|
+
|
|
35
|
+
1. **`docs/pipeline.md`** — o diagrama expande o Estágio 5 na sub-cadeia
|
|
36
|
+
**Review.Code → User Review → Review.Product → Review.LLM** (com escala de
|
|
37
|
+
Impeditivo); §5 deixa de ser "fase futura"; nova seção **"Em que estágio
|
|
38
|
+
estou?"** com a tabela de rastros e a regra de fonte (a spec é fonte;
|
|
39
|
+
`_overview`/tabela de estado são índice derivado).
|
|
40
|
+
2. **`docs/agents/README.md`** — os quatro `agente-review-*` entram na tabela,
|
|
41
|
+
no diagrama e na prosa (antes não apareciam como estágio).
|
|
42
|
+
3. **`docs/lessons-learned.md`** — nova lição "Status se lê do conteúdo, não do
|
|
43
|
+
índice".
|
|
44
|
+
|
|
45
|
+
Os três acima são arquivos de template **não-customizados** — o diff de estado
|
|
46
|
+
do `update` os reconcilia sozinho. Nada a conduzir neles.
|
|
47
|
+
|
|
48
|
+
## Conduza com o humano (parte não-mecânica)
|
|
49
|
+
|
|
50
|
+
Os dois arquivos abaixo são **customizados** no seu projeto, então o `update` os
|
|
51
|
+
marca como REVISAR e **não** os toca automaticamente. A mudança aqui é
|
|
52
|
+
**aditiva** — preserve todo o seu conteúdo e apenas inclua o que falta:
|
|
53
|
+
|
|
54
|
+
- **`CLAUDE.md`:**
|
|
55
|
+
- No item do `pipeline` em "Docs de referência", **complete a cadeia** com o
|
|
56
|
+
estágio de review: `… → implementação → review (Review.Code → User Review →
|
|
57
|
+
Review.Product → Review.LLM)`.
|
|
58
|
+
- Na seção **"Estado do refactor / desenvolvimento"**, adicione o caveat
|
|
59
|
+
**"Índice derivado, não fonte"** acima da tabela (a tabela e o `_overview`
|
|
60
|
+
são conveniência que drift-a; a fonte é o conteúdo da spec).
|
|
61
|
+
- Adicione a subseção **"Em que etapa o projeto está"** (descobrir o estágio
|
|
62
|
+
pelo rastro no conteúdo da spec; cuidado para não confundir "Decisões de
|
|
63
|
+
implementação" = estágio 4 com review = estágio 5; nunca responder status de
|
|
64
|
+
leitura truncada `head -N`). Veja o texto de referência em
|
|
65
|
+
`common/claude-md.template.md` do pacote.
|
|
66
|
+
|
|
67
|
+
- **`specs/_overview.md`:** adicione no header o caveat **"Índice derivado, não
|
|
68
|
+
fonte"** — não responder "qual a próxima etapa" a partir desta tabela,
|
|
69
|
+
confirmar na spec. Mesmo princípio LDoc→HDoc.
|
|
70
|
+
|
|
71
|
+
Depois, confira:
|
|
72
|
+
|
|
73
|
+
- `docs/pipeline.md` tem a seção "Em que estágio estou?" e o Estágio 5 com a
|
|
74
|
+
sub-cadeia de review.
|
|
75
|
+
- O `CLAUDE.md` aponta para essa seção e a tabela de estado está marcada como
|
|
76
|
+
índice derivado.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
{
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"previous": "0.4.0",
|
|
5
|
+
"title": "rename: project-docs-blueprints -> product-runner (e pdb -> prod-runner)",
|
|
6
|
+
"risk": "low",
|
|
7
|
+
"autoApply": true,
|
|
8
|
+
"affects": [
|
|
9
|
+
"docs/.project-docs-blueprints.json -> docs/.product-runner.json",
|
|
10
|
+
"docs/agents/agente-pdb.md -> docs/agents/agente-prod-runner.md",
|
|
11
|
+
"docs/.pdb-update/ -> docs/.prod-runner-update/ (efêmero, regenera)",
|
|
12
|
+
"CLAUDE.md / docs customizados (referências a `npx project-docs-blueprints`, `agente-pdb`, `.pdb-update`)"
|
|
13
|
+
],
|
|
14
|
+
"ops": [
|
|
15
|
+
{ "type": "rename", "from": "docs/.project-docs-blueprints.json", "to": "docs/.product-runner.json" },
|
|
16
|
+
{ "type": "rename", "from": "docs/agents/agente-pdb.md", "to": "docs/agents/agente-prod-runner.md" }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## O que mudou na 0.5.0
|
|
22
|
+
|
|
23
|
+
O pacote foi renomeado de **`project-docs-blueprints`** para **`product-runner`**,
|
|
24
|
+
e a abreviação **`pdb`** virou **`prod-runner`**:
|
|
25
|
+
|
|
26
|
+
- `agente-pdb.md` → `agente-prod-runner.md` (a porta de entrada / roteador)
|
|
27
|
+
- `.pdb-update/` → `.prod-runner-update/` (pasta efêmera de handoffs)
|
|
28
|
+
- diretivas `pdb-merge` → `prod-runner-merge` (internas ao pacote; não vão pra projetos)
|
|
29
|
+
|
|
30
|
+
Mudança de identidade/marca — nada de comportamento.
|
|
31
|
+
|
|
32
|
+
As partes mecânicas são aplicadas sozinhas pelos `ops`: o manifesto
|
|
33
|
+
`docs/.project-docs-blueprints.json` → `docs/.product-runner.json` e o agente
|
|
34
|
+
`docs/agents/agente-pdb.md` → `docs/agents/agente-prod-runner.md`. O `update`
|
|
35
|
+
ainda tem um **fallback** que lê o manifesto no nome antigo se o novo não
|
|
36
|
+
existir, então rodar o `update` de um projeto pré-rename funciona.
|
|
37
|
+
|
|
38
|
+
A pasta `docs/.pdb-update/` é efêmera e gitignored — **não** é renomeada por op;
|
|
39
|
+
ela simplesmente regenera como `docs/.prod-runner-update/` no próximo handoff.
|
|
40
|
+
Pode apagar a antiga.
|
|
41
|
+
|
|
42
|
+
## Conduza com o humano (parte não-mecânica)
|
|
43
|
+
|
|
44
|
+
Em arquivos **customizados** (que o `update` não toca), troque referências ao
|
|
45
|
+
nome antigo:
|
|
46
|
+
|
|
47
|
+
- `CLAUDE.md` — `npx project-docs-blueprints …` → `npx product-runner …`; o
|
|
48
|
+
ponteiro de manutenção `agente-pdb` → `agente-prod-runner`; o aviso de
|
|
49
|
+
`.gitignore` de `docs/.pdb-update/` → `docs/.prod-runner-update/`.
|
|
50
|
+
- `.gitignore` do projeto — troque a linha `docs/.pdb-update/` por
|
|
51
|
+
`docs/.prod-runner-update/`.
|
|
52
|
+
- Qualquer script/CI/README seu que invoque o pacote ou cite `agente-pdb`.
|
|
53
|
+
|
|
54
|
+
Busca rápida: `grep -rni "project-docs-blueprints\|pdb" .` — deve sobrar só
|
|
55
|
+
histórico (CHANGELOG/migrations).
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Migrations
|
|
2
|
+
|
|
3
|
+
Notas de migração entre versões dos templates. **Opcionais por versão** — a
|
|
4
|
+
maioria dos bumps (um arquivo novo, formatação, pequena adição) é resolvida pelo
|
|
5
|
+
diff de estado do `update` e **não** precisa de migration. Uma migration só
|
|
6
|
+
existe quando há mudança que o diff não expressa ou não deve aplicar sozinho:
|
|
7
|
+
rename/split de arquivos, mudança de convenção, ou transformação acoplada a
|
|
8
|
+
código.
|
|
9
|
+
|
|
10
|
+
## Como o `update` usa
|
|
11
|
+
|
|
12
|
+
1. Lê o `version` do manifesto do projeto (o "cursor").
|
|
13
|
+
2. Junta as migrations no intervalo `(cursor, versão-do-pacote]`, em ordem.
|
|
14
|
+
3. Roda-as **antes** do diff de estado: as `autoApply` aplicam seus `ops`
|
|
15
|
+
mecânicos; as demais viram handoff (`docs/.prod-runner-update/MIGRATION-<v>.md`) pra
|
|
16
|
+
conduzir com o humano.
|
|
17
|
+
4. Depois o diff de estado reconcilia o que sobrou.
|
|
18
|
+
|
|
19
|
+
## Formato
|
|
20
|
+
|
|
21
|
+
Um arquivo por versão: `migrations/<x.y.z>.md`. Frontmatter **JSON** entre `---`
|
|
22
|
+
(o pacote é zero-dependência; JSON.parse é robusto), seguido do corpo em
|
|
23
|
+
markdown com as instruções conduzidas.
|
|
24
|
+
|
|
25
|
+
```md
|
|
26
|
+
---
|
|
27
|
+
{
|
|
28
|
+
"version": "0.3.0",
|
|
29
|
+
"previous": "0.2.3",
|
|
30
|
+
"title": "Resumo curto da migração",
|
|
31
|
+
"risk": "high",
|
|
32
|
+
"autoApply": false,
|
|
33
|
+
"affects": ["docs/DESIGN-SYSTEM.md"],
|
|
34
|
+
"ops": []
|
|
35
|
+
}
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## O que mudou
|
|
39
|
+
|
|
40
|
+
Prosa explicando a mudança e, se `autoApply: false`, como conduzir a decisão
|
|
41
|
+
com o humano (o que trazer, o que preservar, qual o tradeoff).
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Campos
|
|
45
|
+
|
|
46
|
+
| Campo | Significado |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `version` | versão que esta migration leva o projeto a alcançar (= nome do arquivo) |
|
|
49
|
+
| `previous` | versão anterior (informativo) |
|
|
50
|
+
| `title` | resumo de uma linha |
|
|
51
|
+
| `risk` | `low` ou `high` |
|
|
52
|
+
| `autoApply` | `true` = o CLI aplica os `ops` sozinho; `false` = só apresenta, a LLM conduz |
|
|
53
|
+
| `affects` | lista informativa de arquivos/áreas afetados |
|
|
54
|
+
| `ops` | passos mecânicos (só executados se `autoApply: true`) |
|
|
55
|
+
|
|
56
|
+
### Ops mecânicos
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{ "type": "rename", "from": "docs/a.md", "to": "docs/b.md" }
|
|
60
|
+
{ "type": "replace", "glob": "docs/**/*.md", "find": "regex", "replace": "texto" }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- `rename`: move o arquivo (no-op se a origem não existir — ex.: já renomeado).
|
|
64
|
+
- `replace`: regex global nos arquivos que casam o `glob` (`*` num segmento,
|
|
65
|
+
`**` atravessa).
|
|
66
|
+
|
|
67
|
+
> Mudança acoplada a código (ex.: migração de tokens primitivos → semânticos)
|
|
68
|
+
> **não** deve ser `autoApply` — descreva em prosa e deixe virar spec/issue.
|