ai-dev-workflow-cli 0.0.0-stage → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/.continue/prompts/analisar-projeto.md +7 -0
  2. package/.continue/prompts/documentar-task.md +7 -0
  3. package/.continue/prompts/planejar-task.md +9 -0
  4. package/.continue/prompts/revisar-codigo.md +7 -0
  5. package/.continue/rules/workflow.md +12 -0
  6. package/.github/pull_request_template.md +10 -0
  7. package/.gitignore +28 -0
  8. package/AGENTS.md +38 -0
  9. package/CLAUDE.md +7 -0
  10. package/GEMINI.md +7 -0
  11. package/LICENSE +21 -0
  12. package/README.md +93 -2
  13. package/agents/architect.md +5 -0
  14. package/agents/developer.md +5 -0
  15. package/agents/discovery.md +5 -0
  16. package/agents/documentation.md +5 -0
  17. package/agents/environment.md +3 -0
  18. package/agents/reviewer.md +5 -0
  19. package/bin/ai-dev-workflow.js +10 -0
  20. package/catalog/central-v0.2.json +197 -0
  21. package/catalog/legacy-v1.json +158 -0
  22. package/catalog/template.json +207 -0
  23. package/docs/CLI.md +224 -0
  24. package/docs/DISCOVERY_CLI.md +83 -0
  25. package/docs/LAYOUT.md +13 -0
  26. package/docs/MULTI_AI.md +36 -0
  27. package/docs/PERSISTENCE.md +80 -0
  28. package/docs/QUICKSTART.md +219 -0
  29. package/docs/WORKFLOW.md +58 -0
  30. package/integrations/codex/README.md +14 -0
  31. package/integrations/continue/README.md +19 -0
  32. package/package.json +45 -4
  33. package/payload/central/.ai-dev-workflow/agents/architect.md +5 -0
  34. package/payload/central/.ai-dev-workflow/agents/developer.md +5 -0
  35. package/payload/central/.ai-dev-workflow/agents/discovery.md +5 -0
  36. package/payload/central/.ai-dev-workflow/agents/documentation.md +5 -0
  37. package/payload/central/.ai-dev-workflow/agents/environment.md +3 -0
  38. package/payload/central/.ai-dev-workflow/agents/reviewer.md +5 -0
  39. package/payload/central/.ai-dev-workflow/docs/WORKFLOW.md +58 -0
  40. package/payload/central/.ai-dev-workflow/integrations/codex/README.md +14 -0
  41. package/payload/central/.ai-dev-workflow/integrations/continue/README.md +19 -0
  42. package/payload/central/.ai-dev-workflow/rules/cost-control.md +6 -0
  43. package/payload/central/.ai-dev-workflow/rules/database.md +46 -0
  44. package/payload/central/.ai-dev-workflow/rules/general.md +7 -0
  45. package/payload/central/.ai-dev-workflow/rules/git-workflow.md +16 -0
  46. package/payload/central/.ai-dev-workflow/rules/security.md +11 -0
  47. package/payload/central/.ai-dev-workflow/rules/testing.md +6 -0
  48. package/payload/central/.ai-dev-workflow/stacks/nodejs.md +3 -0
  49. package/payload/central/.ai-dev-workflow/stacks/php-laravel.md +6 -0
  50. package/payload/central/.ai-dev-workflow/stacks/php-native.md +3 -0
  51. package/payload/central/.ai-dev-workflow/stacks/wordpress.md +4 -0
  52. package/payload/central/.ai-dev-workflow/templates/ARCHITECTURE.md +8 -0
  53. package/payload/central/.ai-dev-workflow/templates/BASELINE.md +16 -0
  54. package/payload/central/.ai-dev-workflow/templates/CURRENT_STATE.md +15 -0
  55. package/payload/central/.ai-dev-workflow/templates/DATABASE_CHANGE.md +31 -0
  56. package/payload/central/.ai-dev-workflow/templates/DECISIONS.md +9 -0
  57. package/payload/central/.ai-dev-workflow/templates/DISCOVERY.md +13 -0
  58. package/payload/central/.ai-dev-workflow/templates/ENVIRONMENT.md +20 -0
  59. package/payload/central/.ai-dev-workflow/templates/PR_CHECKLIST.md +9 -0
  60. package/payload/central/.ai-dev-workflow/templates/REQUIREMENTS.md +9 -0
  61. package/payload/central/.ai-dev-workflow/templates/TASK.md +26 -0
  62. package/payload/central/.ai-dev-workflow/templates/TASK_REPORT.md +20 -0
  63. package/payload/central/.continue/prompts/analisar-projeto.md +7 -0
  64. package/payload/central/.continue/prompts/documentar-task.md +7 -0
  65. package/payload/central/.continue/prompts/planejar-task.md +9 -0
  66. package/payload/central/.continue/prompts/revisar-codigo.md +7 -0
  67. package/payload/central/.continue/rules/workflow.md +12 -0
  68. package/payload/central/.github/pull_request_template.md +10 -0
  69. package/payload/central/.gitignore.txt +28 -0
  70. package/payload/central/AGENTS.md +38 -0
  71. package/payload/central/CLAUDE.md +7 -0
  72. package/payload/central/GEMINI.md +7 -0
  73. package/payload/central-v0.2/.ai-dev-workflow/agents/architect.md +5 -0
  74. package/payload/central-v0.2/.ai-dev-workflow/agents/developer.md +5 -0
  75. package/payload/central-v0.2/.ai-dev-workflow/agents/discovery.md +5 -0
  76. package/payload/central-v0.2/.ai-dev-workflow/agents/documentation.md +5 -0
  77. package/payload/central-v0.2/.ai-dev-workflow/agents/environment.md +3 -0
  78. package/payload/central-v0.2/.ai-dev-workflow/agents/reviewer.md +5 -0
  79. package/payload/central-v0.2/.ai-dev-workflow/docs/WORKFLOW.md +58 -0
  80. package/payload/central-v0.2/.ai-dev-workflow/integrations/codex/README.md +14 -0
  81. package/payload/central-v0.2/.ai-dev-workflow/integrations/continue/README.md +19 -0
  82. package/payload/central-v0.2/.ai-dev-workflow/rules/cost-control.md +6 -0
  83. package/payload/central-v0.2/.ai-dev-workflow/rules/database.md +46 -0
  84. package/payload/central-v0.2/.ai-dev-workflow/rules/general.md +7 -0
  85. package/payload/central-v0.2/.ai-dev-workflow/rules/git-workflow.md +16 -0
  86. package/payload/central-v0.2/.ai-dev-workflow/rules/security.md +11 -0
  87. package/payload/central-v0.2/.ai-dev-workflow/rules/testing.md +6 -0
  88. package/payload/central-v0.2/.ai-dev-workflow/stacks/nodejs.md +3 -0
  89. package/payload/central-v0.2/.ai-dev-workflow/stacks/php-laravel.md +6 -0
  90. package/payload/central-v0.2/.ai-dev-workflow/stacks/php-native.md +3 -0
  91. package/payload/central-v0.2/.ai-dev-workflow/stacks/wordpress.md +4 -0
  92. package/payload/central-v0.2/.ai-dev-workflow/templates/ARCHITECTURE.md +8 -0
  93. package/payload/central-v0.2/.ai-dev-workflow/templates/BASELINE.md +16 -0
  94. package/payload/central-v0.2/.ai-dev-workflow/templates/CURRENT_STATE.md +15 -0
  95. package/payload/central-v0.2/.ai-dev-workflow/templates/DATABASE_CHANGE.md +31 -0
  96. package/payload/central-v0.2/.ai-dev-workflow/templates/DECISIONS.md +9 -0
  97. package/payload/central-v0.2/.ai-dev-workflow/templates/DISCOVERY.md +13 -0
  98. package/payload/central-v0.2/.ai-dev-workflow/templates/ENVIRONMENT.md +20 -0
  99. package/payload/central-v0.2/.ai-dev-workflow/templates/PR_CHECKLIST.md +9 -0
  100. package/payload/central-v0.2/.ai-dev-workflow/templates/REQUIREMENTS.md +9 -0
  101. package/payload/central-v0.2/.ai-dev-workflow/templates/TASK.md +26 -0
  102. package/payload/central-v0.2/.ai-dev-workflow/templates/TASK_REPORT.md +20 -0
  103. package/payload/central-v0.2/.continue/prompts/analisar-projeto.md +7 -0
  104. package/payload/central-v0.2/.continue/prompts/documentar-task.md +7 -0
  105. package/payload/central-v0.2/.continue/prompts/planejar-task.md +9 -0
  106. package/payload/central-v0.2/.continue/prompts/revisar-codigo.md +7 -0
  107. package/payload/central-v0.2/.continue/rules/workflow.md +12 -0
  108. package/payload/central-v0.2/.github/pull_request_template.md +10 -0
  109. package/payload/central-v0.2/.gitignore.txt +28 -0
  110. package/payload/central-v0.2/AGENTS.md +38 -0
  111. package/payload/legacy-v1/.continue/prompts/analisar-projeto.md +7 -0
  112. package/payload/legacy-v1/.continue/prompts/documentar-task.md +7 -0
  113. package/payload/legacy-v1/.continue/prompts/planejar-task.md +9 -0
  114. package/payload/legacy-v1/.continue/prompts/revisar-codigo.md +7 -0
  115. package/payload/legacy-v1/.continue/rules/workflow.md +12 -0
  116. package/payload/legacy-v1/.github/pull_request_template.md +10 -0
  117. package/payload/legacy-v1/.gitignore.txt +28 -0
  118. package/payload/legacy-v1/AGENTS.md +38 -0
  119. package/payload/legacy-v1/agents/architect.md +5 -0
  120. package/payload/legacy-v1/agents/developer.md +5 -0
  121. package/payload/legacy-v1/agents/discovery.md +5 -0
  122. package/payload/legacy-v1/agents/documentation.md +5 -0
  123. package/payload/legacy-v1/agents/environment.md +3 -0
  124. package/payload/legacy-v1/agents/reviewer.md +5 -0
  125. package/payload/legacy-v1/docs/WORKFLOW.md +58 -0
  126. package/payload/legacy-v1/integrations/codex/README.md +14 -0
  127. package/payload/legacy-v1/integrations/continue/README.md +19 -0
  128. package/payload/legacy-v1/rules/cost-control.md +6 -0
  129. package/payload/legacy-v1/rules/database.md +46 -0
  130. package/payload/legacy-v1/rules/general.md +7 -0
  131. package/payload/legacy-v1/rules/git-workflow.md +16 -0
  132. package/payload/legacy-v1/rules/security.md +11 -0
  133. package/payload/legacy-v1/rules/testing.md +6 -0
  134. package/payload/legacy-v1/stacks/nodejs.md +3 -0
  135. package/payload/legacy-v1/stacks/php-laravel.md +6 -0
  136. package/payload/legacy-v1/stacks/php-native.md +3 -0
  137. package/payload/legacy-v1/stacks/wordpress.md +4 -0
  138. package/payload/legacy-v1/templates/ARCHITECTURE.md +8 -0
  139. package/payload/legacy-v1/templates/BASELINE.md +16 -0
  140. package/payload/legacy-v1/templates/CURRENT_STATE.md +15 -0
  141. package/payload/legacy-v1/templates/DATABASE_CHANGE.md +31 -0
  142. package/payload/legacy-v1/templates/DECISIONS.md +9 -0
  143. package/payload/legacy-v1/templates/DISCOVERY.md +13 -0
  144. package/payload/legacy-v1/templates/ENVIRONMENT.md +20 -0
  145. package/payload/legacy-v1/templates/PR_CHECKLIST.md +9 -0
  146. package/payload/legacy-v1/templates/REQUIREMENTS.md +9 -0
  147. package/payload/legacy-v1/templates/TASK.md +26 -0
  148. package/payload/legacy-v1/templates/TASK_REPORT.md +20 -0
  149. package/rules/cost-control.md +6 -0
  150. package/rules/database.md +46 -0
  151. package/rules/general.md +7 -0
  152. package/rules/git-workflow.md +16 -0
  153. package/rules/security.md +11 -0
  154. package/rules/testing.md +6 -0
  155. package/scripts/check-catalog.js +11 -0
  156. package/src/cli.js +88 -0
  157. package/src/commands/discover.js +2 -0
  158. package/src/commands/doctor.js +1 -0
  159. package/src/commands/init.js +2 -0
  160. package/src/commands/status.js +1 -0
  161. package/src/commands/validate.js +1 -0
  162. package/src/core/adapters.js +9 -0
  163. package/src/core/catalog.js +52 -0
  164. package/src/core/diagnostics.js +82 -0
  165. package/src/core/errors.js +10 -0
  166. package/src/core/install-plan.js +49 -0
  167. package/src/core/installer.js +72 -0
  168. package/src/core/journal.js +28 -0
  169. package/src/core/manifest.js +55 -0
  170. package/src/discovery/detectors.js +156 -0
  171. package/src/discovery/evidence.js +12 -0
  172. package/src/discovery/reader.js +47 -0
  173. package/src/discovery/reconcile.js +18 -0
  174. package/src/discovery/report.js +24 -0
  175. package/src/discovery/scanner.js +60 -0
  176. package/src/interaction/init.js +52 -0
  177. package/src/output/json.js +1 -0
  178. package/src/output/text.js +18 -0
  179. package/src/persistence/history.js +23 -0
  180. package/src/persistence/markdown.js +25 -0
  181. package/src/persistence/record.js +90 -0
  182. package/src/persistence/storage.js +47 -0
  183. package/src/platform/filesystem.js +71 -0
  184. package/src/platform/git.js +123 -0
  185. package/stacks/nodejs.md +3 -0
  186. package/stacks/php-laravel.md +6 -0
  187. package/stacks/php-native.md +3 -0
  188. package/stacks/wordpress.md +4 -0
  189. package/templates/ARCHITECTURE.md +8 -0
  190. package/templates/BASELINE.md +16 -0
  191. package/templates/CURRENT_STATE.md +15 -0
  192. package/templates/DATABASE_CHANGE.md +31 -0
  193. package/templates/DECISIONS.md +9 -0
  194. package/templates/DISCOVERY.md +13 -0
  195. package/templates/ENVIRONMENT.md +20 -0
  196. package/templates/PR_CHECKLIST.md +9 -0
  197. package/templates/REQUIREMENTS.md +9 -0
  198. package/templates/TASK.md +26 -0
  199. package/templates/TASK_REPORT.md +20 -0
