@pathbit/agentdevkit 0.0.8 → 0.0.9
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/.pbai4devs/COMPATIBILIDADE.md +4 -2
- package/.pbai4devs/INSTRUCTIONS.md +4 -0
- package/.pbai4devs/commands/sincronizar/COMMAND.md +4 -3
- package/.pbai4devs/rules/documentacao/RULE.md +4 -0
- package/.pbai4devs/rules/qualidade/RULE.md +4 -2
- package/.pbai4devs/scripts/README.md +3 -3
- package/.pbai4devs/scripts/inspecionar-projeto/README.md +31 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +142 -1
- package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +168 -1
- package/.pbai4devs/scripts/lib/native-paths.mjs +35 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +3 -0
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +73 -29
- package/.pbai4devs/scripts/sincronizar-agentes/tests/COEXISTENCE.mjs +190 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +8 -2
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +28 -28
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +56 -8
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +92 -1
- package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +2 -0
- package/.pbai4devs/skills/produto/criar-prd/SKILL.md +2 -0
- package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +2 -0
- package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +18 -4
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +2 -0
- package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +1 -0
- package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +3 -2
- package/.pbai4devs/skills/specs/criar-spec/SKILL.md +1 -0
- package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +2 -1
- package/.pbai4devs/templates/README.md +4 -1
- package/.pbai4devs/templates/configuracao/TEMPLATE.md +6 -0
- package/CHANGELOG.md +10 -0
- package/README.en.md +3 -2
- package/README.es.md +3 -2
- package/README.md +3 -2
- package/docs/README.md +1 -0
- package/docs/guides/README.md +1 -0
- package/docs/guides/cli.md +2 -0
- package/docs/guides/documentation.md +63 -0
- package/docs/guides/installation.md +12 -5
- package/docs/guides/resources.md +6 -5
- package/docs/guides/troubleshooting.md +6 -6
- package/docs/guides/workflow.md +2 -0
- package/docs/maintainers/packages.md +1 -1
- package/docs/product/design.md +12 -0
- package/docs/product/prd.md +4 -1
- package/docs/product/roadmap.md +2 -0
- package/docs/specs/README.md +1 -0
- package/docs/specs/v0.0.9/README.md +9 -0
- package/docs/specs/v0.0.9/SPEC.md +68 -0
- package/package.json +3 -2
- package/src/cli.mjs +3 -0
- package/src/i18n.mjs +3 -0
- package/src/install.mjs +84 -16
|
@@ -87,8 +87,10 @@ remoção em fevereiro de 2027. No Copilot, use skills para rotinas invocáveis.
|
|
|
87
87
|
|
|
88
88
|
O instalador gera a seleção e a política em `espelhos.conf`; `sincronizar`
|
|
89
89
|
reconstrói os apontamentos somente com Node e os arquivos do hub instalado.
|
|
90
|
-
`--check` não altera arquivos. `--force` faz backup ignorado
|
|
91
|
-
|
|
90
|
+
`--check` não altera arquivos. `--force` faz backup ignorado ao reparar
|
|
91
|
+
conflitos gerenciados. Recursos e links humanos dos clientes são preservados;
|
|
92
|
+
uma rota preservada não recebe apontamentos novos. No Zod, `.codex/skills`
|
|
93
|
+
continua apontando para suas skills e o Codex recebe o kit em `.agents/skills`.
|
|
92
94
|
|
|
93
95
|
As fontes confirmam formatos, não execução de cada cliente. Valide descoberta,
|
|
94
96
|
invocação e resultado no cliente escolhido antes de considerar uma integração
|
|
@@ -6,6 +6,10 @@ description: Ordem de trabalho e manutenção dos agentes.
|
|
|
6
6
|
|
|
7
7
|
Peça a skill pelo nome ou solicite a leitura de seu `SKILL.md`.
|
|
8
8
|
|
|
9
|
+
Antes de trabalhar na documentação, use `atualizar-documentacao` para inventariar
|
|
10
|
+
e ler os arquivos existentes. Preserve seus caminhos e conteúdo; complemente
|
|
11
|
+
lacunas e organize por índices, sem substituir documentos pelos modelos.
|
|
12
|
+
|
|
9
13
|
Antes de instalar ou preparar ferramentas, pergunte quais clientes o usuário
|
|
10
14
|
usa, se não houver escolha no pedido ou no estado do kit. A CLI aceita
|
|
11
15
|
`--agents codex,claude`; `agents` consulta ou altera a seleção depois.
|
|
@@ -8,9 +8,10 @@ Entrada: sem argumentos usa `--force` para reparar; `--check` só confere.
|
|
|
8
8
|
Pré-requisitos: Node.js 24+ e manifesto `.pbai4devs/espelhos.conf` válido.
|
|
9
9
|
|
|
10
10
|
Siga [sincronizar-agentes](../../skills/projeto/sincronizar-agentes/SKILL.md)
|
|
11
|
-
e execute o script correspondente pelo seletor. A reconstrução
|
|
12
|
-
apontamentos e
|
|
13
|
-
|
|
11
|
+
e execute o script correspondente pelo seletor. A reconstrução repara
|
|
12
|
+
apontamentos e remove saídas obsoletas cuja propriedade do kit foi comprovada,
|
|
13
|
+
com backup local. Recursos, configurações e links humanos são preservados,
|
|
14
|
+
inclusive quando seus nomes coincidem com os destinos do kit.
|
|
14
15
|
Após reparar, execute também `--check`.
|
|
15
16
|
|
|
16
17
|
Respeite os clientes registrados no manifesto. Para mudar a seleção salva,
|
|
@@ -5,6 +5,10 @@ description: Documentação e revisão fazem parte de toda entrega implementada.
|
|
|
5
5
|
|
|
6
6
|
# Documentação da entrega
|
|
7
7
|
|
|
8
|
+
Antes de criar, organizar ou alterar documentação, use `atualizar-documentacao`:
|
|
9
|
+
inventarie e leia o que já existe. Reutilize caminhos e formatos;
|
|
10
|
+
complemente lacunas sem apagar, mover ou substituir arquivos por templates.
|
|
11
|
+
|
|
8
12
|
Após implementar, siga `atualizar-documentacao` e o comando `revisar-entrega`
|
|
9
13
|
antes de commit ou merge. Use os modelos da fase atual; registre decisões sem
|
|
10
14
|
impacto com justificativa e resolva os achados da revisão independente.
|
|
@@ -15,8 +15,10 @@ Use `verificar-trabalho` para provar as mudanças. Alterações web exigem testa
|
|
|
15
15
|
o fluxo real no navegador; desktop exige abrir e testar a aplicação. Código
|
|
16
16
|
revisado, build ou fixture isolada não substituem essa prova funcional.
|
|
17
17
|
|
|
18
|
-
Comece arquivos Markdown com frontmatter YAML adequado ao tipo e título
|
|
19
|
-
preserve os metadados exigidos pela ferramenta.
|
|
18
|
+
Comece novos arquivos Markdown com frontmatter YAML adequado ao tipo e título
|
|
19
|
+
H1; preserve os metadados exigidos pela ferramenta. Em documentos humanos
|
|
20
|
+
existentes, mantenha o formato e o conteúdo: não os reformate apenas para
|
|
21
|
+
impor esse padrão.
|
|
20
22
|
|
|
21
23
|
Ao criar ou atualizar `.gitignore`, mantenha o bloco do
|
|
22
24
|
[modelo de gitignore](../../templates/gitignore/TEMPLATE.md)
|
|
@@ -59,9 +59,9 @@ recursos, com frontmatter YAML sem corpo. Caminhos são relativos à raiz.
|
|
|
59
59
|
Somente os quatro arquivos principais são fontes; não registre documentos.
|
|
60
60
|
Use `gitignore|pasta` para gerar apenas o `.gitignore`, como em `.vscode`.
|
|
61
61
|
|
|
62
|
-
`sincronizar-agentes --force`
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
`sincronizar-agentes --force` repara os destinos gerenciados com backup em
|
|
63
|
+
`.agent-tests/`. Recursos e configurações humanos permanecem intactos, inclusive
|
|
64
|
+
quando ocupam o caminho de um apontamento. `--check` não altera arquivos.
|
|
65
65
|
O sincronizador também gera o catálogo completo e os índices auxiliares a partir
|
|
66
66
|
das fontes. Helpers e testes constam como suporte, sem fingir comandos próprios.
|
|
67
67
|
Ele protege os derivados contra versionamento e preserva regras humanas.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Inventário somente leitura da estrutura e documentação do projeto.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Inspecionar projeto
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
node .pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
O JSON identifica tecnologia, comandos, estrutura e documentos existentes.
|
|
12
|
+
`documentation.files` mantém caminhos e caixa, papel provável, tamanho e hash
|
|
13
|
+
quando a leitura couber nos limites. Referências válidas do estado aparecem em
|
|
14
|
+
`configured`; nomes sugerem papéis, mas não comprovam conteúdo ou aprovação.
|
|
15
|
+
|
|
16
|
+
Links simbólicos são listados como opacos, sem ler seus destinos. Conteúdo dos
|
|
17
|
+
documentos e valores de configuração não aparecem no relatório. `scan` e
|
|
18
|
+
`warnings` informam limites e leitura incompleta; `scan.complete` resume essas
|
|
19
|
+
condições. Uma pasta simbólica como `docs` também aparece, sem presumir que
|
|
20
|
+
esteja vazia. Links não classificados recebem `role: unclassified`; enquanto
|
|
21
|
+
existir uma fronteira não inspecionada, `scan.complete` será falso. Não use
|
|
22
|
+
inventário incompleto como prova de ausência de documentos.
|
|
23
|
+
|
|
24
|
+
`exclusions` descreve pastas de clientes, artefatos e caches fora do inventário.
|
|
25
|
+
Documentos humanos em `.github`, exemplos, wiki e RFCs são incluídos; os
|
|
26
|
+
apontamentos gerados em `.github/skills`, agentes e comandos ficam de fora.
|
|
27
|
+
|
|
28
|
+
O script não cria nem altera arquivos. Leia os documentos relevantes antes de
|
|
29
|
+
planejar complementos; preserve sua estrutura e use templates só para lacunas.
|
|
30
|
+
`--check` confere a estrutura técnica. `--root` aceita somente as fixtures
|
|
31
|
+
documentadas pelo próprio comando, sem ampliar o acesso a outros projetos.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Inventario somente leitura; nomes e caminhos, nunca valores de configuracao.
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { requireNode, isMain } from '../lib/runtime.mjs';
|
|
7
8
|
|
|
@@ -92,7 +93,146 @@ function maturity(root, report) {
|
|
|
92
93
|
return { level, planning, configuration, released, signals: { application: report.mode === 'aplicacao-detectada', structure: report.contract.complete }, limits: ['Classificacao descritiva; status em arquivos nao substitui aprovacao, testes ou provas de funcionamento.'] };
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
|
|
96
|
+
const documentRoles = new Set(['readme', 'contribuicao', 'changelog', 'seguranca', 'adrs', 'prd', 'ux_ui', 'design', 'roadmap', 'releases', 'specs', 'tarefas']);
|
|
97
|
+
const documentLimits = { maxDepth: 12, maxEntries: 25000, maxFiles: 2000, maxFileBytes: 2 * 1024 * 1024, maxTotalBytes: 16 * 1024 * 1024 };
|
|
98
|
+
const documentClients = [...defaultAgents, '.zcode', '.zai', '.kimi', '.kimi-code', '.minimax', '.mimocode', '.dsh'];
|
|
99
|
+
const documentName = name => /\.(?:md|mdx|rst|adoc)$/i.test(name) || /^(?:readme|contributing|changelog|history|security|licen[cs]e|copying|notice|authors|code_of_conduct|architecture|design|roadmap)(?:\.txt)?$/i.test(name);
|
|
100
|
+
const documentRole = file => {
|
|
101
|
+
const name = path.posix.basename(file).toLowerCase();
|
|
102
|
+
if (/^readme(?:\.|$)/.test(name)) return 'readme';
|
|
103
|
+
if (/^(?:agents|claude|gemini|copilot-instructions)\.md$/.test(name)) return 'instructions';
|
|
104
|
+
if (/^(?:licen[cs]e|copying|notice)(?:\.|$)/.test(name)) return 'license';
|
|
105
|
+
if (/^code_of_conduct(?:\.|$)/.test(name)) return 'conduct';
|
|
106
|
+
if (/^contribut/.test(name)) return 'contribution';
|
|
107
|
+
if (/^security(?:\.|$)/.test(name)) return 'security';
|
|
108
|
+
if (/^(?:changelog|history)(?:\.|$)/.test(name)) return 'changelog';
|
|
109
|
+
if (/(^|\/)(?:adr|adrs|decisions|rfcs)(\/|$)/i.test(file) || /^adr[-_.]/.test(name)) return 'decision';
|
|
110
|
+
if (/(^|\/)(?:prd|product|requisitos)(?:\.|\/|$)/i.test(file)) return 'product';
|
|
111
|
+
if (/(?:architecture|arquitetura|design)(?:\.|\/|$)/i.test(file)) return 'design';
|
|
112
|
+
if (/roadmap/i.test(name)) return 'roadmap';
|
|
113
|
+
if (/(^|\/)(?:spec|specs|specifications)(\/|$)/i.test(file)) return 'spec';
|
|
114
|
+
return 'document';
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Names, kinds and bounded hashes only. A guessed role is never an approval.
|
|
118
|
+
export function inspectDocumentation(root, limits = {}) {
|
|
119
|
+
root = fs.realpathSync(path.resolve(root));
|
|
120
|
+
const bounds = { ...documentLimits, ...limits };
|
|
121
|
+
for (const [key, value] of Object.entries(bounds)) {
|
|
122
|
+
if (!(key in documentLimits) || !Number.isSafeInteger(value) || value < 0 || value > documentLimits[key]) throw new Error('Limite documental invalido.');
|
|
123
|
+
}
|
|
124
|
+
const ignored = new Set([...defaultExcluded, ...documentClients, ...agentRoots(root)].filter(name => !['examples', '.github'].includes(name)).map(name => name.toLowerCase()));
|
|
125
|
+
const excludedPaths = ['examples/generated', ...['skills', 'agents', 'commands', 'rules', 'instructions', 'prompts'].map(name => `.github/${name}`)];
|
|
126
|
+
const files = new Map();
|
|
127
|
+
const configured = [];
|
|
128
|
+
const warnings = [];
|
|
129
|
+
const scan = { ...bounds, entries: 0, bytesRead: 0, bytesHashed: 0, truncated: false };
|
|
130
|
+
const warn = (file, reason) => warnings.push({ file, reason });
|
|
131
|
+
const excludedDocument = file => file.split('/').some(name => ignored.has(name.toLowerCase()) || /^\.?playwright/i.test(name))
|
|
132
|
+
|| excludedPaths.some(prefix => file.toLowerCase() === prefix || file.toLowerCase().startsWith(prefix + '/'));
|
|
133
|
+
const safeDocument = value => typeof value === 'string' && value.length > 0 && !path.isAbsolute(value) && !/[\\\x00-\x1f\x7f:]/.test(value)
|
|
134
|
+
&& !value.split('/').some(part => !part || part === '.' || part === '..');
|
|
135
|
+
const locate = file => {
|
|
136
|
+
let current = root;
|
|
137
|
+
const parts = file.split('/');
|
|
138
|
+
for (const [index, part] of parts.entries()) {
|
|
139
|
+
current = path.join(current, part);
|
|
140
|
+
let item;
|
|
141
|
+
try { item = fs.lstatSync(current); }
|
|
142
|
+
catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return { kind: 'missing' }; throw error; }
|
|
143
|
+
if (item.isSymbolicLink()) return { kind: 'symbolic-link', boundary: parts.slice(0, index + 1).join('/'), item };
|
|
144
|
+
if (index < parts.length - 1 && !item.isDirectory()) return { kind: 'missing' };
|
|
145
|
+
if (index === parts.length - 1) return { kind: item.isFile() ? 'file' : item.isDirectory() ? 'directory' : 'unsupported', item };
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
function boundedHash(file, expected) {
|
|
149
|
+
if (expected.size > bounds.maxFileBytes || expected.size > bounds.maxTotalBytes - scan.bytesRead) {
|
|
150
|
+
scan.truncated = true; warn(file, 'hash-byte-limit'); return;
|
|
151
|
+
}
|
|
152
|
+
let descriptor;
|
|
153
|
+
try {
|
|
154
|
+
descriptor = fs.openSync(path.join(root, file), fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
155
|
+
const before = fs.fstatSync(descriptor);
|
|
156
|
+
if (!before.isFile() || before.dev !== expected.dev || before.ino !== expected.ino || before.size !== expected.size
|
|
157
|
+
|| before.mtimeMs !== expected.mtimeMs || before.ctimeMs !== expected.ctimeMs) { warn(file, 'changed-during-read'); return; }
|
|
158
|
+
const buffer = Buffer.alloc(Math.min(before.size + 1, bounds.maxTotalBytes - scan.bytesRead));
|
|
159
|
+
let length = 0;
|
|
160
|
+
while (length < buffer.length) {
|
|
161
|
+
const read = fs.readSync(descriptor, buffer, length, buffer.length - length, length);
|
|
162
|
+
if (!read) break;
|
|
163
|
+
length += read;
|
|
164
|
+
scan.bytesRead += read;
|
|
165
|
+
}
|
|
166
|
+
const after = fs.fstatSync(descriptor);
|
|
167
|
+
if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) {
|
|
168
|
+
warn(file, 'changed-during-read'); return;
|
|
169
|
+
}
|
|
170
|
+
scan.bytesHashed += length;
|
|
171
|
+
return createHash('sha256').update(buffer.subarray(0, length)).digest('hex');
|
|
172
|
+
} catch (error) { warn(file, error.code === 'ELOOP' ? 'symbolic-link-during-read' : 'unreadable-document'); }
|
|
173
|
+
finally { if (descriptor !== undefined) fs.closeSync(descriptor); }
|
|
174
|
+
}
|
|
175
|
+
function add(file, found, role) {
|
|
176
|
+
if (files.has(file)) {
|
|
177
|
+
if (role) { const entry = files.get(file); entry.origin = 'configured'; entry.configuredAs = [...new Set([...(entry.configuredAs || []), role])].sort(); }
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (files.size >= bounds.maxFiles) { scan.truncated = true; warn(file, 'file-limit'); return; }
|
|
181
|
+
const inferredRole = found.kind === 'symbolic-link' && !documentName(path.posix.basename(file)) ? 'unclassified' : documentRole(file);
|
|
182
|
+
const entry = { path: file, kind: found.kind, role: inferredRole, origin: role ? 'configured' : 'inferred', bytes: found.item.size };
|
|
183
|
+
if (role) entry.configuredAs = [role];
|
|
184
|
+
if (found.kind === 'symbolic-link') warn(file, 'symbolic-link-not-inspected');
|
|
185
|
+
if (found.kind === 'file') {
|
|
186
|
+
const digest = boundedHash(file, found.item);
|
|
187
|
+
if (digest) entry.sha256 = digest;
|
|
188
|
+
}
|
|
189
|
+
files.set(file, entry);
|
|
190
|
+
}
|
|
191
|
+
function walk(directory = '', depth = 0, role) {
|
|
192
|
+
if (directory) {
|
|
193
|
+
const found = locate(directory);
|
|
194
|
+
if (found.kind !== 'directory') { if (found.kind === 'symbolic-link') add(found.boundary, found, role); return; }
|
|
195
|
+
}
|
|
196
|
+
let children;
|
|
197
|
+
try { children = fs.readdirSync(path.join(root, directory), { withFileTypes: true }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0); }
|
|
198
|
+
catch { warn(directory || '.', 'unreadable-directory'); return; }
|
|
199
|
+
// Discard a listing if the directory became a link while being inspected.
|
|
200
|
+
if (directory && locate(directory).kind !== 'directory') { warn(directory, 'changed-during-read'); return; }
|
|
201
|
+
for (const child of children) {
|
|
202
|
+
if (scan.entries >= bounds.maxEntries || files.size >= bounds.maxFiles) { scan.truncated = true; return; }
|
|
203
|
+
scan.entries++;
|
|
204
|
+
const file = directory ? `${directory}/${child.name}` : child.name;
|
|
205
|
+
if (excludedDocument(file)) continue;
|
|
206
|
+
const found = locate(file);
|
|
207
|
+
if (found.kind === 'directory') {
|
|
208
|
+
if (depth < bounds.maxDepth) walk(file, depth + 1, role);
|
|
209
|
+
else { scan.truncated = true; warn(file, 'depth-limit'); }
|
|
210
|
+
} else if (found.kind === 'file' && documentName(child.name)) add(file, found, role);
|
|
211
|
+
else if (found.kind === 'symbolic-link') add(found.boundary, found, role);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const declared = metadata(root, '.pbai4devs/estado/configuracao/ESTADO.md').documents;
|
|
215
|
+
for (const [role, file] of Object.entries(declared)) {
|
|
216
|
+
if (!documentRoles.has(role)) continue;
|
|
217
|
+
if (!safeDocument(file)) { if (file !== null) warn(null, 'invalid-configured-path'); continue; }
|
|
218
|
+
if (excludedDocument(file)) { configured.push({ role, path: file, kind: 'excluded' }); continue; }
|
|
219
|
+
const found = locate(file);
|
|
220
|
+
configured.push({ role, path: file, kind: found.kind });
|
|
221
|
+
if (found.kind === 'directory') walk(file, 0, role);
|
|
222
|
+
else if (found.kind === 'file' && documentName(path.posix.basename(file))) add(file, found, role);
|
|
223
|
+
else if (found.kind === 'file') warn(file, 'configured-path-not-document');
|
|
224
|
+
else if (found.kind === 'symbolic-link') add(found.boundary, found, role);
|
|
225
|
+
}
|
|
226
|
+
walk();
|
|
227
|
+
return {
|
|
228
|
+
files: [...files.values()].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0),
|
|
229
|
+
configured, scan: { ...scan, complete: !scan.truncated && warnings.length === 0 }, warnings,
|
|
230
|
+
exclusions: { directories: [...ignored].sort(), paths: excludedPaths, prefixes: ['playwright', '.playwright'] },
|
|
231
|
+
limits: ['Papeis inferidos por nome; confirme o conteudo antes de propor alteracoes.', 'Links simbolicos sao opacos; destinos nao sao lidos.', 'O inventario nao cria documentos nem comprova aprovacao ou completude.'],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function inspectProject(root = repository, { check = false, documentationLimits } = {}) {
|
|
96
236
|
requireNode();
|
|
97
237
|
root = fs.realpathSync(path.resolve(root));
|
|
98
238
|
if (!fs.statSync(root).isDirectory()) throw new Error('A origem deve ser um diretorio.');
|
|
@@ -160,6 +300,7 @@ export function inspectProject(root = repository, { check = false } = {}) {
|
|
|
160
300
|
report.languages = [...languages].sort();
|
|
161
301
|
report.contract = { checked: check, complete: report.structure.missing.length === 0 };
|
|
162
302
|
report.maturity = maturity(root, report);
|
|
303
|
+
report.documentation = inspectDocumentation(root, documentationLimits);
|
|
163
304
|
return report;
|
|
164
305
|
}
|
|
165
306
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import { dirname, join } from 'node:path';
|
|
4
5
|
import { spawnSync } from 'node:child_process';
|
|
5
6
|
import test from 'node:test';
|
|
6
7
|
import { projectRoot, requireNode } from '../../lib/runtime.mjs';
|
|
8
|
+
import { inspectDocumentation } from '../SCRIPT.mjs';
|
|
7
9
|
|
|
8
10
|
requireNode();
|
|
9
11
|
const script = join(projectRoot, '.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs');
|
|
@@ -159,3 +161,168 @@ test('status arbitrários e referências fora da raiz não expõem dados da conf
|
|
|
159
161
|
assert.equal(result.report.maturity.planning.ready, false);
|
|
160
162
|
assert.ok(!result.stdout.includes(secret));
|
|
161
163
|
});
|
|
164
|
+
|
|
165
|
+
test('inventário documental vazio não cria arquivos nem presume aprovação', t => {
|
|
166
|
+
const { root, run } = fixture(t);
|
|
167
|
+
const result = run();
|
|
168
|
+
assert.equal(result.status, 0, result.stderr);
|
|
169
|
+
assert.deepEqual(result.report.documentation.files, []);
|
|
170
|
+
assert.deepEqual(result.report.documentation.configured, []);
|
|
171
|
+
assert.equal(result.report.documentation.scan.truncated, false);
|
|
172
|
+
assert.equal(result.report.documentation.scan.complete, true);
|
|
173
|
+
assert.deepEqual(readdirSync(root), []);
|
|
174
|
+
assert.match(result.report.documentation.limits.at(-1), /nao cria documentos/);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test('descobre documentação real sem impor caixa, pasta docs ou frontmatter', t => {
|
|
178
|
+
const { root, file, run } = fixture(t);
|
|
179
|
+
const content = 'DOC_CONTENT_MUST_NOT_BE_EMITTED_182711';
|
|
180
|
+
for (const name of ['Readme.md', 'CONTRIBUTING.rst', 'SECURITY.adoc', 'History.md', 'ARCHITECTURE.md', 'AGENTS.md', 'LICENSE', 'examples/README', '_examples/server/README.md', 'wiki/guide.mdx', 'rfcs/001-change.md', 'packages/docs/content/api.mdx', '.github/CONTRIBUTING.md', '.github/SECURITY.md', '.github/copilot-instructions.md']) file(name, content);
|
|
181
|
+
for (const name of ['node_modules/lib/README.md', '.pbai4devs/README.md', '.claude/skills/private/SKILL.md', '.github/skills/demo/SKILL.md', '.github/agents/revisor.agent.md', 'examples/generated/fixture/README.md', '.PLAYWRIGHT-MCP/README.md', 'tmp/README.md']) file(name, content);
|
|
182
|
+
const result = run();
|
|
183
|
+
assert.equal(result.status, 0, result.stderr);
|
|
184
|
+
const docs = result.report.documentation;
|
|
185
|
+
assert.equal(docs.files.length, 15);
|
|
186
|
+
assert.equal(docs.files.find(item => item.path === 'Readme.md').role, 'readme');
|
|
187
|
+
assert.equal(docs.files.find(item => item.path === 'rfcs/001-change.md').role, 'decision');
|
|
188
|
+
assert.equal(docs.files.find(item => item.path === 'ARCHITECTURE.md').role, 'design');
|
|
189
|
+
assert.equal(docs.files.find(item => item.path === 'AGENTS.md').role, 'instructions');
|
|
190
|
+
assert.equal(docs.files.find(item => item.path === '.github/copilot-instructions.md').role, 'instructions');
|
|
191
|
+
assert.equal(docs.files.find(item => item.path === '.github/CONTRIBUTING.md').role, 'contribution');
|
|
192
|
+
assert.equal(docs.files.find(item => item.path === 'LICENSE').role, 'license');
|
|
193
|
+
assert.ok(docs.exclusions.paths.includes('.github/skills'));
|
|
194
|
+
assert.equal(docs.files.find(item => item.path === 'wiki/guide.mdx').sha256, createHash('sha256').update(content).digest('hex'));
|
|
195
|
+
assert.ok(docs.files.every(item => item.origin === 'inferred' && item.kind === 'file'));
|
|
196
|
+
assert.ok(!result.stdout.includes(content));
|
|
197
|
+
assert.equal(readFileSync(join(root, 'Readme.md'), 'utf8'), content);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('caminhos cadastrados têm proveniência sem assumir status ou ler arquivos de configuração', t => {
|
|
201
|
+
const { file, run } = fixture(t);
|
|
202
|
+
file('.pbai4devs/estado/configuracao/ESTADO.md', '---\ndocumentos:\n design: architecture/current.md\n specs: specifications\n prd: planning/absent.md\n readme: ../private-secret.md\n seguranca: .env\n---\n# Estado\n');
|
|
203
|
+
file('architecture/current.md', '# Existing architecture\n');
|
|
204
|
+
file('specifications/current.mdx', '# Increment\n');
|
|
205
|
+
file('.env', 'DO_NOT_EMIT_OR_HASH_THIS_SECRET');
|
|
206
|
+
const result = run();
|
|
207
|
+
assert.equal(result.status, 0, result.stderr);
|
|
208
|
+
const docs = result.report.documentation;
|
|
209
|
+
assert.deepEqual(docs.files.find(item => item.path === 'architecture/current.md').configuredAs, ['design']);
|
|
210
|
+
assert.equal(docs.files.find(item => item.path === 'specifications/current.mdx').origin, 'configured');
|
|
211
|
+
assert.ok(docs.configured.some(item => item.role === 'prd' && item.kind === 'missing'));
|
|
212
|
+
assert.ok(docs.warnings.some(item => item.reason === 'invalid-configured-path'));
|
|
213
|
+
assert.ok(docs.warnings.some(item => item.reason === 'configured-path-not-document'));
|
|
214
|
+
assert.ok(!docs.files.some(item => item.path === '.env'));
|
|
215
|
+
assert.ok(!result.stdout.includes('private-secret') && !result.stdout.includes('DO_NOT_EMIT'));
|
|
216
|
+
assert.equal(docs.files.some(item => Object.hasOwn(item, 'status')), false);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('documentos simbólicos permanecem opacos sem ler destinos internos, externos ou ausentes', t => {
|
|
220
|
+
const { root, file, run } = fixture(t);
|
|
221
|
+
const outside = mkdtempSync(join(projectRoot, '.agent-tests', 'outside-docs-'));
|
|
222
|
+
t.after(() => rmSync(outside, { recursive: true, force: true }));
|
|
223
|
+
file('docs/real.md', '# Actual document\n');
|
|
224
|
+
writeFileSync(join(outside, 'private.md'), 'OUTSIDE_SECRET_MUST_NOT_APPEAR');
|
|
225
|
+
try {
|
|
226
|
+
symlinkSync('docs/real.md', join(root, 'README.md'));
|
|
227
|
+
symlinkSync(join(outside, 'private.md'), join(root, 'CLAUDE.md'));
|
|
228
|
+
symlinkSync('absent.md', join(root, 'missing.md'));
|
|
229
|
+
symlinkSync(outside, join(root, 'external'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (process.platform !== 'win32' || !['EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
232
|
+
t.skip('Links simbólicos indisponíveis sem permissão do Windows.'); return;
|
|
233
|
+
}
|
|
234
|
+
file('.pbai4devs/estado/configuracao/ESTADO.md', '---\ndocumentos:\n design: external/private.md\n---\n# State\n');
|
|
235
|
+
const result = run();
|
|
236
|
+
assert.equal(result.status, 0, result.stderr);
|
|
237
|
+
const docs = result.report.documentation;
|
|
238
|
+
for (const name of ['README.md', 'CLAUDE.md', 'missing.md', 'external']) {
|
|
239
|
+
const item = docs.files.find(item => item.path === name);
|
|
240
|
+
assert.equal(item?.kind, 'symbolic-link', name);
|
|
241
|
+
assert.equal(item.sha256, undefined);
|
|
242
|
+
}
|
|
243
|
+
assert.ok(!docs.files.some(item => item.path === 'external/private.md'));
|
|
244
|
+
assert.ok(!result.stdout.includes(outside) && !result.stdout.includes('OUTSIDE_SECRET'));
|
|
245
|
+
assert.equal(docs.scan.complete, false);
|
|
246
|
+
assert.ok(docs.warnings.some(item => item.file === 'external' && item.reason === 'symbolic-link-not-inspected'));
|
|
247
|
+
assert.equal(readFileSync(join(outside, 'private.md'), 'utf8'), 'OUTSIDE_SECRET_MUST_NOT_APPEAR');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('pastas documentais simbólicas sem cadastro não ocultam uma leitura incompleta', t => {
|
|
251
|
+
const { root, run } = fixture(t);
|
|
252
|
+
const outside = mkdtempSync(join(projectRoot, '.agent-tests', 'outside-doc-tree-'));
|
|
253
|
+
t.after(() => rmSync(outside, { recursive: true, force: true }));
|
|
254
|
+
const privateContent = 'UNREAD_EXTERNAL_DOCUMENT_719329';
|
|
255
|
+
writeFileSync(join(outside, 'private.md'), privateContent);
|
|
256
|
+
try {
|
|
257
|
+
for (const name of ['docs', 'wiki', 'rfcs', 'examples', 'custom-docs']) symlinkSync(outside, join(root, name), process.platform === 'win32' ? 'junction' : 'dir');
|
|
258
|
+
symlinkSync('absent-directory', join(root, 'missing-directory'), process.platform === 'win32' ? 'junction' : 'dir');
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (process.platform !== 'win32' || !['EPERM', 'EACCES'].includes(error.code)) throw error;
|
|
261
|
+
t.skip('Links simbólicos indisponíveis sem permissão do Windows.'); return;
|
|
262
|
+
}
|
|
263
|
+
const result = run();
|
|
264
|
+
assert.equal(result.status, 0, result.stderr);
|
|
265
|
+
const docs = result.report.documentation;
|
|
266
|
+
assert.equal(docs.scan.complete, false);
|
|
267
|
+
assert.equal(docs.scan.bytesRead, 0);
|
|
268
|
+
assert.deepEqual(docs.configured, []);
|
|
269
|
+
assert.deepEqual(docs.files.map(item => item.path), ['custom-docs', 'docs', 'examples', 'missing-directory', 'rfcs', 'wiki']);
|
|
270
|
+
assert.ok(docs.files.every(item => item.kind === 'symbolic-link' && item.role === 'unclassified' && !item.sha256));
|
|
271
|
+
assert.equal(docs.warnings.filter(item => item.reason === 'symbolic-link-not-inspected').length, 6);
|
|
272
|
+
assert.ok(!result.stdout.includes(outside) && !result.stdout.includes(privateContent));
|
|
273
|
+
assert.equal(readFileSync(join(outside, 'private.md'), 'utf8'), privateContent);
|
|
274
|
+
const limited = inspectDocumentation(root, { maxFiles: 1 });
|
|
275
|
+
assert.equal(limited.files.length, 1);
|
|
276
|
+
assert.equal(limited.scan.truncated, true);
|
|
277
|
+
assert.equal(limited.scan.complete, false);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test('limites documentais são explícitos e não retornam inventário parcial como completo', t => {
|
|
281
|
+
const { root, file } = fixture(t);
|
|
282
|
+
file('a.md', '123'); file('b.md', '456'); file('deep/nested/c.md', '789');
|
|
283
|
+
const files = inspectDocumentation(root, { maxFiles: 1 });
|
|
284
|
+
assert.equal(files.files.length, 1); assert.equal(files.scan.truncated, true);
|
|
285
|
+
assert.equal(files.scan.complete, false);
|
|
286
|
+
const entries = inspectDocumentation(root, { maxEntries: 1 });
|
|
287
|
+
assert.equal(entries.scan.entries, 1); assert.equal(entries.scan.truncated, true);
|
|
288
|
+
const depth = inspectDocumentation(root, { maxDepth: 0 });
|
|
289
|
+
assert.ok(!depth.files.some(item => item.path.startsWith('deep/')));
|
|
290
|
+
assert.ok(depth.warnings.some(item => item.reason === 'depth-limit'));
|
|
291
|
+
const bytes = inspectDocumentation(root, { maxFileBytes: 2 });
|
|
292
|
+
assert.equal(bytes.scan.bytesHashed, 0); assert.equal(bytes.scan.truncated, true);
|
|
293
|
+
assert.ok(bytes.files.every(item => !item.sha256));
|
|
294
|
+
const total = inspectDocumentation(root, { maxTotalBytes: 4 });
|
|
295
|
+
assert.ok(total.scan.bytesRead <= 4); assert.ok(total.scan.bytesHashed <= 4);
|
|
296
|
+
assert.equal(total.scan.truncated, true);
|
|
297
|
+
for (const limits of [{ maxFiles: -1 }, { maxEntries: 25001 }, { unknown: 1 }]) assert.throws(() => inspectDocumentation(root, limits), /Limite documental/);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test('inventário repetido preserva bytes, modos, caminhos e índice Git', t => {
|
|
301
|
+
const { root, file, run } = fixture(t);
|
|
302
|
+
file('README.md', '# Existing\n'); file('docs/guide.md', 'No imposed frontmatter\n');
|
|
303
|
+
const git = (...args) => {
|
|
304
|
+
const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' });
|
|
305
|
+
assert.equal(result.status, 0, result.stderr); return result.stdout;
|
|
306
|
+
};
|
|
307
|
+
git('init', '--quiet', '--template='); git('add', '--', 'README.md');
|
|
308
|
+
const snapshot = () => {
|
|
309
|
+
const files = {};
|
|
310
|
+
const visit = (folder = '') => {
|
|
311
|
+
for (const name of readdirSync(join(root, folder)).sort()) {
|
|
312
|
+
if (name === '.git') continue;
|
|
313
|
+
const relative = folder ? `${folder}/${name}` : name;
|
|
314
|
+
const info = lstatSync(join(root, relative));
|
|
315
|
+
files[relative] = { mode: info.mode, kind: info.isDirectory() ? 'directory' : info.isSymbolicLink() ? 'symbolic-link' : 'file' };
|
|
316
|
+
if (info.isSymbolicLink()) files[relative].target = readlinkSync(join(root, relative));
|
|
317
|
+
else if (info.isDirectory()) visit(relative);
|
|
318
|
+
else files[relative].sha256 = createHash('sha256').update(readFileSync(join(root, relative))).digest('hex');
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
visit(); return { files, index: git('ls-files', '--stage'), staged: git('diff', '--cached', '--binary'), config: readFileSync(join(root, '.git/config'), 'utf8') };
|
|
322
|
+
};
|
|
323
|
+
const before = snapshot();
|
|
324
|
+
const first = run(), second = run();
|
|
325
|
+
assert.equal(first.status, 0, first.stderr); assert.equal(second.status, 0, second.stderr);
|
|
326
|
+
assert.deepEqual(first.report.documentation, second.report.documentation);
|
|
327
|
+
assert.deepEqual(snapshot(), before);
|
|
328
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// Inspect native paths without opening a symlink target. Callers restrict this
|
|
5
|
+
// exception to user-owned adapter paths; canonical kit files stay strict.
|
|
6
|
+
export function nativePathBoundary(root, path, ownedPaths = [], { preserveRootAliases = false } = {}) {
|
|
7
|
+
if (typeof path !== 'string' || !path || isAbsolute(path) || /[|\\\x00-\x1f\x7f:]/.test(path) || path.split('/').some(part => !part || part === '.' || part === '..')) {
|
|
8
|
+
throw Object.assign(new Error(`Caminho invalido: ${path}`), { code: 'E_PATH' });
|
|
9
|
+
}
|
|
10
|
+
const parts = path.split('/');
|
|
11
|
+
let current = root;
|
|
12
|
+
for (const [index, part] of parts.entries()) {
|
|
13
|
+
try {
|
|
14
|
+
const aliases = lstatSync(current).isDirectory() ? readdirSync(current).filter(name => name.toLowerCase() === part.toLowerCase() && name !== part).sort() : [];
|
|
15
|
+
if (aliases.length) {
|
|
16
|
+
const rootEntry = parts.length === 1 && ['README.md', 'AGENTS.md', 'CLAUDE.md', 'GEMINI.md'].includes(path);
|
|
17
|
+
const owned = [...ownedPaths].some(value => value.toLowerCase() === path.toLowerCase() || value.toLowerCase().startsWith(`${path.toLowerCase()}/`));
|
|
18
|
+
if (preserveRootAliases && rootEntry && !owned) return { path: aliases[0], reason: 'case-variant' };
|
|
19
|
+
throw Object.assign(new Error(`Caminhos diferem somente por maiusculas/minusculas: ${path}`), { code: 'E_PATH' });
|
|
20
|
+
}
|
|
21
|
+
} catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; }
|
|
22
|
+
current = join(current, part);
|
|
23
|
+
let info;
|
|
24
|
+
try { info = lstatSync(current); }
|
|
25
|
+
catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; }
|
|
26
|
+
const reason = info.isSymbolicLink() ? 'symbolic-link' : index < parts.length - 1 && !info.isDirectory() ? 'non-directory' : null;
|
|
27
|
+
if (!reason) continue;
|
|
28
|
+
const boundary = parts.slice(0, index + 1).join('/');
|
|
29
|
+
if ([...ownedPaths].some(value => value === boundary || value.startsWith(`${boundary}/`))) {
|
|
30
|
+
throw Object.assign(new Error(`${reason === 'symbolic-link' ? 'Link simbolico' : 'Ancestral nao e diretorio'} em caminho gerenciado: ${boundary}`), { code: reason === 'symbolic-link' ? 'E_SYMLINK' : 'E_PATH' });
|
|
31
|
+
}
|
|
32
|
+
return { path: boundary, reason };
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
@@ -12,6 +12,9 @@ node --test .pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs
|
|
|
12
12
|
|
|
13
13
|
`--check` só confere; a execução normal atualiza os recursos. `--force` salva
|
|
14
14
|
conflitos em `.agent-tests/` antes de reparar os caminhos gerenciados.
|
|
15
|
+
Recursos nativos humanos e seus links permanecem intactos, inclusive com
|
|
16
|
+
`--force`. O retorno inclui `preserved`; esses caminhos não contam como
|
|
17
|
+
derivados pendentes nem passam a pertencer ao kit.
|
|
15
18
|
|
|
16
19
|
Metadados vêm da fonte ou do YAML nativo declarado no manifesto. Regras exigem
|
|
17
20
|
`trigger` válido; `glob` exige `globs`. Origens nativas precisam pertencer ao
|