@tea-agent/loop-agent 0.38.2 → 0.39.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +2 -0
- 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/pi-event-serializer.js +42 -1
- package/dist/executors/pi-executor.js +51 -0
- package/dist/executors/pi-sdk-executor.js +7 -26
- package/dist/executors/shell-executor.js +2 -2
- 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/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/frontend-implementation-contract.js +79 -17
- package/dist/workflows/dag/frontend-prewrite-gate.js +104 -27
- package/dist/workflows/dag/init-hybrid.js +59 -31
- package/dist/workflows/dag/node-execution.js +52 -2
- package/dist/workflows/dag/prompt.js +23 -1
- package/dist/workflows/dag/types.js +6 -0
- package/dist/workflows/dag/upstream-artifacts.js +4 -0
- 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/backend-test-dag.json +2 -2
- 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/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
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import YAML from "yaml";
|
|
7
7
|
import { resolveBackendTestLayout, } from "./backend-test-layout.js";
|
|
8
|
-
import { assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
|
|
8
|
+
import { applyOpenApiPartitionDomainPolicy, assessScenarioPartitionCoverage, parseScenarioPartitions, } from "./backend-test-scenario-partitions.js";
|
|
9
9
|
import { backendTestCaseManifestSchema, computeCaseManifestCoverageSummary, } from "./backend-test-case-manifest.js";
|
|
10
10
|
const CASE_HEADING = /^##\s+(BE-[A-Z0-9_-]+-\d{3})\b.*$/gm;
|
|
11
11
|
const CASE_ID_IN_TEXT = /\bBE-[A-Z0-9_-]+-\d{3}\b/g;
|
|
@@ -486,6 +486,55 @@ export async function extractBackendTestOpenApiRules(input) {
|
|
|
486
486
|
}
|
|
487
487
|
return rules.sort((left, right) => left.ruleKey.localeCompare(right.ruleKey));
|
|
488
488
|
}
|
|
489
|
+
export async function extractBackendTestOpenApiPartitionAxes(input) {
|
|
490
|
+
const axes = [];
|
|
491
|
+
const seen = new Set();
|
|
492
|
+
for (const referencePath of input.sourceBinding.referencePaths) {
|
|
493
|
+
const physicalPath = resolveBoundSourcePath(input.workspaceRoot, input.sourceBinding.taskId, referencePath);
|
|
494
|
+
let document;
|
|
495
|
+
try {
|
|
496
|
+
const parsed = YAML.parse(await readFile(physicalPath, "utf8"));
|
|
497
|
+
if (!isRecord(parsed) || !isRecord(parsed.paths))
|
|
498
|
+
continue;
|
|
499
|
+
document = parsed;
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
for (const [apiPath, pathItem] of Object.entries(document.paths)) {
|
|
505
|
+
if (!isRecord(pathItem))
|
|
506
|
+
continue;
|
|
507
|
+
const operation = pathItem.get;
|
|
508
|
+
if (!isRecord(operation))
|
|
509
|
+
continue;
|
|
510
|
+
const parameterValues = [
|
|
511
|
+
...(Array.isArray(pathItem.parameters) ? pathItem.parameters : []),
|
|
512
|
+
...(Array.isArray(operation.parameters) ? operation.parameters : []),
|
|
513
|
+
];
|
|
514
|
+
for (const rawParameter of parameterValues) {
|
|
515
|
+
const parameter = resolveOpenApiSchema(document, rawParameter);
|
|
516
|
+
if (!parameter || typeof parameter.name !== "string")
|
|
517
|
+
continue;
|
|
518
|
+
const location = parameter.in === "path" ? "path" : parameter.in === "query" ? "query" : undefined;
|
|
519
|
+
if (!location)
|
|
520
|
+
continue;
|
|
521
|
+
const property = resolveOpenApiSchema(document, parameter.schema);
|
|
522
|
+
const enumValues = property && Array.isArray(property.enum) ? property.enum.map(String) : null;
|
|
523
|
+
const identity = `GET ${apiPath}|${location}|${parameter.name}`;
|
|
524
|
+
if (seen.has(identity))
|
|
525
|
+
continue;
|
|
526
|
+
seen.add(identity);
|
|
527
|
+
axes.push({
|
|
528
|
+
operation: `GET ${apiPath}`,
|
|
529
|
+
name: parameter.name,
|
|
530
|
+
in: location,
|
|
531
|
+
enumValues,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return axes;
|
|
537
|
+
}
|
|
489
538
|
function orderedUnique(values) {
|
|
490
539
|
const result = [];
|
|
491
540
|
const seen = new Set();
|
|
@@ -985,8 +1034,11 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
985
1034
|
// Plan C: deterministic query/filter axis partitions (fail-closed on missing slots).
|
|
986
1035
|
const parsedPartitions = parseScenarioPartitions(readme);
|
|
987
1036
|
findings.push(...parsedPartitions.issues);
|
|
1037
|
+
const openApiPartitionAxes = await extractBackendTestOpenApiPartitionAxes(input);
|
|
1038
|
+
const partitionedBySource = applyOpenApiPartitionDomainPolicy(parsedPartitions.partitions, openApiPartitionAxes);
|
|
1039
|
+
findings.push(...partitionedBySource.issues);
|
|
988
1040
|
const declaredPointUniverse = new Set(cases.flatMap((item) => item.testPoints));
|
|
989
|
-
const partitionAssessment = assessScenarioPartitionCoverage(
|
|
1041
|
+
const partitionAssessment = assessScenarioPartitionCoverage(partitionedBySource.partitions, declaredPointUniverse);
|
|
990
1042
|
findings.push(...partitionAssessment.findings);
|
|
991
1043
|
const [enumValueCount, coveredEnumValueCount] = dimensionCounts(rules, /enum/);
|
|
992
1044
|
const [boundaryPointCount, coveredBoundaryPointCount] = dimensionCounts(rules, /boundary|length|numeric/);
|
|
@@ -1003,7 +1055,7 @@ export async function analyzeBackendTestCaseCoverage(input) {
|
|
|
1003
1055
|
sourceBinding: input.sourceBinding,
|
|
1004
1056
|
inputFiles: await inputFileFacts(input.workspaceRoot, [
|
|
1005
1057
|
...((await exists(readmePath)) ? [readmePath] : []),
|
|
1006
|
-
...(await markdownModuleFiles(input.workspaceRoot)),
|
|
1058
|
+
...(await markdownModuleFiles(input.workspaceRoot, layout)),
|
|
1007
1059
|
...(await Promise.all(input.sourceBinding.referencePaths.map(async (referencePath) => {
|
|
1008
1060
|
const physicalPath = resolveBoundSourcePath(input.workspaceRoot, input.sourceBinding.taskId, referencePath);
|
|
1009
1061
|
return (await exists(physicalPath)) ? physicalPath : "";
|
|
@@ -1794,7 +1846,7 @@ export async function analyzeBackendTestMarkdownPytestCorrespondence(input) {
|
|
|
1794
1846
|
status: findings.length === 0 ? "PASS" : "FAIL",
|
|
1795
1847
|
inputFiles: await inputFileFacts(input.workspaceRoot, [
|
|
1796
1848
|
path.join(input.workspaceRoot, ...layout.readmePath.split("/")),
|
|
1797
|
-
...(await markdownModuleFiles(input.workspaceRoot)),
|
|
1849
|
+
...(await markdownModuleFiles(input.workspaceRoot, layout)),
|
|
1798
1850
|
...scriptFiles,
|
|
1799
1851
|
].filter((file) => file && path.isAbsolute(file))),
|
|
1800
1852
|
summary: {
|
|
@@ -35,6 +35,8 @@ export const scenarioPartitionRowSchema = z.object({
|
|
|
35
35
|
const PARTITION_HEADING = /^##\s+Scenario Partitions\s*$/;
|
|
36
36
|
const PARTITION_TABLE_HEADER = /^\|\s*Partition ID\s*\|\s*Operation\s*\|\s*Axis\s*\|\s*Domain\s*\|\s*Required Slots\s*\|\s*Expected by Slot\s*\|\s*Bind Rule\s*\|\s*$/;
|
|
37
37
|
const TABLE_ROW = /^\|(.+)\|$/;
|
|
38
|
+
const FINITE_DOMAIN_MEMBER = /^[A-Za-z0-9]+(?:[._\- ][A-Za-z0-9]+)*$/;
|
|
39
|
+
const RESERVED_SLOT_TOKENS = new Set(["OMITTED", "NOT-IN-SET"]);
|
|
38
40
|
function splitCells(raw) {
|
|
39
41
|
return raw
|
|
40
42
|
.split("|")
|
|
@@ -100,6 +102,11 @@ export function parseScenarioPartitions(readmeMarkdown) {
|
|
|
100
102
|
continue;
|
|
101
103
|
}
|
|
102
104
|
seen.add(partitionId);
|
|
105
|
+
const domainIssue = describeInvalidPartitionDomain(domain);
|
|
106
|
+
if (domainIssue) {
|
|
107
|
+
issues.push(`INVALID_PARTITION_DOMAIN: partition ${partitionId} ${domainIssue}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
103
110
|
partitions.push({
|
|
104
111
|
partitionId,
|
|
105
112
|
operation,
|
|
@@ -119,6 +126,40 @@ function slotToken(value) {
|
|
|
119
126
|
.replace(/[^A-Z0-9]+/g, "-")
|
|
120
127
|
.replace(/^-+|-+$/g, "");
|
|
121
128
|
}
|
|
129
|
+
function describeInvalidPartitionDomain(domain) {
|
|
130
|
+
const tokens = [];
|
|
131
|
+
const seenTokens = new Set();
|
|
132
|
+
for (const value of domain) {
|
|
133
|
+
if (!FINITE_DOMAIN_MEMBER.test(value)) {
|
|
134
|
+
return `value is not a finite identifier member: ${value}`;
|
|
135
|
+
}
|
|
136
|
+
const token = slotToken(value);
|
|
137
|
+
if (!token) {
|
|
138
|
+
return `value cannot produce a stable non-empty slot token: ${value}`;
|
|
139
|
+
}
|
|
140
|
+
if (RESERVED_SLOT_TOKENS.has(token)) {
|
|
141
|
+
return `value maps to reserved slot token ${token}: ${value}`;
|
|
142
|
+
}
|
|
143
|
+
if (seenTokens.has(token)) {
|
|
144
|
+
return `values collapse to duplicate slot token ${token}`;
|
|
145
|
+
}
|
|
146
|
+
seenTokens.add(token);
|
|
147
|
+
tokens.push(token);
|
|
148
|
+
}
|
|
149
|
+
return tokens.length === 0 ? "domain produced no slot tokens" : undefined;
|
|
150
|
+
}
|
|
151
|
+
function normalizePartitionAxisName(value) {
|
|
152
|
+
return value.trim().replace(/^(?:query|path)\s*:\s*/i, "").toLowerCase();
|
|
153
|
+
}
|
|
154
|
+
function normalizePartitionOperation(value) {
|
|
155
|
+
return value.trim().replace(/\s+/g, " ").toUpperCase();
|
|
156
|
+
}
|
|
157
|
+
function sameFiniteSet(left, right) {
|
|
158
|
+
if (left.length !== right.length)
|
|
159
|
+
return false;
|
|
160
|
+
const expected = new Set(left);
|
|
161
|
+
return right.every((value) => expected.has(value));
|
|
162
|
+
}
|
|
122
163
|
const STABLE_PARTITION_ID = /^SP-[A-Z0-9][A-Z0-9._-]*$/i;
|
|
123
164
|
/**
|
|
124
165
|
* Slot IDs are `TP-<Partition ID>-<VALUE|OMITTED|NOT-IN-SET>`.
|
|
@@ -138,13 +179,16 @@ function partitionToken(row) {
|
|
|
138
179
|
export function expandScenarioPartitions(partitions) {
|
|
139
180
|
const slots = [];
|
|
140
181
|
for (const row of partitions) {
|
|
182
|
+
if (describeInvalidPartitionDomain(row.domain))
|
|
183
|
+
continue;
|
|
141
184
|
const prefix = partitionToken(row);
|
|
142
185
|
for (const value of row.domain) {
|
|
186
|
+
const token = slotToken(value);
|
|
143
187
|
slots.push({
|
|
144
188
|
partitionId: row.partitionId,
|
|
145
189
|
operation: row.operation,
|
|
146
190
|
axis: row.axis,
|
|
147
|
-
slotId: `${prefix}-${
|
|
191
|
+
slotId: `${prefix}-${token}`,
|
|
148
192
|
kind: "each-value",
|
|
149
193
|
value,
|
|
150
194
|
intent: "nominal-filter",
|
|
@@ -176,6 +220,34 @@ export function expandScenarioPartitions(partitions) {
|
|
|
176
220
|
}
|
|
177
221
|
return slots;
|
|
178
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Bind parsed partitions to GET query/path OpenAPI axes.
|
|
225
|
+
* Unconfirmed axes keep identifier-valid Domain rows (requirement finite sets).
|
|
226
|
+
*/
|
|
227
|
+
export function applyOpenApiPartitionDomainPolicy(partitions, axes) {
|
|
228
|
+
const issues = [];
|
|
229
|
+
const valid = [];
|
|
230
|
+
for (const row of partitions) {
|
|
231
|
+
const operation = normalizePartitionOperation(row.operation);
|
|
232
|
+
const axisName = normalizePartitionAxisName(row.axis);
|
|
233
|
+
const matched = axes.find((axis) => normalizePartitionOperation(axis.operation) === operation &&
|
|
234
|
+
normalizePartitionAxisName(axis.name) === axisName);
|
|
235
|
+
if (!matched) {
|
|
236
|
+
valid.push(row);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (matched.enumValues === null) {
|
|
240
|
+
issues.push(`NO_FINITE_DOMAIN: partition ${row.partitionId} axis ${row.axis} exists on ${row.operation} without a documented enum`);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (!sameFiniteSet(matched.enumValues, row.domain)) {
|
|
244
|
+
issues.push(`PARTITION_SOURCE_MISMATCH: partition ${row.partitionId} domain does not match OpenAPI enum for ${row.operation} ${row.axis}`);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
valid.push(row);
|
|
248
|
+
}
|
|
249
|
+
return { partitions: valid, issues };
|
|
250
|
+
}
|
|
179
251
|
/** Facts projection consumed by N6 and plan D gap-fill. */
|
|
180
252
|
export const scenarioPartitionFactsSchema = z.object({
|
|
181
253
|
partitions: z.array(z.object({
|
|
@@ -1722,9 +1722,37 @@ function extractSingleOpenspecCitationsBlock(text) {
|
|
|
1722
1722
|
}
|
|
1723
1723
|
return blocks[0];
|
|
1724
1724
|
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Frontend plan/revision output has two JSON-bearing regions: the primary
|
|
1727
|
+
* payload and line-oriented OpenSpec citations. Keep their parsing domains
|
|
1728
|
+
* separate so a citation row can never become a competing contract candidate.
|
|
1729
|
+
* A bare JSON payload is retained only for historical node-output compatibility.
|
|
1730
|
+
*/
|
|
1731
|
+
export function extractFrontendPlanPayload(text) {
|
|
1732
|
+
const citationsBlock = extractSingleOpenspecCitationsBlock(text);
|
|
1733
|
+
return {
|
|
1734
|
+
payload: extractFrontendPlanPrimaryPayload(text.replace(citationsBlock, "")),
|
|
1735
|
+
citationsBlock,
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
function extractFrontendPlanPrimaryPayload(text) {
|
|
1739
|
+
const fences = [...text.matchAll(/```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi)];
|
|
1740
|
+
if (fences.length > 1) {
|
|
1741
|
+
throw new Error(`frontend plan output must contain exactly one primary fenced json payload (found ${fences.length})`);
|
|
1742
|
+
}
|
|
1743
|
+
if (fences.length === 1) {
|
|
1744
|
+
try {
|
|
1745
|
+
return JSON.parse(fences[0][1]);
|
|
1746
|
+
}
|
|
1747
|
+
catch (error) {
|
|
1748
|
+
throw new Error(`frontend plan primary json fence is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
return extractFrontendImplementationJson(text);
|
|
1752
|
+
}
|
|
1725
1753
|
async function writeFrontendPlanCandidateRaw(input) {
|
|
1726
1754
|
const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
|
|
1727
|
-
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text
|
|
1755
|
+
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text);
|
|
1728
1756
|
return relativePath;
|
|
1729
1757
|
}
|
|
1730
1758
|
/**
|
|
@@ -1770,13 +1798,15 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1770
1798
|
const skeleton = input.structuredContractOutput?.skeleton;
|
|
1771
1799
|
if (!skeleton)
|
|
1772
1800
|
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
1773
|
-
const patch =
|
|
1801
|
+
const patch = extractFrontendPlanPrimaryPayload(input.text);
|
|
1774
1802
|
if (!isPlainObject(patch))
|
|
1775
1803
|
throw new Error("frontend plan patch must extract to one JSON object");
|
|
1776
1804
|
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
1777
1805
|
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
1778
1806
|
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1779
1807
|
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1808
|
+
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1809
|
+
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1780
1810
|
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1781
1811
|
if (protectedViolations.length > 0)
|
|
1782
1812
|
throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
@@ -1807,6 +1837,19 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1807
1837
|
return {
|
|
1808
1838
|
ok: true,
|
|
1809
1839
|
contract: analysis.canonical,
|
|
1840
|
+
structuredArtifact: {
|
|
1841
|
+
path: normalizedArtifact.path,
|
|
1842
|
+
sha256: normalizedArtifact.sha256,
|
|
1843
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1844
|
+
},
|
|
1845
|
+
frontendPlanOutput: {
|
|
1846
|
+
schemaVersion: 1,
|
|
1847
|
+
payloadKind: "canonical-contract",
|
|
1848
|
+
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1849
|
+
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1850
|
+
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1851
|
+
rawCandidateSha256: sha256Hex(input.text),
|
|
1852
|
+
},
|
|
1810
1853
|
normalizedText: [
|
|
1811
1854
|
"Frontend implementation contract compiled from the runtime skeleton.",
|
|
1812
1855
|
"",
|
|
@@ -1862,27 +1905,26 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1862
1905
|
* the prewrite gate, which remains the only authority.
|
|
1863
1906
|
*/
|
|
1864
1907
|
export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
1908
|
+
const nodeId = input.nodeId ?? "frontend-plan-revision-pi";
|
|
1909
|
+
const attempt = input.attempt ?? 1;
|
|
1910
|
+
const candidateDir = path.posix.join("contracts", "candidates", nodeId);
|
|
1911
|
+
const rawPath = await writeFrontendPlanCandidateRaw({
|
|
1912
|
+
runDir: input.runDir,
|
|
1913
|
+
nodeId,
|
|
1914
|
+
attempt,
|
|
1915
|
+
text: input.text,
|
|
1916
|
+
});
|
|
1865
1917
|
let citationsBlock;
|
|
1866
|
-
try {
|
|
1867
|
-
citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1868
|
-
}
|
|
1869
|
-
catch (error) {
|
|
1870
|
-
return {
|
|
1871
|
-
ok: false,
|
|
1872
|
-
reason: error instanceof Error ? error.message : String(error),
|
|
1873
|
-
};
|
|
1874
|
-
}
|
|
1875
|
-
// Strip the citation blocks before extraction: their per-row JSON objects
|
|
1876
|
-
// would otherwise count as competing balanced objects in the extractor.
|
|
1877
|
-
const textWithoutCitations = input.text.replace(citationsBlock, "");
|
|
1878
1918
|
let patch;
|
|
1879
1919
|
try {
|
|
1880
|
-
|
|
1920
|
+
const parsed = extractFrontendPlanPayload(input.text);
|
|
1921
|
+
citationsBlock = parsed.citationsBlock;
|
|
1922
|
+
patch = parsed.payload;
|
|
1881
1923
|
}
|
|
1882
1924
|
catch (error) {
|
|
1883
1925
|
return {
|
|
1884
1926
|
ok: false,
|
|
1885
|
-
reason:
|
|
1927
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1886
1928
|
};
|
|
1887
1929
|
}
|
|
1888
1930
|
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
@@ -1898,7 +1940,27 @@ export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
|
1898
1940
|
reason: `frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`,
|
|
1899
1941
|
};
|
|
1900
1942
|
}
|
|
1901
|
-
|
|
1943
|
+
const payloadPath = path.posix.join(candidateDir, `attempt-${attempt}.patch.json`);
|
|
1944
|
+
const payloadArtifact = await writeDeterministicJsonArtifact(input.runDir, payloadPath, patch);
|
|
1945
|
+
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1946
|
+
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1947
|
+
return {
|
|
1948
|
+
ok: true,
|
|
1949
|
+
contract: patch,
|
|
1950
|
+
structuredArtifact: {
|
|
1951
|
+
path: payloadArtifact.path,
|
|
1952
|
+
sha256: payloadArtifact.sha256,
|
|
1953
|
+
schemaId: "frontend-implementation-contract-revision-patch-v1",
|
|
1954
|
+
},
|
|
1955
|
+
frontendPlanOutput: {
|
|
1956
|
+
schemaVersion: 1,
|
|
1957
|
+
payloadKind: "merge-patch",
|
|
1958
|
+
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1959
|
+
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1960
|
+
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1961
|
+
rawCandidateSha256: sha256Hex(input.text),
|
|
1962
|
+
},
|
|
1963
|
+
};
|
|
1902
1964
|
}
|
|
1903
1965
|
export async function materializeFrontendImplementationContract(input) {
|
|
1904
1966
|
const analysis = await analyzeFrontendImplementationContract(input);
|
|
@@ -11,8 +11,6 @@ import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
|
11
11
|
import { normalizeVerdictCandidateLine } from "./dynamic-runtime/shared.js";
|
|
12
12
|
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
13
13
|
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
14
|
-
/** Canonical contract serialization must stay at or below this byte size. */
|
|
15
|
-
export const FRONTEND_CONTRACT_MAX_BYTES = 65536;
|
|
16
14
|
/**
|
|
17
15
|
* Machine-readable failure classification for blocked/retryable-invalid
|
|
18
16
|
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
@@ -39,7 +37,6 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
|
39
37
|
"component-spec-reference-invalid",
|
|
40
38
|
"component-spec-not-cited",
|
|
41
39
|
"component-choices-missing",
|
|
42
|
-
"contract-too-large",
|
|
43
40
|
"revision-patch-unmergeable",
|
|
44
41
|
]);
|
|
45
42
|
export const frontendPrewriteResultV1Schema = z
|
|
@@ -176,6 +173,82 @@ async function readNodeRawText(runDir, nodeId) {
|
|
|
176
173
|
const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
177
174
|
return record.assistantText ?? record.stdout ?? "";
|
|
178
175
|
}
|
|
176
|
+
function resolveRunOwnedArtifact(runDir, candidate, label) {
|
|
177
|
+
const root = path.resolve(runDir);
|
|
178
|
+
const resolved = path.resolve(candidate);
|
|
179
|
+
const relative = path.relative(root, resolved);
|
|
180
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
181
|
+
throw new Error(`frontend prewrite gate ${label} must be contained by the current run directory`);
|
|
182
|
+
}
|
|
183
|
+
return resolved;
|
|
184
|
+
}
|
|
185
|
+
async function readHashBoundArtifact(input) {
|
|
186
|
+
const resolved = resolveRunOwnedArtifact(input.runDir, input.artifactPath, input.label);
|
|
187
|
+
const text = await readFile(resolved, "utf8");
|
|
188
|
+
const actual = sha256Hex(text);
|
|
189
|
+
if (actual !== input.expectedSha256) {
|
|
190
|
+
throw new Error(`frontend prewrite gate ${input.label} SHA-256 mismatch (expected ${input.expectedSha256}, got ${actual})`);
|
|
191
|
+
}
|
|
192
|
+
return text;
|
|
193
|
+
}
|
|
194
|
+
async function readFrontendPlanNodeInput(runDir, nodeId) {
|
|
195
|
+
const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
196
|
+
if (record.status !== "FINISHED") {
|
|
197
|
+
throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
|
|
198
|
+
}
|
|
199
|
+
const frontend = record.frontendPlanOutput;
|
|
200
|
+
if (!frontend) {
|
|
201
|
+
const payloadText = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
202
|
+
if (!payloadText)
|
|
203
|
+
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
204
|
+
return {
|
|
205
|
+
artifactFirst: false,
|
|
206
|
+
payloadText,
|
|
207
|
+
citationsText: payloadText,
|
|
208
|
+
rawCandidateSha256: sha256Hex(record.assistantText ?? record.stdout ?? ""),
|
|
209
|
+
payloadKind: "canonical-contract",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (frontend.schemaVersion !== 1 ||
|
|
213
|
+
!record.structuredArtifactPath ||
|
|
214
|
+
!record.structuredArtifactSha256 ||
|
|
215
|
+
!record.structuredArtifactSchemaId) {
|
|
216
|
+
throw new Error(`frontend prewrite gate ${nodeId} has incomplete artifact-first metadata`);
|
|
217
|
+
}
|
|
218
|
+
const expectedSchema = frontend.payloadKind === "canonical-contract"
|
|
219
|
+
? "frontend-implementation-contract-v1"
|
|
220
|
+
: "frontend-implementation-contract-revision-patch-v1";
|
|
221
|
+
if (record.structuredArtifactSchemaId !== expectedSchema) {
|
|
222
|
+
throw new Error(`frontend prewrite gate ${nodeId} artifact schema ${record.structuredArtifactSchemaId} does not match ${frontend.payloadKind}`);
|
|
223
|
+
}
|
|
224
|
+
const [payloadText, citationsText] = await Promise.all([
|
|
225
|
+
readHashBoundArtifact({
|
|
226
|
+
runDir,
|
|
227
|
+
artifactPath: record.structuredArtifactPath,
|
|
228
|
+
expectedSha256: record.structuredArtifactSha256,
|
|
229
|
+
label: `${nodeId} primary payload`,
|
|
230
|
+
}),
|
|
231
|
+
readHashBoundArtifact({
|
|
232
|
+
runDir,
|
|
233
|
+
artifactPath: frontend.citationsArtifactPath,
|
|
234
|
+
expectedSha256: frontend.citationsArtifactSha256,
|
|
235
|
+
label: `${nodeId} citations`,
|
|
236
|
+
}),
|
|
237
|
+
readHashBoundArtifact({
|
|
238
|
+
runDir,
|
|
239
|
+
artifactPath: frontend.rawCandidatePath,
|
|
240
|
+
expectedSha256: frontend.rawCandidateSha256,
|
|
241
|
+
label: `${nodeId} raw candidate`,
|
|
242
|
+
}).then(() => undefined),
|
|
243
|
+
]);
|
|
244
|
+
return {
|
|
245
|
+
artifactFirst: true,
|
|
246
|
+
payloadText,
|
|
247
|
+
citationsText,
|
|
248
|
+
rawCandidateSha256: frontend.rawCandidateSha256,
|
|
249
|
+
payloadKind: frontend.payloadKind,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
179
252
|
/**
|
|
180
253
|
* Same shape predicate as the internal `isContract` inside
|
|
181
254
|
* `extractFrontendImplementationJson` (frontend-implementation-contract.ts):
|
|
@@ -458,9 +531,9 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
458
531
|
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
459
532
|
openspecPolicy,
|
|
460
533
|
};
|
|
461
|
-
let
|
|
534
|
+
let planInput;
|
|
462
535
|
try {
|
|
463
|
-
|
|
536
|
+
planInput = await readFrontendPlanNodeInput(input.runDir, planNodeId);
|
|
464
537
|
}
|
|
465
538
|
catch (error) {
|
|
466
539
|
return finalizePrewrite(input, {
|
|
@@ -470,6 +543,9 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
470
543
|
failureCode: "plan-text-unreadable",
|
|
471
544
|
});
|
|
472
545
|
}
|
|
546
|
+
const planText = planInput.payloadText;
|
|
547
|
+
const citationsText = planInput.citationsText;
|
|
548
|
+
basePending.candidateRawSha256 = planInput.rawCandidateSha256;
|
|
473
549
|
let reviewText;
|
|
474
550
|
try {
|
|
475
551
|
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
@@ -502,7 +578,20 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
502
578
|
if (revisionPatchMode) {
|
|
503
579
|
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
504
580
|
try {
|
|
505
|
-
|
|
581
|
+
const originalInput = await readFrontendPlanNodeInput(input.runDir, originalNodeId);
|
|
582
|
+
if (planInput.artifactFirst && !originalInput.artifactFirst) {
|
|
583
|
+
throw new Error(`artifact-first revision ${planNodeId} requires artifact-first original contract ${originalNodeId}`);
|
|
584
|
+
}
|
|
585
|
+
if (planInput.artifactFirst) {
|
|
586
|
+
if (planInput.payloadKind !== "merge-patch" ||
|
|
587
|
+
originalInput.payloadKind !== "canonical-contract") {
|
|
588
|
+
throw new Error("artifact-first revision/original payload kinds are invalid");
|
|
589
|
+
}
|
|
590
|
+
effectiveContractText = serializeDeterministicJson(applyFrontendImplementationContractPatch(JSON.parse(originalInput.payloadText), JSON.parse(planInput.payloadText)));
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
594
|
+
}
|
|
506
595
|
}
|
|
507
596
|
catch (error) {
|
|
508
597
|
return finalizePrewrite(input, {
|
|
@@ -610,14 +699,17 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
610
699
|
workspaceRoot,
|
|
611
700
|
});
|
|
612
701
|
}
|
|
613
|
-
// Candidate -> canonical conversion.
|
|
614
|
-
//
|
|
702
|
+
// Candidate -> canonical conversion. Artifact-first inputs must keep using
|
|
703
|
+
// the already hash-verified payload: re-reading the selected node record
|
|
704
|
+
// here would silently fall back to its prose and defeat the integrity gate.
|
|
705
|
+
// Historical records intentionally retain their node-text compatibility path.
|
|
615
706
|
let analysis;
|
|
616
707
|
try {
|
|
708
|
+
const rawContractText = effectiveContractText ?? (planInput.artifactFirst ? planText : undefined);
|
|
617
709
|
analysis = await analyzeFrontendImplementationContract({
|
|
618
710
|
runDir: input.runDir,
|
|
619
|
-
fromNodeId:
|
|
620
|
-
rawContractText
|
|
711
|
+
fromNodeId: rawContractText ? undefined : planNodeId,
|
|
712
|
+
rawContractText,
|
|
621
713
|
sourceBinding: input.sourceBinding,
|
|
622
714
|
});
|
|
623
715
|
}
|
|
@@ -637,21 +729,6 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
637
729
|
}
|
|
638
730
|
throw error;
|
|
639
731
|
}
|
|
640
|
-
// Size guard: a canonical contract above 64KB is a retryable-invalid defect
|
|
641
|
-
// (compress the contract) rather than a writer authorization failure.
|
|
642
|
-
const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
|
|
643
|
-
if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
|
|
644
|
-
return finalizePrewrite(input, {
|
|
645
|
-
...basePending,
|
|
646
|
-
verdict,
|
|
647
|
-
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
648
|
-
normalizationActions: [],
|
|
649
|
-
mockStrategy: analysis.canonical.mockApi.strategy,
|
|
650
|
-
classification: "retryable-invalid",
|
|
651
|
-
failureReason: `frontend prewrite gate: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit. 压缩契约(移除冗余 prose、重复目标或过长描述)后重试。`,
|
|
652
|
-
failureCode: "contract-too-large",
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
732
|
const mockStrategy = analysis.canonical.mockApi.strategy;
|
|
656
733
|
if (!input.config.allowedMockStrategies.includes(mockStrategy)) {
|
|
657
734
|
const quotedAllowed = input.config.allowedMockStrategies
|
|
@@ -762,7 +839,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
762
839
|
repoRoot: candidateRoot,
|
|
763
840
|
});
|
|
764
841
|
if (candidatePaths.length > 0) {
|
|
765
|
-
const parsed = parseOpenspecCitations(
|
|
842
|
+
const parsed = parseOpenspecCitations(citationsText, candidateRoot);
|
|
766
843
|
if (!parsed.blockPresent || parsed.unparseable) {
|
|
767
844
|
return finalizePrewrite(input, {
|
|
768
845
|
...basePending,
|
|
@@ -903,7 +980,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
903
980
|
});
|
|
904
981
|
}
|
|
905
982
|
if (openspecPolicy === "cited") {
|
|
906
|
-
const parsedCitations = parseOpenspecCitations(
|
|
983
|
+
const parsedCitations = parseOpenspecCitations(citationsText, candidateRoot);
|
|
907
984
|
const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
|
|
908
985
|
const readSet = new Set(openspecReadPaths);
|
|
909
986
|
const uncorroborated = [
|