@@ -0,0 +1,28 @@
1
+ import fs from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { checkedPath, readKnown, hash, stat } from '../platform/filesystem.js';
4
+ import { fail } from './errors.js';
5
+ export const journalPath = '.ai-dev-workflow/install-journal.jsonl';
6
+ export async function assertReconciled(root) {
7
+ // Até journal vazio/truncado impede adoção; nunca confiar em um evento completed isolado.
8
+ if (await stat(await checkedPath(root, journalPath)))
9
+ fail('INSTALL_INCOMPLETE', 'Journal de instalação presente. Init/status/validate bloqueados até reconciliação humana segura; nenhum arquivo será adotado.');
10
+ }
11
+ export function declaredMode(content) {
12
+ return /^Modo(?: de (?:banco|operação de banco))?:\s*(MANUAL_SQL|AUTOMATIC_LOCAL|PRODUCTION_CONTROLLED)\b/m.exec(content.toString('utf8'))?.[1];
13
+ }
14
+ export async function checkContext(root) {
15
+ const env = await readKnown(root, 'docs/project/ENVIRONMENT.md');
16
+ if (env && !declaredMode(env)) fail('CONTEXT_REVIEW_REQUIRED', 'ENVIRONMENT deve declarar Modo de banco explicitamente; revisar sem sobrescrever o contexto.');
17
+ }
18
+ export async function startJournal(root, plan, track, templateVersion, plannedManifest) {
19
+ const file = await checkedPath(root, journalPath);
20
+ const handle = await fs.open(file, 'wx', 0o600);
21
+ const append = async record => { await handle.writeFile(JSON.stringify(record) + '\n'); await handle.sync(); };
22
+ try {
23
+ await append({ journalVersion: '2.0.0', layoutVersion: plannedManifest.layoutVersion, event: 'begin', id: randomUUID(), track, templateVersion, manifest: plannedManifest,
24
+ files: plan.map(e => ({ path: e.path, kind: e.kind, action: e.action,
25
+ baseHash: e.installedHash, plannedHash: hash(e.content) })) });
26
+ } catch (e) { await handle.close().catch(() => {}); throw e; }
27
+ return { append, close: () => handle.close(), file };
28
+ }
@@ -0,0 +1,55 @@
1
+ import { hash, readKnown } from '../platform/filesystem.js';
2
+ import { fail } from './errors.js';
3
+ import { manifestPath, loadCatalog, contextEntries, semver, sha256 } from './catalog.js';
4
+ export async function readManifest(root, expected) {
5
+ const content = await readKnown(root, manifestPath);
6
+ if (!content) return null;
7
+ let m;
8
+ try { m = JSON.parse(content.toString('utf8')); }
9
+ catch { fail('INVALID_MANIFEST', 'Manifesto não contém JSON válido.'); }
10
+ if (!m || typeof m !== 'object' || Array.isArray(m)) fail('INVALID_MANIFEST', 'Manifesto deve ser objeto JSON.');
11
+ if (m.manifestVersion !== expected.manifestVersion) fail('UNSUPPORTED_MANIFEST', 'Formato do manifesto não suportado; nenhuma migração será executada.');
12
+ if (expected.layoutVersion === 'central-v2' && m.layoutVersion !== 'central-v2') fail('UNSUPPORTED_LAYOUT', 'Layout do manifesto não suportado.');
13
+ if (expected.layoutVersion === 'legacy-v1' && m.layoutVersion !== undefined) fail('UNSUPPORTED_LAYOUT', 'Layout legado ambíguo.');
14
+ if (!semver.test(m.templateVersion) || !semver.test(m.cliVersion) || !['new', 'existing'].includes(m.track) || !Array.isArray(m.files))
15
+ fail('INVALID_MANIFEST', 'Campos de manifesto inválidos.');
16
+ if (m.templateVersion !== expected.templateVersion)
17
+ fail('VERSION_PLAN_REQUIRED', 'Versão do template difere; upgrade/downgrade requer plano humano e não está implementado.');
18
+ if (m.track !== expected.track && expected.track) fail('TRACK_CONFLICT', 'Trilha instalada difere; alteração requer plano humano.');
19
+ const entries = new Map(expected.entries.map(e => [e.path, e]));
20
+ const seen = new Set();
21
+ for (const f of m.files) {
22
+ if (!f || typeof f !== 'object') fail('INVALID_MANIFEST', 'Entrada de manifesto inválida.');
23
+ const entry = entries.get(f.path);
24
+ const legacyGitignore = expected.layoutVersion === 'legacy-v1' && f.path === '.gitignore' && entry?.kind === 'custom' && f.kind === 'managed';
25
+ if (!entry || seen.has(f.path) || (entry.kind !== f.kind && !legacyGitignore) || !sha256.test(f.installedHash) || entry.source !== f.source)
26
+ fail('INVALID_MANIFEST', 'Arquivo inesperado ou inválido no manifesto.');
27
+ if (f.kind === 'managed' && f.installedHash !== hash(entry.content))
28
+ fail('INVALID_MANIFEST', 'Hash base não corresponde ao catálogo distribuído.');
29
+ // Compatibilidade restrita: hash managed foi validado acima; normalizar só em memória.
30
+ if (legacyGitignore) f.kind = 'custom';
31
+ seen.add(f.path);
32
+ }
33
+ if (seen.size !== entries.size) fail('INVALID_MANIFEST', 'Lista de arquivos incompleta.');
34
+ return m;
35
+ }
36
+ export function makeManifest(catalog, version, track, files) {
37
+ return { manifestVersion: catalog.manifestVersion, layoutVersion: catalog.layoutVersion, cliVersion: version, templateVersion: catalog.templateVersion,
38
+ track, files: files.map(f => ({ path: f.path, kind: f.kind, source: f.source, installedHash: f.installedHash })) };
39
+ }
40
+
41
+ // Seleção explícita: nunca reinterpretar manifesto desconhecido como instalação nova.
42
+ export async function installedProfile(root) {
43
+ const raw = await readKnown(root, manifestPath);
44
+ if (!raw) return null;
45
+ let value;
46
+ try { value = JSON.parse(raw.toString('utf8')); }
47
+ catch { fail('INVALID_MANIFEST', 'Manifesto não contém JSON válido.'); }
48
+ if (!value || typeof value !== 'object' || Array.isArray(value)) fail('INVALID_MANIFEST', 'Manifesto deve ser objeto JSON.');
49
+ const layout = value.manifestVersion === '1.0.0' ? 'legacy-v1' : value.manifestVersion === '2.0.0' ? 'central-v2' : null;
50
+ if (!layout) fail('UNSUPPORTED_MANIFEST', 'Formato do manifesto não suportado; nenhuma migração será executada.');
51
+ const catalog = await loadCatalog(layout, value.templateVersion);
52
+ const entries = [...catalog.entries, ...contextEntries(value.track, layout)];
53
+ const manifest = await readManifest(root, { ...catalog, entries });
54
+ return { catalog, manifest };
55
+ }
@@ -0,0 +1,156 @@
1
+ import { evidence, version } from './evidence.js';
2
+ const nodeFrameworks = ['next','react','vue','nuxt','@angular/core','express','fastify','nestjs','@nestjs/core'];
3
+ const phpFrameworks = ['laravel/framework','symfony/framework-bundle','slim/slim','cakephp/cakephp'];
4
+ const drivers = new Map([['mysql','MySQL'],['mysql2','MySQL'],['pg','PostgreSQL'],['sqlite3','SQLite'],['better-sqlite3','SQLite'],['mongodb','MongoDB'],['ext-pdo_mysql','MySQL'],['ext-pdo_pgsql','PostgreSQL'],['ext-pdo_sqlite','SQLite']]);
5
+ export function detect(file) {
6
+ const observations = []; const diagnostics = []; const base = file.path.split('/').at(-1);
7
+ const add = (category,name,value,basis,classification='confirmado',needle='') => observations.push(evidence(file,category,name,value,basis,classification,needle));
8
+ const dependency = (name, constraint, locked=false) => {
9
+ const v=version(constraint);
10
+ if(nodeFrameworks.includes(name)||phpFrameworks.includes(name)) add('framework',name,v,locked?'versão registrada em lockfile':'dependência declarada','confirmado',JSON.stringify(name));
11
+ if(drivers.has(name)) add('database',drivers.get(name),v,'driver declarado; seleção e servidor não comprovados','inferido',JSON.stringify(name));
12
+ };
13
+ let json;
14
+ if(['package.json','package-lock.json','composer.json','composer.lock','Pipfile.lock'].includes(base)) {
15
+ try { json=JSON.parse(file.text); if(!json||typeof json!=='object'||Array.isArray(json))throw new Error(); }
16
+ catch { return {observations,diagnostics:[{code:'MALFORMED_MANIFEST',path:file.path}]}; }
17
+ }
18
+ if(base==='package.json') {
19
+ add('manifest','package.json',null,'manifesto presente');
20
+ const nodeVersion = version(json.engines?.node);
21
+ if (nodeVersion) add('technology','Node.js',nodeVersion,'requisito declarado; runtime instalado não verificado','confirmado','"node"');
22
+ else {
23
+ const declared = json.engines && typeof json.engines === 'object' && Object.hasOwn(json.engines,'node');
24
+ add('technology','Node.js',null,declared ? 'declaração de requisito não interpretável; versão desconhecida' : 'manifesto do ecossistema JavaScript; requisito Node.js não declarado',declared ? 'desconhecido' : 'inferido');
25
+ }
26
+ for(const group of ['dependencies','devDependencies','peerDependencies']) {
27
+ const deps=json[group]; if(!deps||typeof deps!=='object')continue;
28
+ for(const name of [...nodeFrameworks,...drivers.keys()])if(Object.hasOwn(deps,name))dependency(name,deps[name]);
29
+ }
30
+ const manager=/^(npm|pnpm|yarn|bun)@(\d+(?:\.\d+){0,2})$/.exec(typeof json.packageManager==='string'?json.packageManager:'');
31
+ if(manager)add('manager',manager[1],manager[2],'gerenciador declarado','confirmado','"packageManager"');
32
+ }
33
+ if(base==='package-lock.json') {
34
+ add('manager','npm',null,'lockfile presente');
35
+ for(const name of nodeFrameworks) {
36
+ const value=json.packages?.['node_modules/'+name]?.version??json.dependencies?.[name]?.version;
37
+ if(value!==undefined)dependency(name,value,true);
38
+ }
39
+ }
40
+ if(['pnpm-lock.yaml','yarn.lock','bun.lock'].includes(base))add('manager',base==='pnpm-lock.yaml'?'pnpm':base==='yarn.lock'?'yarn':'bun',null,'lockfile presente; resolução interna não interpretada');
41
+ if(base==='composer.json') {
42
+ add('manager','Composer',null,'manifesto presente');
43
+ add('technology','PHP',version(json.require?.php),'requisito declarado; runtime instalado não verificado','confirmado','"php"');
44
+ for(const group of ['require','require-dev'])for(const name of [...phpFrameworks,...drivers.keys()])if(json[group]&&Object.hasOwn(json[group],name))dependency(name,json[group][name]);
45
+ }
46
+ if(base==='composer.lock') {
47
+ add('manager','Composer',null,'lockfile presente');
48
+ for(const group of ['packages','packages-dev'])if(Array.isArray(json[group]))for(const p of json[group])if(p&&phpFrameworks.includes(p.name))dependency(p.name,p.version,true);
49
+ }
50
+ if(['.nvmrc','.node-version','.ruby-version'].includes(base))add('technology',base==='.ruby-version'?'Ruby':'Node.js',version(file.text.trim()),'versão declarada em arquivo; instalação desconhecida');
51
+ if(['pyproject.toml','requirements.txt','poetry.lock','Pipfile.lock'].includes(base)) {
52
+ add('technology','Python',null,'manifesto ou lockfile reconhecido');
53
+ add('manager',base==='poetry.lock'?'Poetry':base==='Pipfile.lock'?'Pipenv':base==='requirements.txt'?'pip':'Python packaging',null,'marcador de configuração');
54
+ const requirement=/^\s*requires-python\s*=\s*["']([^"']+)["']/m.exec(file.text);
55
+ if(requirement)add('technology','Python',version(requirement[1]),'requisito declarado em pyproject; instalação desconhecida','confirmado','requires-python');
56
+ for(const framework of ['django','flask','fastapi']) {
57
+ const pattern=new RegExp('(?:^|[\\s"\\\'])'+framework+'(?:[\\s"\\\'[<>=~!]|$)','im');
58
+ if(pattern.test(file.text))add('framework',framework,null,'indício textual limitado; contexto dinâmico não avaliado','inferido',framework);
59
+ }
60
+ }
61
+ if(['pom.xml','build.gradle','build.gradle.kts'].includes(base)) {
62
+ add('technology','Java/JVM',null,'arquivo de build presente; linguagem/runtime efetivos não verificados','inferido');
63
+ add('manager',base==='pom.xml'?'Maven':'Gradle',null,'arquivo de build presente');
64
+ const target=/<(?:maven.compiler.release|maven.compiler.source|java.version)>\s*(\d+(?:\.\d+)?)\s*<\//.exec(file.text);
65
+ if(target)add('technology','Java/JVM',target[1],'alvo de compilação declarado; runtime instalado desconhecido');
66
+ if(/org\.springframework|spring-boot/.test(file.text))add('framework','Spring',null,'indício textual de dependência','inferido');
67
+ }
68
+ if(['Gemfile','Gemfile.lock'].includes(base)) {
69
+ add('technology','Ruby',null,'manifesto ou lockfile presente');add('manager','Bundler',null,'manifesto ou lockfile presente');
70
+ if(/(?:^\s*gem\s+['"]rails['"]|^\s+rails\s+\()/m.test(file.text))add('framework','Rails',null,'indício estático de dependência','inferido');
71
+ }
72
+ if(base==='Dockerfile'||/^(?:docker-)?compose\.ya?ml$/.test(base)) {
73
+ add('container','Docker',null,'arquivo de configuração presente; execução não verificada');
74
+ const result = containerImages(file.text, base === 'Dockerfile');
75
+ for (const { token, line } of result.images) {
76
+ // Valores e produtos da saída continuam restritos à lista anterior.
77
+ const image=/^(node|php|python|ruby|openjdk|eclipse-temurin|mysql|mariadb|postgres|mongo|redis)(?::(\d+(?:\.\d+){0,3})(?:-[a-z0-9.-]+)?)?$/.exec(token);
78
+ if (!image) { result.ambiguous = true; continue; }
79
+ const db=['mysql','mariadb','postgres','mongo','redis'].includes(image[1]);
80
+ add(db?'database':'technology',image[1],image[2]??null,'imagem declarada no contexto estático suportado; serviço e runtime ativos não verificados','confirmado',base==='Dockerfile'?'FROM':'image');
81
+ observations.at(-1).evidence.line = line;
82
+ }
83
+ if (result.ambiguous) {
84
+ add('container-image','selection',null,'sintaxe de imagem não suportada ou ambígua; nenhuma resolução dinâmica','desconhecido');
85
+ diagnostics.push({code:'CONTAINER_SYNTAX_UNSUPPORTED',path:file.path});
86
+ }
87
+ }
88
+ return { observations, diagnostics };
89
+ }
90
+
91
+ // Reconhecedor conservador: não é parser geral Dockerfile/YAML.
92
+ function containerImages(text, dockerfile) {
93
+ const lines = text.split(/\r?\n/);
94
+ const images = [];
95
+ if (dockerfile) {
96
+ // Conteúdo de heredoc e continuações não são instruções independentes.
97
+ // Sem interpretar shell/delimitadores, tornar todas as imagens desconhecidas.
98
+ if (lines.some(line => /^\s*#\s*escape\s*=/i.test(line) ||
99
+ (!/^\s*#/.test(line) && (line.includes('<<') || /\\\s*$/.test(line)))))
100
+ return {images:[],ambiguous:true};
101
+ let ambiguous = false;
102
+ const stageAliases = new Set();
103
+ for (let i = 0; i < lines.length; i++) {
104
+ if (!/^\s*FROM\b/i.test(lines[i])) continue;
105
+ const match = /^\s*FROM\s+(?:--platform=[a-zA-Z0-9_.\/-]+\s+)?([^\s#]+)(?:\s+AS\s+([a-zA-Z0-9_.-]+))?\s*(?:#.*)?$/i.exec(lines[i]);
106
+ if (match) {
107
+ // Somente aliases definidos anteriormente; imagem externa com tag não
108
+ // coincide com alias. Não expor nem resolver conteúdo do stage local.
109
+ if (!stageAliases.has(match[1].toLowerCase())) images.push({token:match[1],line:i+1});
110
+ if (match[2]) stageAliases.add(match[2].toLowerCase());
111
+ } else ambiguous = true;
112
+ }
113
+ return {images,ambiguous};
114
+ }
115
+ // YAML suportado: services na raiz, serviço a dois espaços, image a quatro.
116
+ // Tabulação, fluxo, aliases, documentos múltiplos e indentação alternativa
117
+ // são conservadoramente desconhecidos, sem interpretar nem seguir referências.
118
+ if (lines.some(line => !/^\s*#/.test(line) &&
119
+ (/\t/.test(line) || /^ */.exec(line)[0].length % 2 !== 0 ||
120
+ (/:\s*["']/.test(line) && !/^ image:/.test(line)) || /^\s*(?:---|\.\.\.)(?:\s|$)/.test(line) || /[&*][a-zA-Z_]|<<:/.test(line))))
121
+ return {images:[],ambiguous:true};
122
+ let services = false; let service = false; let scalarIndent = null; let ambiguous = false;
123
+ let servicesCount = 0; let seenImage = false; const serviceNames = new Set();
124
+ for (let i = 0; i < lines.length; i++) {
125
+ const line = lines[i];
126
+ if (!line.trim() || /^\s*#/.test(line)) continue;
127
+ const indent = /^ */.exec(line)[0].length;
128
+ if (scalarIndent !== null) {
129
+ if (indent > scalarIndent) continue;
130
+ scalarIndent = null;
131
+ }
132
+ if (/^[^#]*:\s*[|>][+-]?[1-9]?\s*(?:#.*)?$/.test(line)) scalarIndent = indent;
133
+ if (indent === 0) {
134
+ services = /^services:\s*(?:#.*)?$/.test(line); service = false;
135
+ if (services && ++servicesCount > 1) ambiguous = true;
136
+ if (/^services:/.test(line) && !services) ambiguous = true;
137
+ continue;
138
+ }
139
+ if (!services) continue;
140
+ if (indent <= 2) {
141
+ const name = /^ ([a-zA-Z0-9_.-]+):\s*(?:#.*)?$/.exec(line);
142
+ service = !!name; seenImage = false;
143
+ if (name) { if (serviceNames.has(name[1])) ambiguous = true; serviceNames.add(name[1]); }
144
+ if (!service) ambiguous = true;
145
+ continue;
146
+ }
147
+ if (!service) { ambiguous = true; continue; }
148
+ // image em environment, labels, extensões ou blocos literais não é imagem de serviço.
149
+ if (/^ image:/.test(line)) {
150
+ if (seenImage) ambiguous = true; seenImage = true;
151
+ const value = /^ image:\s*(?:"([^"\\]*)"|'([^']*)'|([^\s"'#]+))\s*(?:#.*)?$/.exec(line);
152
+ if (value) images.push({token:value[1]??value[2]??value[3],line:i+1}); else ambiguous = true;
153
+ } else if (indent === 4 && /^ ["']image["']:/.test(line)) ambiguous = true;
154
+ }
155
+ return {images:ambiguous ? [] : images,ambiguous};
156
+ }
@@ -0,0 +1,12 @@
1
+ // Nunca encaminhar texto arbitrário do projeto ao relatório.
2
+ export function version(value) {
3
+ return typeof value === 'string' && /\d/.test(value) && /^[v~^<>=0-9xX.*| +,-]{1,64}$/.test(value) ? value : null;
4
+ }
5
+ export function evidence(file, category, name, value, basis, classification = 'confirmado', needle = '') {
6
+ const index = needle ? file.text.indexOf(needle) : -1;
7
+ return { category, name, value, classification, basis, evidence: { path: file.path,
8
+ ...(index >= 0 ? { line: file.text.slice(0,index).split('\n').length } : {}), locator: needle || 'file' } };
9
+ }
10
+ export function unknown(category, name, basis) {
11
+ return { category, name, value: null, classification: 'desconhecido', basis, evidence: null };
12
+ }
@@ -0,0 +1,47 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { constants } from 'node:fs';
4
+ import { checkedPath, safeRelative } from '../platform/filesystem.js';
5
+ import { fail } from '../core/errors.js';
6
+ export const limits = { files: 256, fileBytes: 2 * 1024 * 1024, totalBytes: 8 * 1024 * 1024, modules: 64 };
7
+ export async function readCandidate(root, relative, budget) {
8
+ const file = await checkedPath(root, relative);
9
+ let info;
10
+ try { info = await fs.lstat(file); } catch (e) { if (e.code === 'ENOENT') return null; throw e; }
11
+ if (!info.isFile() || info.nlink !== 1 || await fs.realpath(file) !== file)
12
+ fail('DISCOVERY_UNSAFE_PATH', 'Candidato indireto ou não regular; análise bloqueada.');
13
+ if (++budget.files > limits.files || info.size > limits.fileBytes || (budget.bytes += info.size) > limits.totalBytes)
14
+ fail('DISCOVERY_LIMIT', 'Limite de leitura estática excedido; análise incompleta.');
15
+ const handle = await fs.open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
16
+ try {
17
+ const opened = await handle.stat();
18
+ if (opened.ino !== info.ino || opened.dev !== info.dev || opened.nlink !== 1 || opened.size !== info.size)
19
+ fail('DISCOVERY_UNSAFE_PATH', 'Candidato mudou durante a validação.');
20
+ // Leitura limitada mesmo se outro processo ampliar o arquivo após stat.
21
+ const bytes = Buffer.alloc(info.size + 1);
22
+ let offset = 0;
23
+ while (offset < bytes.length) {
24
+ const result = await handle.read(bytes, offset, bytes.length - offset, null);
25
+ if (!result.bytesRead) break;
26
+ offset += result.bytesRead;
27
+ }
28
+ const after = await handle.stat();
29
+ if (offset !== info.size || after.size !== info.size || after.mtimeMs !== opened.mtimeMs || after.nlink !== 1)
30
+ fail('DISCOVERY_UNSAFE_PATH', 'Candidato mudou durante a leitura.');
31
+ return bytes.subarray(0, offset).toString('utf8');
32
+ } finally { await handle.close(); }
33
+ }
34
+ export async function safeDirectory(root, relative) {
35
+ if (!safeRelative(relative)) fail('UNSAFE_PATH', 'Caminho fora do contrato do template.');
36
+ let candidate = root;
37
+ // Validar somente os componentes reais, inclusive raiz, sem nome auxiliar.
38
+ for (const part of [null, ...relative.split('/')]) {
39
+ if (part !== null) candidate = path.join(candidate, part);
40
+ let info;
41
+ try { info = await fs.lstat(candidate); }
42
+ catch (e) { if (e.code === 'ENOENT') return null; throw e; }
43
+ if (!info.isDirectory() || info.isSymbolicLink() || await fs.realpath(candidate) !== candidate)
44
+ fail('DISCOVERY_UNSAFE_PATH', 'Diretório indireto ou não regular; análise bloqueada.');
45
+ }
46
+ return candidate;
47
+ }
@@ -0,0 +1,18 @@
1
+ import { unknown } from './evidence.js';
2
+ export function reconcile(observations, diagnostics) {
3
+ // Não escolher silenciosamente uma versão entre requisitos, lock e imagem.
4
+ const groups = new Map();
5
+ for(const item of observations) {
6
+ const key=item.category+':'+item.name;
7
+ if(!groups.has(key))groups.set(key,new Set());
8
+ if(item.value!==null)groups.get(key).add(item.value);
9
+ }
10
+ for(const [key,values] of groups)if(values.size>1)diagnostics.push({code:'MULTIPLE_VERSION_EVIDENCE',subject:key});
11
+ observations.push(unknown('runtime','installed','Análise estática não verifica runtimes ou pacotes instalados.'),
12
+ unknown('database','active','Sem leitura de credenciais, conexão ou execução de banco.'),
13
+ unknown('ci','execution','Presença de configuração não comprova execução ou sucesso.'));
14
+ if(!observations.some(o=>o.category==='framework'))observations.push(unknown('framework','selection','Nenhum framework reconhecido; ausência não comprovada.'));
15
+ observations.sort((a,b)=>JSON.stringify(a).localeCompare(JSON.stringify(b),'en'));
16
+ diagnostics.sort((a,b)=>JSON.stringify(a).localeCompare(JSON.stringify(b),'en'));
17
+ return {observations,diagnostics};
18
+ }
@@ -0,0 +1,24 @@
1
+ import { inspectDiscoveryGit } from '../platform/git.js';
2
+ import { fail } from '../core/errors.js';
3
+ import { resolveTarget } from '../platform/filesystem.js';
4
+ import { scan } from './scanner.js';
5
+ import { detect } from './detectors.js';
6
+ import { reconcile } from './reconcile.js';
7
+ export async function discover(target, allowNoGit = false) {
8
+ const root=await resolveTarget(target);
9
+ const git=await inspectDiscoveryGit(root,allowNoGit);
10
+ if(!git.safe)fail(git.code,git.message,git.exitCode);
11
+ const inventory=await scan(root);
12
+ const observations=[];const diagnostics=[...inventory.diagnostics];
13
+ for(const file of inventory.files) {
14
+ const result=detect(file);observations.push(...result.observations);diagnostics.push(...result.diagnostics);
15
+ }
16
+ for(const item of inventory.ci)observations.push({category:'ci',name:item.name,value:null,classification:'confirmado',basis:'arquivo presente; conteúdo e execução não verificados',evidence:{path:item.path,locator:'presence'}});
17
+ const report=reconcile(observations,diagnostics);
18
+ return {ok:true,exitCode:0,reportVersion:'1.0.0',mode:git.mode,head:git.head??'unknown',readOnly:true,
19
+ databasePolicy:'MANUAL_SQL',approvals:'Nenhuma aprovação ou execução de banco concedida.',
20
+ coverage:{profile:'bounded-static-v1',filesRead:inventory.budget.files,bytesRead:inventory.budget.bytes,
21
+ moduleContainers:['apps','packages','services'],partial:report.diagnostics.length>0,
22
+ excluded:['credentials','environment files','installed dependencies','workflow payloads','external references']},
23
+ ...report,message:git.mode==='no-git'?'Discovery limitado sem Git; raiz fornecida não comprova identidade do projeto.':'Discovery estático somente leitura; fatos declarados não comprovam execução.'};
24
+ }
@@ -0,0 +1,60 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { readCandidate, safeDirectory, limits } from './reader.js';
4
+ import { fail } from '../core/errors.js';
5
+ const candidates = ['package.json','package-lock.json','pnpm-lock.yaml','yarn.lock','bun.lock','composer.json','composer.lock','.nvmrc','.node-version',
6
+ 'pyproject.toml','requirements.txt','poetry.lock','Pipfile.lock','pom.xml','build.gradle','build.gradle.kts',
7
+ 'Gemfile','Gemfile.lock','.ruby-version','Dockerfile','compose.yml','compose.yaml','docker-compose.yml','docker-compose.yaml'];
8
+ export async function scan(root) {
9
+ const files = []; const diagnostics = []; const budget = { files: 0, bytes: 0 }; let modules = 0;
10
+ async function visit(prefix = '') {
11
+ if (prefix) {
12
+ // Não entrar em outro repositório, mesmo sem Git no alvo principal.
13
+ if (await fs.lstat(path.join(root, prefix, '.git')).catch(e => { if (e.code === 'ENOENT') return null; throw e; })) {
14
+ diagnostics.push({ code: 'NESTED_PROJECT_EXCLUDED', path: prefix }); return;
15
+ }
16
+ }
17
+ for (const name of candidates) {
18
+ const relative = prefix ? prefix + '/' + name : name;
19
+ const text = await readCandidate(root, relative, budget);
20
+ if (text !== null) files.push({ path: relative, text });
21
+ }
22
+ }
23
+ await visit();
24
+ // Descoberta limitada: módulos diretamente sob três contêineres conhecidos.
25
+ for (const container of ['apps','packages','services']) {
26
+ const dir = await safeDirectory(root, container); if (!dir) continue;
27
+ // Limitar entradas antes de acumular listagem em memória.
28
+ const listing = await fs.opendir(dir);
29
+ for await (const child of listing) {
30
+ if (++modules > limits.modules) fail('DISCOVERY_LIMIT', 'Limite de entradas de módulos excedido.');
31
+ if (!/^[a-zA-Z0-9_-]{1,64}$/.test(child.name)) { diagnostics.push({ code: 'MODULE_NAME_EXCLUDED' }); continue; }
32
+ const relative = container + '/' + child.name;
33
+ if (child.isSymbolicLink()) fail('DISCOVERY_UNSAFE_PATH', 'Link em módulo selecionado.');
34
+ if (child.isDirectory() && await safeDirectory(root, relative)) await visit(relative);
35
+ }
36
+ }
37
+ // CI: somente presença de arquivos, sem leitura de YAML/scripts potencialmente sensíveis.
38
+ const ci = [];
39
+ const workflow = await safeDirectory(root, '.github/workflows');
40
+ if (workflow) {
41
+ let count = 0;
42
+ const listing = await fs.opendir(workflow);
43
+ for await (const entry of listing) {
44
+ if (++count > limits.files) fail('DISCOVERY_LIMIT', 'Limite de entradas CI excedido.');
45
+ if (!/^[a-zA-Z0-9_.-]{1,80}\.ya?ml$/.test(entry.name)) continue;
46
+ const file = path.join(workflow, entry.name); const info = await fs.lstat(file);
47
+ if (!info.isFile() || info.nlink !== 1 || info.isSymbolicLink()) fail('DISCOVERY_UNSAFE_PATH', 'Arquivo CI indireto.');
48
+ ci.push({ name: 'GitHub Actions', path: '.github/workflows/' + entry.name });
49
+ }
50
+ }
51
+ for (const [name, relative] of [['GitLab CI','.gitlab-ci.yml'],['Jenkins','Jenkinsfile']]) {
52
+ const { checkedPath } = await import('../platform/filesystem.js');
53
+ const file = await checkedPath(root, relative);
54
+ const info = await fs.lstat(file).catch(e => { if (e.code === 'ENOENT') return null; throw e; });
55
+ if (!info) continue;
56
+ if (!info.isFile() || info.nlink !== 1) fail('DISCOVERY_UNSAFE_PATH', 'Arquivo CI indireto.');
57
+ ci.push({ name, path: relative });
58
+ }
59
+ return { files, diagnostics, ci, budget };
60
+ }
@@ -0,0 +1,52 @@
1
+ import { createInterface } from 'node:readline';
2
+ import { checkedPath, stat } from '../platform/filesystem.js';
3
+ import { inspectGit } from '../platform/git.js';
4
+ import { installedProfile } from '../core/manifest.js';
5
+ import { assertReconciled } from '../core/journal.js';
6
+ import { fail } from '../core/errors.js';
7
+ import { run as init } from '../commands/init.js';
8
+
9
+ export async function prepareInit(target, io) {
10
+ const git = await inspectGit(target);
11
+ if (!git.safe) fail(git.code, git.message, git.exitCode);
12
+ await assertReconciled(target);
13
+ const installed = await installedProfile(target);
14
+ let track = installed?.manifest.track;
15
+ if (!track) {
16
+ let hint=false;
17
+ // Somente presença de manifestos conhecidos; não ler conteúdo/configurações/segredos.
18
+ for (const name of ['package.json','composer.json','pyproject.toml','requirements.txt','pom.xml','build.gradle','build.gradle.kts','Gemfile','Cargo.toml','go.mod']) {
19
+ const info = await stat(await checkedPath(target, name));
20
+ if (info?.nlink !== undefined && info.nlink !== 1) fail('UNSAFE_PATH', 'Manifesto indireto; revisar o destino.');
21
+ if (info) hint = true;
22
+ }
23
+ if (hint) io.out('Indício de projeto existente: manifesto conhecido presente. Confirme a trilha; presença de manifesto não determina a fase do projeto.\n');
24
+ }
25
+ const rl = createInterface({ input: io.input ?? process.stdin, terminal: false, crlfDelay: Infinity });
26
+ const lines = rl[Symbol.asyncIterator]();
27
+ const ask = async prompt => {
28
+ io.out(prompt);
29
+ const line = await lines.next();
30
+ return line.done ? null : line.value.trim().toLowerCase();
31
+ };
32
+ const cancelled = { result: { ok:true, code:'CANCELLED', message:'Instalação cancelada; nenhum arquivo escrito.' } };
33
+ try {
34
+ if (!track) {
35
+ const answer = await ask('Trilha não determinada (diretório vazio também é ambíguo). [1] Projeto novo [2] Projeto existente: ');
36
+ if (answer === null) return cancelled;
37
+ track = ['1','new'].includes(answer) ? 'new' : ['2','existing'].includes(answer) ? 'existing' : null;
38
+ if (!track) fail('ARGUMENTS', 'Escolha new ou existing; nenhuma escrita. Também pode usar init --track new|existing.', 3);
39
+ }
40
+ const plan = await init({ target, track, dryRun:true });
41
+ io.out(`Plano para ${target}\nTrilha: ${track}\n`);
42
+ for (const [label, actions] of [['Criar',['create']],['Preservar',['unchanged','preserve-custom']],['Conflitos',['conflict']]]) {
43
+ const files = plan.operations.filter(e => actions.includes(e.action)).map(e => e.path);
44
+ if (files.length) io.out(`${label} (${files.length}): ${files.join(', ')}\n`);
45
+ }
46
+ if (!plan.ok) return { result:plan };
47
+ io.out('Serão adicionados arquivos de regras, agentes e documentação para trabalhar com IA. Seu código e suas dependências serão preservados.\n');
48
+ const answer = await ask('Deseja adicionar o AI Dev Workflow a este projeto? (s/N) ');
49
+ if (answer !== 's') return cancelled;
50
+ return { track };
51
+ } finally { rl.close(); }
52
+ }
@@ -0,0 +1 @@
1
+ export const renderJson = result => JSON.stringify(result, null, 2) + '\n';
@@ -0,0 +1,18 @@
1
+ export function renderText(result) {
2
+ const lines = [result.message ?? (result.ok ? 'OK' : 'Falha')];
3
+ if (result.code) lines.push(`Código: ${result.code}`);
4
+ for (const field of ['layoutVersion', 'templateVersion', 'manifestVersion', 'installedTrack'])
5
+ if (result[field]) lines.push(`${field}: ${result[field]}`);
6
+ for (const e of result.operations ?? []) lines.push(`${e.action}: ${e.path}`);
7
+ for (const e of result.files ?? []) lines.push(`${e.state} (${e.kind}): ${e.path}`);
8
+ for (const e of result.findings ?? []) lines.push(`${e.code}: ${e.path}${e.reference ? ` -> ${e.reference}` : ''}`);
9
+ for (const e of result.checks ?? []) {
10
+ lines.push(`${e.ok ? 'OK' : 'FALHA'} ${e.code}: ${e.message}`);
11
+ for (const finding of e.findings ?? []) lines.push(` ${finding.code}: ${finding.path}${finding.reference ? ` -> ${finding.reference}` : ''}`);
12
+ }
13
+ for (const e of result.observations ?? []) lines.push(`${e.classification}: ${e.category}/${e.name} = ${e.value ?? 'não determinado'} (${e.basis})${e.evidence ? ` [${e.evidence.path}${e.evidence.line ? ':' + e.evidence.line : ''}]` : ''}`);
14
+ for (const e of result.diagnostics ?? []) lines.push(`Diagnóstico: ${e.code}${e.path ? ' [' + e.path + ']' : ''}`);
15
+ if (result.persistence) lines.push(`Persistência: ${result.persistence.ok?'concluída':'não concluída'}${result.persistence.current?' / Discovery '+result.persistence.current:''}${result.persistence.code?' / '+result.persistence.code:''}`);
16
+ if (result.created?.length) lines.push(`Criados: ${result.created.join(', ')}`);
17
+ return lines.join('\n') + '\n';
18
+ }
@@ -0,0 +1,23 @@
1
+ import { fail } from '../core/errors.js';
2
+ import { limits } from './storage.js';
3
+ export const commands=['init','discover','doctor','validate','status'];
4
+ export const uuid=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
5
+ const sha=/^[a-f0-9]{64}$/;
6
+ const keys=(value,allowed)=>value&&typeof value==='object'&&!Array.isArray(value)&&Object.keys(value).every(k=>allowed.includes(k));
7
+ export function parseHistory(data){
8
+ if(!data?.length)return [];
9
+ const text=data.toString('utf8');
10
+ if(!Buffer.from(text).equals(data)||!text.endsWith('\n'))fail('HISTORY_INVALID','Histórico inválido/truncado; nenhuma correção automática.');
11
+ const events=[];const seen=new Set();
12
+ for(const line of text.slice(0,-1).split('\n')){
13
+ if(Buffer.byteLength(line+'\n')>limits.event)fail('PERSISTENCE_LIMIT','Evento excede limite.');
14
+ let e;try{e=JSON.parse(line);}catch{fail('HISTORY_INVALID','Histórico não contém JSONL válido.');}
15
+ if(!keys(e,['historyVersion','id','timestamp','cliVersion','command','result','operationExitCode','summaryCode','discovery'])||e.historyVersion!=='1.0.0'||typeof e.id!=='string'||!uuid.test(e.id)||seen.has(e.id)||
16
+ typeof e.timestamp!=='string'||!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(e.timestamp)||!Number.isFinite(Date.parse(e.timestamp))||new Date(e.timestamp).toISOString()!==e.timestamp||
17
+ typeof e.cliVersion!=='string'||!/^\d{1,6}\.\d{1,6}\.\d{1,6}$/.test(e.cliVersion)||!commands.includes(e.command)||![0,1,2,3].includes(e.operationExitCode)||
18
+ e.result!==(e.operationExitCode===0?'completed':e.operationExitCode===1?'error':'blocked')||e.summaryCode!==['COMPLETED','OPERATIONAL_ERROR','BLOCKED','INVALID_ARGUMENTS'][e.operationExitCode])fail('HISTORY_INVALID','Schema de histórico inválido; revisão humana necessária.');
19
+ if(e.discovery!==undefined&&(!keys(e.discovery,['snapshot','sha256','current'])||e.command!=='discover'||e.operationExitCode!==0||typeof e.discovery.snapshot!=='string'||!uuid.test(e.discovery.snapshot.slice(0,-3))||!e.discovery.snapshot.endsWith('.md')||e.discovery.snapshot!==e.id+'.md'||typeof e.discovery.sha256!=='string'||!sha.test(e.discovery.sha256)||!['created','updated','preserved-custom'].includes(e.discovery.current)))fail('HISTORY_INVALID','Referência de Discovery inválida.');
20
+ seen.add(e.id);events.push(e);
21
+ }
22
+ return events;
23
+ }
@@ -0,0 +1,25 @@
1
+ import { version } from '../discovery/evidence.js';
2
+ import { safeRelative } from '../platform/filesystem.js';
3
+ import { limits } from './storage.js';
4
+ import { fail } from '../core/errors.js';
5
+ const names = new Set(['Node.js','PHP','Python','Java/JVM','Ruby','next','react','vue','nuxt','@angular/core','express','fastify','nestjs','@nestjs/core','laravel/framework','symfony/framework-bundle','slim/slim','cakephp/cakephp','django','flask','fastapi','Spring','Rails','MySQL','PostgreSQL','SQLite','MongoDB','mysql','mariadb','postgres','mongo','redis','node','php','python','ruby','openjdk','eclipse-temurin','npm','pnpm','yarn','bun','Composer','Poetry','Pipenv','pip','Python packaging','Maven','Gradle','Bundler','Docker','GitHub Actions','GitLab CI','Jenkins','installed','active','execution','selection','package.json']);
6
+ const bases = {technology:'Declaração/indício estático; instalação desconhecida.',framework:'Dependência/indício estático; revisão humana necessária.',database:'Driver/configuração; banco ativo desconhecido.',ci:'Presença/estado não funcional; execução não comprovada.',manager:'Manifesto/lock; gerenciador não executado.',container:'Configuração presente; nenhum build executado.',manifest:'Manifesto presente.',runtime:'Não verificado.', 'container-image':'Sintaxe não suportada ou ambígua.'};
7
+ const escape = value=>String(value).replace(/[\u0000-\u001f\u007f]/g,' ').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/[\\`|\[\]*_]/g,'\\$&');
8
+ export function discoveryMarkdown(report,id,timestamp){
9
+ const lines=['# Discovery — AI Dev Workflow','',`Registro: ${id}`,`Data UTC: ${timestamp}`,'Status G1: pendente. Política padrão MANUAL_SQL; este relatório não seleciona modo nem concede execução de banco.','',
10
+ '## Stack, declarações e evidências','Confirmado significa fato declarado no arquivo; não comprova runtime instalado ou serviço ativo.','',
11
+ '| Categoria | Tecnologia | Classificação | Valor declarado | Evidência | Limitação / base |','|---|---|---|---|---|---|'];
12
+ if(!Array.isArray(report.observations)||report.observations.length>10000)fail('PERSISTENCE_LIMIT','Relatório excede cobertura permitida.');
13
+ for(const o of report.observations){
14
+ if(!['confirmado','inferido','desconhecido'].includes(o.classification))fail('DISCOVERY_INVALID','Classificação inválida.');
15
+ // Somente a projeção segura do detector, nunca objeto/saída bruta.
16
+ const category=Object.hasOwn(bases,o.category)?o.category:'unknown';
17
+ const name=names.has(o.name)?o.name:'não reconhecido';
18
+ let evidence='não disponível';
19
+ if(o.evidence&&typeof o.evidence.path==='string'&&safeRelative(o.evidence.path))evidence=o.evidence.path+(Number.isSafeInteger(o.evidence.line)&&o.evidence.line>0?':'+o.evidence.line:'');
20
+ lines.push('| '+[category,name,o.classification,version(o.value)??'não determinado',evidence,bases[category]??'Não interpretado.'].map(escape).join(' | ')+' |');
21
+ }
22
+ lines.push('','## Limitações','Análise estática limitada; sem leitura de credenciais, execução de código, rede ou banco.','Runtimes instalados, banco ativo e execução de CI permanecem desconhecidos.','Não substitui Discovery humano completo, baseline funcional ou aprovação G1.','Campos não reconhecidos e formatos dinâmicos não são resolvidos.');
23
+ if(report.coverage?.partial)lines.push('Cobertura parcial: revisar os diagnósticos e exclusões no resultado da CLI.');
24
+ const data=Buffer.from(lines.join('\n')+'\n');if(data.length>limits.report)fail('PERSISTENCE_LIMIT','Relatório excede limite.');return data;
25
+ }