@tea-agent/loop-agent 0.39.0-next.18 → 0.39.0-next.19
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 +11 -0
- package/dist/build-stamp.json +2 -2
- package/dist/workflows/dag/contract-validator-registrations.js +2 -1
- package/dist/workflows/dag/frontend-implementation-contract.js +236 -8
- package/dist/workflows/dag/init-hybrid.js +20 -30
- package/dist/workflows/dag/node-execution.js +45 -18
- package/dist/workflows/dag/types.js +18 -0
- package/package.json +1 -1
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### 变更
|
|
6
|
+
|
|
7
|
+
- npm `next` 渠道发布改为提交数节流:main push 仅当距上次 next 发布的 source commit ≥ 5 个 commit 时才自动发布(新增 `scripts/next-publish-gate.mjs`,读已发布 tarball build-stamp 对比 HEAD);不足阈值时 workflow 绿色跳过,手动 `workflow_dispatch` 不受限(切 release 线前强制出包 / 紧急补发)。发布文档真源 §4.1/§5.1/§7/§12 同步。
|
|
8
|
+
|
|
5
9
|
### 新增
|
|
6
10
|
|
|
7
11
|
- backend-test 适配 A–D:可配置 `backendTest` 布局(`testRoot`/`markdownRel`/`scriptRel`)、用户公共前置文档引用与模块顶部 `user_shared_setup`(不写 conftest)、查询轴 Scenario Partitions 确定性展开与 fail-closed 门禁、`mode=gap-fill` 13 节点增量 DAG。Slot ID 以声明的 Partition ID 为真源(`TP-<Partition ID>-...`)。
|
|
@@ -24,6 +28,7 @@
|
|
|
24
28
|
|
|
25
29
|
### 改进
|
|
26
30
|
|
|
31
|
+
- `frontend-plan-pi` 改为 runtime skeleton + editable RFC 7386 patch:`schemaVersion`、source binding、risk、writer target files 与生产 Mock 安全位由 DAG 冻结且不可被 plan/revision 覆盖;节点 validator 将补丁编译为 canonical 完整契约后再交给 review/prewrite,并在 `contracts/candidates/<nodeId>/` 保存每次 raw/extracted/normalized/validation 证据。任务提示与三类结构化重试(invalid-output / provider length 截断 / output-too-large)同步收敛为单一 patch + 唯一 `openspec-citations`;无法在声明文件中解析的可选 verification symbol 会被确定性移除并留下 normalization action,避免格式冲突、完整契约重复输出和说明性 symbol 延迟到 trace 才失败。
|
|
27
32
|
- Operator Chat 恢复工具对齐真实能力:系统提示改用 `dagReport` / `dagRerunPlan` / `dagRerun` / `standaloneTaskRerun` / `workerTaskRetry` 真名,不再写 `operator_*` 或把 `dagResume` 当成模型可执行项;`dagDoctor` / `dagStatus` / `workerPoolDoctor` 改为 typed `runId`/`taskId`,不再让模型猜 CLI `args[]`。ADR 0013 自定义 base prompt 上限由 1900 统一调整为 2500(2026-08-18,废止 0.8×2498 比例),预算用于写清节点续跑 / 整单重生 / Worker 重排队分流,以及多轮迭代恢复纪律(先一句话总结上一轮错误、修根因不修症状、禁止原样重交、同错两轮后停下报告)。
|
|
28
33
|
|
|
29
34
|
- Operator Chat 对齐 pi-web:建会话不再传封闭 `tools` 白名单(避免 SDK 把扩展 MCP 工具滤出 registry),创建后 `bindExtensions` 发 `session_start`,再钉激活集。扩展注册的 MCP 工具可被模型调用;`apply_patch` / `full-tools` / `shell` / `coding-chat` 仍排除。
|
|
@@ -59,6 +64,7 @@
|
|
|
59
64
|
- 强化 backend-test correspondence、scenario-param、Markdown/pytest writer、collection repair、日志审计和 Windows pytest argfile 执行链,稳定区分 controller TestBug、generated TestBug 与真实 API ProductBug
|
|
60
65
|
|
|
61
66
|
### 改进
|
|
67
|
+
|
|
62
68
|
- backend-test correspondence 只分析目标请求实际绑定的 body/payload,忽略 `scenario` 元数据与 setup-only payload;对 `scenario["body"]` 保留字段 shape,并把 intentional invalid enum 交由 scenario-param 门禁判断;同时识别下划线前缀 pytest 参数集合与带说明的 `Payload Contract: none(…)`,避免零 eligible 的 payload/parameter 误阻断
|
|
63
69
|
- backend-test correspondence 继续解析模块内 transport wrapper 的位置 payload 与 wrapper 构造 body,并清理 `Payload Enum` 值后的中文说明,避免 PUT/lifecycle 合法载荷误报;Markdown/pytest writer 同步禁止臆测响应 ID 类型,并在路径 ID 类型未声明时使用 create-delete 派生的合法 missing ID,避免把测试资产缺陷误分类为产品 400/404 缺陷
|
|
64
70
|
- backend-test correspondence 可还原 `_apply_patch(_base_body(...), patch)` 等 transport-body 组合表达式,并把 setup wrapper payload 限定为有 body 契约时的 fallback;traceability 接受经递归脱敏和有界输出的结构化 request/response 日志,scenario-param 支持带显式例值的 string wrong-type;pytest writer/repair 同步禁止把 path/query/control `id` 混入 DTO patch
|
|
@@ -240,6 +246,7 @@
|
|
|
240
246
|
|
|
241
247
|
- 修复「处理详情」思考折叠块背景引用未定义样式变量 --panel 的问题,改为确定性透明背景,消除 Console 样式变量完整性门禁对发布的拦截(视觉无变化)
|
|
242
248
|
- 移除自指的 /help 命令项:输入 / 已弹出命令面板,不再单独提供重复的帮助入口
|
|
249
|
+
|
|
243
250
|
## [0.35.1] - 2026-08-15
|
|
244
251
|
|
|
245
252
|
### 重点更新
|
|
@@ -473,19 +480,23 @@
|
|
|
473
480
|
## [0.33.7-beta.2] - 2026-08-13
|
|
474
481
|
|
|
475
482
|
### Fixed
|
|
483
|
+
|
|
476
484
|
- scenario-param 解析 `bound=LEN100`、解引用 `_STATUS_ACTIVE` 等 Python 字符串常量,识别 `"x" * _TITLE_MAX_LENGTH` 长度表达式
|
|
477
485
|
- correspondence 忽略 pytest payload 中的 `expect`/`echo_*` 辅助键,避免假 PAYLOAD_SHAPE_MISMATCH
|
|
478
486
|
|
|
479
487
|
### Changed
|
|
488
|
+
|
|
480
489
|
- my-webapp 五轮 backend-test 全流程 live campaign 归档(R1–R5 均 finished;R5 correspondence PASS)
|
|
481
490
|
|
|
482
491
|
## [0.33.7-beta.1] - 2026-08-12
|
|
483
492
|
|
|
484
493
|
### Fixed
|
|
494
|
+
|
|
485
495
|
- scenario-param 识别 `_*_MAX_LENGTH` / `_*_OVER_LENGTH` 等长度 helper 名,明确 `custom-literal:trim` 不能是全空白,并把无 pytest.param 的 request-level/health nominal 记为 MATCH 以免污染 eligibility
|
|
486
496
|
- backend-test writer 合同明确:`enum-invalid` 必须使用具体非法字面量且禁止 `_OMIT`/missing-key;`custom-literal:trim|ACTIVE|ARCHIVED` 必须使用真实 padded/enum 字面量;max/min/max+1 应用长度表达式/helper
|
|
487
497
|
|
|
488
498
|
### Changed
|
|
499
|
+
|
|
489
500
|
- 归档 R108–R110 live campaign 证据与 residual 收紧结果
|
|
490
501
|
|
|
491
502
|
## [0.33.7-beta.0] - 2026-08-11
|
package/dist/build-stamp.json
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* its schemaId in the contract output registry. Imported once by the node
|
|
4
4
|
* executor; keeps per-contract imports out of the executor.
|
|
5
5
|
*/
|
|
6
|
-
import { validateFrontendContractNodeOutput, validateFrontendRevisionPatchNodeOutput, } from "./frontend-implementation-contract.js";
|
|
6
|
+
import { validateFrontendContractNodeOutput, validateFrontendPlanPatchNodeOutput, validateFrontendRevisionPatchNodeOutput, } from "./frontend-implementation-contract.js";
|
|
7
7
|
import { registerStructuredContractValidator } from "./contract-output-registry.js";
|
|
8
8
|
registerStructuredContractValidator("frontend-implementation-contract-v1", validateFrontendContractNodeOutput);
|
|
9
|
+
registerStructuredContractValidator("frontend-implementation-contract-plan-patch-v1", validateFrontendPlanPatchNodeOutput);
|
|
9
10
|
registerStructuredContractValidator("frontend-implementation-contract-revision-patch-v1", validateFrontendRevisionPatchNodeOutput);
|
|
@@ -13,6 +13,7 @@ export const frontendNormalizationActionSchema = z.enum([
|
|
|
13
13
|
"strip-comments",
|
|
14
14
|
"canonicalize-requirement-ids",
|
|
15
15
|
"canonicalize-verification-alias",
|
|
16
|
+
"drop-unresolved-verification-symbols",
|
|
16
17
|
"inject-source-binding",
|
|
17
18
|
]);
|
|
18
19
|
export function sha256Hex(input) {
|
|
@@ -102,6 +103,21 @@ async function deriveFrozenCommandLabelsFromRun(runDir) {
|
|
|
102
103
|
return [...labels];
|
|
103
104
|
}
|
|
104
105
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
106
|
+
export const FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID = "frontend-implementation-contract-plan-patch-v1";
|
|
107
|
+
/**
|
|
108
|
+
* Build the deterministic, run-owned portion of the frontend contract. The
|
|
109
|
+
* planner is allowed to fill semantic fields with an RFC 7386 merge patch but
|
|
110
|
+
* cannot redefine task identity, risk, or the implementation write boundary.
|
|
111
|
+
*/
|
|
112
|
+
export function buildFrontendImplementationContractSkeleton(input) {
|
|
113
|
+
return {
|
|
114
|
+
schemaVersion: 1,
|
|
115
|
+
sourceBinding: canonicalFrontendContractSourceBinding(input.sourceBinding),
|
|
116
|
+
riskLevel: input.riskLevel,
|
|
117
|
+
targets: { files: [...input.targetFiles] },
|
|
118
|
+
mockApi: { productionDefaultOff: true },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
105
121
|
/**
|
|
106
122
|
* Load the canonical frontend-implementation-contract-v1 JSON Schema from the
|
|
107
123
|
* installed loop-agent package docs/templates/ path. Package-root discovery
|
|
@@ -499,9 +515,9 @@ function isPlainObject(value) {
|
|
|
499
515
|
* ever materialized, so a patch that removes a required field still fails
|
|
500
516
|
* closed at the gate.
|
|
501
517
|
*/
|
|
502
|
-
export function
|
|
518
|
+
export function applyFrontendContractMergePatch(target, patch) {
|
|
503
519
|
if (!isPlainObject(patch)) {
|
|
504
|
-
throw new Error("frontend
|
|
520
|
+
throw new Error("frontend contract patch must be a JSON object");
|
|
505
521
|
}
|
|
506
522
|
const merge = (base, delta) => {
|
|
507
523
|
if (delta === null)
|
|
@@ -526,6 +542,9 @@ export function applyFrontendImplementationContractPatch(target, patch) {
|
|
|
526
542
|
};
|
|
527
543
|
return merge(target, patch);
|
|
528
544
|
}
|
|
545
|
+
export function applyFrontendImplementationContractPatch(target, patch) {
|
|
546
|
+
return applyFrontendContractMergePatch(target, patch);
|
|
547
|
+
}
|
|
529
548
|
export async function assertFrontendSourceBindingFresh(input) {
|
|
530
549
|
for (const source of input.binding.sources) {
|
|
531
550
|
const absolute = resolveDagTaskSourcePath({
|
|
@@ -1608,6 +1627,206 @@ export async function writeFrontendImplementationContractArtifact(input) {
|
|
|
1608
1627
|
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID,
|
|
1609
1628
|
};
|
|
1610
1629
|
}
|
|
1630
|
+
const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
1631
|
+
"/schemaVersion",
|
|
1632
|
+
"/sourceBinding",
|
|
1633
|
+
"/riskLevel",
|
|
1634
|
+
"/targets/files",
|
|
1635
|
+
"/mockApi/productionDefaultOff",
|
|
1636
|
+
];
|
|
1637
|
+
function frontendPlanPatchProtectedPathViolations(patch) {
|
|
1638
|
+
const record = asRecord(patch);
|
|
1639
|
+
if (!record)
|
|
1640
|
+
return ["/"];
|
|
1641
|
+
const violations = [];
|
|
1642
|
+
for (const key of ["schemaVersion", "sourceBinding", "riskLevel"]) {
|
|
1643
|
+
if (Object.hasOwn(record, key))
|
|
1644
|
+
violations.push(`/${key}`);
|
|
1645
|
+
}
|
|
1646
|
+
const targets = asRecord(record.targets);
|
|
1647
|
+
if (targets && Object.hasOwn(targets, "files"))
|
|
1648
|
+
violations.push("/targets/files");
|
|
1649
|
+
const mockApi = asRecord(record.mockApi);
|
|
1650
|
+
if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
|
|
1651
|
+
violations.push("/mockApi/productionDefaultOff");
|
|
1652
|
+
return violations;
|
|
1653
|
+
}
|
|
1654
|
+
function workspaceRootFromDagRunDir(runDir) {
|
|
1655
|
+
const absolute = path.resolve(runDir);
|
|
1656
|
+
const segments = absolute.split(path.sep);
|
|
1657
|
+
const harnessIndex = segments.lastIndexOf(".harness");
|
|
1658
|
+
if (harnessIndex < 0 ||
|
|
1659
|
+
segments[harnessIndex + 1] !== "dag-runs" ||
|
|
1660
|
+
harnessIndex + 3 >= segments.length) {
|
|
1661
|
+
return null;
|
|
1662
|
+
}
|
|
1663
|
+
const joined = segments.slice(0, harnessIndex).join(path.sep);
|
|
1664
|
+
return joined || path.parse(absolute).root;
|
|
1665
|
+
}
|
|
1666
|
+
function verificationSymbolMatchesFile(symbol, content) {
|
|
1667
|
+
if (symbol.trim().toLowerCase() === "all describe blocks")
|
|
1668
|
+
return content.includes("describe(");
|
|
1669
|
+
const symbols = symbol.includes(" / ")
|
|
1670
|
+
? symbol
|
|
1671
|
+
.split(/[((]/, 1)[0]
|
|
1672
|
+
.split("/")
|
|
1673
|
+
.map((item) => item.trim())
|
|
1674
|
+
.filter(Boolean)
|
|
1675
|
+
: [symbol];
|
|
1676
|
+
return symbols.every((item) => {
|
|
1677
|
+
const describeTitle = item.match(/^describe\((?:['"])(.+?)(?:['"])/i)?.[1];
|
|
1678
|
+
return [item, describeTitle, describeTitle ? "describe(" : undefined]
|
|
1679
|
+
.filter((candidate) => Boolean(candidate))
|
|
1680
|
+
.some((candidate) => content.includes(candidate));
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
async function dropUnresolvedVerificationSymbols(input) {
|
|
1684
|
+
const workspaceRoot = workspaceRootFromDagRunDir(input.runDir);
|
|
1685
|
+
const targets = asRecord(input.contract)?.verificationTargets;
|
|
1686
|
+
if (!workspaceRoot || !Array.isArray(targets))
|
|
1687
|
+
return false;
|
|
1688
|
+
let changed = false;
|
|
1689
|
+
for (const targetValue of targets) {
|
|
1690
|
+
const target = asRecord(targetValue);
|
|
1691
|
+
const file = asString(target?.file);
|
|
1692
|
+
const symbol = asString(target?.symbol);
|
|
1693
|
+
if (!target || !file || !symbol)
|
|
1694
|
+
continue;
|
|
1695
|
+
const absolute = path.resolve(workspaceRoot, file);
|
|
1696
|
+
const relative = path.relative(workspaceRoot, absolute);
|
|
1697
|
+
if (relative.startsWith("..") ||
|
|
1698
|
+
path.isAbsolute(relative) ||
|
|
1699
|
+
relative.includes("..")) {
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
let content;
|
|
1703
|
+
try {
|
|
1704
|
+
content = await readFile(absolute, "utf8");
|
|
1705
|
+
}
|
|
1706
|
+
catch {
|
|
1707
|
+
// Missing files remain a trace-time hard failure; do not hide them by
|
|
1708
|
+
// changing the optional symbol during candidate normalization.
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
if (verificationSymbolMatchesFile(symbol, content))
|
|
1712
|
+
continue;
|
|
1713
|
+
delete target.symbol;
|
|
1714
|
+
changed = true;
|
|
1715
|
+
}
|
|
1716
|
+
return changed;
|
|
1717
|
+
}
|
|
1718
|
+
function extractSingleOpenspecCitationsBlock(text) {
|
|
1719
|
+
const blocks = text.match(/```openspec-citations[ \t]*\r?\n[\s\S]*?\r?\n```/g) ?? [];
|
|
1720
|
+
if (blocks.length !== 1) {
|
|
1721
|
+
throw new Error(`frontend plan patch output must include exactly one fenced openspec-citations block (found ${blocks.length})`);
|
|
1722
|
+
}
|
|
1723
|
+
return blocks[0];
|
|
1724
|
+
}
|
|
1725
|
+
async function writeFrontendPlanCandidateRaw(input) {
|
|
1726
|
+
const relativePath = path.posix.join("contracts", "candidates", input.nodeId, `attempt-${input.attempt}.raw.md`);
|
|
1727
|
+
await writeTextArtifactFile(path.join(input.runDir, relativePath), input.text.endsWith("\n") ? input.text : `${input.text}\n`);
|
|
1728
|
+
return relativePath;
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Compile the planner's editable RFC 7386 patch against the deterministic
|
|
1732
|
+
* runtime skeleton. Every attempt is preserved under contracts/candidates so
|
|
1733
|
+
* format/schema failures can be inspected and replayed without trusting model
|
|
1734
|
+
* prose. A successful result returns canonical full-contract text for all
|
|
1735
|
+
* downstream review and prewrite nodes.
|
|
1736
|
+
*/
|
|
1737
|
+
export async function validateFrontendPlanPatchNodeOutput(input) {
|
|
1738
|
+
const nodeId = input.nodeId ?? "frontend-plan-pi";
|
|
1739
|
+
const attempt = input.attempt ?? 1;
|
|
1740
|
+
const candidateDir = path.posix.join("contracts", "candidates", nodeId);
|
|
1741
|
+
const rawPath = await writeFrontendPlanCandidateRaw({
|
|
1742
|
+
runDir: input.runDir,
|
|
1743
|
+
nodeId,
|
|
1744
|
+
attempt,
|
|
1745
|
+
text: input.text,
|
|
1746
|
+
});
|
|
1747
|
+
const reportBase = {
|
|
1748
|
+
schemaVersion: 1,
|
|
1749
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID,
|
|
1750
|
+
nodeId,
|
|
1751
|
+
attempt,
|
|
1752
|
+
rawPath,
|
|
1753
|
+
protectedPaths: [...FRONTEND_PLAN_PATCH_PROTECTED_PATHS],
|
|
1754
|
+
candidateRawSha256: sha256Hex(input.text),
|
|
1755
|
+
};
|
|
1756
|
+
let extractedPatchPath;
|
|
1757
|
+
let extractedPatchSha256;
|
|
1758
|
+
const writeReport = async (value) => {
|
|
1759
|
+
await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.validation.json`), {
|
|
1760
|
+
...reportBase,
|
|
1761
|
+
...(extractedPatchPath
|
|
1762
|
+
? { extractedPatchPath, extractedPatchSha256 }
|
|
1763
|
+
: {}),
|
|
1764
|
+
...value,
|
|
1765
|
+
});
|
|
1766
|
+
};
|
|
1767
|
+
try {
|
|
1768
|
+
if (!input.sourceBinding)
|
|
1769
|
+
throw new Error("frontend plan patch validator requires DAG sourceBinding");
|
|
1770
|
+
const skeleton = input.structuredContractOutput?.skeleton;
|
|
1771
|
+
if (!skeleton)
|
|
1772
|
+
throw new Error("frontend plan patch validator requires a deterministic runtime skeleton");
|
|
1773
|
+
const patch = extractFrontendImplementationJson(input.text);
|
|
1774
|
+
if (!isPlainObject(patch))
|
|
1775
|
+
throw new Error("frontend plan patch must extract to one JSON object");
|
|
1776
|
+
extractedPatchPath = path.posix.join(candidateDir, `attempt-${attempt}.extracted.json`);
|
|
1777
|
+
const extractedArtifact = await writeDeterministicJsonArtifact(input.runDir, extractedPatchPath, patch);
|
|
1778
|
+
extractedPatchSha256 = extractedArtifact.sha256;
|
|
1779
|
+
const citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1780
|
+
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1781
|
+
if (protectedViolations.length > 0)
|
|
1782
|
+
throw new Error(`frontend plan patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`);
|
|
1783
|
+
const merged = applyFrontendContractMergePatch(skeleton, patch);
|
|
1784
|
+
const droppedUnresolvedSymbols = await dropUnresolvedVerificationSymbols({
|
|
1785
|
+
runDir: input.runDir,
|
|
1786
|
+
contract: merged,
|
|
1787
|
+
});
|
|
1788
|
+
const analysis = await analyzeFrontendImplementationContract({
|
|
1789
|
+
runDir: input.runDir,
|
|
1790
|
+
rawContractText: serializeDeterministicJson(merged),
|
|
1791
|
+
sourceBinding: input.sourceBinding,
|
|
1792
|
+
});
|
|
1793
|
+
const normalizedArtifact = await writeDeterministicJsonArtifact(input.runDir, path.posix.join(candidateDir, `attempt-${attempt}.normalized.json`), analysis.canonical);
|
|
1794
|
+
await writeReport({
|
|
1795
|
+
classification: "accepted-normalized",
|
|
1796
|
+
normalizedContractPath: path.posix.relative(input.runDir.replaceAll(path.sep, "/"), normalizedArtifact.path.replaceAll(path.sep, "/")),
|
|
1797
|
+
normalizedContractSha256: normalizedArtifact.sha256,
|
|
1798
|
+
normalizationActions: [
|
|
1799
|
+
"apply-runtime-skeleton",
|
|
1800
|
+
...(droppedUnresolvedSymbols
|
|
1801
|
+
? ["drop-unresolved-verification-symbols"]
|
|
1802
|
+
: []),
|
|
1803
|
+
...analysis.normalizationActions,
|
|
1804
|
+
],
|
|
1805
|
+
errors: [],
|
|
1806
|
+
});
|
|
1807
|
+
return {
|
|
1808
|
+
ok: true,
|
|
1809
|
+
contract: analysis.canonical,
|
|
1810
|
+
normalizedText: [
|
|
1811
|
+
"Frontend implementation contract compiled from the runtime skeleton.",
|
|
1812
|
+
"",
|
|
1813
|
+
"```json",
|
|
1814
|
+
serializeDeterministicJson(analysis.canonical),
|
|
1815
|
+
"```",
|
|
1816
|
+
citationsBlock,
|
|
1817
|
+
].join("\n"),
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
catch (error) {
|
|
1821
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1822
|
+
await writeReport({
|
|
1823
|
+
classification: "retryable-invalid",
|
|
1824
|
+
normalizationActions: [],
|
|
1825
|
+
errors: [{ path: "$", code: "invalid-output", message: reason }],
|
|
1826
|
+
});
|
|
1827
|
+
return { ok: false, reason };
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1611
1830
|
/**
|
|
1612
1831
|
* Node-output self-check for plan nodes that produce the implementation
|
|
1613
1832
|
* contract. Mirrors the prewrite gate validation so schema/typo/null violations
|
|
@@ -1635,7 +1854,7 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1635
1854
|
/**
|
|
1636
1855
|
* Node-output self-check for the plan revision node, which emits an RFC 7386
|
|
1637
1856
|
* merge-patch delta against the original contract instead of a full contract.
|
|
1638
|
-
* Deliberately loose and format-level: the output must contain
|
|
1857
|
+
* Deliberately loose and format-level: the output must contain exactly one
|
|
1639
1858
|
* ```openspec-citations fenced block and must extract (after the citations
|
|
1640
1859
|
* blocks are stripped) to exactly one JSON object. The delta carries no
|
|
1641
1860
|
* schemaVersion/targets, so no full-contract schema/semantic checks run here;
|
|
@@ -1643,17 +1862,19 @@ export async function validateFrontendContractNodeOutput(input) {
|
|
|
1643
1862
|
* the prewrite gate, which remains the only authority.
|
|
1644
1863
|
*/
|
|
1645
1864
|
export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1865
|
+
let citationsBlock;
|
|
1866
|
+
try {
|
|
1867
|
+
citationsBlock = extractSingleOpenspecCitationsBlock(input.text);
|
|
1868
|
+
}
|
|
1869
|
+
catch (error) {
|
|
1649
1870
|
return {
|
|
1650
1871
|
ok: false,
|
|
1651
|
-
reason:
|
|
1872
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1652
1873
|
};
|
|
1653
1874
|
}
|
|
1654
1875
|
// Strip the citation blocks before extraction: their per-row JSON objects
|
|
1655
1876
|
// would otherwise count as competing balanced objects in the extractor.
|
|
1656
|
-
const textWithoutCitations = input.text.replace(
|
|
1877
|
+
const textWithoutCitations = input.text.replace(citationsBlock, "");
|
|
1657
1878
|
let patch;
|
|
1658
1879
|
try {
|
|
1659
1880
|
patch = extractFrontendImplementationJson(textWithoutCitations);
|
|
@@ -1670,6 +1891,13 @@ export async function validateFrontendRevisionPatchNodeOutput(input) {
|
|
|
1670
1891
|
reason: "revision patch output must extract to exactly one JSON object (RFC 7386 merge-patch delta)",
|
|
1671
1892
|
};
|
|
1672
1893
|
}
|
|
1894
|
+
const protectedViolations = frontendPlanPatchProtectedPathViolations(patch);
|
|
1895
|
+
if (protectedViolations.length > 0) {
|
|
1896
|
+
return {
|
|
1897
|
+
ok: false,
|
|
1898
|
+
reason: `frontend revision patch must not modify runtime-protected paths: ${protectedViolations.join(", ")}`,
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1673
1901
|
return { ok: true, contract: patch };
|
|
1674
1902
|
}
|
|
1675
1903
|
export async function materializeFrontendImplementationContract(input) {
|
|
@@ -33,7 +33,7 @@ import { buildBackendTestIntakeContext } from "./backend-test-intake-context.js"
|
|
|
33
33
|
import { buildFrontendTestOutcomeGateShellSnippet } from "./frontend-test-result-contract.js";
|
|
34
34
|
import { classifyFrontendRisk, } from "./frontend-risk.js";
|
|
35
35
|
import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
|
|
36
|
-
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
|
|
36
|
+
import { buildFrontendImplementationContractSkeleton, FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
|
|
37
37
|
import { serializeDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
38
38
|
const REQUIREMENT_FILE = "需求.md";
|
|
39
39
|
const CONSTRAINT_FILE = "执行约束.md";
|
|
@@ -2418,33 +2418,21 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2418
2418
|
complexity: taskConfig.complexity,
|
|
2419
2419
|
});
|
|
2420
2420
|
const frontendSourceBinding = buildDagSourceBinding(sources);
|
|
2421
|
+
const frontendContractSkeleton = buildFrontendImplementationContractSkeleton({
|
|
2422
|
+
sourceBinding: frontendSourceBinding,
|
|
2423
|
+
riskLevel: frontendRisk.selectedRisk,
|
|
2424
|
+
targetFiles: implementPaths.writeSet,
|
|
2425
|
+
});
|
|
2421
2426
|
const frontendContractSchemaBlock = (() => {
|
|
2422
2427
|
const schema = loadFrontendImplementationContractJsonSchema();
|
|
2423
|
-
const requirement = frontendSourceBinding.sources.find((source) => source.kind === "requirement");
|
|
2424
|
-
if (!requirement) {
|
|
2425
|
-
throw new Error("frontend implementation contract context requires a bound requirement source");
|
|
2426
|
-
}
|
|
2427
|
-
const referencePaths = frontendSourceBinding.sources
|
|
2428
|
-
.filter((s) => s.kind === "reference")
|
|
2429
|
-
.map((s) => s.path);
|
|
2430
|
-
const fixedFields = {
|
|
2431
|
-
schemaVersion: 1,
|
|
2432
|
-
sourceBinding: {
|
|
2433
|
-
taskId: frontendSourceBinding.taskId,
|
|
2434
|
-
requirementPath: requirement.path,
|
|
2435
|
-
requirementSha256: requirement.sha256,
|
|
2436
|
-
referencePaths,
|
|
2437
|
-
requirementIds: frontendSourceBinding.requirementIds,
|
|
2438
|
-
},
|
|
2439
|
-
riskLevel: frontendRisk.selectedRisk,
|
|
2440
|
-
targets: { files: implementPaths.writeSet },
|
|
2441
|
-
};
|
|
2442
2428
|
return [
|
|
2443
|
-
`## ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative
|
|
2429
|
+
`## Final ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative after runtime merge)`,
|
|
2444
2430
|
schema,
|
|
2445
2431
|
"",
|
|
2446
|
-
"##
|
|
2447
|
-
JSON.stringify(
|
|
2432
|
+
"## Runtime contract skeleton (deterministic and protected)",
|
|
2433
|
+
JSON.stringify(frontendContractSkeleton),
|
|
2434
|
+
"",
|
|
2435
|
+
"The initial planner emits an editable RFC 7386 patch against this skeleton. It MUST omit schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. The runtime merges and validates the final contract, then replaces the planner output with canonical full-contract JSON for downstream review.",
|
|
2448
2436
|
"",
|
|
2449
2437
|
"## Forbidden fields (these are NOT in the schema; do not emit)",
|
|
2450
2438
|
"- schemaId",
|
|
@@ -2796,20 +2784,22 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2796
2784
|
outputMode: "structured-required",
|
|
2797
2785
|
retryPolicy: STRUCTURED_REQUIRED_PI_RETRY_POLICY,
|
|
2798
2786
|
structuredContractOutput: {
|
|
2799
|
-
schemaId:
|
|
2787
|
+
schemaId: FRONTEND_IMPLEMENTATION_CONTRACT_PLAN_PATCH_SCHEMA_ID,
|
|
2800
2788
|
retryOnInvalid: true,
|
|
2789
|
+
skeleton: frontendContractSkeleton,
|
|
2801
2790
|
},
|
|
2802
2791
|
allowedPaths: readOnlyPaths,
|
|
2803
2792
|
forbiddenPaths,
|
|
2804
2793
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
2805
|
-
outputContract: "
|
|
2794
|
+
outputContract: "JSON-only patch output: one-line lead-in, then exactly ONE fenced json object (```json ... ```) containing only the editable RFC 7386 plan patch for the runtime contract skeleton. Omit protected fields: schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff. Immediately after it, append exactly one ```openspec-citations``` fenced citation block. The runtime applies the patch, validates it, and promotes canonical full-contract JSON for downstream review. Do NOT emit a full contract, Markdown plan explanation, raw JSON, or any other fenced block. No file writes.",
|
|
2806
2795
|
subtask_prompt: [
|
|
2807
|
-
"
|
|
2808
|
-
"
|
|
2796
|
+
"Use frontend-contract-pi, frontend-scout-pi, task sources, and the generation-time Mock capability evidence to fill the runtime-owned frontend contract skeleton. Return JSON-only output containing only an editable RFC 7386 plan patch. The runtime already owns schemaVersion, sourceBinding, riskLevel, targets.files, and mockApi.productionDefaultOff; omit those protected paths even when their values look obvious.",
|
|
2797
|
+
"The patch fields become the complete implementation plan after deterministic merge. Do not produce a separate plan document, prose mirror, or full contract.",
|
|
2798
|
+
"Select the Mock / API strategy only in the patch. Encode endpoint/fixture mapping, explicit activation, verification commands, and Real Integration Gap in schema-defined editable fields; productionDefaultOff comes from the protected skeleton and there is no second plan output.",
|
|
2809
2799
|
"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.",
|
|
2810
2800
|
"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.",
|
|
2811
2801
|
"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.",
|
|
2812
|
-
"Output in this exact order: (1) exactly one fenced json object
|
|
2802
|
+
"Output in this exact order: (1) exactly one fenced json object containing the editable plan patch; (2) exactly one openspec-citations citation fenced block appended immediately after it. Do NOT emit protected skeleton fields, a full contract, Markdown plan explanation, raw JSON, or any other fenced block.",
|
|
2813
2803
|
"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.",
|
|
2814
2804
|
requirementCoverageInstruction,
|
|
2815
2805
|
"verificationTargets[].commandLabel MUST be one of the frozen command labels listed above. Any other value will be rejected at contract materialization.",
|
|
@@ -2837,7 +2827,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2837
2827
|
outputProtocol: REVIEW_VERDICT_OUTPUT_PROTOCOL,
|
|
2838
2828
|
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.",
|
|
2839
2829
|
subtask_prompt: [
|
|
2840
|
-
"Audit the frontend plan before implementation.
|
|
2830
|
+
"Audit the frontend plan before implementation. frontend-plan-pi is emitted to you as canonical full-contract JSON after the runtime applied and validated the planner's editable patch against its protected skeleton; there is no separate plan prose.",
|
|
2841
2831
|
"First non-empty line must be exactly VERDICT: pass or VERDICT: request-revision.",
|
|
2842
2832
|
"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.",
|
|
2843
2833
|
"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.",
|
|
@@ -2870,7 +2860,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
2870
2860
|
subtask_prompt: [
|
|
2871
2861
|
"Consume frontend-plan-pi (original contract JSON) and frontend-design-review-pi (first design review findings).",
|
|
2872
2862
|
"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.",
|
|
2873
|
-
"The patch delta may update
|
|
2863
|
+
"The patch delta may update editable contract fields such as requirements, implementationSteps, targets.routes/publicApiChanges, uiStates, interactions, mockApi.strategy/activation/endpoints, dependencyPolicy, stylingStrategy, uiComponentChoices, verificationTargets, evidenceGaps, residualRisks, and realIntegrationGap. It must not modify protected schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff. 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.",
|
|
2874
2864
|
requirementCoverageInstruction,
|
|
2875
2865
|
"Do not turn MOCK_STRATEGY: blocked into an implementable strategy without new repository or contract evidence that resolves every blocker.",
|
|
2876
2866
|
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
@@ -129,6 +129,26 @@ export function buildNodePrompt(spec, task, upstream, options) {
|
|
|
129
129
|
convergenceFeedback: options?.convergenceFeedback,
|
|
130
130
|
});
|
|
131
131
|
}
|
|
132
|
+
function frontendStructuredArtifactRetryGuidance(schemaId) {
|
|
133
|
+
if (schemaId === "frontend-implementation-contract-plan-patch-v1") {
|
|
134
|
+
return [
|
|
135
|
+
"Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing only the editable RFC 7386 plan patch, immediately followed by exactly one fenced openspec-citations block.",
|
|
136
|
+
"The runtime applies your patch to its protected contract skeleton. Do not emit schemaVersion, sourceBinding, riskLevel, targets.files, or mockApi.productionDefaultOff; do not emit the full contract.",
|
|
137
|
+
"Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
if (schemaId === "frontend-implementation-contract-revision-patch-v1") {
|
|
141
|
+
return [
|
|
142
|
+
"Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block containing the RFC 7386 merge-patch delta, immediately followed by exactly one fenced openspec-citations block.",
|
|
143
|
+
"The delta contains only the fields you change; null deletes a key, arrays and scalars replace, and plain objects merge recursively. Do not emit a full contract or a schemaVersion/targets section.",
|
|
144
|
+
"Do not emit Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
|
|
145
|
+
];
|
|
146
|
+
}
|
|
147
|
+
return [
|
|
148
|
+
"Return exactly this artifact and nothing else: one short lead-in line, then exactly one fenced json block conforming to frontend-implementation-contract-v1, immediately followed by exactly one fenced openspec-citations block.",
|
|
149
|
+
"The contract JSON fields are the complete implementation plan. Do not emit a separate plan document, Markdown headings, bullets, plan prose, explanations, raw JSON, or any other fenced block.",
|
|
150
|
+
];
|
|
151
|
+
}
|
|
132
152
|
function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCategory, previousProtocolReason, recoveryTargetPaths, recoveryDiagnostics) {
|
|
133
153
|
if (attemptNumber <= 1)
|
|
134
154
|
return basePrompt;
|
|
@@ -144,27 +164,14 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
144
164
|
if (previousFailureCategory === "invalid-output" &&
|
|
145
165
|
task.structuredContractOutput &&
|
|
146
166
|
previousProtocolReason) {
|
|
147
|
-
// Revision nodes emit an RFC 7386 merge-patch delta (no schemaVersion /
|
|
148
|
-
// targets), so the retry instruction must not push them toward a full
|
|
149
|
-
// contract rewrite; plan nodes keep the strict v1 guidance.
|
|
150
|
-
const guidance = task.structuredContractOutput.schemaId ===
|
|
151
|
-
"frontend-implementation-contract-revision-patch-v1"
|
|
152
|
-
? [
|
|
153
|
-
"Return exactly one fenced json object containing the RFC 7386 merge-patch delta against the original contract (null deletes a key; arrays and scalars replace; plain objects merge recursively) + exactly one openspec-citations block.",
|
|
154
|
-
"The delta contains only the fields you change; do not emit a full contract or a schemaVersion/targets section.",
|
|
155
|
-
]
|
|
156
|
-
: [
|
|
157
|
-
"Return exactly one fenced json block conforming to the frontend-implementation-contract-v1 schema.",
|
|
158
|
-
"Fix every reported field violation: do not emit null for optional fields, do not misspell field names, and match the required types exactly.",
|
|
159
|
-
"The Markdown explanation may be omitted; prioritize a complete contract.",
|
|
160
|
-
];
|
|
161
167
|
return [
|
|
162
168
|
basePrompt,
|
|
163
169
|
"",
|
|
164
170
|
"<retry_instruction>",
|
|
165
171
|
"Previous attempt produced an invalid frontend implementation contract:",
|
|
166
172
|
previousProtocolReason,
|
|
167
|
-
...
|
|
173
|
+
...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
|
|
174
|
+
"Fix every reported field violation: do not emit null for optional fields, do not misspell field names, and match the required types exactly.",
|
|
168
175
|
"</retry_instruction>",
|
|
169
176
|
].join("\n");
|
|
170
177
|
}
|
|
@@ -175,9 +182,8 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
175
182
|
"",
|
|
176
183
|
"<retry_instruction>",
|
|
177
184
|
"Previous attempt was truncated by the provider (stopReason=length) before the JSON contract was completed.",
|
|
178
|
-
|
|
179
|
-
"
|
|
180
|
-
"The contract JSON must be complete; the trailing Markdown explanation may be omitted entirely.",
|
|
185
|
+
...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
|
|
186
|
+
"The JSON artifact must be complete; omit evidence excerpts and duplicated upstream context.",
|
|
181
187
|
"</retry_instruction>",
|
|
182
188
|
].join("\n");
|
|
183
189
|
}
|
|
@@ -246,6 +252,17 @@ function buildAttemptPrompt(task, basePrompt, attemptNumber, previousFailureCate
|
|
|
246
252
|
return basePrompt;
|
|
247
253
|
}
|
|
248
254
|
if (task.outputMode === "structured-required") {
|
|
255
|
+
if (task.structuredContractOutput) {
|
|
256
|
+
return [
|
|
257
|
+
basePrompt,
|
|
258
|
+
"",
|
|
259
|
+
"<retry_instruction>",
|
|
260
|
+
"Previous attempt exceeded the structured output size limit.",
|
|
261
|
+
...frontendStructuredArtifactRetryGuidance(task.structuredContractOutput.schemaId),
|
|
262
|
+
"Keep every required field but make values concise; omit evidence excerpts and duplicated upstream context.",
|
|
263
|
+
"</retry_instruction>",
|
|
264
|
+
].join("\n");
|
|
265
|
+
}
|
|
249
266
|
return [
|
|
250
267
|
basePrompt,
|
|
251
268
|
"",
|
|
@@ -792,6 +809,9 @@ export async function executeDagNode(input) {
|
|
|
792
809
|
runDir,
|
|
793
810
|
text: contractText,
|
|
794
811
|
sourceBinding: spec.sourceBinding,
|
|
812
|
+
nodeId,
|
|
813
|
+
attempt: attemptNumber,
|
|
814
|
+
structuredContractOutput: task.structuredContractOutput,
|
|
795
815
|
});
|
|
796
816
|
if (!contractCheck.ok) {
|
|
797
817
|
// A provider stopReason=length means the response was cut before the
|
|
@@ -811,6 +831,13 @@ export async function executeDagNode(input) {
|
|
|
811
831
|
previousProtocolReason = contractCheck.reason;
|
|
812
832
|
}
|
|
813
833
|
else {
|
|
834
|
+
if (contractCheck.normalizedText) {
|
|
835
|
+
result = {
|
|
836
|
+
...result,
|
|
837
|
+
assistantText: contractCheck.normalizedText,
|
|
838
|
+
stdout: contractCheck.normalizedText,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
814
841
|
previousProtocolReason = undefined;
|
|
815
842
|
}
|
|
816
843
|
}
|
|
@@ -713,6 +713,7 @@ export const dagConvergenceSpecSchema = z
|
|
|
713
713
|
*/
|
|
714
714
|
export const structuredContractOutputSchemaIds = [
|
|
715
715
|
"frontend-implementation-contract-v1",
|
|
716
|
+
"frontend-implementation-contract-plan-patch-v1",
|
|
716
717
|
"frontend-implementation-contract-revision-patch-v1",
|
|
717
718
|
];
|
|
718
719
|
export const dagTaskSchema = z.object({ id: z.string().regex(/^[a-z][a-z0-9-]*$/, "task id must be kebab-case"),
|
|
@@ -775,8 +776,25 @@ export const dagTaskSchema = z.object({ id: z.string().regex(/^[a-z][a-z0-9-]*$/
|
|
|
775
776
|
.object({
|
|
776
777
|
schemaId: z.enum(structuredContractOutputSchemaIds),
|
|
777
778
|
retryOnInvalid: z.boolean().default(true),
|
|
779
|
+
/**
|
|
780
|
+
* Optional deterministic base object for patch-producing structured
|
|
781
|
+
* nodes. The frontend plan patch validator applies the model delta to
|
|
782
|
+
* this run-owned skeleton before strict contract validation.
|
|
783
|
+
*/
|
|
784
|
+
skeleton: z.record(z.string(), z.unknown()).optional(),
|
|
778
785
|
})
|
|
779
786
|
.strict()
|
|
787
|
+
.superRefine((value, ctx) => {
|
|
788
|
+
if (value.schemaId ===
|
|
789
|
+
"frontend-implementation-contract-plan-patch-v1" &&
|
|
790
|
+
!value.skeleton) {
|
|
791
|
+
ctx.addIssue({
|
|
792
|
+
code: "custom",
|
|
793
|
+
message: "frontend plan patch output requires a runtime skeleton",
|
|
794
|
+
path: ["skeleton"],
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
})
|
|
780
798
|
.optional(),
|
|
781
799
|
/**
|
|
782
800
|
* Fail-closed outcome/diff consistency contract for bounded Pi writers.
|
package/package.json
CHANGED
|
@@ -6,14 +6,14 @@ 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, 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, UI states, reuse, deps, Mock/API
|
|
9
|
+
- **`frontend-plan-pi` + conditional design loop**: AC → observable `expectedOutcome`; interactions → `trigger`+`expectedBehavior`; include steps, in-bound files, UI states, reuse, deps, Mock/API, frozen verify entrypoints and integration gaps. DAG freezes a protected skeleton (`schemaVersion`, `sourceBinding`, `riskLevel`, `targets.files`, `mockApi.productionDefaultOff`). Model returns a **JSON-only** editable RFC 7386 patch (`frontend-implementation-contract-plan-patch-v1`) plus one `openspec-citations` block. Runtime compiles canonical `frontend-implementation-contract-v1` and archives raw/extracted/normalized/validation candidates; unresolved optional symbols are dropped and audited. No plan prose; gate renders plan.md. IDs+paths ≠ behavior semantics. Applicable UI states need behavior/implementation/verification; non-applicable states need reasons. Active Mock needs frozen commands; `auto` without confirmed capability/command → `not-needed`; `required` without command → generation-time blocked. Only `request-revision` runs revision+final review; small-risk gets one review.
|
|
10
10
|
- `uiComponentChoices[]` (optional): `{purpose, component, decision, specReference, rationale}`; `specified` needs `specReference{path,section,line}`; `new` needs null ref+rationale.
|
|
11
11
|
- **`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; malformed primary fails closed. Generation-time blocked Mock yields one blocking shell node, no writer.
|
|
12
12
|
- `verificationTarget.commandLabel` must be a **frozen command label** from `verifyEvidence.commandLabels`; others fail closed (`invalid-output`). Empty frozen set (no `run.json`) skips check.
|
|
13
13
|
- `mockApi.strategy !== "not-needed"` fails closed when `mockCommandLabels` empty (`no authorized Mock verification commands`).
|
|
14
14
|
- 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 read event → `openspec-citation-not-read` (anti-fabrication). Empty candidates skip enforcement. `scan-strict` keeps full-read semantics (`openspec-not-read`); both modes keep `candidate-missing-drift`.
|
|
15
15
|
- Component conformance: missing `uiComponentChoices` (frozen bucket non-empty + UI-visible work) → `component-choices-missing`; bad/uncited `specReference.path` → `component-spec-reference-invalid` / `component-spec-not-cited`.
|
|
16
|
-
- revisionPatch mode: revision output is an RFC 7386 merge-patch delta applied on the original contract node (`planFallbackFromNodeIds[0]`);
|
|
16
|
+
- revisionPatch mode: revision output is an RFC 7386 merge-patch delta applied on the original canonical contract node (`planFallbackFromNodeIds[0]`); protected skeleton paths remain immutable. Legacy DAGs keep full-contract semantics. Size guard: canonical serialization >64KB → `retryable-invalid` + `contract-too-large`.
|
|
17
17
|
- **`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 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.
|
|
18
18
|
|
|
19
19
|
## Contract / trace / stages (M1–M2)
|