@williambeto/ai-workflow 2.7.1 → 2.8.1

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;
@@ -407,6 +495,12 @@ var EvidenceCollector = class {
407
495
  explicitApprovals;
408
496
  visualDist;
409
497
  port;
498
+ workflowEvidence;
499
+ claimedOutcome;
500
+ deliveryChangedFiles;
501
+ deliveryAddedFiles;
502
+ deliveryDeletedFiles;
503
+ riskLevel;
410
504
  constructor({
411
505
  cwd,
412
506
  maxLogLength = 2e3,
@@ -419,7 +513,13 @@ var EvidenceCollector = class {
419
513
  userRequest = null,
420
514
  explicitApprovals = [],
421
515
  visualDist = null,
422
- port = 8080
516
+ port = 8080,
517
+ workflowEvidence = null,
518
+ claimedOutcome = null,
519
+ deliveryChangedFiles = null,
520
+ deliveryAddedFiles = null,
521
+ deliveryDeletedFiles = null,
522
+ riskLevel = null
423
523
  }) {
424
524
  this.cwd = cwd;
425
525
  this.maxLogLength = maxLogLength;
@@ -433,6 +533,12 @@ var EvidenceCollector = class {
433
533
  this.explicitApprovals = explicitApprovals;
434
534
  this.visualDist = visualDist;
435
535
  this.port = Number(port || 8080);
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");
436
542
  }
437
543
  async findUserRequest() {
438
544
  if (this.userRequest) return this.userRequest;
@@ -470,8 +576,8 @@ var EvidenceCollector = class {
470
576
  try {
471
577
  const { execSync: execSync2 } = await import("child_process");
472
578
  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("-", " ");
579
+ if (branch && branch !== "main" && branch !== "master" && /^(feat|fix|docs|chore)\//.test(branch)) {
580
+ return branch.replace(/^(feat|fix|docs|chore)\//, "").replaceAll("-", " ");
475
581
  }
476
582
  } catch {
477
583
  }
@@ -479,6 +585,29 @@ var EvidenceCollector = class {
479
585
  }
480
586
  runTask(task) {
481
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
+ }
482
611
  if (task.presetStatus) {
483
612
  return { name: task.name, command: task.command, kind, status: task.presetStatus, exitCode: null, summary: task.summary || "", output: "" };
484
613
  }
@@ -520,15 +649,42 @@ var EvidenceCollector = class {
520
649
  ... [TRUNCATED]` : rawOutput;
521
650
  return { name: task.name, command: task.command, kind, status, exitCode: result.status, signal: result.signal, summary, output };
522
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
+ }
523
664
  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();
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);
527
675
  const results = tasks.map((task) => this.runTask(task));
528
676
  const policyValidation = await qualityGuard.verify();
529
677
  const behaviorTests = results.filter((result) => result.kind === "test");
530
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
+ );
531
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;
532
688
  const engine = new DeliveryDecisionEngine({ cwd: this.cwd });
533
689
  const projectContext = await engine.detectContext();
534
690
  const deliveryDecision = await engine.determineDecision({
@@ -543,16 +699,18 @@ var EvidenceCollector = class {
543
699
  explicitApprovals: this.explicitApprovals || []
544
700
  });
545
701
  const changedFiles = policyValidation.git?.changedFiles || [];
702
+ const deliveryChangedFiles = this.deliveryChangedFiles || changedFiles;
703
+ const addedFiles = this.deliveryAddedFiles || this.findAddedFiles(deliveryChangedFiles);
546
704
  const artifactFidelityCheck = await fidelityGate.verifyRequestFidelity({
547
705
  userRequest,
548
706
  deliveryDecision,
549
707
  projectContext,
550
- changedFiles
708
+ changedFiles: deliveryChangedFiles
551
709
  });
552
710
  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";
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) overallStatus = "BLOCKED";
556
714
  else if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(policyValidation.overallStatus)) overallStatus = "FAIL_QUALITY_GATE";
557
715
  else if (results.some((result) => result.status === "PASS_WITH_NOTES") || policyValidation.overallStatus === "PASS_WITH_NOTES") overallStatus = "PASS_WITH_NOTES";
558
716
  if (!artifactFidelityCheck || !artifactFidelityCheck.passed) {
@@ -564,14 +722,95 @@ var EvidenceCollector = class {
564
722
  overallStatus = "BLOCKED";
565
723
  }
566
724
  }
725
+ const testDecision = decideTestAction({
726
+ executableBehavior,
727
+ changedFiles: deliveryChangedFiles,
728
+ addedFiles,
729
+ deletedFiles: this.deliveryDeletedFiles || [],
730
+ passingBehaviorTest
731
+ });
732
+ const deliveryMode = requestUnderstanding?.deliveryMode || (implementation ? "workspace" : "answer-only");
733
+ const mutationIntent = requestUnderstanding?.mutationIntent || (implementation ? "write" : "readonly");
734
+ const deliveryOutcome = resolveDeliveryOutcome({
735
+ deliveryMode,
736
+ mutationIntent,
737
+ changedFiles: deliveryChangedFiles,
738
+ validationStatus: overallStatus,
739
+ validationObserved,
740
+ deliveryDecision: deliveryDecision.decision,
741
+ claimedOutcome: this.claimedOutcome
742
+ });
743
+ if (["NEEDS_CLARIFICATION", "NOT_DELIVERED"].includes(deliveryOutcome) && ["PASS", "PASS_WITH_NOTES"].includes(overallStatus)) {
744
+ overallStatus = "BLOCKED";
745
+ }
567
746
  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.");
747
+ if (implementation && deliveryChangedFiles.length > 0 && !validationObserved) limitations.push(blockingDiagnostic(
748
+ "validation",
749
+ "the requested workspace changed but no meaningful validation result was observed",
750
+ "at least one proportional validation command must pass after the change",
751
+ "run the relevant check and finalize again"
752
+ ));
753
+ if (implementation && executableBehavior && tasks.length === 0) limitations.push(blockingDiagnostic(
754
+ "validation-path",
755
+ "executable implementation work has no available validation command",
756
+ "the project must provide a proportional syntax, build, or behavior check",
757
+ "configure or run the smallest relevant validator"
758
+ ));
759
+ if (behaviorTestRequired && behaviorTests.length === 0) {
760
+ limitations.push(blockingDiagnostic(
761
+ "behavior-test",
762
+ "UI or test-capable executable behavior changed without an automated behavior test",
763
+ "a relevant automated behavior test must pass",
764
+ "run or configure the focused behavior test; build, lint, and screenshots are not substitutes"
765
+ ));
766
+ }
767
+ for (const result of results) {
768
+ if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(result.status)) {
769
+ limitations.push(blockingDiagnostic(
770
+ `command:${result.name}`,
771
+ `${result.summary || result.status} (${result.status})`,
772
+ `${Array.isArray(result.command) ? result.command.join(" ") : result.command} must exit successfully`,
773
+ "correct the failure and rerun this command"
774
+ ));
775
+ }
571
776
  }
572
777
  for (const [name, check] of Object.entries(policyValidation.checks || {})) {
573
778
  if (check.status === "PASS_WITH_NOTES" && check.reason) limitations.push(`${name}: ${check.reason}`);
779
+ if (["FAIL", "BLOCKED", "FAIL_QUALITY_GATE"].includes(check.status)) {
780
+ limitations.push(blockingDiagnostic(
781
+ `quality:${name}`,
782
+ check.reason || check.status,
783
+ `${name} must pass under the active risk/evidence policy`,
784
+ "satisfy the stated quality requirement and finalize again"
785
+ ));
786
+ }
787
+ }
788
+ if (!artifactFidelityCheck?.passed) {
789
+ const detail = artifactFidelityCheck?.reason || artifactFidelityCheck?.violations?.join("; ") || "the observed artifact does not match the requested delivery shape";
790
+ limitations.push(blockingDiagnostic(
791
+ "artifact-fidelity",
792
+ detail,
793
+ "the delivered artifact must match the requested stack and shape",
794
+ "correct the artifact or clarify the requested target"
795
+ ));
796
+ }
797
+ if (implementation && (!deliveryDecision?.decision || deliveryDecision.decision.startsWith("BLOCK") || deliveryDecision.decision.includes("BLOCKED") || deliveryDecision.decision === "REQUIRE_EXPLICIT_AUTHORIZATION" || deliveryDecision.decision === "RELEASE_OR_DEPLOY_REQUIRES_APPROVAL")) {
798
+ limitations.push(blockingDiagnostic(
799
+ "delivery-decision",
800
+ deliveryDecision?.reason || deliveryDecision?.decision || "no delivery decision was produced",
801
+ "the delivery decision must authorize this workspace outcome",
802
+ "satisfy the stated decision requirement before finalizing"
803
+ ));
804
+ }
805
+ if (["NEEDS_CLARIFICATION", "NOT_DELIVERED"].includes(deliveryOutcome)) {
806
+ limitations.push(blockingDiagnostic(
807
+ "delivery-outcome",
808
+ `the resolved outcome is ${deliveryOutcome}`,
809
+ "a workspace request requires a scoped change or verified ALREADY_SATISFIED result plus proportional validation",
810
+ "complete the missing delivery condition or request clarification"
811
+ ));
574
812
  }
813
+ const uniqueLimitations = [...new Set(limitations)];
575
814
  let visualEvidence = null;
576
815
  if (this.visualDist) {
577
816
  console.log(`
@@ -600,14 +839,26 @@ var EvidenceCollector = class {
600
839
  branchRecovery: this.branchRecovery,
601
840
  branch: policyValidation.git?.branch || "unknown",
602
841
  changedFiles,
842
+ deliveryChangedFiles,
843
+ mutationOwner: routingDecision?.mutationOwner || null,
844
+ capabilitiesUsed: requestUnderstanding?.capabilityRoles || [],
845
+ deliveryMode,
846
+ deliveryOutcome,
847
+ testAction: testDecision.action,
848
+ testReason: testDecision.reason,
603
849
  status: publicStatus(overallStatus),
604
850
  internalStatus: overallStatus,
605
851
  commands: results,
606
852
  checks: policyValidation.checks,
607
- limitations,
853
+ limitations: uniqueLimitations,
608
854
  deliveryDecision,
609
855
  artifactFidelityCheck,
610
- visualEvidence
856
+ visualEvidence,
857
+ workflowId: this.workflowEvidence?.workflowId || void 0,
858
+ baseSha: this.workflowEvidence?.baseSha || void 0,
859
+ artifactHashes: this.workflowEvidence?.artifactHashes || void 0,
860
+ phaseOrder: this.workflowEvidence?.phaseOrder || void 0,
861
+ confirmedActors: this.workflowEvidence?.confirmedActors || void 0
611
862
  };
612
863
  if (writeArtifact) {
613
864
  await fs3.writeFile(path3.join(this.cwd, "EVIDENCE.json"), JSON.stringify(evidence, null, 2));
@@ -619,6 +870,7 @@ var EvidenceCollector = class {
619
870
  // src/core/validation/validation-planner.ts
620
871
  import fs4 from "fs/promises";
621
872
  import path4 from "path";
873
+ import { spawnSync as spawnSync2 } from "child_process";
622
874
  var ValidationPlanner = class {
623
875
  cwd;
624
876
  qualityGuard;
@@ -628,26 +880,80 @@ var ValidationPlanner = class {
628
880
  this.qualityGuard = qualityGuard;
629
881
  this.stackDetector = new StackDetector({ cwd });
630
882
  }
631
- async plan(profile = "generic") {
883
+ async plan(profile = "generic", { requireObservedValidation = false } = {}) {
632
884
  const stacks = await this.stackDetector.detect();
633
885
  const tasks = [];
634
- const changedFiles = this.qualityGuard.getChangedFiles().filter((file) => {
886
+ const sourceFiles = typeof this.qualityGuard.getScopedChangedFiles === "function" ? this.qualityGuard.getScopedChangedFiles() : this.qualityGuard.getChangedFiles();
887
+ const changedFiles = sourceFiles.filter((file) => {
635
888
  return !/(^|\/)(node_modules|vendor|dist|coverage|\.cache)(\/|$)/.test(String(file).replaceAll("\\", "/"));
636
889
  });
637
890
  const executableBehavior = await this.qualityGuard.hasExecutableBehaviorChanges();
891
+ const highRisk = this.qualityGuard.riskLevel === "high";
892
+ const noChangeVerification = changedFiles.length === 0 && requireObservedValidation && !highRisk;
893
+ const docsOnly = changedFiles.length > 0 && changedFiles.every(
894
+ (file) => /(^|\/)(docs?|readme|changelog)(\/|\.|$)|\.(md|txt)$/i.test(file)
895
+ );
896
+ if (docsOnly || changedFiles.length === 0 && !highRisk && !requireObservedValidation) return tasks;
897
+ const uiChanged = changedFiles.some(
898
+ (file) => /\.(tsx|jsx|vue|html|css|scss|less|blade\.php)$/i.test(file) || /(^|\/)(components|pages|views)\//i.test(file)
899
+ );
638
900
  if (stacks.includes("node")) {
639
901
  try {
640
902
  const pkg = JSON.parse(await fs4.readFile(path4.join(this.cwd, "package.json"), "utf8"));
641
903
  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
904
  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) {
905
+ const addValidate = () => {
906
+ if (scripts.validate && !String(scripts.validate).includes("ai-workflow collect-evidence")) {
907
+ this.addNodeScriptTask(tasks, scripts, "validate", ["npm", "run", "validate"]);
908
+ }
909
+ };
910
+ if (noChangeVerification) {
911
+ if (scripts.test && !this.isNoOpScript(scripts.test)) {
912
+ this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"]);
913
+ } else if (scripts.typecheck) {
914
+ this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
915
+ } else if (scripts.lint) {
916
+ this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
917
+ } else if (scripts.build) {
918
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
919
+ } else {
920
+ addValidate();
921
+ }
922
+ } else if (highRisk) {
923
+ this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
924
+ this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"], { rejectNoOp: executableBehavior });
925
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
926
+ this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
927
+ addValidate();
928
+ } else if (executableBehavior) {
929
+ this.addNodeScriptTask(tasks, scripts, "test", ["npm", "test"], { rejectNoOp: true });
930
+ if (uiChanged) {
931
+ if (scripts.build) {
932
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
933
+ } else {
934
+ tasks.push({
935
+ name: "build",
936
+ command: ["npm", "run", "build"],
937
+ kind: "build",
938
+ presetStatus: "FAIL_QUALITY_GATE",
939
+ summary: "Executable UI change requires a configured build script."
940
+ });
941
+ }
942
+ } else if (hasTs) {
943
+ if (scripts.build) this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
944
+ else this.addNodeScriptTask(tasks, scripts, "typecheck", ["npm", "run", "typecheck"]);
945
+ }
946
+ } else if (uiChanged) {
947
+ if (scripts.build) this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
948
+ else this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
949
+ } else if (scripts.lint) {
950
+ this.addNodeScriptTask(tasks, scripts, "lint", ["npm", "run", "lint"]);
951
+ } else if (scripts.build) {
952
+ this.addNodeScriptTask(tasks, scripts, "build", ["npm", "run", "build"]);
953
+ } else {
954
+ addValidate();
955
+ }
956
+ if (hasTs && !scripts.typecheck && !scripts.build && !noChangeVerification) {
651
957
  tasks.push({
652
958
  name: "typecheck",
653
959
  command: ["npm", "run", "typecheck"],
@@ -668,9 +974,10 @@ var ValidationPlanner = class {
668
974
  kind: "lint"
669
975
  });
670
976
  }
671
- if (executableBehavior) {
977
+ if (executableBehavior || changedFiles.length === 0 && requireObservedValidation && tasks.length === 0) {
672
978
  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) {
979
+ const pytestAvailable = highRisk && spawnCommandAvailable("pytest", this.cwd);
980
+ if (hasPytestConfig || pytestAvailable) {
674
981
  tasks.push({
675
982
  name: "python-test",
676
983
  command: ["pytest"],
@@ -690,9 +997,10 @@ var ValidationPlanner = class {
690
997
  });
691
998
  }
692
999
  }
693
- if (executableBehavior) {
1000
+ if (executableBehavior || changedFiles.length === 0 && requireObservedValidation && tasks.length === 0) {
694
1001
  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) {
1002
+ const localPhpUnit = await fs4.access(path4.join(this.cwd, "vendor/bin/phpunit")).then(() => true).catch(() => false);
1003
+ if (hasPhpUnitXml || highRisk && localPhpUnit) {
696
1004
  tasks.push({
697
1005
  name: "php-test",
698
1006
  command: ["vendor/bin/phpunit"],
@@ -724,11 +1032,16 @@ var ValidationPlanner = class {
724
1032
  return /^(echo\b|true$|exit\s+0$|node\s+-e\s+["']?console\.log)/.test(value) || value.includes("tests-pass");
725
1033
  }
726
1034
  };
1035
+ function spawnCommandAvailable(command, cwd) {
1036
+ const lookup = process.platform === "win32" ? ["where", [command]] : ["which", [command]];
1037
+ return spawnSync2(lookup[0], lookup[1], { cwd, stdio: "ignore" }).status === 0;
1038
+ }
727
1039
 
728
1040
  export {
729
1041
  QualityGuard,
730
1042
  VisualVerifier,
1043
+ parseDeliveryOutcomeClaim,
731
1044
  EvidenceCollector,
732
1045
  ValidationPlanner
733
1046
  };
734
- //# sourceMappingURL=chunk-BDZPUAEX.js.map
1047
+ //# sourceMappingURL=chunk-LD7EHAU2.js.map