@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.
- package/CHANGELOG.md +75 -0
- package/README.md +19 -6
- package/docs/en/1-understand/ecosystem-map.md +45 -29
- package/docs/en/1-understand/glossary.md +5 -5
- package/docs/en/1-understand/what-is-aioson.md +5 -5
- package/docs/en/2-start/existing-project.md +7 -7
- package/docs/en/2-start/first-project.md +32 -38
- package/docs/en/2-start/initial-decisions.md +18 -17
- package/docs/en/3-recipes/README.md +2 -2
- package/docs/en/3-recipes/continuity-between-sessions.md +2 -2
- package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +5 -2
- package/docs/en/3-recipes/full-feature-with-sheldon.md +327 -338
- package/docs/en/4-agents/README.md +28 -14
- package/docs/en/4-agents/discovery-design-doc.md +20 -8
- package/docs/en/4-agents/forge-run.md +3 -2
- package/docs/en/5-reference/cli-reference.md +51 -48
- package/docs/en/5-reference/executable-verification.md +10 -7
- package/docs/en/5-reference/parallel.md +2 -0
- package/docs/en/5-reference/qa-browser.md +2 -2
- package/docs/en/README.md +3 -3
- package/docs/pt/1-entender/glossario.md +4 -4
- package/docs/pt/1-entender/mapa-do-ecossistema.md +32 -21
- package/docs/pt/2-comecar/decisoes-iniciais.md +16 -16
- package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
- package/docs/pt/3-receitas/README.md +1 -1
- package/docs/pt/3-receitas/app-saas-do-zero.md +35 -122
- package/docs/pt/3-receitas/feature-completa-com-sheldon.md +289 -338
- package/docs/pt/4-agentes/analyst.md +9 -5
- package/docs/pt/4-agentes/architect.md +9 -5
- package/docs/pt/4-agentes/dev.md +19 -7
- package/docs/pt/4-agentes/discovery-design-doc.md +25 -23
- package/docs/pt/4-agentes/orchestrator.md +164 -118
- package/docs/pt/4-agentes/pentester.md +5 -5
- package/docs/pt/4-agentes/pm.md +15 -7
- package/docs/pt/4-agentes/sheldon.md +166 -99
- package/docs/pt/4-agentes/ux-ui.md +7 -4
- package/docs/pt/5-referencia/agent-chain-continuity.md +14 -5
- package/docs/pt/5-referencia/comandos-cli.md +1 -1
- package/docs/pt/5-referencia/harness-retro.md +2 -1
- package/docs/pt/5-referencia/sdd-automation-scripts.md +6 -6
- package/docs/pt/5-referencia/sdd-framework.md +53 -16
- package/docs/pt/5-referencia/sdd-planos-e-estrutura.md +27 -15
- package/docs/pt/README.md +4 -4
- package/docs/pt/agentes.md +48 -50
- package/package.json +2 -2
- package/src/artifact-kinds.js +110 -0
- package/src/cli.js +82 -42
- package/src/commands/agent-epilogue.js +251 -186
- package/src/commands/agents.js +104 -48
- package/src/commands/audit-code.js +344 -0
- package/src/commands/classify.js +75 -13
- package/src/commands/feature-close.js +43 -18
- package/src/commands/harness-check.js +259 -175
- package/src/commands/harness-retro-promote.js +387 -0
- package/src/commands/live.js +24 -0
- package/src/commands/prototype-check.js +163 -0
- package/src/commands/review-feature.js +189 -0
- package/src/commands/runtime.js +81 -66
- package/src/commands/sync-agents-copy.js +115 -0
- package/src/commands/verification-plan.js +116 -0
- package/src/commands/verify-artifact.js +530 -0
- package/src/commands/verify-implementation.js +428 -0
- package/src/commands/workflow-execute.js +309 -309
- package/src/commands/workflow-next.js +361 -19
- package/src/commands/workflow-plan.js +5 -5
- package/src/constants.js +4 -0
- package/src/gateway-pointer-merge.js +7 -1
- package/src/handoff-contract.js +267 -172
- package/src/harness/contract-integrity-gate.js +172 -0
- package/src/harness/contract-integrity.js +111 -0
- package/src/harness/contract-schema.js +377 -332
- package/src/harness/detect-runtime-feature.js +90 -0
- package/src/harness/static-criteria.js +192 -0
- package/src/i18n/messages/en.js +8 -4
- package/src/i18n/messages/es.js +8 -4
- package/src/i18n/messages/fr.js +8 -4
- package/src/i18n/messages/pt-BR.js +8 -4
- package/src/install-wizard.js +8 -8
- package/src/installer.js +13 -6
- package/src/lib/retro/retro-render.js +10 -1
- package/src/lib/retro/retro-sources.js +45 -27
- package/src/lib/retro/verification-reports.js +230 -0
- package/src/parser.js +6 -0
- package/src/preflight-engine.js +12 -12
- package/src/runtime-store.js +13 -9
- package/src/verification/evidence-bundle.js +251 -0
- package/src/verification/ledger-store.js +221 -0
- package/src/verification/path-policy.js +74 -0
- package/src/verification/policy-engine.js +95 -0
- package/src/verification/prompt-package.js +314 -0
- package/src/verification/redaction.js +77 -0
- package/src/verification/report-parser.js +132 -0
- package/src/verification/report-store.js +97 -0
- package/src/verification/result.js +16 -0
- package/src/verification/runners/index.js +319 -0
- package/src/verification/runtime-telemetry.js +144 -0
- package/src/verification/schema.js +276 -0
- package/src/verification/source-discovery.js +153 -0
- package/src/verification-policy.js +398 -0
- package/src/version.js +52 -1
- package/template/.aioson/agents/analyst.md +9 -6
- package/template/.aioson/agents/architect.md +34 -5
- package/template/.aioson/agents/briefing-refiner.md +25 -0
- package/template/.aioson/agents/briefing.md +69 -12
- package/template/.aioson/agents/committer.md +2 -1
- package/template/.aioson/agents/copywriter.md +30 -21
- package/template/.aioson/agents/design-hybrid-forge.md +28 -15
- package/template/.aioson/agents/dev.md +28 -10
- package/template/.aioson/agents/deyvin.md +3 -2
- package/template/.aioson/agents/discover.md +12 -3
- package/template/.aioson/agents/discovery-design-doc.md +7 -2
- package/template/.aioson/agents/genome.md +19 -10
- package/template/.aioson/agents/neo.md +30 -30
- package/template/.aioson/agents/orache.md +20 -11
- package/template/.aioson/agents/orchestrator.md +84 -7
- package/template/.aioson/agents/pm.md +8 -8
- package/template/.aioson/agents/product.md +15 -11
- package/template/.aioson/agents/profiler-enricher.md +20 -11
- package/template/.aioson/agents/profiler-forge.md +20 -11
- package/template/.aioson/agents/profiler-researcher.md +20 -11
- package/template/.aioson/agents/qa.md +87 -17
- package/template/.aioson/agents/scope-check.md +19 -5
- package/template/.aioson/agents/setup.md +12 -1
- package/template/.aioson/agents/sheldon.md +92 -9
- package/template/.aioson/agents/site-forge.md +11 -2
- package/template/.aioson/agents/squad.md +20 -5
- package/template/.aioson/agents/ux-ui.md +4 -2
- package/template/.aioson/agents/validator.md +33 -1
- package/template/.aioson/config/verification.json +61 -0
- package/template/.aioson/config.md +13 -9
- package/template/.aioson/docs/LAYERS.md +2 -2
- package/template/.aioson/docs/autopilot-handoff.md +10 -10
- package/template/.aioson/docs/dev/execution-discipline.md +41 -0
- package/template/.aioson/docs/dev/phase-loop.md +47 -0
- package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
- package/template/.aioson/docs/presets/workflow.config.full-merged.json +15 -0
- package/template/.aioson/docs/presets/workflow.config.lean.json +15 -0
- package/template/.aioson/docs/product/prd-contract.md +12 -12
- package/template/.aioson/docs/prototype-contract.md +98 -0
- package/template/.aioson/docs/reference-identity.md +94 -0
- package/template/.aioson/docs/sheldon/harness-contract.md +155 -28
- package/template/.aioson/docs/verification-config.md +82 -0
- package/template/.aioson/docs/verify-artifact-gates.md +91 -0
- package/template/.aioson/docs/workflow-lean-lane.md +129 -0
- package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +2 -0
- package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +16 -14
- package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +2 -2
- package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -4
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +15 -15
- package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
- package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
- package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
- package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
- package/template/.aioson/skills/process/prototype-forge/SKILL.md +98 -0
- package/template/.aioson/skills/process/reference-identity-extract/SKILL.md +164 -0
- package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +23 -2
- package/template/.aioson/skills/static/multi-agent-patterns.md +5 -5
- 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
|
+
};
|
package/src/commands/classify.js
CHANGED
|
@@ -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.
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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 ?
|
|
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.
|
|
327
|
-
//
|
|
328
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|