@tea-agent/loop-agent 0.39.0-next.4 → 0.39.0-next.5
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 +7 -0
- package/dist/build-stamp.json +2 -2
- package/dist/workflows/dag/frontend-implementation-contract.js +48 -0
- package/dist/workflows/dag/frontend-plan-render.js +73 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +97 -3
- package/dist/workflows/dag/init-hybrid.js +35 -11
- package/dist/workflows/dag/types.js +16 -0
- package/docs/templates/frontend-implementation-contract.schema.json +6 -1
- package/package.json +1 -1
- package/skills/frontend-implementation/references/node-contracts.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
### 新增
|
|
6
6
|
|
|
7
|
+
- 前端 plan 唯一机器契约化(JSON-only):`frontend-plan-pi` / `frontend-plan-revision-pi` 输出收敛为「一行导语 + 唯一 fenced `json` 契约 + 唯一 `openspec-citations` 引用块」,彻底移除「可选 Markdown 说明」;plan.md(`contracts/frontend-plan.md`)改由 prewrite gate 用纯函数 `renderFrontendPlanMarkdown` 确定性渲染(不新增 DAG 节点/不新增 Pi 调用)
|
|
8
|
+
- 契约 schema 新增 5 个可选字段(`implementationSteps` / `stylingStrategy` / `dependencyPolicy` / `residualRisks` / `realIntegrationGap`),`schemaVersion` 保持 1;`docs/templates/frontend-implementation-contract.schema.json` 同步
|
|
9
|
+
- revision 输出改为 RFC 7386 merge-patch delta + 引用块:gate 配置新增 `revisionPatch`,生效时在 `planFallbackFromNodeIds[0]` 原始契约上应用 `applyFrontendImplementationContractPatch` 后进入既有分析/规范化/物化管线;旧 DAG 缺字段按整契约处理
|
|
10
|
+
- canonical contract 序列化(UTF-8)超过 64KB → `retryable-invalid` + 新增 `contract-too-large` failureCode(体积守卫兜底)
|
|
11
|
+
- `design-review` / `final-design-review` 提示改为消费契约 JSON(注入契约字段摘要),不再要求读取 plan prose
|
|
12
|
+
|
|
7
13
|
- `frontend-prewrite-gate` 的 OpenSpec 读取校验支持 `task.json.frontendOpenspec` 配置:`policy` 缺省 `cited`(候选 = `requiredReadPaths` ∪ 任务源显式引用的 openspec 路径),`scan-strict` 保留止血任务后的全量必读语义;`requiredReadPaths` 中非 openspec 支持路径在生成期确定性失败
|
|
8
14
|
- 确定性引用块协议:生效 plan 输出在 fenced `json` 契约块后追加 `openspec-citations` fenced 块(每行一个 JSON `{"path","section","line"}`);gate 解析并与生效 plan/review 节点的成功 read 事件核验,新增 `openspec-citation-block-unparseable` / `openspec-not-cited` / `openspec-citation-not-read` 三个 retryable-invalid failureCode(防捏造、防遗漏)
|
|
9
15
|
- `src/shared/openspec-spec.ts` 新增 `extractTaskSourceOpenspecPaths` 纯函数(反引号内联、markdown 链接目标、裸路径 token → 归一化 → spec 过滤 → 治理子树排除 → 去重排序)
|
|
@@ -11,6 +17,7 @@
|
|
|
11
17
|
|
|
12
18
|
### 文档
|
|
13
19
|
|
|
20
|
+
- 新增 `docs/decisions/0015-frontend-plan-json-only-contract.md`,并同步 `docs/runtime/frontend-implementation-workflow.md`、`docs/templates/frontend-implementation-contract.schema.json`、`skills/frontend-implementation/references/node-contracts.md`
|
|
14
21
|
- 新增 `docs/decisions/0014-openspec-gate-claim-verification.md`,并同步 `docs/runtime/frontend-implementation-workflow.md`、`docs/templates/frontend-task-constraints.md`、`docs/templates/frontend-design-contract.md`、`skills/frontend-implementation/references/node-contracts.md`
|
|
15
22
|
|
|
16
23
|
## [0.36.4-beta.0] - 2026-08-16
|
package/dist/build-stamp.json
CHANGED
|
@@ -316,6 +316,11 @@ export const frontendImplementationContractSchema = z
|
|
|
316
316
|
})
|
|
317
317
|
.strict()).min(1),
|
|
318
318
|
evidenceGaps: z.array(gap),
|
|
319
|
+
implementationSteps: z.array(z.string().min(1)).optional(),
|
|
320
|
+
stylingStrategy: z.string().min(1).optional(),
|
|
321
|
+
dependencyPolicy: z.string().min(1).optional(),
|
|
322
|
+
residualRisks: z.array(z.string().min(1)).optional(),
|
|
323
|
+
realIntegrationGap: z.string().min(1).optional(),
|
|
319
324
|
})
|
|
320
325
|
.strict()
|
|
321
326
|
.superRefine((value, ctx) => {
|
|
@@ -426,6 +431,44 @@ export const frontendImplementationContractSchema = z
|
|
|
426
431
|
});
|
|
427
432
|
}
|
|
428
433
|
}));
|
|
434
|
+
function isPlainObject(value) {
|
|
435
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Apply an RFC 7386 merge-patch on a target contract without mutating it.
|
|
439
|
+
* `null` removes the key; plain objects merge recursively when the target value
|
|
440
|
+
* is also a plain object; arrays and scalars replace the whole value. The
|
|
441
|
+
* result is re-validated by analyzeFrontendImplementationContract before it is
|
|
442
|
+
* ever materialized, so a patch that removes a required field still fails
|
|
443
|
+
* closed at the gate.
|
|
444
|
+
*/
|
|
445
|
+
export function applyFrontendImplementationContractPatch(target, patch) {
|
|
446
|
+
if (!isPlainObject(patch)) {
|
|
447
|
+
throw new Error("frontend revision patch must be a JSON object");
|
|
448
|
+
}
|
|
449
|
+
const merge = (base, delta) => {
|
|
450
|
+
if (delta === null)
|
|
451
|
+
return undefined;
|
|
452
|
+
if (!isPlainObject(delta))
|
|
453
|
+
return delta;
|
|
454
|
+
const result = isPlainObject(base)
|
|
455
|
+
? { ...base }
|
|
456
|
+
: {};
|
|
457
|
+
for (const [key, value] of Object.entries(delta)) {
|
|
458
|
+
if (value === null) {
|
|
459
|
+
delete result[key];
|
|
460
|
+
}
|
|
461
|
+
else if (isPlainObject(value) && isPlainObject(result[key])) {
|
|
462
|
+
result[key] = merge(result[key], value);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
result[key] = value;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return result;
|
|
469
|
+
};
|
|
470
|
+
return merge(target, patch);
|
|
471
|
+
}
|
|
429
472
|
export async function assertFrontendSourceBindingFresh(input) {
|
|
430
473
|
for (const source of input.binding.sources) {
|
|
431
474
|
const absolute = resolveDagTaskSourcePath({
|
|
@@ -1331,6 +1374,11 @@ export function coerceFrontendImplementationContractInput(value, canonicalBindin
|
|
|
1331
1374
|
},
|
|
1332
1375
|
verificationTargets,
|
|
1333
1376
|
evidenceGaps,
|
|
1377
|
+
implementationSteps: asStringArray(record.implementationSteps),
|
|
1378
|
+
stylingStrategy: asString(record.stylingStrategy) || undefined,
|
|
1379
|
+
dependencyPolicy: asString(record.dependencyPolicy) || undefined,
|
|
1380
|
+
residualRisks: asStringArray(record.residualRisks),
|
|
1381
|
+
realIntegrationGap: asString(record.realIntegrationGap) || undefined,
|
|
1334
1382
|
};
|
|
1335
1383
|
}
|
|
1336
1384
|
/**
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic Markdown renderer for a canonical frontend implementation
|
|
3
|
+
* contract. The prewrite gate materializes this as `contracts/frontend-plan.md`
|
|
4
|
+
* next to the contract JSON, so plan prose is never produced by the model — it
|
|
5
|
+
* is derived from the single machine contract at materialization time.
|
|
6
|
+
*
|
|
7
|
+
* The function is pure: the same contract always renders byte-identical
|
|
8
|
+
* Markdown, and absent optional fields render as placeholders without throwing.
|
|
9
|
+
*/
|
|
10
|
+
function bulletList(items) {
|
|
11
|
+
if (!items || items.length === 0)
|
|
12
|
+
return "";
|
|
13
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
14
|
+
}
|
|
15
|
+
function requirementsCoverage(contract) {
|
|
16
|
+
const lines = contract.requirements.map((requirement) => `- ${requirement.id}: ${requirement.expectedOutcome}`);
|
|
17
|
+
return lines.length > 0 ? lines.join("\n") : "_(no requirements)_";
|
|
18
|
+
}
|
|
19
|
+
function uiStateHandling(contract) {
|
|
20
|
+
const lines = contract.uiStates.map((state) => {
|
|
21
|
+
if (state.applicable) {
|
|
22
|
+
return `- ${state.name}: ${state.expectedBehavior ?? ""}`;
|
|
23
|
+
}
|
|
24
|
+
return `- ${state.name}: not applicable — ${state.notApplicableReason ?? ""}`;
|
|
25
|
+
});
|
|
26
|
+
return lines.length > 0 ? lines.join("\n") : "_(none)_";
|
|
27
|
+
}
|
|
28
|
+
function interactionNotes(contract) {
|
|
29
|
+
const lines = contract.interactions.map((interaction) => `- ${interaction.name}: ${interaction.trigger} → ${interaction.expectedBehavior}`);
|
|
30
|
+
return lines.length > 0 ? lines.join("\n") : "_(none)_";
|
|
31
|
+
}
|
|
32
|
+
function verificationPlan(contract) {
|
|
33
|
+
const lines = contract.verificationTargets.map((target) => `- ${target.id}: ${target.commandLabel} (${target.file})`);
|
|
34
|
+
return lines.length > 0 ? lines.join("\n") : "_(none)_";
|
|
35
|
+
}
|
|
36
|
+
export function renderFrontendPlanMarkdown(contract) {
|
|
37
|
+
return [
|
|
38
|
+
"# Frontend Implementation Plan",
|
|
39
|
+
"",
|
|
40
|
+
"## Requirement Coverage",
|
|
41
|
+
requirementsCoverage(contract),
|
|
42
|
+
"",
|
|
43
|
+
"## Implementation Steps",
|
|
44
|
+
bulletList(contract.implementationSteps) || "_(not specified)_",
|
|
45
|
+
"",
|
|
46
|
+
"## Target Files",
|
|
47
|
+
bulletList(contract.targets.files) || "_(none)_",
|
|
48
|
+
"",
|
|
49
|
+
"## UI State Handling",
|
|
50
|
+
uiStateHandling(contract),
|
|
51
|
+
"",
|
|
52
|
+
"## Styling & Component Strategy",
|
|
53
|
+
contract.stylingStrategy || "_(not specified)_",
|
|
54
|
+
"",
|
|
55
|
+
"## Interaction Notes",
|
|
56
|
+
interactionNotes(contract),
|
|
57
|
+
"",
|
|
58
|
+
"## Mock & API Strategy",
|
|
59
|
+
contract.mockApi.strategy,
|
|
60
|
+
"",
|
|
61
|
+
"## Dependency Policy",
|
|
62
|
+
contract.dependencyPolicy || "_(not specified)_",
|
|
63
|
+
"",
|
|
64
|
+
"## Verification Plan",
|
|
65
|
+
verificationPlan(contract),
|
|
66
|
+
"",
|
|
67
|
+
"## Real Integration Gap",
|
|
68
|
+
contract.realIntegrationGap || "_(not specified)_",
|
|
69
|
+
"",
|
|
70
|
+
"## Residual Risks",
|
|
71
|
+
bulletList(contract.residualRisks) || "_(none)_",
|
|
72
|
+
].join("\n") + "\n";
|
|
73
|
+
}
|
|
@@ -2,12 +2,16 @@ import { readFile, stat } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { isOpenspecSpecFilePath } from "../../shared/openspec-spec.js";
|
|
5
|
+
import { writeTextArtifactFile } from "../../infrastructure/harness/artifact-store.js";
|
|
5
6
|
import { countExcludedGovernanceSpecFiles, resolveOpenspecGovernanceRoot, } from "../../task/frontend-project-capability.js";
|
|
6
|
-
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
7
|
+
import { analyzeFrontendImplementationContract, writeFrontendImplementationContractArtifact, writeDeterministicJsonArtifact, assertFrontendSourceBindingFresh, deterministicSha256, sha256Hex, serializeDeterministicJson, applyFrontendImplementationContractPatch, extractFrontendImplementationJson, FrontendContractFailure, frontendNormalizationActionSchema, } from "./frontend-implementation-contract.js";
|
|
8
|
+
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
7
9
|
import { captureFrontendWorktreeBaseline } from "./frontend-worktree-diff.js";
|
|
8
10
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
9
11
|
export const FRONTEND_PREWRITE_RESULT_SCHEMA_ID = "frontend-prewrite-result-v1";
|
|
10
12
|
export const FRONTEND_PREWRITE_RESULT_ARTIFACT_NAME = "frontend-prewrite-result.json";
|
|
13
|
+
/** Canonical contract serialization must stay at or below this byte size. */
|
|
14
|
+
export const FRONTEND_CONTRACT_MAX_BYTES = 65536;
|
|
11
15
|
/**
|
|
12
16
|
* Machine-readable failure classification for blocked/retryable-invalid
|
|
13
17
|
* prewrite outcomes. Kept alongside the human-readable failureReason so
|
|
@@ -31,6 +35,7 @@ export const frontendPrewriteFailureCodeSchema = z.enum([
|
|
|
31
35
|
"openspec-citation-block-unparseable",
|
|
32
36
|
"openspec-not-cited",
|
|
33
37
|
"openspec-citation-not-read",
|
|
38
|
+
"contract-too-large",
|
|
34
39
|
]);
|
|
35
40
|
export const frontendPrewriteResultV1Schema = z
|
|
36
41
|
.object({
|
|
@@ -442,7 +447,60 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
442
447
|
failureCode: "verdict-not-pass",
|
|
443
448
|
});
|
|
444
449
|
}
|
|
445
|
-
|
|
450
|
+
// revisionPatch mode: the effective plan node emits an RFC 7386 merge-patch
|
|
451
|
+
// delta against the original contract node (planFallbackFromNodeIds[0]).
|
|
452
|
+
// Legacy DAGs without revisionPatch (undefined) keep full-contract semantics.
|
|
453
|
+
const revisionPatchMode = input.config.revisionPatch === true &&
|
|
454
|
+
planNodeId === input.config.planFromNodeId &&
|
|
455
|
+
input.config.planFallbackFromNodeIds.length > 0;
|
|
456
|
+
let effectiveContractText = null;
|
|
457
|
+
if (revisionPatchMode) {
|
|
458
|
+
const originalNodeId = input.config.planFallbackFromNodeIds[0];
|
|
459
|
+
let originalText;
|
|
460
|
+
try {
|
|
461
|
+
originalText = await readNodeText(input.runDir, originalNodeId);
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
return finalizePrewrite(input, {
|
|
465
|
+
...basePending,
|
|
466
|
+
verdict,
|
|
467
|
+
classification: "retryable-invalid",
|
|
468
|
+
failureReason: `frontend prewrite gate revisionPatch mode requires original contract node ${originalNodeId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
469
|
+
failureCode: "contract-invalid",
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
let originalValue;
|
|
473
|
+
let patchValue;
|
|
474
|
+
try {
|
|
475
|
+
originalValue = extractFrontendImplementationJson(originalText);
|
|
476
|
+
patchValue = extractFrontendImplementationJson(planText);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
return finalizePrewrite(input, {
|
|
480
|
+
...basePending,
|
|
481
|
+
verdict,
|
|
482
|
+
classification: "retryable-invalid",
|
|
483
|
+
failureReason: `frontend prewrite gate revisionPatch mode failed to parse contract/patch: ${error instanceof Error ? error.message : String(error)}`,
|
|
484
|
+
failureCode: "contract-invalid",
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
let merged;
|
|
488
|
+
try {
|
|
489
|
+
merged = applyFrontendImplementationContractPatch(originalValue, patchValue);
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
return finalizePrewrite(input, {
|
|
493
|
+
...basePending,
|
|
494
|
+
verdict,
|
|
495
|
+
classification: "retryable-invalid",
|
|
496
|
+
failureReason: `frontend prewrite gate revisionPatch apply failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
497
|
+
failureCode: "contract-invalid",
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
effectiveContractText = serializeDeterministicJson(merged);
|
|
501
|
+
}
|
|
502
|
+
const requirementIdSearchText = effectiveContractText ?? planText;
|
|
503
|
+
const missingIds = input.config.requiredRequirementIds.filter((id) => !requirementIdSearchText.includes(id));
|
|
446
504
|
if (missingIds.length > 0) {
|
|
447
505
|
return finalizePrewrite(input, {
|
|
448
506
|
...basePending,
|
|
@@ -495,7 +553,8 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
495
553
|
try {
|
|
496
554
|
analysis = await analyzeFrontendImplementationContract({
|
|
497
555
|
runDir: input.runDir,
|
|
498
|
-
fromNodeId: planNodeId,
|
|
556
|
+
fromNodeId: effectiveContractText ? undefined : planNodeId,
|
|
557
|
+
rawContractText: effectiveContractText ?? undefined,
|
|
499
558
|
sourceBinding: input.sourceBinding,
|
|
500
559
|
});
|
|
501
560
|
}
|
|
@@ -515,6 +574,21 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
515
574
|
}
|
|
516
575
|
throw error;
|
|
517
576
|
}
|
|
577
|
+
// Size guard: a canonical contract above 64KB is a retryable-invalid defect
|
|
578
|
+
// (compress the contract) rather than a writer authorization failure.
|
|
579
|
+
const contractSizeBytes = Buffer.byteLength(serializeDeterministicJson(analysis.canonical), "utf8");
|
|
580
|
+
if (contractSizeBytes > FRONTEND_CONTRACT_MAX_BYTES) {
|
|
581
|
+
return finalizePrewrite(input, {
|
|
582
|
+
...basePending,
|
|
583
|
+
verdict,
|
|
584
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
585
|
+
normalizationActions: [],
|
|
586
|
+
mockStrategy: analysis.canonical.mockApi.strategy,
|
|
587
|
+
classification: "retryable-invalid",
|
|
588
|
+
failureReason: `frontend prewrite gate: canonical contract serialization is ${contractSizeBytes} bytes, exceeding the ${FRONTEND_CONTRACT_MAX_BYTES}-byte limit. 压缩契约(移除冗余 prose、重复目标或过长描述)后重试。`,
|
|
589
|
+
failureCode: "contract-too-large",
|
|
590
|
+
});
|
|
591
|
+
}
|
|
518
592
|
const mockStrategy = analysis.canonical.mockApi.strategy;
|
|
519
593
|
if (!input.config.allowedMockStrategies.includes(mockStrategy)) {
|
|
520
594
|
const quotedAllowed = input.config.allowedMockStrategies
|
|
@@ -711,6 +785,24 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
711
785
|
}
|
|
712
786
|
}
|
|
713
787
|
}
|
|
788
|
+
// Render the deterministic plan.md from the canonical contract (fail-closed
|
|
789
|
+
// before any artifact is written: no contract, no writer authorization).
|
|
790
|
+
let planMarkdown;
|
|
791
|
+
try {
|
|
792
|
+
planMarkdown = renderFrontendPlanMarkdown(analysis.canonical);
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
return finalizePrewrite(input, {
|
|
796
|
+
...basePending,
|
|
797
|
+
verdict,
|
|
798
|
+
candidateJsonSha256: analysis.candidateJsonSha256,
|
|
799
|
+
normalizationActions: [],
|
|
800
|
+
mockStrategy,
|
|
801
|
+
classification: "retryable-invalid",
|
|
802
|
+
failureReason: `frontend prewrite gate: failed to render ${input.config.planMdArtifactName ?? "frontend-plan.md"}: ${error instanceof Error ? error.message : String(error)}`,
|
|
803
|
+
failureCode: "contract-invalid",
|
|
804
|
+
});
|
|
805
|
+
}
|
|
714
806
|
// Materialize the canonical contract only after every governance check passed.
|
|
715
807
|
const artifact = await writeFrontendImplementationContractArtifact({
|
|
716
808
|
runDir: input.runDir,
|
|
@@ -718,6 +810,8 @@ export async function runFrontendPrewriteGate(input) {
|
|
|
718
810
|
artifactName: input.config.artifactName,
|
|
719
811
|
canonical: analysis.canonical,
|
|
720
812
|
});
|
|
813
|
+
// Materialize the rendered plan.md in the same outputDir as the contract.
|
|
814
|
+
await writeTextArtifactFile(path.join(input.runDir, path.posix.join(input.config.outputDir, input.config.planMdArtifactName ?? "frontend-plan.md")), planMarkdown);
|
|
721
815
|
const classification = analysis.normalizationActions.length > 0 ? "accepted-normalized" : "accepted";
|
|
722
816
|
return finalizePrewrite(input, {
|
|
723
817
|
...basePending,
|
|
@@ -2339,6 +2339,7 @@ function pruneFrontendTasksForRisk(tasks, risk) {
|
|
|
2339
2339
|
planFallbackFromNodeIds: [],
|
|
2340
2340
|
reviewFromNodeId: "frontend-design-review-pi",
|
|
2341
2341
|
reviewFallbackFromNodeIds: [],
|
|
2342
|
+
revisionPatch: false,
|
|
2342
2343
|
},
|
|
2343
2344
|
}
|
|
2344
2345
|
: task.shell,
|
|
@@ -2485,8 +2486,27 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2485
2486
|
"",
|
|
2486
2487
|
"### mockApi.endpoints - GOOD (strategy=native with complete endpoint):",
|
|
2487
2488
|
'{"strategy":"native","productionDefaultOff":true,"activation":"VITE_ENABLE_MOCK=true","endpoints":[{"method":"GET","path":"/api/users","fixture":"mocks/fixtures/users.json","consumer":"src/api/users.ts"}]}',
|
|
2489
|
+
"",
|
|
2490
|
+
"### optional plan fields - GOOD (all optional; omit when absent):",
|
|
2491
|
+
'{"implementationSteps":["confirm contract","sync tests"],"stylingStrategy":"reuse existing design tokens","dependencyPolicy":"no new runtime deps","residualRisks":["browser a11y not-run"],"realIntegrationGap":"FE-TEST owns live HTTP"}',
|
|
2492
|
+
"",
|
|
2493
|
+
"### optional plan fields - BAD (present-but-empty strings are rejected):",
|
|
2494
|
+
'{"stylingStrategy":"","dependencyPolicy":""} <-- REJECTED: optional string fields must be non-empty when present; omit them instead',
|
|
2488
2495
|
].join("\n");
|
|
2489
2496
|
})();
|
|
2497
|
+
const frontendContractFieldSummary = [
|
|
2498
|
+
"## Contract field summary (authoritative JSON; no plan prose)",
|
|
2499
|
+
"The plan/revision node emits only a fenced json contract — there is no Markdown plan explanation to read. Review these fields:",
|
|
2500
|
+
"- requirements[]: id, expectedOutcome, implementationTargets, verificationTargetIds, evidenceGap",
|
|
2501
|
+
"- uiStates[]: name, applicable, expectedBehavior, implementationTargets, verificationTargetIds, notApplicableReason",
|
|
2502
|
+
"- interactions[]: name, trigger, expectedBehavior, implementationTargets, verificationTargetIds",
|
|
2503
|
+
"- targets: files, routes, publicApiChanges",
|
|
2504
|
+
"- mockApi: strategy, productionDefaultOff, activation, endpoints[]",
|
|
2505
|
+
"- verificationTargets[]: id, type, commandLabel, file, symbol, requirementIds, uiStates",
|
|
2506
|
+
"- designEvidence: source, paths, conflicts; evidenceGaps[]",
|
|
2507
|
+
"- optional: implementationSteps[], stylingStrategy, dependencyPolicy, residualRisks[], realIntegrationGap",
|
|
2508
|
+
"Do not require or read a separate plan prose section; the contract JSON is the only plan surface.",
|
|
2509
|
+
].join("\n");
|
|
2490
2510
|
const sourceContext = [
|
|
2491
2511
|
buildSourceContextBlock(sources),
|
|
2492
2512
|
capabilityContextBlock,
|
|
@@ -2749,14 +2769,14 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2749
2769
|
allowedPaths: readOnlyPaths,
|
|
2750
2770
|
forbiddenPaths,
|
|
2751
2771
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2752
|
-
outputContract: "One-line lead-in, then exactly ONE fenced json object (```json ... ```) conforming to frontend-implementation-contract-v1. This fenced block is the single authoritative implementation contract the prewrite gate materializes; it must appear exactly once. Immediately after it, append exactly one ```openspec-citations``` fenced citation block.
|
|
2772
|
+
outputContract: "One-line lead-in, then exactly ONE fenced json object (```json ... ```) conforming to frontend-implementation-contract-v1. This fenced block is the single authoritative implementation contract the prewrite gate materializes; it must appear exactly once. Immediately after it, append exactly one ```openspec-citations``` fenced citation block. Do NOT emit a Markdown plan explanation or any prose after the contract JSON — the output is JSON-only (the gate renders plan.md deterministically). Apart from the contract JSON fenced block and the openspec-citations block, do not emit any other fenced block or raw JSON. No file writes.",
|
|
2753
2773
|
subtask_prompt: [
|
|
2754
2774
|
"Based on frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence, return a minimal frontend implementation plan.",
|
|
2755
2775
|
"Select the Mock / API strategy inside the plan and structured contract. Carry endpoint/fixture mapping, explicit activation, production-default-off rule, verification commands, and Real Integration Gap into both outputs.",
|
|
2756
|
-
"
|
|
2776
|
+
"Encode ordered steps (implementationSteps), target files, UI state handling, styling/component strategy (stylingStrategy), interaction notes, Mock/API strategy, dependency policy (dependencyPolicy), deterministic verification entrypoints, Real Integration Gap (realIntegrationGap), and residual risks (residualRisks) into the contract JSON fields. Use only the fixed entrypoints below; implementation may add tests behind them but cannot replace them.",
|
|
2757
2777
|
"Every target file and verification target must be selected from the current target workspace and task scope. Do not reuse paths or symbols from examples, prior tasks, or loop-agent itself; if the project uses app/, packages/, spec/, __tests__, or another layout, preserve that layout.",
|
|
2758
2778
|
"Consume the Scout TARGET_SURFACE evidence before selecting files. Preserve the discovered existing entrypoint and data source. If implementationPaths or testPaths are outside task allowedPaths, record a blocking scope conflict; do not substitute a new page or silently broaden the writeSet.",
|
|
2759
|
-
"Output in this exact order: (1) exactly one fenced json object conforming to frontend-implementation-contract-v1 — this fenced block is the single authoritative contract the prewrite gate materializes; (2) exactly one openspec-citations citation fenced block appended immediately after it
|
|
2779
|
+
"Output in this exact order: (1) exactly one fenced json object conforming to frontend-implementation-contract-v1 — this fenced block is the single authoritative contract the prewrite gate materializes; (2) exactly one openspec-citations citation fenced block appended immediately after it. Do NOT emit a Markdown plan explanation or any prose after the contract JSON — the output is JSON-only. Do not emit any raw JSON or JSON objects in prose. Apart from the single contract JSON fenced block and the openspec-citations block, do not emit any other fenced block.",
|
|
2760
2780
|
"Each requirement must state its user-observable or logic-observable expectedOutcome. Each interaction must state its trigger and expectedBehavior. IDs plus file paths are not sufficient behavior semantics.",
|
|
2761
2781
|
requirementCoverageInstruction,
|
|
2762
2782
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
@@ -2783,13 +2803,14 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2783
2803
|
outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
|
|
2784
2804
|
outputContract: "Plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings, Required Plan Corrections, and Checked Items. No file writes.",
|
|
2785
2805
|
subtask_prompt: [
|
|
2786
|
-
"Audit the frontend plan before implementation.",
|
|
2806
|
+
"Audit the frontend plan before implementation. Consume the contract JSON (the single fenced json block from frontend-plan-pi); there is no separate plan prose.",
|
|
2787
2807
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
2788
2808
|
"Request revision when the Mock strategy is MOCK_STRATEGY: blocked, missing, unsupported by repository evidence, inconsistent with the API contract, outside authorized paths/dependencies, unable to prove production-default-off behavior with the fixed production/default-real-path static check, or missing deterministic behavior verification for a declared behavior target or selected Mock strategy. Mock strategies require Mock-backed evidence. A static-only contract is allowed only when every verification target is static and maps to a declared static entrypoint. not-needed otherwise requires applicable real/no-remote behavior evidence unless auto mode explicitly skipped Mock because no project Mock capability exists; in that case the plan must preserve the real request path and record the Real Integration Gap.",
|
|
2789
2809
|
"Also request revision for missing applicable UI states, unsupported dependency additions, design-system drift without reason, weak interaction coverage, broad scope, inline fake data, schema drift, or missing deterministic verification commands.",
|
|
2790
2810
|
"Read-only: do not modify repository files.",
|
|
2791
2811
|
fixedVerificationContext,
|
|
2792
2812
|
sourceContext,
|
|
2813
|
+
frontendContractFieldSummary,
|
|
2793
2814
|
mockContextBlock,
|
|
2794
2815
|
].join("\n\n"),
|
|
2795
2816
|
},
|
|
@@ -2810,16 +2831,16 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2810
2831
|
allowedPaths: readOnlyPaths,
|
|
2811
2832
|
forbiddenPaths,
|
|
2812
2833
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2813
|
-
outputContract: "When the initial design review requests revision, return a one-line lead-in followed by exactly ONE fenced json object (```json ... ```)
|
|
2834
|
+
outputContract: "When the initial design review requests revision, return a one-line lead-in followed by exactly ONE fenced json object (```json ... ```) containing an RFC 7386 merge-patch delta against the original frontend-implementation-contract-v1 (only the fields you change; null deletes a key; arrays and scalars replace; plain objects merge recursively). Immediately after it, append exactly one ```openspec-citations``` fenced citation block. Do NOT emit a full contract, Markdown explanation, or prose — the output is JSON-only; the gate applies the patch on the original contract and renders plan.md deterministically. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block or raw JSON. No file writes.",
|
|
2814
2835
|
subtask_prompt: [
|
|
2815
|
-
"Consume frontend-plan-pi (original
|
|
2816
|
-
"This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce
|
|
2817
|
-
"The
|
|
2836
|
+
"Consume frontend-plan-pi (original contract JSON) and frontend-design-review-pi (first design review findings).",
|
|
2837
|
+
"This node runs only when frontend-design-review-pi emitted VERDICT: request-revision. Produce an RFC 7386 merge-patch delta against the original contract JSON that addresses every Required Plan Correction from the design findings.",
|
|
2838
|
+
"The patch delta may update any of these contract fields: requirements, implementationSteps, targets, uiStates, interactions, mockApi, dependencyPolicy, stylingStrategy, verificationTargets, evidenceGaps, residualRisks, realIntegrationGap. Only include fields you change; omit unchanged fields (the gate applies the patch on the original contract). null deletes a key; arrays and scalars replace; plain objects merge recursively.",
|
|
2818
2839
|
requirementCoverageInstruction,
|
|
2819
2840
|
"Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
|
|
2820
2841
|
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
2821
|
-
"Output in this exact order: (1) exactly one fenced json object
|
|
2822
|
-
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the
|
|
2842
|
+
"Output in this exact order: (1) exactly one fenced json object containing the merge-patch delta — this is the only plan output the prewrite gate applies on the original contract; (2) exactly one openspec-citations citation fenced block appended immediately after it. Do NOT emit a full contract, Markdown explanation, or prose — the output is JSON-only. Do not emit any raw JSON or JSON objects in prose. Apart from the patch JSON fenced block and the openspec-citations block, do not emit any other fenced block. Do not include secrets or unsafe paths.",
|
|
2843
|
+
"Preserve each requirement expectedOutcome and each interaction trigger/expectedBehavior in the effective (merged) contract; do not reduce behavior semantics to IDs and paths.",
|
|
2823
2844
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
2824
2845
|
verificationTargetFileInstruction,
|
|
2825
2846
|
fixedVerificationContext,
|
|
@@ -2848,7 +2869,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2848
2869
|
outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
|
|
2849
2870
|
outputContract: "For the effective frontend plan, return plain Markdown whose first non-empty line is VERDICT: pass or VERDICT: request-revision, followed by Findings and Checked Items. No file writes.",
|
|
2850
2871
|
subtask_prompt: [
|
|
2851
|
-
"Audit the revised frontend plan before implementation. This node runs only after request-revision and consumes frontend-plan-revision-pi.",
|
|
2872
|
+
"Audit the revised frontend plan before implementation. This node runs only after request-revision and consumes the merge-patch delta from frontend-plan-revision-pi applied on the original frontend-plan-pi contract JSON — there is no separate plan prose.",
|
|
2852
2873
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
2853
2874
|
"Verify that every Required Plan Correction from the initial design review has been fully addressed.",
|
|
2854
2875
|
"Recheck the selected Mock / API strategy, contract-to-fixture mapping, authorized paths/dependencies, explicit activation, production-default-off behavior, behavior verification, and Real Integration Gap. MOCK_STRATEGY: blocked cannot receive VERDICT: pass.",
|
|
@@ -2857,6 +2878,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2857
2878
|
"Read-only: do not modify repository files.",
|
|
2858
2879
|
fixedVerificationContext,
|
|
2859
2880
|
sourceContext,
|
|
2881
|
+
frontendContractFieldSummary,
|
|
2860
2882
|
mockContextBlock,
|
|
2861
2883
|
].join("\n\n"),
|
|
2862
2884
|
},
|
|
@@ -2900,6 +2922,8 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2900
2922
|
],
|
|
2901
2923
|
artifactName: "frontend-implementation-contract.json",
|
|
2902
2924
|
outputDir: "contracts",
|
|
2925
|
+
revisionPatch: true,
|
|
2926
|
+
planMdArtifactName: "frontend-plan.md",
|
|
2903
2927
|
requireSourceFreshness: true,
|
|
2904
2928
|
implementationWriteSet: implementPaths.writeSet,
|
|
2905
2929
|
openspecPolicy: openspecGate.openspecPolicy,
|
|
@@ -199,6 +199,22 @@ export const dagFrontendPrewriteGateSchema = z.object({
|
|
|
199
199
|
* preserved. Generation always writes it explicitly for new DAGs.
|
|
200
200
|
*/
|
|
201
201
|
openspecPolicy: z.enum(["cited", "scan-strict"]).optional(),
|
|
202
|
+
/**
|
|
203
|
+
* When true, the effective plan node (planFromNodeId when it is selected
|
|
204
|
+
* over the fallback) emits an RFC 7386 merge-patch delta instead of a full
|
|
205
|
+
* contract. The gate applies it on the original contract node
|
|
206
|
+
* (planFallbackFromNodeIds[0]) before analysis/materialization. Legacy DAGs
|
|
207
|
+
* without this field keep full-contract semantics.
|
|
208
|
+
*/
|
|
209
|
+
revisionPatch: z.boolean().optional(),
|
|
210
|
+
/**
|
|
211
|
+
* Deterministically rendered plan.md artifact name (same outputDir as the
|
|
212
|
+
* contract). Defaults to frontend-plan.md at runtime.
|
|
213
|
+
*/
|
|
214
|
+
planMdArtifactName: z
|
|
215
|
+
.string()
|
|
216
|
+
.regex(/^[a-z0-9][a-z0-9._-]*\.md$/)
|
|
217
|
+
.optional(),
|
|
202
218
|
/**
|
|
203
219
|
* Generation-frozen candidate source metadata (diagnostic). cited mode
|
|
204
220
|
* candidates are declared ∪ taskSourceCited; scan-strict candidates are
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
"mockApi": { "type": "object", "additionalProperties": false, "required": ["strategy", "productionDefaultOff", "activation", "endpoints"], "properties": { "strategy": { "enum": ["native", "browser-intercept", "request-adapter", "not-needed"] }, "productionDefaultOff": { "const": true }, "activation": { "type": ["string", "null"], "minLength": 1 }, "endpoints": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["method", "path"], "properties": { "method": { "enum": ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"] }, "path": { "type": "string", "pattern": "^/" }, "fixture": { "anyOf": [{ "$ref": "#/$defs/path" }, { "type": "null" }] }, "consumer": { "anyOf": [{ "$ref": "#/$defs/path" }, { "type": "null" }] } } } } } },
|
|
17
17
|
"designEvidence": { "type": "object", "additionalProperties": false, "required": ["source", "paths", "conflicts"], "properties": { "source": { "type": "string", "minLength": 1 }, "paths": { "type": "array", "items": { "$ref": "#/$defs/path" } }, "conflicts": { "type": "array", "items": { "type": "string", "minLength": 1 } } } },
|
|
18
18
|
"verificationTargets": { "type": "array", "minItems": 1, "items": { "type": "object", "additionalProperties": false, "required": ["id", "type", "commandLabel", "file", "requirementIds", "uiStates"], "properties": { "id": { "type": "string", "minLength": 1 }, "type": { "enum": ["static", "unit", "component", "integration", "mock"] }, "commandLabel": { "type": "string", "minLength": 1 }, "file": { "$ref": "#/$defs/path" }, "symbol": { "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "null" }] }, "requirementIds": { "type": "array", "items": { "$ref": "#/$defs/requirementId" } }, "uiStates": { "type": "array", "items": { "type": "string", "minLength": 1 } } } } },
|
|
19
|
-
"evidenceGaps": { "type": "array", "items": { "$ref": "#/$defs/gap" } }
|
|
19
|
+
"evidenceGaps": { "type": "array", "items": { "$ref": "#/$defs/gap" } },
|
|
20
|
+
"implementationSteps": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
21
|
+
"stylingStrategy": { "type": "string", "minLength": 1 },
|
|
22
|
+
"dependencyPolicy": { "type": "string", "minLength": 1 },
|
|
23
|
+
"residualRisks": { "type": "array", "items": { "type": "string", "minLength": 1 } },
|
|
24
|
+
"realIntegrationGap": { "type": "string", "minLength": 1 }
|
|
20
25
|
},
|
|
21
26
|
"allOf": [{ "if": { "properties": { "mockApi": { "properties": { "strategy": { "enum": ["native", "browser-intercept", "request-adapter"] } } } } }, "then": { "properties": { "mockApi": { "properties": { "endpoints": { "minItems": 1, "items": { "required": ["method", "path", "fixture", "consumer"] } } } } } } }],
|
|
22
27
|
"$defs": {
|
package/package.json
CHANGED
|
@@ -6,11 +6,12 @@ Pre-write nodes are read-only. Preserve IDs, labels, commands, language, require
|
|
|
6
6
|
|
|
7
7
|
- **`frontend-contract-pi`**: `Scope`, `Non-goals`, `Acceptance Criteria`, `UI States`, `Target Runtime Environment`, `Risks`, `Verification Expectations`; no guessed requirements.
|
|
8
8
|
- **`frontend-scout-pi`**: routes, components, tokens, data/API/Mock, scripts, tests, assets; fact vs inference vs gap. Search+read `openspec/schemas/`, `openspec/project-specs/`, `ai_workspace/` first. Output stack, routes, components, styling, conventions, state/data, test entry points, reuse, risks.
|
|
9
|
-
- **`frontend-plan-pi` + conditional design loop**: AC → observable `expectedOutcome`; interactions → `trigger`+`expectedBehavior`; steps, in-bound files, applicable UI states, reuse, deps, Mock/API strategy, activation/rollback, frozen verify entrypoints, real-integration gap.
|
|
9
|
+
- **`frontend-plan-pi` + conditional design loop**: AC → observable `expectedOutcome`; interactions → `trigger`+`expectedBehavior`; steps, in-bound files, applicable UI states, reuse, deps, Mock/API strategy, activation/rollback, frozen verify entrypoints, real-integration gap. **JSON-only**: one-line lead-in + **exactly one** fenced `json` block (`frontend-implementation-contract-v1`) + one `openspec-citations` block. No plan prose; plan.md is rendered by the prewrite gate. `frontend-contract-json-pi`/`frontend-contract-json-validate-shell` don't exist. IDs+paths aren't sufficient behavior semantics. Use `uiStates: []` for logic-only changes; don't invent UI states. Applicable states need behavior/implementation/verification; non-applicable need a reason. Non-`not-needed` needs frozen Mock verify commands (`frontendMock.verifyCommands`/`package.json` mock script/capability seed); `auto` with absent/ambiguous capability or no command → `not-needed`; `required` without a command → generation-time blocked. Initial pass uses original plan; only exact `request-revision` runs revision + final review. Small-risk: one design review.
|
|
10
10
|
- **`frontend-prewrite-gate-shell`**: sole write authorization. Resolve effective plan (revised if revision ran, else original) + review; require exact pass; retain every REQ/BR/AC id; enforce Mock policy; validate schema/source binding+writeSet containment; materialize `contracts/frontend-implementation-contract.json` from the **single** fenced contract block (multiple candidates fail closed `invalid-output`). Fallback only when conditional primary absent; existing malformed primary fails closed. Generation-time blocked Mock yields one deterministic blocking shell node, no writer.
|
|
11
11
|
- `verificationTarget.commandLabel` must be a **frozen command label** from `verifyEvidence.commandLabels`; others fail closed (`invalid-output`). Empty frozen set (no `run.json`) skips check.
|
|
12
12
|
- `mockApi.strategy !== "not-needed"` fails closed when `mockCommandLabels` empty (`no authorized Mock verification commands`).
|
|
13
13
|
- OpenSpec policy: `task.json.frontendOpenspec.policy` (default `cited`); candidates = `requiredReadPaths` ∪ task-source citations; effective plan must append one fenced `openspec-citations` block (one JSON `{"path","section","line"}` per line). Candidates non-empty but block missing/unparseable → `openspec-citation-block-unparseable`; candidate not cited → `openspec-not-cited`; cited path without successful read event in plan/review → `openspec-citation-not-read` (anti-fabrication). Empty candidates skip enforcement (generation-time advisory). `scan-strict` keeps auto-discovered full-read semantics (`openspec-not-read`); both modes keep `candidate-missing-drift`.
|
|
14
|
+
- revisionPatch mode: revision output is an RFC 7386 merge-patch delta applied on the original contract node (`planFallbackFromNodeIds[0]`); legacy DAGs keep full-contract semantics. Size guard: canonical serialization >64KB → `retryable-invalid` + `contract-too-large`.
|
|
14
15
|
- **`frontend-implement-pi`**: sole regular exclusive writer; uses `frontend-bounded-implement` (not this skill). Stay in `writeSet`; real requests default-on. Atomic handler/intercept/adapter with consumer+tests. Stop on forbidden paths/guesses. First line `IMPLEMENTATION_OUTCOME: changed|already-satisfied|blocked`; runtime checks against attributed diff. Optional mock-verify when frozen; static+behavior always; behavior must prove page consumption. `not-needed` keeps real integration pending unless real backend has fresh evidence.
|
|
15
16
|
|
|
16
17
|
## Contract / trace / stages (M1–M2)
|