@seedgrid/ai-toolkit 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -17,10 +17,11 @@ Crie uma ADR quando a decisão: muda um comportamento observável; cria, remove
|
|
|
17
17
|
|
|
18
18
|
1. **Listar as decisões.** Escreva cada uma em uma frase e **mostre ao usuário** para confirmar: você propõe, quem decide é o usuário. Ao registrar, repita a decisão em uma frase (decisão registrada errada só aparece quando alguém relê).
|
|
19
19
|
2. **Domínio.** Escolha na lista `docs/ai/decisions/dominios.txt` (um domínio por linha). Se o arquivo não existir, proponha 5 a 10 domínios do projeto (por exemplo `seguranca`, `dados`, `api`, `configuracao`, `processo`), confirme e crie. Domínio novo só com confirmação do usuário.
|
|
20
|
-
3. **
|
|
21
|
-
4. **
|
|
22
|
-
5.
|
|
23
|
-
6.
|
|
20
|
+
3. **Número.** `node .ai/skills/sg-adr/scripts/adr-index.mjs --proximo` dá o próximo número livre; ele é sequencial, vai em `numero:` e **nunca muda** (dá para conversar sobre "a ADR #7"). Para ADRs antigas sem número, `--numerar` numera só as que não têm.
|
|
21
|
+
4. **Criar o arquivo** `docs/ai/decisions/AAAA-MM-DD-slug.md` a partir do modelo `.ai/templates/decisao-arquitetura.md`: cabeçalho completo (`titulo`, `numero`, `data`, `status`, `decidido-por`, `consultados`, `dominio`, `modulos`, `origem`) e as seções Contexto, Decisão, Alternativas e Consequências. Comece com `status: proposta` e mude para `aceita` quando o usuário confirmar.
|
|
22
|
+
5. **Substituição.** Se a decisão troca outra, não edite a antiga: crie a nova com `substitui: [arquivo-antigo.md]` e apenas marque a antiga como `status: substituida` e `substituida-por`.
|
|
23
|
+
6. **Índice.** Rode `node .ai/skills/sg-adr/scripts/adr-index.mjs`. Ele valida o cabeçalho e gera `docs/ai/decisions/INDEX.md` em ordem decrescente de data. Nunca edite o `INDEX.md` à mão; use `--check` para conferir se está em dia.
|
|
24
|
+
7. **Entregar** listando as ADRs criadas e o que ficou como `proposta` (o que o dono confirmou e já está no código é `implementada`).
|
|
24
25
|
|
|
25
26
|
## Consultar
|
|
26
27
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Gera docs/ai/decisions/INDEX.md a partir do cabeçalho das ADRs, da mais nova para a mais antiga.
|
|
3
3
|
// Uso: node .ai/skills/sg-adr/scripts/adr-index.mjs [--dir docs/ai/decisions] [--check]
|
|
4
|
-
// --check
|
|
4
|
+
// --check só valida e falha se o INDEX.md estiver desatualizado (sem escrever)
|
|
5
|
+
// --proximo imprime o próximo número livre para uma ADR nova
|
|
6
|
+
// --numerar dá número, em ordem de data e nome, SÓ às ADRs sem "numero"; nunca renumera
|
|
5
7
|
// Domínios: se existir <dir>/dominios.txt (um por linha), só esses são aceitos; senão, qualquer slug em minúsculas com hífen.
|
|
6
8
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
7
9
|
import path from 'node:path';
|
|
@@ -10,7 +12,7 @@ const args = process.argv.slice(2);
|
|
|
10
12
|
const di = args.indexOf('--dir');
|
|
11
13
|
const dir = path.resolve(di >= 0 && args[di + 1] ? args[di + 1] : 'docs/ai/decisions');
|
|
12
14
|
const check = args.includes('--check');
|
|
13
|
-
const STATUS = ['proposta', 'aceita', 'substituida', 'rejeitada'];
|
|
15
|
+
const STATUS = ['proposta', 'aceita', 'implementada', 'substituida', 'rejeitada'];
|
|
14
16
|
|
|
15
17
|
if (!existsSync(dir)) {
|
|
16
18
|
console.error(`Pasta não encontrada: ${dir}`);
|
|
@@ -35,9 +37,34 @@ function parse(text, file) {
|
|
|
35
37
|
return out;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
const files = readdirSync(dir).filter((n) => n.endsWith('.md') && n !== 'INDEX.md').sort();
|
|
41
|
+
const numeroDe = (f) => {
|
|
42
|
+
try {
|
|
43
|
+
const n = parse(readFileSync(path.join(dir, f), 'utf8'), f).numero;
|
|
44
|
+
return /^\d+$/.test(String(n ?? '')) ? Number(n) : null;
|
|
45
|
+
} catch { return null; }
|
|
46
|
+
};
|
|
47
|
+
const maior = () => Math.max(0, ...files.map(numeroDe).filter((n) => n !== null));
|
|
48
|
+
if (args.includes('--numerar')) {
|
|
49
|
+
let max = maior();
|
|
50
|
+
for (const f of files.filter((x) => numeroDe(x) === null)) {
|
|
51
|
+
const file = path.join(dir, f);
|
|
52
|
+
const raw = readFileSync(file, 'utf8');
|
|
53
|
+
const eol = raw.includes('\r\n') ? '\r\n' : '\n';
|
|
54
|
+
const out = raw.replace(/^(titulo:[^\r\n]*)/m, `$1${eol}numero: ${++max}`);
|
|
55
|
+
if (out === raw) { console.error(`${f}: sem linha "titulo:" para numerar`); process.exit(1); }
|
|
56
|
+
writeFileSync(file, out, 'utf8');
|
|
57
|
+
console.log(`${f} -> #${max}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (args.includes('--proximo')) {
|
|
61
|
+
console.log(maior() + 1);
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
const errors = [];
|
|
39
66
|
const adrs = [];
|
|
40
|
-
for (const f of
|
|
67
|
+
for (const f of files) {
|
|
41
68
|
let h;
|
|
42
69
|
try {
|
|
43
70
|
h = parse(readFileSync(path.join(dir, f), 'utf8'), f);
|
|
@@ -46,7 +73,8 @@ for (const f of readdirSync(dir).filter((n) => n.endsWith('.md') && n !== 'INDEX
|
|
|
46
73
|
continue;
|
|
47
74
|
}
|
|
48
75
|
if (!/^\d{4}-\d{2}-\d{2}-/.test(f)) errors.push(`${f}: o nome deve começar com AAAA-MM-DD-`);
|
|
49
|
-
for (const k of ['titulo', 'data', 'status', 'decidido-por', 'dominio']) if (!h[k] || h[k].length === 0) errors.push(`${f}: falta "${k}"`);
|
|
76
|
+
for (const k of ['titulo', 'numero', 'data', 'status', 'decidido-por', 'dominio']) if (!h[k] || h[k].length === 0) errors.push(`${f}: falta "${k}"`);
|
|
77
|
+
if (h.numero && !/^[1-9]\d*$/.test(h.numero)) errors.push(`${f}: numero inválido (${h.numero}); use inteiro a partir de 1`);
|
|
50
78
|
if (h.data && !/^\d{4}-\d{2}-\d{2}$/.test(h.data)) errors.push(`${f}: data inválida (${h.data})`);
|
|
51
79
|
if (h.status && !STATUS.includes(h.status)) errors.push(`${f}: status inválido (${h.status}); use ${STATUS.join(' | ')}`);
|
|
52
80
|
const doms = Array.isArray(h.dominio) ? h.dominio : [h.dominio].filter(Boolean);
|
|
@@ -57,22 +85,27 @@ for (const f of readdirSync(dir).filter((n) => n.endsWith('.md') && n !== 'INDEX
|
|
|
57
85
|
}
|
|
58
86
|
adrs.push({ file: f, ...h, doms });
|
|
59
87
|
}
|
|
88
|
+
const nums = adrs.map((a) => Number(a.numero)).filter(Number.isInteger).sort((a, b) => a - b);
|
|
89
|
+
for (let i = 0; i < nums.length; i++) {
|
|
90
|
+
if (i > 0 && nums[i] === nums[i - 1]) errors.push(`numero #${nums[i]} repetido`);
|
|
91
|
+
else if (nums[i] !== i + 1) { errors.push(`numeração com lacuna: esperado #${i + 1}, achei #${nums[i]}`); break; }
|
|
92
|
+
}
|
|
60
93
|
if (errors.length) {
|
|
61
94
|
console.error(errors.join('\n'));
|
|
62
95
|
process.exit(1);
|
|
63
96
|
}
|
|
64
|
-
adrs.sort((a, b) => b.data.localeCompare(a.data) || b.
|
|
97
|
+
adrs.sort((a, b) => b.data.localeCompare(a.data) || Number(b.numero) - Number(a.numero));
|
|
65
98
|
|
|
66
99
|
const list = (v) => (Array.isArray(v) ? v.join(', ') : v || '');
|
|
67
|
-
const rows = adrs.map((a) => `| ${a.data} | [${a.titulo}](${a.file}) | ${a.doms.join(', ')} | ${a.status} | ${list(a['decidido-por'])} | ${list(a.consultados)} |`);
|
|
100
|
+
const rows = adrs.map((a) => `| ${a.numero} | ${a.data} | [${a.titulo}](${a.file}) | ${a.doms.join(', ')} | ${a.status} | ${list(a['decidido-por'])} | ${list(a.consultados)} |`);
|
|
68
101
|
const body = `# Decisões (ADR)
|
|
69
102
|
|
|
70
|
-
> Gerado por \`node .ai/skills/sg-adr/scripts/adr-index.mjs\` a partir do cabeçalho de cada ADR. Não edite à mão. Ordem: da mais nova para a mais antiga.
|
|
103
|
+
> Gerado por \`node .ai/skills/sg-adr/scripts/adr-index.mjs\` a partir do cabeçalho de cada ADR. Não edite à mão. Ordem: da mais nova para a mais antiga. O número \`#\` é sequencial, dado na criação e **nunca muda**: use-o para falar de uma ADR ("ADR #7").
|
|
71
104
|
|
|
72
105
|
Para saber **por que** algo foi feito, procure pelo domínio (coluna 3) ou pelo módulo no cabeçalho de cada ADR.${DOMINIOS ? ` Domínios: ${DOMINIOS.join(', ')}.` : ''}
|
|
73
106
|
|
|
74
|
-
| Data | Decisão | Domínio | Status | Decidido por | Consultados |
|
|
75
|
-
|
|
107
|
+
| # | Data | Decisão | Domínio | Status | Decidido por | Consultados |
|
|
108
|
+
|---|---|---|---|---|---|---|
|
|
76
109
|
${rows.join('\n')}
|
|
77
110
|
`;
|
|
78
111
|
const target = path.join(dir, 'INDEX.md');
|
|
@@ -11,8 +11,9 @@ Cabeçalho (obrigatório, no topo do arquivo, entre `---`):
|
|
|
11
11
|
---
|
|
12
12
|
description: Uma linha com a decisão.
|
|
13
13
|
titulo: Título curto da decisão
|
|
14
|
+
numero: 1 # sequencial, dado na criação (--proximo); nunca muda
|
|
14
15
|
data: AAAA-MM-DD
|
|
15
|
-
status: proposta # proposta | aceita | substituida | rejeitada
|
|
16
|
+
status: proposta # proposta | aceita | implementada | substituida | rejeitada
|
|
16
17
|
decidido-por: nome ou papel
|
|
17
18
|
consultados: [nomes, quando houver]
|
|
18
19
|
dominio: [seguranca] # lista do projeto (docs/ai/decisions/dominios.txt)
|
|
@@ -23,6 +24,8 @@ origem: plano, commit ou tarefa que gerou a decisão
|
|
|
23
24
|
---
|
|
24
25
|
```
|
|
25
26
|
|
|
27
|
+
Status: `proposta` (a IA propôs; o dono não confirmou); `aceita` (confirmada, ainda não está no código); `implementada` (confirmada e já no código; o que já foi implementado não fica `proposta`).
|
|
28
|
+
|
|
26
29
|
## Contexto
|
|
27
30
|
Problema, forças em jogo e restrições.
|
|
28
31
|
|