@tea-agent/loop-agent 0.38.2 → 0.38.3
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 +18 -0
- package/README.md +2 -0
- 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/commands/task-advance.js +31 -0
- package/dist/executors/dag-pi-executor.js +58 -1
- 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-sdk-executor.js +187 -59
- package/dist/executors/shell-executor.js +56 -2
- package/dist/executors/shell-write-guard.js +7 -0
- 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/worker/console/pi-readiness.js +4 -4
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-C4kVYweA.js} +1 -1
- package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-Bj2M8iO5.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-CLlXe4-6.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-psEp5xH0.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-DeKcOCGJ.js} +1 -1
- package/dist/worker/console/static/assets/channel-CPF4N7pf.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-K0C744p1.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-xbNw4J9-.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-DjLaSDUO.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-CtiPlKel.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-DZtolir7.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-CPcdNeew.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-NgHc6V37.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-DQ_T-hg8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-DgLsxcVP.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-UrVoM9z1.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-CLkYvTb3.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-BwT_xKrE.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-Bi9tuzif.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-BCIqkWBV.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-Dip6l9_Z.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-B-xkh_wK.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-DZO0kTt_.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-D6ZplNbZ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-BPekbhoS.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-BtmaJpK5.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-6DS0Xc44.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-CihnROSm.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-CZC4zOE2.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js} +1 -1
- package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DTZOKgAn.js} +142 -63
- package/dist/worker/console/static/assets/index-Ya5FE7cD.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-DlSl4BGx.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-snnzFl_U.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-BDE7qpMx.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-CD2Ci04G.js} +1 -1
- package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-DdnapKIH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-BVjAT9b8.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-D0yJk-zA.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BVo9tFP-.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-DnOV-mZ_.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-CzUJo58i.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-Bvikrolh.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-DtaSVCap.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-C2c9A0wW.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-DmXJcU7r.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-B4GUFW92.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-Dsad2MXf.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DGq48Fbi.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-CSSi6OFf.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BqyzPrWv.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DlznVb6S.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-CfBcgJ_K.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
- package/dist/worker/observe/node-input.js +72 -3
- package/dist/worker/observe/static/dag-history-labels.js +4 -0
- package/dist/worker/observe/static/state.js +2 -2
- package/dist/worker/observe/static/views/dag-inspector.js +51 -0
- package/dist/worker/observe/static/views/session-timeline.js +18 -5
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +56 -4
- 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 +4 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +140 -39
- package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
- 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 +10 -18
- package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
- package/dist/workflows/dag/init-hybrid.js +79 -101
- package/dist/workflows/dag/node-execution.js +119 -8
- package/dist/workflows/dag/prompt.js +15 -1
- package/dist/workflows/dag/rerun-plan.js +22 -3
- package/dist/workflows/dag/structured-output-repair.js +712 -0
- package/dist/workflows/dag/types.js +23 -0
- package/dist/workflows/dag/validate.js +41 -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/package.json +8 -4
- 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/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/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
|
@@ -8,6 +8,7 @@ import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-sto
|
|
|
8
8
|
import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
9
9
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
10
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
11
|
+
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, openspecCitationsRel, parseOpenspecCitationsBlock, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
11
12
|
export const frontendNormalizationActionSchema = z.enum([
|
|
12
13
|
"remove-trailing-commas",
|
|
13
14
|
"strip-comments",
|
|
@@ -1485,7 +1486,13 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
1485
1486
|
rawOutput = input.rawContractText;
|
|
1486
1487
|
}
|
|
1487
1488
|
else if (input.fromNodeId !== undefined) {
|
|
1488
|
-
|
|
1489
|
+
let record;
|
|
1490
|
+
try {
|
|
1491
|
+
record = JSON.parse(await readFile(path.join(input.runDir, `${input.fromNodeId}.json`), "utf8"));
|
|
1492
|
+
}
|
|
1493
|
+
catch (error) {
|
|
1494
|
+
throw new Error(`cannot parse node record ${input.fromNodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1495
|
+
}
|
|
1489
1496
|
rawOutput = record.assistantText ?? record.stdout ?? "";
|
|
1490
1497
|
}
|
|
1491
1498
|
else {
|
|
@@ -1731,7 +1738,7 @@ async function writeFrontendPlanCandidateRaw(input) {
|
|
|
1731
1738
|
* Compile the planner's editable RFC 7386 patch against the deterministic
|
|
1732
1739
|
* runtime skeleton. Every attempt is preserved under contracts/candidates so
|
|
1733
1740
|
* format/schema failures can be inspected and replayed without trusting model
|
|
1734
|
-
* prose. A successful result returns
|
|
1741
|
+
* prose. A successful result returns a path-only pointer plus the hash-bound canonical artifact for
|
|
1735
1742
|
* downstream review and prewrite nodes.
|
|
1736
1743
|
*/
|
|
1737
1744
|
export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
@@ -1804,27 +1811,60 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1804
1811
|
],
|
|
1805
1812
|
errors: [],
|
|
1806
1813
|
});
|
|
1814
|
+
const canonicalRel = canonicalContractRel(nodeId);
|
|
1815
|
+
const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalRel, analysis.canonical);
|
|
1816
|
+
const parsedCitations = parseOpenspecCitationsBlock(citationsBlock);
|
|
1817
|
+
await writeDeterministicJsonArtifact(input.runDir, openspecCitationsRel(nodeId), {
|
|
1818
|
+
citations: parsedCitations.citations,
|
|
1819
|
+
block: citationsBlock,
|
|
1820
|
+
});
|
|
1821
|
+
await persistRuntimeSkeleton({
|
|
1822
|
+
runDir: input.runDir,
|
|
1823
|
+
nodeId,
|
|
1824
|
+
skeleton,
|
|
1825
|
+
});
|
|
1826
|
+
const artifact = {
|
|
1827
|
+
path: canonicalArtifact.path,
|
|
1828
|
+
sha256: canonicalArtifact.sha256,
|
|
1829
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1830
|
+
};
|
|
1807
1831
|
return {
|
|
1808
1832
|
ok: true,
|
|
1809
1833
|
contract: analysis.canonical,
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
"",
|
|
1813
|
-
"```json",
|
|
1814
|
-
serializeDeterministicJson(analysis.canonical),
|
|
1815
|
-
"```",
|
|
1816
|
-
citationsBlock,
|
|
1817
|
-
].join("\n"),
|
|
1834
|
+
artifact,
|
|
1835
|
+
normalizedText: formatStructuredArtifactPointer(artifact),
|
|
1818
1836
|
};
|
|
1819
1837
|
}
|
|
1820
1838
|
catch (error) {
|
|
1821
1839
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1840
|
+
const classified = classifyStructuredContractFailure({
|
|
1841
|
+
reason,
|
|
1842
|
+
kind: error instanceof FrontendContractFailure ? error.kind : undefined,
|
|
1843
|
+
});
|
|
1822
1844
|
await writeReport({
|
|
1823
|
-
classification:
|
|
1845
|
+
classification: classified.classification,
|
|
1824
1846
|
normalizationActions: [],
|
|
1825
|
-
errors:
|
|
1847
|
+
errors: classified.errors,
|
|
1826
1848
|
});
|
|
1827
|
-
return {
|
|
1849
|
+
return {
|
|
1850
|
+
ok: false,
|
|
1851
|
+
reason,
|
|
1852
|
+
classification: classified.classification,
|
|
1853
|
+
errors: classified.errors,
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
export async function readFrontendCanonicalCandidate(runDir, nodeId) {
|
|
1858
|
+
const candidatePath = path.join(runDir, canonicalContractRel(nodeId));
|
|
1859
|
+
try {
|
|
1860
|
+
const raw = await readFile(candidatePath, "utf8");
|
|
1861
|
+
const parsed = frontendImplementationContractSchema.safeParse(JSON.parse(raw));
|
|
1862
|
+
return parsed.success ? parsed.data : undefined;
|
|
1863
|
+
}
|
|
1864
|
+
catch (error) {
|
|
1865
|
+
if (error.code === "ENOENT")
|
|
1866
|
+
return undefined;
|
|
1867
|
+
return undefined;
|
|
1828
1868
|
}
|
|
1829
1869
|
}
|
|
1830
1870
|
/**
|
|
@@ -1845,60 +1885,121 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1845
1885
|
return { ok: true, contract: analysis.canonical };
|
|
1846
1886
|
}
|
|
1847
1887
|
catch (error) {
|
|
1888
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1889
|
+
const classified = classifyStructuredContractFailure({
|
|
1890
|
+
reason,
|
|
1891
|
+
kind: error instanceof FrontendContractFailure ? error.kind : undefined,
|
|
1892
|
+
});
|
|
1848
1893
|
return {
|
|
1849
1894
|
ok: false,
|
|
1850
|
-
reason
|
|
1895
|
+
reason,
|
|
1896
|
+
classification: classified.classification,
|
|
1897
|
+
errors: classified.errors,
|
|
1851
1898
|
};
|
|
1852
1899
|
}
|
|
1853
1900
|
}
|
|
1854
1901
|
/**
|
|
1855
|
-
* Node-output self-check for the plan revision node
|
|
1856
|
-
*
|
|
1857
|
-
*
|
|
1858
|
-
*
|
|
1859
|
-
*
|
|
1860
|
-
* schemaVersion/targets, so no full-contract schema/semantic checks run here;
|
|
1861
|
-
* content parsing, patch application, and merged-contract validation stay with
|
|
1862
|
-
* the prewrite gate, which remains the only authority.
|
|
1902
|
+
* Node-output self-check for the plan revision node. Format-level checks still
|
|
1903
|
+
* require one openspec-citations fence and one JSON object. When the original
|
|
1904
|
+
* canonical contract and sourceBinding are present, this node is the merge
|
|
1905
|
+
* authority: it applies the RFC 7386 patch, validates the full contract, and
|
|
1906
|
+
* writes the revision node's own hash-bound canonical artifact.
|
|
1863
1907
|
*/
|
|
1864
1908
|
export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
1909
|
+
const fail = (reason) => {
|
|
1910
|
+
const classified = classifyStructuredContractFailure({ reason });
|
|
1911
|
+
return {
|
|
1912
|
+
ok: false,
|
|
1913
|
+
reason,
|
|
1914
|
+
classification: classified.classification,
|
|
1915
|
+
errors: classified.errors,
|
|
1916
|
+
};
|
|
1917
|
+
};
|
|
1918
|
+
const nodeId = input.nodeId ?? "frontend-plan-revision-pi";
|
|
1919
|
+
const attempt = input.attempt ?? 1;
|
|
1920
|
+
await writeFrontendPlanCandidateRaw({
|
|
1921
|
+
runDir: input.runDir,
|
|
1922
|
+
nodeId,
|
|
1923
|
+
attempt,
|
|
1924
|
+
text: input.text,
|
|
1925
|
+
});
|
|
1865
1926
|
let citationsBlock;
|
|
1866
1927
|
try {
|
|
1867
1928
|
citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1868
1929
|
}
|
|
1869
1930
|
catch (error) {
|
|
1870
|
-
return
|
|
1871
|
-
ok: false,
|
|
1872
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
1873
|
-
};
|
|
1931
|
+
return fail(error instanceof Error ? error.message : String(error));
|
|
1874
1932
|
}
|
|
1875
|
-
// Strip the citation blocks before extraction: their per-row JSON objects
|
|
1876
|
-
// would otherwise count as competing balanced objects in the extractor.
|
|
1877
1933
|
const textWithoutCitations = input.text.replace(citationsBlock, "");
|
|
1878
1934
|
let patch;
|
|
1879
1935
|
try {
|
|
1880
1936
|
patch = extractFrontendImplementationJson(textWithoutCitations);
|
|
1881
1937
|
}
|
|
1882
1938
|
catch (error) {
|
|
1883
|
-
return {
|
|
1884
|
-
ok: false,
|
|
1885
|
-
reason: `revision patch output must contain exactly one valid JSON object: ${error instanceof Error ? error.message : String(error)}`,
|
|
1886
|
-
};
|
|
1939
|
+
return fail(`revision patch output must contain exactly one valid JSON object: ${error instanceof Error ? error.message : String(error)}`);
|
|
1887
1940
|
}
|
|
1888
1941
|
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
1889
|
-
return
|
|
1890
|
-
ok: false,
|
|
1891
|
-
reason: "revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)",
|
|
1892
|
-
};
|
|
1942
|
+
return fail("revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)");
|
|
1893
1943
|
}
|
|
1894
1944
|
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1895
1945
|
if (protectedViolations.length > 0) {
|
|
1946
|
+
return fail(`frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
1947
|
+
}
|
|
1948
|
+
const original = (await readFrontendCanonicalCandidate(input.runDir, "frontend-plan-pi")) ??
|
|
1949
|
+
(await readFrontendCanonicalCandidate(input.runDir, nodeId));
|
|
1950
|
+
if (!input.sourceBinding) {
|
|
1951
|
+
return { ok: true, contract: patch };
|
|
1952
|
+
}
|
|
1953
|
+
if (!original) {
|
|
1954
|
+
return fail("frontend revision patch requires the original frontend-plan-pi canonical contract");
|
|
1955
|
+
}
|
|
1956
|
+
try {
|
|
1957
|
+
const merged = applyFrontendContractMergePatch(original, patch);
|
|
1958
|
+
const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
|
|
1959
|
+
runDir: input.runDir,
|
|
1960
|
+
contract: merged,
|
|
1961
|
+
});
|
|
1962
|
+
const analysis = await analyzeFrontendImplementationContract({
|
|
1963
|
+
runDir: input.runDir,
|
|
1964
|
+
rawContractText: serializeDeterministicJson(merged),
|
|
1965
|
+
sourceBinding: input.sourceBinding,
|
|
1966
|
+
});
|
|
1967
|
+
const candidateDir = path.posix.join("contracts", "candidates", nodeId);
|
|
1968
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`), patch);
|
|
1969
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
1970
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.validation.json`), {
|
|
1971
|
+
classification: "accepted-normalized",
|
|
1972
|
+
normalizationActions: [
|
|
1973
|
+
"apply-original-canonical",
|
|
1974
|
+
...(droppedUnresolvedSymbols
|
|
1975
|
+
? ["drop-unresolved-verification-symbols"]
|
|
1976
|
+
: []),
|
|
1977
|
+
...analysis.normalizationActions,
|
|
1978
|
+
],
|
|
1979
|
+
errors: [],
|
|
1980
|
+
});
|
|
1981
|
+
const canonicalArtifact = await writeDeterministicJsonArtifact(input.runDir, canonicalContractRel(nodeId), analysis.canonical);
|
|
1982
|
+
const parsedCitations = parseOpenspecCitationsBlock(citationsBlock);
|
|
1983
|
+
await writeDeterministicJsonArtifact(input.runDir, openspecCitationsRel(nodeId), {
|
|
1984
|
+
citations: parsedCitations.citations,
|
|
1985
|
+
block: citationsBlock,
|
|
1986
|
+
});
|
|
1987
|
+
const artifact = {
|
|
1988
|
+
path: canonicalArtifact.path,
|
|
1989
|
+
sha256: canonicalArtifact.sha256,
|
|
1990
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1991
|
+
};
|
|
1896
1992
|
return {
|
|
1897
|
-
ok:
|
|
1898
|
-
|
|
1993
|
+
ok: true,
|
|
1994
|
+
contract: analysis.canonical,
|
|
1995
|
+
artifact,
|
|
1996
|
+
normalizedText: formatStructuredArtifactPointer(artifact),
|
|
1899
1997
|
};
|
|
1900
1998
|
}
|
|
1901
|
-
|
|
1999
|
+
catch (error) {
|
|
2000
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2001
|
+
return fail(reason);
|
|
2002
|
+
}
|
|
1902
2003
|
}
|
|
1903
2004
|
export async function materializeFrontendImplementationContract(input) {
|
|
1904
2005
|
const analysis = await analyzeFrontendImplementationContract(input);
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
6
|
import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
|
|
6
7
|
import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
|
|
7
|
-
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson,
|
|
8
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
8
9
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
9
10
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
10
11
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
@@ -157,60 +158,68 @@ async function selectNode(runDir, primary, fallbacks) {
|
|
|
157
158
|
}
|
|
158
159
|
throw new Error(`frontend prewrite gate no FINISHED candidate: ${rejections.join("; ")}`);
|
|
159
160
|
}
|
|
160
|
-
async function
|
|
161
|
-
|
|
161
|
+
async function readFinishedNodeRecord(runDir, nodeId) {
|
|
162
|
+
let record;
|
|
163
|
+
try {
|
|
164
|
+
record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
throw new Error(`frontend prewrite gate cannot parse node record ${nodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
168
|
+
}
|
|
162
169
|
if (record.status !== "FINISHED") {
|
|
163
170
|
throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
|
|
164
171
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// canonical plan appear as two complete JSON contracts to the prewrite gate.
|
|
172
|
+
return record;
|
|
173
|
+
}
|
|
174
|
+
async function readNodeText(runDir, nodeId) {
|
|
175
|
+
const record = await readFinishedNodeRecord(runDir, nodeId);
|
|
170
176
|
const text = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
171
177
|
if (!text)
|
|
172
178
|
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
173
179
|
return text;
|
|
174
180
|
}
|
|
175
|
-
async function
|
|
176
|
-
const record =
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
181
|
+
async function readCanonicalContractFromNode(runDir, nodeId) {
|
|
182
|
+
const record = await readFinishedNodeRecord(runDir, nodeId);
|
|
183
|
+
if (!record.structuredArtifactPath) {
|
|
184
|
+
throw new Error(`frontend prewrite gate missing structured artifact for ${nodeId}`);
|
|
185
|
+
}
|
|
186
|
+
const artifactPath = path.isAbsolute(record.structuredArtifactPath)
|
|
187
|
+
? record.structuredArtifactPath
|
|
188
|
+
: path.join(runDir, record.structuredArtifactPath);
|
|
189
|
+
const resolvedRun = path.resolve(runDir);
|
|
190
|
+
const resolvedArtifact = path.resolve(artifactPath);
|
|
191
|
+
const relative = path.relative(resolvedRun, resolvedArtifact);
|
|
192
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
193
|
+
throw new Error(`frontend prewrite gate structured artifact escapes runDir: ${record.structuredArtifactPath}`);
|
|
194
|
+
}
|
|
195
|
+
const bytes = await readFile(resolvedArtifact);
|
|
196
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
197
|
+
if (record.structuredArtifactSha256 &&
|
|
198
|
+
record.structuredArtifactSha256 !== digest) {
|
|
199
|
+
throw new Error(`frontend prewrite gate structured artifact hash mismatch for ${nodeId}`);
|
|
200
|
+
}
|
|
201
|
+
if (record.structuredArtifactSchemaId &&
|
|
202
|
+
record.structuredArtifactSchemaId !== FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID) {
|
|
203
|
+
throw new Error(`frontend prewrite gate structured artifact schema mismatch for ${nodeId}: ${record.structuredArtifactSchemaId}`);
|
|
204
|
+
}
|
|
205
|
+
const contractText = bytes.toString("utf8");
|
|
206
|
+
let citationsText = "";
|
|
207
|
+
try {
|
|
208
|
+
const citationsRaw = await readFile(path.join(path.dirname(resolvedArtifact), "openspec-citations.json"), "utf8");
|
|
209
|
+
try {
|
|
210
|
+
const parsed = JSON.parse(citationsRaw);
|
|
211
|
+
citationsText =
|
|
212
|
+
typeof parsed.block === "string" ? parsed.block : citationsRaw;
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
citationsText = citationsRaw;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
if (error.code !== "ENOENT")
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
return { contractText, citationsText, rawSha256: digest };
|
|
214
223
|
}
|
|
215
224
|
function firstNonEmptyVerdictLine(text) {
|
|
216
225
|
const lines = text
|
|
@@ -439,7 +448,30 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
439
448
|
// remains an ERROR and is intentionally not classified into prewrite-result.
|
|
440
449
|
const planNodeId = await selectNode(input.runDir, input.config.planFromNodeId, input.config.planFallbackFromNodeIds);
|
|
441
450
|
const reviewNodeId = await selectNode(input.runDir, input.config.reviewFromNodeId, input.config.reviewFallbackFromNodeIds);
|
|
442
|
-
|
|
451
|
+
let planArtifact;
|
|
452
|
+
try {
|
|
453
|
+
planArtifact = await readCanonicalContractFromNode(input.runDir, planNodeId);
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
return finalizePrewrite(input, {
|
|
457
|
+
planNodeId,
|
|
458
|
+
reviewNodeId,
|
|
459
|
+
verdict: "",
|
|
460
|
+
candidateRawSha256: sha256Hex(""),
|
|
461
|
+
candidateJsonSha256: null,
|
|
462
|
+
canonicalSha256: null,
|
|
463
|
+
normalizationActions: [],
|
|
464
|
+
mockStrategy: undefined,
|
|
465
|
+
artifact: undefined,
|
|
466
|
+
failureReason: error instanceof Error ? error.message : String(error),
|
|
467
|
+
failureCode: "plan-text-unreadable",
|
|
468
|
+
classification: "retryable-invalid",
|
|
469
|
+
openspecReadPaths: [],
|
|
470
|
+
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
471
|
+
openspecPolicy: input.config.openspecPolicy ?? "scan-strict",
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
const candidateRawSha256 = planArtifact.rawSha256;
|
|
443
475
|
const openspecPolicy = input.config.openspecPolicy ?? "scan-strict";
|
|
444
476
|
const basePending = {
|
|
445
477
|
planNodeId,
|
|
@@ -458,18 +490,9 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
458
490
|
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
459
491
|
openspecPolicy,
|
|
460
492
|
};
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
catch (error) {
|
|
466
|
-
return finalizePrewrite(input, {
|
|
467
|
-
...basePending,
|
|
468
|
-
classification: "retryable-invalid",
|
|
469
|
-
failureReason: error instanceof Error ? error.message : String(error),
|
|
470
|
-
failureCode: "plan-text-unreadable",
|
|
471
|
-
});
|
|
472
|
-
}
|
|
493
|
+
const planText = [planArtifact.contractText, planArtifact.citationsText]
|
|
494
|
+
.filter(Boolean)
|
|
495
|
+
.join("\n");
|
|
473
496
|
let reviewText;
|
|
474
497
|
try {
|
|
475
498
|
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
@@ -492,77 +515,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
492
515
|
failureCode: "verdict-not-pass",
|
|
493
516
|
});
|
|
494
517
|
}
|
|
495
|
-
|
|
496
|
-
// delta against the original contract node (planFallbackFromNodeIds[0]).
|
|
497
|
-
// Legacy DAGs without revisionPatch (undefined) keep full-contract semantics.
|
|
498
|
-
const revisionPatchMode = input.config.revisionPatch === true &&
|
|
499
|
-
planNodeId === input.config.planFromNodeId &&
|
|
500
|
-
input.config.planFallbackFromNodeIds.length > 0;
|
|
501
|
-
let effectiveContractText = null;
|
|
502
|
-
if (revisionPatchMode) {
|
|
503
|
-
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
504
|
-
try {
|
|
505
|
-
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
506
|
-
}
|
|
507
|
-
catch (error) {
|
|
508
|
-
return finalizePrewrite(input, {
|
|
509
|
-
...basePending,
|
|
510
|
-
verdict,
|
|
511
|
-
classification: "retryable-invalid",
|
|
512
|
-
failureReason: `frontend prewrite gate revisionPatch mode requires original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
513
|
-
failureCode: "contract-invalid",
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
// Content-driven compatibility (Bug 1): legacy DAGs without
|
|
519
|
-
// revisionPatch can still receive a merge-patch delta from the revision
|
|
520
|
-
// node. Detect the output shape instead of trusting the DAG flag alone.
|
|
521
|
-
// - Full contract shape → keep legacy full-contract semantics unchanged.
|
|
522
|
-
// - Patch shape with a usable original contract node → auto-merge and
|
|
523
|
-
// run the existing analysis/materialization pipeline.
|
|
524
|
-
// - Patch shape but unmergeable → blocked + revision-patch-unmergeable
|
|
525
|
-
// (no retryable loop).
|
|
526
|
-
let planJsonValue = null;
|
|
527
|
-
try {
|
|
528
|
-
planJsonValue = extractFrontendImplementationJson(planText);
|
|
529
|
-
}
|
|
530
|
-
catch {
|
|
531
|
-
// Not machine-readable JSON: fall back to the legacy full-contract
|
|
532
|
-
// path where analyzeFrontendImplementationContract surfaces the
|
|
533
|
-
// original diagnostic (fail-closed without a new failure mode).
|
|
534
|
-
}
|
|
535
|
-
if (planJsonValue !== null && !isFullContractShape(planJsonValue)) {
|
|
536
|
-
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
537
|
-
if (!originalNodeId || originalNodeId === planNodeId) {
|
|
538
|
-
return finalizePrewrite(input, {
|
|
539
|
-
...basePending,
|
|
540
|
-
verdict,
|
|
541
|
-
classification: "blocked",
|
|
542
|
-
failureReason: "frontend prewrite gate: the effective plan output is a merge-patch delta but no distinct original contract node is available. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.",
|
|
543
|
-
failureCode: "revision-patch-unmergeable",
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
try {
|
|
547
|
-
const originalText = await readNodeText(input.runDir, originalNodeId);
|
|
548
|
-
const originalValue = extractFrontendImplementationJson(originalText);
|
|
549
|
-
if (!isFullContractShape(originalValue)) {
|
|
550
|
-
throw new Error(`original contract node ${originalNodeId} is not a full frontend implementation contract`);
|
|
551
|
-
}
|
|
552
|
-
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
553
|
-
}
|
|
554
|
-
catch (error) {
|
|
555
|
-
return finalizePrewrite(input, {
|
|
556
|
-
...basePending,
|
|
557
|
-
verdict,
|
|
558
|
-
classification: "blocked",
|
|
559
|
-
failureReason: `frontend prewrite gate: the effective plan output is a merge-patch delta but it cannot be merged onto the original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}. Check the DAG revisionPatch/planFallbackFromNodeIds configuration or regenerate the DAG.`,
|
|
560
|
-
failureCode: "revision-patch-unmergeable",
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
const requirementIdSearchText = effectiveContractText ?? planText;
|
|
518
|
+
const requirementIdSearchText = planArtifact.contractText;
|
|
566
519
|
const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
|
|
567
520
|
if (missingIds.length > 0) {
|
|
568
521
|
return finalizePrewrite(input, {
|
|
@@ -610,14 +563,13 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
610
563
|
workspaceRoot,
|
|
611
564
|
});
|
|
612
565
|
}
|
|
613
|
-
// Candidate -> canonical conversion. The gate consumes the
|
|
614
|
-
//
|
|
566
|
+
// Candidate -> canonical conversion. The gate consumes the producing node's
|
|
567
|
+
// hash-bound canonical artifact and never re-merges plan/revision stdout.
|
|
615
568
|
let analysis;
|
|
616
569
|
try {
|
|
617
570
|
analysis = await analyzeFrontendImplementationContract({
|
|
618
571
|
runDir: input.runDir,
|
|
619
|
-
|
|
620
|
-
rawContractText: effectiveContractText ?? undefined,
|
|
572
|
+
rawContractText: planArtifact.contractText,
|
|
621
573
|
sourceBinding: input.sourceBinding,
|
|
622
574
|
});
|
|
623
575
|
}
|
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { markdownSection } from "./frontend-test-markdown.js";
|
|
3
4
|
const CASE_ID = /^FE-[A-Za-z0-9]+-\d{3}-(?:core|boundary|flow|backend)$/i;
|
|
4
5
|
const AC_ID = /^AC(?:-[A-Z0-9]+)+$/i;
|
|
5
6
|
const PLACEHOLDER = /\b(?:TODO|TBD|FIXME)\b|结果正确|页面正常|按实际情况处理|验证成功/i;
|
|
6
7
|
const SECTION_ALIASES = {
|
|
7
8
|
purpose: ["Test Purpose", "测试目的", "测试场景"],
|
|
8
9
|
source: ["Source References", "需求依据"],
|
|
9
|
-
preconditions: ["Preconditions", "前置条件"],
|
|
10
|
-
steps: ["Steps", "操作步骤"],
|
|
10
|
+
preconditions: ["Preconditions", "前置条件", "前置条件与重置"],
|
|
11
|
+
steps: ["Test Steps", "测试步骤", "Steps", "操作步骤"],
|
|
11
12
|
expected: ["Expected Results", "预期结果"],
|
|
12
13
|
automation: ["Automation Notes", "自动化映射", "自动化说明"],
|
|
13
14
|
};
|
|
14
|
-
function section(body, names) {
|
|
15
|
-
const marker = new RegExp(`^###\\s+(?:${names.map((v) => v.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")).join("|")})\\s*$`, "mi");
|
|
16
|
-
const hit = marker.exec(body);
|
|
17
|
-
if (!hit)
|
|
18
|
-
return "";
|
|
19
|
-
const rest = body.slice(hit.index + hit[0].length);
|
|
20
|
-
const next = /^###\s+/m.exec(rest);
|
|
21
|
-
return rest.slice(0, next?.index ?? rest.length).trim();
|
|
22
|
-
}
|
|
23
15
|
function hasList(value) { return /^\s*(?:\d+[.)]|[-*+])\s+\S+/m.test(value); }
|
|
24
16
|
function hasAssertion(value) { return /(?:status|状态|包含|显示|等于|为|可见|不可见|跳转|错误|成功|失败|should|expect|assert|must)/i.test(value); }
|
|
25
17
|
export async function validateFrontendCaseContent(input) {
|
|
@@ -69,9 +61,9 @@ export async function validateFrontendCaseContent(input) {
|
|
|
69
61
|
if (PLACEHOLDER.test(body))
|
|
70
62
|
findings.push({ ruleId: "placeholder-wording", caseId: id, detail: "case contains placeholder or non-assertable wording" });
|
|
71
63
|
for (const [key, names] of Object.entries(SECTION_ALIASES))
|
|
72
|
-
if (!
|
|
64
|
+
if (!markdownSection(body, names))
|
|
73
65
|
findings.push({ ruleId: `missing-${key}`, caseId: id, detail: `missing section: ${names.join(" or ")}` });
|
|
74
|
-
const steps =
|
|
66
|
+
const steps = markdownSection(body, SECTION_ALIASES.steps), expected = markdownSection(body, SECTION_ALIASES.expected);
|
|
75
67
|
if (!hasList(steps))
|
|
76
68
|
findings.push({ ruleId: "unstructured-steps", caseId: id, detail: "steps should contain numbered or bulleted executable actions" });
|
|
77
69
|
if (!hasList(expected) || !hasAssertion(expected))
|