@jaimevalasek/aioson 1.29.1 → 1.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +7 -5
  3. package/docs/en/5-reference/cli-reference.md +40 -10
  4. package/docs/pt/4-agentes/pm.md +1 -1
  5. package/docs/pt/5-referencia/autopilot-handoff.md +4 -4
  6. package/docs/pt/5-referencia/comandos-cli.md +5 -3
  7. package/docs/pt/5-referencia/fluxo-artefatos.md +1 -1
  8. package/docs/pt/5-referencia/memoria-e-contexto.md +2 -2
  9. package/docs/pt/_arquivo/monitor-de-contexto.md +2 -2
  10. package/package.json +4 -2
  11. package/src/cli.js +67 -24
  12. package/src/commands/ac-test-audit.js +45 -0
  13. package/src/commands/artifact-validate.js +62 -50
  14. package/src/commands/classify.js +73 -2
  15. package/src/commands/context-brief.js +59 -0
  16. package/src/commands/context-guard.js +88 -0
  17. package/src/commands/context-monitor.js +1 -1
  18. package/src/commands/context-search.js +101 -52
  19. package/src/commands/context-select.js +11 -2
  20. package/src/commands/feature-archive.js +21 -12
  21. package/src/commands/feature-current.js +82 -0
  22. package/src/commands/gate-check.js +32 -15
  23. package/src/commands/harness-check.js +17 -1
  24. package/src/commands/hooks-install.js +169 -26
  25. package/src/commands/hygiene-scan.js +423 -0
  26. package/src/commands/rules-lint.js +11 -3
  27. package/src/commands/sdd-benchmark.js +134 -0
  28. package/src/commands/spec-analyze.js +6 -4
  29. package/src/commands/store-system.js +329 -49
  30. package/src/constants.js +19 -6
  31. package/src/context-brief.js +585 -0
  32. package/src/context-guard.js +209 -0
  33. package/src/context-search.js +796 -96
  34. package/src/context-selector.js +802 -444
  35. package/src/handoff-contract.js +14 -6
  36. package/src/harness/contract-schema.js +1 -1
  37. package/src/i18n/messages/en.js +12 -5
  38. package/src/i18n/messages/es.js +11 -4
  39. package/src/i18n/messages/fr.js +11 -4
  40. package/src/i18n/messages/pt-BR.js +12 -5
  41. package/src/lib/ac-test-audit.js +194 -0
  42. package/src/preflight-engine.js +10 -6
  43. package/src/squad/state-manager.js +1 -1
  44. package/template/.aioson/agents/analyst.md +41 -17
  45. package/template/.aioson/agents/architect.md +4 -2
  46. package/template/.aioson/agents/briefing-refiner.md +15 -2
  47. package/template/.aioson/agents/briefing.md +12 -8
  48. package/template/.aioson/agents/committer.md +1 -1
  49. package/template/.aioson/agents/copywriter.md +20 -9
  50. package/template/.aioson/agents/design-hybrid-forge.md +9 -5
  51. package/template/.aioson/agents/dev.md +22 -25
  52. package/template/.aioson/agents/deyvin.md +126 -124
  53. package/template/.aioson/agents/discover.md +3 -1
  54. package/template/.aioson/agents/discovery-design-doc.md +11 -2
  55. package/template/.aioson/agents/forge-run.md +3 -0
  56. package/template/.aioson/agents/genome.md +9 -5
  57. package/template/.aioson/agents/neo.md +30 -24
  58. package/template/.aioson/agents/orache.md +10 -6
  59. package/template/.aioson/agents/orchestrator.md +4 -2
  60. package/template/.aioson/agents/pentester.md +22 -12
  61. package/template/.aioson/agents/pm.md +5 -3
  62. package/template/.aioson/agents/product.md +25 -18
  63. package/template/.aioson/agents/profiler-enricher.md +10 -6
  64. package/template/.aioson/agents/profiler-forge.md +10 -6
  65. package/template/.aioson/agents/profiler-researcher.md +10 -6
  66. package/template/.aioson/agents/qa.md +21 -19
  67. package/template/.aioson/agents/scope-check.md +9 -3
  68. package/template/.aioson/agents/sheldon.md +22 -8
  69. package/template/.aioson/agents/site-forge.md +2 -0
  70. package/template/.aioson/agents/squad.md +4 -2
  71. package/template/.aioson/agents/tester.md +19 -15
  72. package/template/.aioson/agents/ux-ui.md +16 -8
  73. package/template/.aioson/config.md +4 -3
  74. package/template/.aioson/design-docs/agent-loading-contract.md +3 -3
  75. package/template/.aioson/docs/autopilot-handoff.md +3 -3
  76. package/template/.aioson/docs/dev/simple-plan-lane.md +73 -27
  77. package/template/.aioson/docs/dev/stack-conventions.md +1 -1
  78. package/template/.aioson/docs/deyvin/continuity-recovery.md +1 -1
  79. package/template/.aioson/docs/deyvin/runtime-handoffs.md +3 -3
  80. package/template/.aioson/docs/feature-expansion-taxonomy.md +53 -0
  81. package/template/.aioson/docs/handoff-persistence.md +14 -12
  82. package/template/.aioson/docs/integrations/dashboard-app-form-publish-mapping.md +183 -0
  83. package/template/.aioson/docs/play/README.md +72 -0
  84. package/template/.aioson/docs/play/agent-usage-guide.md +106 -0
  85. package/template/.aioson/docs/play/app-compatibility-guide.md +112 -0
  86. package/template/.aioson/docs/play/auth-services-and-testing.md +220 -0
  87. package/template/.aioson/docs/play/llm-data-and-bindings.md +238 -0
  88. package/template/.aioson/docs/play/manifest-and-runtime.md +244 -0
  89. package/template/.aioson/docs/play/source-map.md +104 -0
  90. package/template/.aioson/docs/product/conversation-playbook.md +1 -1
  91. package/template/.aioson/docs/sheldon/enrichment-paths.md +44 -1
  92. package/template/.aioson/docs/sheldon/harness-contract.md +23 -21
  93. package/template/.aioson/docs/tester/coverage-quality.md +1 -1
  94. package/template/.aioson/docs/ux-ui/design-execution.md +9 -7
  95. package/template/.aioson/rules/README.md +35 -17
  96. package/template/.aioson/rules/agent-structural-contract.md +165 -160
  97. package/template/.aioson/rules/aioson-context-boundary.md +5 -4
  98. package/template/.aioson/rules/canonical-path-contract.md +5 -4
  99. package/template/.aioson/rules/data-format-convention.md +5 -4
  100. package/template/.aioson/rules/disk-first-artifacts.md +2 -2
  101. package/template/.aioson/rules/implementation-structure-and-data-access.md +50 -0
  102. package/template/.aioson/rules/security-baseline.md +4 -3
  103. package/template/.aioson/rules/simple-plan-lane.md +18 -6
  104. package/template/.aioson/rules/source-code-language-convention.md +34 -0
  105. package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +24 -23
  106. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -0
  107. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +2 -2
  108. package/template/.aioson/skills/process/aioson-spec-driven/references/qa.md +1 -1
  109. package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +72 -0
  110. package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +74 -0
  111. package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +67 -0
  112. package/template/.aioson/skills/static/context-budget-guide.md +1 -1
  113. package/template/.aioson/skills/static/multi-agent-patterns.md +5 -4
  114. package/template/AGENTS.md +36 -19
  115. 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
