@williambeto/ai-workflow 2.4.8 → 2.6.3
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/bin/ai-workflow.d.ts +3 -0
- package/bin/ai-workflow.js +2669 -1
- package/bin/ai-workflow.js.map +1 -0
- package/chunk-2AOV2ATY.js +749 -0
- package/chunk-5WRI5ZAA.js +31 -0
- package/chunk-6EMG6FAO.js +734 -0
- package/chunk-UHLZX2EG.js +1827 -0
- package/core/index.d.ts +440 -0
- package/core/index.js +32 -0
- package/core/index.js.map +1 -0
- package/dist-assets/AGENTS.md +23 -6
- package/dist-assets/agents/atlas.md +8 -8
- package/dist-assets/agents/nexus.md +4 -4
- package/dist-assets/agents/orion.md +1 -1
- package/dist-assets/agents/phoenix.md +1 -1
- package/dist-assets/agents/sage.md +1 -1
- package/dist-assets/commands/implement.md +1 -1
- package/dist-assets/docs/QUICKSTART.md +8 -8
- package/dist-assets/docs/cli-reference.md +7 -7
- package/dist-assets/docs/compatibility/provider-usage.md +1 -9
- package/dist-assets/docs/compatibility/runtime-matrix.md +5 -6
- package/dist-assets/docs/consumer-onboarding.md +2 -2
- package/dist-assets/docs/design-patterns-policy.md +1 -1
- package/dist-assets/docs/full-documentation.md +0 -2
- package/dist-assets/docs/npm-consumer-quickstart.md +70 -109
- package/dist-assets/docs/policies/01-BRANCH_GATE.md +1 -1
- package/dist-assets/docs/policies/06-FINAL_EVIDENCE_CONTRACT.md +2 -2
- package/dist-assets/docs/policies/08-PRODUCT_TRUTHFULNESS_AND_PROJECT_DOCS.md +1 -1
- package/dist-assets/docs/policies/10-BEHAVIORAL_CONTRACT_HARDENING.md +1 -1
- package/dist-assets/docs/policies/ORCHESTRATION_PROTOCOL.md +8 -9
- package/dist-assets/docs/policies/PROCEDURE_DELIVERY_ARTIFACTS.md +4 -10
- package/dist-assets/docs/policies/SKILLS_COMMON_GOVERNANCE.md +1 -1
- package/dist-assets/docs/profiles/README.md +2 -2
- package/dist-assets/docs/specs/runtime-operational-contract.md +12 -12
- package/dist-assets/docs/troubleshooting-guide.md +2 -2
- package/dist-assets/docs/visual-validation-guide.md +65 -3
- package/dist-assets/prompts/01-create-requirement.md +2 -2
- package/dist-assets/prompts/02-create-spec.md +2 -2
- package/dist-assets/prompts/03-create-tech-plan.md +4 -4
- package/dist-assets/prompts/04-breakdown-prs.md +4 -4
- package/dist-assets/prompts/05-implement-pr.md +5 -5
- package/dist-assets/prompts/06-review-and-fix.md +3 -3
- package/dist-assets/prompts/07-apply-design.md +2 -2
- package/dist-assets/prompts/08-validate.md +5 -5
- package/dist-assets/prompts/09-deploy.md +5 -5
- package/dist-assets/prompts/commands/implement.md +1 -1
- package/dist-assets/prompts/commands/tech-plan.md +2 -2
- package/dist-assets/prompts/commands/validate.md +2 -2
- package/dist-assets/runbooks/deploy-checklist.md +5 -5
- package/dist-assets/runbooks/how-to-use-skills.md +10 -0
- package/dist-assets/runbooks/validation-checklist.md +5 -5
- package/dist-assets/schemas/evidence.schema.json +109 -0
- package/dist-assets/templates/ANTIGRAVITY.md.template +7 -5
- package/dist-assets/templates/CLAUDE.md.template +7 -5
- package/dist-assets/templates/CODEX.md.template +7 -5
- package/evidence-validator-76ZQQYDU.js +6661 -0
- package/package.json +7 -65
- package/{src/core/workspace/read-only-workspace.js → read-only-workspace-PZBE7KAX.js} +14 -31
- package/{src/commands/validate.js → validate-Q2NLLQ5G.js} +30 -22
- package/AGENTS.md +0 -34
- package/CHANGELOG.md +0 -237
- package/LICENSE +0 -21
- package/PUBLISH_MANIFEST.json +0 -34
- package/README.md +0 -129
- package/dist-assets/runbooks/use-linear-for-operational-planning.md +0 -45
- package/dist-assets/runbooks/use-napkin-project-memory.md +0 -33
- package/dist-assets/templates/.geminiignore.template +0 -8
- package/dist-assets/templates/GEMINI.md.template +0 -20
- package/docs/compatibility/provider-usage.md +0 -46
- package/docs/compatibility/runtime-matrix.md +0 -31
- package/docs/getting-started/DESKTOP_PROMPT.md +0 -52
- package/docs/getting-started/quickstart.md +0 -117
- package/docs/getting-started/troubleshooting.md +0 -123
- package/docs/getting-started/upgrading-to-v2.md +0 -62
- package/read_only_safety_verification.md +0 -48
- package/src/adapters/index.js +0 -4
- package/src/adapters/platforms/antigravity.js +0 -382
- package/src/adapters/platforms/claude.js +0 -126
- package/src/adapters/platforms/codex.js +0 -113
- package/src/adapters/platforms/gemini.js +0 -245
- package/src/cli.js +0 -155
- package/src/commands/clean.js +0 -106
- package/src/commands/collect-evidence.js +0 -25
- package/src/commands/doctor.js +0 -202
- package/src/commands/execute.js +0 -382
- package/src/commands/init.js +0 -359
- package/src/commands/run.js +0 -112
- package/src/core/backup.js +0 -65
- package/src/core/completion-contract.js +0 -35
- package/src/core/delegation-controller.js +0 -193
- package/src/core/evidence/evidence-ledger.js +0 -98
- package/src/core/execution-planner.js +0 -62
- package/src/core/filesystem.js +0 -141
- package/src/core/finalization/finalizer.js +0 -165
- package/src/core/finalization/workspace-snapshot.js +0 -110
- package/src/core/gates/branch-gate.js +0 -134
- package/src/core/gates/merge-gate.js +0 -74
- package/src/core/handoff/handoff-engine.js +0 -104
- package/src/core/healing/cli-remediation-executor.js +0 -153
- package/src/core/healing/healer-engine.js +0 -212
- package/src/core/healing/runtime-remediation-executor.js +0 -136
- package/src/core/identity.js +0 -43
- package/src/core/install-plan.js +0 -34
- package/src/core/opencode-merge.js +0 -150
- package/src/core/package-assets.js +0 -141
- package/src/core/request-classifier.js +0 -73
- package/src/core/request-router.js +0 -289
- package/src/core/runtime/opencode-adapter.js +0 -202
- package/src/core/sdd/validator.js +0 -67
- package/src/core/statuses.js +0 -29
- package/src/core/symlink-layout.js +0 -93
- package/src/core/templates.js +0 -229
- package/src/core/validation/artifact-fidelity-gate.js +0 -445
- package/src/core/validation/canonical-finalization.js +0 -43
- package/src/core/validation/delivery-decision-engine.js +0 -344
- package/src/core/validation/evidence-collector.js +0 -245
- package/src/core/validation/quality-guard.js +0 -251
- package/src/core/validation/stack-detector.js +0 -65
- package/src/core/validation/validation-planner.js +0 -134
- package/src/core/validation/visual-verifier.js +0 -175
- package/src/core/workflow-profiles.js +0 -107
- package/src/core/workflow-state-machine.js +0 -46
|
@@ -1,251 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { MergeGate } from "../gates/merge-gate.js";
|
|
5
|
-
|
|
6
|
-
const PROTECTED_BRANCHES = new Set(["main", "master"]);
|
|
7
|
-
|
|
8
|
-
function normalizePath(file) {
|
|
9
|
-
return String(file || "").trim().replaceAll("\\", "/");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function parseStatusLine(line) {
|
|
13
|
-
const value = String(line || "");
|
|
14
|
-
const match = value.match(/^(?:[ MADRCU?!]{2}\s+|[MADRCU?!]\s+)(.+)$/);
|
|
15
|
-
if (!match) return "";
|
|
16
|
-
const candidate = match[1].trim();
|
|
17
|
-
const renamed = candidate.includes(" -> ") ? candidate.split(" -> ").at(-1) : candidate;
|
|
18
|
-
return normalizePath(renamed);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function isManagedChangePath(file = "") {
|
|
22
|
-
return /(^|\/)(node_modules|vendor|dist|coverage|\.cache)(\/|$)/.test(normalizePath(file));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function exists(target) {
|
|
26
|
-
try {
|
|
27
|
-
await fs.access(target);
|
|
28
|
-
return true;
|
|
29
|
-
} catch {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export { parseStatusLine };
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* QualityGuard verifies objective delivery safety. It does not grade prose or
|
|
38
|
-
* require ceremonial workflow files.
|
|
39
|
-
*/
|
|
40
|
-
export class QualityGuard {
|
|
41
|
-
constructor({ cwd, taskSlug = null, mode = null, evidenceWillBePersisted = false } = {}) {
|
|
42
|
-
this.cwd = cwd;
|
|
43
|
-
this.taskSlug = taskSlug;
|
|
44
|
-
this.mode = mode;
|
|
45
|
-
this._changedFiles = null;
|
|
46
|
-
this._hasHead = null;
|
|
47
|
-
this.evidenceWillBePersisted = evidenceWillBePersisted;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
runGit(command) {
|
|
51
|
-
return execSync(command, {
|
|
52
|
-
cwd: this.cwd,
|
|
53
|
-
encoding: "utf8",
|
|
54
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
55
|
-
}).trim();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
hasGitHead() {
|
|
59
|
-
if (this._hasHead !== null) return this._hasHead;
|
|
60
|
-
try {
|
|
61
|
-
this.runGit("git rev-parse --verify HEAD");
|
|
62
|
-
this._hasHead = true;
|
|
63
|
-
} catch {
|
|
64
|
-
this._hasHead = false;
|
|
65
|
-
}
|
|
66
|
-
return this._hasHead;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
getCurrentBranch() {
|
|
70
|
-
try {
|
|
71
|
-
return this.runGit("git branch --show-current") || "unknown";
|
|
72
|
-
} catch {
|
|
73
|
-
return "unknown";
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
getChangedFiles() {
|
|
78
|
-
if (this._changedFiles) return this._changedFiles;
|
|
79
|
-
const files = new Set();
|
|
80
|
-
try {
|
|
81
|
-
if (this.hasGitHead()) {
|
|
82
|
-
this.runGit("git diff --name-only HEAD")
|
|
83
|
-
.split("\n")
|
|
84
|
-
.map(normalizePath)
|
|
85
|
-
.filter(Boolean)
|
|
86
|
-
.forEach((file) => files.add(file));
|
|
87
|
-
}
|
|
88
|
-
this.runGit("git status --short --untracked-files=all")
|
|
89
|
-
.split("\n")
|
|
90
|
-
.map(parseStatusLine)
|
|
91
|
-
.filter(Boolean)
|
|
92
|
-
.filter((file) => !isManagedChangePath(file))
|
|
93
|
-
.forEach((file) => files.add(file));
|
|
94
|
-
} catch {
|
|
95
|
-
// A non-git consumer may still run validation commands. Branch safety is
|
|
96
|
-
// reported as unavailable rather than fabricated.
|
|
97
|
-
}
|
|
98
|
-
this._changedFiles = [...files].sort();
|
|
99
|
-
return this._changedFiles;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
isDocsTask() {
|
|
103
|
-
const files = this.getChangedFiles();
|
|
104
|
-
return files.length > 0 && files.every((file) => file.endsWith(".md") || file.endsWith(".txt") || file.startsWith("docs/"));
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
isUiTask() {
|
|
108
|
-
return this.getChangedFiles().some((file) =>
|
|
109
|
-
/\.(tsx|jsx|vue|html|css|scss|less|blade\.php)$/.test(file) ||
|
|
110
|
-
/(^|\/)(components|pages|views)\//.test(file)
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
isImplementationTask() {
|
|
115
|
-
const files = this.getChangedFiles();
|
|
116
|
-
if (files.length === 0 || this.isDocsTask()) return false;
|
|
117
|
-
return files.some((file) =>
|
|
118
|
-
file === "package.json" ||
|
|
119
|
-
/^(src|app|pages|components|server|api)\//.test(file) ||
|
|
120
|
-
/\.(js|jsx|ts|tsx|vue|php|py|rb|go|java|css|scss|html)$/.test(file)
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
async hasExecutableBehaviorChanges() {
|
|
126
|
-
const files = this.getChangedFiles().filter((file) => !isManagedChangePath(file));
|
|
127
|
-
const executableExtensions = /\.(js|jsx|mjs|cjs|ts|tsx|vue|php|py|rb|go|java|cs|rs|swift|kt|kts|sh|bash)$/i;
|
|
128
|
-
if (files.some((file) => executableExtensions.test(file))) return true;
|
|
129
|
-
|
|
130
|
-
for (const file of files.filter((item) => /\.(html?|xhtml)$/i.test(item))) {
|
|
131
|
-
try {
|
|
132
|
-
const content = await fs.readFile(path.join(this.cwd, file), "utf8");
|
|
133
|
-
if (/<script\b|\son(?:click|change|input|submit|keydown|keyup|load|error)\s*=|javascript:/i.test(content)) return true;
|
|
134
|
-
} catch {
|
|
135
|
-
// Missing or unreadable changed files are handled by the normal validation path.
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
verifyBranchSafety() {
|
|
142
|
-
const branch = this.getCurrentBranch();
|
|
143
|
-
const implementation = this.isImplementationTask();
|
|
144
|
-
if (!this.hasGitHead()) {
|
|
145
|
-
return { status: "PASS_WITH_NOTES", branch, reason: "Git HEAD unavailable; branch safety could not be verified." };
|
|
146
|
-
}
|
|
147
|
-
if (implementation && PROTECTED_BRANCHES.has(branch)) {
|
|
148
|
-
return { status: "FAIL_QUALITY_GATE", branch, reason: `Implementation changes are present on protected branch '${branch}'.` };
|
|
149
|
-
}
|
|
150
|
-
if (this.mode === "full") {
|
|
151
|
-
const mergeGate = new MergeGate({ cwd: this.cwd });
|
|
152
|
-
const mergeResult = mergeGate.check(branch);
|
|
153
|
-
if (mergeResult.status === "FAIL_QUALITY_GATE") {
|
|
154
|
-
return { status: "FAIL_QUALITY_GATE", branch, reason: mergeResult.reason };
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return { status: "PASS", branch };
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async verifyDocumentationNeed() {
|
|
161
|
-
if (!this.isImplementationTask()) return { status: "NOT_RUN", reason: "No implementation changes detected." };
|
|
162
|
-
const files = this.getChangedFiles();
|
|
163
|
-
const docsChanged = files.some((file) => file === "README.md" || file.startsWith("docs/") || file.endsWith(".md"));
|
|
164
|
-
const publicSurfaceChanged = files.some((file) =>
|
|
165
|
-
file === "package.json" ||
|
|
166
|
-
/(^|\/)(routes?|api|cli|commands?|config|public)\//.test(file) ||
|
|
167
|
-
/README|CHANGELOG/.test(file)
|
|
168
|
-
);
|
|
169
|
-
if (publicSurfaceChanged && !docsChanged) {
|
|
170
|
-
return { status: "PASS_WITH_NOTES", reason: "Public behavior may have changed without a documentation update." };
|
|
171
|
-
}
|
|
172
|
-
return { status: "PASS" };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
async verifyUiEvidence() {
|
|
176
|
-
if (!this.isUiTask()) return { status: "NOT_RUN", reason: "Non-UI task." };
|
|
177
|
-
const candidates = ["docs", "screenshots", "evidence"];
|
|
178
|
-
const found = [];
|
|
179
|
-
for (const dir of candidates) {
|
|
180
|
-
const root = path.join(this.cwd, dir);
|
|
181
|
-
if (!(await exists(root))) continue;
|
|
182
|
-
const entries = await fs.readdir(root, { recursive: true }).catch(() => []);
|
|
183
|
-
for (const entry of entries) {
|
|
184
|
-
if (/\.(png|jpe?g|webp)$/i.test(String(entry))) found.push(normalizePath(path.join(dir, String(entry))));
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
return found.length
|
|
188
|
-
? { status: "PASS", screenshots: found.slice(0, 20) }
|
|
189
|
-
: { status: "PASS_WITH_NOTES", reason: "UI changed without persisted screenshots; rely on the stated browser/manual review only if it actually occurred." };
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async verifyFullModeEvidence() {
|
|
193
|
-
if (this.mode !== "full") return { status: "NOT_RUN", reason: "Persisted evidence is optional outside full mode." };
|
|
194
|
-
if (this.evidenceWillBePersisted) return { status: "PASS", reason: "Collector will persist observed full-mode evidence." };
|
|
195
|
-
const candidates = [
|
|
196
|
-
path.join(this.cwd, "EVIDENCE.json"),
|
|
197
|
-
this.taskSlug ? path.join(this.cwd, "docs/workflows", this.taskSlug, "evidence.json") : null
|
|
198
|
-
].filter(Boolean);
|
|
199
|
-
for (const file of candidates) {
|
|
200
|
-
if (await exists(file)) return { status: "PASS", file: path.relative(this.cwd, file) };
|
|
201
|
-
}
|
|
202
|
-
return { status: "FAIL_QUALITY_GATE", reason: "Full mode requires persisted evidence generated from observed execution." };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
verifyTruthfulnessBoundary() {
|
|
206
|
-
if (!this.isUiTask()) return { status: "NOT_RUN", reason: "Non-UI task." };
|
|
207
|
-
return {
|
|
208
|
-
status: "NOT_RUN",
|
|
209
|
-
reason: "Product truthfulness requires semantic review; keyword matching is intentionally not used as proof."
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async verify() {
|
|
214
|
-
const branchSafety = this.verifyBranchSafety();
|
|
215
|
-
const documentation = await this.verifyDocumentationNeed();
|
|
216
|
-
const uiEvidence = await this.verifyUiEvidence();
|
|
217
|
-
const fullModeEvidence = await this.verifyFullModeEvidence();
|
|
218
|
-
const productTruthfulness = this.verifyTruthfulnessBoundary();
|
|
219
|
-
|
|
220
|
-
// productTruthfulness is intentionally excluded from the blocking checks array.
|
|
221
|
-
// Semantic truthfulness cannot be verified by keyword matching; it always returns
|
|
222
|
-
// NOT_RUN by design. It appears in the output as an observational signal for
|
|
223
|
-
// human reviewers, but never affects overallStatus.
|
|
224
|
-
const checks = [branchSafety, documentation, uiEvidence, fullModeEvidence];
|
|
225
|
-
let overallStatus = "PASS";
|
|
226
|
-
if (checks.some((check) => ["FAIL", "FAIL_QUALITY_GATE", "BLOCKED"].includes(check.status))) {
|
|
227
|
-
overallStatus = "FAIL_QUALITY_GATE";
|
|
228
|
-
} else if (checks.some((check) => check.status === "PASS_WITH_NOTES")) {
|
|
229
|
-
overallStatus = "PASS_WITH_NOTES";
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return {
|
|
233
|
-
overallStatus,
|
|
234
|
-
isUiTask: this.isUiTask(),
|
|
235
|
-
isDocsTask: this.isDocsTask(),
|
|
236
|
-
isImplementationTask: this.isImplementationTask(),
|
|
237
|
-
git: {
|
|
238
|
-
hasHead: this.hasGitHead(),
|
|
239
|
-
branch: this.getCurrentBranch(),
|
|
240
|
-
changedFiles: this.getChangedFiles()
|
|
241
|
-
},
|
|
242
|
-
checks: {
|
|
243
|
-
branchSafety,
|
|
244
|
-
documentation,
|
|
245
|
-
uiEvidence,
|
|
246
|
-
fullModeEvidence,
|
|
247
|
-
productTruthfulness
|
|
248
|
-
}
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
export class StackDetector {
|
|
5
|
-
constructor({ cwd = process.cwd() } = {}) {
|
|
6
|
-
this.cwd = cwd;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
async detect() {
|
|
10
|
-
const stacks = [];
|
|
11
|
-
|
|
12
|
-
// 1. Node check
|
|
13
|
-
const packageJsonExists = await fs.access(path.join(this.cwd, "package.json")).then(() => true).catch(() => false);
|
|
14
|
-
if (packageJsonExists) {
|
|
15
|
-
stacks.push("node");
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// 2. Python check
|
|
19
|
-
const pyprojectExists = await fs.access(path.join(this.cwd, "pyproject.toml")).then(() => true).catch(() => false) ||
|
|
20
|
-
await fs.access(path.join(this.cwd, "requirements.txt")).then(() => true).catch(() => false) ||
|
|
21
|
-
await fs.access(path.join(this.cwd, "setup.py")).then(() => true).catch(() => false) ||
|
|
22
|
-
await fs.access(path.join(this.cwd, "Pipfile")).then(() => true).catch(() => false);
|
|
23
|
-
|
|
24
|
-
let hasPythonFiles = pyprojectExists;
|
|
25
|
-
if (!hasPythonFiles) {
|
|
26
|
-
hasPythonFiles = await this.hasFileWithExtension(this.cwd, ".py");
|
|
27
|
-
}
|
|
28
|
-
if (hasPythonFiles) {
|
|
29
|
-
stacks.push("python");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// 3. PHP check
|
|
33
|
-
const composerExists = await fs.access(path.join(this.cwd, "composer.json")).then(() => true).catch(() => false);
|
|
34
|
-
let hasPhpFiles = composerExists;
|
|
35
|
-
if (!hasPhpFiles) {
|
|
36
|
-
hasPhpFiles = await this.hasFileWithExtension(this.cwd, ".php");
|
|
37
|
-
}
|
|
38
|
-
if (hasPhpFiles) {
|
|
39
|
-
stacks.push("php");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return stacks;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async hasFileWithExtension(dir, ext) {
|
|
46
|
-
try {
|
|
47
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
48
|
-
for (const entry of entries) {
|
|
49
|
-
if (entry.isDirectory()) {
|
|
50
|
-
if (["node_modules", "vendor", ".git", "dist", "build", ".cache"].includes(entry.name)) {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
const subPath = path.join(dir, entry.name);
|
|
54
|
-
const found = await this.hasFileWithExtension(subPath, ext);
|
|
55
|
-
if (found) return true;
|
|
56
|
-
} else if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
} catch {
|
|
61
|
-
// ignore
|
|
62
|
-
}
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { StackDetector } from "./stack-detector.js";
|
|
4
|
-
|
|
5
|
-
export class ValidationPlanner {
|
|
6
|
-
constructor({ cwd = process.cwd(), qualityGuard } = {}) {
|
|
7
|
-
this.cwd = cwd;
|
|
8
|
-
this.qualityGuard = qualityGuard;
|
|
9
|
-
this.stackDetector = new StackDetector({ cwd });
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async plan(profile = "generic") {
|
|
13
|
-
const stacks = await this.stackDetector.detect();
|
|
14
|
-
const tasks = [];
|
|
15
|
-
|
|
16
|
-
const changedFiles = this.qualityGuard.getChangedFiles().filter((file) => {
|
|
17
|
-
return !/(^|\/)(node_modules|vendor|dist|coverage|\.cache)(\/|$)/.test(String(file).replaceAll("\\", "/"));
|
|
18
|
-
});
|
|
19
|
-
const executableBehavior = await this.qualityGuard.hasExecutableBehaviorChanges();
|
|
20
|
-
|
|
21
|
-
// Node Stack Planning
|
|
22
|
-
if (stacks.includes("node")) {
|
|
23
|
-
try {
|
|
24
|
-
const pkg = JSON.parse(await fs.readFile(path.join(this.cwd, "package.json"), "utf8"));
|
|
25
|
-
const scripts = pkg.scripts || {};
|
|
26
|
-
|
|
27
|
-
this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
|
|
28
|
-
this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"], { rejectNoOp: executableBehavior });
|
|
29
|
-
this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
|
|
30
|
-
this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
|
|
31
|
-
|
|
32
|
-
if (scripts.validate && !String(scripts.validate).includes("ai-workflow collect-evidence")) {
|
|
33
|
-
this.addNodeScriptTask(tasks, scripts, "validate", ["npm", "run", "validate"]);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const hasTs = changedFiles.some((file) => /\.(ts|tsx)$/.test(file)) ||
|
|
37
|
-
await fs.access(path.join(this.cwd, "tsconfig.json")).then(() => true).catch(() => false);
|
|
38
|
-
if (hasTs && !scripts.typecheck) {
|
|
39
|
-
tasks.push({
|
|
40
|
-
name: "typecheck",
|
|
41
|
-
command: ["npm", "run", "typecheck"],
|
|
42
|
-
kind: "typecheck",
|
|
43
|
-
presetStatus: "FAIL_QUALITY_GATE",
|
|
44
|
-
summary: "TypeScript detected but no typecheck script is configured."
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
} catch {
|
|
48
|
-
// Fallback or ignore if pkg read fails
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Python Stack Planning
|
|
53
|
-
if (stacks.includes("python")) {
|
|
54
|
-
const pythonFiles = changedFiles.filter(f => f.endsWith(".py"));
|
|
55
|
-
|
|
56
|
-
// Compiler syntax check for changed python files
|
|
57
|
-
if (pythonFiles.length > 0) {
|
|
58
|
-
tasks.push({
|
|
59
|
-
name: "python-syntax",
|
|
60
|
-
command: ["python", "-m", "py_compile", ...pythonFiles],
|
|
61
|
-
kind: "lint"
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Check if testing is needed
|
|
66
|
-
if (executableBehavior) {
|
|
67
|
-
// Check if pytest or unittest is configured
|
|
68
|
-
const hasPytestConfig = await fs.access(path.join(this.cwd, "pytest.ini")).then(() => true).catch(() => false) ||
|
|
69
|
-
await fs.access(path.join(this.cwd, "pyproject.toml")).then(() => true).catch(() => false);
|
|
70
|
-
|
|
71
|
-
if (hasPytestConfig || pythonFiles.length > 0) {
|
|
72
|
-
tasks.push({
|
|
73
|
-
name: "python-test",
|
|
74
|
-
command: ["pytest"],
|
|
75
|
-
kind: "test"
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// PHP Stack Planning
|
|
82
|
-
if (stacks.includes("php")) {
|
|
83
|
-
const phpFiles = changedFiles.filter(f => f.endsWith(".php") || f.endsWith(".inc"));
|
|
84
|
-
|
|
85
|
-
// Compiler syntax check for each changed PHP file - split into separate tasks
|
|
86
|
-
if (phpFiles.length > 0) {
|
|
87
|
-
for (const file of phpFiles) {
|
|
88
|
-
tasks.push({
|
|
89
|
-
name: `php-syntax:${file}`,
|
|
90
|
-
command: ["php", "-l", file],
|
|
91
|
-
kind: "lint"
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Check if testing is needed
|
|
97
|
-
if (executableBehavior) {
|
|
98
|
-
const hasPhpUnitXml = await fs.access(path.join(this.cwd, "phpunit.xml")).then(() => true).catch(() => false) ||
|
|
99
|
-
await fs.access(path.join(this.cwd, "phpunit.xml.dist")).then(() => true).catch(() => false);
|
|
100
|
-
if (hasPhpUnitXml || phpFiles.length > 0) {
|
|
101
|
-
tasks.push({
|
|
102
|
-
name: "php-test",
|
|
103
|
-
command: ["vendor/bin/phpunit"],
|
|
104
|
-
kind: "test"
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return tasks;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
addNodeScriptTask(tasks, scripts, name, command, { rejectNoOp = false } = {}) {
|
|
114
|
-
const script = scripts[name];
|
|
115
|
-
if (!script) return;
|
|
116
|
-
if (!rejectNoOp && name === "test" && this.isNoOpScript(script)) return;
|
|
117
|
-
if (rejectNoOp && this.isNoOpScript(script)) {
|
|
118
|
-
tasks.push({
|
|
119
|
-
name,
|
|
120
|
-
command,
|
|
121
|
-
kind: name === "test" ? "test" : name,
|
|
122
|
-
presetStatus: "FAIL_QUALITY_GATE",
|
|
123
|
-
summary: `Configured ${name} script is a no-op: ${script}`
|
|
124
|
-
});
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
tasks.push({ name, command, kind: name === "test" ? "test" : name });
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
isNoOpScript(script = "") {
|
|
131
|
-
const value = String(script).trim().toLowerCase();
|
|
132
|
-
return /^(echo\b|true$|exit\s+0$|node\s+-e\s+["']?console\.log)/.test(value) || value.includes("tests-pass");
|
|
133
|
-
}
|
|
134
|
-
}
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import http from "node:http";
|
|
2
|
-
import fs from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
export class VisualVerifier {
|
|
6
|
-
constructor({ cwd = process.cwd(), visualDist = null, port = 8080 } = {}) {
|
|
7
|
-
this.cwd = cwd;
|
|
8
|
-
this.visualDist = visualDist;
|
|
9
|
-
this.port = Number(port || 8080);
|
|
10
|
-
this.server = null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async startServer() {
|
|
14
|
-
if (!this.visualDist) {
|
|
15
|
-
throw new Error("No --visual-dist directory specified. Cannot start visual server.");
|
|
16
|
-
}
|
|
17
|
-
const distPath = path.resolve(this.cwd, this.visualDist);
|
|
18
|
-
this.server = http.createServer(async (req, res) => {
|
|
19
|
-
try {
|
|
20
|
-
const urlPath = req.url.split("?")[0];
|
|
21
|
-
const filePath = path.join(distPath, urlPath === "/" ? "index.html" : urlPath);
|
|
22
|
-
const content = await fs.readFile(filePath);
|
|
23
|
-
|
|
24
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
25
|
-
let contentType = "text/html";
|
|
26
|
-
if (ext === ".js" || ext === ".mjs") contentType = "application/javascript";
|
|
27
|
-
else if (ext === ".css") contentType = "text/css";
|
|
28
|
-
else if (ext === ".png") contentType = "image/png";
|
|
29
|
-
else if (ext === ".jpg" || ext === ".jpeg") contentType = "image/jpeg";
|
|
30
|
-
else if (ext === ".svg") contentType = "image/svg+xml";
|
|
31
|
-
|
|
32
|
-
res.writeHead(200, { "Content-Type": contentType });
|
|
33
|
-
res.end(content);
|
|
34
|
-
} catch (err) {
|
|
35
|
-
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
36
|
-
res.end("Not Found");
|
|
37
|
-
}
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
await new Promise((resolve, reject) => {
|
|
41
|
-
this.server.listen(this.port, "127.0.0.1", (err) => {
|
|
42
|
-
if (err) reject(err);
|
|
43
|
-
else resolve();
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async closeServer() {
|
|
49
|
-
if (this.server) {
|
|
50
|
-
await new Promise((resolve) => this.server.close(resolve));
|
|
51
|
-
this.server = null;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async captureScreenshots() {
|
|
56
|
-
if (!this.visualDist) return [];
|
|
57
|
-
|
|
58
|
-
let playwright;
|
|
59
|
-
try {
|
|
60
|
-
playwright = await import("playwright");
|
|
61
|
-
} catch {
|
|
62
|
-
throw new Error(
|
|
63
|
-
"Playwright is required to capture visual evidence. Please install it using 'npm install -D playwright' and initialize browsers using 'npx playwright install'."
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
await this.startServer();
|
|
68
|
-
let browser;
|
|
69
|
-
try {
|
|
70
|
-
browser = await playwright.chromium.launch();
|
|
71
|
-
} catch (err) {
|
|
72
|
-
await this.closeServer();
|
|
73
|
-
throw new Error(
|
|
74
|
-
`Failed to launch Playwright Chromium. Please ensure browsers are installed using 'npx playwright install chromium'. Details: ${err.message}`
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
const page = await browser.newPage();
|
|
78
|
-
|
|
79
|
-
const outputDir = path.join(this.cwd, ".evidence/visual");
|
|
80
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
81
|
-
|
|
82
|
-
const desktopPath = path.join(outputDir, "desktop.png");
|
|
83
|
-
const mobilePath = path.join(outputDir, "mobile.png");
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
await page.goto(`http://127.0.0.1:${this.port}`);
|
|
87
|
-
|
|
88
|
-
// Desktop
|
|
89
|
-
await page.setViewportSize({ width: 1440, height: 900 });
|
|
90
|
-
await page.screenshot({ path: desktopPath, fullPage: true });
|
|
91
|
-
|
|
92
|
-
// Mobile
|
|
93
|
-
await page.setViewportSize({ width: 390, height: 844 });
|
|
94
|
-
await page.screenshot({ path: mobilePath, fullPage: true });
|
|
95
|
-
} finally {
|
|
96
|
-
await browser.close();
|
|
97
|
-
await this.closeServer();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return [desktopPath, mobilePath];
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async runAccessibilityCheck() {
|
|
104
|
-
if (!this.visualDist) {
|
|
105
|
-
throw new Error("Accessibility check requires a target build directory specified via --visual-dist.");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
let playwright;
|
|
109
|
-
try {
|
|
110
|
-
playwright = await import("playwright");
|
|
111
|
-
} catch {
|
|
112
|
-
throw new Error(
|
|
113
|
-
"Playwright is required to run accessibility checks. Please install it using 'npm install -D playwright' and initialize browsers using 'npx playwright install'."
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
await this.startServer();
|
|
118
|
-
let browser;
|
|
119
|
-
try {
|
|
120
|
-
browser = await playwright.chromium.launch();
|
|
121
|
-
} catch (err) {
|
|
122
|
-
await this.closeServer();
|
|
123
|
-
throw new Error(
|
|
124
|
-
`Failed to launch Playwright Chromium. Please ensure browsers are installed using 'npx playwright install chromium'. Details: ${err.message}`
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
const page = await browser.newPage();
|
|
128
|
-
|
|
129
|
-
const outputDir = path.join(this.cwd, ".evidence/a11y");
|
|
130
|
-
await fs.mkdir(outputDir, { recursive: true });
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
await page.goto(`http://127.0.0.1:${this.port}`);
|
|
134
|
-
|
|
135
|
-
// Read axe-core bundle
|
|
136
|
-
let axeScript = "";
|
|
137
|
-
try {
|
|
138
|
-
const axePath = path.join(this.cwd, "node_modules/axe-core/axe.min.js");
|
|
139
|
-
axeScript = await fs.readFile(axePath, "utf8");
|
|
140
|
-
} catch {
|
|
141
|
-
// Fallback: look in dev-kit node_modules
|
|
142
|
-
try {
|
|
143
|
-
// Resolve relative to current module path
|
|
144
|
-
const mainAxePath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../../../node_modules/axe-core/axe.min.js");
|
|
145
|
-
axeScript = await fs.readFile(mainAxePath, "utf8");
|
|
146
|
-
} catch {
|
|
147
|
-
throw new Error("axe-core package is not installed. Please install axe-core using 'npm install -D axe-core'.");
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
await page.evaluate(axeScript);
|
|
152
|
-
|
|
153
|
-
const axeResults = await page.evaluate(() => {
|
|
154
|
-
return window.axe.run();
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
const reportPath = path.join(outputDir, "report.json");
|
|
158
|
-
await fs.writeFile(reportPath, JSON.stringify(axeResults, null, 2));
|
|
159
|
-
|
|
160
|
-
const criticalViolations = axeResults.violations.filter(v => v.impact === "critical");
|
|
161
|
-
const seriousViolations = axeResults.violations.filter(v => v.impact === "serious");
|
|
162
|
-
|
|
163
|
-
return {
|
|
164
|
-
passed: criticalViolations.length === 0,
|
|
165
|
-
reportPath,
|
|
166
|
-
violations: axeResults.violations,
|
|
167
|
-
critical: criticalViolations,
|
|
168
|
-
serious: seriousViolations
|
|
169
|
-
};
|
|
170
|
-
} finally {
|
|
171
|
-
await browser.close();
|
|
172
|
-
await this.closeServer();
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|