@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.
- package/CHANGELOG.md +69 -0
- package/README.md +3 -2
- package/bin/ai-workflow.js +1203 -301
- package/bin/ai-workflow.js.map +1 -1
- package/{chunk-W4RTQWVQ.js → chunk-4FI5ODAM.js} +879 -759
- package/chunk-AINIR25D.js +120 -0
- package/chunk-AY33SA5W.js +46 -0
- package/{evidence-validator-76ZQQYDU.js → chunk-FNT7DN3N.js} +22 -2
- package/{chunk-XW747GIG.js → chunk-H7GIKXFO.js} +339 -14
- package/{chunk-BDZPUAEX.js → chunk-LD7EHAU2.js} +352 -39
- package/chunk-YOBY5C72.js +76 -0
- package/core/index.d.ts +51 -1
- package/core/index.js +5 -3
- 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/cli-reference.md +47 -2
- 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/approval-receipt.schema.json +35 -0
- package/dist-assets/schemas/evidence.schema.json +93 -1
- package/evidence-validator-HS3NTWAB.js +8 -0
- package/package.json +17 -2
- package/skill-4MEGJ3DO.js +211 -0
- package/skill-frontmatter-linter-FMJADOK4.js +14 -0
- package/{validate-A46WUBVZ.js → validate-F3ZH63LI.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;
|
|
@@ -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" &&
|
|
474
|
-
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("-", " ");
|
|
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({
|
|
525
|
-
|
|
526
|
-
|
|
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 (
|
|
554
|
-
else if (
|
|
555
|
-
else if (
|
|
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 &&
|
|
569
|
-
|
|
570
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
1047
|
+
//# sourceMappingURL=chunk-LD7EHAU2.js.map
|