@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.
- package/CHANGELOG.md +41 -0
- package/bin/ai-workflow.js +193 -49
- package/bin/ai-workflow.js.map +1 -1
- package/{chunk-LI76KI7C.js → chunk-4FI5ODAM.js} +259 -296
- package/{chunk-Y4RLP6ZM.js → chunk-GLYX3HEY.js} +353 -39
- package/{chunk-XW747GIG.js → chunk-H7GIKXFO.js} +339 -14
- package/core/index.d.ts +20 -1
- package/core/index.js +2 -2
- package/dist-assets/agents/atlas.md +25 -6
- package/dist-assets/commands/atlas.md +7 -2
- package/dist-assets/docs/QUICKSTART.md +10 -1
- package/dist-assets/docs/compatibility/provider-usage.md +16 -1
- package/dist-assets/docs/compatibility/runtime-matrix.md +13 -2
- package/dist-assets/docs/policies/06-FINAL_EVIDENCE_CONTRACT.md +11 -1
- package/dist-assets/docs/policies/SKILLS_COMMON_GOVERNANCE.md +16 -0
- package/dist-assets/schemas/README.md +4 -0
- package/dist-assets/schemas/evidence.schema.json +60 -1
- package/package.json +1 -1
- package/{validate-IEHLAVZ6.js → validate-DDKN6ORS.js} +3 -3
|
@@ -2,8 +2,9 @@ import {
|
|
|
2
2
|
ArtifactFidelityGate,
|
|
3
3
|
DeliveryDecisionEngine,
|
|
4
4
|
MergeGate,
|
|
5
|
+
RequestRouter,
|
|
5
6
|
StackDetector
|
|
6
|
-
} from "./chunk-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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" &&
|
|
477
|
-
return branch.replace(
|
|
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({
|
|
528
|
-
|
|
529
|
-
|
|
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 (
|
|
557
|
-
else if (
|
|
558
|
-
else if (
|
|
559
|
-
|
|
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 &&
|
|
572
|
-
|
|
573
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1056
|
+
//# sourceMappingURL=chunk-GLYX3HEY.js.map
|