@tea-agent/loop-agent 0.16.4 → 0.16.6-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 +9 -0
- package/dist/executors/dag-pi-executor.js +44 -4
- package/dist/executors/pi-sdk-executor.js +1 -3
- package/dist/worker/observe/spec-evidence.js +1 -2
- package/dist/workflows/dag/backend-test-analysis-contract.js +309 -17
- package/dist/workflows/dag/backend-test-execution-contract.js +186 -9
- package/dist/workflows/dag/frontend-implementation-contract.js +77 -0
- package/dist/workflows/dag/frontend-project-capability.js +7 -7
- package/dist/workflows/dag/frontend-risk.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +48 -4
- package/dist/workflows/dag/types.js +1 -0
- package/docs/templates/agent-dag.schema.json +5 -0
- package/package.json +1 -1
- package/skills/frontend-design-review/SKILL.md +4 -4
- package/skills/frontend-design-review/references/review-checklist.md +3 -3
- package/skills/frontend-implementation/SKILL.md +1 -1
- package/skills/frontend-implementation/references/code-standards.md +1 -1
- package/skills/frontend-implementation/references/design-spec.md +9 -9
- package/skills/frontend-implementation/references/node-contracts.md +5 -5
- package/skills/frontend-review/SKILL.md +1 -1
- package/skills/frontend-review/references/review-findings.md +2 -2
- package/skills/frontend-verification/SKILL.md +2 -2
- package/skills/frontend-verification/references/verification-checklist.md +2 -2
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -9,12 +9,21 @@
|
|
|
9
9
|
|
|
10
10
|
### 修复
|
|
11
11
|
|
|
12
|
+
- 前端实现计划/修订节点会注入当前包内权威 `frontend-implementation-contract-v1` Schema 与固定 source binding,避免模型猜测字段导致契约门禁失败。
|
|
13
|
+
- 前端 Mock 策略节点的 canonical 输出会把首条 `MOCK_STRATEGY:` 协议行提升为第一行,避免解释性前言触发 `first-non-empty` 门禁误判。
|
|
14
|
+
- 前端规范回退目录统一为本地 `openspec/`,DAG 能力发现、提示词、Skill 与验证证据检查不再查找大小写不一致的旧目录名。
|
|
12
15
|
- 后端测试复合 Shell pipeline 现在与普通 Shell 节点共享 Git write guard;即使命令退出成功,只要越过 `read-only`、`allowedPaths` 或 `forbiddenPaths` 边界,节点仍会 fail-closed。
|
|
13
16
|
- Observe 现使用实际的 repair safety 节点,并只在修复节点真正开始执行后计入一次 attempt;条件跳过不再误报已修复,安全门禁失败会显示为 `rejected`。
|
|
14
17
|
- Pi SDK 对缺少响应 ID 的累计 Token 生命周期事件改为取本次执行最大快照,避免同一响应的匿名 usage 被重复累加。
|
|
15
18
|
- Pi SDK 执行长推理或大段结构化输出时不再把高频流式增量事件无界累积到内存;同一响应在多个生命周期事件中重复出现的 Token 用量只统计一次,避免 `Invalid string length` 和成本数据虚高。
|
|
16
19
|
- 后端测试复合执行节点继续保持 clean environment、失败分类和 fail-closed outcome,并为 initial/final Result、repair eligibility、traceability 与 Observe 投影保留结构化运行证据。
|
|
17
20
|
|
|
21
|
+
## [0.16.5] - 2026-07-20
|
|
22
|
+
|
|
23
|
+
### 修复
|
|
24
|
+
|
|
25
|
+
- 后端测试分析/执行合同物化支持 free-form 环境侦察与 GWT/嵌套 endpoint 形状的严格 schema 归一化,避免 BE-TEST 在 contracts 门被模型字段漂移误拦。
|
|
26
|
+
|
|
18
27
|
## [0.16.4] - 2026-07-19
|
|
19
28
|
|
|
20
29
|
### 修复
|
|
@@ -272,7 +272,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
272
272
|
persona,
|
|
273
273
|
step,
|
|
274
274
|
});
|
|
275
|
-
const mapped = mapPiResultToDagNodeResult(result);
|
|
275
|
+
const mapped = mapPiResultToDagNodeResult(result, input.task.firstProtocolLine);
|
|
276
276
|
if (!isWriteTask) {
|
|
277
277
|
return mapped;
|
|
278
278
|
}
|
|
@@ -315,17 +315,57 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep) {
|
|
|
315
315
|
durationMs: mapped.durationMs || Date.now() - started,
|
|
316
316
|
};
|
|
317
317
|
}
|
|
318
|
-
export function mapPiResultToDagNodeResult(result) {
|
|
318
|
+
export function mapPiResultToDagNodeResult(result, firstProtocolLine) {
|
|
319
|
+
const assistantText = canonicalizeProtocolFirstLine(result.assistantText, firstProtocolLine);
|
|
319
320
|
return {
|
|
320
321
|
ok: result.ok,
|
|
321
|
-
stdout:
|
|
322
|
+
stdout: assistantText || result.stdout,
|
|
322
323
|
stderr: result.stderr,
|
|
323
324
|
failureCategory: result.failureCategory,
|
|
324
325
|
durationMs: result.durationMs,
|
|
325
|
-
assistantText
|
|
326
|
+
assistantText,
|
|
326
327
|
backend: result.backend,
|
|
327
328
|
sdkAttempted: result.sdkAttempted,
|
|
328
329
|
tokensUsed: result.tokensUsed,
|
|
329
330
|
parsedEvents: result.parsedEvents,
|
|
330
331
|
};
|
|
331
332
|
}
|
|
333
|
+
function canonicalizeProtocolFirstLine(assistantText, firstProtocolLine) {
|
|
334
|
+
if (!assistantText || !firstProtocolLine)
|
|
335
|
+
return assistantText;
|
|
336
|
+
const lines = assistantText.split(/\r?\n/);
|
|
337
|
+
let protocolIndex = -1;
|
|
338
|
+
let protocolLine = "";
|
|
339
|
+
for (const [index, line] of lines.entries()) {
|
|
340
|
+
const normalized = normalizeProtocolLine(line);
|
|
341
|
+
if (normalized.startsWith(firstProtocolLine)) {
|
|
342
|
+
protocolIndex = index;
|
|
343
|
+
protocolLine = normalized;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (protocolIndex < 0)
|
|
348
|
+
return assistantText;
|
|
349
|
+
if (protocolIndex === 0) {
|
|
350
|
+
return [protocolLine, ...lines.slice(1)].join("\n");
|
|
351
|
+
}
|
|
352
|
+
const before = lines.slice(0, protocolIndex);
|
|
353
|
+
const after = lines.slice(protocolIndex + 1);
|
|
354
|
+
while (before.at(-1)?.trim() === "" &&
|
|
355
|
+
after.at(0)?.trim() === "") {
|
|
356
|
+
after.shift();
|
|
357
|
+
}
|
|
358
|
+
const bodyLines = [...before, ...after];
|
|
359
|
+
while (bodyLines.at(0)?.trim() === "")
|
|
360
|
+
bodyLines.shift();
|
|
361
|
+
while (bodyLines.at(-1)?.trim() === "")
|
|
362
|
+
bodyLines.pop();
|
|
363
|
+
return bodyLines.length > 0
|
|
364
|
+
? `${protocolLine}\n\n${bodyLines.join("\n")}`
|
|
365
|
+
: protocolLine;
|
|
366
|
+
}
|
|
367
|
+
function normalizeProtocolLine(line) {
|
|
368
|
+
const trimmed = line.trim();
|
|
369
|
+
const emphasized = trimmed.match(/^(\*{1,3})\s*(.*?)\s*\1$/);
|
|
370
|
+
return (emphasized?.[2] ?? trimmed).trim();
|
|
371
|
+
}
|
|
@@ -308,9 +308,7 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
308
308
|
return;
|
|
309
309
|
const line = serializeSessionEvent(event);
|
|
310
310
|
stdoutLines.push(line);
|
|
311
|
-
|
|
312
|
-
sessionEventAppender.append(line, event);
|
|
313
|
-
}
|
|
311
|
+
sessionEventAppender?.append(line, event);
|
|
314
312
|
});
|
|
315
313
|
const filePrefix = options.attachedFiles.map((file) => `@${file}`).join(' ');
|
|
316
314
|
const promptMessage = filePrefix
|
|
@@ -18,7 +18,7 @@ const KB_CONNECTOR_TOOLS = new Set([
|
|
|
18
18
|
]);
|
|
19
19
|
/**
|
|
20
20
|
* Pattern for detecting spec-related files:
|
|
21
|
-
* -
|
|
21
|
+
* - openspec/** files
|
|
22
22
|
* - *.spec.md / *.spec.ts / *.spec.tsx
|
|
23
23
|
* - project-specs/**
|
|
24
24
|
* - design-spec.md, code-standards.md, review-checklist.md, etc.
|
|
@@ -27,7 +27,6 @@ const KB_CONNECTOR_TOOLS = new Set([
|
|
|
27
27
|
*/
|
|
28
28
|
const SPEC_FILE_PATTERNS = [
|
|
29
29
|
/openspec\//i,
|
|
30
|
-
/\/openSpec\//i,
|
|
31
30
|
/\/project-specs\//i,
|
|
32
31
|
/\/spec\//i,
|
|
33
32
|
/\.spec\.(md|tsx?|jsx?)$/i,
|
|
@@ -108,25 +108,317 @@ export function extractStrictJsonObject(text) {
|
|
|
108
108
|
}
|
|
109
109
|
return JSON.parse(blocks[0][1]);
|
|
110
110
|
}
|
|
111
|
+
function asRecord(value) {
|
|
112
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
113
|
+
? value
|
|
114
|
+
: null;
|
|
115
|
+
}
|
|
116
|
+
function asArray(value) {
|
|
117
|
+
return Array.isArray(value) ? value : [];
|
|
118
|
+
}
|
|
119
|
+
function firstSourceRef(value, fallback = "source/需求.md") {
|
|
120
|
+
const record = asRecord(value);
|
|
121
|
+
if (!record)
|
|
122
|
+
return fallback;
|
|
123
|
+
if (typeof record.sourceRef === "string" && record.sourceRef.trim())
|
|
124
|
+
return record.sourceRef.trim();
|
|
125
|
+
const refs = asArray(record.sourceRefs).filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
126
|
+
return refs[0]?.trim() || fallback;
|
|
127
|
+
}
|
|
128
|
+
function coerceField(value) {
|
|
129
|
+
const record = asRecord(value);
|
|
130
|
+
if (!record || typeof record.name !== "string" || !record.name.trim())
|
|
131
|
+
return null;
|
|
132
|
+
const sourceRefs = asArray(record.sourceRefs).filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
133
|
+
const field = {
|
|
134
|
+
name: record.name.trim(),
|
|
135
|
+
sourceRefs,
|
|
136
|
+
};
|
|
137
|
+
if (typeof record.type === "string" && record.type.trim())
|
|
138
|
+
field.type = record.type.trim();
|
|
139
|
+
if (typeof record.required === "boolean")
|
|
140
|
+
field.required = record.required;
|
|
141
|
+
if (typeof record.description === "string")
|
|
142
|
+
field.description = record.description;
|
|
143
|
+
else if (typeof record.notes === "string")
|
|
144
|
+
field.description = record.notes;
|
|
145
|
+
if (typeof record.format === "string" && record.format.trim())
|
|
146
|
+
field.format = record.format.trim();
|
|
147
|
+
if (record.comparison === "exact" || record.comparison === "parseable-only" || record.comparison === "semantic") {
|
|
148
|
+
field.comparison = record.comparison;
|
|
149
|
+
}
|
|
150
|
+
if (typeof record.precision === "string" && record.precision.trim())
|
|
151
|
+
field.precision = record.precision.trim();
|
|
152
|
+
return field;
|
|
153
|
+
}
|
|
154
|
+
function coerceAcceptanceCriterion(value) {
|
|
155
|
+
const record = asRecord(value);
|
|
156
|
+
if (!record || typeof record.id !== "string" || !record.id.trim())
|
|
157
|
+
return null;
|
|
158
|
+
const textParts = [];
|
|
159
|
+
if (typeof record.text === "string" && record.text.trim())
|
|
160
|
+
textParts.push(record.text.trim());
|
|
161
|
+
else {
|
|
162
|
+
if (typeof record.title === "string" && record.title.trim())
|
|
163
|
+
textParts.push(record.title.trim());
|
|
164
|
+
const gwt = ["given", "when", "then"]
|
|
165
|
+
.map((key) => (typeof record[key] === "string" && record[key].trim() ? `${key}: ${String(record[key]).trim()}` : ""))
|
|
166
|
+
.filter(Boolean);
|
|
167
|
+
if (gwt.length)
|
|
168
|
+
textParts.push(gwt.join("; "));
|
|
169
|
+
else if (typeof record.description === "string" && record.description.trim())
|
|
170
|
+
textParts.push(record.description.trim());
|
|
171
|
+
}
|
|
172
|
+
const text = textParts.join(" — ").trim();
|
|
173
|
+
if (!text)
|
|
174
|
+
return null;
|
|
175
|
+
return { id: record.id.trim(), text, sourceRef: firstSourceRef(record) };
|
|
176
|
+
}
|
|
177
|
+
function coerceEndpoint(value) {
|
|
178
|
+
const record = asRecord(value);
|
|
179
|
+
if (!record || typeof record.id !== "string" || !record.id.trim())
|
|
180
|
+
return null;
|
|
181
|
+
if (typeof record.method !== "string" || typeof record.path !== "string")
|
|
182
|
+
return null;
|
|
183
|
+
const method = record.method.trim().toUpperCase();
|
|
184
|
+
const pathValue = record.path.trim();
|
|
185
|
+
if (!pathValue.startsWith("/"))
|
|
186
|
+
return null;
|
|
187
|
+
const request = asRecord(record.request);
|
|
188
|
+
const successResponse = asRecord(record.successResponse);
|
|
189
|
+
const nestedBody = asRecord(successResponse?.responseBody) ?? asRecord(record.responseBody);
|
|
190
|
+
const requestFields = [
|
|
191
|
+
...asArray(record.requestFields),
|
|
192
|
+
...asArray(request?.headers),
|
|
193
|
+
...asArray(request?.query),
|
|
194
|
+
...asArray(request?.pathParams),
|
|
195
|
+
]
|
|
196
|
+
.map(coerceField)
|
|
197
|
+
.filter((item) => item !== null);
|
|
198
|
+
const responseFields = [
|
|
199
|
+
...asArray(record.responseFields),
|
|
200
|
+
...asArray(successResponse?.fields),
|
|
201
|
+
]
|
|
202
|
+
.map(coerceField)
|
|
203
|
+
.filter((item) => item !== null);
|
|
204
|
+
const successStatuses = asArray(record.successStatuses)
|
|
205
|
+
.map((item) => (typeof item === "number" ? item : Number.NaN))
|
|
206
|
+
.filter((item) => Number.isInteger(item) && item >= 100 && item <= 399);
|
|
207
|
+
if (typeof successResponse?.status === "number" && successResponse.status >= 100 && successResponse.status <= 399) {
|
|
208
|
+
successStatuses.push(successResponse.status);
|
|
209
|
+
}
|
|
210
|
+
const uniqueSuccess = [...new Set(successStatuses)];
|
|
211
|
+
const errorCases = [
|
|
212
|
+
...asArray(record.errorCases),
|
|
213
|
+
...asArray(record.errorResponses),
|
|
214
|
+
]
|
|
215
|
+
.map((item) => {
|
|
216
|
+
const err = asRecord(item);
|
|
217
|
+
if (!err)
|
|
218
|
+
return null;
|
|
219
|
+
const description = (typeof err.description === "string" && err.description.trim()) ||
|
|
220
|
+
(typeof err.message === "string" && err.message.trim()) ||
|
|
221
|
+
(typeof err.code === "string" && err.code.trim()) ||
|
|
222
|
+
(typeof err.status === "number" ? `HTTP ${err.status}` : "");
|
|
223
|
+
if (!description)
|
|
224
|
+
return null;
|
|
225
|
+
const out = { description };
|
|
226
|
+
if (typeof err.status === "number" && err.status >= 400 && err.status <= 599)
|
|
227
|
+
out.status = err.status;
|
|
228
|
+
if (typeof err.code === "string" && err.code.trim())
|
|
229
|
+
out.code = err.code.trim();
|
|
230
|
+
if (typeof err.messageField === "string" && err.messageField.trim())
|
|
231
|
+
out.messageField = err.messageField.trim();
|
|
232
|
+
return out;
|
|
233
|
+
})
|
|
234
|
+
.filter((item) => item !== null);
|
|
235
|
+
const kind = nestedBody && typeof nestedBody.kind === "string" && ["array", "object", "scalar", "empty", "unknown"].includes(nestedBody.kind)
|
|
236
|
+
? nestedBody.kind
|
|
237
|
+
: "unknown";
|
|
238
|
+
const ordering = nestedBody && typeof nestedBody.ordering === "string" && ["specified", "unspecified", "not-applicable"].includes(nestedBody.ordering)
|
|
239
|
+
? nestedBody.ordering
|
|
240
|
+
: "unspecified";
|
|
241
|
+
const responseBody = { kind, ordering };
|
|
242
|
+
if (nestedBody && typeof nestedBody.itemSchemaRef === "string" && nestedBody.itemSchemaRef.trim()) {
|
|
243
|
+
responseBody.itemSchemaRef = nestedBody.itemSchemaRef.trim();
|
|
244
|
+
}
|
|
245
|
+
if (nestedBody && typeof nestedBody.description === "string" && nestedBody.description.trim()) {
|
|
246
|
+
responseBody.description = nestedBody.description.trim();
|
|
247
|
+
}
|
|
248
|
+
else if (nestedBody && typeof nestedBody.schemaRef === "string" && nestedBody.schemaRef.trim()) {
|
|
249
|
+
responseBody.description = `schemaRef=${nestedBody.schemaRef.trim()}`;
|
|
250
|
+
}
|
|
251
|
+
const sourceRefs = [
|
|
252
|
+
...asArray(record.sourceRefs),
|
|
253
|
+
...asArray(successResponse?.sourceRefs),
|
|
254
|
+
].filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
255
|
+
return {
|
|
256
|
+
id: record.id.trim(),
|
|
257
|
+
method,
|
|
258
|
+
path: pathValue,
|
|
259
|
+
requestFields,
|
|
260
|
+
responseFields,
|
|
261
|
+
responseBody,
|
|
262
|
+
successStatuses: uniqueSuccess.length ? uniqueSuccess : [200],
|
|
263
|
+
errorCases,
|
|
264
|
+
sourceRefs,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function coerceEvidencedIdItem(value, textKey) {
|
|
268
|
+
const record = asRecord(value);
|
|
269
|
+
if (!record)
|
|
270
|
+
return null;
|
|
271
|
+
const id = (typeof record.id === "string" && record.id.trim()) ||
|
|
272
|
+
(typeof record.name === "string" && record.name.trim()) ||
|
|
273
|
+
"";
|
|
274
|
+
if (!id)
|
|
275
|
+
return null;
|
|
276
|
+
const text = (typeof record[textKey] === "string" && String(record[textKey]).trim()) ||
|
|
277
|
+
(typeof record.description === "string" && record.description.trim()) ||
|
|
278
|
+
(typeof record.text === "string" && record.text.trim()) ||
|
|
279
|
+
(typeof record.notes === "string" && record.notes.trim()) ||
|
|
280
|
+
(typeof record.title === "string" && record.title.trim()) ||
|
|
281
|
+
"";
|
|
282
|
+
if (!text)
|
|
283
|
+
return null;
|
|
284
|
+
return { id, [textKey]: text, sourceRef: firstSourceRef(record) };
|
|
285
|
+
}
|
|
286
|
+
function coerceBoundary(value) {
|
|
287
|
+
const record = asRecord(value);
|
|
288
|
+
if (!record)
|
|
289
|
+
return null;
|
|
290
|
+
const field = (typeof record.field === "string" && record.field.trim()) ||
|
|
291
|
+
(typeof record.id === "string" && record.id.trim()) ||
|
|
292
|
+
(typeof record.name === "string" && record.name.trim()) ||
|
|
293
|
+
"";
|
|
294
|
+
const constraint = (typeof record.constraint === "string" && record.constraint.trim()) ||
|
|
295
|
+
(typeof record.description === "string" && record.description.trim()) ||
|
|
296
|
+
(typeof record.text === "string" && record.text.trim()) ||
|
|
297
|
+
"";
|
|
298
|
+
if (!field || !constraint)
|
|
299
|
+
return null;
|
|
300
|
+
return { field, constraint, sourceRef: firstSourceRef(record) };
|
|
301
|
+
}
|
|
302
|
+
const OPTIONAL_EVIDENCE_KNOWN_KEYS = new Set([
|
|
303
|
+
"name",
|
|
304
|
+
"id",
|
|
305
|
+
"description",
|
|
306
|
+
"notes",
|
|
307
|
+
"text",
|
|
308
|
+
"title",
|
|
309
|
+
"sourceRef",
|
|
310
|
+
"sourceRefs",
|
|
311
|
+
"severity",
|
|
312
|
+
"mitigation",
|
|
313
|
+
"level",
|
|
314
|
+
"impact",
|
|
315
|
+
"kind",
|
|
316
|
+
"required",
|
|
317
|
+
]);
|
|
318
|
+
function coerceOptionalEvidence(value) {
|
|
319
|
+
const record = asRecord(value);
|
|
320
|
+
if (!record)
|
|
321
|
+
return null;
|
|
322
|
+
// Fail closed on unknown keys so near-schema payloads cannot strip extras and pass.
|
|
323
|
+
for (const key of Object.keys(record)) {
|
|
324
|
+
if (!OPTIONAL_EVIDENCE_KNOWN_KEYS.has(key)) {
|
|
325
|
+
throw new Error(`optional evidence has unrecognized key: ${key}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const description = (typeof record.description === "string" && record.description.trim()) ||
|
|
329
|
+
(typeof record.notes === "string" && record.notes.trim()) ||
|
|
330
|
+
(typeof record.text === "string" && record.text.trim()) ||
|
|
331
|
+
(typeof record.title === "string" && record.title.trim()) ||
|
|
332
|
+
"";
|
|
333
|
+
if (!description)
|
|
334
|
+
return null;
|
|
335
|
+
const out = { description };
|
|
336
|
+
const name = (typeof record.name === "string" && record.name.trim()) ||
|
|
337
|
+
(typeof record.id === "string" && record.id.trim()) ||
|
|
338
|
+
"";
|
|
339
|
+
if (name)
|
|
340
|
+
out.name = name;
|
|
341
|
+
const sourceRef = firstSourceRef(record, "");
|
|
342
|
+
if (sourceRef)
|
|
343
|
+
out.sourceRef = sourceRef;
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
function coerceEvidenceGap(value) {
|
|
347
|
+
const record = asRecord(value);
|
|
348
|
+
if (!record)
|
|
349
|
+
return null;
|
|
350
|
+
const description = (typeof record.description === "string" && record.description.trim()) ||
|
|
351
|
+
(typeof record.text === "string" && record.text.trim()) ||
|
|
352
|
+
"";
|
|
353
|
+
if (!description)
|
|
354
|
+
return null;
|
|
355
|
+
const out = { description };
|
|
356
|
+
if (typeof record.requirementId === "string" && record.requirementId.trim())
|
|
357
|
+
out.requirementId = record.requirementId.trim();
|
|
358
|
+
else if (typeof record.acId === "string" && record.acId.trim())
|
|
359
|
+
out.requirementId = record.acId.trim();
|
|
360
|
+
const sourceRef = firstSourceRef(record, "");
|
|
361
|
+
if (sourceRef)
|
|
362
|
+
out.sourceRef = sourceRef;
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
/** Coerce common free-form model shapes into Backend Test Analysis v2 before strict parse. */
|
|
366
|
+
export function coerceBackendTestAnalysisInput(value) {
|
|
367
|
+
const record = asRecord(value);
|
|
368
|
+
if (!record)
|
|
369
|
+
return value;
|
|
370
|
+
const next = { ...record, schemaVersion: 2 };
|
|
371
|
+
next.acceptanceCriteria = asArray(record.acceptanceCriteria)
|
|
372
|
+
.map(coerceAcceptanceCriterion)
|
|
373
|
+
.filter((item) => item !== null);
|
|
374
|
+
next.endpoints = asArray(record.endpoints)
|
|
375
|
+
.map(coerceEndpoint)
|
|
376
|
+
.filter((item) => item !== null);
|
|
377
|
+
next.dataModels = asArray(record.dataModels)
|
|
378
|
+
.map((item) => coerceEvidencedIdItem(item, "description"))
|
|
379
|
+
.filter((item) => item !== null);
|
|
380
|
+
next.businessRules = asArray(record.businessRules)
|
|
381
|
+
.map((item) => coerceEvidencedIdItem(item, "text"))
|
|
382
|
+
.filter((item) => item !== null);
|
|
383
|
+
next.stateTransitions = asArray(record.stateTransitions);
|
|
384
|
+
next.boundaryConstraints = asArray(record.boundaryConstraints)
|
|
385
|
+
.map(coerceBoundary)
|
|
386
|
+
.filter((item) => item !== null);
|
|
387
|
+
next.externalDependencies = asArray(record.externalDependencies)
|
|
388
|
+
.map(coerceOptionalEvidence)
|
|
389
|
+
.filter((item) => item !== null);
|
|
390
|
+
next.risks = asArray(record.risks)
|
|
391
|
+
.map(coerceOptionalEvidence)
|
|
392
|
+
.filter((item) => item !== null);
|
|
393
|
+
next.evidenceGaps = asArray(record.evidenceGaps)
|
|
394
|
+
.map(coerceEvidenceGap)
|
|
395
|
+
.filter((item) => item !== null);
|
|
396
|
+
return next;
|
|
397
|
+
}
|
|
111
398
|
function normalizeAnalysis(value) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
399
|
+
const candidates = [value, coerceBackendTestAnalysisInput(value)];
|
|
400
|
+
let lastError = "invalid analysis contract";
|
|
401
|
+
for (const candidate of candidates) {
|
|
402
|
+
const v2 = backendTestAnalysisContractSchema.safeParse(candidate);
|
|
403
|
+
if (v2.success)
|
|
404
|
+
return v2.data;
|
|
405
|
+
lastError = v2.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
|
|
406
|
+
const v1 = analysisV1Schema.safeParse(candidate);
|
|
407
|
+
if (v1.success) {
|
|
408
|
+
return backendTestAnalysisContractSchema.parse({
|
|
409
|
+
...v1.data,
|
|
410
|
+
schemaVersion: 2,
|
|
411
|
+
endpoints: v1.data.endpoints.map((endpoint) => ({
|
|
412
|
+
...endpoint,
|
|
413
|
+
requestFields: endpoint.requestFields.map((field) => ({ ...field, sourceRefs: [] })),
|
|
414
|
+
responseFields: endpoint.responseFields.map((field) => ({ ...field, sourceRefs: [] })),
|
|
415
|
+
responseBody: { kind: "unknown", ordering: "unspecified" },
|
|
416
|
+
sourceRefs: [],
|
|
417
|
+
})),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
118
420
|
}
|
|
119
|
-
|
|
120
|
-
...v1.data,
|
|
121
|
-
schemaVersion: 2,
|
|
122
|
-
endpoints: v1.data.endpoints.map((endpoint) => ({
|
|
123
|
-
...endpoint,
|
|
124
|
-
requestFields: endpoint.requestFields.map((field) => ({ ...field, sourceRefs: [] })),
|
|
125
|
-
responseFields: endpoint.responseFields.map((field) => ({ ...field, sourceRefs: [] })),
|
|
126
|
-
responseBody: { kind: "unknown", ordering: "unspecified" },
|
|
127
|
-
sourceRefs: [],
|
|
128
|
-
})),
|
|
129
|
-
});
|
|
421
|
+
throw new Error(lastError);
|
|
130
422
|
}
|
|
131
423
|
function assertSourceBinding(contract, binding) {
|
|
132
424
|
const requirement = binding.sources.find((source) => source.kind === "requirement");
|
|
@@ -324,6 +324,171 @@ export function assertBackendTestExecutionPreflight(input) {
|
|
|
324
324
|
workingDirectory,
|
|
325
325
|
};
|
|
326
326
|
}
|
|
327
|
+
function asRecord(value) {
|
|
328
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
329
|
+
? value
|
|
330
|
+
: null;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Coerce free-form environment scout JSON into Backend Test Execution Contract v1.
|
|
334
|
+
* Prefer exact schema payloads; otherwise map common discovery shapes onto the
|
|
335
|
+
* pytest-centric runtime contract without inventing secrets or managed commands.
|
|
336
|
+
*/
|
|
337
|
+
export function coerceBackendTestExecutionInput(value) {
|
|
338
|
+
const direct = backendTestExecutionContractSchema.safeParse(value);
|
|
339
|
+
if (direct.success)
|
|
340
|
+
return direct.data;
|
|
341
|
+
const record = asRecord(value);
|
|
342
|
+
if (!record)
|
|
343
|
+
return value;
|
|
344
|
+
// Near-schema payloads (string framework + runner + testRoot) must stay fail-closed.
|
|
345
|
+
// Only free-form discovery envelopes are rewritten onto the pytest contract.
|
|
346
|
+
const looksSchemaShaped = typeof record.framework === "string" &&
|
|
347
|
+
asRecord(record.runner) !== null &&
|
|
348
|
+
typeof record.testRoot === "string";
|
|
349
|
+
if (looksSchemaShaped)
|
|
350
|
+
return value;
|
|
351
|
+
if (typeof record.framework === "string" && record.framework !== "pytest") {
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
const frameworkObj = asRecord(record.framework);
|
|
355
|
+
const discovered = asRecord(record.discoveredFixtures);
|
|
356
|
+
const verification = asRecord(record.verification);
|
|
357
|
+
const layout = asRecord(record.repositoryLayout);
|
|
358
|
+
const environment = asRecord(record.environment);
|
|
359
|
+
const commandHints = [];
|
|
360
|
+
if (typeof record.primaryCommand === "string" && record.primaryCommand.trim()) {
|
|
361
|
+
commandHints.push(record.primaryCommand.trim());
|
|
362
|
+
}
|
|
363
|
+
if (frameworkObj && typeof frameworkObj.primaryCommand === "string" && frameworkObj.primaryCommand.trim()) {
|
|
364
|
+
commandHints.push(frameworkObj.primaryCommand.trim());
|
|
365
|
+
}
|
|
366
|
+
for (const item of Array.isArray(verification?.commands) ? verification.commands : []) {
|
|
367
|
+
if (typeof item === "string" && item.trim())
|
|
368
|
+
commandHints.push(item.trim());
|
|
369
|
+
}
|
|
370
|
+
if (commandHints.length === 0) {
|
|
371
|
+
commandHints.push(`python -m pytest ${BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT}/ -v`);
|
|
372
|
+
}
|
|
373
|
+
const existingFixtures = [];
|
|
374
|
+
for (const item of Array.isArray(record.existingFixtures) ? record.existingFixtures : []) {
|
|
375
|
+
const fixture = asRecord(item);
|
|
376
|
+
if (!fixture)
|
|
377
|
+
continue;
|
|
378
|
+
if (typeof fixture.name === "string" && typeof fixture.sourcePath === "string" && typeof fixture.kind === "string") {
|
|
379
|
+
existingFixtures.push({
|
|
380
|
+
name: fixture.name,
|
|
381
|
+
sourcePath: fixture.sourcePath,
|
|
382
|
+
kind: fixture.kind,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (existingFixtures.length === 0 && discovered) {
|
|
387
|
+
const bootstrap = asRecord(discovered.serverBootstrap);
|
|
388
|
+
if (bootstrap && typeof bootstrap.module === "string" && bootstrap.module.trim()) {
|
|
389
|
+
existingFixtures.push({
|
|
390
|
+
name: typeof bootstrap.symbol === "string" && bootstrap.symbol.trim()
|
|
391
|
+
? bootstrap.symbol.trim()
|
|
392
|
+
: "server-bootstrap",
|
|
393
|
+
sourcePath: bootstrap.module.trim(),
|
|
394
|
+
kind: "server-bootstrap",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
for (const key of ["auth", "database", "remoteServices"]) {
|
|
398
|
+
const val = discovered[key];
|
|
399
|
+
if (typeof val === "string" && val.trim() && val.trim() !== "none") {
|
|
400
|
+
existingFixtures.push({
|
|
401
|
+
name: key,
|
|
402
|
+
sourcePath: ".",
|
|
403
|
+
kind: val.trim(),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (existingFixtures.length === 0) {
|
|
409
|
+
// Fail-closed in-process contract still needs one fixture entry; use default test root as a non-secret anchor.
|
|
410
|
+
existingFixtures.push({
|
|
411
|
+
name: "pytest-test-root",
|
|
412
|
+
sourcePath: BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT,
|
|
413
|
+
kind: "test-root",
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
const authenticationMode = (typeof record.authenticationMode === "string" && record.authenticationMode.trim()) ||
|
|
417
|
+
(typeof discovered?.auth === "string" && discovered.auth.trim()) ||
|
|
418
|
+
"none";
|
|
419
|
+
const evidenceRefs = [];
|
|
420
|
+
for (const item of Array.isArray(record.evidenceRefs) ? record.evidenceRefs : []) {
|
|
421
|
+
if (typeof item === "string" && item.trim())
|
|
422
|
+
evidenceRefs.push(item.trim());
|
|
423
|
+
}
|
|
424
|
+
if (layout) {
|
|
425
|
+
for (const key of [
|
|
426
|
+
"apiContractDoc",
|
|
427
|
+
"routeImplementation",
|
|
428
|
+
"serviceImplementation",
|
|
429
|
+
"serverEntry",
|
|
430
|
+
]) {
|
|
431
|
+
const val = layout[key];
|
|
432
|
+
if (typeof val === "string" && val.trim())
|
|
433
|
+
evidenceRefs.push(val.trim());
|
|
434
|
+
}
|
|
435
|
+
for (const item of Array.isArray(layout.existingApiTests) ? layout.existingApiTests : []) {
|
|
436
|
+
if (typeof item === "string" && item.trim())
|
|
437
|
+
evidenceRefs.push(item.trim());
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const targetMode = record.targetMode === "external-running-service" ||
|
|
441
|
+
record.targetMode === "managed-command" ||
|
|
442
|
+
record.targetMode === "in-process"
|
|
443
|
+
? record.targetMode
|
|
444
|
+
: "in-process";
|
|
445
|
+
const coerced = {
|
|
446
|
+
schemaVersion: 1,
|
|
447
|
+
framework: "pytest",
|
|
448
|
+
runner: {
|
|
449
|
+
frozenCommandHints: [...new Set(commandHints)],
|
|
450
|
+
},
|
|
451
|
+
testRoot: (typeof record.testRoot === "string" && record.testRoot.trim()) ||
|
|
452
|
+
BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT,
|
|
453
|
+
workingDirectory: (typeof record.workingDirectory === "string" && record.workingDirectory.trim()) ||
|
|
454
|
+
".",
|
|
455
|
+
report: asRecord(record.report) ?? {
|
|
456
|
+
format: "junit",
|
|
457
|
+
relativeHint: "reports/backend-test-junit.xml",
|
|
458
|
+
},
|
|
459
|
+
targetMode,
|
|
460
|
+
existingFixtures,
|
|
461
|
+
authenticationMode,
|
|
462
|
+
requiredEnvNames: Array.isArray(record.requiredEnvNames)
|
|
463
|
+
? record.requiredEnvNames.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
464
|
+
: [],
|
|
465
|
+
dataIsolation: asRecord(record.dataIsolation) ?? {
|
|
466
|
+
mode: "ephemeral-local",
|
|
467
|
+
evidence: "coerced from free-form environment scout; no durable shared fixtures",
|
|
468
|
+
},
|
|
469
|
+
evidenceGaps: Array.isArray(record.evidenceGaps) ? record.evidenceGaps : [],
|
|
470
|
+
evidenceRefs: [...new Set(evidenceRefs)],
|
|
471
|
+
};
|
|
472
|
+
if (typeof record.baseUrlEnvName === "string" && record.baseUrlEnvName.trim()) {
|
|
473
|
+
coerced.baseUrlEnvName = record.baseUrlEnvName.trim();
|
|
474
|
+
}
|
|
475
|
+
if (Array.isArray(record.readiness))
|
|
476
|
+
coerced.readiness = record.readiness;
|
|
477
|
+
if (asRecord(record.managedCommand))
|
|
478
|
+
coerced.managedCommand = record.managedCommand;
|
|
479
|
+
// Preserve free-form discovery notes as non-blocking evidence gaps when useful.
|
|
480
|
+
if (frameworkObj &&
|
|
481
|
+
typeof frameworkObj.testRunner === "string" &&
|
|
482
|
+
frameworkObj.testRunner !== "pytest") {
|
|
483
|
+
const gaps = Array.isArray(coerced.evidenceGaps) ? [...coerced.evidenceGaps] : [];
|
|
484
|
+
gaps.push({
|
|
485
|
+
description: `environment scout reported testRunner=${frameworkObj.testRunner}; runtime contract remains pytest with default testRoot=${BACKEND_TEST_EXECUTION_DEFAULT_TEST_ROOT}`,
|
|
486
|
+
sourceRef: typeof environment?.cwd === "string" ? "package.json" : "source/需求.md",
|
|
487
|
+
});
|
|
488
|
+
coerced.evidenceGaps = gaps;
|
|
489
|
+
}
|
|
490
|
+
return coerced;
|
|
491
|
+
}
|
|
327
492
|
export async function materializeBackendTestExecutionContract(input) {
|
|
328
493
|
if (!/^[a-z0-9][a-z0-9._-]*\.json$/.test(input.artifactName) ||
|
|
329
494
|
!/^[a-z0-9][a-z0-9._-]*$/.test(input.outputDir)) {
|
|
@@ -339,17 +504,29 @@ export async function materializeBackendTestExecutionContract(input) {
|
|
|
339
504
|
catch (error) {
|
|
340
505
|
throw new Error(`invalid-output: ${error instanceof Error ? error.message : String(error)}`);
|
|
341
506
|
}
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
507
|
+
const candidates = [parsed, coerceBackendTestExecutionInput(parsed)];
|
|
508
|
+
let accepted = null;
|
|
509
|
+
let lastSchemaError = "invalid execution contract";
|
|
510
|
+
let lastSecretError = "";
|
|
511
|
+
for (const candidate of candidates) {
|
|
512
|
+
const secrets = secretIssues(candidate);
|
|
513
|
+
if (secrets.length) {
|
|
514
|
+
lastSecretError = secrets.join("; ");
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
const result = backendTestExecutionContractSchema.safeParse(candidate);
|
|
518
|
+
if (result.success) {
|
|
519
|
+
accepted = result.data;
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
lastSchemaError = result.error.issues
|
|
349
523
|
.map((issue) => `${issue.path.join(".")}: ${issue.message}`)
|
|
350
|
-
.join("; ")
|
|
524
|
+
.join("; ");
|
|
525
|
+
}
|
|
526
|
+
if (!accepted) {
|
|
527
|
+
throw new Error(`invalid-output: ${lastSecretError || lastSchemaError}`);
|
|
351
528
|
}
|
|
352
|
-
const normalized = normalizeContractPaths(
|
|
529
|
+
const normalized = normalizeContractPaths(accepted);
|
|
353
530
|
// evidenceGaps may record greenfield/incomplete discovery (no test_*.py yet,
|
|
354
531
|
// missing pytest.ini, projected schema path, etc.). Do not block materialize:
|
|
355
532
|
// generate-functional-cases / generate-pytest are expected to fill automation.
|
|
@@ -1,9 +1,86 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
4
6
|
import { z } from "zod";
|
|
5
7
|
import { writeDagRunJsonArtifact } from "../../infrastructure/harness/artifact-store.js";
|
|
8
|
+
import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
6
9
|
export const FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID = "frontend-implementation-contract-v1";
|
|
10
|
+
/**
|
|
11
|
+
* Load the canonical frontend-implementation-contract-v1 JSON Schema from the
|
|
12
|
+
* installed loop-agent package docs/templates/ path. Package-root discovery
|
|
13
|
+
* works from both the source module and the compiled dist module without
|
|
14
|
+
* relying on CommonJS globals in the ESM runtime.
|
|
15
|
+
*
|
|
16
|
+
* Validation is fail-closed: missing file, malformed JSON, mismatched $id,
|
|
17
|
+
* missing additionalProperties: false, or incomplete top-level required keys
|
|
18
|
+
* all throw before any DAG prompt is assembled.
|
|
19
|
+
*/
|
|
20
|
+
export function loadFrontendImplementationContractJsonSchema(startDir = path.dirname(fileURLToPath(import.meta.url))) {
|
|
21
|
+
const packageRoot = findPackageRoot(startDir);
|
|
22
|
+
if (!packageRoot) {
|
|
23
|
+
throw new Error(`cannot locate loop-agent package root from ${path.resolve(startDir)}`);
|
|
24
|
+
}
|
|
25
|
+
const schemaPath = path.join(packageRoot, "docs", "templates", "frontend-implementation-contract.schema.json");
|
|
26
|
+
let content;
|
|
27
|
+
try {
|
|
28
|
+
content = readFileSync(schemaPath, "utf-8");
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
throw new Error(`cannot load frontend-implementation-contract.schema.json from current loop-agent package at ${schemaPath}: ${error.code ?? String(error)}`);
|
|
32
|
+
}
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(content);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
throw new Error(`frontend-implementation-contract.schema.json is not valid JSON: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
41
|
+
throw new Error("frontend-implementation-contract.schema.json root is not a JSON object");
|
|
42
|
+
}
|
|
43
|
+
const schema = parsed;
|
|
44
|
+
if (schema.$id !== FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID) {
|
|
45
|
+
throw new Error(`schema $id mismatch: expected ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID}, got ${String(schema.$id)}`);
|
|
46
|
+
}
|
|
47
|
+
if (schema.additionalProperties !== false) {
|
|
48
|
+
throw new Error("schema must have additionalProperties: false at top level");
|
|
49
|
+
}
|
|
50
|
+
const expectedRequired = [
|
|
51
|
+
"schemaVersion",
|
|
52
|
+
"sourceBinding",
|
|
53
|
+
"riskLevel",
|
|
54
|
+
"targets",
|
|
55
|
+
"requirements",
|
|
56
|
+
"uiStates",
|
|
57
|
+
"interactions",
|
|
58
|
+
"mockApi",
|
|
59
|
+
"designEvidence",
|
|
60
|
+
"verificationTargets",
|
|
61
|
+
"evidenceGaps",
|
|
62
|
+
];
|
|
63
|
+
const actualRequired = Array.isArray(schema.required) ? schema.required : [];
|
|
64
|
+
const missing = expectedRequired.filter((key) => !actualRequired.includes(key));
|
|
65
|
+
if (missing.length > 0) {
|
|
66
|
+
throw new Error(`schema required fields missing: ${missing.join(", ")}`);
|
|
67
|
+
}
|
|
68
|
+
const properties = schema.properties && typeof schema.properties === "object"
|
|
69
|
+
? schema.properties
|
|
70
|
+
: {};
|
|
71
|
+
const missingProperties = expectedRequired.filter((key) => !Object.hasOwn(properties, key));
|
|
72
|
+
if (missingProperties.length > 0) {
|
|
73
|
+
throw new Error(`schema properties missing: ${missingProperties.join(", ")}`);
|
|
74
|
+
}
|
|
75
|
+
const schemaVersion = properties.schemaVersion;
|
|
76
|
+
const mockApi = properties.mockApi;
|
|
77
|
+
const mockApiProperties = mockApi?.properties;
|
|
78
|
+
const productionDefaultOff = mockApiProperties?.productionDefaultOff;
|
|
79
|
+
if (schemaVersion?.const !== 1 || productionDefaultOff?.const !== true) {
|
|
80
|
+
throw new Error("schema fixed values are incomplete: schemaVersion.const must be 1 and mockApi.productionDefaultOff.const must be true");
|
|
81
|
+
}
|
|
82
|
+
return JSON.stringify(parsed);
|
|
83
|
+
}
|
|
7
84
|
const id = z.string().regex(/^(?:REQ|BR|AC)-[A-Z0-9]+(?:-[A-Z0-9]+)*$/);
|
|
8
85
|
const safePath = z
|
|
9
86
|
.string()
|
|
@@ -32,8 +32,8 @@ function allDeps(pkg) {
|
|
|
32
32
|
function hasDep(deps, name) {
|
|
33
33
|
return Object.hasOwn(deps, name);
|
|
34
34
|
}
|
|
35
|
-
async function
|
|
36
|
-
const root = path.join(repoRoot, "
|
|
35
|
+
async function listOpenspec(repoRoot) {
|
|
36
|
+
const root = path.join(repoRoot, "openspec");
|
|
37
37
|
if (!(await exists(root)))
|
|
38
38
|
return [];
|
|
39
39
|
const out = [];
|
|
@@ -70,7 +70,7 @@ export function buildAdapterGuidance(capability) {
|
|
|
70
70
|
"## Frontend project capability (generation-time)",
|
|
71
71
|
`Framework: ${capability.framework}${capability.frameworkVersion ? `@${capability.frameworkVersion}` : ""}`,
|
|
72
72
|
`Evidence: ${capability.evidencePaths.join(", ") || "(none)"}`,
|
|
73
|
-
"Rules:
|
|
73
|
+
"Rules: openspec/** and task sources outrank adapter tips; do not invent APIs for unknown versions; lockfile-only is not enough.",
|
|
74
74
|
];
|
|
75
75
|
if (capability.framework === "react") {
|
|
76
76
|
lines.push("React adapter: prefer function components + hooks; reuse existing Testing Library / Vitest patterns; do not introduce new state libs without authorization.");
|
|
@@ -276,14 +276,14 @@ export async function discoverFrontendProjectCapability(repoRoot) {
|
|
|
276
276
|
router = router ?? "next-router";
|
|
277
277
|
if (hasDep(deps, "vue-router"))
|
|
278
278
|
router = "vue-router";
|
|
279
|
-
const
|
|
279
|
+
const openspec = await listOpenspec(repoRoot);
|
|
280
280
|
const designEvidence = {
|
|
281
|
-
normativePaths:
|
|
281
|
+
normativePaths: openspec,
|
|
282
282
|
advisoryPaths: [],
|
|
283
283
|
conflicts: [],
|
|
284
284
|
};
|
|
285
|
-
if (
|
|
286
|
-
evidencePaths.push(...
|
|
285
|
+
if (openspec.length)
|
|
286
|
+
evidencePaths.push(...openspec.slice(0, 5));
|
|
287
287
|
const base = {
|
|
288
288
|
schemaVersion: 1,
|
|
289
289
|
framework,
|
|
@@ -22,6 +22,7 @@ import { buildBackendTestEffectiveResultSelectorShellSnippet, buildBackendTestRe
|
|
|
22
22
|
import { buildBackendTestOutcomeGateShellSnippet } from "./backend-test-result-contract.js";
|
|
23
23
|
import { classifyFrontendRisk, } from "./frontend-risk.js";
|
|
24
24
|
import { discoverFrontendProjectCapability, } from "./frontend-project-capability.js";
|
|
25
|
+
import { FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID, loadFrontendImplementationContractJsonSchema, } from "./frontend-implementation-contract.js";
|
|
25
26
|
const REQUIREMENT_FILE = "需求.md";
|
|
26
27
|
const CONSTRAINT_FILE = "执行约束.md";
|
|
27
28
|
const REFERENCE_DIRECTORY = "references";
|
|
@@ -1355,10 +1356,11 @@ function buildFrontendMockAssessNode(sources, sourceContext, mockContextBlock, f
|
|
|
1355
1356
|
allowedPaths: readOnlyPaths,
|
|
1356
1357
|
forbiddenPaths,
|
|
1357
1358
|
skills: FRONTEND_IMPLEMENTATION_SKILLS,
|
|
1358
|
-
|
|
1359
|
+
firstProtocolLine: "MOCK_STRATEGY:",
|
|
1360
|
+
outputContract: "Plain Markdown whose first line is MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked, followed by Mock Decision, API Contract Evidence, Specification Evidence, Service Evidence, Backend Readiness, Selection Evidence, Endpoint / Fixture Matrix, Activation, Target Files, Production Safety, Verification Plan, Real Integration Gap, and Blocking Issues. No file writes.",
|
|
1359
1361
|
subtask_prompt: [
|
|
1360
1362
|
"Perform read-only Mock assessment and select one safe frontend data strategy.",
|
|
1361
|
-
"The first
|
|
1363
|
+
"The first line must be exactly one of: MOCK_STRATEGY: native, MOCK_STRATEGY: browser-intercept, MOCK_STRATEGY: request-adapter, MOCK_STRATEGY: not-needed, or MOCK_STRATEGY: blocked. Do not emit blank lines, headings, or explanatory preamble before it.",
|
|
1362
1364
|
"Prefer an existing native Mock facility. Use browser-intercept only with an existing browser/e2e harness. When no Mock exists but the API layer is writable, use request-adapter by adding a minimal reversible adapter/DI seam within the approved writeSet; the real adapter must remain the production default.",
|
|
1363
1365
|
autoMaySkipMissingMock
|
|
1364
1366
|
? "Auto mode may skip Mock when no project Mock capability is confirmed. Select not-needed with positive evidence from contract/scout that no project Mock capability is confirmed, continue without adding Mock files or dependencies, run the fixed verification entrypoints, and record any unproved real API behavior in Real Integration Gap. Do not block solely because no project Mock capability, browser interception harness, or request adapter exists."
|
|
@@ -1616,7 +1618,7 @@ function resolveFrontendCapabilityContextBlock(sources) {
|
|
|
1616
1618
|
if (capability) {
|
|
1617
1619
|
parts.push("", capability.adapterGuidance);
|
|
1618
1620
|
if (capability.designEvidence.normativePaths.length > 0) {
|
|
1619
|
-
parts.push(`
|
|
1621
|
+
parts.push(`openspec normative candidates: ${capability.designEvidence.normativePaths.slice(0, 12).join(", ")}`);
|
|
1620
1622
|
}
|
|
1621
1623
|
parts.push(`A11y capability: ${capability.a11y.status}` +
|
|
1622
1624
|
(capability.a11y.tools.length
|
|
@@ -1749,6 +1751,46 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1749
1751
|
allowedPaths: taskConfig.allowedPaths,
|
|
1750
1752
|
complexity: taskConfig.complexity,
|
|
1751
1753
|
});
|
|
1754
|
+
const frontendSourceBinding = buildDagSourceBinding(sources);
|
|
1755
|
+
const frontendContractSchemaBlock = (() => {
|
|
1756
|
+
const schema = loadFrontendImplementationContractJsonSchema();
|
|
1757
|
+
const requirement = frontendSourceBinding.sources.find((source) => source.kind === "requirement");
|
|
1758
|
+
if (!requirement) {
|
|
1759
|
+
throw new Error("frontend implementation contract context requires a bound requirement source");
|
|
1760
|
+
}
|
|
1761
|
+
const referencePaths = frontendSourceBinding.sources
|
|
1762
|
+
.filter((s) => s.kind === "reference")
|
|
1763
|
+
.map((s) => s.path);
|
|
1764
|
+
const fixedFields = {
|
|
1765
|
+
schemaVersion: 1,
|
|
1766
|
+
sourceBinding: {
|
|
1767
|
+
taskId: frontendSourceBinding.taskId,
|
|
1768
|
+
requirementPath: requirement.path,
|
|
1769
|
+
requirementSha256: requirement.sha256,
|
|
1770
|
+
referencePaths,
|
|
1771
|
+
requirementIds: frontendSourceBinding.requirementIds,
|
|
1772
|
+
},
|
|
1773
|
+
riskLevel: frontendRisk.selectedRisk,
|
|
1774
|
+
targets: { files: implementPaths.writeSet },
|
|
1775
|
+
};
|
|
1776
|
+
return [
|
|
1777
|
+
`## ${FRONTEND_IMPLEMENTATION_CONTRACT_SCHEMA_ID} JSON Schema (authoritative; do not guess fields)`,
|
|
1778
|
+
schema,
|
|
1779
|
+
"",
|
|
1780
|
+
"## Fixed contract fields (deterministic; copy exactly and do not modify)",
|
|
1781
|
+
JSON.stringify(fixedFields),
|
|
1782
|
+
"",
|
|
1783
|
+
"## Forbidden fields (these are NOT in the schema; do not emit)",
|
|
1784
|
+
"- schemaId",
|
|
1785
|
+
"- targetFiles",
|
|
1786
|
+
"- requirementCoverage",
|
|
1787
|
+
"",
|
|
1788
|
+
"## Critical rules",
|
|
1789
|
+
"- verificationTargets is a TOP-LEVEL required array",
|
|
1790
|
+
"- uiStates items use name/applicable/expectedBehavior/implementationTargets/verificationTargetIds/notApplicableReason",
|
|
1791
|
+
"- mockApi.productionDefaultOff must always be true (including strategy: not-needed)",
|
|
1792
|
+
].join("\n");
|
|
1793
|
+
})();
|
|
1752
1794
|
const sourceContext = [
|
|
1753
1795
|
buildSourceContextBlock(sources),
|
|
1754
1796
|
capabilityContextBlock,
|
|
@@ -1757,7 +1799,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1757
1799
|
.join("\n\n");
|
|
1758
1800
|
const hasMockVerifyCommands = (taskConfig.frontendMock?.verifyCommands.length ?? 0) > 0 ||
|
|
1759
1801
|
mockCapability.verifyCommands.length > 0;
|
|
1760
|
-
const requirementIds =
|
|
1802
|
+
const requirementIds = frontendSourceBinding.requirementIds;
|
|
1761
1803
|
const requirementCoverageInstruction = requirementIds.length > 0
|
|
1762
1804
|
? `Include a Requirement Coverage section that lists every exact source identifier: ${requirementIds.join(", ")}. Preserve each identifier verbatim and map it to concrete implementation and verification steps.`
|
|
1763
1805
|
: "";
|
|
@@ -1916,6 +1958,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1916
1958
|
fixedVerificationContext,
|
|
1917
1959
|
sourceContext,
|
|
1918
1960
|
mockContextBlock,
|
|
1961
|
+
frontendContractSchemaBlock,
|
|
1919
1962
|
].join("\n\n"),
|
|
1920
1963
|
},
|
|
1921
1964
|
{
|
|
@@ -1992,6 +2035,7 @@ function buildFrontendHybridDagFromTask(sources) {
|
|
|
1992
2035
|
"Read-only: do not modify code, docs, artifacts, or repository files. This node revises the plan only.",
|
|
1993
2036
|
"End the response with exactly one fenced json object conforming to frontend-implementation-contract-v1. Bind it to the supplied task sources; map every requirement and applicable UI state to concrete implementation and verification targets or an explicit blocking evidence gap. Do not include secrets or unsafe paths.",
|
|
1994
2037
|
sourceContext,
|
|
2038
|
+
frontendContractSchemaBlock,
|
|
1995
2039
|
].join("\n\n"),
|
|
1996
2040
|
},
|
|
1997
2041
|
...(requirementIds.length > 0
|
|
@@ -306,6 +306,7 @@ export const dagTaskSchema = z.object({
|
|
|
306
306
|
shell: dagShellConfigSchema.optional(),
|
|
307
307
|
static: dagStaticConfigSchema.optional(),
|
|
308
308
|
outputContract: z.string().optional(),
|
|
309
|
+
firstProtocolLine: z.string().min(1).optional(),
|
|
309
310
|
allowedPaths: z.array(z.string()).optional().default([]),
|
|
310
311
|
forbiddenPaths: z.array(z.string()).optional().default([]),
|
|
311
312
|
decisionGate: dagDecisionGateSchema.optional(),
|
|
@@ -389,6 +389,11 @@
|
|
|
389
389
|
"type": "string",
|
|
390
390
|
"minLength": 1
|
|
391
391
|
},
|
|
392
|
+
"firstProtocolLine": {
|
|
393
|
+
"type": "string",
|
|
394
|
+
"minLength": 1,
|
|
395
|
+
"description": "Optional protocol prefix whose first matching Pi assistant-output line is promoted to the canonical first line. Missing matches are not synthesized."
|
|
396
|
+
},
|
|
392
397
|
"allowedPaths": {
|
|
393
398
|
"type": "array",
|
|
394
399
|
"items": { "type": "string", "minLength": 1 },
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ references:
|
|
|
12
12
|
For first/final design review nodes. Read the checklist, then audit contract, scout,
|
|
13
13
|
mock strategy, plan/revision, task constraints/bounds, and traceable design evidence. The knowledge-
|
|
14
14
|
base connector is TODO: never invent results. If absent/failed/unmatched, require
|
|
15
|
-
`<repoRoot>/
|
|
15
|
+
`<repoRoot>/openspec/**` search/read evidence before repo conventions.
|
|
16
16
|
|
|
17
17
|
## Verdict Contract
|
|
18
18
|
|
|
@@ -27,7 +27,7 @@ remaining, incomplete, or newly introduced gaps.
|
|
|
27
27
|
|
|
28
28
|
- Any criterion lacks implementation/verification; UI states lack reasons; a
|
|
29
29
|
dependency lacks permission; confirmed primitives/rules are ignored; design claims
|
|
30
|
-
lack knowledge-base or required `
|
|
30
|
+
lack knowledge-base or required `openspec/` evidence; paths cross write bounds;
|
|
31
31
|
commands are missing/non-deterministic; or interaction, responsive, accessibility,
|
|
32
32
|
data, or failure behavior requires guessing.
|
|
33
33
|
- `MOCK_STRATEGY: blocked`; missing permitted target paths, endpoint/schema-to-fixture
|
|
@@ -35,7 +35,7 @@ remaining, incomplete, or newly introduced gaps.
|
|
|
35
35
|
inline fake data; commented real requests; Mock-on production defaults; test-only
|
|
36
36
|
production imports; or Mock evidence reported as real integration.
|
|
37
37
|
|
|
38
|
-
Knowledge-base absence is advisory if relevant `
|
|
38
|
+
Knowledge-base absence is advisory if relevant `openspec/` rules were searched/read
|
|
39
39
|
and applied. Block skipped fallback, unresolved conflict, or unresolved UI decisions.
|
|
40
40
|
|
|
41
41
|
## Method And Output
|
|
@@ -48,7 +48,7 @@ Advisory, and never edit files.
|
|
|
48
48
|
Run `grep`/`find`, then explicit `read` calls for applicable specs and checklist.
|
|
49
49
|
Only successful paired reads count as “已读取规范文件”; summaries do not. List each
|
|
50
50
|
read path/section in `Checked Items`. If the connector is unavailable, search/read
|
|
51
|
-
`
|
|
51
|
+
`openspec/` before accepting repository conventions.
|
|
52
52
|
|
|
53
53
|
```markdown
|
|
54
54
|
VERDICT: pass
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
## Project Fit
|
|
10
10
|
|
|
11
11
|
- Reuse components, hooks, API helpers, mocks, schemas, router patterns, tokens, and theme rules.
|
|
12
|
-
- Cite knowledge-base or `
|
|
12
|
+
- Cite knowledge-base or `openspec/`; failed/empty knowledge queries must search `<repoRoot>/openspec/**`.
|
|
13
13
|
- Record source status, query terms, paths/headings, conflicts, authorized deps, and allowed paths.
|
|
14
14
|
|
|
15
15
|
## Interaction / Quality
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
|
|
36
36
|
## Verdict Matrix
|
|
37
37
|
|
|
38
|
-
- Request revision for coverage gaps, unsafe scope, unauthorized deps, unresolved required interaction, missing required verification, skipped `
|
|
39
|
-
- Knowledge-base unavailable but relevant `
|
|
38
|
+
- Request revision for coverage gaps, unsafe scope, unauthorized deps, unresolved required interaction, missing required verification, skipped `openspec/` fallback, unsafe/missing Mock strategy, or Mock evidence presented as real integration.
|
|
39
|
+
- Knowledge-base unavailable but relevant `openspec/` rules applied is advisory only.
|
|
40
40
|
- Optional cleanup that cannot affect acceptance is advisory.
|
|
@@ -23,7 +23,7 @@ Read all required references before running any listed frontend node.
|
|
|
23
23
|
## Source And Evidence Rules
|
|
24
24
|
|
|
25
25
|
Use task sources/references, constraints, then `task.json`. Follow `design-spec.md`:
|
|
26
|
-
knowledge base; `<repoRoot>/
|
|
26
|
+
knowledge base; `<repoRoot>/openspec/**` after unavailable/failed/empty; then repo
|
|
27
27
|
evidence. Cite tight paths/symbols, label gaps/conflicts, and never invent APIs,
|
|
28
28
|
rules, commands, or retrievals. Scout/planners locate and explicitly read applicable
|
|
29
29
|
specs; only successful paired reads count. Lockfile-only, fixture-only, or unread
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Discover rules from task constraints, `design-spec.md` source order, config, code,
|
|
4
4
|
tests, manifests, and generated types. After knowledge-base failure, applicable
|
|
5
|
-
`<repoRoot>/
|
|
5
|
+
`<repoRoot>/openspec/**` rules are normative. Preferences are not rules, and docs do
|
|
6
6
|
not override installed APIs without an explicit compatibility decision.
|
|
7
7
|
|
|
8
8
|
## Discover And Cite
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
## Required Source Sequence
|
|
4
4
|
|
|
5
5
|
1. Attempt the configured component/design knowledge-base query first.
|
|
6
|
-
2. If unavailable, failed, timed out, or unmatched, recursively search the project root's exact `
|
|
6
|
+
2. If unavailable, failed, timed out, or unmatched, recursively search the project root's exact `openspec/` directory.
|
|
7
7
|
3. Treat relevant matches as the current project's specification for this run.
|
|
8
8
|
4. Only then use component source, tokens, stories, tests, and pages as non-normative repository fallback.
|
|
9
9
|
|
|
10
|
-
Never skip `
|
|
10
|
+
Never skip `openspec/` directly to neighboring-code conventions. Report source
|
|
11
11
|
conflicts instead of combining them. Explicit task requirements remain the contract;
|
|
12
|
-
flag conflicts with knowledge-base or `
|
|
12
|
+
flag conflicts with knowledge-base or `openspec/` rules.
|
|
13
13
|
|
|
14
14
|
## Knowledge Base Connection — TODO
|
|
15
15
|
|
|
@@ -17,11 +17,11 @@ Request format is undecided. TODO: define connector/owner, namespaces, secret-fr
|
|
|
17
17
|
auth, query fields, result identity/version/time, and failure behavior.
|
|
18
18
|
|
|
19
19
|
Attempt only a connector actually available in the execution environment. Otherwise
|
|
20
|
-
record `not-configured` and run the `
|
|
20
|
+
record `not-configured` and run the `openspec/` fallback; never invent a connection.
|
|
21
21
|
|
|
22
|
-
## `
|
|
22
|
+
## `openspec/` Fallback Procedure
|
|
23
23
|
|
|
24
|
-
- Confirm whether `<repoRoot>/
|
|
24
|
+
- Confirm whether `<repoRoot>/openspec/` exists and enumerate its files recursively.
|
|
25
25
|
- Read indexes first, then search names/content using task, route, component, interaction, theme, token, and state terms.
|
|
26
26
|
- Read relevant matches in context; do not treat a filename-only hit as a rule.
|
|
27
27
|
- Record search terms, inspected/matched paths, headings or tight line ranges, applied rules, and conflicts.
|
|
@@ -29,9 +29,9 @@ record `not-configured` and run the `openSpec/` fallback; never invent a connect
|
|
|
29
29
|
|
|
30
30
|
## Retrieval Evidence
|
|
31
31
|
|
|
32
|
-
Record source as `knowledge-base`, `
|
|
32
|
+
Record source as `knowledge-base`, `openspec fallback`, `repository fallback`, or
|
|
33
33
|
`unavailable`. Knowledge-base evidence includes query, source ID/version/time, rules,
|
|
34
|
-
and conflicts. `
|
|
34
|
+
and conflicts. `openspec fallback` includes terms, paths/headings/lines, rules, and conflicts.
|
|
35
35
|
|
|
36
36
|
## Rules To Retrieve Or Discover
|
|
37
37
|
|
|
@@ -42,5 +42,5 @@ and conflicts. `openSpec fallback` includes terms, paths/headings/lines, rules,
|
|
|
42
42
|
|
|
43
43
|
- Reuse confirmed primitives unless a new pattern is authorized.
|
|
44
44
|
- Define applicable states and responsive behavior before implementation.
|
|
45
|
-
- Cite knowledge-base or `
|
|
45
|
+
- Cite knowledge-base or `openspec/` evidence for component/token choices; label weaker repository fallback.
|
|
46
46
|
- Make deviations and unresolved gaps explicit.
|
|
@@ -5,11 +5,11 @@ Pre-write nodes are read-only. Preserve IDs, labels, commands, language, require
|
|
|
5
5
|
## Core nodes
|
|
6
6
|
|
|
7
7
|
- **`frontend-contract-pi`**: `Scope`, `Non-goals`, `Acceptance Criteria`, `UI States`, `Target Runtime Environment`, `Risks`, `Verification Expectations`. No guessed requirements.
|
|
8
|
-
- **`frontend-scout-pi`**: routes, components, tokens, data/API/Mock, scripts, tests, assets. Fact vs inference vs gap. Knowledge base first; else search+read `<repoRoot>/
|
|
9
|
-
- **`frontend-mock-assess-pi` + gate**: first
|
|
8
|
+
- **`frontend-scout-pi`**: routes, components, tokens, data/API/Mock, scripts, tests, assets. Fact vs inference vs gap. Knowledge base first; else search+read `<repoRoot>/openspec/**` before repo fallback. Output stack, routes, components, styling, conventions, state/data, test entry points, reuse, risks.
|
|
9
|
+
- **`frontend-mock-assess-pi` + gate**: declares `firstProtocolLine: "MOCK_STRATEGY:"`; canonical output first line
|
|
10
10
|
`MOCK_STRATEGY: native|browser-intercept|request-adapter|not-needed|blocked`
|
|
11
|
-
Prefer native Mock; browser intercept only with existing e2e; request-adapter only for reversible local preview. Default `auto` may select `not-needed` when contract/scout evidence confirms no project Mock capability, without adding Mock files/deps, while keeping real requests default and recording the Real Integration Gap. Other `not-needed` cases need positive no-remote/stable-backend evidence; invalid when `frontendMock.policy=required`. `blocked` for missing/conflicting contracts, unsafe paths/deps, unread specs, production-default-on, unverifiable entrypoints. Output Mock Decision, API/spec/service evidence, backend readiness, selection evidence, endpoint/fixture matrix, activation, targets, production safety, verification plan, real-integration gap, blocking issues. Never invent fields, store secrets, comment real requests, import test mocks into production, or treat Mock as real integration. Gate uses `first-non-empty` only; never authorizes writes. Unsafe required contracts → no writer.
|
|
12
|
-
- **`frontend-plan-pi` + design loop**: AC → steps, in-bound files, UI states, reuse, deps, activation/rollback, frozen verify entrypoints, real-integration gap. First gate: `VERDICT: pass|request-revision`. Pass may emit `PASS_NO_REVISION_NEEDED`; else full corrected plan without invented evidence. Final review rechecks plan/findings/revision/assessment/Mock safety. Only final `VERDICT: pass` authorizes writes; failure → replan/rerun (not dev-fix).
|
|
11
|
+
Pi output mapping promotes the first matching protocol line ahead of any preamble without inventing or replacing its value; missing, malformed, or blocked strategies still fail closed. Prefer native Mock; browser intercept only with existing e2e; request-adapter only for reversible local preview. Default `auto` may select `not-needed` when contract/scout evidence confirms no project Mock capability, without adding Mock files/deps, while keeping real requests default and recording the Real Integration Gap. Other `not-needed` cases need positive no-remote/stable-backend evidence; invalid when `frontendMock.policy=required`. `blocked` for missing/conflicting contracts, unsafe paths/deps, unread specs, production-default-on, unverifiable entrypoints. Output Mock Decision, API/spec/service evidence, backend readiness, selection evidence, endpoint/fixture matrix, activation, targets, production safety, verification plan, real-integration gap, blocking issues. Never invent fields, store secrets, comment real requests, import test mocks into production, or treat Mock as real integration. Gate uses `first-non-empty` only; never authorizes writes. Unsafe required contracts → no writer.
|
|
12
|
+
- **`frontend-plan-pi` + design loop**: AC → steps, in-bound files, UI states, reuse, deps, activation/rollback, frozen verify entrypoints, real-integration gap. First gate: `VERDICT: pass|request-revision`. Pass may emit `PASS_NO_REVISION_NEEDED`; else full corrected plan without invented evidence. Final review rechecks plan/findings/revision/assessment/Mock safety. Only final `VERDICT: pass` authorizes writes; failure → replan/rerun (not dev-fix). The plan-pi prompt now includes the complete `frontend-implementation-contract-v1` JSON Schema loaded from the loop-agent package `docs/templates/` path, plus deterministic source binding, risk level, and allowed implementation targets. The model does not need to search or guess contract fields; `schemaId`, `targetFiles`, `requirementCoverage` are explicitly forbidden.
|
|
13
13
|
- **`frontend-implement-pi`**: sole exclusive writer. Stay in `writeSet`; real requests default-on; Mock reversible, dev/test-only, production-off. Atomic handler/intercept/adapter with consumer+tests. Stop on forbidden paths or guesses. Output changed files, behavior, UI states, styling notes, verification attempted, residual risks. Optional mock-verify when frozen; static+behavior always; behavior must prove page consumption. Skipped-Mock `not-needed` keeps real integration pending unless the real backend path has fresh evidence.
|
|
14
14
|
|
|
15
15
|
## Contract / trace / stages (M1–M2)
|
|
@@ -24,4 +24,4 @@ static/behavior/trace may `nonZeroExitPolicy: record`. Assess → `contracts/fro
|
|
|
24
24
|
|
|
25
25
|
## Risk & capability (M4–M6)
|
|
26
26
|
|
|
27
|
-
Deterministic risk (no model); high-risk beats small; supervised never small. Small may drop first design gate + plan-revision; contract shell retargets to `frontend-plan-pi`. Capability seed injects adapters;
|
|
27
|
+
Deterministic risk (no model); high-risk beats small; supervised never small. Small may drop first design gate + plan-revision; contract shell retargets to `frontend-plan-pi`. Capability seed injects adapters; openspec/task sources outrank. A11y: static/component tools only when present; Browser a11y always not-run.
|
|
@@ -37,7 +37,7 @@ required check, forbidden write, or unmet acceptance criterion forces revision.
|
|
|
37
37
|
and no false real-integration claim. `not-needed` needs applicable real/no-remote
|
|
38
38
|
evidence, or an explicit default-auto skipped-Mock rationale with the Real
|
|
39
39
|
Integration Gap preserved when no project Mock capability is confirmed.
|
|
40
|
-
- Component/design claims require traceable knowledge-base evidence or, after connection/query failure or no match, relevant `<repoRoot>/
|
|
40
|
+
- Component/design claims require traceable knowledge-base evidence or, after connection/query failure or no match, relevant `<repoRoot>/openspec/**` evidence. The connector format is TODO; never claim a query or fallback search without evidence. Execute explicit `grep`/`find` to locate spec files and `read` to load them before referencing their rules. Only successful `read` tool calls are observable as "已读取规范文件" in the spec-evidence inspector.
|
|
41
41
|
- Treat shell exit status as authoritative. Do not edit files.
|
|
42
42
|
|
|
43
43
|
## Evidence And Output
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
- Cite tight file locations, exact commands/results, or named DAG artifacts.
|
|
12
12
|
- Never invent evidence; name the missing check. An implementation summary is not the actual diff.
|
|
13
13
|
- Failed required static/behavior verification is at least Important unless proven unrelated.
|
|
14
|
-
- A knowledge-base claim records connector/query, source ID/version, and retrieval time. If absent, failed, or unmatched, review evidence must show `<repoRoot>/
|
|
14
|
+
- A knowledge-base claim records connector/query, source ID/version, and retrieval time. If absent, failed, or unmatched, review evidence must show `<repoRoot>/openspec/**` search terms and matched paths/headings; label `openspec fallback`, `repository fallback`, or `unavailable` accurately.
|
|
15
15
|
|
|
16
16
|
## Review Sequence
|
|
17
17
|
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
6. Check component/design evidence, responsive/accessibility behavior, dependencies, and maintenance fit when applicable.
|
|
26
26
|
7. Classify findings and derive the verdict mechanically.
|
|
27
27
|
|
|
28
|
-
Skipping the required `
|
|
28
|
+
Skipping the required `openspec/` search after knowledge-base failure is Important
|
|
29
29
|
when component/design compliance affects acceptance or implementation choices.
|
|
30
30
|
|
|
31
31
|
Use one issue per finding:
|
|
@@ -24,9 +24,9 @@ verdict/findings, and required browser, visual, manual, or knowledge evidence.
|
|
|
24
24
|
- Mock-backed behavior proves frontend rendering and state transitions only. It never
|
|
25
25
|
proves backend readiness, transport compatibility, or real API integration.
|
|
26
26
|
- Unavailable commands remain gaps.
|
|
27
|
-
- Resolve design evidence via knowledge base, then `<repoRoot>/
|
|
27
|
+
- Resolve design evidence via knowledge base, then `<repoRoot>/openspec/**` after
|
|
28
28
|
failure/no match. Its connector format remains TODO; never invent it. An applied
|
|
29
|
-
`
|
|
29
|
+
`openspec fallback` is available project evidence.
|
|
30
30
|
- Separate Mock service/handler checks from page consumption and record the
|
|
31
31
|
dev/test-only boundary; handler tests alone do not prove page use.
|
|
32
32
|
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
## Design And Component Evidence
|
|
14
14
|
|
|
15
|
-
- Claims cite knowledge-base retrieval or `<repoRoot>/
|
|
15
|
+
- Claims cite knowledge-base retrieval or `<repoRoot>/openspec/**` fallback.
|
|
16
16
|
- Evidence records query/source/time or fallback search terms, paths, headings, and applied rules.
|
|
17
|
-
- Relevant `
|
|
17
|
+
- Relevant `openspec/` matches satisfy source availability; missing both sources blocks explicit compliance or required design decisions.
|
|
18
18
|
|
|
19
19
|
## Status
|
|
20
20
|
|
|
@@ -120,7 +120,7 @@ contract-pi → scout-src ∥ scout-tests → plan-pi → write-set-audit-pi
|
|
|
120
120
|
| `authority-surface-audit-pi` + `authority-surface-gate-shell` | 可选 permission/state/tool-exposure audit;仅 authority signal 或显式 `authority-surface-audit` marker 时插入;gate 仅接受 `VERDICT: pass` |
|
|
121
121
|
| `review-pi` + `review-verdict-recovery-pi` + `review-gate-shell` | Critical/Important → `request-revision`;recovery 只规范化 VERDICT 协议(不得从自然语言猜 pass);gate 只认 `review-verdict-recovery-pi` 的 `VERDICT: pass` |
|
|
122
122
|
|
|
123
|
-
**Verdict gate contract(`shell.verdictGate`)**:声明 `fromNodeId`、`accept[]`、可选 `label`、可选 `lineMode`。runner 展开为一条 shell command,从 injected current run directory 读 `$HARNESS_DAG_RUN_DIR/<fromNodeId>.json`,对 extracted `assistantText ?? stdout` verdict line 与 `accept[]` exact-match。默认 `lineMode` 为 `first-non-empty`
|
|
123
|
+
**Verdict gate contract(`shell.verdictGate`)**:声明 `fromNodeId`、`accept[]`、可选 `label`、可选 `lineMode`。runner 展开为一条 shell command,从 injected current run directory 读 `$HARNESS_DAG_RUN_DIR/<fromNodeId>.json`,对 extracted `assistantText ?? stdout` verdict line 与 `accept[]` exact-match。默认 `lineMode` 为 `first-non-empty` 以兼容;需要固定非 `VERDICT:` 协议首行的 Pi 节点可显式声明 `firstProtocolLine`,executor 会把第一条匹配前缀的行提升为 canonical 首行,不匹配时不伪造。`frontend-mock-assess-pi` 用它固定 `MOCK_STRATEGY:`,gate 仍保持 `first-non-empty` exact-match。supervised gate 用 `first-verdict-line` 选 Pi 在 preamble 或常见整行 Markdown emphasis(如 `**VERDICT: pass**`)后第一条 normalized `VERDICT:` line。勿用 `result.summary.md`、grep VERDICT、latest-active-run discovery 或 multi-command stateful gate。`--strict-governance` 对 anti-pattern fail。supervisor 仍为 `executor: pi` 上的 `role: supervisor`。
|
|
124
124
|
|
|
125
125
|
**Repair artifact gate contract(`shell.repairArtifactGate`)**:声明 `fromNodeId`(supervisor artifact 节点)与 `repairNodeId`(承接修订的 Pi 修复节点)。runner **不再**按节点名(历史 `repair-cursor` / `repair-pi`)猜测 repair 节点:显式 `repairNodeId` 必须存在、直接 `depends_on` gate、且是受治理 Pi writer(`executor: pi`、`toolProfile: write`、`writePolicy: exclusive`、`allowedPaths`+`writeSet` 非空且 `writeSet` 不与 `forbiddenPaths` 冲突)。新生成的 supervised DAG 总是写入 `repairNodeId`;旧 DAG 缺失时只在能唯一、安全地推导出下游 Pi writer 时兼容,零个或多个候选、或候选不满足契约都在执行前 fail closed。validation 覆盖存在性、直接下游、writer 属性与路径边界。
|
|
126
126
|
|