@williambeto/ai-workflow 2.3.6 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,11 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
4
  export class ArtifactFidelityGate {
5
- constructor({ cwd = process.cwd(), deliveryClass } = {}) {
5
+ constructor({ cwd = process.cwd(), deliveryClass, userRequest, explicitApprovals } = {}) {
6
6
  this.cwd = cwd;
7
7
  this.deliveryClass = deliveryClass;
8
+ this.userRequest = userRequest;
9
+ this.explicitApprovals = explicitApprovals;
8
10
  }
9
11
 
10
12
  async verify(changedFiles = []) {
@@ -16,7 +18,6 @@ export class ArtifactFidelityGate {
16
18
  for (const file of changedFiles) {
17
19
  const ext = path.extname(file).toLowerCase();
18
20
 
19
- // 1. Forbid adding/modifying HTML files that are not SPA entry points
20
21
  if (ext === ".html" || ext === ".htm") {
21
22
  const basename = path.basename(file).toLowerCase();
22
23
  const isSpaEntry = basename === "index.html" &&
@@ -28,7 +29,6 @@ export class ArtifactFidelityGate {
28
29
  };
29
30
  }
30
31
 
31
- // Check if SPA entry imports framework libraries via CDN script tags
32
32
  try {
33
33
  const content = await fs.readFile(path.join(this.cwd, file), "utf8");
34
34
  if (content.includes("unpkg.com") || content.includes("cdnjs.cloudflare.com") || content.includes("cdn.jsdelivr.net")) {
@@ -58,6 +58,367 @@ export class ArtifactFidelityGate {
58
58
  }
59
59
  }
60
60
 
61
+ const frontendViolations = await this.verifyFrontendFidelity(changedFiles);
62
+ if (frontendViolations && frontendViolations.length > 0) {
63
+ return {
64
+ passed: false,
65
+ reason: frontendViolations.join("; ")
66
+ };
67
+ }
68
+
61
69
  return { passed: true };
62
70
  }
71
+
72
+ async verifyRequestFidelity(arg) {
73
+ let userRequest = this.userRequest || "";
74
+ let deliveryDecision = this.deliveryDecision || {};
75
+ let projectContext = this.projectContext || {};
76
+ let changedFiles = [];
77
+ let explicitApprovals = this.explicitApprovals || [];
78
+
79
+ if (Array.isArray(arg)) {
80
+ changedFiles = arg;
81
+ } else if (arg && typeof arg === "object") {
82
+ userRequest = arg.userRequest || userRequest;
83
+ deliveryDecision = arg.deliveryDecision || deliveryDecision;
84
+ projectContext = arg.projectContext || projectContext;
85
+ changedFiles = arg.changedFiles || changedFiles;
86
+ explicitApprovals = arg.explicitApprovals || explicitApprovals;
87
+ }
88
+
89
+ const blockingDecisions = new Set([
90
+ "BLOCK_STACK_CONFLICT",
91
+ "BLOCK_UNCLEAR_SCOPE",
92
+ "BLOCK_UNSUPPORTED_CONTEXT",
93
+ "REQUIRE_EXPLICIT_AUTHORIZATION",
94
+ "RELEASE_OR_DEPLOY_REQUIRES_APPROVAL"
95
+ ]);
96
+
97
+ if (blockingDecisions.has(deliveryDecision?.decision)) {
98
+ const filtered = changedFiles.filter(file =>
99
+ !file.startsWith(".ai-workflow/") &&
100
+ !file.startsWith(".git/") &&
101
+ file !== "EVIDENCE.json" &&
102
+ file !== "EVIDENCE.temp.json"
103
+ );
104
+ return {
105
+ status: "BLOCKED",
106
+ passed: false,
107
+ reason: `Delivery decision is blocking: ${deliveryDecision.decision}`,
108
+ decision: deliveryDecision.decision,
109
+ checkedFiles: filtered,
110
+ violations: [`Blocking delivery decision: ${deliveryDecision.decision}`]
111
+ };
112
+ }
113
+
114
+ const filteredFiles = changedFiles.filter(file =>
115
+ !file.startsWith(".ai-workflow/") &&
116
+ !file.startsWith(".git/") &&
117
+ file !== "EVIDENCE.json" &&
118
+ file !== "EVIDENCE.temp.json"
119
+ );
120
+
121
+ const req = userRequest.toLowerCase();
122
+ const isReadOnly = /(inspect|audit|read-only|dependencies)/.test(req);
123
+ const isDocsOnly = /(document|doc|readme)/.test(req) && !req.includes("code");
124
+ const isRelease = /(release|publish|deploy)/.test(req);
125
+
126
+ const violations = [];
127
+
128
+ if (isReadOnly && filteredFiles.length > 0) {
129
+ violations.push("Read-only audit task mutated repository files mismatch.");
130
+ }
131
+
132
+ if (isDocsOnly) {
133
+ const codeChanged = filteredFiles.some(file =>
134
+ !file.endsWith(".md") && !file.endsWith(".txt") && !file.startsWith("docs/")
135
+ );
136
+ if (codeChanged) {
137
+ violations.push("Documentation-only task mutated codebase files without justification mismatch.");
138
+ }
139
+ }
140
+
141
+ if (isRelease) {
142
+ const approved = explicitApprovals.includes("npm-publish") || explicitApprovals.includes("deploy-production");
143
+ if (!approved) {
144
+ violations.push("Release/deploy task proceeds without explicit approval token mismatch.");
145
+ }
146
+ }
147
+
148
+ const reqReact = /(react|next)/.test(req);
149
+ const reqVue = /(vue|nuxt)/.test(req);
150
+ const reqBackend = /(laravel|django|rails|express|nest|fastify|flask)/.test(req);
151
+ const reqWp = /(wordpress|wp)/.test(req);
152
+ const reqCli = /(cli|command)/.test(req);
153
+
154
+ if (reqReact && this.deliveryClass === "standalone-web" && !/prototype/.test(req)) {
155
+ violations.push("React framework page requested, but a standalone HTML/CSS substitute mismatch was delivered instead.");
156
+ }
157
+ if (reqVue && this.deliveryClass === "standalone-web" && !/prototype/.test(req)) {
158
+ violations.push("Vue framework page requested, but a standalone HTML/CSS substitute mismatch was delivered instead.");
159
+ }
160
+
161
+ if (reqBackend && filteredFiles.length > 0) {
162
+ const isLaravel = /\blaravel\b/i.test(req);
163
+ const isDjango = /\bdjango\b/i.test(req);
164
+ const isExpressNestFastify = /\b(express|nest|fastify)\b/i.test(req);
165
+ const isRails = /\brails\b/i.test(req);
166
+
167
+ if (isLaravel) {
168
+ const isLaravelArtifact = filteredFiles.some(file =>
169
+ file === "composer.json" ||
170
+ file === "artisan" ||
171
+ file.startsWith("routes/") ||
172
+ file.startsWith("app/Http/Controllers/") ||
173
+ file.startsWith("app/Models/") ||
174
+ file.startsWith("app/Services/") ||
175
+ file.startsWith("tests/Feature/") ||
176
+ file.startsWith("tests/Unit/") ||
177
+ /(^|\/)(routes\/.*\.php|app\/Http\/Controllers\/.*\.php|app\/Models\/.*\.php|app\/Services\/.*\.php|tests\/Feature\/.*\.php|tests\/Unit\/.*\.php|artisan|composer\.json)$/.test(file)
178
+ );
179
+ if (!isLaravelArtifact) {
180
+ violations.push("Laravel endpoint/scaffold requested, but no valid Laravel files/structure mismatch was delivered.");
181
+ }
182
+ } else if (isDjango) {
183
+ const isDjangoArtifact = filteredFiles.some(file =>
184
+ file === "manage.py" ||
185
+ file === "requirements.txt" ||
186
+ file === "pyproject.toml" ||
187
+ file.endsWith("settings.py") ||
188
+ file.endsWith("urls.py") ||
189
+ file.endsWith("views.py") ||
190
+ file.endsWith("models.py") ||
191
+ file.endsWith("tests.py") ||
192
+ /(^|\/)(manage\.py|settings\.py|urls\.py|views\.py|models\.py|tests\.py|requirements\.txt|pyproject\.toml)$/.test(file)
193
+ );
194
+ if (!isDjangoArtifact) {
195
+ violations.push("Django backend requested, but no valid Django files/structure mismatch was delivered.");
196
+ }
197
+ } else if (isExpressNestFastify) {
198
+ const isExpressNestFastifyArtifact = filteredFiles.some(file =>
199
+ file === "package.json" ||
200
+ file.startsWith("src/routes/") ||
201
+ file.startsWith("src/controllers/") ||
202
+ file.startsWith("src/services/") ||
203
+ file.startsWith("tests/") ||
204
+ /(^|\/)(package\.json|src\/routes\/.*|src\/controllers\/.*|src\/services\/.*|tests\/.*|server\.[a-z]+|app\.[a-z]+|main\.[a-z]+)$/.test(file)
205
+ );
206
+ if (!isExpressNestFastifyArtifact) {
207
+ violations.push("Express/Fastify/Nest backend requested, but no valid Node.js server files/structure mismatch was delivered.");
208
+ }
209
+ } else if (isRails) {
210
+ const isRailsArtifact = filteredFiles.some(file =>
211
+ file === "Gemfile" ||
212
+ file === "config/routes.rb" ||
213
+ file.startsWith("app/controllers/") ||
214
+ file.startsWith("app/models/") ||
215
+ file.startsWith("app/services/") ||
216
+ file.startsWith("test/") ||
217
+ file.startsWith("spec/") ||
218
+ /(^|\/)(Gemfile|config\/routes\.rb|app\/controllers\/.*|app\/models\/.*|app\/services\/.*|test\/.*|spec\/.*)$/.test(file)
219
+ );
220
+ if (!isRailsArtifact) {
221
+ violations.push("Ruby on Rails backend requested, but no valid Rails files/structure mismatch was delivered.");
222
+ }
223
+ } else {
224
+ const isGenericBackendArtifact = filteredFiles.some(file =>
225
+ !file.includes("plain_script.py") &&
226
+ (
227
+ file.startsWith("app/") ||
228
+ file.startsWith("routes/") ||
229
+ file.startsWith("config/") ||
230
+ file.startsWith("src/controllers/") ||
231
+ file.startsWith("src/routes/") ||
232
+ file.startsWith("src/services/") ||
233
+ file.startsWith("backend/") ||
234
+ file.startsWith("server/") ||
235
+ file.startsWith("api/") ||
236
+ file.endsWith(".py") ||
237
+ file.endsWith(".php") ||
238
+ file.endsWith(".rb") ||
239
+ file.endsWith(".go")
240
+ )
241
+ );
242
+ if (!isGenericBackendArtifact) {
243
+ violations.push("Backend endpoint requested, but wrong backend artifact structure mismatch was delivered.");
244
+ }
245
+ }
246
+ }
247
+
248
+ if (reqCli && filteredFiles.length > 0) {
249
+ const isCliArtifact = filteredFiles.some(file =>
250
+ file.startsWith("bin/") || file.startsWith("src/commands/")
251
+ );
252
+ if (!isCliArtifact) {
253
+ violations.push("CLI command requested, but a loose script outside the executable bin/commands configuration mismatch was delivered.");
254
+ }
255
+ }
256
+
257
+ if (reqWp && filteredFiles.length > 0) {
258
+ let isWpArtifact = false;
259
+ for (const file of filteredFiles) {
260
+ const basename = path.basename(file);
261
+ const ext = path.extname(file).toLowerCase();
262
+
263
+ if (file.startsWith("wp-content/") || file === "wp-config.php") {
264
+ isWpArtifact = true;
265
+ break;
266
+ }
267
+
268
+ if (basename === "functions.php" ||
269
+ basename === "front-page.php" ||
270
+ basename === "single.php" ||
271
+ basename === "page.php" ||
272
+ basename === "archive.php" ||
273
+ basename === "theme.json" ||
274
+ file.startsWith("template-parts/") ||
275
+ file.includes("/template-parts/")) {
276
+ isWpArtifact = true;
277
+ break;
278
+ }
279
+ if (basename === "style.css") {
280
+ const hasHeader = await hasWpThemeHeader(this.cwd, file);
281
+ if (hasHeader) {
282
+ isWpArtifact = true;
283
+ break;
284
+ }
285
+ }
286
+ if (basename === "index.php") {
287
+ isWpArtifact = true;
288
+ break;
289
+ }
290
+
291
+ if (basename === "uninstall.php" ||
292
+ basename === "composer.json" ||
293
+ file.startsWith("includes/") || file.includes("/includes/") ||
294
+ file.startsWith("admin/") || file.includes("/admin/") ||
295
+ file.startsWith("public/") || file.includes("/public/") ||
296
+ file.startsWith("src/") || file.includes("/src/")) {
297
+ isWpArtifact = true;
298
+ break;
299
+ }
300
+ if (ext === ".php") {
301
+ const hasHeader = await hasWpPluginHeader(this.cwd, file);
302
+ if (hasHeader) {
303
+ isWpArtifact = true;
304
+ break;
305
+ }
306
+ }
307
+ }
308
+
309
+ const hasHtml = filteredFiles.some(file => path.extname(file).toLowerCase() === ".html" || path.extname(file).toLowerCase() === ".htm");
310
+
311
+ if (!isWpArtifact || hasHtml) {
312
+ violations.push("WordPress plugin or theme requested, but files were delivered outside the WordPress structure or contained standalone HTML.");
313
+ }
314
+ }
315
+
316
+ const frontendViolations = await this.verifyFrontendFidelity(filteredFiles);
317
+ if (frontendViolations && frontendViolations.length > 0) {
318
+ violations.push(...frontendViolations);
319
+ }
320
+
321
+ const status = violations.length > 0 ? "BLOCKED" : "PASS";
322
+ return {
323
+ status,
324
+ passed: status === "PASS",
325
+ reason: violations.length > 0 ? `Fidelity violations: ${violations.join("; ")}` : "Verification passed",
326
+ decision: deliveryDecision.decision || this.deliveryClass,
327
+ checkedFiles: filteredFiles,
328
+ violations
329
+ };
330
+ }
331
+
332
+ async verifyFrontendFidelity(changedFiles) {
333
+ const dsSkillPath1 = path.join(this.cwd, ".agents/skills/frontend-design-system/SKILL.md");
334
+ const dsSkillPath2 = path.join(this.cwd, ".agents/skills/frontend_design_system/SKILL.md");
335
+ const hasDesignSystemSkill = await fs.access(dsSkillPath1).then(() => true).catch(() => false) ||
336
+ await fs.access(dsSkillPath2).then(() => true).catch(() => false);
337
+ if (!hasDesignSystemSkill) {
338
+ return null;
339
+ }
340
+
341
+ const uiFiles = changedFiles.filter((file) =>
342
+ /\.(tsx|jsx|vue|html|css|scss|less|blade\.php)$/.test(file)
343
+ );
344
+ if (uiFiles.length === 0) return null;
345
+
346
+ const violations = [];
347
+ let cssContentCombined = "";
348
+ let htmlContentCombined = "";
349
+ let styleAttrsCount = 0;
350
+
351
+ for (const file of uiFiles) {
352
+ try {
353
+ const fullPath = path.join(this.cwd, file);
354
+ const content = await fs.readFile(fullPath, "utf8");
355
+
356
+ if (/\.(css|scss|less|vue|tsx|jsx)$/.test(file)) {
357
+ cssContentCombined += content;
358
+ }
359
+ if (/\.(html|vue|tsx|jsx|blade\.php)$/.test(file)) {
360
+ htmlContentCombined += content;
361
+
362
+ // Match inline style attributes
363
+ const inlineStyles = content.match(/style\s*=\s*["']/g) || [];
364
+ styleAttrsCount += inlineStyles.length;
365
+
366
+ // CDN Checks
367
+ if (/(unpkg\.com|cdnjs\.cloudflare\.com|cdn\.jsdelivr\.net|tailwindcss|bootstrap)/i.test(content)) {
368
+ const isApproved = (this.explicitApprovals || []).some(app =>
369
+ /cdn-styles|tailwind|bootstrap/i.test(app)
370
+ );
371
+ if (!isApproved) {
372
+ violations.push(`Fidelity Violation: standalone external CDN style/library dependency detected in '${file}' without explicit authorization.`);
373
+ }
374
+ }
375
+ }
376
+ } catch {
377
+ // ignore
378
+ }
379
+ }
380
+
381
+ // 1. Tokens CSS mínimos Check
382
+ const hasRules = cssContentCombined.includes("{");
383
+ const hasCssVars = /--[a-zA-Z0-9_-]+\s*:/g.test(cssContentCombined);
384
+ if (hasRules && !hasCssVars) {
385
+ violations.push("Fidelity Violation: Design System token mismatch. No CSS custom properties (variables) detected in changed UI stylesheets.");
386
+ }
387
+
388
+ // 2. Excesso de style="..."
389
+ if (styleAttrsCount > 5) {
390
+ violations.push(`Fidelity Violation: excessive inline styles detected (${styleAttrsCount} style="..." declarations). Use CSS classes or utility classes instead.`);
391
+ }
392
+
393
+ // 3. HTML principal tags semânticas básicas Check
394
+ if (htmlContentCombined.length > 0) {
395
+ const hasSemanticTags = /<(header|main|footer|nav|section)\b/i.test(htmlContentCombined);
396
+ const hasH1 = /<h1\b/i.test(htmlContentCombined);
397
+ if (!hasSemanticTags && !hasH1) {
398
+ violations.push("Fidelity Violation: HTML layout structure lacks basic semantic elements (<header>, <main>, <footer>, <nav>, <section>) or <h1> tag.");
399
+ }
400
+ }
401
+
402
+ return violations;
403
+ }
404
+ }
405
+
406
+ async function hasWpThemeHeader(cwd, file) {
407
+ try {
408
+ const fullPath = path.join(cwd, file);
409
+ const content = await fs.readFile(fullPath, "utf8");
410
+ return /\bTheme Name\s*:/i.test(content) || /\bTheme URI\s*:/i.test(content);
411
+ } catch {
412
+ return false;
413
+ }
414
+ }
415
+
416
+ async function hasWpPluginHeader(cwd, file) {
417
+ try {
418
+ const fullPath = path.join(cwd, file);
419
+ const content = await fs.readFile(fullPath, "utf8");
420
+ return /\bPlugin Name\s*:/i.test(content) || /\bDescription\s*:/i.test(content);
421
+ } catch {
422
+ return false;
423
+ }
63
424
  }