+ };
@@ -4,7 +4,15 @@ const fs = require('node:fs/promises');
4
4
  const path = require('node:path');
5
5
  const { parseFrontmatter } = require('../preflight-engine');
6
6
 
7
- const ROUTING_FIELDS = ['task_types', 'triggers', 'paths', 'globs'];
7
+ const ROUTING_FIELDS = [
8
+ 'task_types',
9
+ 'triggers',
10
+ 'aliases',
11
+ 'entities',
12
+ 'retrieval_intents',
13
+ 'paths',
14
+ 'globs'
15
+ ];
8
16
 
9
17
  function hasValue(raw) {
10
18
  if (raw === undefined || raw === null) return false;
@@ -24,7 +32,7 @@ function lintRule(relPath, frontmatter) {
24
32
 
25
33
  if (loadTier !== 'always' && routing.length === 0) {
26
34
  warnings.push(
27
- 'selector-invisible: no task_types, triggers, or paths — context:select can never score this rule above the load threshold, so agents will not load it on demand. Add routing metadata or set load_tier: always.'
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.'
28
36
  );
29
37
  }
30
38
 
@@ -107,7 +115,7 @@ async function runRulesLint({ args, options = {}, logger }) {
107
115
  const clean = rules.filter((rule) => rule.ok).length;
108
116
  logger.log(`Summary: ${clean}/${rules.length} ok, ${warningsCount} warning${warningsCount === 1 ? '' : 's'}.`);
109
117
  if (warningsCount > 0) {
110
- logger.log('Tip: add task_types/triggers/paths frontmatter so context:select can load the rule on demand (see .aioson/rules/README.md).');
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).');
111
119
  }
112
120
 
113
121
  return result;
@@ -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 };
@@ -30,9 +30,9 @@ const path = require('node:path');
30
30
 
31
31
  const { scanArtifacts, detectClassification } = require('../preflight-engine');
32
32
  const { validateContract } = require('../harness/contract-schema');
33
+ const { AC_ID_RE } = require('../lib/ac-test-audit');
33
34
 
34
- const REQ_ID_RE = /\bREQ(?:-[A-Z0-9]+)+\b/g;
35
- const AC_ID_RE = /\bAC(?:-[A-Z0-9]+)+\b/g;
35
+ const REQ_ID_RE = /\bREQ(?:-[A-Za-z0-9]+)+\b/g;
36
36
 
37
37
  /** Edições upstream no mesmo fluxo de geração não são drift — tolerância. */
38
38
  const STALENESS_TOLERANCE_MS = 60000;
@@ -88,6 +88,7 @@ async function runSpecAnalyze({ args, options = {}, logger }) {
88
88
 
89
89
  const artifacts = await scanArtifacts(targetDir, slug);
90
90
  const classification = await detectClassification(targetDir, slug);
91
+ const strict = Boolean(options.strict);
91
92
  const contractInfo = readContract(targetDir, slug);
92
93
  const findings = [];
93
94
 
@@ -236,7 +237,7 @@ async function runSpecAnalyze({ args, options = {}, logger }) {
236
237
  }
237
238
  for (const warn of schema.warnings) {
238
239
  findings.push({
239
- severity: 'info',
240
+ severity: strict ? 'error' : 'info',
240
241
  check: 'contract_coverage',
241
242
  message: `${warn.field}: ${warn.reason}`,
242
243
  artifacts: ['harness-contract']
@@ -249,7 +250,7 @@ async function runSpecAnalyze({ args, options = {}, logger }) {
249
250
  const mentioned = [...declaredAcs].filter((id) => contractInfo.raw.includes(id));
250
251
  if (mentioned.length === 0) {
251
252
  findings.push({
252
- severity: 'info',
253
+ severity: strict ? 'error' : 'info',
253
254
  check: 'contract_ac_unlinked',
254
255
  message: `none of the ${declaredAcs.size} AC id(s) from requirements appear in harness-contract.json — confirm criteria[] actually derive from the enriched ACs`,
255
256
  artifacts: ['requirements', 'harness-contract']
@@ -269,6 +270,7 @@ async function runSpecAnalyze({ args, options = {}, logger }) {
269
270
  ok: summary.errors === 0,
270
271
  feature: slug,
271
272
  classification: classification || 'unknown',
273
+ strict,
272
274
  analyzed_at: new Date().toISOString(),
273
275
  artifacts_present: present,
274
276
  contract_present: Boolean(contractInfo.exists && !contractInfo.parseError),