@pathbit/agentdevkit 0.0.21 → 0.0.23
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 +11 -1
- 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/diagnosticar-ambiente/tests/TEST.mjs +60 -1
- package/.pbai4devs/scripts/inspecionar-projeto/README.md +6 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +218 -18
- package/.pbai4devs/scripts/lib/adapters.mjs +5 -2
- package/.pbai4devs/scripts/lib/runtime.mjs +38 -2
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +5 -1
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +136 -9
- package/.pbai4devs/scripts/sincronizar-agentes/tests/HUMAN_OWNERSHIP.mjs +30 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +22 -0
- package/.pbai4devs/scripts/testar-maturidade/README.md +12 -10
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +86 -44
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +157 -33
- 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/scripts/verificar-recursos/SCRIPT.mjs +12 -1
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +22 -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/.pbai4devs/templates/clients/ADAPTERS.json +3 -2
- package/CHANGELOG.md +20 -0
- package/docs/guides/cli.md +9 -0
- package/docs/guides/troubleshooting.md +4 -0
- package/docs/specs/README.md +5 -0
- package/docs/specs/discovery-preservation/SPEC.md +26 -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/install.mjs +155 -17
- package/src/next.mjs +26 -9
|
@@ -21,7 +21,7 @@ const auxiliary = ['templates', 'scripts', 'tools', 'external-skills', 'state',
|
|
|
21
21
|
const reserved = new Set(['.git', '.pbai4devs', '.agent-tests']);
|
|
22
22
|
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
23
23
|
const sorted = values => [...new Set(values)].sort(compare);
|
|
24
|
-
const layouts = new Set(['agrupado', 'plano', 'arquivo', 'mdc', 'instrucoes', 'prompt', 'agente']);
|
|
24
|
+
const layouts = new Set(['agrupado', 'plano', 'arquivo', 'mdc', 'instrucoes', 'prompt', 'agente', 'nenhum']);
|
|
25
25
|
const fail = message => { throw new Error(message); };
|
|
26
26
|
const normalize = text => text.replace(/\r\n?/g, '\n');
|
|
27
27
|
const read = file => normalize(readFileSync(file, 'utf8'));
|
|
@@ -30,6 +30,77 @@ const stat = file => {
|
|
|
30
30
|
catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; }
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
function replaceJsonMember(bytes, member, value) {
|
|
34
|
+
const text = bytes.toString('utf8');
|
|
35
|
+
if (!Buffer.from(text, 'utf8').equals(bytes)) fail('Invalid UTF-8 in derived state.');
|
|
36
|
+
let i = 0;
|
|
37
|
+
const space = () => { while (/\s/.test(text[i] || '') && i < text.length) i++; };
|
|
38
|
+
const stringEnd = () => {
|
|
39
|
+
if (text[i++] !== '"') fail('Invalid JSON member.');
|
|
40
|
+
while (i < text.length) {
|
|
41
|
+
const char = text[i++];
|
|
42
|
+
if (char === '\\') { i++; continue; }
|
|
43
|
+
if (char === '"') return;
|
|
44
|
+
}
|
|
45
|
+
fail('Unterminated JSON string.');
|
|
46
|
+
};
|
|
47
|
+
space(); if (text[i++] !== '{') fail('Invalid derived state.');
|
|
48
|
+
const properties = [];
|
|
49
|
+
while (true) {
|
|
50
|
+
space(); if (text[i] === '}') break;
|
|
51
|
+
const start = i; stringEnd(); const key = JSON.parse(text.slice(start, i));
|
|
52
|
+
space(); if (text[i++] !== ':') fail('Invalid JSON member.');
|
|
53
|
+
space(); const valueStart = i;
|
|
54
|
+
if (text[i] === '"') stringEnd();
|
|
55
|
+
else if (text[i] === '{' || text[i] === '[') {
|
|
56
|
+
const stack = [];
|
|
57
|
+
do {
|
|
58
|
+
const char = text[i];
|
|
59
|
+
if (char === '"') { stringEnd(); continue; }
|
|
60
|
+
i++;
|
|
61
|
+
if (char === '{' || char === '[') stack.push(char);
|
|
62
|
+
else if (char === '}' || char === ']') stack.pop();
|
|
63
|
+
} while (stack.length && i < text.length);
|
|
64
|
+
if (stack.length) fail('Invalid derived state.');
|
|
65
|
+
} else { while (i < text.length && text[i] !== ',' && text[i] !== '}') i++; while (i > valueStart && /\s/.test(text[i - 1])) i--; }
|
|
66
|
+
const end = i; properties.push({ key, start, valueStart, end });
|
|
67
|
+
space(); if (text[i] === ',') { i++; continue; } if (text[i] === '}') break;
|
|
68
|
+
fail('Invalid JSON member separator.');
|
|
69
|
+
}
|
|
70
|
+
const matches = properties.filter(item => item.key === member);
|
|
71
|
+
if (matches.length > 1) fail('Ambiguous derived state member.');
|
|
72
|
+
if (!matches.length) {
|
|
73
|
+
if (value === undefined) return bytes;
|
|
74
|
+
const insertion = `${properties.length ? ',' : ''}\n ${JSON.stringify(member)}: ${JSON.stringify(value, null, 2)}\n`;
|
|
75
|
+
const next = text.slice(0, i) + insertion + text.slice(i);
|
|
76
|
+
try { JSON.parse(next); } catch { fail('Unable to preserve derived state safely.'); }
|
|
77
|
+
return Buffer.from(next);
|
|
78
|
+
}
|
|
79
|
+
const current = matches[0], index = properties.indexOf(current);
|
|
80
|
+
let next;
|
|
81
|
+
if (value !== undefined) next = text.slice(0, current.valueStart) + JSON.stringify(value, null, 2) + text.slice(current.end);
|
|
82
|
+
else if (index > 0) next = text.slice(0, properties[index - 1].end) + text.slice(current.end);
|
|
83
|
+
else if (properties.length > 1) next = text.slice(0, current.start) + text.slice(properties[1].start);
|
|
84
|
+
else next = text.slice(0, current.start) + text.slice(current.end);
|
|
85
|
+
try { JSON.parse(next); } catch { fail('Unable to preserve derived state safely.'); }
|
|
86
|
+
return Buffer.from(next);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
// Compare owned fields structurally, preserving all other JSON bytes.
|
|
91
|
+
function derivedContent(original, previous, next) {
|
|
92
|
+
if (!original) return JSON.stringify(next, null, 2) + '\n';
|
|
93
|
+
const keys = ['schemaVersion', 'entries', 'outputs', 'files', 'blocks', 'removedBlocks', 'removedFiles'];
|
|
94
|
+
const canonical = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
95
|
+
? Object.fromEntries(Object.keys(value).sort(compare).map(key => [key, canonical(value[key])]))
|
|
96
|
+
: Array.isArray(value) ? value.map(canonical) : value;
|
|
97
|
+
let result = original;
|
|
98
|
+
for (const key of keys) {
|
|
99
|
+
if (JSON.stringify(canonical(previous[key])) !== JSON.stringify(canonical(next[key]))) result = replaceJsonMember(result, key, next[key]);
|
|
100
|
+
}
|
|
101
|
+
return result.toString('utf8');
|
|
102
|
+
}
|
|
103
|
+
|
|
33
104
|
export function header(text) {
|
|
34
105
|
const lines = normalize(text).split('\n');
|
|
35
106
|
if (lines[0] !== '---') fail('Frontmatter YAML ausente.');
|
|
@@ -76,7 +147,7 @@ function link(target, source) {
|
|
|
76
147
|
|
|
77
148
|
export { resourcePath } from '../lib/resource-path.mjs';
|
|
78
149
|
|
|
79
|
-
export function synchronize({ root = projectRoot, check = false, force = false, localIgnores = true, generateEntrypoints = true, recordDerivedState = true, output = console.log } = {}) {
|
|
150
|
+
export function synchronize({ root = projectRoot, check = false, force = false, localIgnores = true, generateEntrypoints = true, recordDerivedState = true, outputBlocked, output = console.log } = {}) {
|
|
80
151
|
requireNode();
|
|
81
152
|
root = realpathSync(resolve(root));
|
|
82
153
|
const absolute = value => join(root, ...value.split('/'));
|
|
@@ -156,6 +227,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
156
227
|
const protectedPath = value => /^(workflows(?:\/|$)|settings\.json$|extensions\.json$)/.test(value.slice(value.indexOf('/') + 1).toLowerCase());
|
|
157
228
|
const agents = new Map();
|
|
158
229
|
const formats = new Map();
|
|
230
|
+
const fallbacks = new Map();
|
|
159
231
|
const discoveries = new Set();
|
|
160
232
|
const ignoreRoots = new Set();
|
|
161
233
|
const natives = [];
|
|
@@ -182,14 +254,20 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
182
254
|
&& !['readme.md', 'instructions.md'].includes(posix.basename(path).toLowerCase())
|
|
183
255
|
&& (/\.(md|mdc)$/i.test(path) || path.startsWith('.gemini/commands/') && path.endsWith('.toml'));
|
|
184
256
|
};
|
|
257
|
+
const validIgnoreBlock = path => typeof path === 'string' && /^\.[A-Za-z0-9][A-Za-z0-9._-]*\/\.gitignore$/.test(path) && !reserved.has(path.split('/')[0].toLowerCase());
|
|
185
258
|
for (const [name, values] of [['files', previousEntries.files ?? {}], ['blocks', previousEntries.blocks ?? {}]]) {
|
|
186
|
-
if (!values || typeof values !== 'object' || Array.isArray(values) || Object.entries(values).some(([path, hash]) => !(validResourcePath(path) || name === 'blocks' && canonicalEntrypoint(path)) || !/^[a-f0-9]{64}$/.test(hash))) fail(`Registro de ${name} derivados invalido.`);
|
|
259
|
+
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.`);
|
|
187
260
|
for (const path of Object.keys(values)) safe(canonicalEntrypoint(path) ? resolveEntrypoint(root, canonicalEntrypoint(path)) : path);
|
|
188
261
|
}
|
|
189
262
|
const removedBlocks = new Set(previousEntries.removedBlocks ?? []);
|
|
190
263
|
if (!Array.isArray(previousEntries.removedBlocks ?? []) || [...removedBlocks].some(path => typeof path !== 'string' || !(validResourcePath(path) || canonicalEntrypoint(path)))) fail('Registro de blocos removidos invalido.');
|
|
191
264
|
for (const path of removedBlocks) safe(canonicalEntrypoint(path) ? resolveEntrypoint(root, canonicalEntrypoint(path)) : path);
|
|
192
|
-
const
|
|
265
|
+
const removedFiles = previousEntries.removedFiles ?? {};
|
|
266
|
+
if (!removedFiles || typeof removedFiles !== 'object' || Array.isArray(removedFiles) || Object.entries(removedFiles).some(([path, hash]) => !validResourcePath(path) || !/^[a-f0-9]{64}$/.test(hash))) fail('Registro de arquivos removidos invalido.');
|
|
267
|
+
if (Object.entries(removedFiles).some(([path, hash]) => installed?.files?.[path] && installed.files[path] !== hash)) fail('Registro de arquivos removidos diverge do estado instalado.');
|
|
268
|
+
for (const path of Object.keys(removedFiles)) safe(path);
|
|
269
|
+
const effectiveRemovedFiles = Object.fromEntries(Object.entries(removedFiles).filter(([path, hash]) => installed?.files?.[path] === hash));
|
|
270
|
+
const priorFiles = { ...Object.fromEntries(Object.entries(installed?.files ?? {}).filter(([path, hash]) => effectiveRemovedFiles[path] !== hash)), ...previousEntries.files };
|
|
193
271
|
const priorBlocks = { ...Object.fromEntries(Object.entries(installed?.blocks ?? {}).filter(([path]) => !removedBlocks.has(path))), ...previousEntries.blocks };
|
|
194
272
|
const ownedPaths = new Set([...Object.keys(priorFiles), ...Object.keys(priorBlocks), ...Object.keys(previousEntries.entries), ...Object.keys(previousOutputs)]);
|
|
195
273
|
const nativeRoots = new Set(rows.map(row => row.split('|')).filter(([type]) => ['agente', 'gitignore'].includes(type)).map(([, target]) => target));
|
|
@@ -200,6 +278,28 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
200
278
|
const boundary = nativePathBoundary(root, value, ownedPaths, { preserveRootAliases: true });
|
|
201
279
|
return boundary ? remember(boundary.path, boundary.reason) : false;
|
|
202
280
|
}
|
|
281
|
+
const fallbackChecks = new Map();
|
|
282
|
+
function blockedOutput(path) {
|
|
283
|
+
if (outputBlocked) return outputBlocked(path);
|
|
284
|
+
let boundary = nativePathBoundary(root, path, ownedPaths, { preserveRootAliases: true });
|
|
285
|
+
if (!boundary) {
|
|
286
|
+
const info = stat(safe(path));
|
|
287
|
+
if (info && !info.isFile()) boundary = { path, reason: 'non-file' };
|
|
288
|
+
}
|
|
289
|
+
if (!boundary) return { blocked: false };
|
|
290
|
+
remember(boundary.path, boundary.reason);
|
|
291
|
+
const info = lstatSync(absolute(boundary.path));
|
|
292
|
+
return { blocked: true, ...boundary, identity: [info.dev, info.ino, info.mode, info.size, info.mtimeMs, info.ctimeMs] };
|
|
293
|
+
}
|
|
294
|
+
function unavailable(path) {
|
|
295
|
+
const status = blockedOutput(path);
|
|
296
|
+
if (fallbackChecks.has(path) && JSON.stringify(fallbackChecks.get(path)) !== JSON.stringify(status)) fail(`Caminho de skill mudou durante o planejamento: ${path}`);
|
|
297
|
+
fallbackChecks.set(path, status);
|
|
298
|
+
return status.blocked;
|
|
299
|
+
}
|
|
300
|
+
function assertFallbackChecks() {
|
|
301
|
+
for (const [path, status] of fallbackChecks) if (JSON.stringify(blockedOutput(path)) !== JSON.stringify(status)) fail(`Caminho de skill mudou apos o planejamento: ${path}`);
|
|
302
|
+
}
|
|
203
303
|
function claimed(value) {
|
|
204
304
|
if (ownedPaths.has(value)) return true;
|
|
205
305
|
const info = stat(safe(value));
|
|
@@ -241,7 +341,8 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
241
341
|
agents.set(target, mode);
|
|
242
342
|
} else if (type === 'formato') {
|
|
243
343
|
if (fields.length !== 4 || !Object.hasOwn(principals, source) || !layouts.has(metadata)) fail(`Formato invalido: ${row}`);
|
|
244
|
-
if (source === 'skills' && !['agrupado', 'plano'].includes(metadata)) fail(`Skills exigem formato agrupado ou
|
|
344
|
+
if (source === 'skills' && !['agrupado', 'plano', 'nenhum'].includes(metadata)) fail(`Skills exigem formato agrupado, plano ou nenhum: ${row}`);
|
|
345
|
+
if (metadata === 'nenhum' && source !== 'skills') fail(`Formato incompativel com recurso: ${row}`);
|
|
245
346
|
if ((metadata === 'mdc' || metadata === 'instrucoes') && source !== 'rules' || metadata === 'prompt' && source !== 'commands' || metadata === 'agente' && source !== 'agents') fail(`Formato incompativel com recurso: ${row}`);
|
|
246
347
|
const key = `${target}/${source}`;
|
|
247
348
|
if (formats.has(key)) fail(`Formato duplicado: ${key}`);
|
|
@@ -249,6 +350,9 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
249
350
|
} else if (type === 'descoberta') {
|
|
250
351
|
if (fields.length !== 3 || target !== '.agents' || source !== 'antigravity' || discoveries.has(target)) fail(`Descoberta invalida: ${row}`);
|
|
251
352
|
discoveries.add(target);
|
|
353
|
+
} else if (type === 'fallback') {
|
|
354
|
+
if (fields.length !== 4 || metadata !== 'skills' || target === source || fallbacks.has(target)) fail(`Fallback invalido: ${row}`);
|
|
355
|
+
fallbacks.set(target, source);
|
|
252
356
|
} else if (type === 'gitignore') {
|
|
253
357
|
if (fields.length !== 2) fail('gitignore aceita somente a pasta de destino.');
|
|
254
358
|
ignoreRoot(target);
|
|
@@ -269,6 +373,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
269
373
|
}
|
|
270
374
|
if (!agents.size) fail('Nenhum agente cadastrado.');
|
|
271
375
|
for (const key of formats.keys()) if (!agents.has(key.split('/')[0])) fail(`Formato fora dos agentes: ${key}`);
|
|
376
|
+
for (const [target, source] of fallbacks) if (!agents.has(target) || !agents.has(source) || formats.get(`${target}/skills`) !== 'nenhum' || formats.get(`${source}/skills`) === 'nenhum') fail(`Fallback fora dos agentes: ${target}`);
|
|
272
377
|
for (const root of discoveries) if (!agents.has(root)) fail(`Descoberta fora dos agentes: ${root}`);
|
|
273
378
|
const managed = value => [...agents.keys()].some(agent => value.startsWith(agent + '/'));
|
|
274
379
|
const built = buildCatalog(root);
|
|
@@ -287,7 +392,19 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
287
392
|
}
|
|
288
393
|
for (const [source, kind] of sources) {
|
|
289
394
|
for (const [agent, mode] of agents) {
|
|
290
|
-
|
|
395
|
+
const format = formats.get(`${agent}/${kind}`) || mode;
|
|
396
|
+
if (format === 'nenhum') {
|
|
397
|
+
const primary = kind === 'skills' && fallbacks.get(agent);
|
|
398
|
+
if (!primary) continue;
|
|
399
|
+
const primaryFormat = formats.get(`${primary}/${kind}`) || agents.get(primary);
|
|
400
|
+
const primaryTarget = `${primary}/${resourcePath(source, kind, primaryFormat)}`;
|
|
401
|
+
if (!unavailable(primaryTarget)) continue;
|
|
402
|
+
const target = `${agent}/${resourcePath(source, kind, mode)}`;
|
|
403
|
+
if (unavailable(target)) fail(`Skill indisponivel: ${source}; caminhos ${primaryTarget} e ${target} obstruidos.`);
|
|
404
|
+
add(target, source);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
let path = resourcePath(source, kind, format);
|
|
291
408
|
if (agent === '.gemini' && kind === 'commands') path = path.replace(/\.md$/, '.toml');
|
|
292
409
|
add(`${agent}/${path}`, source);
|
|
293
410
|
}
|
|
@@ -385,7 +502,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
385
502
|
const unchanged = current === null || priorHashes.includes(createHash('sha256').update(current).digest('hex'));
|
|
386
503
|
if (!unchanged) { remember(manifest, 'edited-generated-file'); continue; }
|
|
387
504
|
const nextHash = createHash('sha256').update(content).digest('hex');
|
|
388
|
-
if (installed?.files?.[manifest] !== nextHash) discoveryOutputs[manifest] = nextHash;
|
|
505
|
+
if (installed?.files?.[manifest] !== nextHash || Object.hasOwn(previousOutputs, manifest)) discoveryOutputs[manifest] = nextHash;
|
|
389
506
|
if (!info || readFileSync(absolute(manifest), 'utf8') !== content) {
|
|
390
507
|
if (info && force) backup.add(manifest);
|
|
391
508
|
registries.set(manifest, content);
|
|
@@ -416,6 +533,10 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
416
533
|
merged = mergeGitignore(current, patterns);
|
|
417
534
|
}
|
|
418
535
|
catch (error) { fail(`${target}: ${error.message}`); }
|
|
536
|
+
if (target !== '.pbai4devs/.gitignore') {
|
|
537
|
+
const start = merged.text.indexOf(ignoreBegin), finish = merged.text.indexOf(ignoreEnd);
|
|
538
|
+
if (start >= 0 && finish > start) managedBlocks[target] = createHash('sha256').update(merged.text.slice(start, finish + ignoreEnd.length)).digest('hex');
|
|
539
|
+
}
|
|
419
540
|
if (merged.text !== current) {
|
|
420
541
|
ignoreFiles.set(target, merged.text);
|
|
421
542
|
if (force && info && merged.hasBlock) backup.add(target);
|
|
@@ -568,8 +689,11 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
568
689
|
if (Object.keys(derivedBlocks).length) next.blocks = derivedBlocks;
|
|
569
690
|
for (const path of Object.keys(managedBlocks)) removedBlocks.delete(path);
|
|
570
691
|
if (removedBlocks.size) next.removedBlocks = sorted(removedBlocks);
|
|
571
|
-
const
|
|
572
|
-
|
|
692
|
+
for (const path of expected.keys()) delete effectiveRemovedFiles[path];
|
|
693
|
+
for (const path of remove) if (validResourcePath(path) && installed?.files?.[path] && !expected.has(path)) effectiveRemovedFiles[path] = installed.files[path];
|
|
694
|
+
if (Object.keys(effectiveRemovedFiles).length) next.removedFiles = Object.fromEntries(Object.entries(effectiveRemovedFiles).sort(([a], [b]) => compare(a, b)));
|
|
695
|
+
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);
|
|
573
697
|
const ownedOutputs = sorted([...targets, ...built.outputs.keys(), '.pbai4devs/espelhos.conf', ...rootEntries.keys(), ...(recordDerivedState && (stat(safe(entryRegistry)) || registries.has(entryRegistry)) ? [entryRegistry] : []),
|
|
574
698
|
...[...discoveries].flatMap(agent => ['skills', 'rules', 'agents'].map(kind => `${agent}/${kind}.json`)),
|
|
575
699
|
...[...ignoreTemplates.keys()].filter(target => {
|
|
@@ -633,6 +757,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
633
757
|
const summary = { changed: changed.length + rootEntryChanges.length, stale: removals.length, conflicts: conflicts.size, missing: missing.length, legacy: legacy.length, registries: registries.size, gitignores: ignoreFiles.size, manifests: configExists ? 0 : 1, localIgnores: localStatus.changed + localStatus.missing.length + localStatus.trackedOutputs.length + (localStatus.status === 'no-git' ? 1 : 0) };
|
|
634
758
|
const dirty = Object.values(summary).some(Boolean);
|
|
635
759
|
output(`${summary.changed} desatualizadas; ${summary.stale} obsoletas; ${summary.conflicts} conflitos; ${summary.missing} origens nativas ausentes; ${summary.legacy} documentos antigos; ${summary.registries} indices derivados; ${summary.gitignores} gitignores; ${summary.localIgnores} exclusoes locais.`);
|
|
760
|
+
assertFallbackChecks();
|
|
636
761
|
if (localStatus.status === 'no-git') output('Git ausente: apos git init, execute a sincronizacao para proteger os arquivos locais.');
|
|
637
762
|
const preservedPaths = sorted(preserved.keys()).map(path => preserved.get(path));
|
|
638
763
|
if (check) return { ...summary, ownedOutputs, managedBlocks, preserved: preservedPaths, exitCode: dirty ? 1 : 0 };
|
|
@@ -700,6 +825,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
700
825
|
chmodSync(safe(value), content.mode);
|
|
701
826
|
}
|
|
702
827
|
try {
|
|
828
|
+
assertFallbackChecks();
|
|
703
829
|
if (localPlan) assertLocalIgnoreUnchanged(localPlan);
|
|
704
830
|
if (Object.keys(affectedHashes).length) {
|
|
705
831
|
if (!assertGitReadableForChanges() && hadGit) fail('Git deixou de estar disponivel antes da sincronizacao; arquivos preservados.');
|
|
@@ -712,6 +838,7 @@ export function synchronize({ root = projectRoot, check = false, force = false,
|
|
|
712
838
|
for (const [value, text] of ignoreFiles) mutate(value, () => atomic(value, text), text);
|
|
713
839
|
for (const [value, text] of rootEntryChanges) mutate(value, () => atomic(value, text), text);
|
|
714
840
|
if (configChanged) mutate(config, () => atomic(config, finalConfig), finalConfig);
|
|
841
|
+
assertFallbackChecks();
|
|
715
842
|
// Directory ownership is not recorded: keep even empty parent directories.
|
|
716
843
|
if (localPlan) applyLocalIgnore(localPlan);
|
|
717
844
|
} catch (error) {
|
|
@@ -122,6 +122,36 @@ test('conteúdo humano concorrente não é apagado durante rollback', t => {
|
|
|
122
122
|
assert.equal(fs.readFileSync(join(root, generated), 'utf8'), human);
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
+
test('falha ao registrar remoção derivada restaura fallback e preserva índice', t => {
|
|
126
|
+
const root = fixture(t), fallback = '.codex/skills/demo/SKILL.md';
|
|
127
|
+
write(root, '.pbai4devs/espelhos.conf', [
|
|
128
|
+
'agente|.agents|agrupado', 'agente|.codex|plano',
|
|
129
|
+
'formato|.codex|skills|nenhum', 'fallback|.codex|.agents|skills',
|
|
130
|
+
].join('\n') + '\n');
|
|
131
|
+
write(root, '.agents/skills', 'obstrução humana');
|
|
132
|
+
write(root, '.gitignore', '# ignore humano\n');
|
|
133
|
+
assert.equal(spawnSync('git', ['-C', root, 'add', '.gitignore']).status, 0);
|
|
134
|
+
run(root);
|
|
135
|
+
const initial = fs.readFileSync(join(root, fallback));
|
|
136
|
+
write(root, '.pbai4devs/state/agentdevkit.json', JSON.stringify({ schemaVersion: 1, files: { [fallback]: hash(initial) }, blocks: {} }));
|
|
137
|
+
fs.rmSync(join(root, '.pbai4devs/state/derived.json'), { force: true });
|
|
138
|
+
fs.rmSync(join(root, '.agents/skills')); // A obstrução pertence à fixture.
|
|
139
|
+
const before = snapshot(root), index = fs.readFileSync(join(root, '.git/index'));
|
|
140
|
+
const originalRename = fs.renameSync;
|
|
141
|
+
let injected = false;
|
|
142
|
+
t.mock.method(fs, 'renameSync', (from, to) => {
|
|
143
|
+
if (!injected && to === join(root, '.pbai4devs/state/derived.json')) { injected = true; throw new Error('falha injetada no registro derivado'); }
|
|
144
|
+
return originalRename(from, to);
|
|
145
|
+
});
|
|
146
|
+
syncBuiltinESMExports();
|
|
147
|
+
t.after(() => { t.mock.restoreAll(); syncBuiltinESMExports(); });
|
|
148
|
+
assert.throws(() => run(root), /falha injetada no registro derivado/);
|
|
149
|
+
assert.equal(injected, true);
|
|
150
|
+
assert.deepEqual(snapshot(root), before);
|
|
151
|
+
assert.deepEqual(fs.readFileSync(join(root, '.git/index')), index);
|
|
152
|
+
assert.deepEqual(fs.readFileSync(join(root, fallback)), initial);
|
|
153
|
+
});
|
|
154
|
+
|
|
125
155
|
test('conteúdo humano é preservado quando a escrita lança erro após promover o arquivo', t => {
|
|
126
156
|
const root = fixture(t), rename = fs.renameSync;
|
|
127
157
|
const failure = new Error('falha depois de promover');
|
|
@@ -913,6 +913,9 @@ test('formatos desconhecidos ou fora do cliente falham antes de mutar', t => {
|
|
|
913
913
|
for (const row of [
|
|
914
914
|
'formato|.missing|skills|agrupado', 'formato|.codex|unknown|agrupado',
|
|
915
915
|
'formato|.codex|skills|mdc', 'formato|.codex|rules|prompt',
|
|
916
|
+
'formato|.codex|agents|nenhum', 'formato|.codex|commands|nenhum',
|
|
917
|
+
'fallback|.codex|.missing|skills', 'fallback|.codex|.codex|skills',
|
|
918
|
+
'fallback|.codex|.agents|rules',
|
|
916
919
|
'formato|.codex|skills|plano\nformato|.codex|skills|agrupado',
|
|
917
920
|
'descoberta|.codex|antigravity', 'descoberta|.agents|antigravity',
|
|
918
921
|
]) {
|
|
@@ -922,3 +925,22 @@ test('formatos desconhecidos ou fora do cliente falham antes de mutar', t => {
|
|
|
922
925
|
assert.equal(resourcePath('.pbai4devs/commands/team/check/COMMAND.md', 'commands', 'arquivo'), 'commands/team/check.md');
|
|
923
926
|
assert.throws(() => resourcePath(source, 'skills', 'arquivo'), /Skills/);
|
|
924
927
|
});
|
|
928
|
+
|
|
929
|
+
test('sincronização direta recusa troca concorrente da obstrução de fallback', t => {
|
|
930
|
+
const root = fixture(t, true);
|
|
931
|
+
write(root, source, skill);
|
|
932
|
+
write(root, '.pbai4devs/espelhos.conf', [
|
|
933
|
+
'agente|.agents|agrupado', 'agente|.codex|agrupado',
|
|
934
|
+
'formato|.codex|skills|nenhum', 'fallback|.codex|.agents|skills',
|
|
935
|
+
].join('\n') + '\n');
|
|
936
|
+
write(root, '.agents/skills/projeto', 'Obstrução humana.');
|
|
937
|
+
let called = false;
|
|
938
|
+
assert.throws(() => synchronize({ root, localIgnores: false, generateEntrypoints: false, output() {
|
|
939
|
+
if (called) return;
|
|
940
|
+
called = true;
|
|
941
|
+
rmSync(join(root, '.agents/skills/projeto')); // Simula mudança humana após o planejamento.
|
|
942
|
+
} }), /mudou apos o planejamento/);
|
|
943
|
+
assert.equal(called, true);
|
|
944
|
+
assert.equal(existsSync(join(root, '.codex')), false);
|
|
945
|
+
assert.equal(existsSync(join(root, '.agents/skills/projeto/demo/SKILL.md')), false);
|
|
946
|
+
});
|
|
@@ -14,19 +14,21 @@ node .pbai4devs/scripts/testar-maturidade/SCRIPT.mjs run --case legado
|
|
|
14
14
|
node --test .pbai4devs/scripts/testar-maturidade/tests/TEST.mjs
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
`prepare` só gera as cópias; `run` gera e executa.
|
|
18
|
-
`examples/generated
|
|
19
|
-
|
|
17
|
+
`prepare` só gera as cópias; `run` gera e executa. Cada execução cria uma nova
|
|
18
|
+
pasta `examples/generated/maturity-*` e informa seu caminho no console. Por
|
|
19
|
+
padrão são oito cenários: agentes, descoberta, planejamento, aplicação inicial,
|
|
20
|
+
configurada, manutenção, legado e monorepo com caminhos em português. Use
|
|
21
|
+
`--case` para executar apenas um.
|
|
20
22
|
|
|
21
23
|
O harness copia recursos e configurações reais; exclui pacotes, caches, logs,
|
|
22
24
|
resultados de testes, Playwright e pastas temporárias em qualquer caixa,
|
|
23
25
|
`.git` e nomes conhecidos de credenciais/overrides locais. Não faz varredura
|
|
24
26
|
semântica de segredos. A API também aceita `tmp/examples/` para compatibilidade
|
|
25
|
-
e descendentes de `.agent-tests/`.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
e descendentes de `.agent-tests/`. Um destino explícito deve ser inexistente;
|
|
28
|
+
execuções e projetos anteriores não são substituídos nem removidos. Cada
|
|
29
|
+
exemplo recebe Git local, sem commits nem identidade alterada. O diretório
|
|
30
|
+
gerado contém um ignore próprio, inclusive em projetos consumidores. Se a
|
|
31
|
+
preparação falhar, o índice da nova execução permanece em `preparing`.
|
|
30
32
|
O hub `.pbai4devs` e os clientes registrados são copiados, incluindo comandos
|
|
31
33
|
nativos em TOML. Sem manifesto local, os templates definem os clientes em
|
|
32
34
|
memória, respeitando a seleção salva. `.vscode` exige registro. Apenas as entradas
|
|
@@ -50,5 +52,5 @@ O gate positivo simula o parecer para testar seu contrato; não representa
|
|
|
50
52
|
revisão humana. Estados de aprovação são dados sintéticos. Nenhum teste
|
|
51
53
|
certifica produção, Docker, interface visual ou outro sistema operacional.
|
|
52
54
|
|
|
53
|
-
Índice e relatório
|
|
54
|
-
|
|
55
|
+
Índice e relatório ficam em `README.md` e `REPORT.json` na pasta informada.
|
|
56
|
+
Logs e provas ficam em `.agent-tests/` dentro de cada exemplo, todos ignorados.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
-
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { isMain, projectRoot, requireNode } from '../lib/runtime.mjs';
|
|
@@ -9,6 +9,7 @@ import { isGeneratedOutput } from '../lib/gitignore.mjs';
|
|
|
9
9
|
import { resourcePath, synchronize } from '../sincronizar-agentes/SCRIPT.mjs';
|
|
10
10
|
import { adapterManifest } from '../lib/adapters.mjs';
|
|
11
11
|
import { stateDirectory } from '../lib/state-path.mjs';
|
|
12
|
+
import { nativePathBoundary } from '../lib/native-paths.mjs';
|
|
12
13
|
|
|
13
14
|
const assets = join(dirname(fileURLToPath(import.meta.url)), 'assets');
|
|
14
15
|
const owner = 'testar-maturidade';
|
|
@@ -36,6 +37,39 @@ function noLinks(root, target) {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// The child binds its cwd to the verified parent directory. A swapped parent
|
|
41
|
+
// changes the inode or real path before the child can create the new directory.
|
|
42
|
+
const createDirectoryScript = `const fs=require('node:fs');
|
|
43
|
+
const [name,expected,dev,ino]=process.argv.slice(1);
|
|
44
|
+
const info=fs.statSync('.');
|
|
45
|
+
if(fs.realpathSync('.')!==expected || String(info.dev)!==dev || String(info.ino)!==ino) process.exit(73);
|
|
46
|
+
try { fs.mkdirSync(name); } catch(error) { if(error.code==='EEXIST') process.exit(17); throw error; }
|
|
47
|
+
`;
|
|
48
|
+
function createChild(root, parent, name, { allowExisting = false } = {}) {
|
|
49
|
+
if (!name || name === '.' || name === '..' || basename(name) !== name) fail('Nome de diretório inválido.');
|
|
50
|
+
noLinks(root, parent);
|
|
51
|
+
const info = stat(parent);
|
|
52
|
+
if (!info?.isDirectory() || info.isSymbolicLink()) fail('Diretório ancestral inválido.');
|
|
53
|
+
const canonical = realpathSync(parent), inside = relative(root, canonical);
|
|
54
|
+
if (isAbsolute(inside) || inside === '..' || inside.startsWith(`..${sep}`)) fail('Diretório ancestral fora do projeto.');
|
|
55
|
+
const env = { ...process.env }; delete env.NODE_TEST_CONTEXT;
|
|
56
|
+
const result = spawnSync(process.execPath, ['-e', createDirectoryScript, name, canonical, String(info.dev), String(info.ino)], { cwd: canonical, env, encoding: 'utf8', timeout: 30000, windowsHide: true, shell: false });
|
|
57
|
+
if (result.error || result.status !== 0 && !(allowExisting && result.status === 17)) fail(`Não foi possível criar diretório exclusivo: ${result.error?.message || result.stderr?.trim() || result.status}`);
|
|
58
|
+
noLinks(root, join(parent, name));
|
|
59
|
+
const output = stat(join(parent, name));
|
|
60
|
+
if (!output?.isDirectory() || output.isSymbolicLink()) fail('Diretório de saída inválido.');
|
|
61
|
+
}
|
|
62
|
+
function ensureDirectories(root, target) {
|
|
63
|
+
let current = root;
|
|
64
|
+
for (const part of relative(root, target).split(sep).filter(Boolean)) {
|
|
65
|
+
const next = join(current, part), info = stat(next);
|
|
66
|
+
if (!info) createChild(root, current, part, { allowExisting: true });
|
|
67
|
+
else if (!info.isDirectory() || info.isSymbolicLink()) fail('Diretório ancestral inválido ou link simbólico.');
|
|
68
|
+
noLinks(root, next);
|
|
69
|
+
current = next;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
39
73
|
export const SCENARIOS = [
|
|
40
74
|
{ id: 'somente-agentes', title: 'Somente agentes', expected: 'preparacao', complete: false, app: false, planning: 'none', note: 'Recursos compartilhados, sem produto ou aplicação.' },
|
|
41
75
|
{ id: 'descoberta', title: 'Descoberta de produto', expected: 'descoberta', complete: false, app: false, planning: 'draft', note: 'PRD em rascunho; nenhuma stack é iniciada.' },
|
|
@@ -63,11 +97,28 @@ export function registeredRoots(root) {
|
|
|
63
97
|
|
|
64
98
|
export function resourceTargets(root, source, kind = 'skills') {
|
|
65
99
|
const rows = manifest(root).split(/\r?\n/).filter(line => line && !line.startsWith('#')).map(line => line.split('|'));
|
|
66
|
-
const targets = [];
|
|
100
|
+
const targets = [], suppressed = new Set();
|
|
101
|
+
for (const [type, fallback, primary, resource] of rows) {
|
|
102
|
+
if (type !== 'fallback' || resource !== kind) continue;
|
|
103
|
+
const primaryRow = rows.find(([entry, target]) => entry === 'agente' && target === primary);
|
|
104
|
+
const fallbackRow = rows.find(([entry, target]) => entry === 'agente' && target === fallback);
|
|
105
|
+
if (!primaryRow || !fallbackRow) fail(`Fallback sem cliente: ${fallback}`);
|
|
106
|
+
const primaryFormat = rows.find(([entry, target, scope]) => entry === 'formato' && target === primary && scope === kind)?.[3] || primaryRow[2] || 'agrupado';
|
|
107
|
+
const primaryTarget = `${primary}/${resourcePath(source, kind, primaryFormat)}`;
|
|
108
|
+
const boundary = nativePathBoundary(root, primaryTarget, [], { preserveRootAliases: true });
|
|
109
|
+
const info = boundary ? null : stat(join(root, primaryTarget));
|
|
110
|
+
if (boundary || (info && !info.isFile())) {
|
|
111
|
+
suppressed.add(primaryTarget);
|
|
112
|
+
targets.push(`${fallback}/${resourcePath(source, kind, fallbackRow[2] || 'agrupado')}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
67
115
|
for (const [type, agent, layout = 'agrupado'] of rows) {
|
|
68
116
|
if (type !== 'agente') continue;
|
|
69
117
|
const override = rows.find(([entry, target, resource]) => entry === 'formato' && target === agent && resource === kind)?.[3];
|
|
70
|
-
|
|
118
|
+
if (override === 'nenhum') continue;
|
|
119
|
+
let target = `${agent}/${resourcePath(source, kind, override || layout)}`;
|
|
120
|
+
if (agent === '.gemini' && kind === 'commands') target = target.replace(/\.md$/, '.toml');
|
|
121
|
+
if (!suppressed.has(target)) targets.push(target);
|
|
71
122
|
}
|
|
72
123
|
for (const [type, target, origin] of rows) if (type === 'nativo' && origin === source) targets.push(target);
|
|
73
124
|
return [...new Set(targets)];
|
|
@@ -139,29 +190,6 @@ export function copyAgentStructure(sourceRoot, destination) {
|
|
|
139
190
|
return { roots, copiedFiles: files.length, skipped, files, sourceDigest, entrypoints, toolDefinitions: files.filter(file => /^\.pbai4devs\/tools\/[^/]+\/TOOL\.json$/.test(file)).length };
|
|
140
191
|
}
|
|
141
192
|
|
|
142
|
-
function ownership(root, file, expectedId) {
|
|
143
|
-
if (stat(root)?.isSymbolicLink()) return false;
|
|
144
|
-
const path = join(root, file);
|
|
145
|
-
if (!existsSync(path)) return false;
|
|
146
|
-
const item = lstatSync(path);
|
|
147
|
-
if (!item.isFile() || item.isSymbolicLink()) return false;
|
|
148
|
-
try { const data = json(path); return data.owner === owner && (!expectedId || data.id === expectedId); }
|
|
149
|
-
catch { return false; }
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function ensureOutput(output) {
|
|
153
|
-
if (existsSync(output) && lstatSync(output).isSymbolicLink()) fail('A pasta de exemplos não pode ser um link.');
|
|
154
|
-
for (const name of [suiteMarker, 'README.md', 'REPORT.json', '.gitignore']) {
|
|
155
|
-
const item = stat(join(output, name));
|
|
156
|
-
if (item && (!item.isFile() || item.isSymbolicLink())) fail(`Arquivo de saída deve ser regular: ${name}`);
|
|
157
|
-
}
|
|
158
|
-
if (existsSync(output) && [suiteMarker, 'README.md', 'REPORT.json'].some(name => existsSync(join(output, name)))
|
|
159
|
-
&& !ownership(output, suiteMarker)) fail('Índice de exemplos existente não pertence a este gerador.');
|
|
160
|
-
if (existsSync(join(output, 'REPORT.json')) && !ownership(output, 'REPORT.json')) fail('Relatório de exemplos existente não pertence a este gerador.');
|
|
161
|
-
if (existsSync(join(output, '.gitignore')) && readFileSync(join(output, '.gitignore'), 'utf8') !== outputIgnore) fail('Ignore de exemplos existente não pertence a este gerador.');
|
|
162
|
-
mkdirSync(output, { recursive: true });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
193
|
function planning(root, scenario) {
|
|
166
194
|
const stateRoot = stateDirectory(root);
|
|
167
195
|
const state = `${stateRoot}/configuracao/ESTADO.md`;
|
|
@@ -228,6 +256,13 @@ function application(root, scenario) {
|
|
|
228
256
|
preserved.push(server, test, 'config/legacy.json', '.github/workflows/legacy.yml', '.vscode/settings.json', 'Makefile', 'package.json');
|
|
229
257
|
}
|
|
230
258
|
if (scenario.configured) {
|
|
259
|
+
// Published packages and existing projects need not contain these files;
|
|
260
|
+
// create only the missing synthetic fixtures, never replace copied content.
|
|
261
|
+
for (const [file, content] of Object.entries({
|
|
262
|
+
'.gitignore': '# Arquivos temporários do cenário sintético.\nnode_modules/\n.agent-tests/\n',
|
|
263
|
+
'.gitattributes': '* text=auto\n',
|
|
264
|
+
'.editorconfig': 'root = true\n\n[*]\ncharset = utf-8\nend_of_line = lf\ninsert_final_newline = true\n',
|
|
265
|
+
})) if (!stat(join(root, file))) write(root, file, content);
|
|
231
266
|
write(root, '.dockerignore', '.git\n.agent-tests\nnode_modules\n');
|
|
232
267
|
write(root, '.env.example', 'PORT=3000\n');
|
|
233
268
|
write(root, 'Dockerfile', 'FROM node:24-alpine\nWORKDIR /app\nCOPY package.json ./\nCOPY src ./src\nEXPOSE 3000\nCMD ["node", "src/server.mjs"]\n');
|
|
@@ -242,31 +277,37 @@ function gitInit(root) {
|
|
|
242
277
|
if (result.error || result.status !== 0) fail(`Git init: ${result.error?.message || result.stderr}`);
|
|
243
278
|
}
|
|
244
279
|
|
|
245
|
-
export function prepareExamples({ sourceRoot = projectRoot, outputRoot
|
|
246
|
-
requireNode(); sourceRoot = realpathSync(resolve(sourceRoot));
|
|
247
|
-
const
|
|
280
|
+
export function prepareExamples({ sourceRoot = projectRoot, outputRoot, scenarios = SCENARIOS, initializeGit = true, renderResources = prepareResources } = {}) {
|
|
281
|
+
requireNode(); sourceRoot = realpathSync(resolve(sourceRoot));
|
|
282
|
+
const base = resolve(outputRoot ?? defaultOutput(sourceRoot));
|
|
283
|
+
const outputPath = portable(relative(sourceRoot, base));
|
|
248
284
|
if (!(outputPath === 'examples/generated' || outputPath.startsWith('examples/generated/')
|
|
249
285
|
|| outputPath === 'tmp/examples' || outputPath.startsWith('tmp/examples/') || outputPath.startsWith('.agent-tests/'))) fail('Saída permitida somente em examples/generated, tmp/examples ou descendente de .agent-tests.');
|
|
250
|
-
noLinks(sourceRoot,
|
|
286
|
+
noLinks(sourceRoot, base);
|
|
251
287
|
const ids = new Set();
|
|
288
|
+
if (!Array.isArray(scenarios) || !scenarios.length) fail('Selecione pelo menos um cenário.');
|
|
252
289
|
for (const scenario of scenarios) {
|
|
253
290
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(scenario.id) || ids.has(scenario.id)) fail('Cenários devem ter IDs únicos e seguros.');
|
|
254
291
|
ids.add(scenario.id);
|
|
255
292
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
293
|
+
if (outputRoot !== undefined && stat(base)) fail(`Pasta de saída já existe; escolha um destino novo: ${base}`);
|
|
294
|
+
if (outputRoot !== undefined) {
|
|
295
|
+
for (let parent = dirname(base); parent !== sourceRoot && parent.startsWith(sourceRoot + sep); parent = dirname(parent)) {
|
|
296
|
+
if (stat(join(parent, suiteMarker)) || stat(join(parent, marker))) fail('Saída não pode ficar dentro de uma execução anterior.');
|
|
297
|
+
}
|
|
261
298
|
}
|
|
299
|
+
const parent = outputRoot === undefined ? base : dirname(base);
|
|
300
|
+
ensureDirectories(sourceRoot, parent);
|
|
301
|
+
const name = outputRoot === undefined ? `maturity-${randomUUID()}` : basename(base);
|
|
302
|
+
createChild(sourceRoot, parent, name);
|
|
303
|
+
outputRoot = join(parent, name);
|
|
262
304
|
write(outputRoot, suiteMarker, JSON.stringify({ owner, version: 1 }, null, 2) + '\n');
|
|
263
305
|
write(outputRoot, '.gitignore', outputIgnore);
|
|
264
|
-
rmSync(join(outputRoot, 'REPORT.json'), { force: true });
|
|
265
306
|
writeIndex(outputRoot, [], 'preparing');
|
|
266
307
|
const examples = [];
|
|
267
308
|
for (const scenario of scenarios) {
|
|
268
309
|
const destination = join(outputRoot, scenario.id);
|
|
269
|
-
|
|
310
|
+
mkdirSync(destination);
|
|
270
311
|
write(destination, marker, JSON.stringify({ owner, version: 1, id: scenario.id }, null, 2) + '\n');
|
|
271
312
|
const copied = copyAgentStructure(sourceRoot, destination);
|
|
272
313
|
planning(destination, scenario);
|
|
@@ -283,8 +324,8 @@ export function prepareExamples({ sourceRoot = projectRoot, outputRoot = default
|
|
|
283
324
|
|
|
284
325
|
function writeIndex(outputRoot, examples, status) {
|
|
285
326
|
const rows = examples.map(item => `| [${item.id}](${item.id}/README.md) | ${item.expected} | ${item.status || 'prepared'} |`).join('\n');
|
|
286
|
-
const progress = status === 'preparing' ? '\nPreparação em andamento
|
|
287
|
-
write(outputRoot, 'README.md', document('Exemplos de maturidade', `Projetos sintéticos com cópias locais da estrutura de agentes.\nNenhum resultado certifica produção, revisão humana ou outros sistemas operacionais.\n${progress}\n| Cenário | Nível esperado | Resultado |\n| --- | --- | --- |${rows ? '\n' + rows : ''}\n\nRelatório: \`REPORT.json\`. Logs e evidências por projeto ficam em \`.agent-tests/\`.\
|
|
327
|
+
const progress = status === 'preparing' ? '\nPreparação em andamento nesta execução.\n' : '';
|
|
328
|
+
write(outputRoot, 'README.md', document('Exemplos de maturidade', `Projetos sintéticos com cópias locais da estrutura de agentes.\nNenhum resultado certifica produção, revisão humana ou outros sistemas operacionais.\n${progress}\n| Cenário | Nível esperado | Resultado |\n| --- | --- | --- |${rows ? '\n' + rows : ''}\n\nRelatório: \`REPORT.json\`. Logs e evidências por projeto ficam em \`.agent-tests/\`.\nCada execução usa uma pasta nova e preserva as anteriores.\n`, `status: ${status}\n`));
|
|
288
329
|
}
|
|
289
330
|
|
|
290
331
|
function hashes(root, files) { return Object.fromEntries(files.map(file => [file, digest(readFileSync(join(root, file)))])); }
|
|
@@ -453,7 +494,7 @@ export function testExample(example) {
|
|
|
453
494
|
export function runExamples(options = {}) {
|
|
454
495
|
const examples = prepareExamples(options);
|
|
455
496
|
const results = examples.map(example => { const result = testExample(example); console.log(`${example.id}=${result.status}${result.error ? ': ' + result.error : ''}`); return result; });
|
|
456
|
-
const outputRoot =
|
|
497
|
+
const outputRoot = dirname(examples[0].root);
|
|
457
498
|
const report = { owner, version: 1, status: results.every(item => item.status === 'passed') ? 'passed' : 'failed', platform: process.platform, arch: process.arch, node: process.version, results, limits: ['Provas reais somente das operações descritas e APIs locais.', 'Pareceres do gate são simulações explícitas, sem revisão humana.', 'Não certifica produção nem sistemas operacionais não executados.'] };
|
|
458
499
|
write(outputRoot, 'REPORT.json', JSON.stringify(report, null, 2) + '\n');
|
|
459
500
|
writeIndex(outputRoot, examples.map(example => ({ ...example, status: results.find(result => result.id === example.id).status })), report.status);
|
|
@@ -462,15 +503,16 @@ export function runExamples(options = {}) {
|
|
|
462
503
|
|
|
463
504
|
export function main(args = process.argv.slice(2)) {
|
|
464
505
|
requireNode(); const [mode = 'help', ...rest] = args;
|
|
465
|
-
if (['help', '--help', '-h'].includes(mode)) { console.log('Uso: testar-maturidade prepare|run [--case <id>].
|
|
506
|
+
if (['help', '--help', '-h'].includes(mode)) { console.log('Uso: testar-maturidade prepare|run [--case <id>]. Cada execução cria uma nova pasta em examples/generated.'); return 0; }
|
|
466
507
|
if (!['prepare', 'run'].includes(mode)) fail('Use prepare ou run.');
|
|
467
508
|
let scenarios = SCENARIOS;
|
|
468
509
|
if (rest.length) {
|
|
469
510
|
if (rest.length !== 2 || rest[0] !== '--case') fail('Use --case <id>.');
|
|
470
511
|
scenarios = SCENARIOS.filter(item => item.id === rest[1]); if (!scenarios.length) fail('Cenário desconhecido.');
|
|
471
512
|
}
|
|
472
|
-
if (mode === 'prepare') { const examples = prepareExamples({ scenarios }); console.log(`prepared=${examples.length}`); return 0; }
|
|
473
|
-
|
|
513
|
+
if (mode === 'prepare') { const examples = prepareExamples({ scenarios }); console.log(`prepared=${examples.length}\noutput=${dirname(examples[0].root)}`); return 0; }
|
|
514
|
+
const result = runExamples({ scenarios }); console.log(`output=${result.outputRoot}`);
|
|
515
|
+
return result.report.status === 'passed' ? 0 : 1;
|
|
474
516
|
}
|
|
475
517
|
|
|
476
518
|
if (isMain(import.meta.url)) {
|