@pathbit/agentdevkit 0.0.0-stage → 0.0.5
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 +109 -0
- package/.pbai4devs/INSTRUCTIONS.md +87 -0
- package/.pbai4devs/README.md +50 -0
- package/.pbai4devs/agents/README.md +18 -0
- package/.pbai4devs/agents/revisor/AGENT.md +16 -0
- package/.pbai4devs/agents/revisor/antigravity.yaml +8 -0
- package/.pbai4devs/agents/revisor/claude.yaml +5 -0
- package/.pbai4devs/agents/revisor/cursor.yaml +5 -0
- package/.pbai4devs/agents/revisor/gemini.yaml +6 -0
- package/.pbai4devs/agents/revisor/github.yaml +5 -0
- package/.pbai4devs/agents/revisor/kimi-code.yaml +5 -0
- package/.pbai4devs/agents/revisor/mimocode.yaml +10 -0
- package/.pbai4devs/agents/revisor/opencode.yaml +7 -0
- package/.pbai4devs/agents/revisor/zcode.yaml +5 -0
- package/.pbai4devs/commands/README.md +30 -0
- package/.pbai4devs/commands/atualizar-ferramentas/COMMAND.md +17 -0
- package/.pbai4devs/commands/diagnosticar/COMMAND.md +18 -0
- package/.pbai4devs/commands/instalar-ferramentas/COMMAND.md +21 -0
- package/.pbai4devs/commands/preparar-dev/COMMAND.md +14 -0
- package/.pbai4devs/commands/proximos-passos/COMMAND.md +9 -0
- package/.pbai4devs/commands/revisar-entrega/COMMAND.md +29 -0
- package/.pbai4devs/commands/sincronizar/COMMAND.md +20 -0
- package/.pbai4devs/commands/status-roadmap/COMMAND.md +12 -0
- package/.pbai4devs/commands/testar-navegador/COMMAND.md +16 -0
- package/.pbai4devs/commands/verificar-agentes/COMMAND.md +19 -0
- package/.pbai4devs/estado/README.md +12 -0
- package/.pbai4devs/external-skills/CATALOG.json +121 -0
- package/.pbai4devs/external-skills/README.md +48 -0
- package/.pbai4devs/rules/README.md +18 -0
- package/.pbai4devs/rules/ambiente-local/RULE.md +33 -0
- package/.pbai4devs/rules/ambiente-local/cursor.yaml +4 -0
- package/.pbai4devs/rules/documentacao/RULE.md +11 -0
- package/.pbai4devs/rules/documentacao/cursor.yaml +4 -0
- package/.pbai4devs/rules/estrutura/RULE.md +33 -0
- package/.pbai4devs/rules/estrutura/cursor.yaml +4 -0
- package/.pbai4devs/rules/git/RULE.md +18 -0
- package/.pbai4devs/rules/git/cursor.yaml +4 -0
- package/.pbai4devs/rules/proximos-passos/RULE.md +14 -0
- package/.pbai4devs/rules/qualidade/RULE.md +32 -0
- package/.pbai4devs/rules/qualidade/cursor.yaml +4 -0
- package/.pbai4devs/scripts/README.md +69 -0
- package/.pbai4devs/scripts/diagnosticar-ambiente/SCRIPT.mjs +41 -0
- package/.pbai4devs/scripts/diagnosticar-ambiente/tests/TEST.mjs +30 -0
- package/.pbai4devs/scripts/executar-script/README.md +28 -0
- package/.pbai4devs/scripts/executar-script/SCRIPT.mjs +78 -0
- package/.pbai4devs/scripts/executar-script/tests/TEST.mjs +122 -0
- package/.pbai4devs/scripts/gerenciar-skills-externas/README.md +31 -0
- package/.pbai4devs/scripts/gerenciar-skills-externas/SCRIPT.mjs +182 -0
- package/.pbai4devs/scripts/gerenciar-skills-externas/tests/TEST.mjs +134 -0
- package/.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs +190 -0
- package/.pbai4devs/scripts/inspecionar-projeto/tests/TEST.mjs +161 -0
- package/.pbai4devs/scripts/lib/README.md +9 -0
- package/.pbai4devs/scripts/lib/adapters.mjs +135 -0
- package/.pbai4devs/scripts/lib/catalog.mjs +170 -0
- package/.pbai4devs/scripts/lib/gitignore.mjs +348 -0
- package/.pbai4devs/scripts/lib/npm.mjs +17 -0
- package/.pbai4devs/scripts/lib/resource-path.mjs +20 -0
- package/.pbai4devs/scripts/lib/runtime.mjs +28 -0
- package/.pbai4devs/scripts/lib/script-config.mjs +8 -0
- package/.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.md +34 -0
- package/.pbai4devs/scripts/preparar-ferramentas/BOOTSTRAP.mjs +189 -0
- package/.pbai4devs/scripts/preparar-ferramentas/README.md +71 -0
- package/.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs +455 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/FIXTURE.html +20 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_BROWSER.mjs +55 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_DESKTOP.mjs +61 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/SMOKE_VERCEL.mjs +192 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/TEST.mjs +552 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/bootstrap.test.mjs +94 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/just-bash/input.txt +2 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/just-bash-overlay/input.txt +1 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/skills/agentdevkit-smoke/SKILL.md +8 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/webreel/index.html +6 -0
- package/.pbai4devs/scripts/preparar-ferramentas/tests/fixtures-vercel/webreel/webreel.config.json +10 -0
- package/.pbai4devs/scripts/revisar-entrega/README.md +36 -0
- package/.pbai4devs/scripts/revisar-entrega/SCRIPT.mjs +249 -0
- package/.pbai4devs/scripts/revisar-entrega/tests/TEST.mjs +249 -0
- package/.pbai4devs/scripts/sincronizar-agentes/README.md +41 -0
- package/.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs +473 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/LOCAL_IGNORE.mjs +290 -0
- package/.pbai4devs/scripts/sincronizar-agentes/tests/TEST.mjs +869 -0
- package/.pbai4devs/scripts/testar-infraestrutura/SCRIPT.mjs +39 -0
- package/.pbai4devs/scripts/testar-infraestrutura/tests/TEST.mjs +29 -0
- package/.pbai4devs/scripts/testar-maturidade/README.md +54 -0
- package/.pbai4devs/scripts/testar-maturidade/SCRIPT.mjs +477 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/calculator.mjs +1 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/calculator.test.mjs.template +8 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/http-api.test.mjs.template +28 -0
- package/.pbai4devs/scripts/testar-maturidade/assets/server.mjs +34 -0
- package/.pbai4devs/scripts/testar-maturidade/tests/TEST.mjs +305 -0
- package/.pbai4devs/scripts/testar-navegador/REFERENCE.md +49 -0
- package/.pbai4devs/scripts/testar-navegador/SCRIPT.conf +1 -0
- package/.pbai4devs/scripts/testar-navegador/SCRIPT.js +64 -0
- package/.pbai4devs/scripts/testar-navegador/tests/FAIL.js +8 -0
- package/.pbai4devs/scripts/testar-navegador/tests/SUCCESS.js +13 -0
- package/.pbai4devs/scripts/verificar-recursos/README.md +25 -0
- package/.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs +209 -0
- package/.pbai4devs/scripts/verificar-recursos/tests/TEST.mjs +310 -0
- package/.pbai4devs/scripts/verificar-recursos/tests/ambiente-local.mjs +80 -0
- package/.pbai4devs/skills/README.md +24 -0
- package/.pbai4devs/skills/git/git-branch/SKILL.md +11 -0
- package/.pbai4devs/skills/git/git-commit/SKILL.md +16 -0
- package/.pbai4devs/skills/git/git-diff/SKILL.md +9 -0
- package/.pbai4devs/skills/git/git-fetch/SKILL.md +10 -0
- package/.pbai4devs/skills/git/git-log/SKILL.md +9 -0
- package/.pbai4devs/skills/git/git-merge/SKILL.md +13 -0
- package/.pbai4devs/skills/git/git-pull/SKILL.md +12 -0
- package/.pbai4devs/skills/git/git-push/SKILL.md +12 -0
- package/.pbai4devs/skills/git/git-status/SKILL.md +9 -0
- package/.pbai4devs/skills/git/git-worktree/SKILL.md +10 -0
- package/.pbai4devs/skills/produto/criar-design-doc/SKILL.md +37 -0
- package/.pbai4devs/skills/produto/criar-prd/SKILL.md +35 -0
- package/.pbai4devs/skills/produto/criar-ux-ui/SKILL.md +34 -0
- package/.pbai4devs/skills/projeto/atualizar-documentacao/SKILL.md +26 -0
- package/.pbai4devs/skills/projeto/configurar-projeto/SKILL.md +62 -0
- package/.pbai4devs/skills/projeto/criar-agente/SKILL.md +20 -0
- package/.pbai4devs/skills/projeto/criar-comando/SKILL.md +25 -0
- package/.pbai4devs/skills/projeto/criar-recurso/SKILL.md +41 -0
- package/.pbai4devs/skills/projeto/criar-regra/SKILL.md +19 -0
- package/.pbai4devs/skills/projeto/criar-skill/SKILL.md +22 -0
- package/.pbai4devs/skills/projeto/executar-script/SKILL.md +29 -0
- package/.pbai4devs/skills/projeto/preparar-dev/SKILL.md +45 -0
- package/.pbai4devs/skills/projeto/preparar-ferramentas/SKILL.md +56 -0
- package/.pbai4devs/skills/projeto/proximos-passos/SKILL.md +23 -0
- package/.pbai4devs/skills/projeto/sincronizar-agentes/SKILL.md +42 -0
- package/.pbai4devs/skills/projeto/usar-skills-externas/SKILL.md +28 -0
- package/.pbai4devs/skills/projeto/verificar-trabalho/SKILL.md +45 -0
- package/.pbai4devs/skills/specs/criar-spec/SKILL.md +33 -0
- package/.pbai4devs/skills/specs/gerenciar-roadmap/SKILL.md +26 -0
- package/.pbai4devs/skills/specs/implementar-spec/SKILL.md +48 -0
- package/.pbai4devs/templates/README.md +65 -0
- package/.pbai4devs/templates/adr/TEMPLATE.md +21 -0
- package/.pbai4devs/templates/ambiente-local/Makefile.template +48 -0
- package/.pbai4devs/templates/ambiente-local/TEMPLATE.md +48 -0
- package/.pbai4devs/templates/ambiente-local/compose.local.yml.template +16 -0
- package/.pbai4devs/templates/ambiente-local/dockerignore.template +28 -0
- package/.pbai4devs/templates/changelog/TEMPLATE.md +17 -0
- package/.pbai4devs/templates/claude/TEMPLATE.md +8 -0
- package/.pbai4devs/templates/clients/ADAPTERS.json +306 -0
- package/.pbai4devs/templates/clients/TEMPLATE.md +12 -0
- package/.pbai4devs/templates/comando/TEMPLATE.md +15 -0
- package/.pbai4devs/templates/configuracao/TEMPLATE.md +46 -0
- package/.pbai4devs/templates/configuracao-raiz/TEMPLATE.md +22 -0
- package/.pbai4devs/templates/configuracao-raiz/editorconfig.template +21 -0
- package/.pbai4devs/templates/configuracao-raiz/env.example.template +3 -0
- package/.pbai4devs/templates/configuracao-raiz/gitattributes.template +6 -0
- package/.pbai4devs/templates/contribuicao/TEMPLATE.md +23 -0
- package/.pbai4devs/templates/design/TEMPLATE.md +30 -0
- package/.pbai4devs/templates/entrypoints/AGENTS.md +19 -0
- package/.pbai4devs/templates/entrypoints/CLAUDE.md +7 -0
- package/.pbai4devs/templates/entrypoints/GEMINI.md +7 -0
- package/.pbai4devs/templates/entrypoints/TEMPLATE.md +12 -0
- package/.pbai4devs/templates/gitignore/TEMPLATE.md +42 -0
- package/.pbai4devs/templates/gitignore/agents.ignore +45 -0
- package/.pbai4devs/templates/gitignore/clients.json +20 -0
- package/.pbai4devs/templates/gitignore/local.ignore +10 -0
- package/.pbai4devs/templates/gitignore/shared.ignore +60 -0
- package/.pbai4devs/templates/instrucoes-agentes/TEMPLATE.md +25 -0
- package/.pbai4devs/templates/plano-implementacao/TEMPLATE.md +27 -0
- package/.pbai4devs/templates/prd/TEMPLATE.md +30 -0
- package/.pbai4devs/templates/readme-pasta/TEMPLATE.md +16 -0
- package/.pbai4devs/templates/readme-projeto/TEMPLATE.md +34 -0
- package/.pbai4devs/templates/release/TEMPLATE.md +23 -0
- package/.pbai4devs/templates/revisao-entrega/TEMPLATE.md +41 -0
- package/.pbai4devs/templates/roadmap/TEMPLATE.md +18 -0
- package/.pbai4devs/templates/seguranca/TEMPLATE.md +22 -0
- package/.pbai4devs/templates/spec/TEMPLATE.md +61 -0
- package/.pbai4devs/templates/tasks/TEMPLATE.md +20 -0
- package/.pbai4devs/templates/ux-ui/TEMPLATE.md +26 -0
- package/.pbai4devs/tools/README.md +87 -0
- package/.pbai4devs/tools/agent-browser/README.md +26 -0
- package/.pbai4devs/tools/agent-browser/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/agent-browser/TOOL.json +33 -0
- package/.pbai4devs/tools/agent-desktop/README.md +26 -0
- package/.pbai4devs/tools/agent-desktop/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/agent-desktop/TOOL.json +30 -0
- package/.pbai4devs/tools/just-bash/README.md +23 -0
- package/.pbai4devs/tools/just-bash/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/just-bash/TOOL.json +27 -0
- package/.pbai4devs/tools/markdownlint-cli2/README.md +18 -0
- package/.pbai4devs/tools/markdownlint-cli2/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/markdownlint-cli2/TOOL.json +28 -0
- package/.pbai4devs/tools/opensrc/README.md +25 -0
- package/.pbai4devs/tools/opensrc/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/opensrc/TOOL.json +29 -0
- package/.pbai4devs/tools/portless/README.md +21 -0
- package/.pbai4devs/tools/portless/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/portless/TOOL.json +30 -0
- package/.pbai4devs/tools/skills/README.md +24 -0
- package/.pbai4devs/tools/skills/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/skills/TOOL.json +33 -0
- package/.pbai4devs/tools/webreel/README.md +23 -0
- package/.pbai4devs/tools/webreel/SCRIPT.mjs +5 -0
- package/.pbai4devs/tools/webreel/TOOL.json +25 -0
- package/CHANGELOG.md +58 -0
- package/CONTRIBUTING.md +73 -0
- package/LICENSE +21 -0
- package/README.en.md +78 -0
- package/README.es.md +80 -0
- package/README.md +88 -2
- package/bin/README.md +8 -0
- package/bin/agentdevkit.mjs +35 -0
- package/docs/README.md +81 -0
- package/docs/guides/README.md +14 -0
- package/docs/guides/cli.md +81 -0
- package/docs/guides/installation.md +78 -0
- package/docs/guides/resources.md +121 -0
- package/docs/guides/tools-and-testing.md +85 -0
- package/docs/guides/troubleshooting.md +151 -0
- package/docs/guides/workflow.md +130 -0
- package/docs/maintainers/README.md +12 -0
- package/docs/maintainers/community.md +49 -0
- package/docs/maintainers/development.md +54 -0
- package/docs/maintainers/maintenance.md +104 -0
- package/docs/maintainers/packages.md +76 -0
- package/docs/product/README.md +12 -0
- package/docs/product/design.md +94 -0
- package/docs/product/prd.md +64 -0
- package/docs/product/roadmap.md +36 -0
- package/docs/product/ux-ui.md +42 -0
- package/docs/specs/README.md +20 -0
- package/docs/specs/github-packages/SPEC.md +45 -0
- package/docs/specs/v0.0.1/README.md +10 -0
- package/docs/specs/v0.0.1/SPEC.md +57 -0
- package/docs/specs/v0.0.2/README.md +10 -0
- package/docs/specs/v0.0.2/SPEC.md +67 -0
- package/docs/specs/v0.0.3/README.md +10 -0
- package/docs/specs/v0.0.3/SPEC.md +64 -0
- package/docs/specs/v0.0.4/README.md +10 -0
- package/docs/specs/v0.0.4/SPEC.md +53 -0
- package/docs/specs/v0.0.5/README.md +9 -0
- package/docs/specs/v0.0.5/SPEC.md +42 -0
- package/package.json +52 -4
- package/src/README.md +16 -0
- package/src/adapters/index.mjs +1 -0
- package/src/cli.mjs +162 -0
- package/src/config.mjs +14 -0
- package/src/i18n.mjs +42 -0
- package/src/install.mjs +367 -0
- package/src/next.mjs +79 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { isMain, projectRoot, requireNode } from '../lib/runtime.mjs';
|
|
6
|
+
|
|
7
|
+
export function testInfrastructure(root = projectRoot) {
|
|
8
|
+
requireNode();
|
|
9
|
+
root = resolve(root);
|
|
10
|
+
const scripts = join(root, '.pbai4devs/scripts');
|
|
11
|
+
const output = join(root, '.agent-tests/infraestrutura');
|
|
12
|
+
const suites = readdirSync(scripts, { withFileTypes: true })
|
|
13
|
+
.filter(entry => entry.isDirectory() && existsSync(join(scripts, entry.name, 'tests/TEST.mjs')))
|
|
14
|
+
.map(entry => entry.name).sort();
|
|
15
|
+
if (!suites.length) throw new Error('Nenhuma suite Node encontrada.');
|
|
16
|
+
mkdirSync(output, { recursive: true });
|
|
17
|
+
const report = { status: 'passed', suites: [] };
|
|
18
|
+
const environment = { ...process.env };
|
|
19
|
+
// Cada --test precisa de seu proprio contexto, inclusive ao testar este runner.
|
|
20
|
+
delete environment.NODE_TEST_CONTEXT;
|
|
21
|
+
for (const name of suites) {
|
|
22
|
+
const file = join(scripts, name, 'tests/TEST.mjs');
|
|
23
|
+
const result = spawnSync(process.execPath, ['--test', file], { cwd: root, env: environment, encoding: 'utf8', timeout: 180000, maxBuffer: 8 * 1024 * 1024, windowsHide: true });
|
|
24
|
+
const log = join(output, `${name}.log`);
|
|
25
|
+
writeFileSync(log, `${result.stdout || ''}${result.stderr || ''}${result.error ? '\n' + result.error.message : ''}`);
|
|
26
|
+
const passed = !result.error && result.status === 0;
|
|
27
|
+
report.suites.push({ name, status: passed ? 'passed' : 'failed', log });
|
|
28
|
+
console.log(`${name}=${passed ? 'passed' : 'failed'}`);
|
|
29
|
+
if (!passed) { report.status = 'failed'; console.error(readFileSync(log, 'utf8').slice(-4000)); }
|
|
30
|
+
}
|
|
31
|
+
writeFileSync(join(output, 'REPORT.json'), JSON.stringify(report, null, 2) + '\n');
|
|
32
|
+
console.log(`suites=${suites.length}`);
|
|
33
|
+
return report;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isMain(import.meta.url)) {
|
|
37
|
+
try { if (testInfrastructure().status !== 'passed') process.exitCode = 1; }
|
|
38
|
+
catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join, relative } from 'node:path';
|
|
5
|
+
import { projectRoot } from '../../lib/runtime.mjs';
|
|
6
|
+
import { testInfrastructure } from '../SCRIPT.mjs';
|
|
7
|
+
|
|
8
|
+
test('executa suites em raiz relativa, registra falhas e continua as demais', t => {
|
|
9
|
+
const output = join(projectRoot, '.agent-tests'); mkdirSync(output, { recursive: true });
|
|
10
|
+
const root = mkdtempSync(join(output, 'runner-'));
|
|
11
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
12
|
+
for (const [name, pass] of [['a-fail', false], ['b-pass', true]]) {
|
|
13
|
+
const folder = join(root, '.pbai4devs/scripts', name, 'tests'); mkdirSync(folder, { recursive: true });
|
|
14
|
+
writeFileSync(join(folder, 'TEST.mjs'), `import { test } from 'node:test';\nimport assert from 'node:assert/strict';\ntest('fixture', () => assert.equal(${pass}, true));\n`);
|
|
15
|
+
}
|
|
16
|
+
const report = testInfrastructure(relative(process.cwd(), root));
|
|
17
|
+
assert.equal(report.status, 'failed');
|
|
18
|
+
assert.deepEqual(report.suites.map(suite => suite.status), ['failed', 'passed']);
|
|
19
|
+
for (const suite of report.suites) assert.ok(readFileSync(suite.log, 'utf8').includes('fixture'));
|
|
20
|
+
assert.deepEqual(JSON.parse(readFileSync(join(root, '.agent-tests/infraestrutura/REPORT.json'))), report);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('nao aprova uma execucao sem suites', t => {
|
|
24
|
+
const output = join(projectRoot, '.agent-tests'); mkdirSync(output, { recursive: true });
|
|
25
|
+
const root = mkdtempSync(join(output, 'runner-empty-'));
|
|
26
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
27
|
+
mkdirSync(join(root, '.pbai4devs/scripts'), { recursive: true });
|
|
28
|
+
assert.throws(() => testInfrastructure(root), /Nenhuma suite Node/);
|
|
29
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Projetos copiados para testar os fluxos em maturidades diferentes.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Testar maturidade
|
|
6
|
+
|
|
7
|
+
Exige Node.js 24+ e Git. Na raiz do repositório:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm run examples
|
|
11
|
+
npm run test:examples
|
|
12
|
+
node .pbai4devs/scripts/testar-maturidade/SCRIPT.mjs run
|
|
13
|
+
node .pbai4devs/scripts/testar-maturidade/SCRIPT.mjs run --case legado
|
|
14
|
+
node --test .pbai4devs/scripts/testar-maturidade/tests/TEST.mjs
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`prepare` só gera as cópias; `run` gera e executa. Os oito cenários ficam em
|
|
18
|
+
`examples/generated/`: agentes, descoberta, planejamento, aplicação inicial,
|
|
19
|
+
configurada, manutenção, legado e monorepo com caminhos em português.
|
|
20
|
+
|
|
21
|
+
O harness copia recursos e configurações reais; exclui pacotes, caches, logs,
|
|
22
|
+
resultados de testes, Playwright e pastas temporárias em qualquer caixa,
|
|
23
|
+
`.git` e nomes conhecidos de credenciais/overrides locais. Não faz varredura
|
|
24
|
+
semântica de segredos. A API também aceita `tmp/examples/` para compatibilidade
|
|
25
|
+
e descendentes de `.agent-tests/`. Regenera apenas pastas identificadas como
|
|
26
|
+
suas. Cada exemplo recebe Git local, sem commits nem identidade alterada.
|
|
27
|
+
O diretório gerado contém um ignore próprio, inclusive em projetos consumidores.
|
|
28
|
+
`prepare` redefine o relatório como `prepared`, sem conservar provas antigas.
|
|
29
|
+
Se a preparação falhar, o índice fica em `preparing` e o relatório antigo sai.
|
|
30
|
+
O hub `.pbai4devs` e os clientes registrados são copiados, incluindo comandos
|
|
31
|
+
nativos em TOML. Sem manifesto local, os templates definem os clientes em
|
|
32
|
+
memória, respeitando a seleção salva. `.vscode` exige registro. Apenas as entradas
|
|
33
|
+
`AGENTS.md`, `CLAUDE.md` e `GEMINI.md` presentes na origem são incluídas na raiz.
|
|
34
|
+
Os templates substituem somente entradas com blocos gerenciados válidos;
|
|
35
|
+
entradas próprias ou editadas são preservadas. Sem estado de instalação,
|
|
36
|
+
o mantenedor usa `.pbai4devs/templates/entrypoints/`, com fallback para a raiz.
|
|
37
|
+
Os testes resolvem cada apontamento pelo formato declarado do cliente e recurso.
|
|
38
|
+
|
|
39
|
+
As cópias geram os derivados locais antes dos testes, sem alterar a origem.
|
|
40
|
+
Não copiam o código do produto em `bin/`, `src/`, `scripts/` ou `tests/`,
|
|
41
|
+
nem essas pastas na localização antiga dentro do hub.
|
|
42
|
+
Executam sincronização/reparo/idempotência, catálogo, verificação de
|
|
43
|
+
recursos, diagnóstico, inspeção e gates negativos/positivo. Em dois casos,
|
|
44
|
+
criação/edição/renomeação/remoção e corrupção dos índices exercitam o ciclo todo.
|
|
45
|
+
Aplicações executam testes HTTP reais em
|
|
46
|
+
portas efêmeras, encerradas ao fim. Legado e monorepo preservam seus arquivos.
|
|
47
|
+
Ferramentas copiadas ficam sem downloads: o check negativo é esperado.
|
|
48
|
+
|
|
49
|
+
O gate positivo simula o parecer para testar seu contrato; não representa
|
|
50
|
+
revisão humana. Estados de aprovação são dados sintéticos. Nenhum teste
|
|
51
|
+
certifica produção, Docker, interface visual ou outro sistema operacional.
|
|
52
|
+
|
|
53
|
+
Índice e relatório: `examples/generated/README.md` e `REPORT.json`. Logs e provas
|
|
54
|
+
individuais ficam em `.agent-tests/` dentro de cada exemplo, todos ignorados.
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
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';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { isMain, projectRoot, requireNode } from '../lib/runtime.mjs';
|
|
8
|
+
import { isGeneratedOutput } from '../lib/gitignore.mjs';
|
|
9
|
+
import { resourcePath, synchronize } from '../sincronizar-agentes/SCRIPT.mjs';
|
|
10
|
+
import { adapterManifest } from '../lib/adapters.mjs';
|
|
11
|
+
|
|
12
|
+
const assets = join(dirname(fileURLToPath(import.meta.url)), 'assets');
|
|
13
|
+
const owner = 'testar-maturidade';
|
|
14
|
+
const marker = '.maturity-case.json';
|
|
15
|
+
const suiteMarker = '.maturity-suite.json';
|
|
16
|
+
const outputIgnore = '# Exemplos gerados permanecem locais.\n*\n';
|
|
17
|
+
const defaultOutput = root => join(root, 'examples/generated');
|
|
18
|
+
const prepareResources = root => {
|
|
19
|
+
const result = synchronize({ root, localIgnores: false, output: () => {} });
|
|
20
|
+
if (result.exitCode !== 0) fail('Não foi possível preparar os apontamentos do exemplo.');
|
|
21
|
+
};
|
|
22
|
+
const portable = value => value.split(sep).join('/');
|
|
23
|
+
const digest = value => createHash('sha256').update(value).digest('hex');
|
|
24
|
+
const fail = message => { throw new Error(message); };
|
|
25
|
+
const document = (title, body, metadata = '') => `---\ndescription: Cenário sintético de maturidade.\nfixture: true\n${metadata}---\n\n# ${title}\n\n${body.trim()}\n`;
|
|
26
|
+
const write = (root, file, content) => { const path = join(root, file); mkdirSync(dirname(path), { recursive: true }); writeFileSync(path, content); return path; };
|
|
27
|
+
const json = file => JSON.parse(readFileSync(file, 'utf8'));
|
|
28
|
+
const stat = file => { try { return lstatSync(file); } catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return null; throw error; } };
|
|
29
|
+
|
|
30
|
+
function noLinks(root, target) {
|
|
31
|
+
let current = root;
|
|
32
|
+
for (const part of ['', ...relative(root, target).split(sep).filter(Boolean)]) {
|
|
33
|
+
if (part) current = join(current, part);
|
|
34
|
+
if (stat(current)?.isSymbolicLink()) fail(`Caminho gerenciado não pode ser link: ${current}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const SCENARIOS = [
|
|
39
|
+
{ id: 'somente-agentes', title: 'Somente agentes', expected: 'preparacao', complete: false, app: false, planning: 'none', note: 'Recursos compartilhados, sem produto ou aplicação.' },
|
|
40
|
+
{ id: 'descoberta', title: 'Descoberta de produto', expected: 'descoberta', complete: false, app: false, planning: 'draft', note: 'PRD em rascunho; nenhuma stack é iniciada.' },
|
|
41
|
+
{ id: 'planejado', title: 'Produto planejado', expected: 'planejado', complete: false, app: false, planning: 'ready', note: 'PRD/design aprovados no cenário e UX não aplicável; ainda sem código.' },
|
|
42
|
+
{ id: 'app-inicial', title: 'Aplicação inicial', expected: 'implementacao', complete: false, app: true, planning: 'none', note: 'API funcional, mas contrato de estrutura incompleto.' },
|
|
43
|
+
{ id: 'app-configurada', title: 'Aplicação configurada', expected: 'configurado', complete: true, app: true, planning: 'ready', configured: true, note: 'Estrutura e planejamento completos; testes reais de API.' },
|
|
44
|
+
{ id: 'manutencao', title: 'Manutenção de aplicação', expected: 'manutencao', complete: true, app: true, planning: 'ready', configured: true, released: true, note: 'Aplicação configurada com release publicada sintética.' },
|
|
45
|
+
{ id: 'legado', title: 'Aplicação legada', expected: 'implementacao', complete: false, app: true, planning: 'none', layout: 'legacy', note: 'Pastas, comandos e configurações antigas são preservados.' },
|
|
46
|
+
{ id: 'monorepo', title: 'Monorepo com dois módulos', expected: 'implementacao', complete: false, app: true, planning: 'ready', layout: 'monorepo', note: 'Dois módulos e caminhos com espaços/acentos; sem impor src na raiz.' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
function manifest(root) {
|
|
50
|
+
const file = join(root, '.pbai4devs/espelhos.conf');
|
|
51
|
+
if (existsSync(file)) return readFileSync(file, 'utf8');
|
|
52
|
+
const state = join(root, '.pbai4devs/estado/agentdevkit.json');
|
|
53
|
+
return adapterManifest(root, { clients: existsSync(state) ? json(state).clients : undefined });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function registeredRoots(root) {
|
|
57
|
+
const names = manifest(root).split(/\r?\n/)
|
|
58
|
+
.filter(line => /^(?:agente|gitignore)\|/.test(line)).map(line => line.split('|')[1]);
|
|
59
|
+
for (const name of names) if (!/^\.[a-z0-9][a-z0-9.-]*$/i.test(name) || ['.git', '.pbai4devs', '.agent-tests'].includes(name.toLowerCase())) fail(`Raiz de agente inválida: ${name}`);
|
|
60
|
+
return [...new Set(names)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resourceTargets(root, source, kind = 'skills') {
|
|
64
|
+
const rows = manifest(root).split(/\r?\n/).filter(line => line && !line.startsWith('#')).map(line => line.split('|'));
|
|
65
|
+
const targets = [];
|
|
66
|
+
for (const [type, agent, layout = 'agrupado'] of rows) {
|
|
67
|
+
if (type !== 'agente') continue;
|
|
68
|
+
const override = rows.find(([entry, target, resource]) => entry === 'formato' && target === agent && resource === kind)?.[3];
|
|
69
|
+
targets.push(`${agent}/${resourcePath(source, kind, override || layout)}`);
|
|
70
|
+
}
|
|
71
|
+
for (const [type, target, origin] of rows) if (type === 'nativo' && origin === source) targets.push(target);
|
|
72
|
+
return [...new Set(targets)];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function copyAllowed(relativePath, directory = false) {
|
|
76
|
+
const parts = portable(relativePath).split('/');
|
|
77
|
+
const name = parts.at(-1);
|
|
78
|
+
if (parts[0] === '.pbai4devs' && ['bin', 'src', 'release', 'tests', 'DEVELOPMENT.md'].includes(parts[1])) return false;
|
|
79
|
+
if (isGeneratedOutput(relativePath, directory)) return false;
|
|
80
|
+
if (parts.some(part => ['.git', '.local', '.cache', 'node_modules', 'logs', '.agent-tests', '__pycache__', '.ssh', '.gnupg'].includes(part.toLowerCase()))) return false;
|
|
81
|
+
if (/^\.playwright/i.test(name) || /\.log$/i.test(name) || name === '.DS_Store') return false;
|
|
82
|
+
if (/^\.env(?:\.|$)/i.test(name) && !/^\.env\.(?:.*\.)?(?:example|sample|template)$/i.test(name)) return false;
|
|
83
|
+
if (/^(?:\.?auth|\.?credentials|secrets?)(?:\.(?:jsonc?|json5|ya?ml|toml|ini|conf))?$/i.test(name)) return false;
|
|
84
|
+
if (/^(?:\.netrc|\.npmrc|\.pypirc|id_(?:rsa|dsa|ecdsa|ed25519)(?:\.pub)?)$/i.test(name)) return false;
|
|
85
|
+
if (/\.(?:pem|p12|pfx|key|jks|keystore)$/i.test(name) || /\.local\.(?:jsonc?|json5|ya?ml|toml|ini|conf)$/i.test(name)) return false;
|
|
86
|
+
if (parts[0] === '.pbai4devs' && parts[1] === 'tools') {
|
|
87
|
+
if (parts.length === 2) return true;
|
|
88
|
+
if (parts.length === 3) return parts[2] === 'README.md' || !['bin', 'npm-cache'].includes(parts[2]);
|
|
89
|
+
return parts.length === 4 && ['TOOL.json', 'SCRIPT.mjs', 'README.md'].includes(parts[3]);
|
|
90
|
+
}
|
|
91
|
+
if (parts[0] === '.pbai4devs' && parts[1] === 'external-skills') return parts.length === 2 || (parts.length === 3 && ['README.md', 'CATALOG.json'].includes(parts[2]));
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function copyAgentStructure(sourceRoot, destination) {
|
|
96
|
+
const roots = ['.pbai4devs', ...registeredRoots(sourceRoot)];
|
|
97
|
+
const stateFile = join(sourceRoot, '.pbai4devs/estado/agentdevkit.json');
|
|
98
|
+
const stateInfo = stat(stateFile);
|
|
99
|
+
if (stateInfo && (!stateInfo.isFile() || stateInfo.isSymbolicLink())) fail('Estado da instalação deve ser arquivo regular.');
|
|
100
|
+
const installation = stateInfo ? json(stateFile) : null;
|
|
101
|
+
if (stateInfo && (!installation || !installation.blocks || typeof installation.blocks !== 'object' || Array.isArray(installation.blocks))) fail('Estado da instalação deve registrar os blocos gerenciados.');
|
|
102
|
+
function managedEntry(name) {
|
|
103
|
+
if (!installation) return true;
|
|
104
|
+
const expected = installation.blocks[name];
|
|
105
|
+
if (typeof expected !== 'string') return false;
|
|
106
|
+
const content = readFileSync(join(sourceRoot, name), 'utf8');
|
|
107
|
+
const begin = '<!-- BEGIN AGENTDEVKIT -->', end = '<!-- END AGENTDEVKIT -->';
|
|
108
|
+
if (content.split(begin).length !== 2 || content.split(end).length !== 2) return false;
|
|
109
|
+
const start = content.indexOf(begin), finish = content.indexOf(end);
|
|
110
|
+
return finish > start && digest(content.slice(start, finish + end.length)) === expected;
|
|
111
|
+
}
|
|
112
|
+
const files = []; const entrypoints = {}; let skipped = 0;
|
|
113
|
+
function copy(file, targetFile = file) {
|
|
114
|
+
const source = join(sourceRoot, file);
|
|
115
|
+
const item = stat(source); if (!item) return;
|
|
116
|
+
if (!copyAllowed(file, item.isDirectory())) { skipped++; return; }
|
|
117
|
+
if (item.isSymbolicLink()) fail(`A cópia recusa links simbólicos: ${file}`);
|
|
118
|
+
if (item.isDirectory()) {
|
|
119
|
+
mkdirSync(join(destination, targetFile), { recursive: true });
|
|
120
|
+
for (const name of readdirSync(source).sort()) copy(join(file, name), join(targetFile, name));
|
|
121
|
+
} else if (item.isFile()) {
|
|
122
|
+
const target = join(destination, targetFile); mkdirSync(dirname(target), { recursive: true }); copyFileSync(source, target); files.push(portable(targetFile));
|
|
123
|
+
} else fail(`Arquivo não regular na origem: ${file}`);
|
|
124
|
+
}
|
|
125
|
+
for (const file of [...roots, 'README.md', '.gitignore', '.gitattributes', '.editorconfig', '.dockerignore', '.env.example', '.env.sample', '.env.template', 'Makefile']) copy(file);
|
|
126
|
+
for (const name of ['AGENTS.md', 'CLAUDE.md', 'GEMINI.md']) {
|
|
127
|
+
const installed = stat(join(sourceRoot, name));
|
|
128
|
+
if (!installed) continue;
|
|
129
|
+
if (!installed.isFile() || installed.isSymbolicLink()) fail(`Entrada de agente deve ser arquivo regular: ${name}`);
|
|
130
|
+
const template = `.pbai4devs/templates/entrypoints/${name}`;
|
|
131
|
+
const selected = managedEntry(name) && stat(join(sourceRoot, template)) ? template : name;
|
|
132
|
+
const info = stat(join(sourceRoot, selected));
|
|
133
|
+
if (info && !info.isFile()) fail(`Entrada de agente deve ser arquivo regular: ${selected}`);
|
|
134
|
+
copy(selected, name);
|
|
135
|
+
if (info) entrypoints[name] = selected;
|
|
136
|
+
}
|
|
137
|
+
const sourceDigest = digest(files.map(file => `${file}\0${digest(readFileSync(join(destination, file)))}`).join('\n'));
|
|
138
|
+
return { roots, copiedFiles: files.length, skipped, files, sourceDigest, entrypoints, toolDefinitions: files.filter(file => /^\.pbai4devs\/tools\/[^/]+\/TOOL\.json$/.test(file)).length };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ownership(root, file, expectedId) {
|
|
142
|
+
if (stat(root)?.isSymbolicLink()) return false;
|
|
143
|
+
const path = join(root, file);
|
|
144
|
+
if (!existsSync(path)) return false;
|
|
145
|
+
const item = lstatSync(path);
|
|
146
|
+
if (!item.isFile() || item.isSymbolicLink()) return false;
|
|
147
|
+
try { const data = json(path); return data.owner === owner && (!expectedId || data.id === expectedId); }
|
|
148
|
+
catch { return false; }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function ensureOutput(output) {
|
|
152
|
+
if (existsSync(output) && lstatSync(output).isSymbolicLink()) fail('A pasta de exemplos não pode ser um link.');
|
|
153
|
+
for (const name of [suiteMarker, 'README.md', 'REPORT.json', '.gitignore']) {
|
|
154
|
+
const item = stat(join(output, name));
|
|
155
|
+
if (item && (!item.isFile() || item.isSymbolicLink())) fail(`Arquivo de saída deve ser regular: ${name}`);
|
|
156
|
+
}
|
|
157
|
+
if (existsSync(output) && [suiteMarker, 'README.md', 'REPORT.json'].some(name => existsSync(join(output, name)))
|
|
158
|
+
&& !ownership(output, suiteMarker)) fail('Índice de exemplos existente não pertence a este gerador.');
|
|
159
|
+
if (existsSync(join(output, 'REPORT.json')) && !ownership(output, 'REPORT.json')) fail('Relatório de exemplos existente não pertence a este gerador.');
|
|
160
|
+
if (existsSync(join(output, '.gitignore')) && readFileSync(join(output, '.gitignore'), 'utf8') !== outputIgnore) fail('Ignore de exemplos existente não pertence a este gerador.');
|
|
161
|
+
mkdirSync(output, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function planning(root, scenario) {
|
|
165
|
+
const state = '.pbai4devs/estado/configuracao/ESTADO.md';
|
|
166
|
+
rmSync(join(root, state), { force: true });
|
|
167
|
+
if (scenario.planning === 'none' && !scenario.app) return;
|
|
168
|
+
write(root, '.pbai4devs/estado/README.md', document('Estado dos exemplos', 'Dados sintéticos deste cenário, sem aprovação real de produto.'));
|
|
169
|
+
let paths = { prd: null, ux_ui: null, design: null, releases: null, specs: null };
|
|
170
|
+
if (scenario.planning !== 'none') {
|
|
171
|
+
for (const folder of ['docs', 'docs/produto', 'docs/design']) write(root, `${folder}/README.md`, document('Documentos do cenário', 'Entradas sintéticas para testar a classificação de maturidade.'));
|
|
172
|
+
paths.prd = 'docs/produto/PRD.md';
|
|
173
|
+
write(root, paths.prd, document('Produto de exemplo', 'Problema e escopo sintéticos. Aprovação é dado de teste, não decisão humana.', `status: ${scenario.planning === 'ready' ? 'aprovado' : 'rascunho'}\n`));
|
|
174
|
+
if (scenario.planning === 'ready') {
|
|
175
|
+
paths.ux_ui = 'docs/produto/UX.md'; paths.design = 'docs/design/DESIGN.md';
|
|
176
|
+
write(root, paths.ux_ui, document('UX do cenário', 'API sem interface visual: não aplicação justificada apenas para o cenário.', 'status: nao-aplicavel\n'));
|
|
177
|
+
write(root, paths.design, document('Design do cenário', 'Node HTTP sem dependências. Decisão sintética para os testes.', 'status: aprovado\n'));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (scenario.released) {
|
|
181
|
+
paths.releases = 'docs/releases';
|
|
182
|
+
write(root, 'docs/releases/README.md', document('Releases de exemplo', 'Histórico sintético do cenário.'));
|
|
183
|
+
write(root, 'docs/releases/1.0.0/README.md', document('Release 1.0.0', 'Documentos sintéticos desta versão.'));
|
|
184
|
+
write(root, 'docs/releases/1.0.0/RELEASE.md', document('Release 1.0.0', 'Publicação sintética para classificar a manutenção.', 'status: publicado\n'));
|
|
185
|
+
}
|
|
186
|
+
if (scenario.configured) {
|
|
187
|
+
paths.specs = 'docs/specs';
|
|
188
|
+
write(root, 'docs/specs/README.md', document('Specs do cenário', 'Incrementos sintéticos aprovados apenas como dados de teste.'));
|
|
189
|
+
write(root, 'docs/specs/http-api/README.md', document('API de exemplo', 'Spec e provas do incremento sintético.'));
|
|
190
|
+
write(root, 'docs/specs/http-api/SPEC.md', document('API HTTP do cenário', '## Critérios de aceite\n\n| Critério | Resultado |\n| --- | --- |\n| AC-001 | GET /health retorna HTTP 200 e status ok. |\n| AC-002 | POST /sum calcula valores e rejeita entrada inválida. |\n\n## Plano de verificação\n\nExecutar os testes Node da API e guardar o log em .agent-tests/.', 'status: aprovado\n'));
|
|
191
|
+
}
|
|
192
|
+
const values = Object.entries(paths).map(([key, value]) => ` ${key}: ${value ?? 'null'}`).join('\n');
|
|
193
|
+
write(root, state, document('Configuração do cenário', 'Somente fixture; não certifica configuração nem aprovação de produção.', `preparacao: concluido\nconfiguracao_tecnica: ${scenario.configured ? 'concluido' : 'pendente'}\ndocumentos:\n${values}\n`));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function application(root, scenario) {
|
|
197
|
+
if (!scenario.app) return { tests: [], scope: [], protected: [], criteria: ['AC-001'] };
|
|
198
|
+
const monorepo = scenario.layout === 'monorepo'; const legacy = scenario.layout === 'legacy';
|
|
199
|
+
const server = monorepo ? 'apps/painel com espaços/src/server.mjs' : legacy ? 'legacy/server.mjs' : 'src/server.mjs';
|
|
200
|
+
const test = monorepo ? 'apps/painel com espaços/tests/api.test.mjs' : legacy ? 'checks/legacy.test.mjs' : 'tests/api.test.mjs';
|
|
201
|
+
let serverText = readFileSync(join(assets, 'server.mjs'), 'utf8');
|
|
202
|
+
if (monorepo) serverText = serverText.replace('const add = (left, right) => left + right;', "import { add } from '../../../packages/cálculo/src/index.mjs';");
|
|
203
|
+
write(root, server, serverText);
|
|
204
|
+
let imported = portable(relative(dirname(test), server)); if (!imported.startsWith('.')) imported = './' + imported;
|
|
205
|
+
write(root, test, readFileSync(join(assets, 'http-api.test.mjs.template'), 'utf8').replace('__SERVER_IMPORT__', imported));
|
|
206
|
+
const tests = [test]; const scope = [server, test]; const preserved = [];
|
|
207
|
+
const scripts = legacy ? { 'dev:old': 'node legacy/server.mjs', 'test:legacy': 'node --test checks/legacy.test.mjs' } : { start: `node "${server}"`, test: `node --test "${test}"` };
|
|
208
|
+
write(root, 'package.json', JSON.stringify({ name: `maturity-${scenario.id}`, version: '1.0.0', private: true, type: 'module', ...(monorepo ? { workspaces: ['apps/*', 'packages/*'] } : {}), scripts }, null, 2) + '\n');
|
|
209
|
+
if (monorepo) {
|
|
210
|
+
write(root, 'apps/painel com espaços/package.json', JSON.stringify({ name: 'painel', private: true, type: 'module', scripts: { start: 'node src/server.mjs', test: 'node --test tests/api.test.mjs' } }, null, 2) + '\n');
|
|
211
|
+
write(root, 'packages/cálculo/package.json', JSON.stringify({ name: 'calculo', private: true, type: 'module', scripts: { test: 'node --test tests/index.test.mjs' } }, null, 2) + '\n');
|
|
212
|
+
write(root, 'packages/cálculo/src/index.mjs', readFileSync(join(assets, 'calculator.mjs')));
|
|
213
|
+
const calculatorTest = 'packages/cálculo/tests/index.test.mjs';
|
|
214
|
+
write(root, calculatorTest, readFileSync(join(assets, 'calculator.test.mjs.template')));
|
|
215
|
+
tests.push(calculatorTest); scope.push('packages/cálculo/src/index.mjs', calculatorTest);
|
|
216
|
+
const manifest = json(join(root, 'package.json'));
|
|
217
|
+
manifest.scripts.test = `node --test ${tests.map(file => JSON.stringify(file)).join(' ')}`;
|
|
218
|
+
write(root, 'package.json', JSON.stringify(manifest, null, 2) + '\n');
|
|
219
|
+
preserved.push('package.json', 'apps/painel com espaços/package.json', 'packages/cálculo/package.json');
|
|
220
|
+
}
|
|
221
|
+
if (legacy) {
|
|
222
|
+
write(root, 'config/legacy.json', '{"mode":"preserve-original-layout"}\n');
|
|
223
|
+
write(root, '.github/workflows/legacy.yml', 'name: Legacy fixture\non: workflow_dispatch\njobs:\n check:\n runs-on: ubuntu-latest\n steps:\n - run: echo legacy-command-preserved\n');
|
|
224
|
+
write(root, '.vscode/settings.json', '{"legacy.setting":"preserve"}\n');
|
|
225
|
+
write(root, 'Makefile', 'legacy-test:\n\tnode --test checks/legacy.test.mjs\n');
|
|
226
|
+
preserved.push(server, test, 'config/legacy.json', '.github/workflows/legacy.yml', '.vscode/settings.json', 'Makefile', 'package.json');
|
|
227
|
+
}
|
|
228
|
+
if (scenario.configured) {
|
|
229
|
+
write(root, '.dockerignore', '.git\n.agent-tests\nnode_modules\n');
|
|
230
|
+
write(root, '.env.example', 'PORT=3000\n');
|
|
231
|
+
write(root, 'Dockerfile', 'FROM node:24-alpine\nWORKDIR /app\nCOPY package.json ./\nCOPY src ./src\nEXPOSE 3000\nCMD ["node", "src/server.mjs"]\n');
|
|
232
|
+
write(root, 'infra/compose/compose.local.yml', 'services:\n app:\n build: ../..\n environment:\n PORT: "3000"\n ports:\n - "127.0.0.1:39001:3000"\n healthcheck:\n test: ["CMD", "node", "-e", "fetch(\'http://127.0.0.1:3000/health\').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]\n interval: 5s\n timeout: 3s\n retries: 5\n');
|
|
233
|
+
write(root, 'Makefile', 'include .env.example\n.PHONY: help doctor setup up down logs test lint build\nhelp:\n\t@echo doctor setup up down logs test lint build\ndoctor:\n\tnode --version\n\tdocker compose -f infra/compose/compose.local.yml config --quiet\nsetup:\n\tnode --version\nup:\n\tdocker compose -f infra/compose/compose.local.yml up -d --wait\ndown:\n\tdocker compose -f infra/compose/compose.local.yml down\nlogs:\n\tdocker compose -f infra/compose/compose.local.yml logs --tail=100\ntest:\n\tnode --test tests/api.test.mjs\nlint:\n\tnode --check src/server.mjs\nbuild:\n\tnode --check src/server.mjs\n');
|
|
234
|
+
}
|
|
235
|
+
return { tests, scope, protected: preserved, criteria: monorepo ? ['AC-001', 'AC-002', 'AC-003'] : ['AC-001', 'AC-002'], spec: scenario.configured ? 'docs/specs/http-api/SPEC.md' : null };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function gitInit(root) {
|
|
239
|
+
const result = spawnSync('git', ['-c', 'init.defaultBranch=main', 'init', '--template=', '-q', root], { encoding: 'utf8' });
|
|
240
|
+
if (result.error || result.status !== 0) fail(`Git init: ${result.error?.message || result.stderr}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function prepareExamples({ sourceRoot = projectRoot, outputRoot = defaultOutput(sourceRoot), scenarios = SCENARIOS, initializeGit = true, renderResources = prepareResources } = {}) {
|
|
244
|
+
requireNode(); sourceRoot = realpathSync(resolve(sourceRoot)); outputRoot = resolve(outputRoot);
|
|
245
|
+
const outputPath = portable(relative(sourceRoot, outputRoot));
|
|
246
|
+
if (!(outputPath === 'examples/generated' || outputPath.startsWith('examples/generated/')
|
|
247
|
+
|| 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.');
|
|
248
|
+
noLinks(sourceRoot, outputRoot);
|
|
249
|
+
const ids = new Set();
|
|
250
|
+
for (const scenario of scenarios) {
|
|
251
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(scenario.id) || ids.has(scenario.id)) fail('Cenários devem ter IDs únicos e seguros.');
|
|
252
|
+
ids.add(scenario.id);
|
|
253
|
+
}
|
|
254
|
+
ensureOutput(outputRoot);
|
|
255
|
+
for (const scenario of scenarios) {
|
|
256
|
+
const destination = join(outputRoot, scenario.id);
|
|
257
|
+
noLinks(sourceRoot, destination);
|
|
258
|
+
if (existsSync(destination) && !ownership(destination, marker, scenario.id)) fail(`Diretório existente não é gerenciado: ${destination}`);
|
|
259
|
+
}
|
|
260
|
+
write(outputRoot, suiteMarker, JSON.stringify({ owner, version: 1 }, null, 2) + '\n');
|
|
261
|
+
write(outputRoot, '.gitignore', outputIgnore);
|
|
262
|
+
rmSync(join(outputRoot, 'REPORT.json'), { force: true });
|
|
263
|
+
writeIndex(outputRoot, [], 'preparing');
|
|
264
|
+
const examples = [];
|
|
265
|
+
for (const scenario of scenarios) {
|
|
266
|
+
const destination = join(outputRoot, scenario.id);
|
|
267
|
+
rmSync(destination, { recursive: true, force: true }); mkdirSync(destination, { recursive: true });
|
|
268
|
+
write(destination, marker, JSON.stringify({ owner, version: 1, id: scenario.id }, null, 2) + '\n');
|
|
269
|
+
const copied = copyAgentStructure(sourceRoot, destination);
|
|
270
|
+
planning(destination, scenario);
|
|
271
|
+
const app = application(destination, scenario);
|
|
272
|
+
write(destination, 'README.md', document(scenario.title, `${scenario.note}\n\nEste é um exemplo gerado. Estados de aprovação/publicação são dados de teste.\nRevisão do gate é simulação contratual, não revisão humana.\n\nOs scripts dos agentes foram copiados, sem pacotes ou credenciais conhecidas.\nO filtro não detecta segredos em conteúdo arbitrário.\n${app.tests.length ? `\nPara repetir os testes: \`${scenario.layout === 'legacy' ? 'npm run test:legacy' : 'npm test'}\`.\nO harness executa os testes Node diretamente, sem instalar dependências.\nO teste abre HTTP em porta efêmera e encerra o servidor.\n` : '\nNão há aplicação; a prova real exercita os scripts copiados.\n'}`, `fase: ${scenario.expected}\n`));
|
|
273
|
+
renderResources(destination);
|
|
274
|
+
if (initializeGit) gitInit(destination);
|
|
275
|
+
examples.push({ ...scenario, root: destination, copied, application: app });
|
|
276
|
+
}
|
|
277
|
+
write(outputRoot, 'REPORT.json', JSON.stringify({ owner, version: 1, status: 'prepared', scenarios: [...ids], results: [] }, null, 2) + '\n');
|
|
278
|
+
writeIndex(outputRoot, examples, 'prepared');
|
|
279
|
+
return examples;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function writeIndex(outputRoot, examples, status) {
|
|
283
|
+
const rows = examples.map(item => `| [${item.id}](${item.id}/README.md) | ${item.expected} | ${item.status || 'prepared'} |`).join('\n');
|
|
284
|
+
const progress = status === 'preparing' ? '\nPreparação em andamento; os resultados anteriores foram invalidados.\n' : '';
|
|
285
|
+
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/\`.\nPastas sem marcador deste gerador são preservadas.\n`, `status: ${status}\n`));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function hashes(root, files) { return Object.fromEntries(files.map(file => [file, digest(readFileSync(join(root, file)))])); }
|
|
289
|
+
|
|
290
|
+
function managedHashes(root, roots) {
|
|
291
|
+
const result = {};
|
|
292
|
+
function visit(relativePath) {
|
|
293
|
+
const full = join(root, relativePath); if (!existsSync(full)) return;
|
|
294
|
+
const info = lstatSync(full); if (info.isSymbolicLink()) fail(`Espelho simbólico: ${relativePath}`);
|
|
295
|
+
if (info.isDirectory()) for (const name of readdirSync(full).sort()) visit(join(relativePath, name));
|
|
296
|
+
else if (info.isFile()) result[portable(relativePath)] = digest(readFileSync(full));
|
|
297
|
+
}
|
|
298
|
+
for (const file of [...roots, '.pbai4devs/catalog.json', '.pbai4devs/skills.json', '.pbai4devs/rules.json', '.pbai4devs/agents.json']) visit(file);
|
|
299
|
+
return result;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function pointer(root, roots) {
|
|
303
|
+
const entry = json(join(root, '.pbai4devs/skills.json')).entries[0]?.path;
|
|
304
|
+
if (!entry) fail('Catálogo copiado não contém uma skill para provar reparação.');
|
|
305
|
+
for (const file of resourceTargets(root, `${entry}/SKILL.md`)) if (roots.includes(file.split('/')[0]) && existsSync(join(root, file))) return file;
|
|
306
|
+
fail('Nenhum apontamento copiado encontrado.');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function runner(root, logRoot, commands) {
|
|
310
|
+
return (script, args = [], expected = 0, label = script.replaceAll('/', '-')) => {
|
|
311
|
+
const command = [script, ...args];
|
|
312
|
+
const environment = { ...process.env }; delete environment.NODE_TEST_CONTEXT;
|
|
313
|
+
const result = spawnSync(process.execPath, command, { cwd: root, env: environment, encoding: 'utf8', timeout: 180000, maxBuffer: 8 * 1024 * 1024, windowsHide: true });
|
|
314
|
+
const output = `${result.stdout || ''}${result.stderr || ''}`;
|
|
315
|
+
const log = write(root, `${logRoot}/${String(commands.length + 1).padStart(2, '0')}-${label}.log`, output || `(sem saída; status ${result.status})\n`);
|
|
316
|
+
commands.push({ command: [process.execPath, ...command], status: result.status, expected, log: portable(relative(root, log)) });
|
|
317
|
+
if (result.error || result.status !== expected) fail(`${label}: esperado ${expected}, recebido ${result.status}: ${result.error?.message || output.slice(-2000)}`);
|
|
318
|
+
return { ...result, output, log: portable(relative(root, log)) };
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function resourceLifecycle(example, run) {
|
|
323
|
+
const root = example.root; const sync = '.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs';
|
|
324
|
+
const initial = `${example.id}-lifecycle-probe`; const renamed = `${initial}-renamed`;
|
|
325
|
+
const folder = name => `.pbai4devs/skills/projeto/${name}`;
|
|
326
|
+
const source = name => `${folder(name)}/SKILL.md`;
|
|
327
|
+
const catalogPath = '.pbai4devs/catalog.json'; const registryPath = '.pbai4devs/skills.json';
|
|
328
|
+
const before = { catalog: readFileSync(join(root, catalogPath), 'utf8'), registry: readFileSync(join(root, registryPath), 'utf8') };
|
|
329
|
+
const rows = readFileSync(join(root, '.pbai4devs/espelhos.conf'), 'utf8').split(/\r?\n/).filter(line => line.startsWith('agente|')).map(line => line.split('|'));
|
|
330
|
+
const pointers = name => resourceTargets(root, source(name));
|
|
331
|
+
const skill = (name, description) => `---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n\nLIFECYCLE_SOURCE_ONLY\n`;
|
|
332
|
+
const assertEntry = (name, description) => {
|
|
333
|
+
const entry = json(join(root, catalogPath)).skills.find(item => item.path === source(name));
|
|
334
|
+
if (!entry || entry.name !== name || entry.description !== description) fail(`Catálogo não refletiu ${name}.`);
|
|
335
|
+
if (!json(join(root, registryPath)).entries.some(item => item.path === folder(name))) fail(`Registro nativo não refletiu ${name}.`);
|
|
336
|
+
for (const file of pointers(name)) {
|
|
337
|
+
const text = readFileSync(join(root, file), 'utf8');
|
|
338
|
+
if (!text.includes(`name: ${name}`) || !text.includes(description) || text.includes('LIFECYCLE_SOURCE_ONLY')) fail(`Apontamento incorreto: ${file}`);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
if (existsSync(join(root, folder(initial))) || existsSync(join(root, folder(renamed)))) fail('A skill temporária já existe na cópia.');
|
|
342
|
+
write(root, source(initial), skill(initial, 'Criada pelo teste de ciclo.'));
|
|
343
|
+
run(sync, ['--check'], 1, 'lifecycle-create-detected'); run(sync, [], 0, 'lifecycle-create-sync');
|
|
344
|
+
assertEntry(initial, 'Criada pelo teste de ciclo.');
|
|
345
|
+
write(root, source(initial), skill(initial, 'Descrição alterada pelo teste.'));
|
|
346
|
+
run(sync, ['--check'], 1, 'lifecycle-edit-detected'); run(sync, [], 0, 'lifecycle-edit-sync');
|
|
347
|
+
assertEntry(initial, 'Descrição alterada pelo teste.');
|
|
348
|
+
renameSync(join(root, folder(initial)), join(root, folder(renamed)));
|
|
349
|
+
write(root, source(renamed), skill(renamed, 'Recurso renomeado pelo teste.'));
|
|
350
|
+
run(sync, ['--check'], 1, 'lifecycle-rename-detected'); run(sync, [], 0, 'lifecycle-rename-sync');
|
|
351
|
+
assertEntry(renamed, 'Recurso renomeado pelo teste.');
|
|
352
|
+
if (pointers(initial).some(file => existsSync(join(root, file))) || json(join(root, catalogPath)).skills.some(item => item.path === source(initial))) fail('Renomeação preservou o recurso antigo.');
|
|
353
|
+
rmSync(join(root, folder(renamed)), { recursive: true });
|
|
354
|
+
run(sync, ['--check'], 1, 'lifecycle-delete-detected'); run(sync, [], 0, 'lifecycle-delete-sync');
|
|
355
|
+
if (pointers(renamed).some(file => existsSync(join(root, file)))) fail('Remoção preservou apontamentos antigos.');
|
|
356
|
+
if (readFileSync(join(root, catalogPath), 'utf8') !== before.catalog || readFileSync(join(root, registryPath), 'utf8') !== before.registry) fail('O ciclo não restaurou os catálogos originais.');
|
|
357
|
+
const corrupted = '{"tampered":true}\n'; write(root, catalogPath, corrupted);
|
|
358
|
+
run(sync, ['--check'], 1, 'catalog-drift-detected');
|
|
359
|
+
const repair = run(sync, ['--force'], 0, 'catalog-drift-repaired');
|
|
360
|
+
const backup = repair.output.match(/Backup local: (.+)/)?.[1]?.trim();
|
|
361
|
+
if (!backup || readFileSync(join(root, backup, catalogPath), 'utf8') !== corrupted) fail('Correção forçada não preservou backup do JSON alterado.');
|
|
362
|
+
rmSync(join(root, catalogPath)); run(sync, ['--check'], 1, 'catalog-missing-detected');
|
|
363
|
+
run(sync, [], 0, 'catalog-missing-repaired'); run(sync, ['--check'], 0, 'lifecycle-clean');
|
|
364
|
+
if (readFileSync(join(root, catalogPath), 'utf8') !== before.catalog || readFileSync(join(root, registryPath), 'utf8') !== before.registry) fail('Catálogos não foram restaurados após corrupção/remoção.');
|
|
365
|
+
return { status: 'passed', operations: ['create', 'edit-description', 'rename', 'delete', 'corrupt-json', 'remove-json'], clients: rows.length, backup, sourceRestored: true };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function reviewContract(example, run, proof) {
|
|
369
|
+
const id = `maturity-${example.id}`; const execution = 'contract';
|
|
370
|
+
const script = '.pbai4devs/scripts/revisar-entrega/SCRIPT.mjs';
|
|
371
|
+
const scope = example.application.scope.length ? example.application.scope : ['README.md'];
|
|
372
|
+
const args = ['prepare', '--id', id, '--execution', execution, ...(example.application.spec ? ['--spec', example.application.spec] : []), ...scope.flatMap(file => ['--scope', file])];
|
|
373
|
+
const prepared = run(script, args, 0, 'review-prepare');
|
|
374
|
+
const reportFile = JSON.parse(prepared.stdout).report;
|
|
375
|
+
run(script, ['check', reportFile], 1, 'review-pending-denied');
|
|
376
|
+
const report = json(join(example.root, reportFile));
|
|
377
|
+
const evidence = `.agent-tests/${id}/${execution}/proof.log`;
|
|
378
|
+
write(example.root, evidence, readFileSync(join(example.root, proof.log)));
|
|
379
|
+
for (const item of report.documentation) Object.assign(item, { decision: 'no-impact', reason: 'Documento de cenário sintético inspecionado para simulação contratual do gate.' });
|
|
380
|
+
report.validation = example.application.criteria.map(criterion => ({ criterion, method: proof.method, prerequisite: 'Node.js 24+; somente fixtures locais, sem pacotes externos.', assertion: criterion === 'AC-001' ? (example.app ? 'GET /health retorna HTTP 200 e status ok.' : 'Os apontamentos copiados passam em sync --check.') : criterion === 'AC-002' ? 'POST /sum calcula 2+3 e rejeita entrada inválida.' : 'O módulo cálculo valida soma e cancelamento.', evidence: [evidence], status: 'passed' }));
|
|
381
|
+
report.review = { status: 'approved', reviewer: 'testar-maturidade:simulacao-contratual-sem-revisao-humana', findings: [] };
|
|
382
|
+
report.simulation = { kind: 'gate-contract-test', humanReview: false, productApproval: false };
|
|
383
|
+
const save = () => write(example.root, reportFile, JSON.stringify(report, null, 2) + '\n');
|
|
384
|
+
report.validation[0].status = 'failed'; save();
|
|
385
|
+
const failed = run(script, ['check', reportFile], 1, 'review-failed-denied');
|
|
386
|
+
if (!failed.output.includes('Criterio sem prova')) fail('O gate não recusou o critério falho pelo motivo esperado.');
|
|
387
|
+
report.validation[0].status = 'passed'; save();
|
|
388
|
+
run(script, ['check', reportFile], 1, 'review-unsealed-denied');
|
|
389
|
+
run(script, ['seal', reportFile], 0, 'review-seal');
|
|
390
|
+
run(script, ['check', reportFile], 0, 'review-positive');
|
|
391
|
+
const changed = scope[0]; const original = readFileSync(join(example.root, changed));
|
|
392
|
+
try {
|
|
393
|
+
write(example.root, changed, Buffer.concat([original, Buffer.from('\n/* maturity stale probe */\n')]));
|
|
394
|
+
const stale = run(script, ['check', reportFile], 1, 'review-stale-denied');
|
|
395
|
+
if (!stale.output.includes('mudaram depois do snapshot')) fail('O gate não recusou alteração posterior pelo motivo esperado.');
|
|
396
|
+
} finally { write(example.root, changed, original); }
|
|
397
|
+
run(script, ['check', reportFile], 0, 'review-restored');
|
|
398
|
+
return { status: 'passed', type: 'contract-simulation', humanReview: false, spec: example.application.spec || null, report: reportFile, evidence, rejected: ['pending', 'failed', 'unsealed', 'stale'] };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function testExample(example) {
|
|
402
|
+
const commands = []; const logRoot = '.agent-tests/maturity';
|
|
403
|
+
const run = runner(example.root, logRoot, commands);
|
|
404
|
+
const result = { id: example.id, expected: example.expected, status: 'failed', copied: { files: example.copied.copiedFiles, roots: example.copied.roots, skipped: example.copied.skipped, sourceDigest: example.copied.sourceDigest, entrypoints: example.copied.entrypoints }, commands, limits: ['Executado somente no SO/runtime deste host.', 'Estado de produto e revisão do gate são dados sintéticos.', 'Build Docker, UI e implantação não são exercitados.', 'Cópia exclui credenciais por nomes conhecidos, sem varredura semântica de segredos.'] };
|
|
405
|
+
const preserved = hashes(example.root, example.application.protected);
|
|
406
|
+
try {
|
|
407
|
+
const sync = '.pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs';
|
|
408
|
+
run(sync, [], 0, 'sync-prepare-copy');
|
|
409
|
+
run(sync, ['--check'], 0, 'sync-initial');
|
|
410
|
+
const damaged = pointer(example.root, example.copied.roots); rmSync(join(example.root, damaged));
|
|
411
|
+
run(sync, ['--check'], 1, 'sync-damage-detected'); run(sync, [], 0, 'sync-repair');
|
|
412
|
+
const proofSync = run(sync, ['--check'], 0, 'sync-repaired');
|
|
413
|
+
const before = managedHashes(example.root, example.copied.roots);
|
|
414
|
+
run(sync, [], 0, 'sync-idempotent');
|
|
415
|
+
if (JSON.stringify(before) !== JSON.stringify(managedHashes(example.root, example.copied.roots))) fail('A segunda sincronização alterou os espelhos.');
|
|
416
|
+
if (['somente-agentes', 'manutencao'].includes(example.id)) result.resourceLifecycle = resourceLifecycle(example, run);
|
|
417
|
+
const resourceCheck = run('.pbai4devs/scripts/verificar-recursos/SCRIPT.mjs', [], 0, 'resources-check');
|
|
418
|
+
if (!resourceCheck.output.includes('recursos=passed') || !resourceCheck.output.includes('markdownlint-cli2=missing')) fail('Verificação nativa/linter ausente não foi reportada como esperado.');
|
|
419
|
+
result.resourceCheck = { status: 'passed', markdownlint: 'not-installed-expected', log: resourceCheck.log };
|
|
420
|
+
const catalog = run('.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs', ['list'], 0, 'tool-catalog');
|
|
421
|
+
const tools = catalog.stdout.trim().split('\n').filter(Boolean).map(line => JSON.parse(line));
|
|
422
|
+
if (!tools.length || tools.length !== example.copied.toolDefinitions || tools.some(tool => tool.installed !== null)) fail('O catálogo deve conter somente as definições copiadas, sem pacotes instalados.');
|
|
423
|
+
const absent = run('.pbai4devs/scripts/preparar-ferramentas/SCRIPT.mjs', ['check', 'base'], 1, 'tools-absent-expected');
|
|
424
|
+
if (!absent.output.includes('Ferramenta ausente')) fail('O check negativo não falhou pela ausência esperada da ferramenta.');
|
|
425
|
+
const diagnosis = JSON.parse(run('.pbai4devs/scripts/diagnosticar-ambiente/SCRIPT.mjs', [], 0, 'diagnosis').stdout);
|
|
426
|
+
if (diagnosis.runtime?.name !== 'node') fail('Diagnóstico não reconheceu Node.');
|
|
427
|
+
const inspection = JSON.parse(run('.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs', [], 0, 'inspection').stdout);
|
|
428
|
+
run('.pbai4devs/scripts/inspecionar-projeto/SCRIPT.mjs', ['--check'], example.complete ? 0 : 1, 'inspection-contract');
|
|
429
|
+
if (inspection.maturity?.level !== example.expected || inspection.contract?.complete !== example.complete) fail(`Maturidade/contrato divergente: ${JSON.stringify(inspection.maturity)}; complete=${inspection.contract?.complete}`);
|
|
430
|
+
if (example.layout === 'monorepo') {
|
|
431
|
+
for (const file of ['apps/painel com espaços/package.json', 'packages/cálculo/package.json']) if (!inspection.manifests.includes(file)) fail(`Monorepo não detectou ${file}`);
|
|
432
|
+
if (existsSync(join(example.root, 'src'))) fail('O cenário monorepo não deve criar scaffold src na raiz.');
|
|
433
|
+
}
|
|
434
|
+
let proof = { log: proofSync.log, method: 'node .pbai4devs/scripts/sincronizar-agentes/SCRIPT.mjs --check' };
|
|
435
|
+
if (example.app) {
|
|
436
|
+
const tested = run('--test', example.application.tests, 0, 'http-tests');
|
|
437
|
+
for (const criterion of example.application.criteria) if (!tested.output.includes(criterion)) fail(`Teste real não registrou ${criterion}.`);
|
|
438
|
+
proof = { log: tested.log, method: `node --test ${example.application.tests.map(file => JSON.stringify(file)).join(' ')}` };
|
|
439
|
+
result.applicationProof = { type: 'real-http', assertions: example.application.criteria, log: tested.log, serverLifetime: 'portas efêmeras, fechadas por t.after' };
|
|
440
|
+
}
|
|
441
|
+
result.reviewGate = reviewContract(example, run, proof);
|
|
442
|
+
if (JSON.stringify(preserved) !== JSON.stringify(hashes(example.root, example.application.protected))) fail('Código ou configuração preexistente do cenário foi alterado.');
|
|
443
|
+
result.preserved = example.application.protected;
|
|
444
|
+
result.maturity = inspection.maturity; result.structure = inspection.contract;
|
|
445
|
+
result.status = 'passed';
|
|
446
|
+
} catch (error) { result.error = error.message; }
|
|
447
|
+
write(example.root, `${logRoot}/REPORT.json`, JSON.stringify(result, null, 2) + '\n');
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export function runExamples(options = {}) {
|
|
452
|
+
const examples = prepareExamples(options);
|
|
453
|
+
const results = examples.map(example => { const result = testExample(example); console.log(`${example.id}=${result.status}${result.error ? ': ' + result.error : ''}`); return result; });
|
|
454
|
+
const outputRoot = resolve(options.outputRoot || defaultOutput(options.sourceRoot || projectRoot));
|
|
455
|
+
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.'] };
|
|
456
|
+
write(outputRoot, 'REPORT.json', JSON.stringify(report, null, 2) + '\n');
|
|
457
|
+
writeIndex(outputRoot, examples.map(example => ({ ...example, status: results.find(result => result.id === example.id).status })), report.status);
|
|
458
|
+
return { outputRoot, report };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function main(args = process.argv.slice(2)) {
|
|
462
|
+
requireNode(); const [mode = 'help', ...rest] = args;
|
|
463
|
+
if (['help', '--help', '-h'].includes(mode)) { console.log('Uso: testar-maturidade prepare|run [--case <id>]. Exemplos gerados em examples/generated; apenas pastas marcadas são substituídas.'); return 0; }
|
|
464
|
+
if (!['prepare', 'run'].includes(mode)) fail('Use prepare ou run.');
|
|
465
|
+
let scenarios = SCENARIOS;
|
|
466
|
+
if (rest.length) {
|
|
467
|
+
if (rest.length !== 2 || rest[0] !== '--case') fail('Use --case <id>.');
|
|
468
|
+
scenarios = SCENARIOS.filter(item => item.id === rest[1]); if (!scenarios.length) fail('Cenário desconhecido.');
|
|
469
|
+
}
|
|
470
|
+
if (mode === 'prepare') { const examples = prepareExamples({ scenarios }); console.log(`prepared=${examples.length}`); return 0; }
|
|
471
|
+
return runExamples({ scenarios }).report.status === 'passed' ? 0 : 1;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (isMain(import.meta.url)) {
|
|
475
|
+
try { process.exitCode = main(); }
|
|
476
|
+
catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
477
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const add = (left, right) => left + right;
|