@tea-agent/loop-agent 0.38.2 → 0.39.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +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 +92 -14
- package/dist/workflows/dag/frontend-prewrite-gate.js +106 -11
- package/dist/workflows/dag/init-hybrid.js +44 -11
- package/dist/workflows/dag/node-execution.js +26 -0
- 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({
|
|
@@ -41,6 +41,9 @@ export function serializeDeterministicJson(value) {
|
|
|
41
41
|
export function deterministicSha256(value) {
|
|
42
42
|
return sha256Hex(serializeDeterministicJson(value));
|
|
43
43
|
}
|
|
44
|
+
/** Provider-safe limits for the two planner outputs. Larger work must split. */
|
|
45
|
+
export const FRONTEND_PLAN_RESPONSE_MAX_BYTES = 14 * 1024;
|
|
46
|
+
export const FRONTEND_CANONICAL_CONTRACT_MAX_BYTES = 16 * 1024;
|
|
44
47
|
/** A contract materialization failure carries its classification plus the
|
|
45
48
|
* audit context collected up to the failure point, so the prewrite gate can
|
|
46
49
|
* persist a complete frontend-prewrite-result-v1 even when the canonical
|
|
@@ -1722,9 +1725,37 @@ function extractSingleOpenspecCitationsBlock(text) {
|
|
|
1722
1725
|
}
|
|
1723
1726
|
return blocks[0];
|
|
1724
1727
|
}
|
|
1728
|
+
/**
|
|
1729
|
+
* Frontend plan/revision output has two JSON-bearing regions: the primary
|
|
1730
|
+
* payload and line-oriented OpenSpec citations. Keep their parsing domains
|
|
1731
|
+
* separate so a citation row can never become a competing contract candidate.
|
|
1732
|
+
* A bare JSON payload is retained only for historical node-output compatibility.
|
|
1733
|
+
*/
|
|
1734
|
+
export function extractFrontendPlanPayload(text) {
|
|
1735
|
+
const citationsBlock = extractSingleOpenspecCitationsBlock(text);
|
|
1736
|
+
return {
|
|
1737
|
+
payload: extractFrontendPlanPrimaryPayload(text.replace(citationsBlock, "")),
|
|
1738
|
+
citationsBlock,
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
function extractFrontendPlanPrimaryPayload(text) {
|
|
1742
|
+
const fences = [...text.matchAll(/```json[ \t]*\r?\n([\s\S]*?)\r?\n```/gi)];
|
|
1743
|
+
if (fences.length > 1) {
|
|
1744
|
+
throw new Error(`frontend plan output must contain exactly one primary fenced json payload (found ${fences.length})`);
|
|
1745
|
+
}
|
|
1746
|
+
if (fences.length === 1) {
|
|
1747
|
+
try {
|
|
1748
|
+
return JSON.parse(fences[0][1]);
|
|
1749
|
+
}
|
|
1750
|
+
catch (error) {
|
|
1751
|
+
throw new Error(`frontend plan primary json fence is invalid: ${error instanceof Error ? error.message : String(error)}`);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
return extractFrontendImplementationJson(text);
|
|
1755
|
+
}
|
|
1725
1756
|
async function writeFrontendPlanCandidateRaw(input) {
|
|
1726
1757
|
const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
|
|
1727
|
-
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text
|
|
1758
|
+
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text);
|
|
1728
1759
|
return relativePath;
|
|
1729
1760
|
}
|
|
1730
1761
|
/**
|
|
@@ -1765,18 +1796,23 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1765
1796
|
});
|
|
1766
1797
|
};
|
|
1767
1798
|
try {
|
|
1799
|
+
if (Buffer.byteLength(input.text, "utf8") > FRONTEND_PLAN_RESPONSE_MAX_BYTES) {
|
|
1800
|
+
throw new Error(`frontend plan response exceeds the ${FRONTEND_PLAN_RESPONSE_MAX_BYTES}-byte limit`);
|
|
1801
|
+
}
|
|
1768
1802
|
if (!input.sourceBinding)
|
|
1769
1803
|
throw new Error("frontend plan patch validator requires DAG sourceBinding");
|
|
1770
1804
|
const skeleton = input.structuredContractOutput?.skeleton;
|
|
1771
1805
|
if (!skeleton)
|
|
1772
1806
|
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
1773
|
-
const patch =
|
|
1807
|
+
const patch = extractFrontendPlanPrimaryPayload(input.text);
|
|
1774
1808
|
if (!isPlainObject(patch))
|
|
1775
1809
|
throw new Error("frontend plan patch must extract to one JSON object");
|
|
1776
1810
|
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
1777
1811
|
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
1778
1812
|
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1779
1813
|
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1814
|
+
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1815
|
+
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1780
1816
|
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1781
1817
|
if (protectedViolations.length > 0)
|
|
1782
1818
|
throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
@@ -1790,6 +1826,10 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1790
1826
|
rawContractText: serializeDeterministicJson(merged),
|
|
1791
1827
|
sourceBinding: input.sourceBinding,
|
|
1792
1828
|
});
|
|
1829
|
+
if (Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8") >
|
|
1830
|
+
FRONTEND_CANONICAL_CONTRACT_MAX_BYTES) {
|
|
1831
|
+
throw new Error(`frontend canonical contract exceeds the ${FRONTEND_CANONICAL_CONTRACT_MAX_BYTES}-byte limit; split the task`);
|
|
1832
|
+
}
|
|
1793
1833
|
const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
1794
1834
|
await writeReport({
|
|
1795
1835
|
classification: "accepted-normalized",
|
|
@@ -1807,6 +1847,19 @@ export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
|
1807
1847
|
return {
|
|
1808
1848
|
ok: true,
|
|
1809
1849
|
contract: analysis.canonical,
|
|
1850
|
+
structuredArtifact: {
|
|
1851
|
+
path: normalizedArtifact.path,
|
|
1852
|
+
sha256: normalizedArtifact.sha256,
|
|
1853
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1854
|
+
},
|
|
1855
|
+
frontendPlanOutput: {
|
|
1856
|
+
schemaVersion: 1,
|
|
1857
|
+
payloadKind: "canonical-contract",
|
|
1858
|
+
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1859
|
+
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1860
|
+
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1861
|
+
rawCandidateSha256: sha256Hex(input.text),
|
|
1862
|
+
},
|
|
1810
1863
|
normalizedText: [
|
|
1811
1864
|
"Frontend implementation contract compiled from the runtime skeleton.",
|
|
1812
1865
|
"",
|
|
@@ -1862,27 +1915,32 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1862
1915
|
* the prewrite gate, which remains the only authority.
|
|
1863
1916
|
*/
|
|
1864
1917
|
export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1918
|
+
const nodeId = input.nodeId ?? "frontend-plan-revision-pi";
|
|
1919
|
+
const attempt = input.attempt ?? 1;
|
|
1920
|
+
const candidateDir = path.posix.join("contracts", "candidates", nodeId);
|
|
1921
|
+
const rawPath = await writeFrontendPlanCandidateRaw({
|
|
1922
|
+
runDir: input.runDir,
|
|
1923
|
+
nodeId,
|
|
1924
|
+
attempt,
|
|
1925
|
+
text: input.text,
|
|
1926
|
+
});
|
|
1927
|
+
if (Buffer.byteLength(input.text, "utf8") > FRONTEND_PLAN_RESPONSE_MAX_BYTES) {
|
|
1870
1928
|
return {
|
|
1871
1929
|
ok: false,
|
|
1872
|
-
reason:
|
|
1930
|
+
reason: `frontend revision response exceeds the ${FRONTEND_PLAN_RESPONSE_MAX_BYTES}-byte limit`,
|
|
1873
1931
|
};
|
|
1874
1932
|
}
|
|
1875
|
-
|
|
1876
|
-
// would otherwise count as competing balanced objects in the extractor.
|
|
1877
|
-
const textWithoutCitations = input.text.replace(citationsBlock, "");
|
|
1933
|
+
let citationsBlock;
|
|
1878
1934
|
let patch;
|
|
1879
1935
|
try {
|
|
1880
|
-
|
|
1936
|
+
const parsed = extractFrontendPlanPayload(input.text);
|
|
1937
|
+
citationsBlock = parsed.citationsBlock;
|
|
1938
|
+
patch = parsed.payload;
|
|
1881
1939
|
}
|
|
1882
1940
|
catch (error) {
|
|
1883
1941
|
return {
|
|
1884
1942
|
ok: false,
|
|
1885
|
-
reason:
|
|
1943
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1886
1944
|
};
|
|
1887
1945
|
}
|
|
1888
1946
|
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
@@ -1898,7 +1956,27 @@ export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
|
1898
1956
|
reason: `frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`,
|
|
1899
1957
|
};
|
|
1900
1958
|
}
|
|
1901
|
-
|
|
1959
|
+
const payloadPath = path.posix.join(candidateDir, `attempt-${attempt}.patch.json`);
|
|
1960
|
+
const payloadArtifact = await writeDeterministicJsonArtifact(input.runDir, payloadPath, patch);
|
|
1961
|
+
const citationsArtifactPath = path.posix.join(candidateDir, `attempt-${attempt}.openspec-citations.md`);
|
|
1962
|
+
await writeTextArtifactFile(path.join(input.runDir, citationsArtifactPath), `${citationsBlock}\n`);
|
|
1963
|
+
return {
|
|
1964
|
+
ok: true,
|
|
1965
|
+
contract: patch,
|
|
1966
|
+
structuredArtifact: {
|
|
1967
|
+
path: payloadArtifact.path,
|
|
1968
|
+
sha256: payloadArtifact.sha256,
|
|
1969
|
+
schemaId: "frontend-implementation-contract-revision-patch-v1",
|
|
1970
|
+
},
|
|
1971
|
+
frontendPlanOutput: {
|
|
1972
|
+
schemaVersion: 1,
|
|
1973
|
+
payloadKind: "merge-patch",
|
|
1974
|
+
citationsArtifactPath: path.join(input.runDir, citationsArtifactPath),
|
|
1975
|
+
citationsArtifactSha256: sha256Hex(`${citationsBlock}\n`),
|
|
1976
|
+
rawCandidatePath: path.join(input.runDir, rawPath),
|
|
1977
|
+
rawCandidateSha256: sha256Hex(input.text),
|
|
1978
|
+
},
|
|
1979
|
+
};
|
|
1902
1980
|
}
|
|
1903
1981
|
export async function materializeFrontendImplementationContract(input) {
|
|
1904
1982
|
const analysis = await analyzeFrontendImplementationContract(input);
|
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
5
|
import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
|
|
6
6
|
import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
|
|
7
|
-
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, applyFrontendImplementationContractPatch, extractFrontendImplementationJson, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
7
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, FRONTEND_CANONICAL_CONTRACT_MAX_BYTES, applyFrontendImplementationContractPatch, extractFrontendImplementationJson, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
8
8
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
9
9
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
10
10
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
@@ -12,7 +12,7 @@ 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
14
|
/** Canonical contract serialization must stay at or below this byte size. */
|
|
15
|
-
export const FRONTEND_CONTRACT_MAX_BYTES =
|
|
15
|
+
export const FRONTEND_CONTRACT_MAX_BYTES = FRONTEND_CANONICAL_CONTRACT_MAX_BYTES;
|
|
16
16
|
/**
|
|
17
17
|
* Machine-readable failure classification for blocked/retryable-invalid
|
|
18
18
|
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
@@ -176,6 +176,82 @@ async function readNodeRawText(runDir, nodeId) {
|
|
|
176
176
|
const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
177
177
|
return record.assistantText ?? record.stdout ?? "";
|
|
178
178
|
}
|
|
179
|
+
function resolveRunOwnedArtifact(runDir, candidate, label) {
|
|
180
|
+
const root = path.resolve(runDir);
|
|
181
|
+
const resolved = path.resolve(candidate);
|
|
182
|
+
const relative = path.relative(root, resolved);
|
|
183
|
+
if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
184
|
+
throw new Error(`frontend prewrite gate ${label} must be contained by the current run directory`);
|
|
185
|
+
}
|
|
186
|
+
return resolved;
|
|
187
|
+
}
|
|
188
|
+
async function readHashBoundArtifact(input) {
|
|
189
|
+
const resolved = resolveRunOwnedArtifact(input.runDir, input.artifactPath, input.label);
|
|
190
|
+
const text = await readFile(resolved, "utf8");
|
|
191
|
+
const actual = sha256Hex(text);
|
|
192
|
+
if (actual !== input.expectedSha256) {
|
|
193
|
+
throw new Error(`frontend prewrite gate ${input.label} SHA-256 mismatch (expected ${input.expectedSha256}, got ${actual})`);
|
|
194
|
+
}
|
|
195
|
+
return text;
|
|
196
|
+
}
|
|
197
|
+
async function readFrontendPlanNodeInput(runDir, nodeId) {
|
|
198
|
+
const record = JSON.parse(await readFile(path.join(runDir, `${nodeId}.json`), "utf8"));
|
|
199
|
+
if (record.status !== "FINISHED") {
|
|
200
|
+
throw new Error(`frontend prewrite gate requires FINISHED node ${nodeId} (got ${String(record.status)})`);
|
|
201
|
+
}
|
|
202
|
+
const frontend = record.frontendPlanOutput;
|
|
203
|
+
if (!frontend) {
|
|
204
|
+
const payloadText = record.assistantText?.trim() || record.stdout?.trim() || "";
|
|
205
|
+
if (!payloadText)
|
|
206
|
+
throw new Error(`frontend prewrite gate empty output from ${nodeId}`);
|
|
207
|
+
return {
|
|
208
|
+
artifactFirst: false,
|
|
209
|
+
payloadText,
|
|
210
|
+
citationsText: payloadText,
|
|
211
|
+
rawCandidateSha256: sha256Hex(record.assistantText ?? record.stdout ?? ""),
|
|
212
|
+
payloadKind: "canonical-contract",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (frontend.schemaVersion !== 1 ||
|
|
216
|
+
!record.structuredArtifactPath ||
|
|
217
|
+
!record.structuredArtifactSha256 ||
|
|
218
|
+
!record.structuredArtifactSchemaId) {
|
|
219
|
+
throw new Error(`frontend prewrite gate ${nodeId} has incomplete artifact-first metadata`);
|
|
220
|
+
}
|
|
221
|
+
const expectedSchema = frontend.payloadKind === "canonical-contract"
|
|
222
|
+
? "frontend-implementation-contract-v1"
|
|
223
|
+
: "frontend-implementation-contract-revision-patch-v1";
|
|
224
|
+
if (record.structuredArtifactSchemaId !== expectedSchema) {
|
|
225
|
+
throw new Error(`frontend prewrite gate ${nodeId} artifact schema ${record.structuredArtifactSchemaId} does not match ${frontend.payloadKind}`);
|
|
226
|
+
}
|
|
227
|
+
const [payloadText, citationsText] = await Promise.all([
|
|
228
|
+
readHashBoundArtifact({
|
|
229
|
+
runDir,
|
|
230
|
+
artifactPath: record.structuredArtifactPath,
|
|
231
|
+
expectedSha256: record.structuredArtifactSha256,
|
|
232
|
+
label: `${nodeId} primary payload`,
|
|
233
|
+
}),
|
|
234
|
+
readHashBoundArtifact({
|
|
235
|
+
runDir,
|
|
236
|
+
artifactPath: frontend.citationsArtifactPath,
|
|
237
|
+
expectedSha256: frontend.citationsArtifactSha256,
|
|
238
|
+
label: `${nodeId} citations`,
|
|
239
|
+
}),
|
|
240
|
+
readHashBoundArtifact({
|
|
241
|
+
runDir,
|
|
242
|
+
artifactPath: frontend.rawCandidatePath,
|
|
243
|
+
expectedSha256: frontend.rawCandidateSha256,
|
|
244
|
+
label: `${nodeId} raw candidate`,
|
|
245
|
+
}).then(() => undefined),
|
|
246
|
+
]);
|
|
247
|
+
return {
|
|
248
|
+
artifactFirst: true,
|
|
249
|
+
payloadText,
|
|
250
|
+
citationsText,
|
|
251
|
+
rawCandidateSha256: frontend.rawCandidateSha256,
|
|
252
|
+
payloadKind: frontend.payloadKind,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
179
255
|
/**
|
|
180
256
|
* Same shape predicate as the internal `isContract` inside
|
|
181
257
|
* `extractFrontendImplementationJson` (frontend-implementation-contract.ts):
|
|
@@ -458,9 +534,9 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
458
534
|
openspecCandidatePaths: input.config.openspecCandidatePaths ?? [],
|
|
459
535
|
openspecPolicy,
|
|
460
536
|
};
|
|
461
|
-
let
|
|
537
|
+
let planInput;
|
|
462
538
|
try {
|
|
463
|
-
|
|
539
|
+
planInput = await readFrontendPlanNodeInput(input.runDir, planNodeId);
|
|
464
540
|
}
|
|
465
541
|
catch (error) {
|
|
466
542
|
return finalizePrewrite(input, {
|
|
@@ -470,6 +546,9 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
470
546
|
failureCode: "plan-text-unreadable",
|
|
471
547
|
});
|
|
472
548
|
}
|
|
549
|
+
const planText = planInput.payloadText;
|
|
550
|
+
const citationsText = planInput.citationsText;
|
|
551
|
+
basePending.candidateRawSha256 = planInput.rawCandidateSha256;
|
|
473
552
|
let reviewText;
|
|
474
553
|
try {
|
|
475
554
|
reviewText = await readNodeText(input.runDir, reviewNodeId);
|
|
@@ -502,7 +581,20 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
502
581
|
if (revisionPatchMode) {
|
|
503
582
|
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
504
583
|
try {
|
|
505
|
-
|
|
584
|
+
const originalInput = await readFrontendPlanNodeInput(input.runDir, originalNodeId);
|
|
585
|
+
if (planInput.artifactFirst && !originalInput.artifactFirst) {
|
|
586
|
+
throw new Error(`artifact-first revision ${planNodeId} requires artifact-first original contract ${originalNodeId}`);
|
|
587
|
+
}
|
|
588
|
+
if (planInput.artifactFirst) {
|
|
589
|
+
if (planInput.payloadKind !== "merge-patch" ||
|
|
590
|
+
originalInput.payloadKind !== "canonical-contract") {
|
|
591
|
+
throw new Error("artifact-first revision/original payload kinds are invalid");
|
|
592
|
+
}
|
|
593
|
+
effectiveContractText = serializeDeterministicJson(applyFrontendImplementationContractPatch(JSON.parse(originalInput.payloadText), JSON.parse(planInput.payloadText)));
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
effectiveContractText = await mergeRevisionPatch(input.runDir, planText, originalNodeId);
|
|
597
|
+
}
|
|
506
598
|
}
|
|
507
599
|
catch (error) {
|
|
508
600
|
return finalizePrewrite(input, {
|
|
@@ -610,14 +702,17 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
610
702
|
workspaceRoot,
|
|
611
703
|
});
|
|
612
704
|
}
|
|
613
|
-
// Candidate -> canonical conversion.
|
|
614
|
-
//
|
|
705
|
+
// Candidate -> canonical conversion. Artifact-first inputs must keep using
|
|
706
|
+
// the already hash-verified payload: re-reading the selected node record
|
|
707
|
+
// here would silently fall back to its prose and defeat the integrity gate.
|
|
708
|
+
// Historical records intentionally retain their node-text compatibility path.
|
|
615
709
|
let analysis;
|
|
616
710
|
try {
|
|
711
|
+
const rawContractText = effectiveContractText ?? (planInput.artifactFirst ? planText : undefined);
|
|
617
712
|
analysis = await analyzeFrontendImplementationContract({
|
|
618
713
|
runDir: input.runDir,
|
|
619
|
-
fromNodeId:
|
|
620
|
-
rawContractText
|
|
714
|
+
fromNodeId: rawContractText ? undefined : planNodeId,
|
|
715
|
+
rawContractText,
|
|
621
716
|
sourceBinding: input.sourceBinding,
|
|
622
717
|
});
|
|
623
718
|
}
|
|
@@ -762,7 +857,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
762
857
|
repoRoot: candidateRoot,
|
|
763
858
|
});
|
|
764
859
|
if (candidatePaths.length > 0) {
|
|
765
|
-
const parsed = parseOpenspecCitations(
|
|
860
|
+
const parsed = parseOpenspecCitations(citationsText, candidateRoot);
|
|
766
861
|
if (!parsed.blockPresent || parsed.unparseable) {
|
|
767
862
|
return finalizePrewrite(input, {
|
|
768
863
|
...basePending,
|
|
@@ -903,7 +998,7 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
903
998
|
});
|
|
904
999
|
}
|
|
905
1000
|
if (openspecPolicy === "cited") {
|
|
906
|
-
const parsedCitations = parseOpenspecCitations(
|
|
1001
|
+
const parsedCitations = parseOpenspecCitations(citationsText, candidateRoot);
|
|
907
1002
|
const citedPaths = new Set(parsedCitations.citations.map((citation) => citation.path));
|
|
908
1003
|
const readSet = new Set(openspecReadPaths);
|
|
909
1004
|
const uncorroborated = [
|