@williambeto/ai-workflow 2.4.9 → 2.6.4
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 +1138 -168
- package/README.md +203 -77
- 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/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 +3 -3
- 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 -64
- 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/PUBLISH_MANIFEST.json +0 -34
- 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/authoring-custom-skills.md +0 -80
- 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/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
|
@@ -0,0 +1,734 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArtifactFidelityGate,
|
|
3
|
+
DeliveryDecisionEngine,
|
|
4
|
+
MergeGate,
|
|
5
|
+
StackDetector
|
|
6
|
+
} from "./chunk-2AOV2ATY.js";
|
|
7
|
+
|
|
8
|
+
// src/core/validation/quality-guard.ts
|
|
9
|
+
import { execSync } from "child_process";
|
|
10
|
+
import fs from "fs/promises";
|
|
11
|
+
import path from "path";
|
|
12
|
+
var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["main", "master"]);
|
|
13
|
+
function normalizePath(file) {
|
|
14
|
+
return String(file || "").trim().replaceAll("\\", "/");
|
|
15
|
+
}
|
|
16
|
+
function parseStatusLine(line) {
|
|
17
|
+
const value = String(line || "");
|
|
18
|
+
const match = value.match(/^(?:[ MADRCU?!]{2}\s+|[MADRCU?!]\s+)(.+)$/);
|
|
19
|
+
if (!match) return "";
|
|
20
|
+
const candidate = match[1].trim();
|
|
21
|
+
const renamed = candidate.includes(" -> ") ? candidate.split(" -> ").at(-1) : candidate;
|
|
22
|
+
return normalizePath(renamed);
|
|
23
|
+
}
|
|
24
|
+
function isManagedChangePath(file = "") {
|
|
25
|
+
return /(^|\/)(node_modules|vendor|dist|coverage|\.cache)(\/|$)/.test(normalizePath(file));
|
|
26
|
+
}
|
|
27
|
+
async function exists(target) {
|
|
28
|
+
try {
|
|
29
|
+
await fs.access(target);
|
|
30
|
+
return true;
|
|
31
|
+
} catch {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
var QualityGuard = class {
|
|
36
|
+
cwd;
|
|
37
|
+
taskSlug;
|
|
38
|
+
evidenceWillBePersisted;
|
|
39
|
+
evidencePolicy;
|
|
40
|
+
riskLevel;
|
|
41
|
+
_changedFiles;
|
|
42
|
+
_hasHead;
|
|
43
|
+
constructor({
|
|
44
|
+
cwd,
|
|
45
|
+
taskSlug = null,
|
|
46
|
+
evidenceWillBePersisted = false,
|
|
47
|
+
evidencePolicy = "optional",
|
|
48
|
+
riskLevel = "medium"
|
|
49
|
+
}) {
|
|
50
|
+
this.cwd = cwd;
|
|
51
|
+
this.taskSlug = taskSlug;
|
|
52
|
+
this.evidencePolicy = evidencePolicy || "optional";
|
|
53
|
+
this.riskLevel = riskLevel || "medium";
|
|
54
|
+
this._changedFiles = null;
|
|
55
|
+
this._hasHead = null;
|
|
56
|
+
this.evidenceWillBePersisted = evidenceWillBePersisted;
|
|
57
|
+
}
|
|
58
|
+
runGit(command) {
|
|
59
|
+
return execSync(command, {
|
|
60
|
+
cwd: this.cwd,
|
|
61
|
+
encoding: "utf8",
|
|
62
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
63
|
+
}).trim();
|
|
64
|
+
}
|
|
65
|
+
hasGitHead() {
|
|
66
|
+
if (this._hasHead !== null) return this._hasHead;
|
|
67
|
+
try {
|
|
68
|
+
this.runGit("git rev-parse --verify HEAD");
|
|
69
|
+
this._hasHead = true;
|
|
70
|
+
} catch {
|
|
71
|
+
this._hasHead = false;
|
|
72
|
+
}
|
|
73
|
+
return this._hasHead;
|
|
74
|
+
}
|
|
75
|
+
getCurrentBranch() {
|
|
76
|
+
try {
|
|
77
|
+
return this.runGit("git branch --show-current") || "unknown";
|
|
78
|
+
} catch {
|
|
79
|
+
return "unknown";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
getChangedFiles() {
|
|
83
|
+
if (this._changedFiles) return this._changedFiles;
|
|
84
|
+
const files = /* @__PURE__ */ new Set();
|
|
85
|
+
try {
|
|
86
|
+
if (this.hasGitHead()) {
|
|
87
|
+
this.runGit("git diff --name-only HEAD").split("\n").map(normalizePath).filter(Boolean).forEach((file) => files.add(file));
|
|
88
|
+
}
|
|
89
|
+
this.runGit("git status --short --untracked-files=all").split("\n").map(parseStatusLine).filter(Boolean).filter((file) => !isManagedChangePath(file)).forEach((file) => files.add(file));
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
this._changedFiles = [...files].sort();
|
|
93
|
+
return this._changedFiles;
|
|
94
|
+
}
|
|
95
|
+
isDocsTask() {
|
|
96
|
+
const files = this.getChangedFiles();
|
|
97
|
+
return files.length > 0 && files.every((file) => file.endsWith(".md") || file.endsWith(".txt") || file.startsWith("docs/"));
|
|
98
|
+
}
|
|
99
|
+
isUiTask() {
|
|
100
|
+
return this.getChangedFiles().some(
|
|
101
|
+
(file) => /\.(tsx|jsx|vue|html|css|scss|less|blade\.php)$/.test(file) || /(^|\/)(components|pages|views)\//.test(file)
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
isImplementationTask() {
|
|
105
|
+
const files = this.getChangedFiles();
|
|
106
|
+
if (files.length === 0 || this.isDocsTask()) return false;
|
|
107
|
+
return files.some(
|
|
108
|
+
(file) => file === "package.json" || /^(src|app|pages|components|server|api)\//.test(file) || /\.(js|jsx|ts|tsx|vue|php|py|rb|go|java|css|scss|html)$/.test(file)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
async hasExecutableBehaviorChanges() {
|
|
112
|
+
const files = this.getChangedFiles().filter((file) => !isManagedChangePath(file));
|
|
113
|
+
const executableExtensions = /\.(js|jsx|mjs|cjs|ts|tsx|vue|php|py|rb|go|java|cs|rs|swift|kt|kts|sh|bash)$/i;
|
|
114
|
+
if (files.some((file) => executableExtensions.test(file))) return true;
|
|
115
|
+
for (const file of files.filter((item) => /\.(html?|xhtml)$/i.test(item))) {
|
|
116
|
+
try {
|
|
117
|
+
const content = await fs.readFile(path.join(this.cwd, file), "utf8");
|
|
118
|
+
if (/<script\b|\son(?:click|change|input|submit|keydown|keyup|load|error)\s*=|javascript:/i.test(content)) return true;
|
|
119
|
+
} catch {
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
verifyBranchSafety() {
|
|
125
|
+
const branch = this.getCurrentBranch();
|
|
126
|
+
const implementation = this.isImplementationTask();
|
|
127
|
+
if (!this.hasGitHead()) {
|
|
128
|
+
return { status: "PASS_WITH_NOTES", branch, reason: "Git HEAD unavailable; branch safety could not be verified." };
|
|
129
|
+
}
|
|
130
|
+
if (implementation && PROTECTED_BRANCHES.has(branch)) {
|
|
131
|
+
return { status: "FAIL_QUALITY_GATE", branch, reason: `Implementation changes are present on protected branch '${branch}'.` };
|
|
132
|
+
}
|
|
133
|
+
if (this.riskLevel === "high") {
|
|
134
|
+
const mergeGate = new MergeGate({ cwd: this.cwd });
|
|
135
|
+
const mergeResult = mergeGate.check(branch);
|
|
136
|
+
if (mergeResult.status === "FAIL_QUALITY_GATE") {
|
|
137
|
+
return { status: "FAIL_QUALITY_GATE", branch, reason: mergeResult.reason };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { status: "PASS", branch };
|
|
141
|
+
}
|
|
142
|
+
async verifyDocumentationNeed() {
|
|
143
|
+
if (!this.isImplementationTask()) return { status: "NOT_RUN", reason: "No implementation changes detected." };
|
|
144
|
+
const files = this.getChangedFiles();
|
|
145
|
+
const docsChanged = files.some((file) => file === "README.md" || file.startsWith("docs/") || file.endsWith(".md"));
|
|
146
|
+
const publicSurfaceChanged = files.some(
|
|
147
|
+
(file) => file === "package.json" || /(^|\/)(routes?|api|cli|commands?|config|public)\//.test(file) || /README|CHANGELOG/.test(file)
|
|
148
|
+
);
|
|
149
|
+
if (publicSurfaceChanged && !docsChanged) {
|
|
150
|
+
return { status: "PASS_WITH_NOTES", reason: "Public behavior may have changed without a documentation update." };
|
|
151
|
+
}
|
|
152
|
+
return { status: "PASS" };
|
|
153
|
+
}
|
|
154
|
+
async verifyUiEvidence() {
|
|
155
|
+
if (!this.isUiTask()) return { status: "NOT_RUN", reason: "Non-UI task." };
|
|
156
|
+
const candidates = ["docs", "screenshots", "evidence"];
|
|
157
|
+
const found = [];
|
|
158
|
+
for (const dir of candidates) {
|
|
159
|
+
const root = path.join(this.cwd, dir);
|
|
160
|
+
if (!await exists(root)) continue;
|
|
161
|
+
const entries = await fs.readdir(root, { recursive: true }).catch(() => []);
|
|
162
|
+
for (const entry of entries) {
|
|
163
|
+
if (/\.(png|jpe?g|webp)$/i.test(String(entry))) found.push(normalizePath(path.join(dir, String(entry))));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return found.length ? { status: "PASS", screenshots: found.slice(0, 20) } : { status: "PASS_WITH_NOTES", reason: "UI changed without persisted screenshots; rely on the stated browser/manual review only if it actually occurred." };
|
|
167
|
+
}
|
|
168
|
+
async verifyPersistedEvidence() {
|
|
169
|
+
if (this.evidencePolicy !== "required") return { status: "NOT_RUN", reason: "Persisted evidence is optional under optional evidence policy." };
|
|
170
|
+
if (this.evidenceWillBePersisted) return { status: "PASS", reason: "Collector will persist observed evidence." };
|
|
171
|
+
const candidates = [
|
|
172
|
+
path.join(this.cwd, "EVIDENCE.json"),
|
|
173
|
+
this.taskSlug ? path.join(this.cwd, "docs/workflows", this.taskSlug, "evidence.json") : null
|
|
174
|
+
].filter(Boolean);
|
|
175
|
+
for (const file of candidates) {
|
|
176
|
+
if (await exists(file)) return { status: "PASS", file: path.relative(this.cwd, file) };
|
|
177
|
+
}
|
|
178
|
+
return { status: "FAIL_QUALITY_GATE", reason: "Persisted evidence is required under required evidence policy." };
|
|
179
|
+
}
|
|
180
|
+
verifyTruthfulnessBoundary() {
|
|
181
|
+
if (!this.isUiTask()) return { status: "NOT_RUN", reason: "Non-UI task." };
|
|
182
|
+
return {
|
|
183
|
+
status: "NOT_RUN",
|
|
184
|
+
reason: "Product truthfulness requires semantic review; keyword matching is intentionally not used as proof."
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async verify() {
|
|
188
|
+
const branchSafety = this.verifyBranchSafety();
|
|
189
|
+
const documentation = await this.verifyDocumentationNeed();
|
|
190
|
+
const uiEvidence = await this.verifyUiEvidence();
|
|
191
|
+
const persistedEvidence = await this.verifyPersistedEvidence();
|
|
192
|
+
const productTruthfulness = this.verifyTruthfulnessBoundary();
|
|
193
|
+
const checks = [branchSafety, documentation, uiEvidence, persistedEvidence];
|
|
194
|
+
let overallStatus = "PASS";
|
|
195
|
+
if (checks.some((check) => ["FAIL", "FAIL_QUALITY_GATE", "BLOCKED"].includes(check.status))) {
|
|
196
|
+
overallStatus = "FAIL_QUALITY_GATE";
|
|
197
|
+
} else if (checks.some((check) => check.status === "PASS_WITH_NOTES")) {
|
|
198
|
+
overallStatus = "PASS_WITH_NOTES";
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
overallStatus,
|
|
202
|
+
isUiTask: this.isUiTask(),
|
|
203
|
+
isDocsTask: this.isDocsTask(),
|
|
204
|
+
isImplementationTask: this.isImplementationTask(),
|
|
205
|
+
git: {
|
|
206
|
+
hasHead: this.hasGitHead(),
|
|
207
|
+
branch: this.getCurrentBranch(),
|
|
208
|
+
changedFiles: this.getChangedFiles()
|
|
209
|
+
},
|
|
210
|
+
checks: {
|
|
211
|
+
branchSafety,
|
|
212
|
+
documentation,
|
|
213
|
+
uiEvidence,
|
|
214
|
+
persistedEvidence,
|
|
215
|
+
productTruthfulness
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// src/core/validation/visual-verifier.ts
|
|
222
|
+
import http from "http";
|
|
223
|
+
import fs2 from "fs/promises";
|
|
224
|
+
import path2 from "path";
|
|
225
|
+
var VisualVerifier = class {
|
|
226
|
+
cwd;
|
|
227
|
+
visualDist;
|
|
228
|
+
port;
|
|
229
|
+
server;
|
|
230
|
+
constructor({ cwd = process.cwd(), visualDist = null, port = 8080 } = {}) {
|
|
231
|
+
this.cwd = cwd;
|
|
232
|
+
this.visualDist = visualDist;
|
|
233
|
+
this.port = Number(port || 8080);
|
|
234
|
+
this.server = null;
|
|
235
|
+
}
|
|
236
|
+
async startServer() {
|
|
237
|
+
if (!this.visualDist) {
|
|
238
|
+
throw new Error("No --visual-dist directory specified. Cannot start visual server.");
|
|
239
|
+
}
|
|
240
|
+
const distPath = path2.resolve(this.cwd, this.visualDist);
|
|
241
|
+
this.server = http.createServer(async (req, res) => {
|
|
242
|
+
try {
|
|
243
|
+
const urlPath = req.url?.split("?")[0] || "/";
|
|
244
|
+
const filePath = path2.join(distPath, urlPath === "/" ? "index.html" : urlPath);
|
|
245
|
+
const content = await fs2.readFile(filePath);
|
|
246
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
247
|
+
let contentType = "text/html";
|
|
248
|
+
if (ext === ".js" || ext === ".mjs") contentType = "application/javascript";
|
|
249
|
+
else if (ext === ".css") contentType = "text/css";
|
|
250
|
+
else if (ext === ".png") contentType = "image/png";
|
|
251
|
+
else if (ext === ".jpg" || ext === ".jpeg") contentType = "image/jpeg";
|
|
252
|
+
else if (ext === ".svg") contentType = "image/svg+xml";
|
|
253
|
+
res.writeHead(200, { "Content-Type": contentType });
|
|
254
|
+
res.end(content);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
257
|
+
res.end("Not Found");
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
await new Promise((resolve, reject) => {
|
|
261
|
+
this.server.listen(this.port, "127.0.0.1", () => {
|
|
262
|
+
resolve();
|
|
263
|
+
});
|
|
264
|
+
this.server.on("error", (err) => {
|
|
265
|
+
reject(err);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
async closeServer() {
|
|
270
|
+
if (this.server) {
|
|
271
|
+
await new Promise((resolve) => this.server.close(() => resolve()));
|
|
272
|
+
this.server = null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async captureScreenshots() {
|
|
276
|
+
if (!this.visualDist) return [];
|
|
277
|
+
let playwright;
|
|
278
|
+
try {
|
|
279
|
+
playwright = await import("playwright");
|
|
280
|
+
} catch {
|
|
281
|
+
throw new Error(
|
|
282
|
+
"Playwright is required to capture visual evidence. Please install it using 'npm install -D playwright' and initialize browsers using 'npx playwright install'."
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
await this.startServer();
|
|
286
|
+
let browser;
|
|
287
|
+
try {
|
|
288
|
+
browser = await playwright.chromium.launch();
|
|
289
|
+
} catch (err) {
|
|
290
|
+
await this.closeServer();
|
|
291
|
+
throw new Error(
|
|
292
|
+
`Failed to launch Playwright Chromium. Please ensure browsers are installed using 'npx playwright install chromium'. Details: ${err.message}`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const page = await browser.newPage();
|
|
296
|
+
const outputDir = path2.join(this.cwd, ".evidence/visual");
|
|
297
|
+
await fs2.mkdir(outputDir, { recursive: true });
|
|
298
|
+
const desktopPath = path2.join(outputDir, "desktop.png");
|
|
299
|
+
const mobilePath = path2.join(outputDir, "mobile.png");
|
|
300
|
+
try {
|
|
301
|
+
await page.goto(`http://127.0.0.1:${this.port}`);
|
|
302
|
+
await page.setViewportSize({ width: 1440, height: 900 });
|
|
303
|
+
await page.screenshot({ path: desktopPath, fullPage: true });
|
|
304
|
+
await page.setViewportSize({ width: 390, height: 844 });
|
|
305
|
+
await page.screenshot({ path: mobilePath, fullPage: true });
|
|
306
|
+
} finally {
|
|
307
|
+
await browser.close();
|
|
308
|
+
await this.closeServer();
|
|
309
|
+
}
|
|
310
|
+
return [desktopPath, mobilePath];
|
|
311
|
+
}
|
|
312
|
+
async runAccessibilityCheck() {
|
|
313
|
+
if (!this.visualDist) {
|
|
314
|
+
throw new Error("Accessibility check requires a target build directory specified via --visual-dist.");
|
|
315
|
+
}
|
|
316
|
+
let playwright;
|
|
317
|
+
try {
|
|
318
|
+
playwright = await import("playwright");
|
|
319
|
+
} catch {
|
|
320
|
+
throw new Error(
|
|
321
|
+
"Playwright is required to run accessibility checks. Please install it using 'npm install -D playwright' and initialize browsers using 'npx playwright install'."
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
await this.startServer();
|
|
325
|
+
let browser;
|
|
326
|
+
try {
|
|
327
|
+
browser = await playwright.chromium.launch();
|
|
328
|
+
} catch (err) {
|
|
329
|
+
await this.closeServer();
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Failed to launch Playwright Chromium. Please ensure browsers are installed using 'npx playwright install chromium'. Details: ${err.message}`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const page = await browser.newPage();
|
|
335
|
+
const outputDir = path2.join(this.cwd, ".evidence/a11y");
|
|
336
|
+
await fs2.mkdir(outputDir, { recursive: true });
|
|
337
|
+
try {
|
|
338
|
+
await page.goto(`http://127.0.0.1:${this.port}`);
|
|
339
|
+
let axeScript = "";
|
|
340
|
+
try {
|
|
341
|
+
const axePath = path2.join(this.cwd, "node_modules/axe-core/axe.min.js");
|
|
342
|
+
axeScript = await fs2.readFile(axePath, "utf8");
|
|
343
|
+
} catch {
|
|
344
|
+
try {
|
|
345
|
+
const mainAxePath = path2.resolve(import.meta.dirname, "../../../node_modules/axe-core/axe.min.js");
|
|
346
|
+
axeScript = await fs2.readFile(mainAxePath, "utf8");
|
|
347
|
+
} catch {
|
|
348
|
+
throw new Error("axe-core package is not installed. Please install axe-core using 'npm install -D axe-core'.");
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
await page.evaluate(axeScript);
|
|
352
|
+
const axeResults = await page.evaluate(() => {
|
|
353
|
+
return window.axe.run();
|
|
354
|
+
});
|
|
355
|
+
const reportPath = path2.join(outputDir, "report.json");
|
|
356
|
+
await fs2.writeFile(reportPath, JSON.stringify(axeResults, null, 2));
|
|
357
|
+
const criticalViolations = axeResults.violations.filter((v) => v.impact === "critical");
|
|
358
|
+
const seriousViolations = axeResults.violations.filter((v) => v.impact === "serious");
|
|
359
|
+
return {
|
|
360
|
+
passed: criticalViolations.length === 0,
|
|
361
|
+
reportPath,
|
|
362
|
+
violations: axeResults.violations,
|
|
363
|
+
critical: criticalViolations,
|
|
364
|
+
serious: seriousViolations
|
|
365
|
+
};
|
|
366
|
+
} finally {
|
|
367
|
+
await browser.close();
|
|
368
|
+
await this.closeServer();
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// src/core/validation/evidence-collector.ts
|
|
374
|
+
import { spawnSync } from "child_process";
|
|
375
|
+
import fs3 from "fs/promises";
|
|
376
|
+
import path3 from "path";
|
|
377
|
+
var VALIDATION_KINDS = /* @__PURE__ */ new Set(["test", "build", "typecheck", "lint", "security", "smoke", "validate", "other"]);
|
|
378
|
+
function publicStatus(status) {
|
|
379
|
+
if (status === "PASS") return "COMPLETED";
|
|
380
|
+
if (status === "PASS_WITH_NOTES") return "COMPLETED_WITH_NOTES";
|
|
381
|
+
return "BLOCKED";
|
|
382
|
+
}
|
|
383
|
+
function inferTaskKind(task = {}) {
|
|
384
|
+
const explicit = String(task.kind || "").trim().toLowerCase();
|
|
385
|
+
if (VALIDATION_KINDS.has(explicit)) return explicit;
|
|
386
|
+
const value = `${task.name || ""} ${task.command || ""}`.toLowerCase();
|
|
387
|
+
if (/\b(test|tests|vitest|jest|mocha|ava|tap|pytest|phpunit|rspec|cypress|playwright)\b/.test(value)) return "test";
|
|
388
|
+
if (/\b(typecheck|tsc\b|mypy|pyright)\b/.test(value)) return "typecheck";
|
|
389
|
+
if (/\b(lint|eslint|stylelint|ruff|flake8|pylint)\b/.test(value)) return "lint";
|
|
390
|
+
if (/\b(build|bundle|compile)\b/.test(value)) return "build";
|
|
391
|
+
if (/\b(audit|security|sast|scan)\b/.test(value)) return "security";
|
|
392
|
+
if (/\b(smoke|curl|preview|serve)\b/.test(value)) return "smoke";
|
|
393
|
+
if (/\bvalidate\b/.test(value)) return "validate";
|
|
394
|
+
return "other";
|
|
395
|
+
}
|
|
396
|
+
var EvidenceCollector = class {
|
|
397
|
+
cwd;
|
|
398
|
+
maxLogLength;
|
|
399
|
+
timeout;
|
|
400
|
+
taskSlug;
|
|
401
|
+
/** @deprecated use evidencePolicy instead */
|
|
402
|
+
mode;
|
|
403
|
+
evidencePolicy;
|
|
404
|
+
profile;
|
|
405
|
+
branchRecovery;
|
|
406
|
+
userRequest;
|
|
407
|
+
explicitApprovals;
|
|
408
|
+
visualDist;
|
|
409
|
+
port;
|
|
410
|
+
constructor({
|
|
411
|
+
cwd,
|
|
412
|
+
maxLogLength = 2e3,
|
|
413
|
+
timeout = 6e4,
|
|
414
|
+
taskSlug = null,
|
|
415
|
+
mode = null,
|
|
416
|
+
evidencePolicy = null,
|
|
417
|
+
profile = "generic",
|
|
418
|
+
branchRecovery = "NOT_RECORDED",
|
|
419
|
+
userRequest = null,
|
|
420
|
+
explicitApprovals = [],
|
|
421
|
+
visualDist = null,
|
|
422
|
+
port = 8080
|
|
423
|
+
}) {
|
|
424
|
+
this.cwd = cwd;
|
|
425
|
+
this.maxLogLength = maxLogLength;
|
|
426
|
+
this.timeout = timeout;
|
|
427
|
+
this.taskSlug = taskSlug;
|
|
428
|
+
this.evidencePolicy = evidencePolicy || (mode === "full" ? "required" : "optional");
|
|
429
|
+
this.mode = this.evidencePolicy === "required" ? "full" : "standard";
|
|
430
|
+
this.profile = profile;
|
|
431
|
+
this.branchRecovery = branchRecovery;
|
|
432
|
+
this.userRequest = userRequest;
|
|
433
|
+
this.explicitApprovals = explicitApprovals;
|
|
434
|
+
this.visualDist = visualDist;
|
|
435
|
+
this.port = Number(port || 8080);
|
|
436
|
+
}
|
|
437
|
+
async findUserRequest() {
|
|
438
|
+
if (this.userRequest) return this.userRequest;
|
|
439
|
+
if (this.taskSlug) {
|
|
440
|
+
const ledgerPath = path3.join(this.cwd, `.ai-workflow/history/${this.taskSlug}-ledger.json`);
|
|
441
|
+
try {
|
|
442
|
+
const content = await fs3.readFile(ledgerPath, "utf8");
|
|
443
|
+
const events = JSON.parse(content);
|
|
444
|
+
const routeEvent = events.find((e) => e.eventType === "routing");
|
|
445
|
+
if (routeEvent && routeEvent.data?.request) {
|
|
446
|
+
return routeEvent.data.request;
|
|
447
|
+
}
|
|
448
|
+
const startEvent = events.find((e) => e.eventType === "implementation_start");
|
|
449
|
+
if (startEvent && startEvent.data?.prompt) {
|
|
450
|
+
return startEvent.data.prompt;
|
|
451
|
+
}
|
|
452
|
+
} catch {
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
try {
|
|
456
|
+
const dir = path3.join(this.cwd, ".ai-workflow/history");
|
|
457
|
+
const files = await fs3.readdir(dir);
|
|
458
|
+
for (const file of files) {
|
|
459
|
+
if (file.endsWith("-ledger.json")) {
|
|
460
|
+
const content = await fs3.readFile(path3.join(dir, file), "utf8");
|
|
461
|
+
const events = JSON.parse(content);
|
|
462
|
+
const routeEvent = events.find((e) => e.eventType === "routing");
|
|
463
|
+
if (routeEvent && routeEvent.data?.request) {
|
|
464
|
+
return routeEvent.data.request;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
} catch {
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
472
|
+
const branch = execSync2("git branch --show-current", { cwd: this.cwd, encoding: "utf8" }).trim();
|
|
473
|
+
if (branch && branch !== "main" && branch !== "master" && branch.startsWith("feat/")) {
|
|
474
|
+
return branch.replace("feat/", "").replaceAll("-", " ");
|
|
475
|
+
}
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
return "";
|
|
479
|
+
}
|
|
480
|
+
runTask(task) {
|
|
481
|
+
const kind = inferTaskKind(task);
|
|
482
|
+
if (task.presetStatus) {
|
|
483
|
+
return { name: task.name, command: task.command, kind, status: task.presetStatus, exitCode: null, summary: task.summary || "", output: "" };
|
|
484
|
+
}
|
|
485
|
+
let cmd = task.command;
|
|
486
|
+
let args = [];
|
|
487
|
+
if (Array.isArray(cmd)) {
|
|
488
|
+
const [c, ...a] = cmd;
|
|
489
|
+
cmd = c;
|
|
490
|
+
args = a;
|
|
491
|
+
} else if (typeof cmd === "string") {
|
|
492
|
+
const parts = cmd.split(/\s+/);
|
|
493
|
+
cmd = parts[0];
|
|
494
|
+
args = parts.slice(1);
|
|
495
|
+
}
|
|
496
|
+
const isWin32 = process.platform === "win32";
|
|
497
|
+
const isUncPath = typeof this.cwd === "string" && (this.cwd.startsWith("\\\\") || this.cwd.startsWith("//"));
|
|
498
|
+
const shellOption = isWin32 && isUncPath ? "powershell.exe" : isWin32 ? true : false;
|
|
499
|
+
const result = spawnSync(cmd, args, {
|
|
500
|
+
cwd: this.cwd,
|
|
501
|
+
shell: shellOption,
|
|
502
|
+
encoding: "utf8",
|
|
503
|
+
timeout: this.timeout
|
|
504
|
+
});
|
|
505
|
+
let status = result.status === 0 ? "PASS" : "FAIL";
|
|
506
|
+
if (result.error) {
|
|
507
|
+
status = "FAIL";
|
|
508
|
+
if (result.error.code === "ETIMEDOUT") {
|
|
509
|
+
status = "BLOCKED";
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
let summary = status === "PASS" ? "Command completed successfully." : "Command failed.";
|
|
513
|
+
if (result.error?.code === "ETIMEDOUT") {
|
|
514
|
+
summary = `Command timed out after ${this.timeout / 1e3}s.`;
|
|
515
|
+
} else if (result.error) {
|
|
516
|
+
summary = `Command execution error: ${result.error.message}`;
|
|
517
|
+
}
|
|
518
|
+
const rawOutput = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
519
|
+
const output = rawOutput.length > this.maxLogLength ? `${rawOutput.slice(0, this.maxLogLength)}
|
|
520
|
+
... [TRUNCATED]` : rawOutput;
|
|
521
|
+
return { name: task.name, command: task.command, kind, status, exitCode: result.status, signal: result.signal, summary, output };
|
|
522
|
+
}
|
|
523
|
+
async collect(tasks, { writeArtifact = false } = {}) {
|
|
524
|
+
const qualityGuard = new QualityGuard({ cwd: this.cwd, taskSlug: this.taskSlug, evidencePolicy: this.evidencePolicy, riskLevel: this.evidencePolicy === "required" ? "high" : "medium", evidenceWillBePersisted: writeArtifact });
|
|
525
|
+
const implementation = qualityGuard.isImplementationTask();
|
|
526
|
+
const executableBehavior = await qualityGuard.hasExecutableBehaviorChanges();
|
|
527
|
+
const results = tasks.map((task) => this.runTask(task));
|
|
528
|
+
const policyValidation = await qualityGuard.verify();
|
|
529
|
+
const behaviorTests = results.filter((result) => result.kind === "test");
|
|
530
|
+
const passingBehaviorTest = behaviorTests.some((result) => result.status === "PASS");
|
|
531
|
+
const userRequest = await this.findUserRequest();
|
|
532
|
+
const engine = new DeliveryDecisionEngine({ cwd: this.cwd });
|
|
533
|
+
const projectContext = await engine.detectContext();
|
|
534
|
+
const deliveryDecision = await engine.determineDecision({
|
|
535
|
+
userRequest,
|
|
536
|
+
projectContext,
|
|
537
|
+
explicitApprovals: this.explicitApprovals || []
|
|
538
|
+
});
|
|
539
|
+
const fidelityGate = new ArtifactFidelityGate({
|
|
540
|
+
cwd: this.cwd,
|
|
541
|
+
deliveryClass: deliveryDecision.allowedArtifactShape === "runnable-framework-app" ? "framework-native-frontend" : deliveryDecision.allowedArtifactShape === "wordpress" ? "wordpress-php" : "generic-code",
|
|
542
|
+
userRequest,
|
|
543
|
+
explicitApprovals: this.explicitApprovals || []
|
|
544
|
+
});
|
|
545
|
+
const changedFiles = policyValidation.git?.changedFiles || [];
|
|
546
|
+
const artifactFidelityCheck = await fidelityGate.verifyRequestFidelity({
|
|
547
|
+
userRequest,
|
|
548
|
+
deliveryDecision,
|
|
549
|
+
projectContext,
|
|
550
|
+
changedFiles
|
|
551
|
+
});
|
|
552
|
+
let overallStatus = "PASS";
|
|
553
|
+
if (implementation && executableBehavior && tasks.length === 0) overallStatus = "BLOCKED";
|
|
554
|
+
else if (results.some((result) => ["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(result.status))) overallStatus = "FAIL_QUALITY_GATE";
|
|
555
|
+
else if (executableBehavior && !passingBehaviorTest) overallStatus = "BLOCKED";
|
|
556
|
+
else if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(policyValidation.overallStatus)) overallStatus = "FAIL_QUALITY_GATE";
|
|
557
|
+
else if (results.some((result) => result.status === "PASS_WITH_NOTES") || policyValidation.overallStatus === "PASS_WITH_NOTES") overallStatus = "PASS_WITH_NOTES";
|
|
558
|
+
if (!artifactFidelityCheck || !artifactFidelityCheck.passed) {
|
|
559
|
+
overallStatus = "BLOCKED";
|
|
560
|
+
} else if (implementation) {
|
|
561
|
+
if (!deliveryDecision || !deliveryDecision.decision || deliveryDecision.decision.startsWith("BLOCK") || deliveryDecision.decision.includes("BLOCKED") || deliveryDecision.decision === "REQUIRE_EXPLICIT_AUTHORIZATION" || deliveryDecision.decision === "RELEASE_OR_DEPLOY_REQUIRES_APPROVAL") {
|
|
562
|
+
overallStatus = "BLOCKED";
|
|
563
|
+
} else if (results.length === 0 && executableBehavior && deliveryDecision.decision !== "DOCUMENTATION_ONLY" && deliveryDecision.decision !== "DOCS_ONLY" && deliveryDecision.decision !== "READ_ONLY_AUDIT") {
|
|
564
|
+
overallStatus = "BLOCKED";
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
const limitations = [];
|
|
568
|
+
if (implementation && executableBehavior && tasks.length === 0) limitations.push("No meaningful validation command was available for executable implementation work.");
|
|
569
|
+
if (executableBehavior && behaviorTests.length === 0) {
|
|
570
|
+
limitations.push("Executable behavior changed without a proportional automated behavior test. Build, lint, typecheck, smoke, screenshots, and manual review do not replace behavior tests.");
|
|
571
|
+
}
|
|
572
|
+
for (const [name, check] of Object.entries(policyValidation.checks || {})) {
|
|
573
|
+
if (check.status === "PASS_WITH_NOTES" && check.reason) limitations.push(`${name}: ${check.reason}`);
|
|
574
|
+
}
|
|
575
|
+
let visualEvidence = null;
|
|
576
|
+
if (this.visualDist) {
|
|
577
|
+
console.log(`
|
|
578
|
+
--- Capturing Visual Evidence ---`);
|
|
579
|
+
console.log(`Serving ${this.visualDist} on port ${this.port}...`);
|
|
580
|
+
try {
|
|
581
|
+
const verifier = new VisualVerifier({ cwd: this.cwd, visualDist: this.visualDist, port: this.port });
|
|
582
|
+
const screenshots = await verifier.captureScreenshots();
|
|
583
|
+
visualEvidence = {
|
|
584
|
+
desktop: path3.relative(this.cwd, screenshots[0]),
|
|
585
|
+
mobile: path3.relative(this.cwd, screenshots[1])
|
|
586
|
+
};
|
|
587
|
+
console.log(`Visual evidence captured successfully:`);
|
|
588
|
+
console.log(`- Desktop: ${visualEvidence.desktop}`);
|
|
589
|
+
console.log(`- Mobile: ${visualEvidence.mobile}`);
|
|
590
|
+
} catch (err) {
|
|
591
|
+
console.error(`Error capturing visual evidence: ${err.message}`);
|
|
592
|
+
throw err;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const evidence = {
|
|
596
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
597
|
+
taskSlug: this.taskSlug,
|
|
598
|
+
executionMode: this.mode,
|
|
599
|
+
workflowProfile: this.profile,
|
|
600
|
+
branchRecovery: this.branchRecovery,
|
|
601
|
+
branch: policyValidation.git?.branch || "unknown",
|
|
602
|
+
changedFiles,
|
|
603
|
+
status: publicStatus(overallStatus),
|
|
604
|
+
internalStatus: overallStatus,
|
|
605
|
+
commands: results,
|
|
606
|
+
checks: policyValidation.checks,
|
|
607
|
+
limitations,
|
|
608
|
+
deliveryDecision,
|
|
609
|
+
artifactFidelityCheck,
|
|
610
|
+
visualEvidence
|
|
611
|
+
};
|
|
612
|
+
if (writeArtifact) {
|
|
613
|
+
await fs3.writeFile(path3.join(this.cwd, "EVIDENCE.json"), JSON.stringify(evidence, null, 2));
|
|
614
|
+
}
|
|
615
|
+
return evidence;
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// src/core/validation/validation-planner.ts
|
|
620
|
+
import fs4 from "fs/promises";
|
|
621
|
+
import path4 from "path";
|
|
622
|
+
var ValidationPlanner = class {
|
|
623
|
+
cwd;
|
|
624
|
+
qualityGuard;
|
|
625
|
+
stackDetector;
|
|
626
|
+
constructor({ cwd = process.cwd(), qualityGuard }) {
|
|
627
|
+
this.cwd = cwd;
|
|
628
|
+
this.qualityGuard = qualityGuard;
|
|
629
|
+
this.stackDetector = new StackDetector({ cwd });
|
|
630
|
+
}
|
|
631
|
+
async plan(profile = "generic") {
|
|
632
|
+
const stacks = await this.stackDetector.detect();
|
|
633
|
+
const tasks = [];
|
|
634
|
+
const changedFiles = this.qualityGuard.getChangedFiles().filter((file) => {
|
|
635
|
+
return !/(^|\/)(node_modules|vendor|dist|coverage|\.cache)(\/|$)/.test(String(file).replaceAll("\\", "/"));
|
|
636
|
+
});
|
|
637
|
+
const executableBehavior = await this.qualityGuard.hasExecutableBehaviorChanges();
|
|
638
|
+
if (stacks.includes("node")) {
|
|
639
|
+
try {
|
|
640
|
+
const pkg = JSON.parse(await fs4.readFile(path4.join(this.cwd, "package.json"), "utf8"));
|
|
641
|
+
const scripts = pkg.scripts || {};
|
|
642
|
+
this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
|
|
643
|
+
this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"], { rejectNoOp: executableBehavior });
|
|
644
|
+
this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
|
|
645
|
+
this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
|
|
646
|
+
if (scripts.validate && !String(scripts.validate).includes("ai-workflow collect-evidence")) {
|
|
647
|
+
this.addNodeScriptTask(tasks, scripts, "validate", ["npm", "run", "validate"]);
|
|
648
|
+
}
|
|
649
|
+
const hasTs = changedFiles.some((file) => /\.(ts|tsx)$/.test(file)) || await fs4.access(path4.join(this.cwd, "tsconfig.json")).then(() => true).catch(() => false);
|
|
650
|
+
if (hasTs && !scripts.typecheck) {
|
|
651
|
+
tasks.push({
|
|
652
|
+
name: "typecheck",
|
|
653
|
+
command: ["npm", "run", "typecheck"],
|
|
654
|
+
kind: "typecheck",
|
|
655
|
+
presetStatus: "FAIL_QUALITY_GATE",
|
|
656
|
+
summary: "TypeScript detected but no typecheck script is configured."
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
} catch {
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
if (stacks.includes("python")) {
|
|
663
|
+
const pythonFiles = changedFiles.filter((f) => f.endsWith(".py"));
|
|
664
|
+
if (pythonFiles.length > 0) {
|
|
665
|
+
tasks.push({
|
|
666
|
+
name: "python-syntax",
|
|
667
|
+
command: ["python", "-m", "py_compile", ...pythonFiles],
|
|
668
|
+
kind: "lint"
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
if (executableBehavior) {
|
|
672
|
+
const hasPytestConfig = await fs4.access(path4.join(this.cwd, "pytest.ini")).then(() => true).catch(() => false) || await fs4.access(path4.join(this.cwd, "pyproject.toml")).then(() => true).catch(() => false);
|
|
673
|
+
if (hasPytestConfig || pythonFiles.length > 0) {
|
|
674
|
+
tasks.push({
|
|
675
|
+
name: "python-test",
|
|
676
|
+
command: ["pytest"],
|
|
677
|
+
kind: "test"
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (stacks.includes("php")) {
|
|
683
|
+
const phpFiles = changedFiles.filter((f) => f.endsWith(".php") || f.endsWith(".inc"));
|
|
684
|
+
if (phpFiles.length > 0) {
|
|
685
|
+
for (const file of phpFiles) {
|
|
686
|
+
tasks.push({
|
|
687
|
+
name: `php-syntax:${file}`,
|
|
688
|
+
command: ["php", "-l", file],
|
|
689
|
+
kind: "lint"
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (executableBehavior) {
|
|
694
|
+
const hasPhpUnitXml = await fs4.access(path4.join(this.cwd, "phpunit.xml")).then(() => true).catch(() => false) || await fs4.access(path4.join(this.cwd, "phpunit.xml.dist")).then(() => true).catch(() => false);
|
|
695
|
+
if (hasPhpUnitXml || phpFiles.length > 0) {
|
|
696
|
+
tasks.push({
|
|
697
|
+
name: "php-test",
|
|
698
|
+
command: ["vendor/bin/phpunit"],
|
|
699
|
+
kind: "test"
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return tasks;
|
|
705
|
+
}
|
|
706
|
+
addNodeScriptTask(tasks, scripts, name, command, { rejectNoOp = false } = {}) {
|
|
707
|
+
const script = scripts[name];
|
|
708
|
+
if (!script) return;
|
|
709
|
+
if (!rejectNoOp && name === "test" && this.isNoOpScript(script)) return;
|
|
710
|
+
if (rejectNoOp && this.isNoOpScript(script)) {
|
|
711
|
+
tasks.push({
|
|
712
|
+
name,
|
|
713
|
+
command,
|
|
714
|
+
kind: name === "test" ? "test" : name,
|
|
715
|
+
presetStatus: "FAIL_QUALITY_GATE",
|
|
716
|
+
summary: `Configured ${name} script is a no-op: ${script}`
|
|
717
|
+
});
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
tasks.push({ name, command, kind: name === "test" ? "test" : name });
|
|
721
|
+
}
|
|
722
|
+
isNoOpScript(script = "") {
|
|
723
|
+
const value = String(script).trim().toLowerCase();
|
|
724
|
+
return /^(echo\b|true$|exit\s+0$|node\s+-e\s+["']?console\.log)/.test(value) || value.includes("tests-pass");
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
export {
|
|
729
|
+
QualityGuard,
|
|
730
|
+
VisualVerifier,
|
|
731
|
+
EvidenceCollector,
|
|
732
|
+
ValidationPlanner
|
|
733
|
+
};
|
|
734
|
+
//# sourceMappingURL=chunk-6EMG6FAO.js.map
|