@tea-agent/loop-agent 0.24.9 → 0.24.11-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/commands/init.js +2 -16
- package/dist/executors/shell-executor.js +6 -122
- package/dist/worker/cli.js +14 -59
- package/dist/worker/delivery/git-transaction.js +7 -32
- package/dist/worker/delivery/verification-bundle.js +5 -26
- package/dist/worker/observe/routes.js +0 -17
- package/dist/worker/observe/static/api.js +0 -9
- package/dist/worker/observe/static/constants.js +0 -9
- package/dist/worker/observe/static/state.js +0 -14
- package/dist/worker/observe/static/styles.css +0 -74
- package/dist/worker/observe/static/views/dag-inspector.js +15 -371
- package/dist/worker/outcomes/projector.js +1 -5
- package/dist/worker/runner/run-ready.js +1 -41
- package/dist/workflows/dag/backend-test-markdown-workflow.js +9 -75
- package/dist/workflows/dag/backend-test-result-contract.js +0 -103
- package/dist/workflows/dag/frontend-test-result-contract.js +41 -106
- package/dist/workflows/dag/init-hybrid.js +14 -13
- package/dist/workflows/dag/node-execution.js +2 -3
- package/dist/workflows/dag/types.js +0 -2
- package/dist/workflows/dag/validate.js +2 -3
- package/docs/architecture/worker-and-feature.md +11 -0
- package/docs/init-surface.manifest.json +3 -0
- package/docs/templates/agent-dag.schema.json +9 -0
- package/docs/templates/frontend-implementation-contract.schema.json +2 -2
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +53 -0
- package/skills/frontend-implementation/SKILL.md +3 -7
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/dist/worker/observe/dag-run-artifacts.js +0 -90
- package/dist/worker/observe/node-input.js +0 -444
|
@@ -413,18 +413,6 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
413
413
|
if (!hasAssertableExpectedResult(expected)) {
|
|
414
414
|
findings.push(`${testCase.id} has no structured expected result`);
|
|
415
415
|
}
|
|
416
|
-
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
417
|
-
const mappedScripts = extractMappedPytestScripts(testCase.body);
|
|
418
|
-
if (mappedScripts.length === 0) {
|
|
419
|
-
findings.push(`${testCase.id} has no mapped pytest script in Automation Notes/自动化映射 (expected ${expectedScript} from module ${path.basename(relativeFile)})`);
|
|
420
|
-
}
|
|
421
|
-
else {
|
|
422
|
-
for (const script of mappedScripts) {
|
|
423
|
-
if (script !== expectedScript) {
|
|
424
|
-
findings.push(`${testCase.id} automation mapping ${script} must equal module one-to-one path ${expectedScript} (from ${path.basename(relativeFile)})`);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
416
|
}
|
|
429
417
|
}
|
|
430
418
|
const missing = input.requiredRequirementIds.filter((id) => !coveredAc.has(id));
|
|
@@ -466,27 +454,6 @@ export async function validateBackendMarkdownCases(input) {
|
|
|
466
454
|
function collectMarkdownCaseIds(markdown) {
|
|
467
455
|
return splitCases(markdown).map((testCase) => testCase.id);
|
|
468
456
|
}
|
|
469
|
-
/**
|
|
470
|
-
* Derive the stable module stem used for Markdown ↔ pytest one-to-one mapping.
|
|
471
|
-
* `testcase/md/<module>.md` → stem → `testcase/test_<stem>.py`.
|
|
472
|
-
* Example: `BE-HEALTH.md` / `be-health.md` / `order-api.md` → `be_health` / `order_api`.
|
|
473
|
-
*/
|
|
474
|
-
export function normalizeBackendTestModuleStem(moduleFileName) {
|
|
475
|
-
const base = path.basename(moduleFileName).replace(/\.md$/i, "");
|
|
476
|
-
const stem = base
|
|
477
|
-
.toLowerCase()
|
|
478
|
-
.replace(/[^a-z0-9]+/g, "_")
|
|
479
|
-
.replace(/^_+|_+$/g, "")
|
|
480
|
-
.replace(/_+/g, "_");
|
|
481
|
-
if (!stem) {
|
|
482
|
-
throw new Error(`cannot derive backend-test module stem from: ${moduleFileName}`);
|
|
483
|
-
}
|
|
484
|
-
return stem;
|
|
485
|
-
}
|
|
486
|
-
/** Expected pytest script path for a Markdown module file (basename or relative path). */
|
|
487
|
-
export function expectedBackendTestPytestScriptForMarkdownModule(moduleFileName) {
|
|
488
|
-
return `testcase/test_${normalizeBackendTestModuleStem(moduleFileName)}.py`;
|
|
489
|
-
}
|
|
490
457
|
function extractMappedPytestScripts(testCaseBody) {
|
|
491
458
|
const automation = sectionBody(testCaseBody, CASE_SECTION_ALIASES.automationNotes);
|
|
492
459
|
if (!automation.trim())
|
|
@@ -495,15 +462,6 @@ function extractMappedPytestScripts(testCaseBody) {
|
|
|
495
462
|
.map((match) => match[1].replaceAll("\\", "/"))
|
|
496
463
|
.filter((value) => !value.includes("..")));
|
|
497
464
|
}
|
|
498
|
-
function isSafeBackendPytestScript(script, workspaceRoot) {
|
|
499
|
-
const absolute = path.resolve(workspaceRoot, script);
|
|
500
|
-
const relative = path.relative(workspaceRoot, absolute).replaceAll(path.sep, "/");
|
|
501
|
-
return (!relative.startsWith("..") &&
|
|
502
|
-
!path.isAbsolute(relative) &&
|
|
503
|
-
script.startsWith("testcase/") &&
|
|
504
|
-
!script.includes("..") &&
|
|
505
|
-
/^test_.*\.py$/i.test(path.basename(script)));
|
|
506
|
-
}
|
|
507
465
|
function testFunctionRegion(input) {
|
|
508
466
|
const functionLineStart = input.source.lastIndexOf("\n", input.functionIndex - 1) + 1;
|
|
509
467
|
const functionLine = input.source.slice(functionLineStart, input.functionHeaderEnd);
|
|
@@ -652,50 +610,26 @@ export async function collectBackendTestMappedPytestScripts(workspaceRoot) {
|
|
|
652
610
|
for (const file of files) {
|
|
653
611
|
if (path.basename(file).toLowerCase() === "readme.md")
|
|
654
612
|
continue;
|
|
655
|
-
const relativeFile = path.relative(workspaceRoot, file).replaceAll(path.sep, "/");
|
|
656
|
-
const expectedScript = expectedBackendTestPytestScriptForMarkdownModule(relativeFile);
|
|
657
613
|
const markdown = await readFile(file, "utf8");
|
|
658
|
-
const mappedFromModule = new Set();
|
|
659
614
|
for (const testCase of splitCases(markdown)) {
|
|
660
|
-
for (const script of extractMappedPytestScripts(testCase.body))
|
|
661
|
-
mappedFromModule.add(script);
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
// Prefer explicit mappings, but recover to the deterministic module stem path
|
|
665
|
-
// when the model wrote the correct one-to-one file while Markdown still names
|
|
666
|
-
// a drifted script (common: health.md maps test_health.py but writer emitted
|
|
667
|
-
// test_be_health.py for BE-HEALTH.md, or the reverse).
|
|
668
|
-
if (mappedFromModule.size === 0) {
|
|
669
|
-
if (await exists(path.resolve(workspaceRoot, expectedScript))) {
|
|
670
|
-
scripts.add(expectedScript);
|
|
671
|
-
}
|
|
672
|
-
continue;
|
|
673
|
-
}
|
|
674
|
-
for (const script of mappedFromModule) {
|
|
675
|
-
if (!isSafeBackendPytestScript(script, workspaceRoot)) {
|
|
676
|
-
throw new Error(`unsafe mapped pytest script: ${script}`);
|
|
677
|
-
}
|
|
678
|
-
if (await exists(path.resolve(workspaceRoot, script))) {
|
|
615
|
+
for (const script of extractMappedPytestScripts(testCase.body))
|
|
679
616
|
scripts.add(script);
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
if (isSafeBackendPytestScript(expectedScript, workspaceRoot) &&
|
|
683
|
-
(await exists(path.resolve(workspaceRoot, expectedScript)))) {
|
|
684
|
-
scripts.add(expectedScript);
|
|
685
|
-
continue;
|
|
686
|
-
}
|
|
687
|
-
throw new Error(`mapped pytest script is missing: ${script} (module one-to-one path ${expectedScript} also missing)`);
|
|
688
617
|
}
|
|
689
618
|
}
|
|
690
619
|
if (scripts.size === 0) {
|
|
691
620
|
throw new Error("no pytest scripts are mapped by final Markdown cases");
|
|
692
621
|
}
|
|
693
622
|
const safeScripts = [];
|
|
694
|
-
for (const script of unique(
|
|
695
|
-
|
|
623
|
+
for (const script of unique(scripts)) {
|
|
624
|
+
const absolute = path.resolve(workspaceRoot, script);
|
|
625
|
+
const relative = path.relative(workspaceRoot, absolute);
|
|
626
|
+
if (relative.startsWith("..") ||
|
|
627
|
+
path.isAbsolute(relative) ||
|
|
628
|
+
!script.startsWith("testcase/") ||
|
|
629
|
+
!/^test_.*\.py$/i.test(path.basename(script))) {
|
|
696
630
|
throw new Error(`unsafe mapped pytest script: ${script}`);
|
|
697
631
|
}
|
|
698
|
-
if (!(await exists(
|
|
632
|
+
if (!(await exists(absolute))) {
|
|
699
633
|
throw new Error(`mapped pytest script is missing: ${script}`);
|
|
700
634
|
}
|
|
701
635
|
safeScripts.push(script);
|
|
@@ -773,109 +773,6 @@ async function readPytestExitCode(runDir, fromNodeId, exitRelativePath = "report
|
|
|
773
773
|
}
|
|
774
774
|
throw new Error("missing valid pytest exit evidence");
|
|
775
775
|
}
|
|
776
|
-
/**
|
|
777
|
-
* Materialize Result v1 from a pytest-html 4.x self-contained report (Markdown-first
|
|
778
|
-
* 8-node pipeline). `junit.relativePath` records the HTML path for integrity hashing
|
|
779
|
-
* (same field used historically for JUnit XML path).
|
|
780
|
-
*/
|
|
781
|
-
export async function materializeBackendTestResultFromPytestHtml(input) {
|
|
782
|
-
const htmlRelativePath = input.htmlRelativePath ?? "reports/backend-test.html";
|
|
783
|
-
const artifactName = input.artifactName ?? "backend-test-result.json";
|
|
784
|
-
const outputDir = input.outputDir ?? "contracts";
|
|
785
|
-
if (!/^[a-z0-9][a-z0-9._-]*\.json$/.test(artifactName) ||
|
|
786
|
-
!/^[a-z0-9][a-z0-9._-]*$/.test(outputDir)) {
|
|
787
|
-
throw new Error("unsafe structured artifact path");
|
|
788
|
-
}
|
|
789
|
-
let html = input.htmlContent;
|
|
790
|
-
if (html === undefined) {
|
|
791
|
-
const htmlAbs = path.join(input.runDir, ...htmlRelativePath.split("/"));
|
|
792
|
-
try {
|
|
793
|
-
html = await readFile(htmlAbs, "utf8");
|
|
794
|
-
}
|
|
795
|
-
catch {
|
|
796
|
-
throw new Error(`missing pytest-html report at ${htmlRelativePath} (fail-closed for Result v1)`);
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
if (!html.trim()) {
|
|
800
|
-
throw new Error("invalid pytest-html report: empty report");
|
|
801
|
-
}
|
|
802
|
-
let parsed;
|
|
803
|
-
try {
|
|
804
|
-
parsed = parsePytestHtmlReport(html);
|
|
805
|
-
}
|
|
806
|
-
catch (error) {
|
|
807
|
-
throw new Error(`invalid pytest-html report: ${error instanceof Error ? error.message : String(error)}`);
|
|
808
|
-
}
|
|
809
|
-
const sha256 = createHash("sha256").update(html).digest("hex");
|
|
810
|
-
const exit = input.pytestExitCode;
|
|
811
|
-
let executionStatus = "completed";
|
|
812
|
-
let collectionStatus = "ok";
|
|
813
|
-
let outcome = "passed";
|
|
814
|
-
const looksLikeCollection = exit === 2 ||
|
|
815
|
-
(parsed.errors > 0 && parsed.passed + parsed.failed === 0) ||
|
|
816
|
-
parsed.failures.some((f) => f.kind === "error" &&
|
|
817
|
-
/collect|import|syntax/i.test(`${f.name} ${f.message}`));
|
|
818
|
-
if (looksLikeCollection && (parsed.errors > 0 || exit >= 2)) {
|
|
819
|
-
executionStatus = "collection-error";
|
|
820
|
-
collectionStatus = "error";
|
|
821
|
-
outcome = "collection-error";
|
|
822
|
-
}
|
|
823
|
-
else if (exit >= 2 && parsed.failed === 0 && parsed.errors === 0) {
|
|
824
|
-
executionStatus = "command-error";
|
|
825
|
-
collectionStatus = "unknown";
|
|
826
|
-
outcome = "command-error";
|
|
827
|
-
}
|
|
828
|
-
else if (parsed.failed > 0 || parsed.errors > 0 || exit === 1) {
|
|
829
|
-
executionStatus = "completed";
|
|
830
|
-
collectionStatus = "ok";
|
|
831
|
-
outcome = "completed-with-failures";
|
|
832
|
-
}
|
|
833
|
-
else if (exit === 0 && parsed.failed === 0 && parsed.errors === 0) {
|
|
834
|
-
executionStatus = "completed";
|
|
835
|
-
collectionStatus = "ok";
|
|
836
|
-
outcome = "passed";
|
|
837
|
-
}
|
|
838
|
-
else {
|
|
839
|
-
executionStatus = "command-error";
|
|
840
|
-
collectionStatus = "unknown";
|
|
841
|
-
outcome = "command-error";
|
|
842
|
-
}
|
|
843
|
-
const commandSummary = input.commandSummary ??
|
|
844
|
-
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 python -m pytest -v -p no:cacheprovider --html=${htmlRelativePath} --self-contained-html`;
|
|
845
|
-
assertNoSecrets("commandSummary", commandSummary);
|
|
846
|
-
const result = backendTestResultContractSchema.parse({
|
|
847
|
-
schemaVersion: 1,
|
|
848
|
-
executionStatus,
|
|
849
|
-
pytestExitCode: exit,
|
|
850
|
-
collectionStatus,
|
|
851
|
-
tests: parsed.tests,
|
|
852
|
-
passed: parsed.passed,
|
|
853
|
-
failed: parsed.failed,
|
|
854
|
-
error: parsed.errors,
|
|
855
|
-
skipped: parsed.skipped,
|
|
856
|
-
durationMs: parsed.durationMs,
|
|
857
|
-
junit: {
|
|
858
|
-
relativePath: htmlRelativePath,
|
|
859
|
-
sha256,
|
|
860
|
-
},
|
|
861
|
-
commandSummary,
|
|
862
|
-
failures: parsed.failures.map((f) => ({
|
|
863
|
-
classname: f.classname || "unknown",
|
|
864
|
-
name: f.name || "unknown",
|
|
865
|
-
message: truncate(f.message || "failure"),
|
|
866
|
-
kind: f.kind,
|
|
867
|
-
})),
|
|
868
|
-
outcome,
|
|
869
|
-
});
|
|
870
|
-
const relativePath = path.posix.join(outputDir, artifactName);
|
|
871
|
-
const artifactPath = await writeDagRunJsonArtifact(input.runDir, relativePath, result);
|
|
872
|
-
const serialized = `${JSON.stringify(result, null, 2)}\n`;
|
|
873
|
-
return {
|
|
874
|
-
path: artifactPath,
|
|
875
|
-
sha256: createHash("sha256").update(serialized).digest("hex"),
|
|
876
|
-
schemaId: BACKEND_TEST_RESULT_SCHEMA_ID,
|
|
877
|
-
};
|
|
878
|
-
}
|
|
879
776
|
export async function materializeBackendTestResultFromRunDir(input) {
|
|
880
777
|
if (!/^[a-z0-9][a-z0-9._-]*\.json$/.test(input.artifactName) ||
|
|
881
778
|
!/^[a-z0-9][a-z0-9._-]*$/.test(input.outputDir)) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { lstat, readFile, realpath
|
|
2
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
@@ -68,111 +68,6 @@ export const frontendTestResultContractSchema = z.object({
|
|
|
68
68
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["integrationMode"], message: "real integration requires passed outcome" });
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
|
-
async function isNonEmptyFile(filePath) {
|
|
72
|
-
try {
|
|
73
|
-
const info = await stat(filePath);
|
|
74
|
-
return info.isFile() && info.size > 0;
|
|
75
|
-
}
|
|
76
|
-
catch {
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
function isSafeEvidenceShellPath(value) {
|
|
81
|
-
return (typeof value === "string" &&
|
|
82
|
-
value.length > 0 &&
|
|
83
|
-
!path.isAbsolute(value) &&
|
|
84
|
-
!path.win32.isAbsolute(value) &&
|
|
85
|
-
!value.includes(".."));
|
|
86
|
-
}
|
|
87
|
-
/**
|
|
88
|
-
* Validate frontend browser evidence without going through a shell command.
|
|
89
|
-
* Keeping this in the Node executor avoids CMD/Git Bash/PowerShell quoting and
|
|
90
|
-
* backslash interpretation for the former long `node -e` command.
|
|
91
|
-
*/
|
|
92
|
-
export async function validateFrontendCaseEvidence(input) {
|
|
93
|
-
const manifestPath = path.join(input.workspaceRoot, "testcase/frontend/cases/manifest.json");
|
|
94
|
-
try {
|
|
95
|
-
await stat(manifestPath);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
throw new Error("missing testcase/frontend/cases/manifest.json");
|
|
99
|
-
}
|
|
100
|
-
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
101
|
-
if (!Array.isArray(manifest.cases))
|
|
102
|
-
throw new Error("invalid frontend case manifest");
|
|
103
|
-
const statuses = new Set(["passed", "failed", "blocked"]);
|
|
104
|
-
const issues = [];
|
|
105
|
-
let hardFail = false;
|
|
106
|
-
for (const rawCase of manifest.cases) {
|
|
107
|
-
const item = rawCase;
|
|
108
|
-
const id = typeof item?.caseId === "string" ? item.caseId : "?";
|
|
109
|
-
const dir = item?.evidenceDir;
|
|
110
|
-
const prefix = `testcase/frontend/evidence/${id}`;
|
|
111
|
-
if (!item ||
|
|
112
|
-
typeof item.caseId !== "string" ||
|
|
113
|
-
typeof dir !== "string" ||
|
|
114
|
-
!isSafeEvidenceShellPath(dir) ||
|
|
115
|
-
!(dir === prefix || dir.startsWith(`${prefix}/`))) {
|
|
116
|
-
hardFail = true;
|
|
117
|
-
issues.push({ ruleId: "unsafe-evidence-dir", caseId: id, detail: String(dir) });
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const execution = path.join(input.workspaceRoot, dir, "execution.md");
|
|
121
|
-
const resultPath = path.join(input.workspaceRoot, dir, "case-result.json");
|
|
122
|
-
if (!(await isNonEmptyFile(execution))) {
|
|
123
|
-
issues.push({ ruleId: "missing-execution", caseId: id, detail: "execution.md is missing or empty" });
|
|
124
|
-
}
|
|
125
|
-
let result = null;
|
|
126
|
-
if (!(await isNonEmptyFile(resultPath))) {
|
|
127
|
-
issues.push({ ruleId: "missing-case-result", caseId: id, detail: "case-result.json is missing" });
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
try {
|
|
131
|
-
result = JSON.parse(await readFile(resultPath, "utf8"));
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
134
|
-
issues.push({ ruleId: "invalid-case-result", caseId: id, detail: "case-result.json is malformed" });
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (!result || result.caseId !== id) {
|
|
138
|
-
issues.push({ ruleId: "case-result-identity", caseId: id, detail: "case-result caseId does not match manifest" });
|
|
139
|
-
}
|
|
140
|
-
if (!statuses.has(String(result?.status))) {
|
|
141
|
-
issues.push({ ruleId: "invalid-case-status", caseId: id, detail: "status must be passed, failed, or blocked" });
|
|
142
|
-
}
|
|
143
|
-
if (!Array.isArray(result?.evidencePaths)) {
|
|
144
|
-
issues.push({ ruleId: "invalid-evidence-paths", caseId: id, detail: "evidencePaths must be an array" });
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
for (const evidencePath of result.evidencePaths) {
|
|
148
|
-
if (!isSafeEvidenceShellPath(evidencePath)) {
|
|
149
|
-
hardFail = true;
|
|
150
|
-
issues.push({ ruleId: "unsafe-evidence-path", caseId: id, detail: String(evidencePath) });
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
const currentPrefix = `${prefix}/`;
|
|
154
|
-
if (evidencePath.startsWith("testcase/frontend/evidence/") && !evidencePath.startsWith(currentPrefix)) {
|
|
155
|
-
hardFail = true;
|
|
156
|
-
issues.push({ ruleId: "cross-case-evidence-path", caseId: id, detail: evidencePath });
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
const target = evidencePath.startsWith("testcase/")
|
|
160
|
-
? path.join(input.workspaceRoot, evidencePath)
|
|
161
|
-
: path.join(input.workspaceRoot, dir, evidencePath);
|
|
162
|
-
if (!(await isNonEmptyFile(target))) {
|
|
163
|
-
issues.push({ ruleId: "missing-evidence", caseId: id, detail: `${evidencePath} is missing or empty` });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
if (result?.status === "passed" && (!Array.isArray(result.evidencePaths) || !result.evidencePaths.some((entry) => /\.(png|jpg|jpeg|webp|zip|har|webm|mp4)$/i.test(String(entry))))) {
|
|
168
|
-
issues.push({ ruleId: "passed-without-browser-evidence", caseId: id, detail: "passed case has no screenshot, HAR, video, or equivalent browser artifact" });
|
|
169
|
-
}
|
|
170
|
-
if (result?.status === "blocked" && (typeof result.blockedReason !== "string" || !result.blockedReason.trim())) {
|
|
171
|
-
issues.push({ ruleId: "blocked-reason", caseId: id, detail: "blocked result has no usable reason" });
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return { cases: manifest.cases.length, issues, hardFail };
|
|
175
|
-
}
|
|
176
71
|
function sha256(content) {
|
|
177
72
|
return createHash("sha256").update(content).digest("hex");
|
|
178
73
|
}
|
|
@@ -319,3 +214,43 @@ export function buildFrontendTestOutcomeGateShellSnippet(options) {
|
|
|
319
214
|
`node -e 'const fs=require("fs");const r=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));const ok=r.outcome==="passed"&&r.integrationMode==="real"&&Number(r.totals?.failed||0)===0&&Number(r.totals?.blocked||0)===0&&Array.isArray(r.acceptanceCoverage?.missing)&&r.acceptanceCoverage.missing.length===0;console.log("frontend-test outcome="+r.outcome+" integrationMode="+r.integrationMode);if(!ok)process.exit(1);' "\${RESULT}"`,
|
|
320
215
|
].join("; ");
|
|
321
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Shared frontend-test evidence advisory check for map children + node 7.
|
|
219
|
+
* Hard-fails only for unsafe evidence directories or evidence paths. Missing,
|
|
220
|
+
* malformed, or empty evidence is reported without mutating case outputs.
|
|
221
|
+
*/
|
|
222
|
+
export function buildFrontendCaseEvidenceValidateShellSnippet() {
|
|
223
|
+
const body = [
|
|
224
|
+
"const fs=require('fs'),path=require('path');",
|
|
225
|
+
"const manifestPath='testcase/frontend/cases/manifest.json';",
|
|
226
|
+
"if(!fs.existsSync(manifestPath))throw new Error('missing '+manifestPath);",
|
|
227
|
+
"const manifest=JSON.parse(fs.readFileSync(manifestPath,'utf8'));",
|
|
228
|
+
"if(!Array.isArray(manifest.cases))throw new Error('invalid frontend case manifest');",
|
|
229
|
+
"const statuses=new Set(['passed','failed','blocked']);",
|
|
230
|
+
"const issues=[];",
|
|
231
|
+
"let hardFail=false;",
|
|
232
|
+
"function isSafeRel(p){return typeof p==='string'&&p.length>0&&!path.isAbsolute(p)&&!path.win32.isAbsolute(p)&&!p.includes('..');}",
|
|
233
|
+
"for(const c of manifest.cases){",
|
|
234
|
+
" const id=c&&typeof c.caseId==='string'?c.caseId:'?';",
|
|
235
|
+
" const dir=c&&c.evidenceDir;",
|
|
236
|
+
" const prefix='testcase/frontend/evidence/'+id;",
|
|
237
|
+
" if(!c||typeof c.caseId!=='string'||typeof dir!=='string'||!isSafeRel(dir)||!(dir===prefix||dir.startsWith(prefix+'/'))){",
|
|
238
|
+
" hardFail=true; issues.push({ruleId:'unsafe-evidence-dir',caseId:id,detail:String(dir)}); continue;",
|
|
239
|
+
" }",
|
|
240
|
+
" const execution=path.join(dir,'execution.md');",
|
|
241
|
+
" const resultPath=path.join(dir,'case-result.json');",
|
|
242
|
+
" if(!fs.existsSync(execution)||!fs.statSync(execution).isFile()||fs.statSync(execution).size===0)issues.push({ruleId:'missing-execution',caseId:id,detail:'execution.md is missing or empty'});",
|
|
243
|
+
" let result=null;",
|
|
244
|
+
" if(!fs.existsSync(resultPath)){issues.push({ruleId:'missing-case-result',caseId:id,detail:'case-result.json is missing'});continue;}",
|
|
245
|
+
" try{result=JSON.parse(fs.readFileSync(resultPath,'utf8'));}catch(e){issues.push({ruleId:'invalid-case-result',caseId:id,detail:'case-result.json is malformed'});continue;}",
|
|
246
|
+
" if(!result||result.caseId!==id)issues.push({ruleId:'case-result-identity',caseId:id,detail:'case-result caseId does not match manifest'});",
|
|
247
|
+
" if(!statuses.has(result&&result.status))issues.push({ruleId:'invalid-case-status',caseId:id,detail:'status must be passed, failed, or blocked'});",
|
|
248
|
+
" if(!Array.isArray(result&&result.evidencePaths)){issues.push({ruleId:'invalid-evidence-paths',caseId:id,detail:'evidencePaths must be an array'});}else{for(const p of result.evidencePaths){if(!isSafeRel(p)){hardFail=true;issues.push({ruleId:'unsafe-evidence-path',caseId:id,detail:String(p)});continue;}const currentPrefix=prefix+'/';if(p.startsWith('testcase/frontend/evidence/')&&!p.startsWith(currentPrefix)){hardFail=true;issues.push({ruleId:'cross-case-evidence-path',caseId:id,detail:p});continue;}const target=p.startsWith('testcase/')?p:path.join(dir,p);if(!fs.existsSync(target)||!fs.statSync(target).isFile()||fs.statSync(target).size===0)issues.push({ruleId:'missing-evidence',caseId:id,detail:p+' is missing or empty'});}}",
|
|
249
|
+
" if(result&&result.status==='passed'&&(!Array.isArray(result.evidencePaths)||!result.evidencePaths.some(p=>/\\.(png|jpg|jpeg|webp|zip|har|webm|mp4)$/i.test(p))))issues.push({ruleId:'passed-without-browser-evidence',caseId:id,detail:'passed case has no screenshot, HAR, video, or equivalent browser artifact'});",
|
|
250
|
+
" if(result&&result.status==='blocked'&&(typeof result.blockedReason!=='string'||!result.blockedReason.trim()))issues.push({ruleId:'blocked-reason',caseId:id,detail:'blocked result has no usable reason'});",
|
|
251
|
+
"}",
|
|
252
|
+
"if(hardFail){console.error('frontend-test evidence hard-fail: '+JSON.stringify(issues)); process.exit(1);}",
|
|
253
|
+
"console.log('frontend case evidence advisory validation ok cases='+manifest.cases.length+' findings='+issues.length+(issues.length?(' issues='+JSON.stringify(issues)):''));",
|
|
254
|
+
].join("");
|
|
255
|
+
return ["node -e", JSON.stringify(body)].join(" ");
|
|
256
|
+
}
|
|
@@ -25,7 +25,7 @@ import { normalizeTaskRequirementText, resolveTaskDagTemplateSelection, } from "
|
|
|
25
25
|
import { BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT, buildBackendTestExecutionPreflightShellSnippet, } from "./backend-test-execution-contract.js";
|
|
26
26
|
import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
|
|
27
27
|
import { buildBackendTestIntakeContext } from "./backend-test-intake-context.js";
|
|
28
|
-
import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
|
|
28
|
+
import { buildFrontendCaseEvidenceValidateShellSnippet, buildFrontendTestOutcomeGateShellSnippet, } from "./frontend-test-result-contract.js";
|
|
29
29
|
import { classifyFrontendRisk, } from "./frontend-risk.js";
|
|
30
30
|
import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
|
|
31
31
|
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
|
|
@@ -3302,9 +3302,8 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3302
3302
|
"Write human-readable content in Simplified Chinese by default. Keep English only for machine-readable IDs and technical literals such as Case/AC/REQ/BR IDs, HTTP methods, paths, field names, enum values, commands, filenames, code symbols and exact source citations.",
|
|
3303
3303
|
"Create testcase/md/README.md as the concise entry page: test objective, target/environment, isolation/cleanup, module summary and a linked case index table with Case ID, Chinese case name, scenario type, endpoint and expected status/result. Avoid repeating every case body in README.",
|
|
3304
3304
|
"Write each module as readable case cards. Every case starts with `## BE-<MODULE>-<NNN>|<中文用例名称>`. The only sections required by the deterministic validator are `### 前置条件`, `### 操作步骤`, and `### 预期结果` (legacy English aliases remain accepted). Add `测试目的`, `验收标准`, `需求依据`, `测试数据`, and `自动化映射` when useful for human readability; every automatable case should explicitly name its target pytest script under `自动化映射` so traceability can scan only that script.",
|
|
3305
|
-
"Name each module file with a stable lowercase business stem such as `testcase/md/health.md` or `testcase/md/resource_notes.md`. Do not use Case-ID-like module filenames such as `BE-HEALTH.md` or `BE-NOTES.md`. For every automatable case, `自动化映射` must name exactly `testcase/test_<module>.py`, where <module> is that Markdown filename without `.md`, lowercased, with non-alphanumeric characters replaced by underscores. Example: `testcase/md/health.md` → `testcase/test_health.py`; `testcase/md/resource_notes.md` → `testcase/test_resource_notes.py`. Never invent a different pytest path in Markdown than the module stem implies.",
|
|
3306
3305
|
"Place steps and their expected results in a compact readable table when that improves clarity; otherwise keep numbered executable steps and numbered/bulleted independently assertable results. Every result must name the observable HTTP status, response field/value, state transition or membership condition, never vague wording such as ‘符合预期’.",
|
|
3307
|
-
"In `自动化映射`, record the planned script path and pytest function name when known
|
|
3306
|
+
"In `自动化映射`, record the planned script path and pytest function name when known. Put implementation-only restrictions in a concise `<details>` block rather than dominating the main case flow. Use only environment-supported fixtures/targets/isolation, record evidence gaps in Chinese, and do not emit JSON, pytest, or execute commands.",
|
|
3308
3307
|
intake.boundedSourceContext, "## Authoritative reference index", JSON.stringify(intake.referenceIndex, null, 2),
|
|
3309
3308
|
"For each index entry, use `readPath` for Pi read-tool calls and copy `path` exactly into Markdown Source References. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.",
|
|
3310
3309
|
"Read only precise indexed references needed for AC/API/field/rule evidence; references remain authoritative over derived text.",
|
|
@@ -3317,8 +3316,8 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3317
3316
|
outputContract: "Review source fidelity and directly revise only testcase/md/**; return concise Markdown, never JSON.",
|
|
3318
3317
|
subtask_prompt: [
|
|
3319
3318
|
"Independently review generated Markdown cases against the task requirements and environment evidence. Treat the files as human-facing test documentation: require clear preconditions, executable steps and assertable expected results; improve names, purpose, metadata and automation mapping where useful while preserving exact machine IDs and technical literals.",
|
|
3320
|
-
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, vague results such as ‘符合预期’,
|
|
3321
|
-
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases,
|
|
3319
|
+
"Check AC completeness/meaning, endpoint, fields/shape, status/error codes, rules, states, documented boundaries/auth, positive/negative coverage, executable steps and assertable results. Reject avoidable English prose, duplicated bilingual wording, repeated boilerplate, oversized unstructured sections, vague results such as ‘符合预期’, and missing script/function mapping where it can be derived.",
|
|
3320
|
+
"Correct testcase/md/** directly: add documented omissions, remove unsupported cases, fix mappings/expectations, merge duplicates, improve navigation/tables/Chinese wording, or record gaps in Chinese. Keep Case IDs, AC/REQ/BR IDs, HTTP methods, paths, fields, enum values, filenames, code symbols and source citations exact. The validator accepts Chinese and legacy English section aliases; retain or converge to the Chinese human-readable headings without losing structure.",
|
|
3322
3321
|
"Read only precise referenced source paths plus requirement sections needed for uncovered ACs. Do not scan the repository, modify source/**, generate pytest, execute tests, or emit JSON.",
|
|
3323
3322
|
intake.boundedSourceContext, "## Authoritative reference index", JSON.stringify(intake.referenceIndex, null, 2),
|
|
3324
3323
|
"For each index entry, use `readPath` for Pi read-tool calls and keep `path` as the exact Markdown Source References citation. Bound files under .harness/tasks/<taskId>/source/** are read-only inputs: reading them is allowed even though writing .harness/** is forbidden. Never resolve `path` relative to the repository root, search for substitutes, or fall back to docs/** when a bound read fails.",
|
|
@@ -3334,7 +3333,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3334
3333
|
subtask_prompt: [
|
|
3335
3334
|
"Convert testcase/md/** to pytest using upstream environment and advisory validation evidence plus only bounded pytest config/conftest. A FAIL advisory report does not authorize inventing missing behavior; use the final Markdown facts that are present.",
|
|
3336
3335
|
"Ensure every final Markdown Case ID appears in at least one real pytest test function or pytest test class method region, preferably as `test_BE_<MODULE>_<NNN>_<description>` and in that function/method docstring. Module-level functions and class-based pytest methods are both supported. Multiple test functions may cover one Case ID; assertions come only from 预期结果/Expected Results and setup comes only from 前置条件 plus any optional 测试数据/自动化映射 or their legacy English aliases.",
|
|
3337
|
-
"Name each generated pytest file so it corresponds one-to-one with its source Markdown module file: for each `testcase/md/<module>.md` (excluding README.md), emit exactly one `testcase/test_<module>.py`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `testcase/md/resource_notes.md` maps to `testcase/test_resource_notes.py`,
|
|
3336
|
+
"Name each generated pytest file so it corresponds one-to-one with its source Markdown module file: for each `testcase/md/<module>.md` (excluding README.md), emit exactly one `testcase/test_<module>.py`. The <module> stem is the Markdown filename without the `.md` extension, lowercased and with non-alphanumeric characters replaced by underscores. For example, `testcase/md/resource_notes.md` maps to `testcase/test_resource_notes.py`, and `testcase/md/order-api.md` maps to `testcase/test_order_api.py`. Never merge multiple Markdown modules into one pytest file, never split one module across several files, and never invent pytest filenames unrelated to the Markdown modules.",
|
|
3338
3337
|
"Generate a reusable HTTP logging helper (or equivalent client wrapper) and call it for every interface request. The request log must include method, URL/path, and request parameters (query plus JSON/body/payload summary). The response log must include status code and response result (JSON/text/body summary), and both records must be visible in pytest stdout/stderr without changing assertions.",
|
|
3339
3338
|
"Compare timestamps and other semantically equivalent protocol values by parsed meaning, not byte-for-byte serialization. In particular, normalize valid ISO-8601 instants before equality/order assertions so differences such as omitted trailing fractional seconds do not create TestBug failures; preserve exact-string assertions only when the Markdown explicitly requires representation equality.",
|
|
3340
3339
|
"Before logging, recursively redact sensitive keys and header values including authorization, proxy-authorization, cookie, set-cookie, token, password, secret, api key and credentials. Never print full Authorization/Cookie values. Apply bounded truncation to serialized request and response bodies (with an explicit truncation marker) so large payloads cannot flood pytest or report artifacts.",
|
|
@@ -3346,7 +3345,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3346
3345
|
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
3347
3346
|
'echo "pytest targets are resolved at runtime from final Markdown 自动化映射"',
|
|
3348
3347
|
].join("; ");
|
|
3349
|
-
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once.
|
|
3348
|
+
const execute = shellNode("execute-backend-pytest-and-html-report-shell", [traceability.id], "markdown-execute-html", "Resolve the final Markdown Automation Notes/自动化映射 to a unique, safe set of testcase/**/test_*.py targets and execute only those scripts exactly once. Validate JUnit, then render the primary self-contained Chinese HTML report from the same JUnit plus final Markdown case metadata without rerun. Keep 测试结论, quality status, failure overview, and a polished per-case result card with concise scenario, automation test name, result, duration, and redacted bounded HTTP request parameters/response results for both passed and failed cases. Do not render a technical/execution evidence section in HTML; retain auditable paths and hashes in facts.", "One scoped pytest execution over Markdown-mapped scripts producing valid JUnit with per-case captured output, self-contained reports/backend-test.html and reports/backend-test.md, plus internal reports/backend-test-facts.md evidence; exit 0/1 with valid evidence continues.", [pytestCommand], 300000);
|
|
3350
3349
|
if (execute.shell) {
|
|
3351
3350
|
execute.shell.envAllowlist = collectBackendTestShellEnvAllowlist(sources);
|
|
3352
3351
|
}
|
|
@@ -3359,11 +3358,11 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3359
3358
|
forbiddenPaths: forbidden,
|
|
3360
3359
|
outputContract: canWriteReport ? "Final Markdown report and L-5 conclusion under docs/test-reports/**; no JSON." : "Final Markdown report and L-5 conclusion in assistant output; no JSON or writes.",
|
|
3361
3360
|
subtask_prompt: [
|
|
3362
|
-
"Generate the final Markdown report from upstream facts and run-owned environment, advisory case-validation, advisory traceability,
|
|
3361
|
+
"Generate the final Markdown report from upstream facts and run-owned environment, advisory case-validation, advisory traceability, JUnit and HTML evidence. Do not emit JSON.",
|
|
3363
3362
|
"Use this exact human-facing section order: 测试结论 → 执行概览 → 质量校验 → 失败分析 → 风险与建议 → 证据与 L-5. Put the decision and key numbers first, use compact tables/bullets, and keep headings concise. Do not paste entire upstream reports, duplicate per-case tables already present in facts, or repeat the same evidence in multiple sections; link to paths/hashes and quote only the findings needed for the conclusion.",
|
|
3364
3363
|
"Always state the exact PASS/FAIL status and findings from nodes 4 and 6. Their FAIL status does not block pytest, but it must remain visible as a quality/traceability risk and must never be rewritten as PASS.",
|
|
3365
3364
|
"Include environment, case quality/review, automation mapping, exact pytest facts, failure classification/analysis, risks, regression recommendations, evidence paths/hashes, coverage availability, and L-5 READY/NOT READY.",
|
|
3366
|
-
"Never override Shell/
|
|
3365
|
+
"Never override Shell/JUnit facts. L-5 requires pass=100%, AC=100%, automation>=90%, line>=80%, branch>=70%, skipped=0 and no blocking Critical risk.",
|
|
3367
3366
|
canWriteReport ? "Write only under docs/test-reports/**." : "Keep the full report in assistant output.",
|
|
3368
3367
|
].join("\n\n"),
|
|
3369
3368
|
};
|
|
@@ -3377,7 +3376,7 @@ async function buildBackendTestHybridDag(sources) {
|
|
|
3377
3376
|
...taskConfig.hardConstraints, ...STANDARD_GLOBAL_CONSTRAINTS,
|
|
3378
3377
|
"backend-test-dag uses exactly 8 real top-level tasks and executes pytest exactly once over only the safe scripts explicitly mapped by final Markdown cases.",
|
|
3379
3378
|
"Model nodes produce Markdown and pytest assets, never backend-test business JSON envelopes.",
|
|
3380
|
-
"Environment, advisory Markdown validation, advisory traceability,
|
|
3379
|
+
"Environment, advisory Markdown validation, advisory traceability, JUnit, HTML and execution facts are deterministic evidence. Nodes 4 and 6 record findings without blocking nodes 5, 7 or 8.",
|
|
3381
3380
|
"Only Markdown case generation/review may read source facts; pytest generation must not read source/**.",
|
|
3382
3381
|
"Functional case IDs use BE-<MODULE>-<NNN>; production code/config, skip/xfail, repair and rerun are forbidden.",
|
|
3383
3382
|
],
|
|
@@ -3504,6 +3503,8 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3504
3503
|
"process.stdout.write(JSON.stringify({cases:manifest.cases}));",
|
|
3505
3504
|
].join("")),
|
|
3506
3505
|
].join(" ");
|
|
3506
|
+
// Heal malformed/missing case evidence to blocked; only unsafe evidenceDir hard-fails.
|
|
3507
|
+
const evidenceValidation = buildFrontendCaseEvidenceValidateShellSnippet();
|
|
3507
3508
|
const frontendTestOutcomeGate = buildFrontendTestOutcomeGateShellSnippet();
|
|
3508
3509
|
const frontendCaseQualityAdvisory = [
|
|
3509
3510
|
"node -e",
|
|
@@ -3751,9 +3752,9 @@ function buildFrontendTestHybridDag(sources) {
|
|
|
3751
3752
|
writeSet: [`${evidenceRoot}/**`],
|
|
3752
3753
|
allowedPaths: ["testcase/frontend/cases/**", `${evidenceRoot}/**`],
|
|
3753
3754
|
forbiddenPaths: forbidden,
|
|
3754
|
-
outputContract: "Deterministic evidence gate: missing/malformed
|
|
3755
|
-
subtask_prompt: "Validate frontend case evidence before result materialization.
|
|
3756
|
-
shell: { commands: [],
|
|
3755
|
+
outputContract: "Deterministic evidence gate: heal missing/malformed case-result to blocked(invalid-evidence-shape); hard-fail only on unsafe evidenceDir. Does not block retrospect.",
|
|
3756
|
+
subtask_prompt: "Validate frontend case evidence before result materialization. Prefer healing bad shapes to blocked so pipeline can still produce a report; only path-escape failures abort the node.",
|
|
3757
|
+
shell: { commands: [evidenceValidation], cwd: ".", timeoutMs: 120000 },
|
|
3757
3758
|
}, {
|
|
3758
3759
|
id: "materialize-frontend-test-result-shell",
|
|
3759
3760
|
depends_on: ["validate-frontend-case-evidence-shell"],
|
|
@@ -593,9 +593,8 @@ export async function executeDagNode(input) {
|
|
|
593
593
|
node.structuredArtifactSha256 = createHash("sha256").update(bytes).digest("hex");
|
|
594
594
|
node.structuredArtifactSchemaId = task.shell.jsonArtifactGate.schemaId;
|
|
595
595
|
}
|
|
596
|
-
else if (task.shell?.backendTestPipeline === "classification-result-context"
|
|
597
|
-
|
|
598
|
-
// Legacy 15-node and Markdown-first 8-node pipelines materialize
|
|
596
|
+
else if (task.shell?.backendTestPipeline === "classification-result-context") {
|
|
597
|
+
// The current 15-node single-run pipeline materializes the canonical
|
|
599
598
|
// contracts/backend-test-result.json without jsonArtifactGate. Bind it
|
|
600
599
|
// so Outcome adapters project kind=backend-test-result for Ready Planner.
|
|
601
600
|
const artifactPath = path.join(runDir, "contracts", "backend-test-result.json");
|
|
@@ -280,7 +280,6 @@ export const dagBackendTestPipelineSchema = z.enum([
|
|
|
280
280
|
"markdown-traceability",
|
|
281
281
|
"markdown-execute-html",
|
|
282
282
|
]);
|
|
283
|
-
export const dagFrontendTestEvidenceValidationSchema = z.object({}).strict();
|
|
284
283
|
export const dagShellConfigSchema = z.object({
|
|
285
284
|
commands: z.array(z.string()).default([]),
|
|
286
285
|
preset: dagShellPresetSchema.optional(),
|
|
@@ -294,7 +293,6 @@ export const dagShellConfigSchema = z.object({
|
|
|
294
293
|
frontendLintBaseline: dagFrontendLintBaselineSchema.optional(),
|
|
295
294
|
frontendVerificationBundle: dagFrontendVerificationBundleSchema.optional(),
|
|
296
295
|
frontendReviewContext: dagFrontendReviewContextSchema.optional(),
|
|
297
|
-
frontendTestEvidenceValidation: dagFrontendTestEvidenceValidationSchema.optional(),
|
|
298
296
|
backendTestPipeline: dagBackendTestPipelineSchema.optional(),
|
|
299
297
|
verifyEvidence: dagShellVerifyEvidenceSchema.optional(),
|
|
300
298
|
repairArtifactGate: dagRepairArtifactGateSchema.optional(),
|
|
@@ -461,11 +461,10 @@ function validateShellTaskConfig(task, spec, issues) {
|
|
|
461
461
|
!shell.backendTestPipeline &&
|
|
462
462
|
!shell.frontendPrewriteGate &&
|
|
463
463
|
!shell.frontendVerificationBundle &&
|
|
464
|
-
!shell.frontendReviewContext
|
|
465
|
-
!shell.frontendTestEvidenceValidation) {
|
|
464
|
+
!shell.frontendReviewContext) {
|
|
466
465
|
issues.push({
|
|
467
466
|
type: "missing-shell-commands",
|
|
468
|
-
message: `shell task ${task.id} requires
|
|
467
|
+
message: `shell task ${task.id} requires shell.preset, shell.verdictGate, shell.jsonArtifactGate, shell.backendTestPipeline, and/or non-empty shell.commands`,
|
|
469
468
|
});
|
|
470
469
|
}
|
|
471
470
|
if (commands.some((command) => command.trim().length === 0)) {
|
|
@@ -83,6 +83,17 @@ controller identity 与 DAG skill snapshot 是两个不同冻结层(前者跨
|
|
|
83
83
|
- Delivery / Closeout:clean Delivery HEAD 上生成 canonical QA/最终验证证据、Delivery Package、Acceptance Coverage、PR 草稿;Closeout 默认预览,显式 `--apply --owner` 才原子写回。
|
|
84
84
|
- 权威证据:`CHANGELOG.md [0.10.0]`、`docs/reports/feature/2026-07-12-m2-completion-audit.md`。
|
|
85
85
|
|
|
86
|
+
### Final Verification 权威(ADR 0007)
|
|
87
|
+
|
|
88
|
+
| 入口 | 职责 |
|
|
89
|
+
| --- | --- |
|
|
90
|
+
| `agent-worker feature verify-final` | **唯一** canonical writer:Feature Verification Bundle、`qa-pass.json`、`final-verification.json` |
|
|
91
|
+
| Feature Packet `FINAL-VERIFY-*`(若保留) | 可选 **local smoke recipe**(样本 `verify:final` / reports);Task Done ≠ Final Verification Record |
|
|
92
|
+
| `feature delivery` | 重验 verify-final 证据;省略路径时默认 `.harness/task-pool/evidence/<featureId>/{qa-pass,final-verification}.json` |
|
|
93
|
+
| `feature closeout` | 只认 Delivery manifest + Final Verification Record 链 |
|
|
94
|
+
|
|
95
|
+
禁止把 packet Task 的 smoke 报告或 agent-dag `verify-pi` 结论当作 Delivery 放行条件。
|
|
96
|
+
|
|
86
97
|
### Inspect(Observe 只读 read model)
|
|
87
98
|
|
|
88
99
|
- 模块:`src/worker/observe/`、`src/worker/observability/{read-model,event-store}.ts`。
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"skills/frontend-implementation/references/node-contracts.md",
|
|
75
75
|
"skills/frontend-implementation/references/design-spec.md",
|
|
76
76
|
"skills/frontend-implementation/references/code-standards.md",
|
|
77
|
+
"skills/frontend-bounded-implement/SKILL.md",
|
|
77
78
|
"skills/frontend-design-review/SKILL.md",
|
|
78
79
|
"skills/frontend-design-review/references/review-checklist.md",
|
|
79
80
|
"skills/frontend-review/SKILL.md",
|
|
@@ -151,6 +152,7 @@
|
|
|
151
152
|
".agents/skills/frontend-implementation/references/node-contracts.md",
|
|
152
153
|
".agents/skills/frontend-implementation/references/design-spec.md",
|
|
153
154
|
".agents/skills/frontend-implementation/references/code-standards.md",
|
|
155
|
+
".agents/skills/frontend-bounded-implement/SKILL.md",
|
|
154
156
|
".agents/skills/frontend-design-review/SKILL.md",
|
|
155
157
|
".agents/skills/frontend-design-review/references/review-checklist.md",
|
|
156
158
|
".agents/skills/frontend-review/SKILL.md",
|
|
@@ -229,6 +231,7 @@
|
|
|
229
231
|
".agents/skills/frontend-implementation/references/node-contracts.md": "copied",
|
|
230
232
|
".agents/skills/frontend-implementation/references/design-spec.md": "copied",
|
|
231
233
|
".agents/skills/frontend-implementation/references/code-standards.md": "copied",
|
|
234
|
+
".agents/skills/frontend-bounded-implement/SKILL.md": "copied",
|
|
232
235
|
".agents/skills/frontend-design-review/SKILL.md": "copied",
|
|
233
236
|
".agents/skills/frontend-design-review/references/review-checklist.md": "copied",
|
|
234
237
|
".agents/skills/frontend-review/SKILL.md": "copied",
|
|
@@ -521,6 +521,15 @@
|
|
|
521
521
|
"retryOnInvalid": { "type": "boolean", "default": true }
|
|
522
522
|
}
|
|
523
523
|
},
|
|
524
|
+
"writerOutcomePolicy": {
|
|
525
|
+
"type": "object",
|
|
526
|
+
"additionalProperties": false,
|
|
527
|
+
"required": ["type"],
|
|
528
|
+
"description": "Fail-closed outcome/diff consistency protocol for bounded Pi writers. The first non-empty assistant line must be IMPLEMENTATION_OUTCOME: changed, already-satisfied, or blocked.",
|
|
529
|
+
"properties": {
|
|
530
|
+
"type": { "const": "implementation-outcome-v1" }
|
|
531
|
+
}
|
|
532
|
+
},
|
|
524
533
|
"allowedPaths": {
|
|
525
534
|
"type": "array",
|
|
526
535
|
"items": { "type": "string", "minLength": 1 },
|