@jaimevalasek/aioson 1.28.1 → 1.30.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 +42 -0
- package/README.md +7 -5
- package/docs/en/5-reference/cli-reference.md +40 -10
- package/docs/pt/4-agentes/briefing.md +2 -0
- package/docs/pt/4-agentes/copywriter.md +2 -0
- package/docs/pt/4-agentes/genome.md +1 -0
- package/docs/pt/4-agentes/pm.md +1 -1
- package/docs/pt/4-agentes/profiler-enricher.md +2 -0
- package/docs/pt/4-agentes/profiler-forge.md +2 -0
- package/docs/pt/4-agentes/sheldon.md +2 -0
- package/docs/pt/4-agentes/squad.md +12 -10
- package/docs/pt/5-referencia/autopilot-handoff.md +4 -4
- package/docs/pt/5-referencia/comandos-cli.md +7 -3
- package/docs/pt/5-referencia/fluxo-artefatos.md +1 -1
- package/docs/pt/5-referencia/memoria-e-contexto.md +62 -2
- package/docs/pt/_arquivo/monitor-de-contexto.md +2 -2
- package/package.json +4 -2
- package/src/cli.js +72 -24
- package/src/commands/ac-test-audit.js +45 -0
- package/src/commands/artifact-validate.js +62 -50
- package/src/commands/classify.js +73 -2
- package/src/commands/context-brief.js +59 -0
- package/src/commands/context-guard.js +88 -0
- package/src/commands/context-monitor.js +1 -1
- package/src/commands/context-search.js +101 -52
- package/src/commands/context-select.js +11 -2
- package/src/commands/feature-archive.js +21 -12
- package/src/commands/feature-current.js +82 -0
- package/src/commands/gate-check.js +32 -15
- package/src/commands/harness-check.js +17 -1
- package/src/commands/hooks-install.js +169 -26
- package/src/commands/hygiene-scan.js +423 -0
- package/src/commands/rules-lint.js +124 -0
- package/src/commands/sdd-benchmark.js +134 -0
- package/src/commands/spec-analyze.js +6 -4
- package/src/commands/store-system.js +329 -49
- package/src/constants.js +8 -3
- package/src/context-brief.js +585 -0
- package/src/context-guard.js +209 -0
- package/src/context-search.js +796 -96
- package/src/context-selector.js +802 -420
- package/src/handoff-contract.js +14 -6
- package/src/harness/contract-schema.js +1 -1
- package/src/i18n/messages/en.js +12 -5
- package/src/i18n/messages/es.js +11 -4
- package/src/i18n/messages/fr.js +11 -4
- package/src/i18n/messages/pt-BR.js +12 -5
- package/src/lib/ac-test-audit.js +194 -0
- package/src/preflight-engine.js +10 -6
- package/src/squad/state-manager.js +1 -1
- package/template/.aioson/agents/analyst.md +93 -53
- package/template/.aioson/agents/architect.md +41 -32
- package/template/.aioson/agents/briefing-refiner.md +15 -2
- package/template/.aioson/agents/briefing.md +105 -86
- package/template/.aioson/agents/committer.md +1 -1
- package/template/.aioson/agents/copywriter.md +53 -10
- package/template/.aioson/agents/design-hybrid-forge.md +9 -5
- package/template/.aioson/agents/dev.md +22 -25
- package/template/.aioson/agents/deyvin.md +126 -124
- package/template/.aioson/agents/discover.md +8 -9
- package/template/.aioson/agents/discovery-design-doc.md +52 -36
- package/template/.aioson/agents/forge-run.md +3 -0
- package/template/.aioson/agents/genome.md +12 -6
- package/template/.aioson/agents/neo.md +30 -24
- package/template/.aioson/agents/orache.md +16 -21
- package/template/.aioson/agents/orchestrator.md +40 -31
- package/template/.aioson/agents/pentester.md +22 -12
- package/template/.aioson/agents/pm.md +11 -2
- package/template/.aioson/agents/product.md +162 -183
- package/template/.aioson/agents/profiler-enricher.md +29 -6
- package/template/.aioson/agents/profiler-forge.md +16 -6
- package/template/.aioson/agents/profiler-researcher.md +10 -6
- package/template/.aioson/agents/qa.md +29 -19
- package/template/.aioson/agents/scope-check.md +14 -2
- package/template/.aioson/agents/sheldon.md +51 -21
- package/template/.aioson/agents/site-forge.md +4 -6
- package/template/.aioson/agents/squad.md +7 -12
- package/template/.aioson/agents/tester.md +40 -30
- package/template/.aioson/agents/ux-ui.md +56 -41
- package/template/.aioson/agents/validator.md +2 -2
- package/template/.aioson/config.md +4 -3
- package/template/.aioson/design-docs/agent-loading-contract.md +3 -3
- package/template/.aioson/docs/LAYERS.md +2 -0
- package/template/.aioson/docs/autonomy-protocol.md +7 -5
- package/template/.aioson/docs/autopilot-handoff.md +5 -3
- package/template/.aioson/docs/dev/execution-discipline.md +3 -0
- package/template/.aioson/docs/dev/simple-plan-lane.md +126 -77
- package/template/.aioson/docs/dev/stack-conventions.md +4 -1
- package/template/.aioson/docs/deyvin/continuity-recovery.md +21 -18
- package/template/.aioson/docs/deyvin/debugging-escalation.md +3 -0
- package/template/.aioson/docs/deyvin/pair-execution.md +3 -0
- package/template/.aioson/docs/deyvin/runtime-handoffs.md +6 -3
- package/template/.aioson/docs/dossier/agent-templates.md +3 -0
- package/template/.aioson/docs/dossier/schema.md +3 -0
- package/template/.aioson/docs/example-external-api-context.md +2 -0
- package/template/.aioson/docs/feature-expansion-taxonomy.md +53 -0
- package/template/.aioson/docs/handoff-persistence.md +95 -91
- package/template/.aioson/docs/pentester/app-playbooks.md +3 -0
- package/template/.aioson/docs/pentester/browser-dast-playbook.md +401 -398
- package/template/.aioson/docs/pentester/llm-supplychain.md +3 -0
- package/template/.aioson/docs/product/conversation-playbook.md +1 -1
- package/template/.aioson/docs/quality/code-health-analysis.md +2 -0
- package/template/.aioson/docs/sheldon/enrichment-paths.md +47 -1
- package/template/.aioson/docs/sheldon/harness-contract.md +26 -21
- package/template/.aioson/docs/sheldon/quality-lens.md +3 -0
- package/template/.aioson/docs/sheldon/research-loop.md +3 -0
- package/template/.aioson/docs/sheldon/web-intelligence.md +3 -0
- package/template/.aioson/docs/site-forge-build.md +4 -2
- package/template/.aioson/docs/site-forge-extraction.md +2 -0
- package/template/.aioson/docs/site-forge-qa.md +2 -0
- package/template/.aioson/docs/site-forge-recon.md +7 -5
- package/template/.aioson/docs/site-forge-transform.md +2 -0
- package/template/.aioson/docs/squad/content-output.md +3 -0
- package/template/.aioson/docs/squad/creation-flow.md +22 -1
- package/template/.aioson/docs/squad/domain-breadth.md +3 -0
- package/template/.aioson/docs/squad/domain-classification.md +3 -0
- package/template/.aioson/docs/squad/eval-gate.md +3 -0
- package/template/.aioson/docs/squad/genome-bindings.md +14 -0
- package/template/.aioson/docs/squad/package-contract.md +5 -0
- package/template/.aioson/docs/squad/persona-grounding.md +65 -62
- package/template/.aioson/docs/squad/quality-lens.md +3 -0
- package/template/.aioson/docs/squad/research-loop.md +3 -0
- package/template/.aioson/docs/squad/session-operations.md +3 -0
- package/template/.aioson/docs/squad/workflow-quality.md +3 -0
- package/template/.aioson/docs/tester/coverage-quality.md +4 -1
- package/template/.aioson/docs/ux-ui/design-execution.md +9 -7
- package/template/.aioson/rules/README.md +48 -2
- package/template/.aioson/rules/agent-language-policy.md +26 -21
- package/template/.aioson/rules/agent-structural-contract.md +168 -158
- package/template/.aioson/rules/aioson-context-boundary.md +7 -1
- package/template/.aioson/rules/canonical-path-contract.md +16 -10
- package/template/.aioson/rules/data-format-convention.md +17 -11
- package/template/.aioson/rules/disk-first-artifacts.md +12 -8
- package/template/.aioson/rules/example-monetary-values.md +4 -0
- package/template/.aioson/rules/implementation-structure-and-data-access.md +50 -0
- package/template/.aioson/rules/output-brevity.md +2 -0
- package/template/.aioson/rules/prd-section-ownership.md +17 -12
- package/template/.aioson/rules/security-baseline.md +8 -3
- package/template/.aioson/rules/simple-plan-lane.md +22 -5
- package/template/.aioson/rules/source-code-language-convention.md +34 -0
- package/template/.aioson/rules/spec-level-ownership.md +10 -5
- package/template/.aioson/rules/squad-driver-pattern.md +5 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +24 -23
- package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +2 -2
- package/template/.aioson/skills/process/aioson-spec-driven/references/qa.md +1 -1
- package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +72 -0
- package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +74 -0
- package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +67 -0
- package/template/.aioson/skills/static/context-budget-guide.md +1 -1
- package/template/.aioson/skills/static/multi-agent-patterns.md +5 -4
- package/template/.aioson/tasks/squad-create.md +11 -0
- package/template/.aioson/tasks/squad-design.md +3 -3
- package/template/AGENTS.md +36 -19
- package/template/CLAUDE.md +9 -5
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readNoiseFileAndRecompute } = require('../neural-chain-noise-file');
|
|
6
|
+
const { contextDir, readFileSafe } = require('../preflight-engine');
|
|
7
|
+
const { runFeatureArchive, runFeatureSweep } = require('./feature-archive');
|
|
8
|
+
|
|
9
|
+
const REVIEW_PREFIXES = new Set(['qa-report', 'security-findings']);
|
|
10
|
+
const GLOBAL_REVIEW_SLUGS = new Set(['project', 'test-coverage']);
|
|
11
|
+
const COMPLETE_DEV_STATUSES = new Set([
|
|
12
|
+
'complete',
|
|
13
|
+
'completed',
|
|
14
|
+
'dev_complete',
|
|
15
|
+
'done',
|
|
16
|
+
'qa_complete'
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const ARTIFACT_PREFIXES = [
|
|
20
|
+
'implementation-plan',
|
|
21
|
+
'security-findings',
|
|
22
|
+
'sheldon-enrichment',
|
|
23
|
+
'test-inventory',
|
|
24
|
+
'requirements',
|
|
25
|
+
'conformance',
|
|
26
|
+
'scope-check',
|
|
27
|
+
'design-doc',
|
|
28
|
+
'qa-report',
|
|
29
|
+
'readiness',
|
|
30
|
+
'test-plan',
|
|
31
|
+
'spec',
|
|
32
|
+
'prd'
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const ARTIFACT_EXT_RE = /\.(md|json|ya?ml)$/i;
|
|
36
|
+
|
|
37
|
+
function parseFrontmatter(content) {
|
|
38
|
+
const match = String(content || '').match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
39
|
+
if (!match) return {};
|
|
40
|
+
const values = {};
|
|
41
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
42
|
+
const idx = line.indexOf(':');
|
|
43
|
+
if (idx === -1) continue;
|
|
44
|
+
const key = line.slice(0, idx).trim();
|
|
45
|
+
const value = line.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
|
|
46
|
+
if (key) values[key] = value;
|
|
47
|
+
}
|
|
48
|
+
return values;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function dirExists(dirPath) {
|
|
52
|
+
try {
|
|
53
|
+
return (await fs.stat(dirPath)).isDirectory();
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readJsonSafe(filePath) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function readFeatureRegistry(ctxDir) {
|
|
68
|
+
const content = await readFileSafe(path.join(ctxDir, 'features.md'));
|
|
69
|
+
const bySlug = new Map();
|
|
70
|
+
if (!content) return bySlug;
|
|
71
|
+
|
|
72
|
+
for (const line of content.split(/\r?\n/)) {
|
|
73
|
+
const match = line.match(/^\|\s*([a-z][a-z0-9-]*)\s*\|\s*([a-z_ -]+)\s*\|/i);
|
|
74
|
+
if (!match) continue;
|
|
75
|
+
const slug = match[1].trim().toLowerCase();
|
|
76
|
+
if (slug === 'slug') continue;
|
|
77
|
+
bySlug.set(slug, { slug, status: match[2].trim().toLowerCase() });
|
|
78
|
+
}
|
|
79
|
+
return bySlug;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function readArchivedSlugs(ctxDir) {
|
|
83
|
+
const content = await readFileSafe(path.join(ctxDir, 'done', 'MANIFEST.md'));
|
|
84
|
+
const slugs = new Set();
|
|
85
|
+
if (!content) return slugs;
|
|
86
|
+
|
|
87
|
+
for (const line of content.split(/\r?\n/)) {
|
|
88
|
+
const match = line.match(/^\|\s*([a-z][a-z0-9-]*)\s*\|/i);
|
|
89
|
+
if (!match) continue;
|
|
90
|
+
const slug = match[1].trim().toLowerCase();
|
|
91
|
+
if (slug !== 'slug') slugs.add(slug);
|
|
92
|
+
}
|
|
93
|
+
return slugs;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function classifyArtifactName(fileName) {
|
|
97
|
+
if (!ARTIFACT_EXT_RE.test(fileName)) return null;
|
|
98
|
+
const base = fileName.replace(ARTIFACT_EXT_RE, '');
|
|
99
|
+
for (const prefix of ARTIFACT_PREFIXES) {
|
|
100
|
+
const marker = `${prefix}-`;
|
|
101
|
+
if (!base.startsWith(marker)) continue;
|
|
102
|
+
const slug = base.slice(marker.length).toLowerCase();
|
|
103
|
+
if (!/^[a-z][a-z0-9-]*$/.test(slug)) return null;
|
|
104
|
+
return {
|
|
105
|
+
fileName,
|
|
106
|
+
prefix,
|
|
107
|
+
slug,
|
|
108
|
+
kind: prefix.replace(/-/g, '_')
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function summarizeArchivePlan(targetDir, slug) {
|
|
115
|
+
try {
|
|
116
|
+
const result = await runFeatureArchive({
|
|
117
|
+
args: [targetDir],
|
|
118
|
+
options: { feature: slug, 'dry-run': true, json: true },
|
|
119
|
+
logger: null
|
|
120
|
+
});
|
|
121
|
+
if (!result || !result.ok) return { move_count: 0, dir_count: 0 };
|
|
122
|
+
const moveCount = Array.isArray(result.move) ? result.move.length : 0;
|
|
123
|
+
const dirCount = Array.isArray(result.dirs)
|
|
124
|
+
? result.dirs.filter((d) => d.action === 'move').length
|
|
125
|
+
: 0;
|
|
126
|
+
return { move_count: moveCount, dir_count: dirCount };
|
|
127
|
+
} catch {
|
|
128
|
+
return { move_count: 0, dir_count: 0 };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function scanDoneFeaturesPendingArchive(targetDir) {
|
|
133
|
+
const sweep = await runFeatureSweep({
|
|
134
|
+
args: [targetDir],
|
|
135
|
+
options: { 'dry-run': true, json: true },
|
|
136
|
+
logger: null
|
|
137
|
+
});
|
|
138
|
+
if (!sweep || !sweep.ok || !Array.isArray(sweep.pending)) return [];
|
|
139
|
+
|
|
140
|
+
const items = [];
|
|
141
|
+
for (const slug of sweep.pending) {
|
|
142
|
+
// eslint-disable-next-line no-await-in-loop
|
|
143
|
+
const plan = await summarizeArchivePlan(targetDir, slug);
|
|
144
|
+
items.push({
|
|
145
|
+
slug,
|
|
146
|
+
path: '.aioson/context/features.md',
|
|
147
|
+
reason: 'feature is done but missing from .aioson/context/done/MANIFEST.md',
|
|
148
|
+
suggested_command: `aioson feature:archive . --feature=${slug}`,
|
|
149
|
+
...plan
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return items;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function scanStaleDevState(ctxDir, featureRegistry) {
|
|
156
|
+
const relPath = '.aioson/context/dev-state.md';
|
|
157
|
+
const content = await readFileSafe(path.join(ctxDir, 'dev-state.md'));
|
|
158
|
+
if (!content) return [];
|
|
159
|
+
|
|
160
|
+
const fm = parseFrontmatter(content);
|
|
161
|
+
const activeFeature = String(fm.active_feature || '').trim().toLowerCase();
|
|
162
|
+
const devStatus = String(fm.status || '').trim().toLowerCase();
|
|
163
|
+
if (!activeFeature) return [];
|
|
164
|
+
|
|
165
|
+
const registered = featureRegistry.get(activeFeature);
|
|
166
|
+
const issues = [];
|
|
167
|
+
if (COMPLETE_DEV_STATUSES.has(devStatus)) {
|
|
168
|
+
issues.push({
|
|
169
|
+
path: relPath,
|
|
170
|
+
active_feature: activeFeature,
|
|
171
|
+
status: devStatus,
|
|
172
|
+
reason: 'dev-state points to a completed implementation state',
|
|
173
|
+
suggested_action: 'clear or rewrite dev-state before the next @dev activation'
|
|
174
|
+
});
|
|
175
|
+
} else if (registered && registered.status !== 'in_progress' && registered.status !== 'paused') {
|
|
176
|
+
issues.push({
|
|
177
|
+
path: relPath,
|
|
178
|
+
active_feature: activeFeature,
|
|
179
|
+
status: devStatus || '(none)',
|
|
180
|
+
feature_status: registered.status,
|
|
181
|
+
reason: `features.md marks ${activeFeature} as ${registered.status}`,
|
|
182
|
+
suggested_action: 'clear or rewrite dev-state so @dev does not resume a closed feature'
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return issues;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function scanPendingChainNoises(ctxDir) {
|
|
190
|
+
const noisesDir = path.join(ctxDir, 'noises');
|
|
191
|
+
const entries = await fs.readdir(noisesDir, { withFileTypes: true }).catch(() => []);
|
|
192
|
+
const items = [];
|
|
193
|
+
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
196
|
+
const fullPath = path.join(noisesDir, entry.name);
|
|
197
|
+
let noise;
|
|
198
|
+
try {
|
|
199
|
+
noise = readNoiseFileAndRecompute({ path: fullPath });
|
|
200
|
+
} catch (err) {
|
|
201
|
+
items.push({
|
|
202
|
+
path: `.aioson/context/noises/${entry.name}`,
|
|
203
|
+
slug: entry.name.replace(/\.md$/i, ''),
|
|
204
|
+
pending_count: 0,
|
|
205
|
+
resolved_count: 0,
|
|
206
|
+
total_count: 0,
|
|
207
|
+
frontmatter_ok: false,
|
|
208
|
+
reason: `noise file could not be parsed: ${err && err.code ? err.code : 'read_error'}`,
|
|
209
|
+
suggested_action: 'inspect this noise file manually before routing with @neo',
|
|
210
|
+
items: []
|
|
211
|
+
});
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (!noise.exists || noise.pendingCount === 0) continue;
|
|
215
|
+
|
|
216
|
+
const frontmatter = noise.frontmatter || {};
|
|
217
|
+
items.push({
|
|
218
|
+
path: `.aioson/context/noises/${entry.name}`,
|
|
219
|
+
slug: String(frontmatter.slug || entry.name.replace(/-\d{8}-\d{4}\.md$/i, '').replace(/\.md$/i, '')),
|
|
220
|
+
pending_count: noise.pendingCount,
|
|
221
|
+
resolved_count: noise.resolvedCount,
|
|
222
|
+
total_count: noise.items.length,
|
|
223
|
+
frontmatter_ok: noise.frontmatterOk,
|
|
224
|
+
reason: 'neural chain impact audit has unchecked items',
|
|
225
|
+
suggested_action: 'verify or fix each pending item, mark it - [x], then let the noise lifecycle delete it',
|
|
226
|
+
items: noise.items
|
|
227
|
+
.filter((item) => !item.checked)
|
|
228
|
+
.slice(0, 20)
|
|
229
|
+
.map((item) => ({
|
|
230
|
+
target_path: item.target_path,
|
|
231
|
+
marker: item.marker,
|
|
232
|
+
reason: item.motivo
|
|
233
|
+
}))
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return items;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function summarizeSecurityFindings(data) {
|
|
241
|
+
if (!data || typeof data !== 'object') {
|
|
242
|
+
return {
|
|
243
|
+
status: 'invalid',
|
|
244
|
+
findings: 0,
|
|
245
|
+
open: 0,
|
|
246
|
+
blockers: []
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const findings = Array.isArray(data && data.findings) ? data.findings : [];
|
|
250
|
+
const open = findings.filter((f) => f.status === 'open' || f.status === 'needs_validation');
|
|
251
|
+
const blockers = open.filter(
|
|
252
|
+
(f) =>
|
|
253
|
+
f.recommended_gate_status === 'block' &&
|
|
254
|
+
(f.severity === 'high' || f.severity === 'critical')
|
|
255
|
+
);
|
|
256
|
+
let status = 'resolved';
|
|
257
|
+
if (blockers.length > 0) status = 'blocking';
|
|
258
|
+
else if (open.length > 0) status = 'needs_review';
|
|
259
|
+
return {
|
|
260
|
+
status,
|
|
261
|
+
findings: findings.length,
|
|
262
|
+
open: open.length,
|
|
263
|
+
blockers: blockers.map((f) => f.id || f.finding_id || 'unknown-finding')
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function scanReviewArtifacts(ctxDir, artifact, featureRegistry) {
|
|
268
|
+
if (!REVIEW_PREFIXES.has(artifact.prefix)) return null;
|
|
269
|
+
if (featureRegistry.has(artifact.slug) || GLOBAL_REVIEW_SLUGS.has(artifact.slug)) return null;
|
|
270
|
+
|
|
271
|
+
const relPath = `.aioson/context/${artifact.fileName}`;
|
|
272
|
+
const fullPath = path.join(ctxDir, artifact.fileName);
|
|
273
|
+
if (artifact.prefix === 'security-findings') {
|
|
274
|
+
const summary = summarizeSecurityFindings(await readJsonSafe(fullPath));
|
|
275
|
+
return {
|
|
276
|
+
path: relPath,
|
|
277
|
+
slug: artifact.slug,
|
|
278
|
+
kind: artifact.kind,
|
|
279
|
+
reason: 'review artifact is not attached to a registered feature',
|
|
280
|
+
suggested_action: summary.status === 'blocking'
|
|
281
|
+
? 'route to @dev/@pentester before any archival decision'
|
|
282
|
+
: summary.status === 'invalid'
|
|
283
|
+
? 'repair or discard the malformed artifact after user review'
|
|
284
|
+
: 'ask the user whether to keep as active evidence or archive as historical context',
|
|
285
|
+
...summary
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const content = await readFileSafe(fullPath);
|
|
290
|
+
const fm = parseFrontmatter(content);
|
|
291
|
+
return {
|
|
292
|
+
path: relPath,
|
|
293
|
+
slug: artifact.slug,
|
|
294
|
+
kind: artifact.kind,
|
|
295
|
+
status: String(fm.verdict || 'unknown').toLowerCase(),
|
|
296
|
+
reason: 'review artifact is not attached to a registered feature',
|
|
297
|
+
suggested_action: 'ask the user whether to keep as active evidence or archive as historical context'
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function classifyOrphanArtifact(artifact, featureRegistry, archivedSlugs, pendingArchiveSlugs) {
|
|
302
|
+
if (REVIEW_PREFIXES.has(artifact.prefix)) return null;
|
|
303
|
+
if (pendingArchiveSlugs.has(artifact.slug)) return null;
|
|
304
|
+
|
|
305
|
+
const registered = featureRegistry.get(artifact.slug);
|
|
306
|
+
if (!registered) {
|
|
307
|
+
return {
|
|
308
|
+
path: `.aioson/context/${artifact.fileName}`,
|
|
309
|
+
slug: artifact.slug,
|
|
310
|
+
kind: artifact.kind,
|
|
311
|
+
reason: 'slug artifact has no row in features.md',
|
|
312
|
+
suggested_action: 'review ownership; register the feature, archive manually, or keep as project-level context'
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (registered.status === 'done' && archivedSlugs.has(artifact.slug)) {
|
|
317
|
+
return {
|
|
318
|
+
path: `.aioson/context/${artifact.fileName}`,
|
|
319
|
+
slug: artifact.slug,
|
|
320
|
+
kind: artifact.kind,
|
|
321
|
+
reason: 'feature is already archived but root artifact still exists',
|
|
322
|
+
suggested_action: `run a targeted review before moving this artifact into .aioson/context/done/${artifact.slug}/`
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function scanRootArtifacts(ctxDir, featureRegistry, archivedSlugs, pendingArchiveSlugs) {
|
|
330
|
+
const entries = await fs.readdir(ctxDir, { withFileTypes: true }).catch(() => []);
|
|
331
|
+
const reviewArtifacts = [];
|
|
332
|
+
const orphanSlugArtifacts = [];
|
|
333
|
+
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
if (!entry.isFile()) continue;
|
|
336
|
+
const artifact = classifyArtifactName(entry.name);
|
|
337
|
+
if (!artifact) continue;
|
|
338
|
+
|
|
339
|
+
// eslint-disable-next-line no-await-in-loop
|
|
340
|
+
const review = await scanReviewArtifacts(ctxDir, artifact, featureRegistry);
|
|
341
|
+
if (review) {
|
|
342
|
+
reviewArtifacts.push(review);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const orphan = classifyOrphanArtifact(artifact, featureRegistry, archivedSlugs, pendingArchiveSlugs);
|
|
347
|
+
if (orphan) orphanSlugArtifacts.push(orphan);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { reviewArtifacts, orphanSlugArtifacts };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function buildSummary(buckets) {
|
|
354
|
+
const counts = {};
|
|
355
|
+
let total = 0;
|
|
356
|
+
for (const [key, items] of Object.entries(buckets)) {
|
|
357
|
+
counts[key] = items.length;
|
|
358
|
+
total += items.length;
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
status: total === 0 ? 'clean' : 'attention',
|
|
362
|
+
total,
|
|
363
|
+
counts
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function runHygieneScan({ args = [], options = {}, logger }) {
|
|
368
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
369
|
+
const jsonOut = Boolean(options.json);
|
|
370
|
+
const ctxDir = contextDir(targetDir);
|
|
371
|
+
|
|
372
|
+
if (!(await dirExists(ctxDir))) {
|
|
373
|
+
const out = { ok: false, reason: 'no_context_dir' };
|
|
374
|
+
if (!jsonOut && logger) logger.log('.aioson/context/ not found. Run aioson setup first.');
|
|
375
|
+
return out;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const featureRegistry = await readFeatureRegistry(ctxDir);
|
|
379
|
+
const archivedSlugs = await readArchivedSlugs(ctxDir);
|
|
380
|
+
const doneFeaturesPendingArchive = await scanDoneFeaturesPendingArchive(targetDir);
|
|
381
|
+
const pendingArchiveSlugs = new Set(doneFeaturesPendingArchive.map((item) => item.slug));
|
|
382
|
+
const staleStateFiles = await scanStaleDevState(ctxDir, featureRegistry);
|
|
383
|
+
const pendingChainNoises = await scanPendingChainNoises(ctxDir);
|
|
384
|
+
const { reviewArtifacts, orphanSlugArtifacts } = await scanRootArtifacts(
|
|
385
|
+
ctxDir,
|
|
386
|
+
featureRegistry,
|
|
387
|
+
archivedSlugs,
|
|
388
|
+
pendingArchiveSlugs
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
const buckets = {
|
|
392
|
+
pending_chain_noises: pendingChainNoises,
|
|
393
|
+
done_features_pending_archive: doneFeaturesPendingArchive,
|
|
394
|
+
stale_state_files: staleStateFiles,
|
|
395
|
+
on_demand_review_artifacts: reviewArtifacts,
|
|
396
|
+
orphan_slug_artifacts: orphanSlugArtifacts
|
|
397
|
+
};
|
|
398
|
+
const result = {
|
|
399
|
+
ok: true,
|
|
400
|
+
readonly: true,
|
|
401
|
+
targetDir,
|
|
402
|
+
summary: buildSummary(buckets),
|
|
403
|
+
buckets
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
if (!jsonOut && logger) {
|
|
407
|
+
logger.log(`hygiene:scan — ${result.summary.status} (${result.summary.total} item(s))`);
|
|
408
|
+
for (const [bucket, items] of Object.entries(buckets)) {
|
|
409
|
+
if (items.length === 0) continue;
|
|
410
|
+
logger.log(` ${bucket}: ${items.length}`);
|
|
411
|
+
for (const item of items.slice(0, 10)) {
|
|
412
|
+
logger.log(` - ${item.path || item.slug}: ${item.reason}`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return result;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
module.exports = {
|
|
421
|
+
classifyArtifactName,
|
|
422
|
+
runHygieneScan
|
|
423
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { parseFrontmatter } = require('../preflight-engine');
|
|
6
|
+
|
|
7
|
+
const ROUTING_FIELDS = [
|
|
8
|
+
'task_types',
|
|
9
|
+
'triggers',
|
|
10
|
+
'aliases',
|
|
11
|
+
'entities',
|
|
12
|
+
'retrieval_intents',
|
|
13
|
+
'paths',
|
|
14
|
+
'globs'
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function hasValue(raw) {
|
|
18
|
+
if (raw === undefined || raw === null) return false;
|
|
19
|
+
const value = String(raw).trim();
|
|
20
|
+
return value !== '' && value !== '[]';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function lintRule(relPath, frontmatter) {
|
|
24
|
+
const warnings = [];
|
|
25
|
+
const isRule = relPath.startsWith('.aioson/rules/');
|
|
26
|
+
|
|
27
|
+
if (isRule && !hasValue(frontmatter.name)) warnings.push('missing required field: name');
|
|
28
|
+
if (!hasValue(frontmatter.description)) warnings.push('missing required field: description');
|
|
29
|
+
|
|
30
|
+
const loadTier = String(frontmatter.load_tier || 'trigger').trim().toLowerCase();
|
|
31
|
+
const routing = ROUTING_FIELDS.filter((field) => hasValue(frontmatter[field]));
|
|
32
|
+
|
|
33
|
+
if (loadTier !== 'always' && routing.length === 0) {
|
|
34
|
+
warnings.push(
|
|
35
|
+
'selector-invisible: no task_types, triggers, aliases, entities, retrieval_intents, paths, or globs — metadata-only routing cannot score this rule above the load threshold; semantic fallback may still find it, but rules should declare routing metadata or set load_tier: always.'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
path: relPath,
|
|
41
|
+
name: String(frontmatter.name || path.basename(relPath, '.md')),
|
|
42
|
+
load_tier: loadTier,
|
|
43
|
+
agents: hasValue(frontmatter.agents) ? String(frontmatter.agents) : 'all',
|
|
44
|
+
routing,
|
|
45
|
+
warnings,
|
|
46
|
+
ok: warnings.length === 0
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function collectMarkdownFiles(absDir, relDir, recursive) {
|
|
51
|
+
let entries = [];
|
|
52
|
+
try {
|
|
53
|
+
entries = await fs.readdir(absDir, { withFileTypes: true });
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
const files = [];
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (entry.name.toLowerCase() === 'readme.md') continue;
|
|
60
|
+
const absChild = path.join(absDir, entry.name);
|
|
61
|
+
const relChild = `${relDir}/${entry.name}`;
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
if (recursive) files.push(...await collectMarkdownFiles(absChild, relChild, recursive));
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
67
|
+
files.push({ abs: absChild, rel: relChild });
|
|
68
|
+
}
|
|
69
|
+
return files;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runRulesLint({ args, options = {}, logger }) {
|
|
73
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
74
|
+
const relDir = '.aioson/rules';
|
|
75
|
+
|
|
76
|
+
const files = await collectMarkdownFiles(path.join(targetDir, '.aioson', 'rules'), relDir, false);
|
|
77
|
+
if (options.docs) {
|
|
78
|
+
files.push(...await collectMarkdownFiles(path.join(targetDir, '.aioson', 'docs'), '.aioson/docs', true));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (files.length === 0) {
|
|
82
|
+
const result = { ok: true, dir: relDir, rules: [], total: 0, warnings: 0 };
|
|
83
|
+
if (options.json) return result;
|
|
84
|
+
logger.log(`No rule${options.docs ? '/doc' : ''} files found under ${relDir}${options.docs ? ' or .aioson/docs' : ''} — nothing to lint.`);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rules = [];
|
|
89
|
+
for (const file of files) {
|
|
90
|
+
const content = await fs.readFile(file.abs, 'utf8');
|
|
91
|
+
rules.push(lintRule(file.rel, parseFrontmatter(content)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const warningsCount = rules.reduce((sum, rule) => sum + rule.warnings.length, 0);
|
|
95
|
+
const result = {
|
|
96
|
+
ok: !(options.strict && warningsCount > 0),
|
|
97
|
+
dir: relDir,
|
|
98
|
+
rules,
|
|
99
|
+
total: rules.length,
|
|
100
|
+
warnings: warningsCount
|
|
101
|
+
};
|
|
102
|
+
if (options.strict && warningsCount > 0) result.exitCode = 1;
|
|
103
|
+
if (options.json) return result;
|
|
104
|
+
|
|
105
|
+
logger.log(`Rules lint for ${relDir} (${rules.length} rule${rules.length === 1 ? '' : 's'})`);
|
|
106
|
+
for (const rule of rules) {
|
|
107
|
+
if (rule.ok) {
|
|
108
|
+
const routing = rule.load_tier === 'always' ? 'load_tier: always' : `routing: ${rule.routing.join(', ')}`;
|
|
109
|
+
logger.log(`OK ${rule.name} [agents: ${rule.agents}] ${routing}`);
|
|
110
|
+
} else {
|
|
111
|
+
logger.log(`WARN ${rule.name}`);
|
|
112
|
+
for (const warning of rule.warnings) logger.log(` - ${warning}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const clean = rules.filter((rule) => rule.ok).length;
|
|
116
|
+
logger.log(`Summary: ${clean}/${rules.length} ok, ${warningsCount} warning${warningsCount === 1 ? '' : 's'}.`);
|
|
117
|
+
if (warningsCount > 0) {
|
|
118
|
+
logger.log('Tip: add routing frontmatter such as task_types, triggers, aliases, entities, retrieval_intents, paths, or globs so context:select can route the rule deterministically; semantic fallback is only a recall aid (see .aioson/rules/README.md).');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = { runRulesLint };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const { scanArtifacts, detectClassification } = require('../preflight-engine');
|
|
7
|
+
const { auditAcceptanceCriteriaTests } = require('../lib/ac-test-audit');
|
|
8
|
+
const { runSpecAnalyze } = require('./spec-analyze');
|
|
9
|
+
|
|
10
|
+
function roundScore(value) {
|
|
11
|
+
return Math.round(Math.max(0, Math.min(1, value)) * 100) / 100;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function artifactScore(artifacts, classification) {
|
|
15
|
+
const required = ['project_context', 'prd', 'requirements', 'spec'];
|
|
16
|
+
if (classification !== 'MICRO') required.push('architecture', 'design_doc', 'readiness');
|
|
17
|
+
if (classification === 'MEDIUM') required.push('implementation_plan');
|
|
18
|
+
|
|
19
|
+
const present = required.filter((key) => artifacts[key] && artifacts[key].exists);
|
|
20
|
+
return {
|
|
21
|
+
score: required.length === 0 ? 1 : roundScore(present.length / required.length),
|
|
22
|
+
required,
|
|
23
|
+
present
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function specScore(specAnalyze) {
|
|
28
|
+
const summary = specAnalyze.summary || { errors: 0, warnings: 0, info: 0 };
|
|
29
|
+
if (summary.errors > 0) return 0;
|
|
30
|
+
return roundScore(1 - (summary.warnings * 0.1) - (summary.info * 0.03));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function acTestScore(acAudit) {
|
|
34
|
+
const total = acAudit.summary.acs_total;
|
|
35
|
+
if (total === 0) return 1;
|
|
36
|
+
return roundScore(acAudit.summary.covered / total);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function renderMarkdown(report) {
|
|
40
|
+
const lines = [
|
|
41
|
+
`# SDD Benchmark — ${report.feature}`,
|
|
42
|
+
'',
|
|
43
|
+
`- Classification: ${report.classification}`,
|
|
44
|
+
`- Final score: ${report.scores.final}`,
|
|
45
|
+
`- Implementation proxy: ${report.scores.implementation}`,
|
|
46
|
+
`- Test proof: ${report.scores.tests}`,
|
|
47
|
+
'',
|
|
48
|
+
'> Deterministic process-hygiene baseline (artifact chain + spec consistency + AC→test citation). It does not measure runtime correctness, token cost, or scope adherence.',
|
|
49
|
+
'',
|
|
50
|
+
'## Evidence',
|
|
51
|
+
'',
|
|
52
|
+
`- Required artifacts present: ${report.artifacts.present.length}/${report.artifacts.required.length}`,
|
|
53
|
+
`- Spec analyze: ${report.spec_analyze.summary.errors} error(s), ${report.spec_analyze.summary.warnings} warning(s), ${report.spec_analyze.summary.info} info`,
|
|
54
|
+
`- AC test audit: ${report.ac_test_audit.summary.covered}/${report.ac_test_audit.summary.acs_total} covered`,
|
|
55
|
+
''
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
if (report.ac_test_audit.missing.length > 0) {
|
|
59
|
+
lines.push('## Missing AC Test Evidence', '');
|
|
60
|
+
for (const ac of report.ac_test_audit.missing) lines.push(`- ${ac}`);
|
|
61
|
+
lines.push('');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
lines.push('## Raw Report', '', '```json', JSON.stringify(report, null, 2), '```', '');
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function runSddBenchmark({ args, options = {}, logger }) {
|
|
69
|
+
const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
|
|
70
|
+
const slug = String(options.feature || options.slug || '').trim();
|
|
71
|
+
|
|
72
|
+
if (!slug) {
|
|
73
|
+
if (options.json) return { ok: false, error: 'missing_feature' };
|
|
74
|
+
logger.error('--feature=<slug> is required.');
|
|
75
|
+
return { ok: false, error: 'missing_feature' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const artifacts = await scanArtifacts(targetDir, slug);
|
|
79
|
+
const classification = await detectClassification(targetDir, slug) || 'unknown';
|
|
80
|
+
const acAudit = await auditAcceptanceCriteriaTests(targetDir, slug);
|
|
81
|
+
const specLogger = { log: () => {}, error: () => {} };
|
|
82
|
+
const specAnalyze = await runSpecAnalyze({
|
|
83
|
+
args: [targetDir],
|
|
84
|
+
options: { feature: slug, strict: Boolean(options.strict) },
|
|
85
|
+
logger: specLogger
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const artifact = artifactScore(artifacts, classification);
|
|
89
|
+
const implementation = roundScore((artifact.score + specScore(specAnalyze)) / 2);
|
|
90
|
+
const tests = acTestScore(acAudit);
|
|
91
|
+
const final = roundScore((implementation * 0.6) + (tests * 0.4));
|
|
92
|
+
|
|
93
|
+
const report = {
|
|
94
|
+
ok: specAnalyze.ok && acAudit.ok,
|
|
95
|
+
feature: slug,
|
|
96
|
+
classification,
|
|
97
|
+
benchmarked_at: new Date().toISOString(),
|
|
98
|
+
strict: Boolean(options.strict),
|
|
99
|
+
scores: { final, implementation, tests, artifacts: artifact.score, spec: specScore(specAnalyze) },
|
|
100
|
+
artifacts: artifact,
|
|
101
|
+
spec_analyze: {
|
|
102
|
+
ok: specAnalyze.ok,
|
|
103
|
+
summary: specAnalyze.summary,
|
|
104
|
+
findings: specAnalyze.findings
|
|
105
|
+
},
|
|
106
|
+
ac_test_audit: acAudit
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const retroDir = path.join(targetDir, '.aioson', 'context', 'retro');
|
|
111
|
+
await fs.mkdir(retroDir, { recursive: true });
|
|
112
|
+
await fs.writeFile(path.join(retroDir, `sdd-benchmark-${slug}.md`), renderMarkdown(report), 'utf8');
|
|
113
|
+
} catch {
|
|
114
|
+
// stdout/JSON remains canonical when persistence is unavailable.
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.json) {
|
|
118
|
+
logger.log(JSON.stringify(report, null, 2));
|
|
119
|
+
return report;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logger.log('');
|
|
123
|
+
logger.log(`SDD benchmark — ${slug}`);
|
|
124
|
+
logger.log('━'.repeat(45));
|
|
125
|
+
logger.log(`Final score: ${final}`);
|
|
126
|
+
logger.log(`Implementation proxy: ${implementation}`);
|
|
127
|
+
logger.log(`Test proof: ${tests}`);
|
|
128
|
+
logger.log(`Report: .aioson/context/retro/sdd-benchmark-${slug}.md`);
|
|
129
|
+
logger.log('Note: deterministic process-hygiene baseline (artifact chain + spec consistency + AC→test citation) — not a measure of runtime correctness.');
|
|
130
|
+
logger.log('');
|
|
131
|
+
return report;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { runSddBenchmark, artifactScore, acTestScore, specScore };
|