@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
|
@@ -0,0 +1,749 @@
|
|
|
1
|
+
// src/core/gates/merge-gate.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
var MergeGate = class {
|
|
4
|
+
cwd;
|
|
5
|
+
constructor({ cwd = process.cwd() } = {}) {
|
|
6
|
+
this.cwd = cwd;
|
|
7
|
+
}
|
|
8
|
+
runGit(command) {
|
|
9
|
+
return execSync(command, {
|
|
10
|
+
cwd: this.cwd,
|
|
11
|
+
encoding: "utf8",
|
|
12
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
13
|
+
}).trim();
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Checks the commit history of the repository/branch.
|
|
17
|
+
*/
|
|
18
|
+
check(branchName) {
|
|
19
|
+
try {
|
|
20
|
+
const logOutput = this.runGit('git log -n 50 --format="%H|%P|%s"');
|
|
21
|
+
const lines = logOutput.split("\n").filter(Boolean);
|
|
22
|
+
const commits = lines.map((line) => {
|
|
23
|
+
const parts = line.split("|");
|
|
24
|
+
const hash = parts[0] || "";
|
|
25
|
+
const parentsStr = parts[1] || "";
|
|
26
|
+
const parents = parentsStr.split(" ").filter(Boolean);
|
|
27
|
+
const subject = parts[2] || "";
|
|
28
|
+
return { hash, parents, subject };
|
|
29
|
+
});
|
|
30
|
+
for (const commit of commits) {
|
|
31
|
+
const isMergeSubject = /merge\b/i.test(commit.subject) || /pull request/i.test(commit.subject);
|
|
32
|
+
if (isMergeSubject && commit.parents.length < 2) {
|
|
33
|
+
return {
|
|
34
|
+
status: "FAIL_QUALITY_GATE",
|
|
35
|
+
reason: `Linear or fast-forward merge detected for commit '${commit.hash.slice(0, 7)}' ("${commit.subject}"). Merges into integration branches must use the non-fast-forward (--no-ff) strategy to preserve topology.`
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const isIntegrationBranch = ["main", "master"].includes(branchName) || String(branchName).startsWith("release");
|
|
40
|
+
if (isIntegrationBranch && commits.length > 5) {
|
|
41
|
+
const hasMergeCommit = commits.some((c) => c.parents.length >= 2);
|
|
42
|
+
if (!hasMergeCommit) {
|
|
43
|
+
return {
|
|
44
|
+
status: "FAIL_QUALITY_GATE",
|
|
45
|
+
reason: `Integration branch '${branchName}' has a purely linear commit history. Integrated branches must use non-fast-forward (--no-ff) merges to preserve feature branches.`
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { status: "PASS" };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return {
|
|
52
|
+
status: "ERROR",
|
|
53
|
+
reason: `Failed to check merge integrity: ${error.message}`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// src/core/validation/stack-detector.ts
|
|
60
|
+
import fs from "fs/promises";
|
|
61
|
+
import path from "path";
|
|
62
|
+
var StackDetector = class {
|
|
63
|
+
cwd;
|
|
64
|
+
constructor({ cwd = process.cwd() } = {}) {
|
|
65
|
+
this.cwd = cwd;
|
|
66
|
+
}
|
|
67
|
+
async detect() {
|
|
68
|
+
const stacks = [];
|
|
69
|
+
const packageJsonExists = await fs.access(path.join(this.cwd, "package.json")).then(() => true).catch(() => false);
|
|
70
|
+
if (packageJsonExists) {
|
|
71
|
+
stacks.push("node");
|
|
72
|
+
}
|
|
73
|
+
const pyprojectExists = await fs.access(path.join(this.cwd, "pyproject.toml")).then(() => true).catch(() => false) || await fs.access(path.join(this.cwd, "requirements.txt")).then(() => true).catch(() => false) || await fs.access(path.join(this.cwd, "setup.py")).then(() => true).catch(() => false) || await fs.access(path.join(this.cwd, "Pipfile")).then(() => true).catch(() => false);
|
|
74
|
+
let hasPythonFiles = pyprojectExists;
|
|
75
|
+
if (!hasPythonFiles) {
|
|
76
|
+
hasPythonFiles = await this.hasFileWithExtension(this.cwd, ".py");
|
|
77
|
+
}
|
|
78
|
+
if (hasPythonFiles) {
|
|
79
|
+
stacks.push("python");
|
|
80
|
+
}
|
|
81
|
+
const composerExists = await fs.access(path.join(this.cwd, "composer.json")).then(() => true).catch(() => false);
|
|
82
|
+
let hasPhpFiles = composerExists;
|
|
83
|
+
if (!hasPhpFiles) {
|
|
84
|
+
hasPhpFiles = await this.hasFileWithExtension(this.cwd, ".php");
|
|
85
|
+
}
|
|
86
|
+
if (hasPhpFiles) {
|
|
87
|
+
stacks.push("php");
|
|
88
|
+
}
|
|
89
|
+
return stacks;
|
|
90
|
+
}
|
|
91
|
+
async hasFileWithExtension(dir, ext) {
|
|
92
|
+
try {
|
|
93
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (entry.isDirectory()) {
|
|
96
|
+
if (["node_modules", "vendor", ".git", "dist", "build", ".cache"].includes(entry.name)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const subPath = path.join(dir, entry.name);
|
|
100
|
+
const found = await this.hasFileWithExtension(subPath, ext);
|
|
101
|
+
if (found) return true;
|
|
102
|
+
} else if (entry.isFile() && entry.name.endsWith(ext)) {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// src/core/validation/delivery-decision-engine.ts
|
|
113
|
+
import fs2 from "fs/promises";
|
|
114
|
+
import path2 from "path";
|
|
115
|
+
function parseRequest(request = "") {
|
|
116
|
+
const req = request.toLowerCase();
|
|
117
|
+
let targetStack = null;
|
|
118
|
+
if (/\b(vue|nuxt|quasar)\b/.test(req)) targetStack = "vue";
|
|
119
|
+
else if (/\b(react|next)\b/.test(req)) targetStack = "react";
|
|
120
|
+
else if (/\b(angular|svelte)\b/.test(req)) targetStack = "svelte-or-angular";
|
|
121
|
+
else if (/\b(laravel|django|rails|express|nest|fastify)\b/.test(req)) {
|
|
122
|
+
if (/\blaravel\b/.test(req)) targetStack = "laravel";
|
|
123
|
+
else targetStack = "backend";
|
|
124
|
+
} else if (/\b(wordpress|wp)\b/.test(req)) targetStack = "wordpress";
|
|
125
|
+
else if (/\b(cli|command|bin)\b/.test(req)) targetStack = "cli";
|
|
126
|
+
let intent = "implementation";
|
|
127
|
+
if (/\b(document|doc|readme)\b/.test(req) || /\b(create|add|write|update|edit)\b.*\b[\w.-]+\.md\b/.test(req)) intent = "docs";
|
|
128
|
+
else if (/\b(audit|security|inspect|dependencies)\b/.test(req)) intent = "audit";
|
|
129
|
+
else if (/\b(release|publish|deploy)\b/.test(req)) intent = "release";
|
|
130
|
+
else if (/\b(prototype|standalone html)\b/.test(req)) intent = "prototype";
|
|
131
|
+
return { targetStack, intent };
|
|
132
|
+
}
|
|
133
|
+
var DeliveryDecisionEngine = class {
|
|
134
|
+
cwd;
|
|
135
|
+
stackDetector;
|
|
136
|
+
constructor({ cwd = process.cwd() } = {}) {
|
|
137
|
+
this.cwd = cwd;
|
|
138
|
+
this.stackDetector = new StackDetector({ cwd });
|
|
139
|
+
}
|
|
140
|
+
async determineClass(changedFiles = []) {
|
|
141
|
+
if (changedFiles.length === 0) {
|
|
142
|
+
return "empty";
|
|
143
|
+
}
|
|
144
|
+
const isDocs = changedFiles.every(
|
|
145
|
+
(file) => file.endsWith(".md") || file.endsWith(".txt") || file.startsWith("docs/")
|
|
146
|
+
);
|
|
147
|
+
if (isDocs) {
|
|
148
|
+
return "docs-only";
|
|
149
|
+
}
|
|
150
|
+
const stacks = await this.stackDetector.detect();
|
|
151
|
+
let usesJsFramework = false;
|
|
152
|
+
try {
|
|
153
|
+
const pkgPath = path2.join(this.cwd, "package.json");
|
|
154
|
+
const pkgContent = await fs2.readFile(pkgPath, "utf8");
|
|
155
|
+
const pkg = JSON.parse(pkgContent);
|
|
156
|
+
const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
157
|
+
const frameworks = ["react", "next", "vue", "nuxt", "svelte", "solid-js", "preact", "quasar", "@angular/core"];
|
|
158
|
+
usesJsFramework = frameworks.some((fw) => deps[fw] !== void 0);
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
const isWordPress = await checkWordPressDirectRoot(this.cwd);
|
|
162
|
+
const hasFrontendFiles = changedFiles.some(
|
|
163
|
+
(file) => /\.(html?|css|scss|sass|less|jsx|tsx|vue|svelte)$/i.test(file) || /\.js$/i.test(file) && !file.includes("config") && !file.includes("test")
|
|
164
|
+
);
|
|
165
|
+
if (isWordPress) {
|
|
166
|
+
return "wordpress-php";
|
|
167
|
+
}
|
|
168
|
+
if (usesJsFramework && hasFrontendFiles) {
|
|
169
|
+
return "framework-native-frontend";
|
|
170
|
+
}
|
|
171
|
+
if (hasFrontendFiles) {
|
|
172
|
+
return "standalone-web";
|
|
173
|
+
}
|
|
174
|
+
if (stacks.includes("python") || stacks.includes("php") || stacks.includes("node") && !usesJsFramework) {
|
|
175
|
+
return "backend-service";
|
|
176
|
+
}
|
|
177
|
+
return "generic-code";
|
|
178
|
+
}
|
|
179
|
+
async detectContext() {
|
|
180
|
+
const stacks = await this.stackDetector.detect();
|
|
181
|
+
let usesJsFramework = null;
|
|
182
|
+
let hasPackageJson = false;
|
|
183
|
+
let isCli = false;
|
|
184
|
+
try {
|
|
185
|
+
const pkgPath = path2.join(this.cwd, "package.json");
|
|
186
|
+
const pkgContent = await fs2.readFile(pkgPath, "utf8");
|
|
187
|
+
hasPackageJson = true;
|
|
188
|
+
const pkg = JSON.parse(pkgContent);
|
|
189
|
+
const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
190
|
+
if (pkg.bin || pkg.name === "@williambeto/ai-workflow") {
|
|
191
|
+
isCli = true;
|
|
192
|
+
}
|
|
193
|
+
const frameworks = ["react", "next", "vue", "nuxt", "svelte", "solid-js", "preact", "quasar", "@angular/core"];
|
|
194
|
+
for (const fw of frameworks) {
|
|
195
|
+
if (deps[fw] !== void 0) {
|
|
196
|
+
usesJsFramework = fw;
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
let isLaravel = false;
|
|
203
|
+
let hasComposerJson = false;
|
|
204
|
+
try {
|
|
205
|
+
const composerPath = path2.join(this.cwd, "composer.json");
|
|
206
|
+
const composerContent = await fs2.readFile(composerPath, "utf8");
|
|
207
|
+
hasComposerJson = true;
|
|
208
|
+
const composer = JSON.parse(composerContent);
|
|
209
|
+
const reqs = { ...composer.require || {}, ...composer.requireDev || {} };
|
|
210
|
+
if (reqs["laravel/framework"]) {
|
|
211
|
+
isLaravel = true;
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
const isWordPress = await checkWordPressDirectRoot(this.cwd);
|
|
216
|
+
let isEmpty = true;
|
|
217
|
+
try {
|
|
218
|
+
const entries = await fs2.readdir(this.cwd);
|
|
219
|
+
const meaningful = entries.filter((e) => !e.startsWith(".") && !["node_modules", "EVIDENCE.json"].includes(e));
|
|
220
|
+
if (meaningful.length > 0) {
|
|
221
|
+
if (meaningful.length === 1 && meaningful[0] === "package.json") {
|
|
222
|
+
try {
|
|
223
|
+
const content = await fs2.readFile(path2.join(this.cwd, "package.json"), "utf8");
|
|
224
|
+
const pkg = JSON.parse(content);
|
|
225
|
+
if (!pkg.dependencies && !pkg.devDependencies) {
|
|
226
|
+
isEmpty = true;
|
|
227
|
+
} else {
|
|
228
|
+
isEmpty = false;
|
|
229
|
+
}
|
|
230
|
+
} catch {
|
|
231
|
+
isEmpty = true;
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
isEmpty = false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
isEmpty = true;
|
|
239
|
+
}
|
|
240
|
+
let projectState = isEmpty ? "empty" : "active";
|
|
241
|
+
let detectedStackType = "unknown";
|
|
242
|
+
let detectedStacks = [];
|
|
243
|
+
if (isWordPress) {
|
|
244
|
+
detectedStackType = "WordPress context";
|
|
245
|
+
detectedStacks.push("wordpress");
|
|
246
|
+
} else if (isLaravel) {
|
|
247
|
+
detectedStackType = "backend context";
|
|
248
|
+
detectedStacks.push("laravel");
|
|
249
|
+
} else if (usesJsFramework) {
|
|
250
|
+
detectedStackType = "existing matching stack";
|
|
251
|
+
detectedStacks.push(usesJsFramework);
|
|
252
|
+
} else if (isCli) {
|
|
253
|
+
detectedStackType = "CLI package context";
|
|
254
|
+
detectedStacks.push("node");
|
|
255
|
+
} else if (stacks.includes("node")) {
|
|
256
|
+
detectedStackType = "existing matching stack";
|
|
257
|
+
detectedStacks.push("node");
|
|
258
|
+
} else if (stacks.includes("python") || stacks.includes("php")) {
|
|
259
|
+
detectedStackType = "backend context";
|
|
260
|
+
detectedStacks.push(...stacks);
|
|
261
|
+
} else if (projectState === "empty") {
|
|
262
|
+
detectedStackType = "empty project";
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
projectState,
|
|
266
|
+
detectedStackType,
|
|
267
|
+
detectedStacks,
|
|
268
|
+
isWordPress,
|
|
269
|
+
isLaravel,
|
|
270
|
+
usesJsFramework,
|
|
271
|
+
isCli
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
async determineDecision(input = {}) {
|
|
275
|
+
const userRequest = input.userRequest || "";
|
|
276
|
+
const explicitApprovals = input.explicitApprovals || [];
|
|
277
|
+
const ctx = input.projectContext || await this.detectContext();
|
|
278
|
+
const projectState = ctx.projectState || "active";
|
|
279
|
+
const detectedStacks = ctx.detectedStacks || [];
|
|
280
|
+
const isWordPress = ctx.isWordPress || false;
|
|
281
|
+
const isLaravel = ctx.isLaravel || false;
|
|
282
|
+
const isCli = ctx.isCli || false;
|
|
283
|
+
const usesJsFramework = ctx.usesJsFramework || null;
|
|
284
|
+
const parsed = parseRequest(userRequest);
|
|
285
|
+
const targetStack = parsed.targetStack;
|
|
286
|
+
const intent = parsed.intent;
|
|
287
|
+
let decision = "IMPLEMENT_IN_EXISTING_CONTEXT";
|
|
288
|
+
let allowedArtifactShape = "generic-code";
|
|
289
|
+
let forbiddenSubstitutes = ["success-without-validation"];
|
|
290
|
+
let validationRequired = ["build-or-equivalent"];
|
|
291
|
+
let approvalRequired = false;
|
|
292
|
+
if (intent === "audit") {
|
|
293
|
+
decision = "READ_ONLY_AUDIT";
|
|
294
|
+
allowedArtifactShape = "audit-report";
|
|
295
|
+
forbiddenSubstitutes.push("any-code-mutation");
|
|
296
|
+
validationRequired = [];
|
|
297
|
+
} else if (intent === "docs") {
|
|
298
|
+
decision = "DOCS_ONLY";
|
|
299
|
+
allowedArtifactShape = "markdown";
|
|
300
|
+
forbiddenSubstitutes.push("unjustified-code-mutation");
|
|
301
|
+
validationRequired = [];
|
|
302
|
+
} else if (intent === "release") {
|
|
303
|
+
decision = "RELEASE_OR_DEPLOY_REQUIRES_APPROVAL";
|
|
304
|
+
allowedArtifactShape = "package-tarball";
|
|
305
|
+
forbiddenSubstitutes.push("publish-without-approval");
|
|
306
|
+
validationRequired = ["release-readiness-or-equivalent"];
|
|
307
|
+
const hasApproval = explicitApprovals.includes("npm-publish") || explicitApprovals.includes("deploy-production");
|
|
308
|
+
if (hasApproval) {
|
|
309
|
+
decision = "RELEASE_OR_DEPLOY_APPROVED";
|
|
310
|
+
approvalRequired = true;
|
|
311
|
+
}
|
|
312
|
+
} else if (intent === "prototype") {
|
|
313
|
+
decision = "CREATE_STANDALONE_PROTOTYPE";
|
|
314
|
+
allowedArtifactShape = "standalone-html";
|
|
315
|
+
validationRequired = [];
|
|
316
|
+
} else if (projectState === "empty") {
|
|
317
|
+
decision = "SCAFFOLD_AND_IMPLEMENT";
|
|
318
|
+
if (targetStack === "vue" || targetStack === "react") {
|
|
319
|
+
allowedArtifactShape = "runnable-framework-app";
|
|
320
|
+
forbiddenSubstitutes.push("standalone-html-as-framework-delivery", "loose-framework-files-without-runtime");
|
|
321
|
+
validationRequired.push("dependency-install-or-verification", "runtime-smoke-or-equivalent");
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
const hasConflict = targetStack === "vue" && detectedStacks.includes("react") || targetStack === "react" && detectedStacks.includes("vue") || targetStack === "laravel" && !detectedStacks.includes("laravel") && detectedStacks.length > 0 || targetStack === "wordpress" && !detectedStacks.includes("wordpress") && detectedStacks.length > 0;
|
|
325
|
+
if (hasConflict) {
|
|
326
|
+
decision = "BLOCK_STACK_CONFLICT";
|
|
327
|
+
} else if (targetStack === "laravel" && !detectedStacks.includes("laravel")) {
|
|
328
|
+
decision = "BLOCK_UNSUPPORTED_CONTEXT";
|
|
329
|
+
} else if (targetStack === "wordpress" && !detectedStacks.includes("wordpress")) {
|
|
330
|
+
decision = "BLOCK_UNSUPPORTED_CONTEXT";
|
|
331
|
+
} else {
|
|
332
|
+
if (targetStack === "vue" || targetStack === "react") {
|
|
333
|
+
decision = "IMPLEMENT_IN_EXISTING_CONTEXT";
|
|
334
|
+
allowedArtifactShape = "runnable-framework-app";
|
|
335
|
+
forbiddenSubstitutes.push("standalone-html-as-framework-delivery", "loose-framework-files-without-runtime");
|
|
336
|
+
validationRequired.push("dependency-install-or-verification", "runtime-smoke-or-equivalent");
|
|
337
|
+
} else if (targetStack === "cli" || isCli) {
|
|
338
|
+
decision = "CLI_COMMAND_INTEGRATION";
|
|
339
|
+
allowedArtifactShape = "bin-entry";
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (userRequest && userRequest.trim().length < 3) {
|
|
344
|
+
decision = "BLOCK_UNCLEAR_SCOPE";
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
intent: intent === "implementation" ? "implementation" : intent,
|
|
348
|
+
requestedOutcome: targetStack === "vue" || targetStack === "react" ? "page" : targetStack === "wordpress" ? "plugin-or-theme" : null,
|
|
349
|
+
requestedStack: targetStack,
|
|
350
|
+
requestedPlatform: targetStack === "wordpress" ? "wordpress" : null,
|
|
351
|
+
projectState,
|
|
352
|
+
detectedStacks,
|
|
353
|
+
decision,
|
|
354
|
+
allowedArtifactShape,
|
|
355
|
+
forbiddenSubstitutes,
|
|
356
|
+
validationRequired,
|
|
357
|
+
approvalRequired: explicitApprovals.length > 0
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
async function checkWordPressDirectRoot(cwd) {
|
|
362
|
+
const hasWpConfig = await fs2.access(path2.join(cwd, "wp-config.php")).then(() => true).catch(() => false);
|
|
363
|
+
const hasWpContent = await fs2.access(path2.join(cwd, "wp-content")).then(() => true).catch(() => false);
|
|
364
|
+
if (hasWpConfig || hasWpContent) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
const hasStyleCss = await fs2.access(path2.join(cwd, "style.css")).then(() => true).catch(() => false);
|
|
368
|
+
if (hasStyleCss) {
|
|
369
|
+
try {
|
|
370
|
+
const content = await fs2.readFile(path2.join(cwd, "style.css"), "utf8");
|
|
371
|
+
if (/\bTheme Name\s*:/i.test(content) || /\bTheme URI\s*:/i.test(content)) {
|
|
372
|
+
const otherIndicators = [
|
|
373
|
+
"functions.php",
|
|
374
|
+
"theme.json",
|
|
375
|
+
"index.php",
|
|
376
|
+
"front-page.php",
|
|
377
|
+
"single.php",
|
|
378
|
+
"page.php",
|
|
379
|
+
"archive.php",
|
|
380
|
+
"template-parts"
|
|
381
|
+
];
|
|
382
|
+
for (const ind of otherIndicators) {
|
|
383
|
+
const exists = await fs2.access(path2.join(cwd, ind)).then(() => true).catch(() => false);
|
|
384
|
+
if (exists) {
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
const files = await fs2.readdir(cwd);
|
|
394
|
+
for (const file of files) {
|
|
395
|
+
if (file.endsWith(".php")) {
|
|
396
|
+
const content = await fs2.readFile(path2.join(cwd, file), "utf8");
|
|
397
|
+
if (/\bPlugin Name\s*:/i.test(content)) {
|
|
398
|
+
return true;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/core/validation/artifact-fidelity-gate.ts
|
|
408
|
+
import fs3 from "fs/promises";
|
|
409
|
+
import path3 from "path";
|
|
410
|
+
var ArtifactFidelityGate = class {
|
|
411
|
+
cwd;
|
|
412
|
+
deliveryClass;
|
|
413
|
+
userRequest;
|
|
414
|
+
explicitApprovals;
|
|
415
|
+
deliveryDecision;
|
|
416
|
+
projectContext;
|
|
417
|
+
constructor({ cwd = process.cwd(), deliveryClass, userRequest, explicitApprovals = [] } = {}) {
|
|
418
|
+
this.cwd = cwd;
|
|
419
|
+
this.deliveryClass = deliveryClass;
|
|
420
|
+
this.userRequest = userRequest;
|
|
421
|
+
this.explicitApprovals = explicitApprovals;
|
|
422
|
+
}
|
|
423
|
+
async verify(changedFiles = []) {
|
|
424
|
+
if (changedFiles.length === 0) {
|
|
425
|
+
return { passed: true };
|
|
426
|
+
}
|
|
427
|
+
if (this.deliveryClass === "framework-native-frontend") {
|
|
428
|
+
for (const file of changedFiles) {
|
|
429
|
+
const ext = path3.extname(file).toLowerCase();
|
|
430
|
+
if (ext === ".html" || ext === ".htm") {
|
|
431
|
+
const basename = path3.basename(file).toLowerCase();
|
|
432
|
+
const isSpaEntry = basename === "index.html" && (file === "index.html" || file === "public/index.html" || file === "src/index.html");
|
|
433
|
+
if (!isSpaEntry) {
|
|
434
|
+
return {
|
|
435
|
+
passed: false,
|
|
436
|
+
reason: `Fidelity Violation: standalone HTML file '${file}' is not permitted in a framework-native frontend project. UI must be implemented within framework components (e.g. React/Vue/Next/Nuxt files) instead of static prototypes.`
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
const content = await fs3.readFile(path3.join(this.cwd, file), "utf8");
|
|
441
|
+
if (content.includes("unpkg.com") || content.includes("cdnjs.cloudflare.com") || content.includes("cdn.jsdelivr.net")) {
|
|
442
|
+
if (content.includes("vue") || content.includes("react")) {
|
|
443
|
+
return {
|
|
444
|
+
passed: false,
|
|
445
|
+
reason: `Fidelity Violation: SPA entry point '${file}' imports framework library via CDN scripts. Framework dependencies must be installed and resolved locally via npm/package.json.`
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} catch {
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (this.deliveryClass === "wordpress-php") {
|
|
455
|
+
for (const file of changedFiles) {
|
|
456
|
+
const ext = path3.extname(file).toLowerCase();
|
|
457
|
+
if (ext === ".html" || ext === ".htm") {
|
|
458
|
+
return {
|
|
459
|
+
passed: false,
|
|
460
|
+
reason: `Fidelity Violation: standalone HTML file '${file}' is not permitted in a WordPress project. UI templates must be implemented in PHP files within theme or plugin structures.`
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
const frontendViolations = await this.verifyFrontendFidelity(changedFiles);
|
|
466
|
+
if (frontendViolations && frontendViolations.length > 0) {
|
|
467
|
+
return {
|
|
468
|
+
passed: false,
|
|
469
|
+
reason: frontendViolations.join("; ")
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
return { passed: true };
|
|
473
|
+
}
|
|
474
|
+
async verifyRequestFidelity(arg) {
|
|
475
|
+
let userRequest = this.userRequest || "";
|
|
476
|
+
let deliveryDecision = this.deliveryDecision || {};
|
|
477
|
+
let projectContext = this.projectContext || {};
|
|
478
|
+
let changedFiles = [];
|
|
479
|
+
let explicitApprovals = this.explicitApprovals || [];
|
|
480
|
+
if (Array.isArray(arg)) {
|
|
481
|
+
changedFiles = arg;
|
|
482
|
+
} else if (arg && typeof arg === "object") {
|
|
483
|
+
userRequest = arg.userRequest || userRequest;
|
|
484
|
+
deliveryDecision = arg.deliveryDecision || deliveryDecision;
|
|
485
|
+
projectContext = arg.projectContext || projectContext;
|
|
486
|
+
changedFiles = arg.changedFiles || changedFiles;
|
|
487
|
+
explicitApprovals = arg.explicitApprovals || explicitApprovals;
|
|
488
|
+
}
|
|
489
|
+
const blockingDecisions = /* @__PURE__ */ new Set([
|
|
490
|
+
"BLOCK_STACK_CONFLICT",
|
|
491
|
+
"BLOCK_UNCLEAR_SCOPE",
|
|
492
|
+
"BLOCK_UNSUPPORTED_CONTEXT",
|
|
493
|
+
"REQUIRE_EXPLICIT_AUTHORIZATION",
|
|
494
|
+
"RELEASE_OR_DEPLOY_REQUIRES_APPROVAL"
|
|
495
|
+
]);
|
|
496
|
+
if (blockingDecisions.has(deliveryDecision?.decision)) {
|
|
497
|
+
const filtered = changedFiles.filter(
|
|
498
|
+
(file) => !file.startsWith(".ai-workflow/") && !file.startsWith(".git/") && file !== "EVIDENCE.json" && file !== "EVIDENCE.temp.json"
|
|
499
|
+
);
|
|
500
|
+
return {
|
|
501
|
+
status: "BLOCKED",
|
|
502
|
+
passed: false,
|
|
503
|
+
reason: `Delivery decision is blocking: ${deliveryDecision.decision}`,
|
|
504
|
+
decision: deliveryDecision.decision,
|
|
505
|
+
checkedFiles: filtered,
|
|
506
|
+
violations: [`Blocking delivery decision: ${deliveryDecision.decision}`]
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const filteredFiles = changedFiles.filter(
|
|
510
|
+
(file) => !file.startsWith(".ai-workflow/") && !file.startsWith(".git/") && file !== "EVIDENCE.json" && file !== "EVIDENCE.temp.json"
|
|
511
|
+
);
|
|
512
|
+
const req = userRequest.toLowerCase();
|
|
513
|
+
const isReadOnly = /(inspect|audit|read-only|dependencies)/.test(req);
|
|
514
|
+
const isDocsOnly = /(document|doc|readme)/.test(req) && !req.includes("code");
|
|
515
|
+
const isRelease = /(release|publish|deploy)/.test(req);
|
|
516
|
+
const violations = [];
|
|
517
|
+
if (isReadOnly && filteredFiles.length > 0) {
|
|
518
|
+
violations.push("Read-only audit task mutated repository files mismatch.");
|
|
519
|
+
}
|
|
520
|
+
if (isDocsOnly) {
|
|
521
|
+
const codeChanged = filteredFiles.some(
|
|
522
|
+
(file) => !file.endsWith(".md") && !file.endsWith(".txt") && !file.startsWith("docs/")
|
|
523
|
+
);
|
|
524
|
+
if (codeChanged) {
|
|
525
|
+
violations.push("Documentation-only task mutated codebase files without justification mismatch.");
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
if (isRelease) {
|
|
529
|
+
const approved = explicitApprovals.includes("npm-publish") || explicitApprovals.includes("deploy-production");
|
|
530
|
+
if (!approved) {
|
|
531
|
+
violations.push("Release/deploy task proceeds without explicit approval token mismatch.");
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const reqReact = /(react|next)/.test(req);
|
|
535
|
+
const reqVue = /(vue|nuxt)/.test(req);
|
|
536
|
+
const reqBackend = /(laravel|django|rails|express|nest|fastify|flask)/.test(req);
|
|
537
|
+
const reqWp = /(wordpress|wp)/.test(req);
|
|
538
|
+
const reqCli = /(cli|command)/.test(req);
|
|
539
|
+
if (reqReact && this.deliveryClass === "standalone-web" && !/prototype/.test(req)) {
|
|
540
|
+
violations.push("React framework page requested, but a standalone HTML/CSS substitute mismatch was delivered instead.");
|
|
541
|
+
}
|
|
542
|
+
if (reqVue && this.deliveryClass === "standalone-web" && !/prototype/.test(req)) {
|
|
543
|
+
violations.push("Vue framework page requested, but a standalone HTML/CSS substitute mismatch was delivered instead.");
|
|
544
|
+
}
|
|
545
|
+
if (reqBackend && filteredFiles.length > 0) {
|
|
546
|
+
const isLaravel = /\blaravel\b/i.test(req);
|
|
547
|
+
const isDjango = /\bdjango\b/i.test(req);
|
|
548
|
+
const isExpressNestFastify = /\b(express|nest|fastify)\b/i.test(req);
|
|
549
|
+
const isRails = /\brails\b/i.test(req);
|
|
550
|
+
if (isLaravel) {
|
|
551
|
+
const isLaravelArtifact = filteredFiles.some(
|
|
552
|
+
(file) => file === "composer.json" || file === "artisan" || file.startsWith("routes/") || file.startsWith("app/Http/Controllers/") || file.startsWith("app/Models/") || file.startsWith("app/Services/") || file.startsWith("tests/Feature/") || file.startsWith("tests/Unit/") || /(^|\/)(routes\/.*\.php|app\/Http\/Controllers\/.*\.php|app\/Models\/.*\.php|app\/Services\/.*\.php|tests\/Feature\/.*\.php|tests\/Unit\/.*\.php|artisan|composer\.json)$/.test(file)
|
|
553
|
+
);
|
|
554
|
+
if (!isLaravelArtifact) {
|
|
555
|
+
violations.push("Laravel endpoint/scaffold requested, but no valid Laravel files/structure mismatch was delivered.");
|
|
556
|
+
}
|
|
557
|
+
} else if (isDjango) {
|
|
558
|
+
const isDjangoArtifact = filteredFiles.some(
|
|
559
|
+
(file) => file === "manage.py" || file === "requirements.txt" || file === "pyproject.toml" || file.endsWith("settings.py") || file.endsWith("urls.py") || file.endsWith("views.py") || file.endsWith("models.py") || file.endsWith("tests.py") || /(^|\/)(manage\.py|settings\.py|urls\.py|views\.py|models\.py|tests\.py|requirements\.txt|pyproject\.toml)$/.test(file)
|
|
560
|
+
);
|
|
561
|
+
if (!isDjangoArtifact) {
|
|
562
|
+
violations.push("Django backend requested, but no valid Django files/structure mismatch was delivered.");
|
|
563
|
+
}
|
|
564
|
+
} else if (isExpressNestFastify) {
|
|
565
|
+
const isExpressNestFastifyArtifact = filteredFiles.some(
|
|
566
|
+
(file) => file === "package.json" || file.startsWith("src/routes/") || file.startsWith("src/controllers/") || file.startsWith("src/services/") || file.startsWith("tests/") || /(^|\/)(package\.json|src\/routes\/.*|src\/controllers\/.*|src\/services\/.*|tests\/.*|server\.[a-z]+|app\.[a-z]+|main\.[a-z]+)$/.test(file)
|
|
567
|
+
);
|
|
568
|
+
if (!isExpressNestFastifyArtifact) {
|
|
569
|
+
violations.push("Express/Fastify/Nest backend requested, but no valid Node.js server files/structure mismatch was delivered.");
|
|
570
|
+
}
|
|
571
|
+
} else if (isRails) {
|
|
572
|
+
const isRailsArtifact = filteredFiles.some(
|
|
573
|
+
(file) => file === "Gemfile" || file === "config/routes.rb" || file.startsWith("app/controllers/") || file.startsWith("app/models/") || file.startsWith("app/services/") || file.startsWith("test/") || file.startsWith("spec/") || /(^|\/)(Gemfile|config\/routes\.rb|app\/controllers\/.*|app\/models\/.*|app\/services\/.*|test\/.*|spec\/.*)$/.test(file)
|
|
574
|
+
);
|
|
575
|
+
if (!isRailsArtifact) {
|
|
576
|
+
violations.push("Ruby on Rails backend requested, but no valid Rails files/structure mismatch was delivered.");
|
|
577
|
+
}
|
|
578
|
+
} else {
|
|
579
|
+
const isGenericBackendArtifact = filteredFiles.some(
|
|
580
|
+
(file) => !file.includes("plain_script.py") && (file.startsWith("app/") || file.startsWith("routes/") || file.startsWith("config/") || file.startsWith("src/controllers/") || file.startsWith("src/routes/") || file.startsWith("src/services/") || file.startsWith("backend/") || file.startsWith("server/") || file.startsWith("api/") || file.endsWith(".py") || file.endsWith(".php") || file.endsWith(".rb") || file.endsWith(".go"))
|
|
581
|
+
);
|
|
582
|
+
if (!isGenericBackendArtifact) {
|
|
583
|
+
violations.push("Backend endpoint requested, but wrong backend artifact structure mismatch was delivered.");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (reqCli && filteredFiles.length > 0) {
|
|
588
|
+
const isCliArtifact = filteredFiles.some(
|
|
589
|
+
(file) => file.startsWith("bin/") || file.startsWith("src/commands/")
|
|
590
|
+
);
|
|
591
|
+
if (!isCliArtifact) {
|
|
592
|
+
violations.push("CLI command requested, but a loose script outside the executable bin/commands configuration mismatch was delivered.");
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (reqWp && filteredFiles.length > 0) {
|
|
596
|
+
let isWpArtifact = false;
|
|
597
|
+
for (const file of filteredFiles) {
|
|
598
|
+
const basename = path3.basename(file);
|
|
599
|
+
const ext = path3.extname(file).toLowerCase();
|
|
600
|
+
if (file.startsWith("wp-content/") || file === "wp-config.php") {
|
|
601
|
+
isWpArtifact = true;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
if (basename === "functions.php" || basename === "front-page.php" || basename === "single.php" || basename === "page.php" || basename === "archive.php" || basename === "theme.json" || file.startsWith("template-parts/") || file.includes("/template-parts/")) {
|
|
605
|
+
isWpArtifact = true;
|
|
606
|
+
break;
|
|
607
|
+
}
|
|
608
|
+
if (basename === "style.css") {
|
|
609
|
+
const hasHeader = await hasWpThemeHeader(this.cwd, file);
|
|
610
|
+
if (hasHeader) {
|
|
611
|
+
isWpArtifact = true;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (basename === "index.php") {
|
|
616
|
+
isWpArtifact = true;
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
if (basename === "uninstall.php" || basename === "composer.json" || file.startsWith("includes/") || file.includes("/includes/") || file.startsWith("admin/") || file.includes("/admin/") || file.startsWith("public/") || file.includes("/public/") || file.startsWith("src/") || file.includes("/src/")) {
|
|
620
|
+
isWpArtifact = true;
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
if (ext === ".php") {
|
|
624
|
+
const hasHeader = await hasWpPluginHeader(this.cwd, file);
|
|
625
|
+
if (hasHeader) {
|
|
626
|
+
isWpArtifact = true;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const hasHtml = filteredFiles.some((file) => path3.extname(file).toLowerCase() === ".html" || path3.extname(file).toLowerCase() === ".htm");
|
|
632
|
+
if (!isWpArtifact || hasHtml) {
|
|
633
|
+
violations.push("WordPress plugin or theme requested, but files were delivered outside the WordPress structure or contained standalone HTML.");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const frontendViolations = await this.verifyFrontendFidelity(filteredFiles);
|
|
637
|
+
if (frontendViolations && frontendViolations.length > 0) {
|
|
638
|
+
violations.push(...frontendViolations);
|
|
639
|
+
}
|
|
640
|
+
const status = violations.length > 0 ? "BLOCKED" : "PASS";
|
|
641
|
+
return {
|
|
642
|
+
status,
|
|
643
|
+
passed: status === "PASS",
|
|
644
|
+
reason: violations.length > 0 ? `Fidelity violations: ${violations.join("; ")}` : "Verification passed",
|
|
645
|
+
decision: deliveryDecision.decision || this.deliveryClass,
|
|
646
|
+
checkedFiles: filteredFiles,
|
|
647
|
+
violations
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
async verifyFrontendFidelity(changedFiles) {
|
|
651
|
+
const skillPaths = [
|
|
652
|
+
path3.join(this.cwd, ".agents/skills/frontend-design-system/SKILL.md"),
|
|
653
|
+
path3.join(this.cwd, ".agents/skills/frontend_design_system/SKILL.md"),
|
|
654
|
+
path3.join(this.cwd, ".ai-workflow/opencode/skills/frontend-design-system/SKILL.md"),
|
|
655
|
+
path3.join(this.cwd, ".ai-workflow/opencode/skills/frontend_design_system/SKILL.md"),
|
|
656
|
+
path3.join(this.cwd, "opencode/skills/frontend-design-system/SKILL.md"),
|
|
657
|
+
path3.join(this.cwd, "opencode/skills/frontend_design_system/SKILL.md")
|
|
658
|
+
];
|
|
659
|
+
let hasDesignSystemSkill = false;
|
|
660
|
+
for (const p of skillPaths) {
|
|
661
|
+
const exists = await fs3.access(p).then(() => true).catch(() => false);
|
|
662
|
+
if (exists) {
|
|
663
|
+
hasDesignSystemSkill = true;
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (!hasDesignSystemSkill) {
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
const uiFiles = changedFiles.filter(
|
|
671
|
+
(file) => /\.(tsx|jsx|vue|html|css|scss|less|blade\.php)$/.test(file)
|
|
672
|
+
);
|
|
673
|
+
if (uiFiles.length === 0) return null;
|
|
674
|
+
const violations = [];
|
|
675
|
+
let cssContentCombined = "";
|
|
676
|
+
let htmlContentCombined = "";
|
|
677
|
+
let styleAttrsCount = 0;
|
|
678
|
+
for (const file of uiFiles) {
|
|
679
|
+
try {
|
|
680
|
+
const fullPath = path3.join(this.cwd, file);
|
|
681
|
+
const content = await fs3.readFile(fullPath, "utf8");
|
|
682
|
+
if (/\.(css|scss|less|vue|tsx|jsx)$/.test(file)) {
|
|
683
|
+
cssContentCombined += content;
|
|
684
|
+
}
|
|
685
|
+
if (/\.(html|vue|tsx|jsx|blade\.php)$/.test(file)) {
|
|
686
|
+
htmlContentCombined += content;
|
|
687
|
+
const inlineStyles = content.match(/style\s*=\s*["']/g) || [];
|
|
688
|
+
styleAttrsCount += inlineStyles.length;
|
|
689
|
+
if (/(unpkg\.com|cdnjs\.cloudflare\.com|cdn\.jsdelivr\.net|tailwindcss|bootstrap)/i.test(content)) {
|
|
690
|
+
const isApproved = (this.explicitApprovals || []).some(
|
|
691
|
+
(app) => /cdn-styles|tailwind|bootstrap/i.test(app)
|
|
692
|
+
);
|
|
693
|
+
if (!isApproved) {
|
|
694
|
+
violations.push(`Fidelity Violation: standalone external CDN style/library dependency detected in '${file}' without explicit authorization.`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
} catch {
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
const hasRules = cssContentCombined.includes("{");
|
|
702
|
+
const cssVarsApproved = (this.explicitApprovals || []).some(
|
|
703
|
+
(app) => /no-css-vars|css-vars-bypass/i.test(app)
|
|
704
|
+
);
|
|
705
|
+
const cssRulesContent = cssContentCombined.replace(/\/\*[\s\S]*?\*\//g, "").trim();
|
|
706
|
+
const isSubstantialCss = cssRulesContent.length > 200;
|
|
707
|
+
const hasCssVars = /(--[a-zA-Z0-9_-]+\s*:|var\(--[a-zA-Z0-9_-]+\))/g.test(cssContentCombined);
|
|
708
|
+
if (hasRules && isSubstantialCss && !hasCssVars && !cssVarsApproved) {
|
|
709
|
+
violations.push("Fidelity Violation: Design System token mismatch. No CSS custom properties (variables) detected or used in changed UI stylesheets. Add explicit approval 'no-css-vars' to bypass this check for simple or legacy stylesheets.");
|
|
710
|
+
}
|
|
711
|
+
if (styleAttrsCount > 5) {
|
|
712
|
+
violations.push(`Fidelity Violation: excessive inline styles detected (${styleAttrsCount} style="..." declarations). Use CSS classes or utility classes instead.`);
|
|
713
|
+
}
|
|
714
|
+
if (htmlContentCombined.length > 0) {
|
|
715
|
+
const hasSemanticTags = /<(header|main|footer|nav|section)\b/i.test(htmlContentCombined);
|
|
716
|
+
const hasH1 = /<h1\b/i.test(htmlContentCombined);
|
|
717
|
+
if (!hasSemanticTags && !hasH1) {
|
|
718
|
+
violations.push("Fidelity Violation: HTML layout structure lacks basic semantic elements (<header>, <main>, <footer>, <nav>, <section>) or <h1> tag.");
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return violations;
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
async function hasWpThemeHeader(cwd, file) {
|
|
725
|
+
try {
|
|
726
|
+
const fullPath = path3.join(cwd, file);
|
|
727
|
+
const content = await fs3.readFile(fullPath, "utf8");
|
|
728
|
+
return /\bTheme Name\s*:/i.test(content) || /\bTheme URI\s*:/i.test(content);
|
|
729
|
+
} catch {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async function hasWpPluginHeader(cwd, file) {
|
|
734
|
+
try {
|
|
735
|
+
const fullPath = path3.join(cwd, file);
|
|
736
|
+
const content = await fs3.readFile(fullPath, "utf8");
|
|
737
|
+
return /\bPlugin Name\s*:/i.test(content) || /\bDescription\s*:/i.test(content);
|
|
738
|
+
} catch {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export {
|
|
744
|
+
MergeGate,
|
|
745
|
+
StackDetector,
|
|
746
|
+
DeliveryDecisionEngine,
|
|
747
|
+
ArtifactFidelityGate
|
|
748
|
+
};
|
|
749
|
+
//# sourceMappingURL=chunk-2AOV2ATY.js.map
|