@jaimevalasek/aioson 1.6.0 → 1.7.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 +49 -0
- package/README.md +729 -232
- package/docs/design-previews/pt.squarespace.com-homepage.html +889 -0
- package/docs/integrations/sdlc-genius-boundary.md +76 -0
- package/docs/integrations/sdlc-genius-eval-matrix.md +75 -0
- package/docs/integrations/sdlc-genius-install-checklist.md +93 -0
- package/docs/integrations/sdlc-genius-review-samples.md +86 -0
- package/docs/pt/README.md +3 -0
- package/docs/pt/agentes.md +1 -0
- package/docs/pt/comandos-cli.md +888 -2
- package/docs/pt/design-hybrid-forge.md +255 -6
- package/docs/pt/devlog-pipeline.md +270 -0
- package/docs/pt/fluxo-artefatos.md +178 -0
- package/docs/pt/hooks-session-guard.md +454 -0
- package/docs/pt/monitor-de-contexto.md +59 -5
- package/docs/pt/sdd-automation-scripts.md +557 -0
- package/docs/pt/site-forge.md +309 -0
- package/docs/pt/spec-learnings-pipeline.md +265 -0
- package/package.json +1 -1
- package/src/a2a/client.js +165 -0
- package/src/a2a/server.js +223 -0
- package/src/cli.js +235 -1
- package/src/commands/agent-audit.js +397 -0
- package/src/commands/agent-export-skill.js +229 -0
- package/src/commands/artifact-validate.js +189 -0
- package/src/commands/brief-gen.js +405 -0
- package/src/commands/brief-validate.js +65 -0
- package/src/commands/classify.js +256 -0
- package/src/commands/context-compact.js +49 -0
- package/src/commands/context-health.js +175 -0
- package/src/commands/context-monitor.js +71 -0
- package/src/commands/context-trim.js +177 -0
- package/src/commands/detect-test-runner.js +55 -0
- package/src/commands/devlog-export-brains.js +27 -0
- package/src/commands/devlog-process.js +292 -0
- package/src/commands/devlog-watch.js +131 -0
- package/src/commands/feature-close.js +165 -0
- package/src/commands/gate-check.js +228 -0
- package/src/commands/hooks-emit.js +253 -0
- package/src/commands/hooks-install.js +347 -0
- package/src/commands/learning-auto-promote.js +195 -0
- package/src/commands/learning-evolve.js +18 -9
- package/src/commands/learning-export.js +103 -0
- package/src/commands/learning-rollback.js +164 -0
- package/src/commands/live.js +25 -1
- package/src/commands/pattern-detect.js +33 -0
- package/src/commands/preflight-context.js +30 -0
- package/src/commands/preflight.js +208 -0
- package/src/commands/pulse-update.js +130 -0
- package/src/commands/runner-daemon.js +274 -0
- package/src/commands/runner-plan.js +70 -0
- package/src/commands/runner-queue-from-plan.js +166 -0
- package/src/commands/runner-queue.js +189 -0
- package/src/commands/runner-run.js +129 -0
- package/src/commands/runtime.js +47 -1
- package/src/commands/self-implement-loop.js +256 -0
- package/src/commands/session-guard.js +218 -0
- package/src/commands/sizing.js +165 -0
- package/src/commands/skill.js +65 -0
- package/src/commands/spec-checkpoint.js +177 -0
- package/src/commands/spec-status.js +79 -0
- package/src/commands/spec-sync.js +190 -0
- package/src/commands/spec-tasks.js +288 -0
- package/src/commands/squad-autorun.js +1220 -0
- package/src/commands/squad-bus.js +217 -0
- package/src/commands/squad-card.js +149 -0
- package/src/commands/squad-daemon.js +134 -0
- package/src/commands/squad-dependency-graph.js +164 -0
- package/src/commands/squad-review.js +106 -0
- package/src/commands/squad-scaffold.js +55 -0
- package/src/commands/squad-tool-register.js +157 -0
- package/src/commands/state-save.js +122 -0
- package/src/commands/update.js +2 -0
- package/src/commands/verify-gate.js +572 -0
- package/src/commands/workflow-execute.js +241 -0
- package/src/constants.js +9 -0
- package/src/install-profile.js +2 -2
- package/src/install-wizard.js +3 -2
- package/src/installer.js +6 -0
- package/src/lib/health-check.js +158 -0
- package/src/lib/hook-protocol.js +76 -0
- package/src/mcp/apps/squad-dashboard/app.js +163 -0
- package/src/mcp/apps/squad-dashboard/index.html +261 -0
- package/src/mcp/apps/squad-dashboard/mcp-manifest.json +23 -0
- package/src/mcp/resources/squad-state.js +130 -0
- package/src/preflight-engine.js +443 -0
- package/src/runner/cascade.js +97 -0
- package/src/runner/cli-launcher.js +109 -0
- package/src/runner/plan-importer.js +63 -0
- package/src/runner/queue-store.js +159 -0
- package/src/runtime-store.js +61 -3
- package/src/squad/agent-teams-adapter.js +264 -0
- package/src/squad/brief-validator.js +350 -0
- package/src/squad/bus-bridge.js +140 -0
- package/src/squad/context-compactor.js +265 -0
- package/src/squad/cross-ai-synthesizer.js +250 -0
- package/src/squad/hooks-generator.js +196 -0
- package/src/squad/inter-squad-events.js +175 -0
- package/src/squad/intra-bus.js +345 -0
- package/src/squad/learning-extractor.js +213 -0
- package/src/squad/pattern-detector.js +365 -0
- package/src/squad/preflight-context.js +296 -0
- package/src/squad/recovery-context.js +242 -71
- package/src/squad/reflection.js +365 -0
- package/src/squad/squad-scaffold.js +177 -0
- package/src/squad/state-manager.js +310 -0
- package/src/squad/task-decomposer.js +652 -0
- package/src/squad/verify-gate.js +303 -0
- package/src/updater.js +4 -5
- package/src/worker-runner.js +186 -1
- package/template/.aioson/agents/analyst.md +62 -1
- package/template/.aioson/agents/architect.md +61 -1
- package/template/.aioson/agents/design-hybrid-forge.md +14 -0
- package/template/.aioson/agents/dev.md +242 -24
- package/template/.aioson/agents/deyvin.md +66 -8
- package/template/.aioson/agents/discovery-design-doc.md +44 -0
- package/template/.aioson/agents/genome.md +14 -0
- package/template/.aioson/agents/neo.md +78 -1
- package/template/.aioson/agents/orache.md +50 -4
- package/template/.aioson/agents/orchestrator.md +197 -1
- package/template/.aioson/agents/pm.md +35 -0
- package/template/.aioson/agents/product.md +50 -5
- package/template/.aioson/agents/profiler-enricher.md +14 -0
- package/template/.aioson/agents/profiler-forge.md +14 -0
- package/template/.aioson/agents/profiler-researcher.md +14 -0
- package/template/.aioson/agents/qa.md +172 -21
- package/template/.aioson/agents/setup.md +79 -9
- package/template/.aioson/agents/sheldon.md +131 -6
- package/template/.aioson/agents/site-forge.md +1753 -0
- package/template/.aioson/agents/squad.md +162 -0
- package/template/.aioson/agents/tester.md +53 -0
- package/template/.aioson/agents/ux-ui.md +34 -1
- package/template/.aioson/brains/README.md +128 -0
- package/template/.aioson/brains/_index.json +16 -0
- package/template/.aioson/brains/scripts/query.js +103 -0
- package/template/.aioson/brains/site-forge/visual-patterns.brain.json +205 -0
- package/template/.aioson/config.md +143 -13
- package/template/.aioson/constitution.md +33 -0
- package/template/.aioson/context/project-pulse.md +34 -0
- package/template/.aioson/docs/LAYERS.md +79 -0
- package/template/.aioson/docs/README.md +76 -0
- package/template/.aioson/docs/example-external-api-context.md +72 -0
- package/template/.aioson/locales/en/agents/architect.md +17 -0
- package/template/.aioson/locales/en/agents/dev.md +79 -13
- package/template/.aioson/locales/en/agents/orache.md +6 -0
- package/template/.aioson/locales/en/agents/orchestrator.md +24 -0
- package/template/.aioson/locales/en/agents/product.md +50 -0
- package/template/.aioson/locales/en/agents/sheldon.md +115 -0
- package/template/.aioson/locales/en/agents/squad.md +14 -0
- package/template/.aioson/locales/en/agents/tester.md +6 -0
- package/template/.aioson/locales/es/agents/analyst.md +2 -0
- package/template/.aioson/locales/es/agents/architect.md +19 -0
- package/template/.aioson/locales/es/agents/dev.md +64 -4
- package/template/.aioson/locales/es/agents/deyvin.md +2 -0
- package/template/.aioson/locales/es/agents/discovery-design-doc.md +2 -0
- package/template/.aioson/locales/es/agents/genome.md +2 -0
- package/template/.aioson/locales/es/agents/neo.md +2 -0
- package/template/.aioson/locales/es/agents/orache.md +2 -0
- package/template/.aioson/locales/es/agents/orchestrator.md +26 -0
- package/template/.aioson/locales/es/agents/pair.md +2 -0
- package/template/.aioson/locales/es/agents/pm.md +2 -0
- package/template/.aioson/locales/es/agents/product.md +52 -0
- package/template/.aioson/locales/es/agents/profiler-enricher.md +2 -0
- package/template/.aioson/locales/es/agents/profiler-forge.md +2 -0
- package/template/.aioson/locales/es/agents/profiler-researcher.md +2 -0
- package/template/.aioson/locales/es/agents/qa.md +2 -0
- package/template/.aioson/locales/es/agents/setup.md +2 -0
- package/template/.aioson/locales/es/agents/sheldon.md +117 -0
- package/template/.aioson/locales/es/agents/squad.md +16 -0
- package/template/.aioson/locales/es/agents/tester.md +9 -0
- package/template/.aioson/locales/es/agents/ux-ui.md +2 -0
- package/template/.aioson/locales/fr/agents/analyst.md +2 -0
- package/template/.aioson/locales/fr/agents/architect.md +19 -0
- package/template/.aioson/locales/fr/agents/dev.md +64 -4
- package/template/.aioson/locales/fr/agents/deyvin.md +2 -0
- package/template/.aioson/locales/fr/agents/discovery-design-doc.md +2 -0
- package/template/.aioson/locales/fr/agents/genome.md +2 -0
- package/template/.aioson/locales/fr/agents/neo.md +2 -0
- package/template/.aioson/locales/fr/agents/orache.md +2 -0
- package/template/.aioson/locales/fr/agents/orchestrator.md +26 -0
- package/template/.aioson/locales/fr/agents/pair.md +2 -0
- package/template/.aioson/locales/fr/agents/pm.md +2 -0
- package/template/.aioson/locales/fr/agents/product.md +52 -0
- package/template/.aioson/locales/fr/agents/profiler-enricher.md +2 -0
- package/template/.aioson/locales/fr/agents/profiler-forge.md +2 -0
- package/template/.aioson/locales/fr/agents/profiler-researcher.md +2 -0
- package/template/.aioson/locales/fr/agents/qa.md +2 -0
- package/template/.aioson/locales/fr/agents/setup.md +2 -0
- package/template/.aioson/locales/fr/agents/sheldon.md +117 -0
- package/template/.aioson/locales/fr/agents/squad.md +16 -0
- package/template/.aioson/locales/fr/agents/tester.md +9 -0
- package/template/.aioson/locales/fr/agents/ux-ui.md +2 -0
- package/template/.aioson/locales/pt-BR/agents/analyst.md +64 -3
- package/template/.aioson/locales/pt-BR/agents/architect.md +42 -0
- package/template/.aioson/locales/pt-BR/agents/dev.md +147 -14
- package/template/.aioson/locales/pt-BR/agents/deyvin.md +47 -0
- package/template/.aioson/locales/pt-BR/agents/neo.md +62 -1
- package/template/.aioson/locales/pt-BR/agents/orchestrator.md +158 -2
- package/template/.aioson/locales/pt-BR/agents/pm.md +95 -1
- package/template/.aioson/locales/pt-BR/agents/product.md +145 -18
- package/template/.aioson/locales/pt-BR/agents/qa.md +16 -0
- package/template/.aioson/locales/pt-BR/agents/setup.md +101 -18
- package/template/.aioson/locales/pt-BR/agents/sheldon.md +132 -1
- package/template/.aioson/locales/pt-BR/agents/squad.md +14 -0
- package/template/.aioson/locales/pt-BR/agents/tester.md +449 -0
- package/template/.aioson/rules/README.md +69 -0
- package/template/.aioson/rules/data-format-convention.md +136 -0
- package/template/.aioson/rules/example-monetary-values.md +30 -0
- package/template/.aioson/schemas/squad-manifest.schema.json +124 -3
- package/template/.aioson/skills/design/pt.squarespace.com/.skill-meta.json +31 -0
- package/template/.aioson/skills/design/pt.squarespace.com/SKILL.md +66 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/components.md +368 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/design-tokens.md +150 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/motion.md +270 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/patterns.md +189 -0
- package/template/.aioson/skills/design/pt.squarespace.com/references/websites.md +165 -0
- package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +1 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/analyst.md +30 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/architect.md +23 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +47 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/deyvin.md +27 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/maintenance-and-state.md +35 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/product.md +25 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/qa.md +30 -0
- package/template/.aioson/skills/process/aioson-spec-driven/references/sheldon.md +25 -0
- package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +4 -1
- package/template/.aioson/skills/process/design-hybrid-forge/references/output-contract.md +15 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/pair-compatibility.md +32 -0
- package/template/.aioson/skills/process/design-hybrid-forge/references/quality-gates.md +20 -0
- package/template/.aioson/skills/process/simplify/SKILL.md +173 -0
- package/template/.aioson/skills/static/context-budget-guide.md +46 -0
- package/template/.aioson/skills/static/harness-sensors.md +74 -0
- package/template/.aioson/skills/static/multi-agent-patterns.md +43 -0
- package/template/.aioson/skills/static/react-motion-patterns.md +22 -0
- package/template/.aioson/skills/static/static-html-patterns/checklists.md +43 -0
- package/template/.aioson/skills/static/static-html-patterns/css-tokens.md +609 -0
- package/template/.aioson/skills/static/static-html-patterns/motion.md +193 -0
- package/template/.aioson/skills/static/static-html-patterns/premium.md +711 -0
- package/template/.aioson/skills/static/static-html-patterns/structure.md +209 -0
- package/template/.aioson/skills/static/static-html-patterns/utilities.md +190 -0
- package/template/.aioson/skills/static/static-html-patterns.md +58 -1913
- package/template/.aioson/skills/static/threejs-patterns.md +929 -0
- package/template/.aioson/skills/static/web-research-cache.md +112 -0
- package/template/.aioson/tasks/implementation-plan.md +21 -1
- package/template/.claude/commands/aioson/agent/design-hybrid-forge.md +5 -0
- package/template/.claude/commands/aioson/agent/orache.md +5 -0
- package/template/.claude/commands/aioson/agent/sheldon.md +5 -0
- package/template/.claude/commands/aioson/agent/site-forge.md +5 -0
- package/template/AGENTS.md +55 -3
- package/template/CLAUDE.md +30 -0
- package/template/OPENCODE.md +4 -0
- package/template/researchs/.gitkeep +0 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson verify:gate — Fresh-eyes verification pass on a deliverable
|
|
5
|
+
*
|
|
6
|
+
* Spawns a verification pass using only the spec and the artifact (file or
|
|
7
|
+
* directory). No conversation history is carried in — this catches bugs the
|
|
8
|
+
* generating agent cannot see due to context bias.
|
|
9
|
+
*
|
|
10
|
+
* Verdict outputs:
|
|
11
|
+
* PASS — artifact satisfies all spec requirements
|
|
12
|
+
* PASS_WITH_NOTES — passes but has minor issues worth flagging
|
|
13
|
+
* FAIL_WITH_ISSUES — one or more requirements not met (list provided)
|
|
14
|
+
* BLOCKED — cannot evaluate (missing spec, unreadable artifact, etc.)
|
|
15
|
+
*
|
|
16
|
+
* What it checks (deterministic, no LLM):
|
|
17
|
+
* - Required file existence (from spec "Files to write" or "Output files")
|
|
18
|
+
* - Forbidden patterns (patterns listed under "Constraints" or "Must not")
|
|
19
|
+
* - Required patterns (patterns listed under "Must contain" or "Done criteria")
|
|
20
|
+
* - File size sanity (files > 0 bytes, no obviously truncated outputs)
|
|
21
|
+
* - Acceptance criteria checkboxes presence (reports unchecked items)
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* aioson verify:gate . --spec=.aioson/context/spec.md --artifact=src/
|
|
25
|
+
* aioson verify:gate . --spec=briefs/phase-2.md --artifact=src/api/
|
|
26
|
+
* aioson verify:gate . --spec=briefs/phase-2.md --artifact=src/api/ --out=verify-phase-2.md
|
|
27
|
+
* aioson verify:gate . --spec=briefs/phase-2.md --artifact=src/foo.ts --strict
|
|
28
|
+
* aioson verify:gate . --json
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs/promises');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
|
|
34
|
+
// ─── Spec parser ──────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extract structured verification requirements from a spec/brief markdown.
|
|
38
|
+
* Looks for:
|
|
39
|
+
* - "Done criteria" / "Acceptance criteria" sections → required checkboxes
|
|
40
|
+
* - "Files to write" / "Output files" → required file paths
|
|
41
|
+
* - "Must contain" / "Required patterns" → regex patterns to match
|
|
42
|
+
* - "Hard constraints" / "Must not" → forbidden patterns
|
|
43
|
+
*/
|
|
44
|
+
function parseSpecRequirements(content) {
|
|
45
|
+
const lines = content.split(/\r?\n/);
|
|
46
|
+
const requirements = {
|
|
47
|
+
required_files: [],
|
|
48
|
+
required_patterns: [],
|
|
49
|
+
forbidden_patterns: [],
|
|
50
|
+
acceptance_criteria: [],
|
|
51
|
+
raw_sections: {}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let currentSection = null;
|
|
55
|
+
|
|
56
|
+
const SECTION_MAP = {
|
|
57
|
+
'done criteria': 'done',
|
|
58
|
+
'acceptance criteria': 'done',
|
|
59
|
+
'files to write': 'output_files',
|
|
60
|
+
'files to create': 'output_files',
|
|
61
|
+
'output files': 'output_files',
|
|
62
|
+
'must contain': 'required_patterns',
|
|
63
|
+
'required patterns': 'required_patterns',
|
|
64
|
+
'hard constraints': 'forbidden',
|
|
65
|
+
'must not': 'forbidden',
|
|
66
|
+
'constraints': 'forbidden',
|
|
67
|
+
'out of scope': 'out_of_scope'
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const headingRe = /^(#{1,4})\s+(.+)$/;
|
|
71
|
+
let sectionLevel = 0;
|
|
72
|
+
|
|
73
|
+
for (const line of lines) {
|
|
74
|
+
const headingMatch = line.match(headingRe);
|
|
75
|
+
if (headingMatch) {
|
|
76
|
+
const level = headingMatch[1].length;
|
|
77
|
+
const title = headingMatch[2].trim().toLowerCase();
|
|
78
|
+
|
|
79
|
+
// Check if this heading exits the current section
|
|
80
|
+
if (currentSection && level <= sectionLevel) {
|
|
81
|
+
currentSection = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check if this heading starts a tracked section
|
|
85
|
+
for (const [keyword, sectionType] of Object.entries(SECTION_MAP)) {
|
|
86
|
+
if (title.includes(keyword)) {
|
|
87
|
+
currentSection = sectionType;
|
|
88
|
+
sectionLevel = level;
|
|
89
|
+
if (!requirements.raw_sections[sectionType]) {
|
|
90
|
+
requirements.raw_sections[sectionType] = [];
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!currentSection) continue;
|
|
99
|
+
|
|
100
|
+
// Parse bullet items
|
|
101
|
+
const bulletMatch = line.match(/^[-*]\s+(.+)$/);
|
|
102
|
+
const checkboxMatch = line.match(/^[-*]\s+\[([ x])\]\s+(.+)$/i);
|
|
103
|
+
const item = checkboxMatch ? checkboxMatch[2].trim() : (bulletMatch ? bulletMatch[1].trim() : null);
|
|
104
|
+
|
|
105
|
+
if (!item) continue;
|
|
106
|
+
|
|
107
|
+
if (requirements.raw_sections[currentSection]) {
|
|
108
|
+
requirements.raw_sections[currentSection].push(item);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
switch (currentSection) {
|
|
112
|
+
case 'done': {
|
|
113
|
+
const checked = checkboxMatch ? checkboxMatch[1].trim().toLowerCase() === 'x' : false;
|
|
114
|
+
requirements.acceptance_criteria.push({ text: item, checked });
|
|
115
|
+
|
|
116
|
+
// Extract file paths from criteria (pattern: `path/to/file`)
|
|
117
|
+
const fileMatch = item.match(/`([^`]+\.[a-z]{1,6})`/g);
|
|
118
|
+
if (fileMatch) {
|
|
119
|
+
for (const m of fileMatch) {
|
|
120
|
+
const fp = m.replace(/`/g, '').trim();
|
|
121
|
+
if (!fp.includes(' ') && fp.includes('/')) {
|
|
122
|
+
requirements.required_files.push(fp);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case 'output_files': {
|
|
129
|
+
// Extract paths from items like "`src/foo.ts` — description"
|
|
130
|
+
const fpMatch = item.match(/`([^`]+)`/);
|
|
131
|
+
if (fpMatch) {
|
|
132
|
+
requirements.required_files.push(fpMatch[1].trim());
|
|
133
|
+
} else if (!item.includes(' ') || item.startsWith('src/') || item.startsWith('./')) {
|
|
134
|
+
requirements.required_files.push(item.split(' ')[0].trim());
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case 'required_patterns': {
|
|
139
|
+
requirements.required_patterns.push(item);
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
case 'forbidden': {
|
|
143
|
+
// Only add as forbidden pattern if it looks like a code or file reference
|
|
144
|
+
const fpMatch2 = item.match(/`([^`]+)`/);
|
|
145
|
+
if (fpMatch2) {
|
|
146
|
+
requirements.forbidden_patterns.push(fpMatch2[1].trim());
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Deduplicate
|
|
154
|
+
requirements.required_files = [...new Set(requirements.required_files)];
|
|
155
|
+
requirements.required_patterns = [...new Set(requirements.required_patterns)];
|
|
156
|
+
requirements.forbidden_patterns = [...new Set(requirements.forbidden_patterns)];
|
|
157
|
+
|
|
158
|
+
return requirements;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ─── Artifact scanner ─────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
async function collectFiles(artifactPath, maxFiles = 200) {
|
|
164
|
+
const collected = [];
|
|
165
|
+
|
|
166
|
+
async function walk(p) {
|
|
167
|
+
if (collected.length >= maxFiles) return;
|
|
168
|
+
let stat;
|
|
169
|
+
try {
|
|
170
|
+
stat = await fs.stat(p);
|
|
171
|
+
} catch {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (stat.isFile()) {
|
|
176
|
+
collected.push(p);
|
|
177
|
+
} else if (stat.isDirectory()) {
|
|
178
|
+
let entries;
|
|
179
|
+
try {
|
|
180
|
+
entries = await fs.readdir(p, { withFileTypes: true });
|
|
181
|
+
} catch {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (entry.name.startsWith('.')) continue; // skip dot files
|
|
186
|
+
await walk(path.join(p, entry.name));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
await walk(artifactPath);
|
|
192
|
+
return collected;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function readFileSafe(filePath, maxBytes = 50000) {
|
|
196
|
+
try {
|
|
197
|
+
const buf = Buffer.alloc(maxBytes);
|
|
198
|
+
const fh = await fs.open(filePath, 'r');
|
|
199
|
+
const { bytesRead } = await fh.read(buf, 0, maxBytes, 0);
|
|
200
|
+
await fh.close();
|
|
201
|
+
return buf.slice(0, bytesRead).toString('utf8');
|
|
202
|
+
} catch {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ─── Checks ───────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
async function checkRequiredFiles(requirements, artifactBase, allFiles, targetDir) {
|
|
210
|
+
const issues = [];
|
|
211
|
+
const passes = [];
|
|
212
|
+
|
|
213
|
+
for (const requiredPath of requirements.required_files) {
|
|
214
|
+
// Try relative to artifact base first, then targetDir
|
|
215
|
+
const candidates = [
|
|
216
|
+
path.resolve(artifactBase, requiredPath),
|
|
217
|
+
path.resolve(targetDir, requiredPath)
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
let found = false;
|
|
221
|
+
for (const c of candidates) {
|
|
222
|
+
if (allFiles.includes(c)) {
|
|
223
|
+
found = true;
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
// Also try if the file appears anywhere in allFiles by basename
|
|
227
|
+
const basename = path.basename(requiredPath);
|
|
228
|
+
if (allFiles.some((f) => path.basename(f) === basename && f.includes(path.dirname(requiredPath)))) {
|
|
229
|
+
found = true;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (found) {
|
|
235
|
+
passes.push(`Required file exists: \`${requiredPath}\``);
|
|
236
|
+
} else {
|
|
237
|
+
issues.push(`Missing required file: \`${requiredPath}\``);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return { issues, passes };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function checkFileContents(requirements, allFiles, targetDir) {
|
|
245
|
+
const issues = [];
|
|
246
|
+
const passes = [];
|
|
247
|
+
const notes = [];
|
|
248
|
+
|
|
249
|
+
// Check for empty files
|
|
250
|
+
for (const filePath of allFiles) {
|
|
251
|
+
try {
|
|
252
|
+
const stat = await fs.stat(filePath);
|
|
253
|
+
if (stat.size === 0) {
|
|
254
|
+
notes.push(`Empty file: \`${path.relative(targetDir, filePath)}\``);
|
|
255
|
+
}
|
|
256
|
+
} catch { /* ignore */ }
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check required patterns across all artifact files
|
|
260
|
+
if (requirements.required_patterns.length > 0) {
|
|
261
|
+
const allContent = [];
|
|
262
|
+
for (const filePath of allFiles.slice(0, 30)) { // limit to first 30 files
|
|
263
|
+
const content = await readFileSafe(filePath);
|
|
264
|
+
if (content) allContent.push({ path: filePath, content });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const pattern of requirements.required_patterns) {
|
|
268
|
+
const found = allContent.some(({ content }) => content.includes(pattern));
|
|
269
|
+
if (found) {
|
|
270
|
+
passes.push(`Required pattern found: \`${pattern}\``);
|
|
271
|
+
} else {
|
|
272
|
+
issues.push(`Required pattern not found in artifact: \`${pattern}\``);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Check forbidden patterns
|
|
278
|
+
if (requirements.forbidden_patterns.length > 0) {
|
|
279
|
+
const allContent = [];
|
|
280
|
+
for (const filePath of allFiles.slice(0, 30)) {
|
|
281
|
+
const content = await readFileSafe(filePath);
|
|
282
|
+
if (content) allContent.push({ path: path.relative(targetDir, filePath), content });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
for (const pattern of requirements.forbidden_patterns) {
|
|
286
|
+
const found = allContent.find(({ content }) => content.includes(pattern));
|
|
287
|
+
if (found) {
|
|
288
|
+
issues.push(`Forbidden pattern found in \`${found.path}\`: \`${pattern}\``);
|
|
289
|
+
} else {
|
|
290
|
+
passes.push(`Forbidden pattern absent: \`${pattern}\``);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { issues, passes, notes };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function checkAcceptanceCriteria(requirements) {
|
|
299
|
+
const unchecked = requirements.acceptance_criteria.filter((c) => !c.checked);
|
|
300
|
+
const checked = requirements.acceptance_criteria.filter((c) => c.checked);
|
|
301
|
+
|
|
302
|
+
const issues = [];
|
|
303
|
+
const passes = [];
|
|
304
|
+
const notes = [];
|
|
305
|
+
|
|
306
|
+
for (const c of checked) {
|
|
307
|
+
passes.push(`Criterion checked: ${c.text}`);
|
|
308
|
+
}
|
|
309
|
+
for (const c of unchecked) {
|
|
310
|
+
// Only warn if criteria look like real requirements (not placeholder text)
|
|
311
|
+
if (c.text.includes('Fill in') || c.text.includes('Example')) {
|
|
312
|
+
notes.push(`Placeholder criterion (not checked): ${c.text}`);
|
|
313
|
+
} else {
|
|
314
|
+
issues.push(`Unchecked criterion: ${c.text}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return { issues, passes, notes };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ─── Report builder ───────────────────────────────────────────────────────────
|
|
322
|
+
|
|
323
|
+
function buildReport({ verdict, specPath, artifactPath, projectDir, issues, passes, notes, fileCount, requirements }) {
|
|
324
|
+
const relSpec = path.relative(projectDir, specPath);
|
|
325
|
+
const relArtifact = path.relative(projectDir, artifactPath);
|
|
326
|
+
const now = new Date().toISOString();
|
|
327
|
+
|
|
328
|
+
const lines = [
|
|
329
|
+
'---',
|
|
330
|
+
`generated_at : ${now}`,
|
|
331
|
+
`spec : ${relSpec}`,
|
|
332
|
+
`artifact : ${relArtifact}`,
|
|
333
|
+
`verdict : ${verdict}`,
|
|
334
|
+
`files_scanned: ${fileCount}`,
|
|
335
|
+
'---',
|
|
336
|
+
'',
|
|
337
|
+
`# Verify Gate — ${verdict}`,
|
|
338
|
+
'',
|
|
339
|
+
`**Spec:** \`${relSpec}\``,
|
|
340
|
+
`**Artifact:** \`${relArtifact}\``,
|
|
341
|
+
`**Files scanned:** ${fileCount}`,
|
|
342
|
+
''
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
if (issues.length > 0) {
|
|
346
|
+
lines.push('## Issues');
|
|
347
|
+
for (const issue of issues) lines.push(`- ❌ ${issue}`);
|
|
348
|
+
lines.push('');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (notes.length > 0) {
|
|
352
|
+
lines.push('## Notes');
|
|
353
|
+
for (const note of notes) lines.push(`- ⚠ ${note}`);
|
|
354
|
+
lines.push('');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (passes.length > 0) {
|
|
358
|
+
lines.push('## Passed checks');
|
|
359
|
+
for (const p of passes) lines.push(`- ✓ ${p}`);
|
|
360
|
+
lines.push('');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (requirements.required_files.length === 0 &&
|
|
364
|
+
requirements.acceptance_criteria.length === 0 &&
|
|
365
|
+
requirements.required_patterns.length === 0) {
|
|
366
|
+
lines.push('## Coverage warning');
|
|
367
|
+
lines.push('');
|
|
368
|
+
lines.push('No verifiable requirements were extracted from the spec.');
|
|
369
|
+
lines.push('The spec may not contain "Done criteria", "Files to write", or "Must contain" sections.');
|
|
370
|
+
lines.push('Add structured sections to the spec to enable meaningful verification.');
|
|
371
|
+
lines.push('');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return lines.join('\n');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ─── Verdict logic ────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
function determineVerdict(issues, notes, strict) {
|
|
380
|
+
if (issues.length > 0) return 'FAIL_WITH_ISSUES';
|
|
381
|
+
if (notes.length > 0 && strict) return 'FAIL_WITH_ISSUES';
|
|
382
|
+
if (notes.length > 0) return 'PASS_WITH_NOTES';
|
|
383
|
+
return 'PASS';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ─── Main command ─────────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
async function runVerifyGate({ args, options = {}, logger }) {
|
|
389
|
+
const targetDir = path.resolve(process.cwd(), args[0] || '.');
|
|
390
|
+
const strict = Boolean(options.strict);
|
|
391
|
+
|
|
392
|
+
// ── Locate spec ──────────────────────────────────────────────────────────
|
|
393
|
+
let specPath;
|
|
394
|
+
if (options.spec) {
|
|
395
|
+
specPath = path.resolve(targetDir, options.spec);
|
|
396
|
+
} else {
|
|
397
|
+
// Auto-discover: prefer brief files, then spec.md
|
|
398
|
+
const candidates = [
|
|
399
|
+
path.join(targetDir, '.aioson', 'context', 'spec.md'),
|
|
400
|
+
path.join(targetDir, 'docs', 'spec.md'),
|
|
401
|
+
path.join(targetDir, 'SPEC.md')
|
|
402
|
+
];
|
|
403
|
+
for (const c of candidates) {
|
|
404
|
+
try {
|
|
405
|
+
await fs.access(c);
|
|
406
|
+
specPath = c;
|
|
407
|
+
break;
|
|
408
|
+
} catch { /* continue */ }
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (!specPath) {
|
|
413
|
+
const msg = 'No spec file found. Use --spec=<path> to specify one.';
|
|
414
|
+
if (options.json) return { ok: false, verdict: 'BLOCKED', error: msg };
|
|
415
|
+
logger.error(msg);
|
|
416
|
+
return { ok: false, verdict: 'BLOCKED' };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const specContent = await (async () => {
|
|
420
|
+
try { return await fs.readFile(specPath, 'utf8'); } catch { return null; }
|
|
421
|
+
})();
|
|
422
|
+
|
|
423
|
+
if (!specContent) {
|
|
424
|
+
const msg = `Cannot read spec file: ${path.relative(targetDir, specPath)}`;
|
|
425
|
+
if (options.json) return { ok: false, verdict: 'BLOCKED', error: msg };
|
|
426
|
+
logger.error(msg);
|
|
427
|
+
return { ok: false, verdict: 'BLOCKED' };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── Locate artifact ──────────────────────────────────────────────────────
|
|
431
|
+
let artifactPath;
|
|
432
|
+
if (options.artifact) {
|
|
433
|
+
artifactPath = path.resolve(targetDir, options.artifact);
|
|
434
|
+
} else {
|
|
435
|
+
// Default to src/ if it exists
|
|
436
|
+
const srcDir = path.join(targetDir, 'src');
|
|
437
|
+
try {
|
|
438
|
+
await fs.access(srcDir);
|
|
439
|
+
artifactPath = srcDir;
|
|
440
|
+
} catch {
|
|
441
|
+
artifactPath = targetDir;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
await fs.access(artifactPath);
|
|
447
|
+
} catch {
|
|
448
|
+
const msg = `Artifact path not found: ${path.relative(targetDir, artifactPath)}`;
|
|
449
|
+
if (options.json) return { ok: false, verdict: 'BLOCKED', error: msg };
|
|
450
|
+
logger.error(msg);
|
|
451
|
+
return { ok: false, verdict: 'BLOCKED' };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ── Parse spec requirements ───────────────────────────────────────────────
|
|
455
|
+
const requirements = parseSpecRequirements(specContent);
|
|
456
|
+
|
|
457
|
+
// ── Collect artifact files ────────────────────────────────────────────────
|
|
458
|
+
const allFiles = await collectFiles(artifactPath);
|
|
459
|
+
|
|
460
|
+
// ── Run checks ────────────────────────────────────────────────────────────
|
|
461
|
+
const [fileCheck, contentCheck] = await Promise.all([
|
|
462
|
+
checkRequiredFiles(requirements, artifactPath, allFiles, targetDir),
|
|
463
|
+
checkFileContents(requirements, allFiles, targetDir)
|
|
464
|
+
]);
|
|
465
|
+
const criteriaCheck = checkAcceptanceCriteria(requirements);
|
|
466
|
+
|
|
467
|
+
const allIssues = [...fileCheck.issues, ...contentCheck.issues, ...criteriaCheck.issues];
|
|
468
|
+
const allPasses = [...fileCheck.passes, ...contentCheck.passes, ...criteriaCheck.passes];
|
|
469
|
+
const allNotes = [...contentCheck.notes, ...criteriaCheck.notes];
|
|
470
|
+
|
|
471
|
+
const verdict = determineVerdict(allIssues, allNotes, strict);
|
|
472
|
+
|
|
473
|
+
// ── Build report ─────────────────────────────────────────────────────────
|
|
474
|
+
const report = buildReport({
|
|
475
|
+
verdict,
|
|
476
|
+
specPath,
|
|
477
|
+
artifactPath,
|
|
478
|
+
projectDir: targetDir,
|
|
479
|
+
issues: allIssues,
|
|
480
|
+
passes: allPasses,
|
|
481
|
+
notes: allNotes,
|
|
482
|
+
fileCount: allFiles.length,
|
|
483
|
+
requirements
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
// ── Write output ──────────────────────────────────────────────────────────
|
|
487
|
+
let outPath;
|
|
488
|
+
if (options.out) {
|
|
489
|
+
outPath = path.resolve(targetDir, options.out);
|
|
490
|
+
} else {
|
|
491
|
+
const slug = path.basename(specPath, '.md').replace(/[^a-z0-9-]/gi, '-').toLowerCase();
|
|
492
|
+
const outDir = path.join(targetDir, '.aioson', 'context');
|
|
493
|
+
outPath = path.join(outDir, `verify-gate-${slug}.md`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
497
|
+
await fs.writeFile(outPath, report, 'utf8');
|
|
498
|
+
|
|
499
|
+
const relOut = path.relative(targetDir, outPath);
|
|
500
|
+
|
|
501
|
+
if (options.json) {
|
|
502
|
+
return {
|
|
503
|
+
ok: verdict === 'PASS' || verdict === 'PASS_WITH_NOTES',
|
|
504
|
+
verdict,
|
|
505
|
+
spec: path.relative(targetDir, specPath),
|
|
506
|
+
artifact: path.relative(targetDir, artifactPath),
|
|
507
|
+
report_path: relOut,
|
|
508
|
+
files_scanned: allFiles.length,
|
|
509
|
+
issues: allIssues,
|
|
510
|
+
notes: allNotes,
|
|
511
|
+
passes: allPasses,
|
|
512
|
+
requirements: {
|
|
513
|
+
required_files: requirements.required_files.length,
|
|
514
|
+
acceptance_criteria: requirements.acceptance_criteria.length,
|
|
515
|
+
required_patterns: requirements.required_patterns.length,
|
|
516
|
+
forbidden_patterns: requirements.forbidden_patterns.length
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Console output
|
|
522
|
+
const VERDICT_ICON = {
|
|
523
|
+
PASS: '✓',
|
|
524
|
+
PASS_WITH_NOTES: '⚠',
|
|
525
|
+
FAIL_WITH_ISSUES: '✗',
|
|
526
|
+
BLOCKED: '?'
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
logger.log('Verify Gate');
|
|
530
|
+
logger.log('─'.repeat(60));
|
|
531
|
+
logger.log(`Spec : ${path.relative(targetDir, specPath)}`);
|
|
532
|
+
logger.log(`Artifact : ${path.relative(targetDir, artifactPath)}`);
|
|
533
|
+
logger.log(`Files : ${allFiles.length}`);
|
|
534
|
+
logger.log('');
|
|
535
|
+
|
|
536
|
+
logger.log(`Verdict : ${VERDICT_ICON[verdict]} ${verdict}`);
|
|
537
|
+
logger.log('');
|
|
538
|
+
|
|
539
|
+
if (allIssues.length > 0) {
|
|
540
|
+
logger.log('Issues:');
|
|
541
|
+
for (const issue of allIssues) logger.log(` ✗ ${issue}`);
|
|
542
|
+
logger.log('');
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (allNotes.length > 0) {
|
|
546
|
+
logger.log('Notes:');
|
|
547
|
+
for (const note of allNotes) logger.log(` ⚠ ${note}`);
|
|
548
|
+
logger.log('');
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if (allPasses.length > 0) {
|
|
552
|
+
logger.log(`Passed: ${allPasses.length} check${allPasses.length !== 1 ? 's' : ''}`);
|
|
553
|
+
logger.log('');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (requirements.required_files.length === 0 &&
|
|
557
|
+
requirements.acceptance_criteria.length === 0 &&
|
|
558
|
+
requirements.required_patterns.length === 0) {
|
|
559
|
+
logger.log('⚠ No verifiable requirements extracted from spec.');
|
|
560
|
+
logger.log(' Add "Done criteria", "Files to write", or "Must contain" sections to enable checks.');
|
|
561
|
+
logger.log('');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
logger.log(`Report : ${relOut}`);
|
|
565
|
+
|
|
566
|
+
return {
|
|
567
|
+
ok: verdict === 'PASS' || verdict === 'PASS_WITH_NOTES',
|
|
568
|
+
verdict
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
module.exports = { runVerifyGate };
|