@jaimevalasek/aioson 1.30.2 → 1.36.0

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 (158) hide show
  1. package/CHANGELOG.md +75 -0
  2. package/README.md +19 -6
  3. package/docs/en/1-understand/ecosystem-map.md +45 -29
  4. package/docs/en/1-understand/glossary.md +5 -5
  5. package/docs/en/1-understand/what-is-aioson.md +5 -5
  6. package/docs/en/2-start/existing-project.md +7 -7
  7. package/docs/en/2-start/first-project.md +32 -38
  8. package/docs/en/2-start/initial-decisions.md +18 -17
  9. package/docs/en/3-recipes/README.md +2 -2
  10. package/docs/en/3-recipes/continuity-between-sessions.md +2 -2
  11. package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +5 -2
  12. package/docs/en/3-recipes/full-feature-with-sheldon.md +327 -338
  13. package/docs/en/4-agents/README.md +28 -14
  14. package/docs/en/4-agents/discovery-design-doc.md +20 -8
  15. package/docs/en/4-agents/forge-run.md +3 -2
  16. package/docs/en/5-reference/cli-reference.md +51 -48
  17. package/docs/en/5-reference/executable-verification.md +10 -7
  18. package/docs/en/5-reference/parallel.md +2 -0
  19. package/docs/en/5-reference/qa-browser.md +2 -2
  20. package/docs/en/README.md +3 -3
  21. package/docs/pt/1-entender/glossario.md +4 -4
  22. package/docs/pt/1-entender/mapa-do-ecossistema.md +32 -21
  23. package/docs/pt/2-comecar/decisoes-iniciais.md +16 -16
  24. package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
  25. package/docs/pt/3-receitas/README.md +1 -1
  26. package/docs/pt/3-receitas/app-saas-do-zero.md +35 -122
  27. package/docs/pt/3-receitas/feature-completa-com-sheldon.md +289 -338
  28. package/docs/pt/4-agentes/analyst.md +9 -5
  29. package/docs/pt/4-agentes/architect.md +9 -5
  30. package/docs/pt/4-agentes/dev.md +19 -7
  31. package/docs/pt/4-agentes/discovery-design-doc.md +25 -23
  32. package/docs/pt/4-agentes/orchestrator.md +164 -118
  33. package/docs/pt/4-agentes/pentester.md +5 -5
  34. package/docs/pt/4-agentes/pm.md +15 -7
  35. package/docs/pt/4-agentes/sheldon.md +166 -99
  36. package/docs/pt/4-agentes/ux-ui.md +7 -4
  37. package/docs/pt/5-referencia/agent-chain-continuity.md +14 -5
  38. package/docs/pt/5-referencia/comandos-cli.md +1 -1
  39. package/docs/pt/5-referencia/harness-retro.md +2 -1
  40. package/docs/pt/5-referencia/sdd-automation-scripts.md +6 -6
  41. package/docs/pt/5-referencia/sdd-framework.md +53 -16
  42. package/docs/pt/5-referencia/sdd-planos-e-estrutura.md +27 -15
  43. package/docs/pt/README.md +4 -4
  44. package/docs/pt/agentes.md +48 -50
  45. package/package.json +2 -2
  46. package/src/artifact-kinds.js +110 -0
  47. package/src/cli.js +82 -42
  48. package/src/commands/agent-epilogue.js +251 -186
  49. package/src/commands/agents.js +104 -48
  50. package/src/commands/audit-code.js +344 -0
  51. package/src/commands/classify.js +75 -13
  52. package/src/commands/feature-close.js +43 -18
  53. package/src/commands/harness-check.js +259 -175
  54. package/src/commands/harness-retro-promote.js +387 -0
  55. package/src/commands/live.js +24 -0
  56. package/src/commands/prototype-check.js +163 -0
  57. package/src/commands/review-feature.js +189 -0
  58. package/src/commands/runtime.js +81 -66
  59. package/src/commands/sync-agents-copy.js +115 -0
  60. package/src/commands/verification-plan.js +116 -0
  61. package/src/commands/verify-artifact.js +530 -0
  62. package/src/commands/verify-implementation.js +428 -0
  63. package/src/commands/workflow-execute.js +309 -309
  64. package/src/commands/workflow-next.js +361 -19
  65. package/src/commands/workflow-plan.js +5 -5
  66. package/src/constants.js +4 -0
  67. package/src/gateway-pointer-merge.js +7 -1
  68. package/src/handoff-contract.js +267 -172
  69. package/src/harness/contract-integrity-gate.js +172 -0
  70. package/src/harness/contract-integrity.js +111 -0
  71. package/src/harness/contract-schema.js +377 -332
  72. package/src/harness/detect-runtime-feature.js +90 -0
  73. package/src/harness/static-criteria.js +192 -0
  74. package/src/i18n/messages/en.js +8 -4
  75. package/src/i18n/messages/es.js +8 -4
  76. package/src/i18n/messages/fr.js +8 -4
  77. package/src/i18n/messages/pt-BR.js +8 -4
  78. package/src/install-wizard.js +8 -8
  79. package/src/installer.js +13 -6
  80. package/src/lib/retro/retro-render.js +10 -1
  81. package/src/lib/retro/retro-sources.js +45 -27
  82. package/src/lib/retro/verification-reports.js +230 -0
  83. package/src/parser.js +6 -0
  84. package/src/preflight-engine.js +12 -12
  85. package/src/runtime-store.js +13 -9
  86. package/src/verification/evidence-bundle.js +251 -0
  87. package/src/verification/ledger-store.js +221 -0
  88. package/src/verification/path-policy.js +74 -0
  89. package/src/verification/policy-engine.js +95 -0
  90. package/src/verification/prompt-package.js +314 -0
  91. package/src/verification/redaction.js +77 -0
  92. package/src/verification/report-parser.js +132 -0
  93. package/src/verification/report-store.js +97 -0
  94. package/src/verification/result.js +16 -0
  95. package/src/verification/runners/index.js +319 -0
  96. package/src/verification/runtime-telemetry.js +144 -0
  97. package/src/verification/schema.js +276 -0
  98. package/src/verification/source-discovery.js +153 -0
  99. package/src/verification-policy.js +398 -0
  100. package/src/version.js +52 -1
  101. package/template/.aioson/agents/analyst.md +9 -6
  102. package/template/.aioson/agents/architect.md +34 -5
  103. package/template/.aioson/agents/briefing-refiner.md +25 -0
  104. package/template/.aioson/agents/briefing.md +69 -12
  105. package/template/.aioson/agents/committer.md +2 -1
  106. package/template/.aioson/agents/copywriter.md +30 -21
  107. package/template/.aioson/agents/design-hybrid-forge.md +28 -15
  108. package/template/.aioson/agents/dev.md +28 -10
  109. package/template/.aioson/agents/deyvin.md +3 -2
  110. package/template/.aioson/agents/discover.md +12 -3
  111. package/template/.aioson/agents/discovery-design-doc.md +7 -2
  112. package/template/.aioson/agents/genome.md +19 -10
  113. package/template/.aioson/agents/neo.md +30 -30
  114. package/template/.aioson/agents/orache.md +20 -11
  115. package/template/.aioson/agents/orchestrator.md +84 -7
  116. package/template/.aioson/agents/pm.md +8 -8
  117. package/template/.aioson/agents/product.md +15 -11
  118. package/template/.aioson/agents/profiler-enricher.md +20 -11
  119. package/template/.aioson/agents/profiler-forge.md +20 -11
  120. package/template/.aioson/agents/profiler-researcher.md +20 -11
  121. package/template/.aioson/agents/qa.md +87 -17
  122. package/template/.aioson/agents/scope-check.md +19 -5
  123. package/template/.aioson/agents/setup.md +12 -1
  124. package/template/.aioson/agents/sheldon.md +92 -9
  125. package/template/.aioson/agents/site-forge.md +11 -2
  126. package/template/.aioson/agents/squad.md +20 -5
  127. package/template/.aioson/agents/ux-ui.md +4 -2
  128. package/template/.aioson/agents/validator.md +33 -1
  129. package/template/.aioson/config/verification.json +61 -0
  130. package/template/.aioson/config.md +13 -9
  131. package/template/.aioson/docs/LAYERS.md +2 -2
  132. package/template/.aioson/docs/autopilot-handoff.md +10 -10
  133. package/template/.aioson/docs/dev/execution-discipline.md +41 -0
  134. package/template/.aioson/docs/dev/phase-loop.md +47 -0
  135. package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
  136. package/template/.aioson/docs/presets/workflow.config.full-merged.json +15 -0
  137. package/template/.aioson/docs/presets/workflow.config.lean.json +15 -0
  138. package/template/.aioson/docs/product/prd-contract.md +12 -12
  139. package/template/.aioson/docs/prototype-contract.md +98 -0
  140. package/template/.aioson/docs/reference-identity.md +94 -0
  141. package/template/.aioson/docs/sheldon/harness-contract.md +155 -28
  142. package/template/.aioson/docs/verification-config.md +82 -0
  143. package/template/.aioson/docs/verify-artifact-gates.md +91 -0
  144. package/template/.aioson/docs/workflow-lean-lane.md +129 -0
  145. package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +2 -0
  146. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +16 -14
  147. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +2 -2
  148. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -4
  149. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +15 -15
  150. package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
  151. package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
  152. package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
  153. package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
  154. package/template/.aioson/skills/process/prototype-forge/SKILL.md +98 -0
  155. package/template/.aioson/skills/process/reference-identity-extract/SKILL.md +164 -0
  156. package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +23 -2
  157. package/template/.aioson/skills/static/multi-agent-patterns.md +5 -5
  158. package/template/CLAUDE.md +15 -11
