@pathbit/agentdevkit 0.0.22 → 0.0.24
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/rules/ambiente-local/RULE.md +5 -0
- package/.pbai4devs/rules/git/RULE.md +5 -1
- package/.pbai4devs/scripts/README.md +1 -0
- package/.pbai4devs/scripts/inspecionar-projeto/README.md +6 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +218 -18
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +59 -11
- package/.pbai4devs/scripts/sincronizar-agentes/tests/CATALOG_OWNERSHIP.mjs +251 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +33 -13
- package/.pbai4devs/scripts/testar-maturidade/README.md +12 -3
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +65 -25
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +150 -5
- package/.pbai4devs/scripts/verificar-configuracao/README.md +22 -0
- package/.pbai4devs/scripts/verificar-configuracao/SCRIPT.mjs +63 -0
- package/.pbai4devs/scripts/verificar-configuracao/tests/TEST.mjs +145 -0
- package/.pbai4devs/skills/desenvolvimento/implementar-backend/SKILL.md +15 -3
- package/.pbai4devs/skills/desenvolvimento/implementar-frontend/SKILL.md +15 -3
- package/.pbai4devs/skills/git/git-diff/SKILL.md +3 -1
- package/.pbai4devs/skills/git/git-status/SKILL.md +2 -1
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +13 -8
- package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +4 -2
- package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +14 -2
- package/CHANGELOG.md +17 -0
- package/docs/guides/cli.md +9 -0
- package/docs/guides/troubleshooting.md +12 -6
- package/docs/specs/README.md +3 -0
- package/docs/specs/preimplementation-order/SPEC.md +23 -0
- package/docs/specs/target-paths/SPEC.md +19 -0
- package/package.json +1 -1
- package/src/i18n.mjs +3 -3
- package/src/install.mjs +121 -23
- package/src/next.mjs +26 -9
|
@@ -27,6 +27,11 @@ O Makefile deve oferecer `help`, `doctor`, `setup`, `up`, `down`, `logs`,
|
|
|
27
27
|
`test`, `lint` e `build`, com comandos reais da stack. Etapas ainda indefinidas
|
|
28
28
|
devem falhar com mensagem útil; não simule sucesso. Documente os comandos,
|
|
29
29
|
dependências e URL/porta no README; valide Compose e saúde antes de concluir.
|
|
30
|
+
Antes de existir aplicação, valide o runtime, o Compose e os serviços de
|
|
31
|
+
infraestrutura necessários. Build, saúde e fluxo da aplicação inexistente
|
|
32
|
+
permanecem pendentes; não crie código de negócio para simular essas provas. Se
|
|
33
|
+
uma dependência obrigatória estiver indisponível, mantenha a configuração
|
|
34
|
+
pendente e registre o impedimento. Após a implementação, prove a aplicação.
|
|
30
35
|
|
|
31
36
|
Use o [modelo de ambiente](../../templates/ambiente-local/TEMPLATE.md) para
|
|
32
37
|
perguntas e arquivos-base. Versões da aplicação seguem as decisões do projeto;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
trigger: model_decision
|
|
3
|
-
description: Aplique ao preparar, integrar ou publicar alterações Git.
|
|
3
|
+
description: Aplique ao consultar, preparar, integrar ou publicar alterações Git.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Regras Git
|
|
7
7
|
|
|
8
|
+
Em consultas sem alterações, defina `GIT_OPTIONAL_LOCKS=0` no ambiente do
|
|
9
|
+
subprocesso para evitar atualizações opcionais dos bytes do índice. Isso não
|
|
10
|
+
substitui as travas necessárias para operações que alteram o Git.
|
|
11
|
+
|
|
8
12
|
Use Conventional Commits (`tipo(escopo): descrição`, escopo opcional), inclusive
|
|
9
13
|
em checkpoints e merges; destaque mudanças incompatíveis com `!` ou
|
|
10
14
|
`BREAKING CHANGE:`. Autor e committer devem ser o usuário, nunca um agente;
|
|
@@ -26,6 +26,7 @@ node .pbai4devs/scripts/executar-script/SCRIPT.mjs diagnosticar-ambiente
|
|
|
26
26
|
| --- | --- |
|
|
27
27
|
| `diagnosticar-ambiente` | Runtimes, pacotes e navegadores disponíveis |
|
|
28
28
|
| `inspecionar-projeto` | Detecta manifests, comandos e estrutura |
|
|
29
|
+
| `verificar-configuracao` | Confere o estado antes de implementar código |
|
|
29
30
|
| `preparar-ferramentas` | Instala/atualiza ferramentas locais |
|
|
30
31
|
| `gerenciar-skills-externas` | Instala/check/read de skills curadas locais |
|
|
31
32
|
| `auditar-seguranca` | Scanners offline, bases locais e relatórios redigidos |
|
|
@@ -12,6 +12,10 @@ O JSON identifica tecnologia, comandos, estrutura e documentos existentes.
|
|
|
12
12
|
`documentation.files` mantém caminhos e caixa, papel provável, tamanho e hash
|
|
13
13
|
quando a leitura couber nos limites. Referências válidas do estado aparecem em
|
|
14
14
|
`configured`; nomes sugerem papéis, mas não comprovam conteúdo ou aprovação.
|
|
15
|
+
Na maturidade, o roadmap e as specs são consultados nos caminhos configurados;
|
|
16
|
+
status são declarações e um vínculo local não comprova aprovação ou revisão.
|
|
17
|
+
Uma lista de specs ambígua ou incompleta não autoriza escolher um incremento;
|
|
18
|
+
o estado do vínculo no roadmap também é apenas uma indicação para consulta.
|
|
15
19
|
|
|
16
20
|
Links simbólicos são listados como opacos, sem ler seus destinos. Conteúdo dos
|
|
17
21
|
documentos e valores de configuração não aparecem no relatório. `scan` e
|
|
@@ -20,6 +24,8 @@ condições. Uma pasta simbólica como `docs` também aparece, sem presumir que
|
|
|
20
24
|
esteja vazia. Links não classificados recebem `role: unclassified`; enquanto
|
|
21
25
|
existir uma fronteira não inspecionada, `scan.complete` será falso. Não use
|
|
22
26
|
inventário incompleto como prova de ausência de documentos.
|
|
27
|
+
O inspetor revalida a identidade dos caminhos durante a leitura; isso detecta
|
|
28
|
+
mudanças, mas não é uma garantia atômica contra toda troca simultânea.
|
|
23
29
|
|
|
24
30
|
`exclusions` descreve pastas de clientes, artefatos e caches fora do inventário.
|
|
25
31
|
Documentos humanos em `.github`, exemplos, wiki e RFCs são incluídos; os
|
|
@@ -41,12 +41,69 @@ const present = (root, name, directory) => {
|
|
|
41
41
|
const inside = candidate => { const relative = path.relative(repository, candidate); return relative !== '..' && !relative.startsWith('..' + path.sep) && !path.isAbsolute(relative); };
|
|
42
42
|
const fixturePath = candidate => /^(?:\.agent-tests\/|tmp\/examples\/|examples\/generated\/)/.test(normalize(path.relative(repository, candidate)));
|
|
43
43
|
|
|
44
|
+
// Rechecking is a conservative race detection, not an atomic openat traversal.
|
|
45
|
+
function pathIdentity(root, file = '') {
|
|
46
|
+
try {
|
|
47
|
+
let current = root; const result = [], parts = file ? file.split('/') : [];
|
|
48
|
+
for (const [index, part] of ['', ...parts].entries()) {
|
|
49
|
+
if (part) current = path.join(current, part);
|
|
50
|
+
const item = fs.lstatSync(current);
|
|
51
|
+
if (item.isSymbolicLink() || (index < parts.length && !item.isDirectory())) return null;
|
|
52
|
+
result.push({ dev: item.dev, ino: item.ino, mode: item.mode, size: item.size, directory: item.isDirectory() });
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
} catch { return null; }
|
|
56
|
+
}
|
|
57
|
+
const sameIdentity = (left, right) => left && right && left.length === right.length
|
|
58
|
+
&& left.every((item, index) => item.dev === right[index].dev && item.ino === right[index].ino && item.mode === right[index].mode);
|
|
59
|
+
|
|
60
|
+
function boundedEntries(folder, limit, verify = () => true) {
|
|
61
|
+
const handle = fs.opendirSync(folder), entries = [];
|
|
62
|
+
try {
|
|
63
|
+
if (!verify()) return { entries: [], truncated: true, changed: true };
|
|
64
|
+
while (true) {
|
|
65
|
+
const entry = handle.readSync();
|
|
66
|
+
if (!entry) return verify() ? { entries, truncated: false } : { entries: [], truncated: true, changed: true };
|
|
67
|
+
if (entries.length >= limit) return verify() ? { entries, truncated: true } : { entries: [], truncated: true, changed: true };
|
|
68
|
+
entries.push(entry);
|
|
69
|
+
}
|
|
70
|
+
} finally { handle.closeSync(); }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function stableDirectory(root, directory, limit) {
|
|
74
|
+
const identity = pathIdentity(root, directory);
|
|
75
|
+
if (!identity?.at(-1).directory) return { entries: [], truncated: true, changed: true, verify: () => false };
|
|
76
|
+
const verify = () => sameIdentity(identity, pathIdentity(root, directory));
|
|
77
|
+
return { ...boundedEntries(path.join(root, directory), limit, verify), verify };
|
|
78
|
+
}
|
|
79
|
+
|
|
44
80
|
function metadata(root, file) {
|
|
45
|
-
|
|
46
|
-
const
|
|
81
|
+
const empty = { fields: {}, documents: {}, text: null };
|
|
82
|
+
const identity = () => pathIdentity(root, file);
|
|
83
|
+
let descriptor, text;
|
|
84
|
+
try {
|
|
85
|
+
const name = path.join(root, file), ancestors = identity();
|
|
86
|
+
if (!ancestors || !sameIdentity(ancestors, identity())) return empty;
|
|
87
|
+
const before = ancestors.at(-1);
|
|
88
|
+
if (before.size > 1024 * 1024) return empty;
|
|
89
|
+
descriptor = fs.openSync(name, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
90
|
+
const opened = fs.fstatSync(descriptor);
|
|
91
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== before.size || !sameIdentity(ancestors, identity())) return empty;
|
|
92
|
+
const buffer = Buffer.alloc(opened.size + 1); let length = 0;
|
|
93
|
+
while (length < buffer.length) {
|
|
94
|
+
const count = fs.readSync(descriptor, buffer, length, buffer.length - length, length);
|
|
95
|
+
if (!count) break;
|
|
96
|
+
length += count;
|
|
97
|
+
}
|
|
98
|
+
const after = fs.fstatSync(descriptor);
|
|
99
|
+
if (length !== opened.size || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs || !sameIdentity(ancestors, identity())) return empty;
|
|
100
|
+
text = buffer.subarray(0, length).toString('utf8').replaceAll('\r', '');
|
|
101
|
+
} catch { return empty; }
|
|
102
|
+
finally { if (descriptor !== undefined) fs.closeSync(descriptor); }
|
|
47
103
|
const header = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1];
|
|
48
|
-
const result = { fields: {}, documents: {} }; if (!header) return result;
|
|
49
|
-
let documents = false;
|
|
104
|
+
const result = { fields: {}, documents: {}, text }; if (!header) return result;
|
|
105
|
+
let documents = false, invalidDocuments = false;
|
|
106
|
+
const seen = new Set(), duplicates = new Set(), seenDocuments = new Set(), duplicateDocuments = new Set();
|
|
50
107
|
const scalar = raw => {
|
|
51
108
|
const value = raw.trim();
|
|
52
109
|
if (!value || ['null', '~'].includes(value)) return null;
|
|
@@ -54,16 +111,131 @@ function metadata(root, file) {
|
|
|
54
111
|
return value.startsWith("'") && value.endsWith("'") ? value.slice(1, -1).replaceAll("''", "'") : value;
|
|
55
112
|
};
|
|
56
113
|
for (const line of header.split('\n')) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
114
|
+
if (!line.trim() || /^\s*#/.test(line)) continue;
|
|
115
|
+
const top = line.match(/^(?:([\p{L}_][\p{L}\p{N}_.-]*)|"([^"\n]+)"|'([^'\n]+)'):\s*(.*)$/u);
|
|
116
|
+
if (top) {
|
|
117
|
+
const key = top[1] || top[2] || top[3];
|
|
118
|
+
if (key.includes('\\')) return { ...empty, text };
|
|
119
|
+
if (seen.has(key)) duplicates.add(key);
|
|
120
|
+
seen.add(key); result.fields[key] = duplicates.has(key) ? null : scalar(top[4]);
|
|
121
|
+
if (key === 'documentos' && duplicates.has(key)) { invalidDocuments = true; result.documents = {}; }
|
|
122
|
+
documents = key === 'documentos' && !invalidDocuments && (!top[4].trim() || ['null', '~'].includes(top[4].trim()));
|
|
123
|
+
} else if (!/^\s/.test(line)) return { ...empty, text };
|
|
124
|
+
else if (documents) {
|
|
125
|
+
const item = line.match(/^ (?:([\p{L}_][\p{L}\p{N}_.-]*)|"([^"\n]+)"|'([^'\n]+)'):\s*(.*)$/u);
|
|
126
|
+
if (item) {
|
|
127
|
+
const key = item[1] || item[2] || item[3];
|
|
128
|
+
if (key.includes('\\')) { invalidDocuments = true; result.documents = {}; documents = false; continue; }
|
|
129
|
+
if (seenDocuments.has(key)) duplicateDocuments.add(key);
|
|
130
|
+
seenDocuments.add(key); result.documents[key] = duplicateDocuments.has(key) ? null : scalar(item[4]);
|
|
131
|
+
} else if (/^ \S/.test(line)) { invalidDocuments = true; result.documents = {}; documents = false; }
|
|
132
|
+
}
|
|
60
133
|
}
|
|
61
134
|
return result;
|
|
62
135
|
}
|
|
63
136
|
|
|
137
|
+
// The configured specs directory contains <slug>/SPEC.md. Refuse ambiguous
|
|
138
|
+
// layouts instead of guessing which increment or declared status to use.
|
|
139
|
+
function configuredSpecs(root, directory) {
|
|
140
|
+
const result = { path: directory, present: false, complete: false, items: [] };
|
|
141
|
+
if (!directory) return result;
|
|
142
|
+
let current = root;
|
|
143
|
+
for (const component of directory.split('/')) {
|
|
144
|
+
current = path.join(current, component);
|
|
145
|
+
try {
|
|
146
|
+
const info = fs.lstatSync(current);
|
|
147
|
+
if (info.isSymbolicLink() || !info.isDirectory()) return result;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error.code === 'ENOENT') result.complete = true;
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
result.present = true;
|
|
154
|
+
const incomplete = () => { result.items = []; return result; };
|
|
155
|
+
let scanned = 0;
|
|
156
|
+
function entriesAt(folder) {
|
|
157
|
+
const relative = normalize(path.relative(root, folder));
|
|
158
|
+
const result = stableDirectory(root, relative, Math.min(200, 500 - scanned));
|
|
159
|
+
scanned += result.entries.length;
|
|
160
|
+
return result.truncated ? null : result.entries;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const entries = entriesAt(current);
|
|
164
|
+
if (!entries) return incomplete();
|
|
165
|
+
for (const entry of entries) {
|
|
166
|
+
if (/^readme(?:\.md)?$/i.test(entry.name) && !entry.isSymbolicLink()) continue;
|
|
167
|
+
if (['.git', '.pbai4devs', '.agent-tests', 'node_modules'].includes(entry.name.toLowerCase())) return incomplete();
|
|
168
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) return incomplete();
|
|
169
|
+
const folder = `${directory}/${entry.name}`;
|
|
170
|
+
if (!present(root, folder, true)) return incomplete();
|
|
171
|
+
const children = entriesAt(path.join(root, folder));
|
|
172
|
+
if (!children) return incomplete();
|
|
173
|
+
const names = children.filter(item => /^spec\.md$/i.test(item.name));
|
|
174
|
+
if (names.length !== 1 || !names[0].isFile() || names[0].isSymbolicLink()) return incomplete();
|
|
175
|
+
const file = `${folder}/${names[0].name}`;
|
|
176
|
+
if (!present(root, file, false)) return incomplete();
|
|
177
|
+
const document = metadata(root, file);
|
|
178
|
+
if (document.text === null) return incomplete();
|
|
179
|
+
result.items.push({ path: file, status: ['rascunho', 'pendente', 'aprovado'].includes(document.fields.status) ? document.fields.status : null });
|
|
180
|
+
if (result.items.length > 1) return incomplete();
|
|
181
|
+
}
|
|
182
|
+
if (!present(root, directory, true)) return incomplete();
|
|
183
|
+
result.complete = true;
|
|
184
|
+
} catch { return incomplete(); }
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function referenceState(roadmap, roadmapFile, specFile) {
|
|
189
|
+
if (!roadmap || !roadmapFile) return { linked: false, roadmapState: 'unknown' };
|
|
190
|
+
// Examples and orphan reference definitions do not select an increment.
|
|
191
|
+
const lines = []; let fence = null;
|
|
192
|
+
const body = roadmap.replace(/^---\n[^]*?\n---(?:\n|$)/, '').replace(/<!--[^]*?-->/g, '');
|
|
193
|
+
for (const line of body.split('\n')) {
|
|
194
|
+
const marker = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
195
|
+
if (fence) {
|
|
196
|
+
if (marker && marker[1][0] === fence[0] && marker[1].length >= fence.length && !line.slice(marker[0].length).trim()) fence = null;
|
|
197
|
+
} else if (marker) fence = marker[1];
|
|
198
|
+
else lines.push(line.replace(/(`+)[^`\n]*?\1/g, ''));
|
|
199
|
+
}
|
|
200
|
+
const normalizeLabel = label => label.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
201
|
+
const definitions = new Map();
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
const match = line.match(/^ {0,3}\[([^\]]+)\]:[ \t]*(?:<([^>\n]+)>|(\S+))(?:[ \t]+(?:"[^"\n]*"|'[^'\n]*'))?[ \t]*$/);
|
|
204
|
+
if (match) { const label = normalizeLabel(match[1]); definitions.set(label, [...(definitions.get(label) || []), match[2] || match[3]]); }
|
|
205
|
+
}
|
|
206
|
+
const references = [], ancestry = [];
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
const list = line.match(/^([ \t]*)(?:[-+*]|\d+[.)])\s+(.*)$/);
|
|
209
|
+
const marker = list?.[2].match(/^\[([ xX-])\](?:\s|$)/);
|
|
210
|
+
const closed = /(?:^|[^\p{L}])(?:cancelad[ao]s?|conclu[ií]d[ao]s?|publicad[ao]s?|finalizad[ao]s?|descartad[ao]s?|cancelled|completed|published)(?=$|[^\p{L}])/iu.test(line);
|
|
211
|
+
let state = 'unknown';
|
|
212
|
+
if (list) {
|
|
213
|
+
const indent = list[1].length;
|
|
214
|
+
while (ancestry.length && ancestry.at(-1).indent >= indent) ancestry.pop();
|
|
215
|
+
const own = closed || marker && /x/i.test(marker[1]) ? 'closed' : marker?.[1] === ' ' ? 'open' : marker?.[1] === '-' ? 'in-progress' : 'unknown';
|
|
216
|
+
state = ancestry.some(item => item.state === 'closed') ? 'closed' : /\t/.test(list[1]) || ancestry.some(item => item.state === 'unknown') ? 'unknown' : own;
|
|
217
|
+
ancestry.push({ indent, state });
|
|
218
|
+
} else if (line.trim() && !/^\s/.test(line)) ancestry.length = 0;
|
|
219
|
+
for (const match of line.matchAll(/(?<![!\\])\[[^\]\n]+\]\(\s*(?:<([^>\n]+)>|([^\s)]+))(?:\s+(?:"[^"\n]*"|'[^'\n]*'))?\s*\)/g)) references.push({ destination: match[1] || match[2], state });
|
|
220
|
+
for (const match of line.matchAll(/(?<![!\\])\[([^\]\n]+)\]\[([^\]\n]*)\]/g)) {
|
|
221
|
+
const label = normalizeLabel(match[2] || match[1]);
|
|
222
|
+
if (definitions.get(label)?.length === 1) references.push({ destination: definitions.get(label)[0], state });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const matches = references.filter(({ destination }) => {
|
|
226
|
+
try {
|
|
227
|
+
const value = decodeURIComponent(destination).split(/[?#]/, 1)[0];
|
|
228
|
+
if (!value || /^(?:[a-z][a-z0-9+.-]*:|\/|\\)/i.test(value) || value.includes('\\')) return false;
|
|
229
|
+
return path.posix.resolve('/', path.posix.dirname(roadmapFile), value) === '/' + specFile;
|
|
230
|
+
} catch { return false; }
|
|
231
|
+
});
|
|
232
|
+
if (matches.length !== 1) return { linked: matches.length > 0, roadmapState: matches.length ? 'ambiguous' : 'unknown' };
|
|
233
|
+
return { linked: true, roadmapState: matches[0].state };
|
|
234
|
+
}
|
|
235
|
+
|
|
64
236
|
function maturity(root, report) {
|
|
65
237
|
const stateFile = `${stateDirectory(root)}/configuracao/ESTADO.md`; const state = metadata(root, stateFile);
|
|
66
|
-
const safeDocument = value => typeof value === 'string' && !path.isAbsolute(value) &&
|
|
238
|
+
const safeDocument = value => typeof value === 'string' && !path.isAbsolute(value) && !/[\\\x00-\x1f\x7f:]/.test(value) && !value.split('/').some(part => !part || ['.', '..', '.git'].includes(part.toLowerCase()));
|
|
67
239
|
const planning = {};
|
|
68
240
|
for (const name of ['prd', 'ux_ui', 'design']) {
|
|
69
241
|
const file = state.documents[name];
|
|
@@ -72,10 +244,22 @@ function maturity(root, report) {
|
|
|
72
244
|
planning[name] = { path: safeDocument(file) ? file : null, present: Boolean(exists), status: ['rascunho', 'aprovado', 'nao-aplicavel', 'pendente'].includes(status) ? status : null };
|
|
73
245
|
}
|
|
74
246
|
planning.ready = planning.prd.status === 'aprovado' && planning.design.status === 'aprovado' && ['aprovado', 'nao-aplicavel'].includes(planning.ux_ui.status);
|
|
75
|
-
|
|
247
|
+
const roadmapPath = safeDocument(state.documents.roadmap) && /\.md$/i.test(state.documents.roadmap) ? state.documents.roadmap : null;
|
|
248
|
+
const roadmapPresent = Boolean(roadmapPath && present(root, roadmapPath, false));
|
|
249
|
+
const roadmapDocument = roadmapPresent ? metadata(root, roadmapPath) : { fields: {}, text: null };
|
|
250
|
+
planning.roadmap = { path: roadmapPath, present: roadmapPresent, status: ['pendente', 'rascunho', 'planejado', 'aprovado'].includes(roadmapDocument.fields.status) ? roadmapDocument.fields.status : null };
|
|
251
|
+
const specsPath = safeDocument(state.documents.specs) ? state.documents.specs : null;
|
|
252
|
+
planning.specs = configuredSpecs(root, specsPath);
|
|
253
|
+
for (const spec of planning.specs.items) Object.assign(spec, referenceState(roadmapDocument.text, roadmapPath, spec.path));
|
|
254
|
+
let released = false, releasesComplete = true; const releases = state.documents.releases;
|
|
76
255
|
if (safeDocument(releases) && present(root, releases, true)) {
|
|
77
256
|
const visit = (directory, depth = 0) => {
|
|
78
|
-
|
|
257
|
+
let listing;
|
|
258
|
+
try { listing = stableDirectory(root, directory, 100); }
|
|
259
|
+
catch { releasesComplete = false; return; }
|
|
260
|
+
if (listing.truncated) releasesComplete = false;
|
|
261
|
+
for (const entry of listing.entries) {
|
|
262
|
+
if (!listing.verify()) { releasesComplete = false; return; }
|
|
79
263
|
const file = normalize(path.join(directory, entry.name));
|
|
80
264
|
if (entry.isSymbolicLink()) continue;
|
|
81
265
|
if (entry.isDirectory() && depth < 2) visit(file, depth + 1);
|
|
@@ -91,7 +275,7 @@ function maturity(root, report) {
|
|
|
91
275
|
if (planning.ready) level = 'planejado';
|
|
92
276
|
if (report.mode === 'aplicacao-detectada') level = 'implementacao';
|
|
93
277
|
if (report.mode === 'aplicacao-detectada' && planning.ready && report.contract.complete && configuration.configuracao_tecnica === 'concluido') level = released ? 'manutencao' : 'configurado';
|
|
94
|
-
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.'] };
|
|
278
|
+
return { level, planning, configuration, released, releasesComplete, signals: { application: report.mode === 'aplicacao-detectada', structure: report.contract.complete }, limits: ['Classificacao descritiva; status em arquivos nao substitui aprovacao, testes ou provas de funcionamento.'] };
|
|
95
279
|
}
|
|
96
280
|
|
|
97
281
|
const documentRoles = new Set(['readme', 'contribuicao', 'changelog', 'seguranca', 'adrs', 'prd', 'ux_ui', 'design', 'roadmap', 'releases', 'specs', 'tarefas']);
|
|
@@ -152,10 +336,12 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
152
336
|
}
|
|
153
337
|
let descriptor;
|
|
154
338
|
try {
|
|
339
|
+
const identity = pathIdentity(root, file);
|
|
340
|
+
if (!identity || !sameIdentity(identity, pathIdentity(root, file))) { warn(file, 'changed-during-read'); return; }
|
|
155
341
|
descriptor = fs.openSync(path.join(root, file), fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
156
342
|
const before = fs.fstatSync(descriptor);
|
|
157
343
|
if (!before.isFile() || before.dev !== expected.dev || before.ino !== expected.ino || before.size !== expected.size
|
|
158
|
-
|| before.mtimeMs !== expected.mtimeMs || before.ctimeMs !== expected.ctimeMs) { warn(file, 'changed-during-read'); return; }
|
|
344
|
+
|| before.mtimeMs !== expected.mtimeMs || before.ctimeMs !== expected.ctimeMs || !sameIdentity(identity, pathIdentity(root, file))) { warn(file, 'changed-during-read'); return; }
|
|
159
345
|
const buffer = Buffer.alloc(Math.min(before.size + 1, bounds.maxTotalBytes - scan.bytesRead));
|
|
160
346
|
let length = 0;
|
|
161
347
|
while (length < buffer.length) {
|
|
@@ -165,7 +351,7 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
165
351
|
scan.bytesRead += read;
|
|
166
352
|
}
|
|
167
353
|
const after = fs.fstatSync(descriptor);
|
|
168
|
-
if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs) {
|
|
354
|
+
if (length !== before.size || after.size !== before.size || after.mtimeMs !== before.mtimeMs || after.ctimeMs !== before.ctimeMs || !sameIdentity(identity, pathIdentity(root, file))) {
|
|
169
355
|
warn(file, 'changed-during-read'); return;
|
|
170
356
|
}
|
|
171
357
|
scan.bytesHashed += length;
|
|
@@ -194,8 +380,13 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
194
380
|
const found = locate(directory);
|
|
195
381
|
if (found.kind !== 'directory') { if (found.kind === 'symbolic-link') add(found.boundary, found, role); return; }
|
|
196
382
|
}
|
|
197
|
-
let children;
|
|
198
|
-
try {
|
|
383
|
+
let children, listing;
|
|
384
|
+
try {
|
|
385
|
+
listing = stableDirectory(root, directory, Math.max(0, bounds.maxEntries - scan.entries));
|
|
386
|
+
if (listing.changed) { scan.truncated = true; warn(directory || '.', 'changed-during-read'); return; }
|
|
387
|
+
if (listing.truncated) scan.truncated = true;
|
|
388
|
+
children = listing.entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
389
|
+
}
|
|
199
390
|
catch { warn(directory || '.', 'unreadable-directory'); return; }
|
|
200
391
|
// Discard a listing if the directory became a link while being inspected.
|
|
201
392
|
if (directory && locate(directory).kind !== 'directory') { warn(directory, 'changed-during-read'); return; }
|
|
@@ -204,6 +395,7 @@ export function inspectDocumentation(root, limits = {}) {
|
|
|
204
395
|
scan.entries++;
|
|
205
396
|
const file = directory ? `${directory}/${child.name}` : child.name;
|
|
206
397
|
if (excludedDocument(file)) continue;
|
|
398
|
+
if (!listing.verify()) { scan.truncated = true; warn(directory || '.', 'changed-during-read'); return; }
|
|
207
399
|
const found = locate(file);
|
|
208
400
|
if (found.kind === 'directory') {
|
|
209
401
|
if (depth < bounds.maxDepth) walk(file, depth + 1, role);
|
|
@@ -244,8 +436,11 @@ export function inspectProject(root = repository, { check = false, documentation
|
|
|
244
436
|
const warning = (file, reason) => report.warnings.push({ file, reason });
|
|
245
437
|
function commands(file, name) {
|
|
246
438
|
if (!['package.json', 'composer.json', 'deno.json', 'deno.jsonc', 'Makefile', 'makefile', 'GNUmakefile', 'pyproject.toml'].includes(name)) return;
|
|
247
|
-
|
|
248
|
-
|
|
439
|
+
const identity = pathIdentity(root, file);
|
|
440
|
+
if (!identity) { warning(file, 'manifest-nao-inspecionado'); return; }
|
|
441
|
+
if (identity.at(-1).size > 1024 * 1024) { warning(file, 'manifest-grande-demais'); return; }
|
|
442
|
+
const text = metadata(root, file).text;
|
|
443
|
+
if (text === null) { warning(file, 'manifest-nao-inspecionado'); return; }
|
|
249
444
|
let names = [];
|
|
250
445
|
try {
|
|
251
446
|
if (['package.json', 'composer.json', 'deno.json', 'deno.jsonc'].includes(name)) {
|
|
@@ -268,7 +463,10 @@ export function inspectProject(root = repository, { check = false, documentation
|
|
|
268
463
|
} catch { warning(file, 'manifest-nao-interpretado'); }
|
|
269
464
|
}
|
|
270
465
|
function walk(directory = '', depth = 0) {
|
|
271
|
-
const
|
|
466
|
+
const listing = stableDirectory(root, directory, Math.max(0, 25000 - entries));
|
|
467
|
+
if (listing.changed) { report.scan.truncated = true; warning(directory || '.', 'changed-during-read'); return; }
|
|
468
|
+
if (listing.truncated) report.scan.truncated = true;
|
|
469
|
+
const children = listing.entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
272
470
|
for (const item of children) {
|
|
273
471
|
if (++entries > 25000) { report.scan.truncated = true; return; }
|
|
274
472
|
const file = normalize(path.join(directory, item.name));
|
|
@@ -276,10 +474,12 @@ export function inspectProject(root = repository, { check = false, documentation
|
|
|
276
474
|
if (item.isSymbolicLink()) continue;
|
|
277
475
|
if (item.isDirectory()) {
|
|
278
476
|
if (excluded.has(item.name) || excluded.has(item.name.toLowerCase()) || /^\.?playwright/i.test(item.name)) continue;
|
|
477
|
+
if (!listing.verify()) { report.scan.truncated = true; warning(directory || '.', 'changed-during-read'); return; }
|
|
279
478
|
if (depth < report.scan.maxDepth) walk(file, depth + 1); else report.scan.truncated = true;
|
|
280
479
|
continue;
|
|
281
480
|
}
|
|
282
481
|
if (!item.isFile()) continue;
|
|
482
|
+
if (!listing.verify()) { report.scan.truncated = true; warning(directory || '.', 'changed-during-read'); return; }
|
|
283
483
|
let language = manifests.get(item.name);
|
|
284
484
|
if (/\.(?:csproj|fsproj|vbproj|sln)$/.test(item.name)) language = 'dotnet';
|
|
285
485
|
if (/^requirements(?:[.-][A-Za-z0-9_-]+)?\.txt$/.test(item.name)) language = 'python';
|
|
@@ -9,8 +9,8 @@ import { buildCatalog } from '../lib/catalog.mjs';
|
|
|
9
9
|
import { resourcePath } from '../lib/resource-path.mjs';
|
|
10
10
|
import { adapterManifest, resolveAdapters } from '../lib/adapters.mjs';
|
|
11
11
|
import { nativePathBoundary } from '../lib/native-paths.mjs';
|
|
12
|
-
import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, mergeManagedContent, stripManagedContent } from '../lib/managed-content.mjs';
|
|
13
|
-
import { mergeTomlContent, stripTomlContent } from '../lib/managed-toml.mjs';
|
|
12
|
+
import { canonicalEntrypoint, resolveEntrypoint, entrypointReference, renderEntrypointImports, extractManagedBlock, mergeManagedContent, stripManagedContent } from '../lib/managed-content.mjs';
|
|
13
|
+
import { extractTomlManagedBlock, mergeTomlContent, stripTomlContent } from '../lib/managed-toml.mjs';
|
|
14
14
|
import { mergeGitignore, validateIgnoreClients, renderClientIgnore, isGeneratedOutput, planLocalIgnore, applyLocalIgnore, checkLocalIgnore, assertLocalIgnoreUnchanged, planUntrack, ignoreBegin, ignoreEnd } from '../lib/gitignore.mjs';
|
|
15
15
|
import { stateDirectory } from '../lib/state-path.mjs';
|
|
16
16
|
|
|
@@ -90,7 +90,7 @@ function replaceJsonMember(bytes, member, value) {
|
|
|
90
90
|
// Compare owned fields structurally, preserving all other JSON bytes.
|
|
91
91
|
function derivedContent(original, previous, next) {
|
|
92
92
|
if (!original) return JSON.stringify(next, null, 2) + '\n';
|
|
93
|
-
const keys = ['schemaVersion', 'entries', 'outputs', 'files', 'blocks', 'removedBlocks', 'removedFiles'];
|
|
93
|
+
const keys = ['schemaVersion', 'entries', 'outputs', 'files', 'blocks', 'removedBlocks', 'removedFiles', 'catalogs', 'config'];
|
|
94
94
|
const canonical = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
95
95
|
? Object.fromEntries(Object.keys(value).sort(compare).map(key => [key, canonical(value[key])]))
|
|
96
96
|
: Array.isArray(value) ? value.map(canonical) : value;
|
|
@@ -247,6 +247,11 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
247
247
|
if (previousEntries?.schemaVersion !== 1 || !previousEntries.entries || typeof previousEntries.entries !== 'object' || Array.isArray(previousEntries.entries) || Object.entries(previousEntries.entries).some(([name, hash]) => !canonicalEntrypoint(name) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de entradas derivadas invalido.');
|
|
248
248
|
const previousOutputs = previousEntries.outputs ?? {};
|
|
249
249
|
if (!previousOutputs || typeof previousOutputs !== 'object' || Array.isArray(previousOutputs) || Object.entries(previousOutputs).some(([path, hash]) => !/^\.[A-Za-z0-9][A-Za-z0-9._-]*\/(skills|rules|agents)\.json$/.test(path) || reserved.has(path.split('/')[0].toLowerCase()) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de saidas derivadas invalido.');
|
|
250
|
+
const previousCatalogs = previousEntries.catalogs ?? {};
|
|
251
|
+
const catalogPaths = new Set(['.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/agents.json', '.pbai4devs/rules.json']);
|
|
252
|
+
if (!previousCatalogs || typeof previousCatalogs !== 'object' || Array.isArray(previousCatalogs) || Object.entries(previousCatalogs).some(([path, hash]) => !catalogPaths.has(path) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de catalogos derivados invalido.');
|
|
253
|
+
for (const path of Object.keys(previousCatalogs)) safe(path);
|
|
254
|
+
if (previousEntries.config !== undefined && (typeof previousEntries.config !== 'string' || !/^[a-f0-9]{64}$/.test(previousEntries.config))) fail('Registro de configuracao derivada invalido.');
|
|
250
255
|
const validResourcePath = path => {
|
|
251
256
|
if (typeof path !== 'string' || !/^\.[A-Za-z0-9][A-Za-z0-9._-]*\/.+/.test(path)) return false;
|
|
252
257
|
const [root, scope] = path.split('/');
|
|
@@ -254,7 +259,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
254
259
|
&& !['readme.md', 'instructions.md'].includes(posix.basename(path).toLowerCase())
|
|
255
260
|
&& (/\.(md|mdc)$/i.test(path) || path.startsWith('.gemini/commands/') && path.endsWith('.toml'));
|
|
256
261
|
};
|
|
257
|
-
const validIgnoreBlock = path => typeof path === 'string' && /^\.[A-Za-z0-9][A-Za-z0-9._-]*\/\.gitignore$/.test(path) && !reserved.has(path.split('/')[0].toLowerCase());
|
|
262
|
+
const validIgnoreBlock = path => path === '.pbai4devs/.gitignore' || typeof path === 'string' && /^\.[A-Za-z0-9][A-Za-z0-9._-]*\/\.gitignore$/.test(path) && !reserved.has(path.split('/')[0].toLowerCase());
|
|
258
263
|
for (const [name, values] of [['files', previousEntries.files ?? {}], ['blocks', previousEntries.blocks ?? {}]]) {
|
|
259
264
|
if (!values || typeof values !== 'object' || Array.isArray(values) || Object.entries(values).some(([path, hash]) => !(validResourcePath(path) || name === 'blocks' && (canonicalEntrypoint(path) || validIgnoreBlock(path))) || !/^[a-f0-9]{64}$/.test(hash))) fail(`Registro de ${name} derivados invalido.`);
|
|
260
265
|
for (const path of Object.keys(values)) safe(canonicalEntrypoint(path) ? resolveEntrypoint(root, canonicalEntrypoint(path)) : path);
|
|
@@ -475,11 +480,23 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
475
480
|
}
|
|
476
481
|
const registries = new Map();
|
|
477
482
|
const discoveryOutputs = {};
|
|
483
|
+
const catalogOutputs = {};
|
|
478
484
|
for (const [target, content] of built.outputs) {
|
|
479
485
|
const info = stat(safe(target));
|
|
480
486
|
if (info && !info.isFile()) fail(`Registro nao e arquivo: ${target}`);
|
|
481
487
|
const current = info ? readFileSync(absolute(target)) : null;
|
|
482
488
|
captureInput(target, current);
|
|
489
|
+
const currentHash = current && createHash('sha256').update(current).digest('hex');
|
|
490
|
+
const known = current && [installed?.files?.[target], previousCatalogs[target]].some(digest => digest && digest === currentHash);
|
|
491
|
+
if (current && !known) {
|
|
492
|
+
// Identical bytes do not establish ownership. An unowned catalogue can
|
|
493
|
+
// remain if already current, but it cannot be adopted or regenerated.
|
|
494
|
+
remember(target, 'existing-catalog');
|
|
495
|
+
if (current.toString('utf8') !== content) { conflicts.add(target); protectedConflicts.add(target); }
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
const nextHash = createHash('sha256').update(content).digest('hex');
|
|
499
|
+
if (installed?.files?.[target] !== nextHash) catalogOutputs[target] = nextHash;
|
|
483
500
|
if (!current || current.toString('utf8') !== content) {
|
|
484
501
|
registries.set(target, content);
|
|
485
502
|
if (force && info) backup.add(target);
|
|
@@ -533,10 +550,22 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
533
550
|
merged = mergeGitignore(current, patterns);
|
|
534
551
|
}
|
|
535
552
|
catch (error) { fail(`${target}: ${error.message}`); }
|
|
536
|
-
if (
|
|
537
|
-
const start =
|
|
538
|
-
|
|
553
|
+
if (merged.hasBlock) {
|
|
554
|
+
const start = current.indexOf(ignoreBegin), finish = current.indexOf(ignoreEnd);
|
|
555
|
+
const blockHash = start >= 0 && finish > start ? createHash('sha256').update(current.slice(start, finish + ignoreEnd.length)).digest('hex') : null;
|
|
556
|
+
const fileHash = createHash('sha256').update(current).digest('hex');
|
|
557
|
+
const known = [installed?.blocks?.[target], previousEntries.blocks?.[target]].some(hash => hash && hash === blockHash)
|
|
558
|
+
|| priorFiles[target] && priorFiles[target] === fileHash;
|
|
559
|
+
if (!known) {
|
|
560
|
+
// A marker or identical contents is not proof that this block belongs
|
|
561
|
+
// to the kit. Do not adopt, replace or remove it even with --force.
|
|
562
|
+
remember(target, 'existing-ignore-block');
|
|
563
|
+
if (merged.text !== current) { conflicts.add(target); protectedConflicts.add(target); }
|
|
564
|
+
continue;
|
|
565
|
+
}
|
|
539
566
|
}
|
|
567
|
+
const start = merged.text.indexOf(ignoreBegin), finish = merged.text.indexOf(ignoreEnd);
|
|
568
|
+
if (start >= 0 && finish > start) managedBlocks[target] = createHash('sha256').update(merged.text.slice(start, finish + ignoreEnd.length)).digest('hex');
|
|
540
569
|
if (merged.text !== current) {
|
|
541
570
|
ignoreFiles.set(target, merged.text);
|
|
542
571
|
if (force && info && merged.hasBlock) backup.add(target);
|
|
@@ -568,6 +597,10 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
568
597
|
if (!command) fail(`Comando Gemini exige COMMAND.md: ${source}`);
|
|
569
598
|
const current = mixed ? targetBytes : null;
|
|
570
599
|
const merged = mixed ? mergeTomlContent(current, `@{${source}}`, { expectedHash: priorBlocks[target] }) : null;
|
|
600
|
+
if (current && !priorBlocks[target] && priorFiles[target] !== createHash('sha256').update(current).digest('hex') && extractTomlManagedBlock(current)) {
|
|
601
|
+
// An identical block can remain, but its bytes are not ownership proof.
|
|
602
|
+
remember(target, 'existing-managed-block'); expected.set(target, current); continue;
|
|
603
|
+
}
|
|
571
604
|
const content = merged ? merged.text : `${tomlMarker}\ndescription = ${JSON.stringify(command.description)}\nprompt = ${JSON.stringify(`@{${source}}\n\n{{args}}`)}\n`;
|
|
572
605
|
if (mixed) { captureInput(target, current); managedBlocks[target] = merged.sha256; remember(target, 'existing-file-extended'); }
|
|
573
606
|
else rememberGenerated(target, content);
|
|
@@ -588,6 +621,9 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
588
621
|
const inclusion = ruleInclusion ? `@[origem](${link(target, source)})` : `Leia e siga [origem](${link(target, source)}).`;
|
|
589
622
|
const current = mixed ? targetBytes : null;
|
|
590
623
|
const merged = mixed ? mergeManagedContent(current, inclusion, { expectedHash: priorBlocks[target] }) : null;
|
|
624
|
+
if (current && !priorBlocks[target] && priorFiles[target] !== createHash('sha256').update(current).digest('hex') && extractManagedBlock(current)) {
|
|
625
|
+
remember(target, 'existing-managed-block'); expected.set(target, current); continue;
|
|
626
|
+
}
|
|
591
627
|
const content = merged ? merged.text : `${frontmatter}\n${title}\n\n${marker}\n${inclusion}\n`;
|
|
592
628
|
if (mixed) { captureInput(target, current); managedBlocks[target] = merged.sha256; remember(target, 'existing-file-extended'); }
|
|
593
629
|
else rememberGenerated(target, content);
|
|
@@ -644,6 +680,9 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
644
680
|
const previousBlockHash = priorBlocks[name] ?? Object.entries(priorBlocks).find(([path]) => canonicalEntrypoint(path) === canonical)?.[1];
|
|
645
681
|
if (current && (previousBlockHash || createHash('sha256').update(current).digest('hex') !== previousHash)) {
|
|
646
682
|
const merged = mergeManagedContent(current, entrypointReference(root, canonical), { expectedHash: previousBlockHash });
|
|
683
|
+
if (!previousBlockHash && previousHash !== createHash('sha256').update(current).digest('hex') && extractManagedBlock(current)) {
|
|
684
|
+
remember(name, 'existing-managed-block'); continue;
|
|
685
|
+
}
|
|
647
686
|
captureInput(name, current);
|
|
648
687
|
managedBlocks[name] = merged.sha256;
|
|
649
688
|
rootEntries.set(name, merged.text);
|
|
@@ -682,8 +721,20 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
682
721
|
removedBlocks.add(target);
|
|
683
722
|
remember(target, 'existing-file-extended');
|
|
684
723
|
}
|
|
724
|
+
const omittedRows = new Set([...legacy, ...missing.map(native => native.index)]);
|
|
725
|
+
const finalConfig = rows.filter((row, index) => !omittedRows.has(index)).join('\n').replace(/\n*$/, '\n');
|
|
726
|
+
const configChanged = !configExists || configText !== finalConfig;
|
|
727
|
+
const configHash = configExists ? createHash('sha256').update(readFileSync(absolute(config))).digest('hex') : null;
|
|
728
|
+
const configKnown = configExists && [installed?.files?.[config], previousEntries.config].some(hash => hash && hash === configHash);
|
|
729
|
+
if (configExists && !configKnown) {
|
|
730
|
+
remember(config, 'existing-configuration');
|
|
731
|
+
if (configChanged) { conflicts.add(config); protectedConflicts.add(config); }
|
|
732
|
+
}
|
|
685
733
|
const next = { schemaVersion: 1, entries: nextEntries };
|
|
686
734
|
if (Object.keys(discoveryOutputs).length) next.outputs = discoveryOutputs;
|
|
735
|
+
if (Object.keys(catalogOutputs).length) next.catalogs = catalogOutputs;
|
|
736
|
+
const nextConfigHash = createHash('sha256').update(finalConfig).digest('hex');
|
|
737
|
+
if ((!configExists || configKnown) && installed?.files?.[config] !== nextConfigHash) next.config = nextConfigHash;
|
|
687
738
|
if (Object.keys(generatedFiles).length) next.files = generatedFiles;
|
|
688
739
|
const derivedBlocks = Object.fromEntries(Object.entries(managedBlocks).filter(([path, hash]) => installed?.blocks?.[path] !== hash || Object.hasOwn(previousEntries.blocks ?? {}, path)));
|
|
689
740
|
if (Object.keys(derivedBlocks).length) next.blocks = derivedBlocks;
|
|
@@ -693,7 +744,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
693
744
|
for (const path of remove) if (validResourcePath(path) && installed?.files?.[path] && !expected.has(path)) effectiveRemovedFiles[path] = installed.files[path];
|
|
694
745
|
if (Object.keys(effectiveRemovedFiles).length) next.removedFiles = Object.fromEntries(Object.entries(effectiveRemovedFiles).sort(([a], [b]) => compare(a, b)));
|
|
695
746
|
const registryContent = derivedContent(registryBytes, previousEntries, next);
|
|
696
|
-
if (recordDerivedState && (Object.keys(nextEntries).length || Object.keys(discoveryOutputs).length || Object.keys(generatedFiles).length || Object.keys(derivedBlocks).length || removedBlocks.size || Object.keys(effectiveRemovedFiles).length || stat(safe(entryRegistry))) && (!stat(safe(entryRegistry)) || readFileSync(absolute(entryRegistry), 'utf8') !== registryContent)) registries.set(entryRegistry, registryContent);
|
|
747
|
+
if (recordDerivedState && (Object.keys(nextEntries).length || Object.keys(discoveryOutputs).length || Object.keys(catalogOutputs).length || next.config || Object.keys(generatedFiles).length || Object.keys(derivedBlocks).length || removedBlocks.size || Object.keys(effectiveRemovedFiles).length || stat(safe(entryRegistry))) && (!stat(safe(entryRegistry)) || readFileSync(absolute(entryRegistry), 'utf8') !== registryContent)) registries.set(entryRegistry, registryContent);
|
|
697
748
|
const ownedOutputs = sorted([...targets, ...built.outputs.keys(), '.pbai4devs/espelhos.conf', ...rootEntries.keys(), ...(recordDerivedState && (stat(safe(entryRegistry)) || registries.has(entryRegistry)) ? [entryRegistry] : []),
|
|
698
749
|
...[...discoveries].flatMap(agent => ['skills', 'rules', 'agents'].map(kind => `${agent}/${kind}.json`)),
|
|
699
750
|
...[...ignoreTemplates.keys()].filter(target => {
|
|
@@ -709,9 +760,6 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
709
760
|
const removals = topPaths(remove);
|
|
710
761
|
const backups = topPaths(backup);
|
|
711
762
|
const rootEntryChanges = [...rootEntries].filter(([name, content]) => !stat(absolute(name)) || readFileSync(absolute(name), 'utf8') !== content);
|
|
712
|
-
const omittedRows = new Set([...legacy, ...missing.map(native => native.index)]);
|
|
713
|
-
const finalConfig = rows.filter((row, index) => !omittedRows.has(index)).join('\n').replace(/\n*$/, '\n');
|
|
714
|
-
const configChanged = !configExists || configText !== finalConfig;
|
|
715
763
|
// Every planned write/removal may cover a different staged version. Compare
|
|
716
764
|
// the worktree bytes with the index before touching any path, even in force
|
|
717
765
|
// mode, and repeat immediately before applying the transaction.
|