@jaimevalasek/aioson 1.30.2 → 1.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -0
- package/README.md +19 -6
- package/docs/en/1-understand/ecosystem-map.md +45 -29
- package/docs/en/1-understand/glossary.md +5 -5
- package/docs/en/1-understand/what-is-aioson.md +5 -5
- package/docs/en/2-start/existing-project.md +7 -7
- package/docs/en/2-start/first-project.md +32 -38
- package/docs/en/2-start/initial-decisions.md +18 -17
- package/docs/en/3-recipes/README.md +2 -2
- package/docs/en/3-recipes/continuity-between-sessions.md +2 -2
- package/docs/en/3-recipes/from-idea-to-prd-via-briefing.md +5 -2
- package/docs/en/3-recipes/full-feature-with-sheldon.md +327 -338
- package/docs/en/4-agents/README.md +28 -14
- package/docs/en/4-agents/discovery-design-doc.md +20 -8
- package/docs/en/4-agents/forge-run.md +3 -2
- package/docs/en/5-reference/cli-reference.md +51 -48
- package/docs/en/5-reference/executable-verification.md +10 -7
- package/docs/en/5-reference/parallel.md +2 -0
- package/docs/en/5-reference/qa-browser.md +2 -2
- package/docs/en/README.md +3 -3
- package/docs/pt/1-entender/glossario.md +4 -4
- package/docs/pt/1-entender/mapa-do-ecossistema.md +32 -21
- package/docs/pt/2-comecar/decisoes-iniciais.md +16 -16
- package/docs/pt/2-comecar/primeiro-projeto.md +1 -1
- package/docs/pt/3-receitas/README.md +1 -1
- package/docs/pt/3-receitas/app-saas-do-zero.md +35 -122
- package/docs/pt/3-receitas/feature-completa-com-sheldon.md +289 -338
- package/docs/pt/4-agentes/analyst.md +9 -5
- package/docs/pt/4-agentes/architect.md +9 -5
- package/docs/pt/4-agentes/dev.md +19 -7
- package/docs/pt/4-agentes/discovery-design-doc.md +25 -23
- package/docs/pt/4-agentes/orchestrator.md +164 -118
- package/docs/pt/4-agentes/pentester.md +5 -5
- package/docs/pt/4-agentes/pm.md +15 -7
- package/docs/pt/4-agentes/sheldon.md +166 -99
- package/docs/pt/4-agentes/ux-ui.md +7 -4
- package/docs/pt/5-referencia/agent-chain-continuity.md +14 -5
- package/docs/pt/5-referencia/comandos-cli.md +1 -1
- package/docs/pt/5-referencia/harness-retro.md +2 -1
- package/docs/pt/5-referencia/sdd-automation-scripts.md +6 -6
- package/docs/pt/5-referencia/sdd-framework.md +53 -16
- package/docs/pt/5-referencia/sdd-planos-e-estrutura.md +27 -15
- package/docs/pt/README.md +4 -4
- package/docs/pt/agentes.md +48 -50
- package/package.json +2 -2
- package/src/artifact-kinds.js +110 -0
- package/src/cli.js +82 -42
- package/src/commands/agent-epilogue.js +251 -186
- package/src/commands/agents.js +104 -48
- package/src/commands/audit-code.js +344 -0
- package/src/commands/classify.js +75 -13
- package/src/commands/feature-close.js +43 -18
- package/src/commands/harness-check.js +259 -175
- package/src/commands/harness-retro-promote.js +387 -0
- package/src/commands/live.js +24 -0
- package/src/commands/prototype-check.js +163 -0
- package/src/commands/review-feature.js +189 -0
- package/src/commands/runtime.js +81 -66
- package/src/commands/sync-agents-copy.js +115 -0
- package/src/commands/verification-plan.js +116 -0
- package/src/commands/verify-artifact.js +530 -0
- package/src/commands/verify-implementation.js +428 -0
- package/src/commands/workflow-execute.js +309 -309
- package/src/commands/workflow-next.js +361 -19
- package/src/commands/workflow-plan.js +5 -5
- package/src/constants.js +4 -0
- package/src/gateway-pointer-merge.js +7 -1
- package/src/handoff-contract.js +267 -172
- package/src/harness/contract-integrity-gate.js +172 -0
- package/src/harness/contract-integrity.js +111 -0
- package/src/harness/contract-schema.js +377 -332
- package/src/harness/detect-runtime-feature.js +90 -0
- package/src/harness/static-criteria.js +192 -0
- package/src/i18n/messages/en.js +8 -4
- package/src/i18n/messages/es.js +8 -4
- package/src/i18n/messages/fr.js +8 -4
- package/src/i18n/messages/pt-BR.js +8 -4
- package/src/install-wizard.js +8 -8
- package/src/installer.js +13 -6
- package/src/lib/retro/retro-render.js +10 -1
- package/src/lib/retro/retro-sources.js +45 -27
- package/src/lib/retro/verification-reports.js +230 -0
- package/src/parser.js +6 -0
- package/src/preflight-engine.js +12 -12
- package/src/runtime-store.js +13 -9
- package/src/verification/evidence-bundle.js +251 -0
- package/src/verification/ledger-store.js +221 -0
- package/src/verification/path-policy.js +74 -0
- package/src/verification/policy-engine.js +95 -0
- package/src/verification/prompt-package.js +314 -0
- package/src/verification/redaction.js +77 -0
- package/src/verification/report-parser.js +132 -0
- package/src/verification/report-store.js +97 -0
- package/src/verification/result.js +16 -0
- package/src/verification/runners/index.js +319 -0
- package/src/verification/runtime-telemetry.js +144 -0
- package/src/verification/schema.js +276 -0
- package/src/verification/source-discovery.js +153 -0
- package/src/verification-policy.js +398 -0
- package/src/version.js +52 -1
- package/template/.aioson/agents/analyst.md +9 -6
- package/template/.aioson/agents/architect.md +34 -5
- package/template/.aioson/agents/briefing-refiner.md +25 -0
- package/template/.aioson/agents/briefing.md +69 -12
- package/template/.aioson/agents/committer.md +2 -1
- package/template/.aioson/agents/copywriter.md +30 -21
- package/template/.aioson/agents/design-hybrid-forge.md +28 -15
- package/template/.aioson/agents/dev.md +28 -10
- package/template/.aioson/agents/deyvin.md +3 -2
- package/template/.aioson/agents/discover.md +12 -3
- package/template/.aioson/agents/discovery-design-doc.md +7 -2
- package/template/.aioson/agents/genome.md +19 -10
- package/template/.aioson/agents/neo.md +30 -30
- package/template/.aioson/agents/orache.md +20 -11
- package/template/.aioson/agents/orchestrator.md +84 -7
- package/template/.aioson/agents/pm.md +8 -8
- package/template/.aioson/agents/product.md +15 -11
- package/template/.aioson/agents/profiler-enricher.md +20 -11
- package/template/.aioson/agents/profiler-forge.md +20 -11
- package/template/.aioson/agents/profiler-researcher.md +20 -11
- package/template/.aioson/agents/qa.md +87 -17
- package/template/.aioson/agents/scope-check.md +19 -5
- package/template/.aioson/agents/setup.md +12 -1
- package/template/.aioson/agents/sheldon.md +92 -9
- package/template/.aioson/agents/site-forge.md +11 -2
- package/template/.aioson/agents/squad.md +20 -5
- package/template/.aioson/agents/ux-ui.md +4 -2
- package/template/.aioson/agents/validator.md +33 -1
- package/template/.aioson/config/verification.json +61 -0
- package/template/.aioson/config.md +13 -9
- package/template/.aioson/docs/LAYERS.md +2 -2
- package/template/.aioson/docs/autopilot-handoff.md +10 -10
- package/template/.aioson/docs/dev/execution-discipline.md +41 -0
- package/template/.aioson/docs/dev/phase-loop.md +47 -0
- package/template/.aioson/docs/feature-expansion-taxonomy.md +31 -3
- package/template/.aioson/docs/presets/workflow.config.full-merged.json +15 -0
- package/template/.aioson/docs/presets/workflow.config.lean.json +15 -0
- package/template/.aioson/docs/product/prd-contract.md +12 -12
- package/template/.aioson/docs/prototype-contract.md +98 -0
- package/template/.aioson/docs/reference-identity.md +94 -0
- package/template/.aioson/docs/sheldon/harness-contract.md +155 -28
- package/template/.aioson/docs/verification-config.md +82 -0
- package/template/.aioson/docs/verify-artifact-gates.md +91 -0
- package/template/.aioson/docs/workflow-lean-lane.md +129 -0
- package/template/.aioson/skills/design/interface-design/references/intent-and-domain.md +2 -0
- package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +16 -14
- package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +2 -2
- package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +4 -4
- package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +15 -15
- package/template/.aioson/skills/process/briefing-expansion-scout/SKILL.md +25 -3
- package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +5 -3
- package/template/.aioson/skills/process/design-hybrid-forge/references/external-source-ingestion.md +89 -0
- package/template/.aioson/skills/process/product-scope-expansion/SKILL.md +29 -2
- package/template/.aioson/skills/process/prototype-forge/SKILL.md +98 -0
- package/template/.aioson/skills/process/reference-identity-extract/SKILL.md +164 -0
- package/template/.aioson/skills/process/sheldon-expansion-audit/SKILL.md +23 -2
- package/template/.aioson/skills/static/multi-agent-patterns.md +5 -5
- package/template/CLAUDE.md +15 -11
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* aioson verify:artifact --kind=<kind> [--slug=<slug>] [path] [--advisory]
|
|
5
|
+
* [--strict] [--json] — the build-free "done = proven, not asserted" gate for
|
|
6
|
+
* the NON-code artifacts the peripheral agents produce.
|
|
7
|
+
*
|
|
8
|
+
* Where an SG-* criterion gates a code feature's harness contract, this gates a
|
|
9
|
+
* produced artifact (project context, a genome, a research report, the bootstrap
|
|
10
|
+
* cache, a hybrid skill, ...) the same cheap way: read the declared files and
|
|
11
|
+
* prove a required structure is present and no placeholder / truncation slipped
|
|
12
|
+
* in before the agent self-declares done. Pure fs + RegExp + JSON.parse — no
|
|
13
|
+
* shell, no build — so it costs ~milliseconds and runs at every agent's done
|
|
14
|
+
* gate, cross-platform by construction.
|
|
15
|
+
*
|
|
16
|
+
* Each `kind` resolves to EITHER an existing validator (project context →
|
|
17
|
+
* validateProjectContextFile; genome → genome:doctor) OR a declarative ruleset
|
|
18
|
+
* evaluated by the shared static-criteria engine. No new analysis logic per
|
|
19
|
+
* artifact — just data. Adding a kind is a registry entry, not a code path.
|
|
20
|
+
*
|
|
21
|
+
* Exit code: a hard failure sets exit 1 (blocking) unless --advisory (warn-only,
|
|
22
|
+
* always exit 0). Pass suppressExitCode for programmatic callers (an agent
|
|
23
|
+
* done-gate that wants to interpret the verdict itself). --strict promotes
|
|
24
|
+
* warnings to blocking issues.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const path = require('node:path');
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
const { spawnSync } = require('node:child_process');
|
|
30
|
+
|
|
31
|
+
const { evaluateStaticCriterion } = require('../harness/static-criteria');
|
|
32
|
+
|
|
33
|
+
const VERSION = '1.0.0';
|
|
34
|
+
const GENERATOR = `aioson verify:artifact@${VERSION}`;
|
|
35
|
+
|
|
36
|
+
// ─── ruleset engine ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Run a list of static criteria (the same {id, files, must_match, must_not_match}
|
|
40
|
+
* shape the harness SG-* gate uses) against the working tree. Returns a uniform
|
|
41
|
+
* { ok, issues, warnings, checks }.
|
|
42
|
+
*
|
|
43
|
+
* @param {Array} criteria
|
|
44
|
+
* @param {string} targetDir
|
|
45
|
+
*/
|
|
46
|
+
function evaluateRuleset(criteria, targetDir) {
|
|
47
|
+
const checks = [];
|
|
48
|
+
const issues = [];
|
|
49
|
+
for (const criterion of Array.isArray(criteria) ? criteria : []) {
|
|
50
|
+
const res = evaluateStaticCriterion(criterion, targetDir);
|
|
51
|
+
checks.push(res);
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
issues.push(`[${res.id}] ${res.detail || 'failed'}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { ok: issues.length === 0, issues, warnings: [], checks };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── ruleset registry (declarative, data-only) ──────────────────────────────
|
|
60
|
+
//
|
|
61
|
+
// Each entry is (ctx) => { label, criteria[] }. ctx = { slug, targetDir }.
|
|
62
|
+
// Frontmatter/placeholder checks reuse the OR-across-files / absent-in-all
|
|
63
|
+
// semantics of the static engine; per-file structure is expressed as one
|
|
64
|
+
// criterion per file.
|
|
65
|
+
|
|
66
|
+
const PLACEHOLDER_PATTERNS = ['\\bTODO\\b', '\\bFIXME\\b', '\\bTBD\\b', 'Lorem ipsum', '\\[\\.\\.\\.\\]', 'XXXX'];
|
|
67
|
+
|
|
68
|
+
const BOOTSTRAP_DIR = '.aioson/context/bootstrap';
|
|
69
|
+
const BOOTSTRAP_FILES = ['what-is.md', 'what-it-does.md', 'how-it-works.md', 'current-state.md'];
|
|
70
|
+
const PROFILER_DIR = '.aioson/profiler-reports';
|
|
71
|
+
|
|
72
|
+
// Unfilled template tokens left in a produced artifact (a tell that the agent
|
|
73
|
+
// emitted the skeleton without filling it). Targeted, not a blanket `[...]`
|
|
74
|
+
// match, to avoid flagging legitimate `[1]`-style citations.
|
|
75
|
+
const TEMPLATE_TOKENS = ['\\[Full Name\\]', '\\[count\\]', '\\[low/medium/high'];
|
|
76
|
+
|
|
77
|
+
const RULESETS = {
|
|
78
|
+
// discover — the 4-file cold-start cache must all exist with real frontmatter.
|
|
79
|
+
bootstrap: () => ({
|
|
80
|
+
label: 'discovery bootstrap cache',
|
|
81
|
+
criteria: BOOTSTRAP_FILES.map((f) => ({
|
|
82
|
+
id: `bootstrap:${f}`,
|
|
83
|
+
files: [`${BOOTSTRAP_DIR}/${f}`],
|
|
84
|
+
must_match: ['generated_by', 'confidence'],
|
|
85
|
+
must_not_match: PLACEHOLDER_PATTERNS
|
|
86
|
+
}))
|
|
87
|
+
}),
|
|
88
|
+
|
|
89
|
+
// profiler-researcher — the research report must carry its frontmatter and the
|
|
90
|
+
// load-bearing skeleton (inventory + extracted material + gaps), with no
|
|
91
|
+
// unfilled template token.
|
|
92
|
+
'research-report': (ctx) => ({
|
|
93
|
+
label: 'profiler research report',
|
|
94
|
+
criteria: [{
|
|
95
|
+
id: 'research-report',
|
|
96
|
+
files: [`${PROFILER_DIR}/${ctx.slug}/research-report.md`],
|
|
97
|
+
must_match: ['sources_found', '## Source Inventory', '## Extracted Material by Category', '## Gaps and Next Research Moves'],
|
|
98
|
+
must_not_match: [...PLACEHOLDER_PATTERNS, ...TEMPLATE_TOKENS]
|
|
99
|
+
}]
|
|
100
|
+
}),
|
|
101
|
+
|
|
102
|
+
// profiler-enricher — the enriched profile must carry the executive summary,
|
|
103
|
+
// psychometric profile, the operational method (the part that makes a genome
|
|
104
|
+
// *work* rather than simulate opinions), and the trait-interaction analysis.
|
|
105
|
+
'enriched-profile': (ctx) => ({
|
|
106
|
+
label: 'profiler enriched profile',
|
|
107
|
+
criteria: [{
|
|
108
|
+
id: 'enriched-profile',
|
|
109
|
+
files: [`${PROFILER_DIR}/${ctx.slug}/enriched-profile.md`],
|
|
110
|
+
must_match: ['## Executive Summary', '## Psychometric Profile', '## Operational Method', '## Trait Interactions'],
|
|
111
|
+
must_not_match: [...PLACEHOLDER_PATTERNS, ...TEMPLATE_TOKENS]
|
|
112
|
+
}]
|
|
113
|
+
}),
|
|
114
|
+
|
|
115
|
+
// orache — the investigation report is date-stamped, so it's resolved via
|
|
116
|
+
// --file. It must carry the 7-dimension skeleton, an impact analysis, and
|
|
117
|
+
// source attribution, with no unfilled template token.
|
|
118
|
+
'orache-report': (ctx) => ({
|
|
119
|
+
label: 'orache investigation report',
|
|
120
|
+
criteria: [{
|
|
121
|
+
id: 'orache-report',
|
|
122
|
+
files: [ctx.file || `squad-searches/${ctx.slug || 'MISSING'}/investigation.md`],
|
|
123
|
+
must_match: ['## D1', '## D4', '## D7', '## Impact Analysis', '\\*\\*Source:\\*\\*'],
|
|
124
|
+
must_not_match: [...PLACEHOLDER_PATTERNS, '\\{where discovered\\}']
|
|
125
|
+
}]
|
|
126
|
+
}),
|
|
127
|
+
|
|
128
|
+
// design-hybrid-forge — the hybrid skill package: a parseable .skill-meta.json
|
|
129
|
+
// recording its sources, a real SKILL.md, and both required previews.
|
|
130
|
+
'hybrid-skill': (ctx) => ({
|
|
131
|
+
label: 'hybrid design skill package',
|
|
132
|
+
criteria: [
|
|
133
|
+
{ id: 'hybrid:meta', files: [`.aioson/installed-skills/${ctx.slug}/.skill-meta.json`], must_match: ['sources'], must_not_match: [] },
|
|
134
|
+
{ id: 'hybrid:skill', files: [`.aioson/installed-skills/${ctx.slug}/SKILL.md`], must_match: [], must_not_match: PLACEHOLDER_PATTERNS },
|
|
135
|
+
{
|
|
136
|
+
id: 'hybrid:previews',
|
|
137
|
+
files: [
|
|
138
|
+
`.aioson/installed-skills/${ctx.slug}/previews/${ctx.slug}.html`,
|
|
139
|
+
`.aioson/installed-skills/${ctx.slug}/previews/${ctx.slug}-website.html`
|
|
140
|
+
],
|
|
141
|
+
must_match: [],
|
|
142
|
+
must_not_match: []
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
// copywriter — an advisory placeholder/template scan over the saved copy doc
|
|
148
|
+
// (the rich resonance checks stay in the agent's Phase-5 checklist; this just
|
|
149
|
+
// makes "no placeholder/Lorem/TODO/unfilled token" deterministic).
|
|
150
|
+
copy: (ctx) => ({
|
|
151
|
+
label: 'copywriter copy document',
|
|
152
|
+
criteria: [{
|
|
153
|
+
id: 'copy',
|
|
154
|
+
files: [`.aioson/context/copy-${ctx.slug}.md`],
|
|
155
|
+
must_match: [],
|
|
156
|
+
must_not_match: [...PLACEHOLDER_PATTERNS, ...TEMPLATE_TOKENS]
|
|
157
|
+
}]
|
|
158
|
+
}),
|
|
159
|
+
|
|
160
|
+
// reference-identity-extract — identity.md is the extracted token + per-component
|
|
161
|
+
// structure system-of-record. Its path varies by scope (briefing vs brand) so it is
|
|
162
|
+
// resolved via --file. It must carry the token skeleton plus the two anti-sameness
|
|
163
|
+
// anchors (pillars + signature moves) and the component-structure section, with no
|
|
164
|
+
// placeholder or unfilled hex/token left behind.
|
|
165
|
+
identity: (ctx) => ({
|
|
166
|
+
label: 'reference identity system',
|
|
167
|
+
criteria: [{
|
|
168
|
+
id: 'identity',
|
|
169
|
+
files: [ctx.file || `.aioson/briefings/${ctx.slug || 'MISSING'}/identity.md`],
|
|
170
|
+
must_match: [
|
|
171
|
+
'generated_by',
|
|
172
|
+
'## Design pillars', '## Palette', '## Typography', '## Spacing',
|
|
173
|
+
'## Radius', '## Motion', '## Signature moves', '## Component structure notes'
|
|
174
|
+
],
|
|
175
|
+
must_not_match: [...PLACEHOLDER_PATTERNS, '#RRGGBB', '#XXXXXX', '\\{hex\\}', '\\{token\\}']
|
|
176
|
+
}]
|
|
177
|
+
})
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Kinds whose target file path is keyed by --slug; without it we cannot resolve
|
|
181
|
+
// the artifact, so fail with a clear usage error instead of a `null/` path.
|
|
182
|
+
const REQUIRES_SLUG = new Set(['genome', 'research-report', 'enriched-profile', 'hybrid-skill', 'copy']);
|
|
183
|
+
|
|
184
|
+
// Kinds whose artifact has a date-stamped / caller-known path — resolved via
|
|
185
|
+
// --file=<path> rather than derived from a slug.
|
|
186
|
+
const REQUIRES_FILE = new Set(['orache-report', 'identity']);
|
|
187
|
+
|
|
188
|
+
// ─── adapters to existing validators ────────────────────────────────────────
|
|
189
|
+
//
|
|
190
|
+
// Each adapter is async (ctx, logger) => { ok, issues, warnings, checks }.
|
|
191
|
+
|
|
192
|
+
function quietLogger() {
|
|
193
|
+
return { log() {}, error() {}, warn() {} };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── kind=site (runtime build gate for a generated Next.js site) ──────────────
|
|
197
|
+
|
|
198
|
+
const SITE_IGNORE = new Set(['node_modules', '.next', '.git', 'dist', 'build', 'out', 'coverage', '.turbo', '.vercel', '.aioson']);
|
|
199
|
+
const SITE_SCAN_EXTS = new Set(['.tsx', '.jsx', '.ts', '.js', '.mjs', '.html', '.vue', '.svelte']);
|
|
200
|
+
const SITE_ENTRY_CANDIDATES = ['app/page.tsx', 'app/page.jsx', 'src/app/page.tsx', 'src/app/page.jsx', 'pages/index.tsx', 'pages/index.jsx', 'pages/index.js'];
|
|
201
|
+
const SITE_LEAKS = [
|
|
202
|
+
{ re: /\balert\s*\(/, msg: 'native alert() dialog — use in-app UI chrome' },
|
|
203
|
+
{ re: /\bconfirm\s*\(/, msg: 'native confirm() dialog — use in-app UI chrome' },
|
|
204
|
+
{ re: /\bwindow\.prompt\s*\(/, msg: 'native window.prompt() dialog — use in-app UI chrome' },
|
|
205
|
+
{ re: /Lorem ipsum/i, msg: 'Lorem ipsum placeholder copy' },
|
|
206
|
+
{ re: /\bTODO\b|\bFIXME\b/, msg: 'TODO/FIXME left in shipped code' }
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
function walkSiteFiles(root, max = 800) {
|
|
210
|
+
const out = [];
|
|
211
|
+
const stack = [root];
|
|
212
|
+
while (stack.length && out.length < max) {
|
|
213
|
+
const dir = stack.pop();
|
|
214
|
+
let entries;
|
|
215
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { continue; }
|
|
216
|
+
for (const e of entries) {
|
|
217
|
+
const full = path.join(dir, e.name);
|
|
218
|
+
if (e.isDirectory()) {
|
|
219
|
+
if (!SITE_IGNORE.has(e.name)) stack.push(full);
|
|
220
|
+
} else if (e.isFile() && SITE_SCAN_EXTS.has(path.extname(e.name).toLowerCase())) {
|
|
221
|
+
out.push(full);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Scan a generated site for native-dialog and placeholder leaks (build-free). */
|
|
229
|
+
function scanSiteForLeaks(siteDir, maxHits = 20) {
|
|
230
|
+
const hits = [];
|
|
231
|
+
for (const abs of walkSiteFiles(siteDir)) {
|
|
232
|
+
let content;
|
|
233
|
+
try { content = fs.readFileSync(abs, 'utf8'); } catch { continue; }
|
|
234
|
+
const rel = path.relative(siteDir, abs).split(path.sep).join('/');
|
|
235
|
+
const lines = content.split('\n');
|
|
236
|
+
for (let i = 0; i < lines.length; i++) {
|
|
237
|
+
const stripped = lines[i].trim();
|
|
238
|
+
if (stripped.startsWith('//') || stripped.startsWith('*') || stripped.startsWith('/*')) continue;
|
|
239
|
+
for (const leak of SITE_LEAKS) {
|
|
240
|
+
if (leak.re.test(lines[i])) { hits.push(`${rel}:${i + 1} — ${leak.msg}`); break; }
|
|
241
|
+
}
|
|
242
|
+
if (hits.length >= maxHits) return hits;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return hits;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Build-free static floor for a site: build script, entry route, no leaks. */
|
|
249
|
+
function staticSiteChecks(siteDir) {
|
|
250
|
+
const issues = [];
|
|
251
|
+
const warnings = [];
|
|
252
|
+
|
|
253
|
+
let pkg = null;
|
|
254
|
+
try {
|
|
255
|
+
pkg = JSON.parse(fs.readFileSync(path.join(siteDir, 'package.json'), 'utf8'));
|
|
256
|
+
} catch {
|
|
257
|
+
issues.push('no readable package.json at the site root');
|
|
258
|
+
}
|
|
259
|
+
if (pkg && !(pkg.scripts && pkg.scripts.build)) issues.push('package.json has no "build" script');
|
|
260
|
+
|
|
261
|
+
if (!SITE_ENTRY_CANDIDATES.some((rel) => fs.existsSync(path.join(siteDir, rel)))) {
|
|
262
|
+
issues.push('no entry route found (app/page.* or pages/index.*)');
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
for (const leak of scanSiteForLeaks(siteDir)) issues.push(leak);
|
|
266
|
+
return { issues, warnings };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* The RG-* runtime floor: the site must actually build on the real toolchain.
|
|
271
|
+
* `command` defaults to `npm run build`; it is overridable so a caller (or a
|
|
272
|
+
* test) can drive the same spawn/exit logic without the npm layer.
|
|
273
|
+
*/
|
|
274
|
+
function runSiteBuild(siteDir, timeout = 600000, command = ['npm', 'run', 'build']) {
|
|
275
|
+
const [cmd, ...rest] = command;
|
|
276
|
+
const res = spawnSync(cmd, rest, { cwd: siteDir, encoding: 'utf8', shell: true, timeout });
|
|
277
|
+
if (res.error) return { ok: false, detail: `build could not start: ${res.error.message}` };
|
|
278
|
+
if (res.status === 0) return { ok: true, detail: null };
|
|
279
|
+
const tail = String(res.stderr || res.stdout || '')
|
|
280
|
+
.split('\n').map((l) => l.trim()).filter(Boolean).slice(-4).join(' | ');
|
|
281
|
+
return { ok: false, detail: `build failed (exit ${res.status}): ${tail || 'see build output'}` };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── kind=commit-message (advisory subject-quality heuristics) ────────────────
|
|
285
|
+
|
|
286
|
+
/** Conservative, low-false-positive commit-subject checks. Returns issue list. */
|
|
287
|
+
function evaluateCommitMessage(message) {
|
|
288
|
+
const issues = [];
|
|
289
|
+
const subject = (String(message || '').replace(/^\s+/, '').split('\n')[0] || '').trim();
|
|
290
|
+
if (!subject) {
|
|
291
|
+
issues.push('empty commit subject');
|
|
292
|
+
return issues;
|
|
293
|
+
}
|
|
294
|
+
if (subject.length > 72) issues.push(`subject is ${subject.length} chars — keep it <= 72 (ideally <= 50)`);
|
|
295
|
+
if (/[.]$/.test(subject)) issues.push('subject ends with a period — drop it');
|
|
296
|
+
if (/^(wip|stuff|misc|various|things|update|updates|changes|tweaks?|minor)$/i.test(subject)) {
|
|
297
|
+
issues.push(`subject is a single vague word: "${subject}"`);
|
|
298
|
+
}
|
|
299
|
+
if (/^(fix|update|change|tweak|adjust)\s+(it|this|that|stuff|things?|bugs?|code|tests?)$/i.test(subject)) {
|
|
300
|
+
issues.push(`subject is vague: "${subject}" — say what changed and why`);
|
|
301
|
+
}
|
|
302
|
+
return issues;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const ADAPTERS = {
|
|
306
|
+
// setup — project.context.md is the root artifact every session reads first.
|
|
307
|
+
'project-context': async (ctx) => {
|
|
308
|
+
const { validateProjectContextFile } = require('../context');
|
|
309
|
+
let res;
|
|
310
|
+
try {
|
|
311
|
+
res = await validateProjectContextFile(ctx.targetDir);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
return { ok: false, issues: [`project.context.md could not be validated: ${err.message}`], warnings: [], checks: [] };
|
|
314
|
+
}
|
|
315
|
+
const issues = [];
|
|
316
|
+
if (!res.exists) {
|
|
317
|
+
issues.push(`project.context.md not found (${res.filePath || '.aioson/context/project.context.md'}) — run /setup`);
|
|
318
|
+
} else if (!res.parsed) {
|
|
319
|
+
issues.push(`project.context.md frontmatter does not parse (${res.parseError || 'invalid YAML'})`);
|
|
320
|
+
} else if (!res.valid) {
|
|
321
|
+
for (const issue of res.issues || []) {
|
|
322
|
+
issues.push(typeof issue === 'string' ? issue : (issue.key || JSON.stringify(issue)));
|
|
323
|
+
}
|
|
324
|
+
if (issues.length === 0) issues.push('project.context.md has invalid or missing required fields');
|
|
325
|
+
}
|
|
326
|
+
return { ok: issues.length === 0, issues, warnings: [], checks: [{ id: 'project-context', ok: issues.length === 0, detail: issues[0] || null }] };
|
|
327
|
+
},
|
|
328
|
+
|
|
329
|
+
// profiler-forge / genome — reuse the comprehensive genome doctor.
|
|
330
|
+
genome: async (ctx, logger) => {
|
|
331
|
+
const { runGenomeDoctor, isGenomeAvailable } = require('./genome-doctor');
|
|
332
|
+
if (!ctx.slug) {
|
|
333
|
+
return { ok: false, issues: ['kind=genome requires --slug=<genome-slug>'], warnings: [], checks: [] };
|
|
334
|
+
}
|
|
335
|
+
const avail = await isGenomeAvailable(ctx.targetDir, ctx.slug);
|
|
336
|
+
if (!avail.found) {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
issues: [`genome "${ctx.slug}" not found under .aioson/genomes/ (neither <slug>.md nor <slug>/SKILL.md)`],
|
|
340
|
+
warnings: [],
|
|
341
|
+
checks: []
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const target = path.resolve(ctx.targetDir, avail.path);
|
|
345
|
+
let res;
|
|
346
|
+
try {
|
|
347
|
+
res = await runGenomeDoctor({ args: [target], options: { json: true }, logger: logger || quietLogger() });
|
|
348
|
+
} catch (err) {
|
|
349
|
+
return { ok: false, issues: [`genome:doctor failed: ${err.message}`], warnings: [], checks: [] };
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
ok: Boolean(res.ok),
|
|
353
|
+
issues: res.ok ? [] : (res.issues || []).slice(),
|
|
354
|
+
warnings: (res.warnings || []).slice(),
|
|
355
|
+
checks: [{ id: `genome:${ctx.slug}`, ok: Boolean(res.ok), detail: res.ok ? null : (res.issues || []).join('; ') }]
|
|
356
|
+
};
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
// site-forge — a generated Next.js site is not done until it BUILDS on the
|
|
360
|
+
// real toolchain (the RG-* runtime floor), on top of a static floor: a build
|
|
361
|
+
// script, an entry route, and no native-dialog / placeholder leak. --no-build
|
|
362
|
+
// runs the static floor only; --dir points at the site root (default: cwd).
|
|
363
|
+
site: async (ctx) => {
|
|
364
|
+
const siteDir = ctx.dir ? path.resolve(ctx.targetDir, ctx.dir) : ctx.targetDir;
|
|
365
|
+
const { issues, warnings } = staticSiteChecks(siteDir);
|
|
366
|
+
if (ctx.noBuild) {
|
|
367
|
+
warnings.push('npm run build skipped (--no-build): static checks only — not a full runtime gate');
|
|
368
|
+
} else if (issues.length === 0) {
|
|
369
|
+
const b = runSiteBuild(siteDir, ctx.buildTimeout, ctx.buildCommand);
|
|
370
|
+
if (!b.ok) issues.push(b.detail);
|
|
371
|
+
} else {
|
|
372
|
+
warnings.push('npm run build skipped: static checks already failed');
|
|
373
|
+
}
|
|
374
|
+
return { ok: issues.length === 0, issues, warnings, checks: [{ id: 'site', ok: issues.length === 0, detail: issues.join('; ') || null }] };
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
// committer — advisory subject-quality audit. Reads --file if given, else the
|
|
378
|
+
// HEAD commit message (post-commit, so the agent can amend before push).
|
|
379
|
+
'commit-message': async (ctx) => {
|
|
380
|
+
let message = null;
|
|
381
|
+
if (ctx.file) {
|
|
382
|
+
try {
|
|
383
|
+
message = fs.readFileSync(path.resolve(ctx.targetDir, ctx.file), 'utf8');
|
|
384
|
+
} catch {
|
|
385
|
+
return { ok: false, issues: [`cannot read commit message file: ${ctx.file}`], warnings: [], checks: [] };
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
const res = spawnSync('git', ['log', '-1', '--pretty=%B'], { cwd: ctx.targetDir, encoding: 'utf8' });
|
|
389
|
+
if (res.status !== 0) {
|
|
390
|
+
return { ok: false, issues: ['could not read HEAD commit message (no git repo or no commits)'], warnings: [], checks: [] };
|
|
391
|
+
}
|
|
392
|
+
message = res.stdout;
|
|
393
|
+
}
|
|
394
|
+
const found = evaluateCommitMessage(message);
|
|
395
|
+
return { ok: found.length === 0, issues: found, warnings: [], checks: [{ id: 'commit-message', ok: found.length === 0, detail: found.join('; ') || null }] };
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// ─── kind resolution ────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
function availableKinds() {
|
|
402
|
+
return [...Object.keys(ADAPTERS), ...Object.keys(RULESETS)].sort();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function evaluateKind(kind, ctx, logger) {
|
|
406
|
+
if (ADAPTERS[kind]) {
|
|
407
|
+
return ADAPTERS[kind](ctx, logger);
|
|
408
|
+
}
|
|
409
|
+
if (RULESETS[kind]) {
|
|
410
|
+
const { criteria } = RULESETS[kind](ctx);
|
|
411
|
+
return evaluateRuleset(criteria, ctx.targetDir);
|
|
412
|
+
}
|
|
413
|
+
return null; // unknown
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ─── main command ─────────────────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
async function runVerifyArtifact({ args, options = {}, logger }) {
|
|
419
|
+
const targetDir = path.resolve(process.cwd(), args?.[0] || '.');
|
|
420
|
+
const kind = options.kind ? String(options.kind).trim() : '';
|
|
421
|
+
const slug = options.slug ? String(options.slug).trim() : null;
|
|
422
|
+
const file = options.file ? String(options.file).trim() : null;
|
|
423
|
+
const dir = options.dir ? String(options.dir).trim() : null;
|
|
424
|
+
const noBuild = Boolean(options['no-build'] || options.noBuild);
|
|
425
|
+
const buildTimeout = options['build-timeout'] ? Number(options['build-timeout']) : undefined;
|
|
426
|
+
const buildCommand = Array.isArray(options.buildCommand) ? options.buildCommand : undefined;
|
|
427
|
+
const advisory = Boolean(options.advisory);
|
|
428
|
+
const strict = Boolean(options.strict);
|
|
429
|
+
const suppressExitCode = Boolean(options.suppressExitCode);
|
|
430
|
+
const setExitCode = (code) => { if (!suppressExitCode) process.exitCode = code; };
|
|
431
|
+
|
|
432
|
+
if (!kind) {
|
|
433
|
+
const msg = `verify:artifact requires --kind=<kind>. Available: ${availableKinds().join(', ')}`;
|
|
434
|
+
if (options.json) { setExitCode(1); return { ok: false, kind: null, error: 'missing_kind', available: availableKinds() }; }
|
|
435
|
+
logger.error(msg);
|
|
436
|
+
setExitCode(1);
|
|
437
|
+
return { ok: false, kind: null };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (REQUIRES_SLUG.has(kind) && !slug) {
|
|
441
|
+
const msg = `verify:artifact kind=${kind} requires --slug=<slug>`;
|
|
442
|
+
const blocking = !advisory;
|
|
443
|
+
if (options.json) {
|
|
444
|
+
setExitCode(blocking ? 1 : 0);
|
|
445
|
+
return { generator: GENERATOR, kind, slug: null, root: targetDir, mode: advisory ? 'advisory' : 'blocking', ok: false, blocking, issues: [msg], warnings: [], checks: [], error: 'missing_slug' };
|
|
446
|
+
}
|
|
447
|
+
logger.error(msg);
|
|
448
|
+
setExitCode(blocking ? 1 : 0);
|
|
449
|
+
return { ok: false, kind };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
if (REQUIRES_FILE.has(kind) && !file) {
|
|
453
|
+
const msg = `verify:artifact kind=${kind} requires --file=<path>`;
|
|
454
|
+
const blocking = !advisory;
|
|
455
|
+
if (options.json) {
|
|
456
|
+
setExitCode(blocking ? 1 : 0);
|
|
457
|
+
return { generator: GENERATOR, kind, slug, root: targetDir, mode: advisory ? 'advisory' : 'blocking', ok: false, blocking, issues: [msg], warnings: [], checks: [], error: 'missing_file' };
|
|
458
|
+
}
|
|
459
|
+
logger.error(msg);
|
|
460
|
+
setExitCode(blocking ? 1 : 0);
|
|
461
|
+
return { ok: false, kind };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const result = await evaluateKind(kind, { slug, targetDir, file, dir, noBuild, buildTimeout, buildCommand }, logger);
|
|
465
|
+
|
|
466
|
+
if (result === null) {
|
|
467
|
+
const msg = `verify:artifact: unknown kind "${kind}". Available: ${availableKinds().join(', ')}`;
|
|
468
|
+
if (options.json) { setExitCode(1); return { ok: false, kind, error: 'unknown_kind', available: availableKinds() }; }
|
|
469
|
+
logger.error(msg);
|
|
470
|
+
setExitCode(1);
|
|
471
|
+
return { ok: false, kind };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// strict promotes warnings to blocking issues.
|
|
475
|
+
const issues = strict ? [...result.issues, ...result.warnings] : [...result.issues];
|
|
476
|
+
const warnings = strict ? [] : [...result.warnings];
|
|
477
|
+
const ok = issues.length === 0;
|
|
478
|
+
const blocking = !ok && !advisory;
|
|
479
|
+
|
|
480
|
+
const report = {
|
|
481
|
+
generator: GENERATOR,
|
|
482
|
+
kind,
|
|
483
|
+
slug,
|
|
484
|
+
root: targetDir,
|
|
485
|
+
mode: advisory ? 'advisory' : 'blocking',
|
|
486
|
+
ok,
|
|
487
|
+
blocking,
|
|
488
|
+
issues,
|
|
489
|
+
warnings,
|
|
490
|
+
checks: result.checks || []
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// Persist for downstream consumption (mirrors audit:code).
|
|
494
|
+
try {
|
|
495
|
+
const ctxDir = path.join(targetDir, '.aioson', 'context');
|
|
496
|
+
fs.mkdirSync(ctxDir, { recursive: true });
|
|
497
|
+
fs.writeFileSync(path.join(ctxDir, `verify-artifact-${kind}.json`), JSON.stringify(report, null, 2), 'utf8');
|
|
498
|
+
} catch {
|
|
499
|
+
// best-effort persistence — never fail the gate on a write error
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (options.json) {
|
|
503
|
+
logger.log(JSON.stringify(report, null, 2));
|
|
504
|
+
setExitCode(blocking ? 1 : 0);
|
|
505
|
+
return report;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const verdict = ok ? 'OK' : (advisory ? 'ADVISORY' : 'FAIL');
|
|
509
|
+
logger.log(`verify:artifact — kind=${kind}${slug ? ` slug=${slug}` : ''} — ${verdict}`);
|
|
510
|
+
for (const issue of issues) logger.log(` ✗ ${issue}`);
|
|
511
|
+
for (const w of warnings) logger.log(` ⚠ ${w}`);
|
|
512
|
+
if (ok && warnings.length === 0) logger.log(' (no issues)');
|
|
513
|
+
|
|
514
|
+
setExitCode(blocking ? 1 : 0);
|
|
515
|
+
return report;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
module.exports = {
|
|
519
|
+
runVerifyArtifact,
|
|
520
|
+
// exported for reuse / tests
|
|
521
|
+
evaluateRuleset,
|
|
522
|
+
availableKinds,
|
|
523
|
+
RULESETS,
|
|
524
|
+
ADAPTERS,
|
|
525
|
+
PLACEHOLDER_PATTERNS,
|
|
526
|
+
staticSiteChecks,
|
|
527
|
+
scanSiteForLeaks,
|
|
528
|
+
runSiteBuild,
|
|
529
|
+
evaluateCommitMessage
|
|
530
|
+
};
|