@tea-agent/loop-agent 0.38.2 → 0.38.4
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/AGENTS.md +4 -1
- package/CHANGELOG.md +171 -96
- package/README.md +11 -5
- package/bin/loop-agent.js +7 -3
- package/dist/application/task-lifecycle/advance.js +4 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +16 -5
- package/dist/cli/program.js +16 -2
- package/dist/cli/update/notifier.js +2 -2
- package/dist/cli.js +2 -1
- package/dist/commands/dag-rerun.js +55 -1
- package/dist/commands/init-model-catalog.js +464 -0
- package/dist/commands/init-upgrade.js +265 -97
- package/dist/commands/init.js +428 -27
- package/dist/commands/inspect-next.js +8 -0
- package/dist/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +76 -10
- package/dist/executors/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-extension-resolver.js +233 -0
- package/dist/executors/pi-playwright-cli-tool.js +51 -12
- package/dist/executors/pi-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +265 -15
- package/dist/executors/shell-write-guard.js +7 -0
- package/dist/shared/dag-prompt-override.js +28 -0
- package/dist/shared/operator/capabilities.js +32 -0
- package/dist/shared/playwright-cli-command-policy.js +15 -0
- package/dist/shared/update/console-notifier.js +100 -0
- package/dist/{cli → shared}/update/npm-client.js +41 -8
- package/dist/{cli → shared}/update/state.js +41 -13
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/diff.js +26 -0
- package/dist/task/contract/project.js +5 -0
- package/dist/task/contract/schema.js +6 -1
- package/dist/task/source-prepare/build-draft.js +15 -0
- package/dist/task/source-prepare/parse-intent.js +24 -4
- package/dist/worker/cli.js +31 -1
- package/dist/worker/console/chat/routes.js +1 -0
- package/dist/worker/console/dag-execution-receipt.js +33 -0
- package/dist/worker/console/operator-actions.js +116 -0
- package/dist/worker/console/operator-user-error.js +2 -2
- package/dist/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/routes.js +43 -2
- package/dist/worker/console/security.js +63 -9
- package/dist/worker/console/server.js +36 -5
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-gauhD9kD.js} +1 -1
- package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-pm3c2Rsl.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-D_JAg3wH.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-Dk7-ex52.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-Uval9oRI.js} +1 -1
- package/dist/worker/console/static/assets/channel-CVrnUdZl.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-BSYjHs-v.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-KGUuBXI8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-JqAtxD9N.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-D6EJOVQK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-B1JriSjm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-D62b96Jd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-BsNKEC_r.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-B56v6-ZP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-D2KPnd8K.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-kzKSCaaL.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-aX91WTya.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-aX91WTya.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-C6N6KNJd.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-DiyymT2D.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-BeKAXvf3.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-TkIdXfPF.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-BFmSG3ks.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-DqpJT2Fs.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-r5ATN43t.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-DEcSCMuR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-VOvYRLVc.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-D7f4Cb1u.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-ziUf3U0G.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-BoRdQ-1y.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-B1-GncNm.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-CTW-ph8W.js} +1 -1
- package/dist/worker/console/static/assets/index-C8rOIAJr.css +1 -0
- package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DEbyrEfW.js} +147 -68
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-BFA337tZ.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-BtEujgRR.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-C8ZwD2Jp.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-Ck-z8B11.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-CAyXjQjN.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-CcWKn6tS.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-C2NuDO7E.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BFub3PDz.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-BJx5THZ5.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-DhalkDWV.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-CVcBM36o.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-D_vy4n0w.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-DGYdJL1D.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-C1xJFg3k.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-D_Fc43df.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-BhGbIjbu.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DUt1lyFL.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DVkNcO-V.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-BM6SGr7j.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-BQU9ufV0.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BCQhRmYB.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DWerOyHO.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-DxLeebQU.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/mutation-gate.js +1 -1
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/loop-agent/loop-agent-client.js +7 -3
- package/dist/worker/observe/node-input.js +74 -3
- package/dist/worker/observe/node-process.js +377 -0
- package/dist/worker/observe/routes.js +36 -3
- package/dist/worker/observe/shell-handler-keys.js +34 -0
- package/dist/worker/observe/static/api.js +66 -0
- package/dist/worker/observe/static/constants.js +8 -1
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/format.js +23 -0
- package/dist/worker/observe/static/markdown-render.js +20 -1
- package/dist/worker/observe/static/operator-chrome.js +4 -0
- package/dist/worker/observe/static/state.js +13 -2
- package/dist/worker/observe/static/styles.css +73 -0
- package/dist/worker/observe/static/views/dag-inspector.js +612 -8
- package/dist/worker/observe/static/views/dag.js +8 -1
- package/dist/worker/observe/static/views/failures.js +5 -2
- package/dist/worker/observe/static/views/session-timeline.js +78 -9
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +76 -6
- package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
- package/dist/workflows/dag/contract-output-registry.js +15 -0
- package/dist/workflows/dag/failure-category.js +12 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +145 -40
- package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
- package/dist/workflows/dag/frontend-repair.js +14 -0
- package/dist/workflows/dag/frontend-risk.js +15 -2
- package/dist/workflows/dag/frontend-test-case-checklist.js +26 -2
- package/dist/workflows/dag/frontend-test-case-manifest.js +5 -0
- package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
- package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
- package/dist/workflows/dag/frontend-test-markdown.js +61 -0
- package/dist/workflows/dag/frontend-test-result-contract.js +160 -28
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/frontend-verification-trace.js +29 -4
- package/dist/workflows/dag/init-hybrid.js +196 -117
- package/dist/workflows/dag/node-execution.js +139 -10
- package/dist/workflows/dag/prompt.js +15 -1
- package/dist/workflows/dag/rerun-plan.js +134 -3
- package/dist/workflows/dag/rerun-run.js +58 -6
- package/dist/workflows/dag/retry-policy.js +91 -0
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +32 -1
- package/dist/workflows/dag/validate.js +41 -1
- package/docs/init-surface.manifest.json +5 -0
- package/docs/operations/README.md +1 -1
- package/docs/operations/local-development-environment.md +1 -5
- package/docs/skills/vetted-skill-registry.md +14 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/agent-dag.schema.json +44 -0
- package/docs/templates/backend-test-dag.json +2 -2
- package/docs/templates/frontend-test-dag.json +8 -10
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
- package/docs/templates/init-managed-agents.md +13 -3
- package/harness.json +2 -2
- package/package.json +12 -4
- package/scripts/next-info.mjs +356 -0
- package/scripts/next-publish-gate.mjs +238 -0
- package/scripts/release-source-binding.mjs +251 -0
- package/skills/analyze-product-requirements/SKILL.md +8 -5
- package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
- package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
- package/skills/analyze-product-requirements/references/example.md +50 -0
- package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
- package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
- package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
- package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
- package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
- package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
- package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
- package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
- package/skills/codebase-scout/SKILL.md +1 -1
- package/skills/fe-test-ui-scout/SKILL.md +65 -0
- package/skills/fe-test-ui-scout/references/ledger-schema.md +62 -0
- package/skills/fe-test-ui-scout/references/recon-protocol.md +54 -0
- package/skills/improve-codebase-architecture/SKILL.md +81 -0
- package/skills/improve-codebase-architecture/deepening.md +37 -0
- package/skills/improve-codebase-architecture/html-report.md +123 -0
- package/skills/improve-codebase-architecture/interface-design.md +44 -0
- package/skills/improve-codebase-architecture/language.md +53 -0
- package/skills/loop-agent/references/command-reference.md +11 -4
- package/skills/playwright-cli/SKILL.md +22 -0
- package/skills/playwright-cli-case-generator/SKILL.md +20 -1
- package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
- package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
|
@@ -5,7 +5,27 @@ import { z } from "zod";
|
|
|
5
5
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { readPlaywrightCliReceipts, summarizePlaywrightCliReceipts, } from "../../executors/pi-playwright-cli-tool.js";
|
|
7
7
|
import { validateFrontendCaseContent } from "./frontend-test-case-quality.js";
|
|
8
|
+
import { markdownH1Title, markdownList, markdownSection, } from "./frontend-test-markdown.js";
|
|
8
9
|
export const FRONTEND_TEST_RESULT_SCHEMA_ID = "frontend-test-result-v1";
|
|
10
|
+
/**
|
|
11
|
+
* Truncated acceptance-id shapes observed in generator output: a bare
|
|
12
|
+
* numeric segment ("AC-002" dropped its AC-FE-<FEATURE> prefix) or an
|
|
13
|
+
* AC-FE-<FEATURE> literal without the trailing -NNN sequence number
|
|
14
|
+
* ("AC-FE-CHAT"). Generic short ids like "AC-1"/"AC-FE-001" stay valid;
|
|
15
|
+
* declared-source cross-checks (unknown-ac) remain the primary guard when a
|
|
16
|
+
* sourceBinding exists.
|
|
17
|
+
*/
|
|
18
|
+
export function isTruncatedFrontendAcId(acId) {
|
|
19
|
+
return /^AC-FE-[A-Z]+$/i.test(acId.trim());
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Bare numeric ids ("AC-002") only indicate truncation when no declared AC
|
|
23
|
+
* list exists to adjudicate them: with a sourceBinding they are ordinary
|
|
24
|
+
* unknown-ac findings ("AC-999" may be a legitimately declared id style).
|
|
25
|
+
*/
|
|
26
|
+
export function isTruncatedWhenUndeclaredFrontendAcId(acId) {
|
|
27
|
+
return /^AC-\d{3}$/i.test(acId.trim()) || isTruncatedFrontendAcId(acId);
|
|
28
|
+
}
|
|
9
29
|
const safeRelativePathSchema = z.string().min(1).refine((value) => !path.posix.isAbsolute(value) &&
|
|
10
30
|
!path.win32.isAbsolute(value) &&
|
|
11
31
|
!value.includes("\\") &&
|
|
@@ -56,6 +76,8 @@ export const frontendTestResultContractSchema = z.object({
|
|
|
56
76
|
passed: z.number().int().min(0),
|
|
57
77
|
failed: z.number().int().min(0),
|
|
58
78
|
blocked: z.number().int().min(0),
|
|
79
|
+
/** Stability metric: passed on attempt 0 (no rerun/self-heal retry). */
|
|
80
|
+
firstTryPassed: z.number().int().min(0).optional(),
|
|
59
81
|
}).strict(),
|
|
60
82
|
acceptanceCoverage: z.object({
|
|
61
83
|
covered: z.array(z.string().min(1)),
|
|
@@ -109,7 +131,13 @@ export async function validateFrontendCaseEvidence(input) {
|
|
|
109
131
|
catch {
|
|
110
132
|
throw new Error("missing testcase/frontend/cases/manifest.json");
|
|
111
133
|
}
|
|
112
|
-
|
|
134
|
+
let manifest;
|
|
135
|
+
try {
|
|
136
|
+
manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new Error("invalid frontend case manifest: malformed JSON");
|
|
140
|
+
}
|
|
113
141
|
if (!Array.isArray(manifest.cases))
|
|
114
142
|
throw new Error("invalid frontend case manifest");
|
|
115
143
|
const statuses = new Set(["passed", "failed", "blocked"]);
|
|
@@ -194,11 +222,17 @@ export function evaluatePassedCaseBrowserReceipts(summary, caseId) {
|
|
|
194
222
|
}
|
|
195
223
|
return { ok: true };
|
|
196
224
|
}
|
|
197
|
-
async function
|
|
225
|
+
async function loadCaseBrowserReceiptsWithSummary(input) {
|
|
198
226
|
try {
|
|
199
227
|
const entries = await readdir(input.runDir, { withFileTypes: true });
|
|
200
228
|
// A passed chain must belong to exactly one browser child stream. Never
|
|
201
|
-
// stitch receipts across DAG nodes or accept ambiguous duplicate
|
|
229
|
+
// stitch receipts across DAG nodes or accept ambiguous duplicate
|
|
230
|
+
// authority — with one exception: bounded rerun creates a second,
|
|
231
|
+
// later stream for the same case (the rerun overwrites the disk
|
|
232
|
+
// case-result, so the final attempt is the authoritative one). When
|
|
233
|
+
// multiple streams authorize, prefer the highest rerun round
|
|
234
|
+
// (rerun-frontend-case-rN-<idx> directories); ties within the same
|
|
235
|
+
// round remain ambiguous and fail closed.
|
|
202
236
|
const authorizingStreams = [];
|
|
203
237
|
for (const entry of entries) {
|
|
204
238
|
if (!entry.isDirectory())
|
|
@@ -206,48 +240,121 @@ async function loadCaseBrowserReceiptSummary(input) {
|
|
|
206
240
|
const nodeReceipts = (await readPlaywrightCliReceipts(input.runDir, entry.name))
|
|
207
241
|
.filter((item) => item.caseId === input.caseId);
|
|
208
242
|
const summary = summarizePlaywrightCliReceipts(nodeReceipts);
|
|
209
|
-
if (summary.hasOrderedPassedReceiptChain)
|
|
210
|
-
|
|
243
|
+
if (summary.hasOrderedPassedReceiptChain) {
|
|
244
|
+
const roundMatch = /rerun-frontend-case-r(\d+)-/i.exec(entry.name);
|
|
245
|
+
authorizingStreams.push({
|
|
246
|
+
receipts: nodeReceipts,
|
|
247
|
+
summary,
|
|
248
|
+
round: roundMatch ? Number(roundMatch[1]) : 0,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (authorizingStreams.length === 0) {
|
|
253
|
+
return { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
|
|
211
254
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
255
|
+
const maxRound = Math.max(...authorizingStreams.map((s) => s.round));
|
|
256
|
+
const finalists = authorizingStreams.filter((s) => s.round === maxRound);
|
|
257
|
+
return finalists.length === 1
|
|
258
|
+
? finalists[0]
|
|
259
|
+
: { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
|
|
215
260
|
}
|
|
216
261
|
catch {
|
|
217
|
-
return summarizePlaywrightCliReceipts([]);
|
|
262
|
+
return { receipts: [], summary: summarizePlaywrightCliReceipts([]) };
|
|
218
263
|
}
|
|
219
264
|
}
|
|
220
|
-
|
|
221
|
-
|
|
265
|
+
/**
|
|
266
|
+
* Extract `find "literal" >=N` / `find "literal"` declarations from a case
|
|
267
|
+
* document. Only quoted literals are binding assertions — the count suffix
|
|
268
|
+
* (`>=N`, `==N`) is the expected minimum/exact match count from the ledger.
|
|
269
|
+
* Placeholder literals ("...", "…", "<字面>", "eX") appear in prose discussing
|
|
270
|
+
* the syntax itself; they are documentation, never declarations.
|
|
271
|
+
*/
|
|
272
|
+
export function extractDeclaredFindAssertions(caseMarkdown) {
|
|
273
|
+
const source = stripFencedCodeBlocks(caseMarkdown);
|
|
274
|
+
const out = [];
|
|
275
|
+
const seen = new Set();
|
|
276
|
+
const pattern = /find\s+"((?:[^"\\]|\\.)+)"\s*(>=|==)\s*(\d+)/g;
|
|
277
|
+
const isPlaceholder = (literal) => /^(?:\.{2,}|…+|<[^>]*>|e\d+)$/.test(literal.trim());
|
|
278
|
+
let match;
|
|
279
|
+
while ((match = pattern.exec(source)) !== null) {
|
|
280
|
+
const literal = match[1].replace(/\\(.)/g, "$1");
|
|
281
|
+
if (isPlaceholder(literal))
|
|
282
|
+
continue;
|
|
283
|
+
const operator = match[2];
|
|
284
|
+
const expected = Number(match[3]);
|
|
285
|
+
const key = `${operator}:${literal}`;
|
|
286
|
+
if (seen.has(key))
|
|
287
|
+
continue;
|
|
288
|
+
seen.add(key);
|
|
289
|
+
out.push({ literal, expected, operator });
|
|
290
|
+
}
|
|
291
|
+
return out;
|
|
222
292
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
293
|
+
function stripFencedCodeBlocks(markdown) {
|
|
294
|
+
return markdown.replace(/```[\s\S]*?```/g, "");
|
|
295
|
+
}
|
|
296
|
+
async function readCaseMarkdownForAudit(workspaceRoot, casePath) {
|
|
297
|
+
try {
|
|
298
|
+
return await readFile(path.join(workspaceRoot, casePath), "utf8");
|
|
299
|
+
}
|
|
300
|
+
catch {
|
|
228
301
|
return "";
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Controller-side assertion audit: every find-family declaration in the case
|
|
306
|
+
* document must be backed by a successful find receipt whose parsed match
|
|
307
|
+
* count satisfies the declared operator. This moves pass authority from
|
|
308
|
+
* "the model says it asserted" to "receipts prove it asserted".
|
|
309
|
+
*/
|
|
310
|
+
export function auditDeclaredFindAssertions(declared, receipts) {
|
|
311
|
+
if (declared.length === 0)
|
|
312
|
+
return { ok: true };
|
|
313
|
+
const successfulFinds = receipts.filter((receipt) => (receipt.command === "find" || receipt.command === "find-unique") &&
|
|
314
|
+
receipt.exitCode === 0 &&
|
|
315
|
+
!receipt.timedOut);
|
|
316
|
+
const missing = [];
|
|
317
|
+
for (const decl of declared) {
|
|
318
|
+
const backing = successfulFinds.some((receipt) => receipt.argsRedacted.some((arg) => arg === decl.literal || arg === `"${decl.literal}"`) &&
|
|
319
|
+
typeof receipt.matchCount === "number" &&
|
|
320
|
+
(decl.operator === ">="
|
|
321
|
+
? receipt.matchCount >= decl.expected
|
|
322
|
+
: receipt.matchCount === decl.expected));
|
|
323
|
+
if (!backing) {
|
|
324
|
+
missing.push(`${decl.operator}${decl.expected} "${decl.literal.slice(0, 40)}"`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (missing.length > 0) {
|
|
328
|
+
return {
|
|
329
|
+
ok: false,
|
|
330
|
+
reason: "assertion-not-receipt-backed",
|
|
331
|
+
missing,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
return { ok: true };
|
|
232
335
|
}
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
return items;
|
|
336
|
+
function sha256(content) {
|
|
337
|
+
return createHash("sha256").update(content).digest("hex");
|
|
236
338
|
}
|
|
237
339
|
/**
|
|
238
340
|
* Read planned case facts from the case Markdown. Recognizes both English and
|
|
239
|
-
* Chinese headings
|
|
240
|
-
* test steps), alongside the legacy `操作步骤`/`Steps`
|
|
341
|
+
* Chinese headings at `##` or `###`, including `测试点` (test points) and
|
|
342
|
+
* `测试步骤` (planned test steps), alongside the legacy `操作步骤`/`Steps`
|
|
343
|
+
* headings. Numbered and bulleted lists are extracted; a missing 测试目的
|
|
344
|
+
* falls back to the case H1 title.
|
|
241
345
|
*/
|
|
242
346
|
async function readFrontendCaseContent(workspaceRoot, casePath) {
|
|
243
347
|
if (!safeRelativePathSchema.safeParse(casePath).success || !casePath.startsWith("testcase/frontend/cases/")) {
|
|
244
348
|
throw new Error(`unsafe frontend case path: ${casePath}`);
|
|
245
349
|
}
|
|
246
350
|
const body = await readFile(path.resolve(workspaceRoot, casePath), "utf8");
|
|
351
|
+
const purpose = markdownSection(body, ["Test Purpose", "测试目的", "测试场景"]) ||
|
|
352
|
+
markdownH1Title(body).replace(/^FE-[^\s::]+[::]\s*/, "").trim() ||
|
|
353
|
+
"未提供测试目的";
|
|
247
354
|
return {
|
|
248
355
|
caseContent: {
|
|
249
|
-
purpose
|
|
250
|
-
preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件"])),
|
|
356
|
+
purpose,
|
|
357
|
+
preconditions: markdownList(markdownSection(body, ["Preconditions", "前置条件", "前置条件与重置"])),
|
|
251
358
|
steps: markdownList(markdownSection(body, ["Test Steps", "测试步骤", "Steps", "操作步骤"])),
|
|
252
359
|
expectedResults: markdownList(markdownSection(body, ["Expected Results", "预期结果"])),
|
|
253
360
|
},
|
|
@@ -332,7 +439,13 @@ export async function materializeFrontendTestResult(input) {
|
|
|
332
439
|
}
|
|
333
440
|
const manifestPath = "testcase/frontend/cases/manifest.json";
|
|
334
441
|
const manifestAbsolute = path.resolve(input.workspaceRoot, manifestPath);
|
|
335
|
-
|
|
442
|
+
let manifest;
|
|
443
|
+
try {
|
|
444
|
+
manifest = JSON.parse(await readFile(manifestAbsolute, "utf-8"));
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
throw new Error("invalid or empty frontend case manifest: malformed JSON");
|
|
448
|
+
}
|
|
336
449
|
if (manifest.schemaVersion !== 1 || !Array.isArray(manifest.cases) || manifest.cases.length === 0) {
|
|
337
450
|
throw new Error("invalid or empty frontend case manifest");
|
|
338
451
|
}
|
|
@@ -395,7 +508,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
395
508
|
advisoryFindings.push({ ruleId: "passed-without-browser-evidence", caseId: item.caseId, detail: "passed case has no browser evidence" });
|
|
396
509
|
let blockedReason = status === "blocked" && typeof resultRaw.blockedReason === "string" && resultRaw.blockedReason.trim() ? resultRaw.blockedReason.trim() : undefined;
|
|
397
510
|
if (status === "passed") {
|
|
398
|
-
const receiptSummary = await
|
|
511
|
+
const { receipts: caseReceipts, summary: receiptSummary } = await loadCaseBrowserReceiptsWithSummary({
|
|
399
512
|
runDir: input.runDir,
|
|
400
513
|
caseId: item.caseId,
|
|
401
514
|
});
|
|
@@ -409,6 +522,24 @@ export async function materializeFrontendTestResult(input) {
|
|
|
409
522
|
detail: "passed case lacks controller-owned open + interaction/assertion + cleanup receipts",
|
|
410
523
|
});
|
|
411
524
|
}
|
|
525
|
+
else {
|
|
526
|
+
// Controller-side assertion audit: every `find "literal" >=N` declared
|
|
527
|
+
// in the case document must be backed by a successful find receipt
|
|
528
|
+
// whose parsed matchCount satisfies the operator. Pass authority moves
|
|
529
|
+
// from model prose to receipt evidence.
|
|
530
|
+
const caseMarkdown = await readCaseMarkdownForAudit(input.workspaceRoot, item.casePath);
|
|
531
|
+
const declared = extractDeclaredFindAssertions(caseMarkdown);
|
|
532
|
+
const audit = auditDeclaredFindAssertions(declared, caseReceipts);
|
|
533
|
+
if (!audit.ok) {
|
|
534
|
+
status = "blocked";
|
|
535
|
+
blockedReason = audit.reason;
|
|
536
|
+
advisoryFindings.push({
|
|
537
|
+
ruleId: "assertion-not-receipt-backed",
|
|
538
|
+
caseId: item.caseId,
|
|
539
|
+
detail: `declared find assertions without receipt backing: ${audit.missing.join("; ")}`,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
412
543
|
}
|
|
413
544
|
const { caseContent, testPoints } = await readFrontendCaseContent(input.workspaceRoot, item.casePath);
|
|
414
545
|
// Parse bounded fixed sections from execution.md. Missing optional
|
|
@@ -459,6 +590,7 @@ export async function materializeFrontendTestResult(input) {
|
|
|
459
590
|
passed: cases.filter((item) => item.status === "passed").length,
|
|
460
591
|
failed: cases.filter((item) => item.status === "failed").length,
|
|
461
592
|
blocked: cases.filter((item) => item.status === "blocked").length,
|
|
593
|
+
firstTryPassed: cases.filter((item) => item.status === "passed" && !item.rerunAttempt).length,
|
|
462
594
|
};
|
|
463
595
|
const outcome = totals.failed > 0 ? "failed" : totals.blocked > 0 || missing.length > 0 ? "incomplete" : "passed";
|
|
464
596
|
const result = frontendTestResultContractSchema.parse({
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME = "frontend-test-standard-scenarios.v1.json";
|
|
4
|
+
export const FRONTEND_TEST_STANDARD_SCENARIOS_DEST = "testcase/frontend/rag/standard-scenarios.v1.json";
|
|
5
|
+
const MINIMAL_STANDARD_SCENARIOS = {
|
|
6
|
+
schemaVersion: 1,
|
|
7
|
+
id: "frontend-test-standard-scenarios-v1",
|
|
8
|
+
scenarios: [
|
|
9
|
+
{
|
|
10
|
+
id: "STD-FE-SMOKE-ENTRY",
|
|
11
|
+
title: "入口可打开",
|
|
12
|
+
category: "smoke",
|
|
13
|
+
priority: "must",
|
|
14
|
+
testPoints: ["open"],
|
|
15
|
+
minCases: 1,
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
function isSafeRelativeDir(value) {
|
|
20
|
+
if (!value || path.isAbsolute(value) || path.win32.isAbsolute(value)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return value.split(/[\\/]/).every((part) => part.length > 0 && part !== "." && part !== "..");
|
|
24
|
+
}
|
|
25
|
+
export function listFrontendTestStandardScenarioCandidates(input) {
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const add = (relative) => {
|
|
28
|
+
const normalized = relative.replaceAll("\\", "/");
|
|
29
|
+
if (!seen.has(normalized))
|
|
30
|
+
seen.add(normalized);
|
|
31
|
+
};
|
|
32
|
+
add(`docs/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
|
|
33
|
+
const governanceRoot = input.governanceRoot?.trim().replaceAll("\\", "/").replace(/\/+$/, "");
|
|
34
|
+
if (governanceRoot && isSafeRelativeDir(governanceRoot)) {
|
|
35
|
+
add(`${governanceRoot}/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
|
|
36
|
+
}
|
|
37
|
+
add(`ai_workspace/loop-agent/templates/${FRONTEND_TEST_STANDARD_SCENARIOS_FILENAME}`);
|
|
38
|
+
return [...seen];
|
|
39
|
+
}
|
|
40
|
+
async function readGovernanceRoot(workspaceRoot) {
|
|
41
|
+
try {
|
|
42
|
+
const raw = JSON.parse(await readFile(path.join(workspaceRoot, "harness.json"), "utf8"));
|
|
43
|
+
return typeof raw.governanceRoot === "string" ? raw.governanceRoot : undefined;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function copyFrontendTestStandardScenarios(input) {
|
|
50
|
+
const destRel = FRONTEND_TEST_STANDARD_SCENARIOS_DEST;
|
|
51
|
+
const destAbs = path.join(input.workspaceRoot, destRel);
|
|
52
|
+
const candidates = listFrontendTestStandardScenarioCandidates({
|
|
53
|
+
governanceRoot: await readGovernanceRoot(input.workspaceRoot),
|
|
54
|
+
});
|
|
55
|
+
await mkdir(path.dirname(destAbs), { recursive: true });
|
|
56
|
+
for (const relative of candidates) {
|
|
57
|
+
const sourceAbs = path.join(input.workspaceRoot, relative);
|
|
58
|
+
try {
|
|
59
|
+
await copyFile(sourceAbs, destAbs);
|
|
60
|
+
return { status: "copied", from: relative, to: destRel };
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// try the next candidate
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await writeFile(destAbs, `${JSON.stringify(MINIMAL_STANDARD_SCENARIOS, null, 2)}\n`, "utf8");
|
|
67
|
+
return { status: "fallback", to: destRel };
|
|
68
|
+
}
|
|
@@ -2,6 +2,18 @@ import { readFile, stat, writeFile, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, frontendImplementationContractSchema, } from "./frontend-implementation-contract.js";
|
|
4
4
|
export const FRONTEND_VERIFICATION_TRACE_SCHEMA_ID = "frontend-verification-trace-v1";
|
|
5
|
+
/**
|
|
6
|
+
* Shared marker for a project that exposes no usable verification command at
|
|
7
|
+
* generation time. The marker command runs (and logs not-run honestly), but it
|
|
8
|
+
* must never count as successful verification evidence: labels carrying this
|
|
9
|
+
* text are excluded from successful evidence and targets bound to it fail the
|
|
10
|
+
* trace gate instead of passing it.
|
|
11
|
+
*/
|
|
12
|
+
export const FRONTEND_NO_VERIFICATION_MARKER_TEXT = "No frontend verification command configured";
|
|
13
|
+
export function isFrontendNoVerificationMarkerLabel(label) {
|
|
14
|
+
return label.includes(FRONTEND_NO_VERIFICATION_MARKER_TEXT);
|
|
15
|
+
}
|
|
16
|
+
export const FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE = `frontend verification unavailable: the only frozen static command is the no-op not-run marker (${FRONTEND_NO_VERIFICATION_MARKER_TEXT}); declare verification commands via task.json --verify or 执行约束.md and regenerate the DAG`;
|
|
5
17
|
async function readJsonFile(filePath) {
|
|
6
18
|
return JSON.parse(await readFile(filePath, "utf8"));
|
|
7
19
|
}
|
|
@@ -52,7 +64,14 @@ export async function evaluateVerificationTargets(input) {
|
|
|
52
64
|
const hardIssues = [];
|
|
53
65
|
for (const target of input.contract.verificationTargets) {
|
|
54
66
|
const issues = [];
|
|
55
|
-
|
|
67
|
+
if (isFrontendNoVerificationMarkerLabel(target.commandLabel)) {
|
|
68
|
+
// A target bound to the not-run marker has no verification behind it;
|
|
69
|
+
// passing it would let the run finish green without any check.
|
|
70
|
+
issues.push(`${target.id}: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`);
|
|
71
|
+
}
|
|
72
|
+
const owners = isFrontendNoVerificationMarkerLabel(target.commandLabel)
|
|
73
|
+
? []
|
|
74
|
+
: (input.labelOwners.get(target.commandLabel) ?? []);
|
|
56
75
|
if (!owners?.length) {
|
|
57
76
|
issues.push(`commandLabel not executed in current-run frozen evidence: ${target.commandLabel}`);
|
|
58
77
|
}
|
|
@@ -189,11 +208,17 @@ export async function runFrontendVerificationTraceGate(input) {
|
|
|
189
208
|
throw new Error("trace: selected Mock strategy requires successful Mock verification commands");
|
|
190
209
|
}
|
|
191
210
|
if (input.evidence) {
|
|
192
|
-
|
|
193
|
-
|
|
211
|
+
// The no-op not-run marker never counts as successful verification
|
|
212
|
+
// evidence, even if it executed with exit code 0.
|
|
213
|
+
const effectiveStaticLabels = input.evidence.static.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
|
|
214
|
+
const effectiveBehaviorLabels = input.evidence.behavior.commandLabels.filter((label) => !isFrontendNoVerificationMarkerLabel(label));
|
|
215
|
+
if (effectiveStaticLabels.length === 0) {
|
|
216
|
+
throw new Error(input.evidence.static.commandLabels.length > 0
|
|
217
|
+
? `trace: ${FRONTEND_VERIFICATION_UNAVAILABLE_MESSAGE}`
|
|
218
|
+
: "trace: no successful static verification commands in current run");
|
|
194
219
|
}
|
|
195
220
|
if (requiresBehaviorVerification &&
|
|
196
|
-
|
|
221
|
+
effectiveBehaviorLabels.length === 0) {
|
|
197
222
|
throw new Error("trace: no successful behavior verification commands in current run");
|
|
198
223
|
}
|
|
199
224
|
}
|