@@ -0,0 +1,344 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson audit:code [path] [--changed] [--category=TODO,ANTI_PATTERN,DUPLICATION,DEAD_CODE]
5
+ * [--json] [--strict] — deterministic, build-free code-quality scan.
6
+ *
7
+ * The non-security half of a categorized code-quality audit, written in pure
8
+ * Node so it runs cross-platform with no python/grep/tsc dependency. SECURITY is
9
+ * NOT re-implemented here — `aioson security:audit` owns secrets/logs/controls.
10
+ *
11
+ * Categories (each a separate concern @qa can consume one at a time, one
12
+ * category cleared per pass):
13
+ *
14
+ * TODO residual TODO/FIXME/XXX/placeholder/not-implemented MED
15
+ * ANTI_PATTERN eval / new Function / innerHTML / `: any` / coerce.boolean HIGH·MED
16
+ * DEAD_CODE unused named imports (build-free TS6133 heuristic) MED
17
+ * DUPLICATION a string literal repeated 3+ times across 2+ files LOW
18
+ *
19
+ * Exit code: a HIGH finding sets exit 1 (blocking); MED/LOW
20
+ * are advisory and never fail the exit. Pass `suppressExitCode` for programmatic
21
+ * callers (the workflow gate / @qa dispatch), matching security:audit.
22
+ */
23
+
24
+ const path = require('node:path');
25
+ const fs = require('node:fs');
26
+
27
+ const { gitChangedFiles } = require('../harness/detect-runtime-feature');
28
+
29
+ const VERSION = '1.0.0';
30
+ const GENERATOR = `aioson audit:code@${VERSION}`;
31
+
32
+ const ALL_CATEGORIES = Object.freeze(['TODO', 'ANTI_PATTERN', 'DEAD_CODE', 'DUPLICATION']);
33
+
34
+ const IGNORE_DIRS = new Set([
35
+ 'node_modules', '.git', 'dist', 'build', 'out', 'coverage',
36
+ '.next', '.nuxt', '.svelte-kit', 'vendor', '.aioson', '.venv', '__pycache__'
37
+ ]);
38
+
39
+ const CODE_EXTS = new Set([
40
+ '.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx',
41
+ '.py', '.rb', '.go', '.java', '.php', '.vue', '.svelte', '.css', '.scss', '.html'
42
+ ]);
43
+ const JS_TS_EXTS = new Set(['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx']);
44
+ const TS_EXTS = new Set(['.ts', '.tsx']);
45
+
46
+ const MAX_FILE_BYTES = 512 * 1024;
47
+
48
+ // ─── TODO / placeholder ───────────────────────────────────────────────────────
49
+
50
+ const TODO_PATTERNS = [
51
+ /\bTODO\b/,
52
+ /\bFIXME\b/,
53
+ /\bXXX\b/,
54
+ /\bHACK\b/,
55
+ /\bplaceholder\b/i,
56
+ /not\s+implemented/i,
57
+ /throw\s+new\s+Error\s*\(\s*['"`]not\s+impl/i
58
+ ];
59
+
60
+ // ─── Anti-patterns (JS/TS unless noted) ───────────────────────────────────────
61
+
62
+ const ANTI_PATTERNS = [
63
+ { re: /\beval\s*\(/, sev: 'HIGH', msg: 'eval() in code', ext: JS_TS_EXTS },
64
+ { re: /\bnew\s+Function\s*\(/, sev: 'HIGH', msg: 'new Function() (dynamic code)', ext: JS_TS_EXTS },
65
+ { re: /\.innerHTML\s*=/, sev: 'HIGH', msg: 'innerHTML assignment (use textContent / safe DOM API)', ext: JS_TS_EXTS },
66
+ { re: /dangerouslySetInnerHTML/, sev: 'HIGH', msg: 'dangerouslySetInnerHTML', ext: JS_TS_EXTS },
67
+ { re: /z\.coerce\.boolean\s*\(/, sev: 'HIGH', msg: 'z.coerce.boolean() ("false" coerces to true — use z.enum)', ext: JS_TS_EXTS },
68
+ { re: /:\s*any\b/, sev: 'MED', msg: '`: any` type (use unknown or an explicit type; // any-ok to allow)', ext: TS_EXTS, skip: (line) => line.includes('any-ok') },
69
+ { re: /\bconsole\.(log|debug|trace)\s*\(/, sev: 'MED', msg: 'console.log/debug/trace left in code', ext: JS_TS_EXTS, prodOnly: true }
70
+ ];
71
+
72
+ // ─── helpers ──────────────────────────────────────────────────────────────────
73
+
74
+ const SEVERITY_RANK = Object.freeze({ HIGH: 3, MED: 2, LOW: 1 });
75
+
76
+ function isProdFile(rel) {
77
+ const lower = rel.toLowerCase();
78
+ return !(
79
+ /(^|[\\/])(test|tests|__tests__|spec|scripts|examples?|fixtures?|mocks?)([\\/]|$)/.test(lower) ||
80
+ /\.(test|spec)\.[a-z]+$/.test(lower) ||
81
+ /\.config\.[a-z]+$/.test(lower)
82
+ );
83
+ }
84
+
85
+ function listSourceFiles(root) {
86
+ const out = [];
87
+ const stack = [root];
88
+ while (stack.length) {
89
+ const dir = stack.pop();
90
+ let entries;
91
+ try {
92
+ entries = fs.readdirSync(dir, { withFileTypes: true });
93
+ } catch {
94
+ continue;
95
+ }
96
+ for (const entry of entries) {
97
+ const full = path.join(dir, entry.name);
98
+ if (entry.isDirectory()) {
99
+ if (!IGNORE_DIRS.has(entry.name)) stack.push(full);
100
+ } else if (entry.isFile() && CODE_EXTS.has(path.extname(entry.name).toLowerCase())) {
101
+ out.push(full);
102
+ }
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+
108
+ function readText(abs) {
109
+ try {
110
+ const stat = fs.statSync(abs);
111
+ if (stat.size > MAX_FILE_BYTES) return null;
112
+ return fs.readFileSync(abs, 'utf8');
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function isCommentLine(stripped) {
119
+ return stripped.startsWith('//') || stripped.startsWith('*') || stripped.startsWith('#') || stripped.startsWith('/*');
120
+ }
121
+
122
+ // ─── category scanners ─────────────────────────────────────────────────────────
123
+
124
+ function scanTodo(rel, ext, lines, push) {
125
+ lines.forEach((line, i) => {
126
+ for (const re of TODO_PATTERNS) {
127
+ if (re.test(line)) {
128
+ push({ category: 'TODO', severity: 'MED', message: 'TODO/FIXME/placeholder/not-implemented residual', file: rel, line: i + 1, snippet: line.trim().slice(0, 120) });
129
+ break;
130
+ }
131
+ }
132
+ });
133
+ }
134
+
135
+ function scanAntiPatterns(rel, ext, lines, push) {
136
+ const prod = isProdFile(rel);
137
+ lines.forEach((line, i) => {
138
+ const stripped = line.trim();
139
+ if (isCommentLine(stripped)) return;
140
+ for (const ap of ANTI_PATTERNS) {
141
+ if (ap.ext && !ap.ext.has(ext)) continue;
142
+ if (ap.prodOnly && !prod) continue;
143
+ if (ap.skip && ap.skip(line)) continue;
144
+ if (ap.re.test(line)) {
145
+ push({ category: 'ANTI_PATTERN', severity: ap.sev, message: ap.msg, file: rel, line: i + 1, snippet: stripped.slice(0, 120) });
146
+ }
147
+ }
148
+ });
149
+ }
150
+
151
+ /**
152
+ * DEAD_CODE — unused named imports, build-free. For each named import / require
153
+ * destructure, the local name must appear more than once in the file (the import
154
+ * itself is occurrence #1). A barrel re-export (`export { X } from`) suppresses
155
+ * the flag. This is the static analogue of tsc TS6133 without needing a build.
156
+ */
157
+ function scanDeadCode(rel, ext, content, lines, push) {
158
+ if (!JS_TS_EXTS.has(ext) || rel.endsWith('.d.ts')) return;
159
+ const specs = [];
160
+ const importRe = /import\s+(?:type\s+)?(?:[A-Za-z0-9_$]+\s*,\s*)?\{([^}]*)\}\s*from\s*['"][^'"]+['"]/gs;
161
+ const requireRe = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\(/gs;
162
+ for (const re of [importRe, requireRe]) {
163
+ let m;
164
+ while ((m = re.exec(content)) !== null) {
165
+ for (const raw of m[1].split(',')) {
166
+ const part = raw.trim().replace(/^type\s+/, '');
167
+ if (!part) continue;
168
+ const local = (part.includes(' as ') ? part.split(/\s+as\s+/)[1] : part).trim();
169
+ if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(local)) specs.push(local);
170
+ }
171
+ }
172
+ }
173
+ if (!specs.length) return;
174
+ for (const name of [...new Set(specs)]) {
175
+ // Re-exported names are "used" (barrel files).
176
+ if (new RegExp(`export\\s*\\{[^}]*\\b${name}\\b`).test(content)) continue;
177
+ const occurrences = (content.match(new RegExp(`\\b${name}\\b`, 'g')) || []).length;
178
+ if (occurrences <= 1) {
179
+ const idx = lines.findIndex((l) => new RegExp(`\\b${name}\\b`).test(l));
180
+ push({ category: 'DEAD_CODE', severity: 'MED', message: `unused import "${name}" (declared, never used)`, file: rel, line: idx >= 0 ? idx + 1 : 1, snippet: idx >= 0 ? lines[idx].trim().slice(0, 120) : '' });
181
+ }
182
+ }
183
+ }
184
+
185
+ /**
186
+ * DUPLICATION — a string literal (12..80 chars) that appears 3+ times across 2+
187
+ * files is a constant waiting to drift. Pure text; comments and URLs ignored.
188
+ */
189
+ function scanDuplication(files, push) {
190
+ const literalRe = /["'`]([^"'`\n]{12,80})["'`]/g;
191
+ const ignore = new Set(['use strict', 'application/json', 'text/plain', 'Content-Type', 'utf-8', 'utf8']);
192
+ const occ = new Map(); // literal -> [{file,line}]
193
+ for (const { rel, lines } of files) {
194
+ lines.forEach((line, i) => {
195
+ const stripped = line.trim();
196
+ if (isCommentLine(stripped)) return;
197
+ let m;
198
+ literalRe.lastIndex = 0;
199
+ while ((m = literalRe.exec(line)) !== null) {
200
+ const lit = m[1];
201
+ if (ignore.has(lit) || lit.startsWith('http://') || lit.startsWith('https://') || /^[\s./\\-]+$/.test(lit)) continue;
202
+ if (!occ.has(lit)) occ.set(lit, []);
203
+ occ.get(lit).push({ file: rel, line: i + 1 });
204
+ }
205
+ });
206
+ }
207
+ for (const [lit, places] of occ) {
208
+ const files2 = new Set(places.map((p) => p.file));
209
+ if (places.length >= 3 && files2.size >= 2) {
210
+ push({
211
+ category: 'DUPLICATION', severity: 'LOW',
212
+ message: `literal "${lit.slice(0, 50)}" repeated ${places.length}x across ${files2.size} files (extract a constant)`,
213
+ file: places[0].file, line: places[0].line,
214
+ snippet: places.slice(0, 4).map((p) => `${p.file}:${p.line}`).join(', ')
215
+ });
216
+ }
217
+ }
218
+ }
219
+
220
+ // ─── core scan ──────────────────────────────────────────────────────────────────
221
+
222
+ /**
223
+ * @param {object} params
224
+ * @param {string} params.targetDir
225
+ * @param {string[]} [params.fileList] — absolute paths to scan; default = full walk
226
+ * @param {Set<string>} params.categories
227
+ * @returns {{ findings: Array, scanned: number }}
228
+ */
229
+ function scanCodeQuality({ targetDir, fileList, categories }) {
230
+ const abs = Array.isArray(fileList) && fileList.length
231
+ ? fileList.filter((f) => CODE_EXTS.has(path.extname(f).toLowerCase()))
232
+ : listSourceFiles(targetDir);
233
+
234
+ const findings = [];
235
+ const push = (f) => findings.push(f);
236
+ const loaded = []; // { rel, ext, content, lines } for duplication pass
237
+
238
+ for (const file of abs) {
239
+ const content = readText(file);
240
+ if (content === null) continue;
241
+ const rel = path.relative(targetDir, file).split(path.sep).join('/');
242
+ const ext = path.extname(file).toLowerCase();
243
+ const lines = content.split('\n');
244
+ loaded.push({ rel, ext, content, lines });
245
+
246
+ if (categories.has('TODO')) scanTodo(rel, ext, lines, push);
247
+ if (categories.has('ANTI_PATTERN')) scanAntiPatterns(rel, ext, lines, push);
248
+ if (categories.has('DEAD_CODE')) scanDeadCode(rel, ext, content, lines, push);
249
+ }
250
+ if (categories.has('DUPLICATION')) scanDuplication(loaded, push);
251
+
252
+ findings.sort((a, b) =>
253
+ (SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]) ||
254
+ a.category.localeCompare(b.category) ||
255
+ a.file.localeCompare(b.file) ||
256
+ (a.line - b.line)
257
+ );
258
+ return { findings, scanned: loaded.length };
259
+ }
260
+
261
+ function parseCategories(option) {
262
+ if (!option) return new Set(ALL_CATEGORIES);
263
+ const wanted = String(option).split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
264
+ const valid = wanted.filter((c) => ALL_CATEGORIES.includes(c));
265
+ return new Set(valid.length ? valid : ALL_CATEGORIES);
266
+ }
267
+
268
+ async function runAuditCode({ args, options = {}, logger }) {
269
+ const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
270
+ const suppressExitCode = Boolean(options.suppressExitCode);
271
+ const setExitCode = (code) => { if (!suppressExitCode) process.exitCode = code; };
272
+ const categories = parseCategories(options.category);
273
+
274
+ let fileList = null;
275
+ if (options.changed) {
276
+ fileList = gitChangedFiles(targetDir).map((rel) => path.resolve(targetDir, rel));
277
+ }
278
+
279
+ const { findings, scanned } = scanCodeQuality({ targetDir, fileList, categories });
280
+
281
+ const bySeverity = { HIGH: 0, MED: 0, LOW: 0 };
282
+ const byCategory = {};
283
+ for (const f of findings) {
284
+ bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
285
+ byCategory[f.category] = (byCategory[f.category] || 0) + 1;
286
+ }
287
+
288
+ const strict = Boolean(options.strict);
289
+ const blocking = bySeverity.HIGH > 0 || (strict && bySeverity.MED > 0);
290
+
291
+ const report = {
292
+ generator: GENERATOR,
293
+ root: targetDir,
294
+ scanned_files: scanned,
295
+ categories: [...categories],
296
+ scope: options.changed ? 'changed' : 'full',
297
+ total: findings.length,
298
+ by_severity: bySeverity,
299
+ by_category: byCategory,
300
+ ok: !blocking,
301
+ findings
302
+ };
303
+
304
+ // Persist for @qa to consume by category across invocations.
305
+ try {
306
+ const ctxDir = path.join(targetDir, '.aioson', 'context');
307
+ fs.mkdirSync(ctxDir, { recursive: true });
308
+ fs.writeFileSync(path.join(ctxDir, 'audit-code.json'), JSON.stringify(report, null, 2), 'utf8');
309
+ } catch {
310
+ // best-effort persistence — never fail the scan
311
+ }
312
+
313
+ if (options.json) {
314
+ logger.log(JSON.stringify(report, null, 2));
315
+ setExitCode(blocking ? 1 : 0);
316
+ return report;
317
+ }
318
+
319
+ logger.log(`Code audit — ${scanned} file(s) scanned (${report.scope}) — ${findings.length} finding(s)`);
320
+ if (bySeverity.HIGH || bySeverity.MED || bySeverity.LOW) {
321
+ logger.log(` HIGH: ${bySeverity.HIGH} MED: ${bySeverity.MED} LOW: ${bySeverity.LOW}`);
322
+ }
323
+ for (const cat of ALL_CATEGORIES) {
324
+ if (byCategory[cat]) logger.log(` ${cat}: ${byCategory[cat]}`);
325
+ }
326
+ for (const f of findings.slice(0, 50)) {
327
+ logger.log(` [${f.severity}] ${f.category} ${f.file}:${f.line} — ${f.message}`);
328
+ }
329
+ if (findings.length > 50) logger.log(` … and ${findings.length - 50} more (see .aioson/context/audit-code.json)`);
330
+ logger.log(blocking ? ' AUDIT=FAIL (HIGH findings present)' : ' AUDIT=OK (no blocking findings)');
331
+
332
+ setExitCode(blocking ? 1 : 0);
333
+ return report;
334
+ }
335
+
336
+ module.exports = {
337
+ runAuditCode,
338
+ scanCodeQuality,
339
+ parseCategories,
340
+ ALL_CATEGORIES,
341
+ // exported for tests
342
+ scanDeadCode,
343
+ scanDuplication
344
+ };
@@ -116,44 +116,80 @@ const COMPLEXITY_SOME_PATTERNS = [
116
116
  /\b(notification|trigger|event)\b/gi
117
117
  ];
118
118
 
119
+ // Fold diacritics so the surface detectors match a localized PRD the same way in
120
+ // any supported language: "cartões"->"cartoes", "gestão"->"gestao",
121
+ // "autenticação"->"autenticacao". Patterns below stay ASCII and run against the
122
+ // folded text, so EN and pt-BR phrasings of the same surface are detected
123
+ // identically. The detector recognizes the surface from the words present in the
124
+ // text — it does NOT depend on the configured interaction_language.
125
+ function foldDiacritics(content) {
126
+ return String(content || '').normalize('NFD').replace(/\p{Diacritic}/gu, '');
127
+ }
128
+
119
129
  // Sensitive-surface floor (Gap 3B): a feature touching any of these surfaces is
120
130
  // never MICRO. Mirrors the secure-tdd sensitive list in @dev. The floor can only
121
131
  // RAISE the tier (MICRO -> SMALL); it never lowers it. Keep patterns tight — a
122
- // false positive needlessly costs the SMALL chain. Tune as the project learns.
132
+ // false positive needlessly costs the SMALL chain. Patterns are bilingual
133
+ // (EN + pt-BR) and matched against diacritic-folded text. Tune as the project learns.
123
134
  const SENSITIVE_SURFACE_PATTERNS = [
124
- { surface: 'money', re: /\b(money|stripe|paypal|braintree|square|payments?|payouts?|refunds?|subscriptions?|billing|invoices?|credit card)\b/i },
125
- { surface: 'auth', re: /\b(oauth|jwt|saml|sso|auth0|firebase auth|log[- ]?in|sign[- ]?in|sign[- ]?up|passwords?|authenticat\w*|2fa|mfa)\b/i },
126
- { surface: 'authz', re: /\b(authoriz\w*|access control|role[- ]based|rbac|ownership|owner[- ]only|only the owner)\b/i },
127
- { surface: 'uploads', re: /\b(file uploads?|uploads?|attachments?)\b/i },
128
- { surface: 'external_url', re: /\b(webhooks?|callback urls?|ssrf|user[- ]?supplied urls?)\b/i },
129
- { surface: 'secrets', re: /\b(secrets?|api keys?|credentials?|private key|access tokens?)\b/i },
130
- { surface: 'sensitive_storage', re: /\b(pii|personal data|ssn|sensitive (data|storage|information))\b/i }
135
+ { surface: 'money', re: /\b(money|stripe|paypal|braintree|square|payments?|payouts?|refunds?|subscriptions?|billing|invoices?|credit card|pagamentos?|pagar|cobran[cç]as?|faturas?|assinaturas?|reembolsos?|cartao de credito|boleto|pix)\b/i },
136
+ { surface: 'auth', re: /\b(oauth|jwt|saml|sso|auth0|firebase auth|log[- ]?in|sign[- ]?in|sign[- ]?up|passwords?|authenticat\w*|2fa|mfa|autentica\w*|senhas?|cadastr(o|os|ar))\b/i },
137
+ { surface: 'authz', re: /\b(authoriz\w*|access control|role[- ]based|rbac|ownership|owner[- ]only|only the owner|autoriza(cao|r)|controle de acesso|apenas o dono|somente o dono)\b/i },
138
+ { surface: 'uploads', re: /\b(file uploads?|uploads?|attachments?|anexos?|envio de arquivos?|upload de arquivos?)\b/i },
139
+ { surface: 'external_url', re: /\b(webhooks?|callback urls?|ssrf|user[- ]?supplied urls?|url de retorno|urls? fornecidas?)\b/i },
140
+ { surface: 'secrets', re: /\b(secrets?|api keys?|credentials?|private key|access tokens?|segredos?|chaves? de api|credenciais|chave privada|tokens? de acesso)\b/i },
141
+ { surface: 'sensitive_storage', re: /\b(pii|personal data|ssn|sensitive (data|storage|information)|dados (pessoais|sensiveis)|cpf|informac\w* sensivel)\b/i }
131
142
  ];
132
143
 
133
144
  function detectSensitiveSurfaces(content) {
145
+ const c = foldDiacritics(content);
134
146
  const found = [];
135
147
  for (const { surface, re } of SENSITIVE_SURFACE_PATTERNS) {
136
- if (re.test(content)) found.push(surface);
148
+ if (re.test(c)) found.push(surface);
137
149
  }
138
150
  return found;
139
151
  }
140
152
 
153
+ // Operational-surface floor: a rich operational surface (workspaces, boards +
154
+ // cards, Kanban/CRM pipelines, explicit CRUD/admin management) is never MICRO —
155
+ // it needs management screens, so it must get at least the SMALL chain so
156
+ // @analyst/@architect/the prototype are not skipped. Patterns are bilingual
157
+ // (EN + pt-BR), matched against diacritic-folded text, and kept tight to avoid
158
+ // flooring genuinely simple features. Bare "dashboard"/"quadro" is intentionally
159
+ // NOT a signal (too common); only admin/management surfaces and board+card pairs count.
160
+ function detectRichSurfaces(content) {
161
+ const c = foldDiacritics(content);
162
+ const found = [];
163
+ if (/\b(kanban|trello|scrum board|task board|quadro kanban|quadro de tarefas)\b/i.test(c)) found.push('kanban');
164
+ if (/\bworkspaces?\b/i.test(c) || /\bespacos? de trabalho\b/i.test(c)) found.push('workspace');
165
+ if ((/\bboards?\b/i.test(c) && /\bcards?\b/i.test(c))
166
+ || (/\bquadros?\b/i.test(c) && /\bcart(ao|oes)\b/i.test(c))) found.push('board_cards');
167
+ if (/\b(crm|sales pipeline|deals? pipeline|leads? pipeline|funil de vendas|pipeline de (vendas|negocios|leads))\b/i.test(c)) found.push('crm_pipeline');
168
+ if (/\bcrud\b/i.test(c)
169
+ || /\badmin (panel|dashboard|area|console)\b/i.test(c)
170
+ || /\bmanagement (screen|page|panel|dashboard|interface|surface)\b/i.test(c)
171
+ || /\barea administrativa\b/i.test(c)
172
+ || /\bpainel (de )?admin(istracao)?\b/i.test(c)
173
+ || /\b(painel|tela|pagina|area|console) de (administracao|gestao|gerenciamento)\b/i.test(c)) found.push('crud_admin');
174
+ return [...new Set(found)];
175
+ }
176
+
141
177
  // Explicit `sensitive_surfaces:` frontmatter override — additive, can only force
142
178
  // the floor when content detection misses. Supports inline (`[a, b]` / `a, b`)
143
179
  // and YAML block list forms.
144
- function parseSensitiveSurfacesOverride(content) {
180
+ function parseSurfacesOverride(content, key) {
145
181
  const fm = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
146
182
  if (!fm) return [];
147
183
  const body = fm[1];
148
184
  const items = [];
149
- const inline = body.match(/^sensitive_surfaces:[ \t]*(.+)$/m);
185
+ const inline = body.match(new RegExp(`^${key}:[ \\t]*(.+)$`, 'm'));
150
186
  if (inline) {
151
187
  inline[1].trim().replace(/^\[|\]$/g, '').split(',').forEach((s) => {
152
188
  const v = s.trim().replace(/^["']|["']$/g, '');
153
189
  if (v) items.push(v);
154
190
  });
155
191
  }
156
- const block = body.match(/^sensitive_surfaces:[ \t]*\r?\n((?:[ \t]*-[ \t]*.+\r?\n?)+)/m);
192
+ const block = body.match(new RegExp(`^${key}:[ \\t]*\\r?\\n((?:[ \\t]*-[ \\t]*.+\\r?\\n?)+)`, 'm'));
157
193
  if (block) {
158
194
  block[1].split(/\r?\n/).forEach((line) => {
159
195
  const m = line.match(/^[ \t]*-[ \t]*(.+)$/);
@@ -276,7 +312,7 @@ async function runClassify({ args, options = {}, logger }) {
276
312
 
277
313
  // Gap 3B — sensitive-surface floor (deterministic; raises MICRO -> SMALL only).
278
314
  const detectedSurfaces = content ? detectSensitiveSurfaces(content) : [];
279
- const declaredSurfaces = content ? parseSensitiveSurfacesOverride(content) : [];
315
+ const declaredSurfaces = content ? parseSurfacesOverride(content, 'sensitive_surfaces') : [];
280
316
  const sensitiveSurfaces = [...new Set([...detectedSurfaces, ...declaredSurfaces])];
281
317
  let floored = false;
282
318
  if (sensitiveSurfaces.length > 0) {
@@ -285,8 +321,26 @@ async function runClassify({ args, options = {}, logger }) {
285
321
  floored = classification !== scored;
286
322
  }
287
323
 
324
+ // Operational-surface floor (deterministic; raises MICRO -> SMALL only). A rich
325
+ // operational surface needs management screens, so a Trello/Kanban/CRM/workspace
326
+ // feature can't take the MICRO shortcut that skips @analyst/@architect/prototype.
327
+ const detectedOps = content ? detectRichSurfaces(content) : [];
328
+ const declaredOps = content ? parseSurfacesOverride(content, 'operational_surfaces') : [];
329
+ const operationalSurfaces = [...new Set([...detectedOps, ...declaredOps])];
330
+ if (operationalSurfaces.length > 0) {
331
+ const scored = classification;
332
+ classification = applySensitiveFloor(classification);
333
+ floored = floored || classification !== scored;
334
+ }
335
+
288
336
  const phaseDepth = classificationToPhaseDepth(classification);
289
337
 
338
+ // A rich operational surface is exactly the case a clickable prototype is meant
339
+ // to de-risk (management screens + interactions before the PRD). Emit the
340
+ // recommendation from the deterministic tool so it does not rely on agent prose
341
+ // alone — @product/@briefing-refiner key off this flag.
342
+ const recommendPrototype = operationalSurfaces.length > 0;
343
+
290
344
  const result = {
291
345
  ok: true,
292
346
  feature_slug: slug,
@@ -295,7 +349,9 @@ async function runClassify({ args, options = {}, logger }) {
295
349
  scores: { user_types: utScore, integrations: intScore, complexity: cxScore, total: totalScore },
296
350
  classification,
297
351
  sensitive_surfaces: sensitiveSurfaces,
352
+ operational_surfaces: operationalSurfaces,
298
353
  floored,
354
+ recommend_prototype: recommendPrototype,
299
355
  phase_depth: phaseDepth
300
356
  };
301
357
 
@@ -314,6 +370,12 @@ async function runClassify({ args, options = {}, logger }) {
314
370
  if (sensitiveSurfaces.length > 0) {
315
371
  logger.log(`Sensitive surfaces: ${sensitiveSurfaces.join(', ')}${floored ? ' → floored to SMALL' : ''}`);
316
372
  }
373
+ if (operationalSurfaces.length > 0) {
374
+ logger.log(`Operational surfaces: ${operationalSurfaces.join(', ')}${floored ? ' → floored to at least SMALL' : ''}`);
375
+ }
376
+ if (recommendPrototype) {
377
+ logger.log('Recommendation: generate a clickable prototype in @briefing-refiner before @product (surfaces management screens + interactions early).');
378
+ }
317
379
  logger.log('');
318
380
  logger.log('Phase depth:');
319
381
  for (const [phase, desc] of Object.entries(phaseDepth)) {
@@ -22,10 +22,14 @@ const { emitDossierEvent } = require('../lib/dossier-telemetry');
22
22
  const { appendScoutToFeatureDossier } = require('../dossier/scout-section');
23
23
  const { emitSubTaskEvent } = require('../sub-task-telemetry');
24
24
  const { loadConfig } = require('../sub-task-engine');
25
- const { runDistillation, readFeatureClassification } = require('../learning-loop-engine');
26
- const { openRuntimeDb } = require('../runtime-store');
27
- const { runNotify } = require('./notify');
28
- const { splitCurrentState, buildArchiveContent, parseActiveSlugs } = require('../current-state-trim');
25
+ const { runDistillation, readFeatureClassification } = require('../learning-loop-engine');
26
+ const { openRuntimeDb } = require('../runtime-store');
27
+ const { runNotify } = require('./notify');
28
+ const { splitCurrentState, buildArchiveContent, parseActiveSlugs } = require('../current-state-trim');
29
+ const {
30
+ evaluateContractIntegrityGate,
31
+ formatContractIntegrityGateError
32
+ } = require('../harness/contract-integrity-gate');
29
33
 
30
34
  // P0 agent-loading-contract: a feature closing is the natural cadence to roll
31
35
  // aged-out current-state.md entries into the cold archive. Conservative window
@@ -322,18 +326,40 @@ async function runFeatureClose({ args, options = {}, logger }) {
322
326
  const updates = [];
323
327
 
324
328
  // 0a. Harness Done Gate (AC-HD-11 refined)
325
- // Only enforced on PASS — FAIL means QA already rejected and we want the
326
- // closure to record that. Without `harness-contract.json` for the feature,
327
- // behavior is unchanged (zero impact on MICRO/SMALL or any feature that
328
- // never opted into the harness).
329
+ // Only enforced on PASS — FAIL means QA already rejected and we want the
330
+ // closure to record that. Runtime features the CLI can detect (prototype or
331
+ // migration/Prisma evidence) must have a valid contract even on MICRO/SMALL;
332
+ // non-runtime features without a contract keep the historical lightweight path.
329
333
  if (verdict === 'PASS') {
330
334
  const planDir = path.join(targetDir, '.aioson', 'plans', slug);
331
- const contractPath = path.join(planDir, 'harness-contract.json');
332
- const progressPath = path.join(planDir, 'progress.json');
333
- const contractContent = await readFileSafe(contractPath);
334
- const progressContent = await readFileSafe(progressPath);
335
-
336
- // REQ-13 (loop-guardrails): tema `publish` é gate de COMANDO — intercepta
335
+ const contractPath = path.join(planDir, 'harness-contract.json');
336
+ const progressPath = path.join(planDir, 'progress.json');
337
+ const contractContent = await readFileSafe(contractPath);
338
+ const progressContent = await readFileSafe(progressPath);
339
+ const force = options.force === true;
340
+
341
+ const integrityGate = await evaluateContractIntegrityGate(targetDir, slug, {
342
+ runChecks: Boolean(contractContent)
343
+ });
344
+ if (!integrityGate.ok) {
345
+ if (!force) {
346
+ const errMsg = formatContractIntegrityGateError(integrityGate);
347
+ if (options.json) {
348
+ return {
349
+ ok: false,
350
+ reason: 'harness_contract_gate_blocked',
351
+ feature: slug,
352
+ error: errMsg,
353
+ errors: integrityGate.errors
354
+ };
355
+ }
356
+ logger.log(errMsg);
357
+ return { ok: false, reason: 'harness_contract_gate_blocked' };
358
+ }
359
+ updates.push('harness contract gate: BYPASSED via --force');
360
+ }
361
+
362
+ // REQ-13 (loop-guardrails): tema `publish` é gate de COMANDO — intercepta
337
363
  // o feature:close quando o contrato ativo o exige e não há gate publish
338
364
  // aprovado. Nunca detectado por diff. `--force` NÃO bypassa: a aprovação
339
365
  // humana é o propósito do gate (decisão registrada no spec da feature).
@@ -372,10 +398,9 @@ async function runFeatureClose({ args, options = {}, logger }) {
372
398
  }
373
399
  } catch { /* contrato ilegível — o done gate abaixo lida com o estado */ }
374
400
  }
375
-
376
- if (contractContent && progressContent) {
377
- const force = options.force === true;
378
- let progress = null;
401
+
402
+ if (contractContent && progressContent) {
403
+ let progress = null;
379
404
  try {
380
405
  progress = JSON.parse(progressContent);
381
406
  } catch (err) {