@williambeto/ai-workflow 2.8.0 → 2.8.2

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,8 +2,9 @@ import {
2
2
  ArtifactFidelityGate,
3
3
  DeliveryDecisionEngine,
4
4
  MergeGate,
5
+ RequestRouter,
5
6
  StackDetector
6
- } from "./chunk-XW747GIG.js";
7
+ } from "./chunk-H7GIKXFO.js";
7
8
 
8
9
  // src/core/validation/quality-guard.ts
9
10
  import { execSync } from "child_process";
@@ -39,19 +40,22 @@ var QualityGuard = class {
39
40
  evidencePolicy;
40
41
  riskLevel;
41
42
  _changedFiles;
43
+ scopedChangedFiles;
42
44
  _hasHead;
43
45
  constructor({
44
46
  cwd,
45
47
  taskSlug = null,
46
48
  evidenceWillBePersisted = false,
47
49
  evidencePolicy = "optional",
48
- riskLevel = "medium"
50
+ riskLevel = "medium",
51
+ scopedChangedFiles = null
49
52
  }) {
50
53
  this.cwd = cwd;
51
54
  this.taskSlug = taskSlug;
52
55
  this.evidencePolicy = evidencePolicy || "optional";
53
56
  this.riskLevel = riskLevel || "medium";
54
57
  this._changedFiles = null;
58
+ this.scopedChangedFiles = scopedChangedFiles ? [...new Set(scopedChangedFiles.map(normalizePath).filter(Boolean))].sort() : null;
55
59
  this._hasHead = null;
56
60
  this.evidenceWillBePersisted = evidenceWillBePersisted;
57
61
  }
@@ -92,24 +96,27 @@ var QualityGuard = class {
92
96
  this._changedFiles = [...files].sort();
93
97
  return this._changedFiles;
94
98
  }
99
+ getScopedChangedFiles() {
100
+ return this.scopedChangedFiles ?? this.getChangedFiles();
101
+ }
95
102
  isDocsTask() {
96
- const files = this.getChangedFiles();
103
+ const files = this.getScopedChangedFiles();
97
104
  return files.length > 0 && files.every((file) => file.endsWith(".md") || file.endsWith(".txt") || file.startsWith("docs/"));
98
105
  }
99
106
  isUiTask() {
100
- return this.getChangedFiles().some(
107
+ return this.getScopedChangedFiles().some(
101
108
  (file) => /\.(tsx|jsx|vue|html|css|scss|less|blade\.php)$/.test(file) || /(^|\/)(components|pages|views)\//.test(file)
102
109
  );
103
110
  }
104
111
  isImplementationTask() {
105
- const files = this.getChangedFiles();
112
+ const files = this.getScopedChangedFiles();
106
113
  if (files.length === 0 || this.isDocsTask()) return false;
107
114
  return files.some(
108
115
  (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
116
  );
110
117
  }
111
- async hasExecutableBehaviorChanges() {
112
- const files = this.getChangedFiles().filter((file) => !isManagedChangePath(file));
118
+ async hasExecutableBehaviorChanges(filesOverride) {
119
+ const files = (filesOverride || this.getScopedChangedFiles()).filter((file) => !isManagedChangePath(file));
113
120
  const executableExtensions = /\.(js|jsx|mjs|cjs|ts|tsx|vue|php|py|rb|go|java|cs|rs|swift|kt|kts|sh|bash)$/i;
114
121
  if (files.some((file) => executableExtensions.test(file))) return true;
115
122
  for (const file of files.filter((item) => /\.(html?|xhtml)$/i.test(item))) {
@@ -141,12 +148,15 @@ var QualityGuard = class {
141
148
  }
142
149
  async verifyDocumentationNeed() {
143
150
  if (!this.isImplementationTask()) return { status: "NOT_RUN", reason: "No implementation changes detected." };
144
- const files = this.getChangedFiles();
151
+ const files = this.getScopedChangedFiles();
145
152
  const docsChanged = files.some((file) => file === "README.md" || file.startsWith("docs/") || file.endsWith(".md"));
146
153
  const publicSurfaceChanged = files.some(
147
154
  (file) => file === "package.json" || /(^|\/)(routes?|api|cli|commands?|config|public)\//.test(file) || /README|CHANGELOG/.test(file)
148
155
  );
149
156
  if (publicSurfaceChanged && !docsChanged) {
157
+ if (this.evidencePolicy === "required") {
158
+ return { status: "FAIL_QUALITY_GATE", reason: "A required-evidence change modified a public API, CLI, configuration, or package surface without updating documentation." };
159
+ }
150
160
  return { status: "PASS_WITH_NOTES", reason: "Public behavior may have changed without a documentation update." };
151
161
  }
152
162
  return { status: "PASS" };
@@ -163,7 +173,11 @@ var QualityGuard = class {
163
173
  if (/\.(png|jpe?g|webp)$/i.test(String(entry))) found.push(normalizePath(path.join(dir, String(entry))));
164
174
  }
165
175
  }
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." };
176
+ if (found.length) return { status: "PASS", screenshots: found.slice(0, 20) };
177
+ if (this.evidencePolicy === "required") {
178
+ return { status: "FAIL_QUALITY_GATE", reason: "Required evidence for a high-risk or release UI change does not include a persisted screenshot." };
179
+ }
180
+ return { status: "PASS_WITH_NOTES", reason: "Screenshots are optional for this low/medium-risk UI change and were not persisted." };
167
181
  }
168
182
  async verifyPersistedEvidence() {
169
183
  if (this.evidencePolicy !== "required") return { status: "NOT_RUN", reason: "Persisted evidence is optional under optional evidence policy." };
@@ -373,13 +387,87 @@ var VisualVerifier = class {
373
387
  // src/core/validation/evidence-collector.ts
374
388
  import { spawnSync } from "child_process";
375
389
  import fs3 from "fs/promises";
390
+ import fsSync from "fs";
376
391
  import path3 from "path";
392
+
393
+ // src/core/completion-contract.ts
394
+ var PASSING_VALIDATION = /* @__PURE__ */ new Set(["PASS", "PASS_WITH_NOTES"]);
395
+ var TEST_FILE_PATTERN = /(^|\/)(__tests__|tests?)(\/|\.)|\.(test|spec)\.[cm]?[jt]sx?$/i;
396
+ function parseDeliveryOutcomeClaim(output = "") {
397
+ const matches = [...String(output).matchAll(/^AIWK_DELIVERY_OUTCOME:\s*(CHANGED|ALREADY_SATISFIED)\s*$/gim)];
398
+ if (matches.length !== 1) return null;
399
+ return matches[0][1].toUpperCase();
400
+ }
401
+ function resolveDeliveryOutcome({
402
+ deliveryMode,
403
+ mutationIntent,
404
+ changedFiles = [],
405
+ validationStatus,
406
+ validationObserved = false,
407
+ deliveryDecision = null,
408
+ claimedOutcome = null
409
+ }) {
410
+ if (deliveryDecision?.startsWith("BLOCK_")) {
411
+ return "NEEDS_CLARIFICATION";
412
+ }
413
+ if (!PASSING_VALIDATION.has(validationStatus)) {
414
+ return "NOT_DELIVERED";
415
+ }
416
+ if (mutationIntent === "readonly") {
417
+ return "READ_ONLY_RESULT";
418
+ }
419
+ if (deliveryMode === "answer-only") {
420
+ return "PREVIEW_ONLY";
421
+ }
422
+ if (changedFiles.length > 0) {
423
+ if (!validationObserved) return "NOT_DELIVERED";
424
+ return claimedOutcome === "ALREADY_SATISFIED" ? "NOT_DELIVERED" : "CHANGED";
425
+ }
426
+ if (mutationIntent === "write" && claimedOutcome === "ALREADY_SATISFIED" && validationObserved) {
427
+ return "ALREADY_SATISFIED";
428
+ }
429
+ return "NOT_DELIVERED";
430
+ }
431
+ function decideTestAction({
432
+ executableBehavior,
433
+ changedFiles = [],
434
+ addedFiles = [],
435
+ deletedFiles = [],
436
+ passingBehaviorTest
437
+ }) {
438
+ if (!executableBehavior) {
439
+ return { action: "none", reason: "No executable behavior changed; automated behavior-test changes are not required." };
440
+ }
441
+ if (!passingBehaviorTest) {
442
+ return { action: "none", reason: "Executable behavior changed, but no proportional automated behavior test passed." };
443
+ }
444
+ const normalizePath2 = (file) => file.replaceAll("\\", "/");
445
+ const deleted = new Set(deletedFiles.map(normalizePath2));
446
+ const added = new Set(addedFiles.map(normalizePath2));
447
+ const deletedTests = changedFiles.filter((file) => TEST_FILE_PATTERN.test(file) && deleted.has(normalizePath2(file)));
448
+ const changedTests = changedFiles.filter((file) => TEST_FILE_PATTERN.test(file) && !deleted.has(normalizePath2(file)));
449
+ if (changedTests.some((file) => added.has(normalizePath2(file)))) {
450
+ return { action: "add", reason: "A new automated behavior test was added for behavior without relevant existing coverage." };
451
+ }
452
+ if (changedTests.length > 0) {
453
+ return { action: "extend", reason: "Existing automated tests were extended to cover the changed behavior invariant." };
454
+ }
455
+ if (deletedTests.length > 0) {
456
+ return { action: "reuse", reason: "Deleted test files do not count as extended coverage; the remaining automated behavior tests passed." };
457
+ }
458
+ return { action: "reuse", reason: "Existing automated behavior tests passed without requiring new test files." };
459
+ }
460
+
461
+ // src/core/validation/evidence-collector.ts
377
462
  var VALIDATION_KINDS = /* @__PURE__ */ new Set(["test", "build", "typecheck", "lint", "security", "smoke", "validate", "other"]);
378
463
  function publicStatus(status) {
379
464
  if (status === "PASS") return "COMPLETED";
380
465
  if (status === "PASS_WITH_NOTES") return "COMPLETED_WITH_NOTES";
381
466
  return "BLOCKED";
382
467
  }
468
+ function blockingDiagnostic(gate, observed, expected, nextAction) {
469
+ return `Gate: ${gate}; Observed: ${observed}; Expected: ${expected}; Next action: ${nextAction}`;
470
+ }
383
471
  function inferTaskKind(task = {}) {
384
472
  const explicit = String(task.kind || "").trim().toLowerCase();
385
473
  if (VALIDATION_KINDS.has(explicit)) return explicit;
@@ -408,6 +496,11 @@ var EvidenceCollector = class {
408
496
  visualDist;
409
497
  port;
410
498
  workflowEvidence;
499
+ claimedOutcome;
500
+ deliveryChangedFiles;
501
+ deliveryAddedFiles;
502
+ deliveryDeletedFiles;
503
+ riskLevel;
411
504
  constructor({
412
505
  cwd,
413
506
  maxLogLength = 2e3,
@@ -421,7 +514,12 @@ var EvidenceCollector = class {
421
514
  explicitApprovals = [],
422
515
  visualDist = null,
423
516
  port = 8080,
424
- workflowEvidence = null
517
+ workflowEvidence = null,
518
+ claimedOutcome = null,
519
+ deliveryChangedFiles = null,
520
+ deliveryAddedFiles = null,
521
+ deliveryDeletedFiles = null,
522
+ riskLevel = null
425
523
  }) {
426
524
  this.cwd = cwd;
427
525
  this.maxLogLength = maxLogLength;
@@ -436,6 +534,11 @@ var EvidenceCollector = class {
436
534
  this.visualDist = visualDist;
437
535
  this.port = Number(port || 8080);
438
536
  this.workflowEvidence = workflowEvidence;
537
+ this.claimedOutcome = claimedOutcome;
538
+ this.deliveryChangedFiles = deliveryChangedFiles;
539
+ this.deliveryAddedFiles = deliveryAddedFiles;
540
+ this.deliveryDeletedFiles = deliveryDeletedFiles;
541
+ this.riskLevel = riskLevel || (this.evidencePolicy === "required" ? "high" : "medium");
439
542
  }
440
543
  async findUserRequest() {
441
544
  if (this.userRequest) return this.userRequest;
@@ -473,8 +576,8 @@ var EvidenceCollector = class {
473
576
  try {
474
577
  const { execSync: execSync2 } = await import("child_process");
475
578
  const branch = execSync2("git branch --show-current", { cwd: this.cwd, encoding: "utf8" }).trim();
476
- if (branch && branch !== "main" && branch !== "master" && branch.startsWith("feat/")) {
477
- return branch.replace("feat/", "").replaceAll("-", " ");
579
+ if (branch && branch !== "main" && branch !== "master" && /^(feat|fix|docs|chore)\//.test(branch)) {
580
+ return branch.replace(/^(feat|fix|docs|chore)\//, "").replaceAll("-", " ");
478
581
  }
479
582
  } catch {
480
583
  }
@@ -482,6 +585,29 @@ var EvidenceCollector = class {
482
585
  }
483
586
  runTask(task) {
484
587
  const kind = inferTaskKind(task);
588
+ if (task.internal === "document-integrity") {
589
+ const failures = [];
590
+ for (const file of task.files || []) {
591
+ try {
592
+ const content = fsSync.readFileSync(path3.join(this.cwd, file), "utf8");
593
+ if (!content.trim()) failures.push(`${file}: empty file`);
594
+ if (/^(<<<<<<<|=======|>>>>>>>)/m.test(content)) failures.push(`${file}: conflict marker`);
595
+ if (content.split(/\r?\n/).some((line) => /[ \t]+$/.test(line))) failures.push(`${file}: trailing whitespace`);
596
+ } catch {
597
+ failures.push(`${file}: unreadable`);
598
+ }
599
+ }
600
+ const status2 = failures.length ? "FAIL" : "PASS";
601
+ return {
602
+ name: task.name,
603
+ command: task.command,
604
+ kind,
605
+ status: status2,
606
+ exitCode: status2 === "PASS" ? 0 : 1,
607
+ summary: status2 === "PASS" ? "Documentation integrity checks passed." : failures.join("; "),
608
+ output: failures.join("\n")
609
+ };
610
+ }
485
611
  if (task.presetStatus) {
486
612
  return { name: task.name, command: task.command, kind, status: task.presetStatus, exitCode: null, summary: task.summary || "", output: "" };
487
613
  }
@@ -523,15 +649,42 @@ var EvidenceCollector = class {
523
649
  ... [TRUNCATED]` : rawOutput;
524
650
  return { name: task.name, command: task.command, kind, status, exitCode: result.status, signal: result.signal, summary, output };
525
651
  }
652
+ findAddedFiles(changedFiles) {
653
+ const added = /* @__PURE__ */ new Set();
654
+ for (const args of [
655
+ ["diff", "--cached", "--diff-filter=A", "--name-only"],
656
+ ["ls-files", "--others", "--exclude-standard"]
657
+ ]) {
658
+ const result = spawnSync("git", args, { cwd: this.cwd, encoding: "utf8" });
659
+ if (result.status !== 0) continue;
660
+ String(result.stdout || "").split("\n").map((file) => file.trim().replaceAll("\\", "/")).filter(Boolean).forEach((file) => added.add(file));
661
+ }
662
+ return changedFiles.filter((file) => added.has(file.replaceAll("\\", "/")));
663
+ }
526
664
  async collect(tasks, { writeArtifact = false } = {}) {
527
- const qualityGuard = new QualityGuard({ cwd: this.cwd, taskSlug: this.taskSlug, evidencePolicy: this.evidencePolicy, riskLevel: this.evidencePolicy === "required" ? "high" : "medium", evidenceWillBePersisted: writeArtifact });
528
- const implementation = qualityGuard.isImplementationTask();
529
- const executableBehavior = await qualityGuard.hasExecutableBehaviorChanges();
665
+ const qualityGuard = new QualityGuard({
666
+ cwd: this.cwd,
667
+ taskSlug: this.taskSlug,
668
+ evidencePolicy: this.evidencePolicy,
669
+ riskLevel: this.riskLevel,
670
+ evidenceWillBePersisted: writeArtifact,
671
+ scopedChangedFiles: this.deliveryChangedFiles
672
+ });
673
+ const implementationFilesPresent = qualityGuard.isImplementationTask();
674
+ const executableBehavior = await qualityGuard.hasExecutableBehaviorChanges(this.deliveryChangedFiles || void 0);
530
675
  const results = tasks.map((task) => this.runTask(task));
531
676
  const policyValidation = await qualityGuard.verify();
532
677
  const behaviorTests = results.filter((result) => result.kind === "test");
533
678
  const passingBehaviorTest = behaviorTests.some((result) => result.status === "PASS");
679
+ const behaviorTestRequired = executableBehavior && (behaviorTests.length > 0 || this.profile.startsWith("frontend") || qualityGuard.isUiTask());
680
+ const validationObserved = results.some(
681
+ (result) => result.kind !== "other" && ["PASS", "PASS_WITH_NOTES"].includes(result.status)
682
+ );
534
683
  const userRequest = await this.findUserRequest();
684
+ const requestRouter = new RequestRouter({ cwd: this.cwd });
685
+ const requestUnderstanding = userRequest ? requestRouter.understand(userRequest) : null;
686
+ const routingDecision = requestUnderstanding ? requestRouter.route(requestUnderstanding) : null;
687
+ const implementation = requestUnderstanding ? requestUnderstanding.deliveryMode === "workspace" && requestUnderstanding.mutationIntent !== "readonly" : implementationFilesPresent;
535
688
  const engine = new DeliveryDecisionEngine({ cwd: this.cwd });
536
689
  const projectContext = await engine.detectContext();
537
690
  const deliveryDecision = await engine.determineDecision({
@@ -546,17 +699,24 @@ var EvidenceCollector = class {
546
699
  explicitApprovals: this.explicitApprovals || []
547
700
  });
548
701
  const changedFiles = policyValidation.git?.changedFiles || [];
702
+ const deliveryChangedFiles = this.deliveryChangedFiles || changedFiles;
703
+ const addedFiles = this.deliveryAddedFiles || this.findAddedFiles(deliveryChangedFiles);
549
704
  const artifactFidelityCheck = await fidelityGate.verifyRequestFidelity({
550
705
  userRequest,
551
706
  deliveryDecision,
552
707
  projectContext,
553
- changedFiles
708
+ changedFiles: deliveryChangedFiles
554
709
  });
555
710
  let overallStatus = "PASS";
556
- if (implementation && executableBehavior && tasks.length === 0) overallStatus = "BLOCKED";
557
- else if (results.some((result) => ["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(result.status))) overallStatus = "FAIL_QUALITY_GATE";
558
- else if (executableBehavior && !passingBehaviorTest) overallStatus = "BLOCKED";
559
- else if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(policyValidation.overallStatus)) overallStatus = "FAIL_QUALITY_GATE";
711
+ if (results.some((result) => ["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(result.status))) overallStatus = "FAIL_QUALITY_GATE";
712
+ else if (implementation && deliveryChangedFiles.length > 0 && !validationObserved) overallStatus = "BLOCKED";
713
+ else if (behaviorTestRequired && !passingBehaviorTest) {
714
+ if (this.riskLevel === "high") {
715
+ overallStatus = "BLOCKED";
716
+ } else {
717
+ overallStatus = "PASS_WITH_NOTES";
718
+ }
719
+ } else if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(policyValidation.overallStatus)) overallStatus = "FAIL_QUALITY_GATE";
560
720
  else if (results.some((result) => result.status === "PASS_WITH_NOTES") || policyValidation.overallStatus === "PASS_WITH_NOTES") overallStatus = "PASS_WITH_NOTES";
561
721
  if (!artifactFidelityCheck || !artifactFidelityCheck.passed) {
562
722
  overallStatus = "BLOCKED";
@@ -567,14 +727,99 @@ var EvidenceCollector = class {
567
727
  overallStatus = "BLOCKED";
568
728
  }
569
729
  }
730
+ const testDecision = decideTestAction({
731
+ executableBehavior,
732
+ changedFiles: deliveryChangedFiles,
733
+ addedFiles,
734
+ deletedFiles: this.deliveryDeletedFiles || [],
735
+ passingBehaviorTest
736
+ });
737
+ const deliveryMode = requestUnderstanding?.deliveryMode || (implementation ? "workspace" : "answer-only");
738
+ const mutationIntent = requestUnderstanding?.mutationIntent || (implementation ? "write" : "readonly");
739
+ const deliveryOutcome = resolveDeliveryOutcome({
740
+ deliveryMode,
741
+ mutationIntent,
742
+ changedFiles: deliveryChangedFiles,
743
+ validationStatus: overallStatus,
744
+ validationObserved,
745
+ deliveryDecision: deliveryDecision.decision,
746
+ claimedOutcome: this.claimedOutcome
747
+ });
748
+ if (["NEEDS_CLARIFICATION", "NOT_DELIVERED"].includes(deliveryOutcome) && ["PASS", "PASS_WITH_NOTES"].includes(overallStatus)) {
749
+ overallStatus = "BLOCKED";
750
+ }
570
751
  const limitations = [];
571
- if (implementation && executableBehavior && tasks.length === 0) limitations.push("No meaningful validation command was available for executable implementation work.");
572
- if (executableBehavior && behaviorTests.length === 0) {
573
- limitations.push("Executable behavior changed without a proportional automated behavior test. Build, lint, typecheck, smoke, screenshots, and manual review do not replace behavior tests.");
752
+ if (implementation && deliveryChangedFiles.length > 0 && !validationObserved) limitations.push(blockingDiagnostic(
753
+ "validation",
754
+ "the requested workspace changed but no meaningful validation result was observed",
755
+ "at least one proportional validation command must pass after the change",
756
+ "run the relevant check and finalize again"
757
+ ));
758
+ if (implementation && executableBehavior && tasks.length === 0) limitations.push(blockingDiagnostic(
759
+ "validation-path",
760
+ "executable implementation work has no available validation command",
761
+ "the project must provide a proportional syntax, build, or behavior check",
762
+ "configure or run the smallest relevant validator"
763
+ ));
764
+ if (behaviorTestRequired && behaviorTests.length === 0) {
765
+ if (this.riskLevel === "high") {
766
+ limitations.push(blockingDiagnostic(
767
+ "behavior-test",
768
+ "UI or test-capable executable behavior changed without an automated behavior test",
769
+ "a relevant automated behavior test must pass",
770
+ "run or configure the focused behavior test; build, lint, and screenshots are not substitutes"
771
+ ));
772
+ } else {
773
+ limitations.push("behavior-test: UI or executable behavior changed without an automated behavior test (allowed for low/medium risk tasks)");
774
+ }
775
+ }
776
+ for (const result of results) {
777
+ if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(result.status)) {
778
+ limitations.push(blockingDiagnostic(
779
+ `command:${result.name}`,
780
+ `${result.summary || result.status} (${result.status})`,
781
+ `${Array.isArray(result.command) ? result.command.join(" ") : result.command} must exit successfully`,
782
+ "correct the failure and rerun this command"
783
+ ));
784
+ }
574
785
  }
575
786
  for (const [name, check] of Object.entries(policyValidation.checks || {})) {
576
787
  if (check.status === "PASS_WITH_NOTES" && check.reason) limitations.push(`${name}: ${check.reason}`);
788
+ if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(check.status)) {
789
+ limitations.push(blockingDiagnostic(
790
+ `quality:${name}`,
791
+ check.reason || check.status,
792
+ `${name} must pass under the active risk/evidence policy`,
793
+ "satisfy the stated quality requirement and finalize again"
794
+ ));
795
+ }
796
+ }
797
+ if (!artifactFidelityCheck?.passed) {
798
+ const detail = artifactFidelityCheck?.reason || artifactFidelityCheck?.violations?.join("; ") || "the observed artifact does not match the requested delivery shape";
799
+ limitations.push(blockingDiagnostic(
800
+ "artifact-fidelity",
801
+ detail,
802
+ "the delivered artifact must match the requested stack and shape",
803
+ "correct the artifact or clarify the requested target"
804
+ ));
805
+ }
806
+ if (implementation && (!deliveryDecision?.decision || deliveryDecision.decision.startsWith("BLOCK") || deliveryDecision.decision.includes("BLOCKED") || deliveryDecision.decision === "REQUIRE_EXPLICIT_AUTHORIZATION" || deliveryDecision.decision === "RELEASE_OR_DEPLOY_REQUIRES_APPROVAL")) {
807
+ limitations.push(blockingDiagnostic(
808
+ "delivery-decision",
809
+ deliveryDecision?.reason || deliveryDecision?.decision || "no delivery decision was produced",
810
+ "the delivery decision must authorize this workspace outcome",
811
+ "satisfy the stated decision requirement before finalizing"
812
+ ));
577
813
  }
814
+ if (["NEEDS_CLARIFICATION", "NOT_DELIVERED"].includes(deliveryOutcome)) {
815
+ limitations.push(blockingDiagnostic(
816
+ "delivery-outcome",
817
+ `the resolved outcome is ${deliveryOutcome}`,
818
+ "a workspace request requires a scoped change or verified ALREADY_SATISFIED result plus proportional validation",
819
+ "complete the missing delivery condition or request clarification"
820
+ ));
821
+ }
822
+ const uniqueLimitations = [...new Set(limitations)];
578
823
  let visualEvidence = null;
579
824
  if (this.visualDist) {
580
825
  console.log(`
@@ -603,11 +848,18 @@ var EvidenceCollector = class {
603
848
  branchRecovery: this.branchRecovery,
604
849
  branch: policyValidation.git?.branch || "unknown",
605
850
  changedFiles,
851
+ deliveryChangedFiles,
852
+ mutationOwner: routingDecision?.mutationOwner || null,
853
+ capabilitiesUsed: requestUnderstanding?.capabilityRoles || [],
854
+ deliveryMode,
855
+ deliveryOutcome,
856
+ testAction: testDecision.action,
857
+ testReason: testDecision.reason,
606
858
  status: publicStatus(overallStatus),
607
859
  internalStatus: overallStatus,
608
860
  commands: results,
609
861
  checks: policyValidation.checks,
610
- limitations,
862
+ limitations: uniqueLimitations,
611
863
  deliveryDecision,
612
864
  artifactFidelityCheck,
613
865
  visualEvidence,
@@ -627,6 +879,7 @@ var EvidenceCollector = class {
627
879
  // src/core/validation/validation-planner.ts
628
880
  import fs4 from "fs/promises";
629
881
  import path4 from "path";
882
+ import { spawnSync as spawnSync2 } from "child_process";
630
883
  var ValidationPlanner = class {
631
884
  cwd;
632
885
  qualityGuard;
@@ -636,26 +889,80 @@ var ValidationPlanner = class {
636
889
  this.qualityGuard = qualityGuard;
637
890
  this.stackDetector = new StackDetector({ cwd });
638
891
  }
639
- async plan(profile = "generic") {
892
+ async plan(profile = "generic", { requireObservedValidation = false } = {}) {
640
893
  const stacks = await this.stackDetector.detect();
641
894
  const tasks = [];
642
- const changedFiles = this.qualityGuard.getChangedFiles().filter((file) => {
895
+ const sourceFiles = typeof this.qualityGuard.getScopedChangedFiles === "function" ? this.qualityGuard.getScopedChangedFiles() : this.qualityGuard.getChangedFiles();
896
+ const changedFiles = sourceFiles.filter((file) => {
643
897
  return !/(^|\/)(node_modules|vendor|dist|coverage|\.cache)(\/|$)/.test(String(file).replaceAll("\\", "/"));
644
898
  });
645
899
  const executableBehavior = await this.qualityGuard.hasExecutableBehaviorChanges();
900
+ const highRisk = this.qualityGuard.riskLevel === "high";
901
+ const noChangeVerification = changedFiles.length === 0 && requireObservedValidation && !highRisk;
902
+ const docsOnly = changedFiles.length > 0 && changedFiles.every(
903
+ (file) => /(^|\/)(docs?|readme|changelog)(\/|\.|$)|\.(md|txt)$/i.test(file)
904
+ );
905
+ if (docsOnly || changedFiles.length === 0 && !highRisk && !requireObservedValidation) return tasks;
906
+ const uiChanged = changedFiles.some(
907
+ (file) => /\.(tsx|jsx|vue|html|css|scss|less|blade\.php)$/i.test(file) || /(^|\/)(components|pages|views)\//i.test(file)
908
+ );
646
909
  if (stacks.includes("node")) {
647
910
  try {
648
911
  const pkg = JSON.parse(await fs4.readFile(path4.join(this.cwd, "package.json"), "utf8"));
649
912
  const scripts = pkg.scripts || {};
650
- this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
651
- this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"], { rejectNoOp: executableBehavior });
652
- this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
653
- this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
654
- if (scripts.validate && !String(scripts.validate).includes("ai-workflow collect-evidence")) {
655
- this.addNodeScriptTask(tasks, scripts, "validate", ["npm", "run", "validate"]);
656
- }
657
913
  const hasTs = changedFiles.some((file) => /\.(ts|tsx)$/.test(file)) || await fs4.access(path4.join(this.cwd, "tsconfig.json")).then(() => true).catch(() => false);
658
- if (hasTs && !scripts.typecheck) {
914
+ const addValidate = () => {
915
+ if (scripts.validate && !String(scripts.validate).includes("ai-workflow collect-evidence")) {
916
+ this.addNodeScriptTask(tasks, scripts, "validate", ["npm", "run", "validate"]);
917
+ }
918
+ };
919
+ if (noChangeVerification) {
920
+ if (scripts.test && !this.isNoOpScript(scripts.test)) {
921
+ this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"]);
922
+ } else if (scripts.typecheck) {
923
+ this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
924
+ } else if (scripts.lint) {
925
+ this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
926
+ } else if (scripts.build) {
927
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
928
+ } else {
929
+ addValidate();
930
+ }
931
+ } else if (highRisk) {
932
+ this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
933
+ this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"], { rejectNoOp: executableBehavior });
934
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
935
+ this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
936
+ addValidate();
937
+ } else if (executableBehavior) {
938
+ this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"], { rejectNoOp: true });
939
+ if (uiChanged) {
940
+ if (scripts.build) {
941
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
942
+ } else {
943
+ tasks.push({
944
+ name: "build",
945
+ command: ["npm", "run", "build"],
946
+ kind: "build",
947
+ presetStatus: "FAIL_QUALITY_GATE",
948
+ summary: "Executable UI change requires a configured build script."
949
+ });
950
+ }
951
+ } else if (hasTs) {
952
+ if (scripts.build) this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
953
+ else this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
954
+ }
955
+ } else if (uiChanged) {
956
+ if (scripts.build) this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
957
+ else this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
958
+ } else if (scripts.lint) {
959
+ this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
960
+ } else if (scripts.build) {
961
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
962
+ } else {
963
+ addValidate();
964
+ }
965
+ if (hasTs && !scripts.typecheck && !scripts.build && !noChangeVerification) {
659
966
  tasks.push({
660
967
  name: "typecheck",
661
968
  command: ["npm", "run", "typecheck"],
@@ -676,9 +983,10 @@ var ValidationPlanner = class {
676
983
  kind: "lint"
677
984
  });
678
985
  }
679
- if (executableBehavior) {
986
+ if (executableBehavior || changedFiles.length === 0 && requireObservedValidation && tasks.length === 0) {
680
987
  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);
681
- if (hasPytestConfig || pythonFiles.length > 0) {
988
+ const pytestAvailable = highRisk && spawnCommandAvailable("pytest", this.cwd);
989
+ if (hasPytestConfig || pytestAvailable) {
682
990
  tasks.push({
683
991
  name: "python-test",
684
992
  command: ["pytest"],
@@ -698,9 +1006,10 @@ var ValidationPlanner = class {
698
1006
  });
699
1007
  }
700
1008
  }
701
- if (executableBehavior) {
1009
+ if (executableBehavior || changedFiles.length === 0 && requireObservedValidation && tasks.length === 0) {
702
1010
  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);
703
- if (hasPhpUnitXml || phpFiles.length > 0) {
1011
+ const localPhpUnit = await fs4.access(path4.join(this.cwd, "vendor/bin/phpunit")).then(() => true).catch(() => false);
1012
+ if (hasPhpUnitXml || highRisk && localPhpUnit) {
704
1013
  tasks.push({
705
1014
  name: "php-test",
706
1015
  command: ["vendor/bin/phpunit"],
@@ -732,11 +1041,16 @@ var ValidationPlanner = class {
732
1041
  return /^(echo\b|true$|exit\s+0$|node\s+-e\s+["']?console\.log)/.test(value) || value.includes("tests-pass");
733
1042
  }
734
1043
  };
1044
+ function spawnCommandAvailable(command, cwd) {
1045
+ const lookup = process.platform === "win32" ? ["where", [command]] : ["which", [command]];
1046
+ return spawnSync2(lookup[0], lookup[1], { cwd, stdio: "ignore" }).status === 0;
1047
+ }
735
1048
 
736
1049
  export {
737
1050
  QualityGuard,
738
1051
  VisualVerifier,
1052
+ parseDeliveryOutcomeClaim,
739
1053
  EvidenceCollector,
740
1054
  ValidationPlanner
741
1055
  };
742
- //# sourceMappingURL=chunk-Y4RLP6ZM.js.map
1056
+ //# sourceMappingURL=chunk-GLYX3HEY.js.map