@tea-agent/loop-agent 0.42.0-next.13 → 0.42.0-next.15
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 +25 -2
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +617 -94
- package/dist/executors/shell-executor.js +4 -0
- package/dist/worker/console/chat/pi-runtime.js +45 -2
- package/dist/worker/console/chat/resource-loader.js +4 -1
- package/dist/worker/console/chat/routes.js +22 -11
- package/dist/worker/console/chat/sdd-data-alignment.js +13 -2
- package/dist/worker/console/chat/session-store.js +5 -1
- package/dist/worker/console/chat/subagents/agent-tool.js +62 -0
- package/dist/worker/console/chat/subagents/explore-agent.js +169 -0
- package/dist/worker/console/chat/subagents/index.js +5 -0
- package/dist/worker/console/chat/subagents/orchestrator.js +273 -0
- package/dist/worker/console/chat/subagents/tool-policy.js +53 -0
- package/dist/worker/console/chat/subagents/types.js +13 -0
- package/dist/worker/console/chat/tool-preview.js +10 -0
- package/dist/worker/console/chat/tools.js +11 -1
- package/dist/worker/console/chat/turn-process.js +1 -0
- package/dist/worker/console/interview/tools.js +1 -0
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BZogYNl1.js → abnfDiagram-N423BO3Z-CgXb0EVO.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DcJTLe2v.js → arc-DN59MZqN.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-B39EQQ9n.js → architectureDiagram-T3A2C74G-BUk3sWpn.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CitCGV56.js → blockDiagram-VBNYF7ZC-IH-cPBFE.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-rvWuOHLG.js → c4Diagram-5PPSVZJV-BJWf1nGo.js} +1 -1
- package/dist/worker/console/static/assets/channel-i1DjpDIw.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-DGXplIJs.js → chunk-2GRJ4B5K-BCPb5H0y.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-ur9OqjPn.js → chunk-2Q5K7J3B-Cfs3jeW2.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-D1KqU_Vi.js → chunk-5RXB4S5H-BhQI1B_T.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BlVZrbtM.js → chunk-5VM5RSS4-Cc_m4gyV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BAzz3nQy.js → chunk-6Q2QTUOP-JPqDS3IV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CP_nYuCV.js → chunk-GF5L2VYU-AeGX6EAg.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-CWvKQ80X.js → chunk-JWPE2WC7-pEyTjekV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-CyUki8wp.js → chunk-KBJHAD2P-Bi5UopKd.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-5NYzNKfY.js → chunk-RYQCIY6F-FkGFJxgQ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-DHdUgjS-.js → chunk-XXDRQBXY-D-7LGrV2.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DgDRdE1V.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DgDRdE1V.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-ANQ5g_t6.js → cose-bilkent-JH36ORCC-CCEDNDaX.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-CruT_4-0.js → cynefin-VYW2F7L2-DDd-t7fm.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-rbFo77ID.js → cynefinDiagram-MW4NZA55-LWe4Ikzr.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-asstxBQs.js → dagre-VZM6K2ZE-BBCZScc-.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DzTyBZwI.js → diagram-7IWD3JNH-B6liRVig.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-gw8ccH3B.js → diagram-B4RE2ZJO-DO_hIrpW.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-B8RpZzOf.js → diagram-LBJQPF4R-dmCy93uR.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-B1zJkCS9.js → diagram-Q27KOJAE-D6mFTIxP.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-B6JLWZmO.js → diagram-UB23O5K3-DCteVUfY.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-q2iZP-Ps.js → ebnfDiagram-BXEA7PRR-DTv530VK.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-B3dbbktQ.js → erDiagram-JOGREHBK-DCXDMxCr.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C-zc6-kB.js → flowDiagram-UKHOOZJN-BwEBLOJh.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-BSolnot9.js → ganttDiagram-PKOTCBZU-Ck-Vymjm.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Bo8jGxTk.js → gitGraphDiagram-DS77QQ5N-Hqs6X_3L.js} +1 -1
- package/dist/worker/console/static/assets/{index-DhfjOafu.js → index-BmMi-Bve.js} +82 -82
- package/dist/worker/console/static/assets/{index-CAZ2fC_X.css → index-Dh0Gh4X0.css} +1 -1
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-DQaZqJOH.js → infoDiagram-6WML65LV-Cjc6M9eg.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-DsdX1WnE.js → ishikawaDiagram-WSZJBQD7-CPchYZMl.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CP5EYDZL.js → journeyDiagram-NVQOT4AX-BABdJNNC.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-DAm14Dr8.js → kanban-definition-27J2QSJJ-1oXhbM4j.js} +1 -1
- package/dist/worker/console/static/assets/{linear-gZECoLc5.js → linear-PTmQ9LkV.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BRJNIM6p.js → mermaid.core-B6Lxil0V.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-Cja3kLkR.js → mindmap-definition-FAOFIHXS-BUqjNGEw.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-22ZGFisL.js → pegDiagram-VL7TDLO6-8D1N-orJ.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-BXin0Y1o.js → pieDiagram-7S7Q4E2Y-BlPMN9d9.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-Cw6T69Yt.js → quadrantDiagram-CIZ2JOQS-BIL5YDes.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-C1-gTieN.js → railroadDiagram-AXF67PYL-Dqmil3ie.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP--H5N2iI1.js → requirementDiagram-LRYGKXZP-C_-Dp4mH.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CgWAPeRz.js → sankeyDiagram-W5VNT64P-C0s5-bQS.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CrKqzGvt.js → sequenceDiagram-SI44F4Z6-DiVCHPfi.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-BB3wG06K.js → sizeCapture-X5ZJPWSS-CxQ9WVvx.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-DjsGgdgY.js → stateDiagram-OKZ733FA-D9mFHA-Y.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BqXQxfFC.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DYKEbvAR.js → swimlanes-SLNWSIFB-DDnC5l-M.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CXwWNRJl.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-eQOb5k9Y.js → timeline-definition-Z64GVDOM-C8QySPC-.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-B2-l9FIu.js → vennDiagram-T6HMQDX7-BeYdDiz6.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-BX2Cda3l.js → wardleyDiagram-T6FBY63Y-D8hjFWLH.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-D9S892ph.js → xychartDiagram-ELKLHX3M-ClqG_JGK.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/chat-view-types.js +1 -1
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +23 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +32 -0
- package/dist/worker/console/static-src/operator-chat/tools-catalog.js +21 -2
- package/dist/worker/console/static-src/operator-chat/turn-stream-controller.js +2 -0
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +30 -14
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +21 -1
- package/dist/workflows/dag/frontend-contract-facts.js +130 -0
- package/dist/workflows/dag/frontend-design-policy.js +99 -14
- package/dist/workflows/dag/frontend-implementation-contract.js +56 -8
- package/dist/workflows/dag/frontend-plan-render.js +13 -2
- package/dist/workflows/dag/frontend-shadow-dual-write.js +1 -0
- package/dist/workflows/dag/frontend-typed-event-store.js +3 -0
- package/dist/workflows/dag/init-hybrid.js +7 -3
- package/dist/workflows/dag/node-execution.js +54 -16
- package/docs/operations/README.md +1 -0
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/harness.json +1 -1
- package/package.json +2 -3
- package/skills/frontend-plan/SKILL.md +8 -0
- package/skills/frontend-plan/references/decision-contract.md +26 -3
- package/dist/worker/console/static/assets/channel-CXqdP5pW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D9C3agF7.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D9C3agF7.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Tp7V0U4q.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DUZWZWi8.js +0 -8
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Shared projections of committed frontend facts. Callers filter commit phase. */
|
|
3
|
+
function object(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
5
|
+
? value
|
|
6
|
+
: undefined;
|
|
7
|
+
}
|
|
8
|
+
/** Join requirement-owned identity with separately committed evidence by id.
|
|
9
|
+
* Later evidence declarations replace only the lanes they explicitly name. */
|
|
10
|
+
export function resolveFrontendContractRequirements(facts) {
|
|
11
|
+
const contractFacts = facts.map(object).filter((fact) => fact?.origin === "contract");
|
|
12
|
+
const evidenceById = new Map();
|
|
13
|
+
for (const fact of contractFacts) {
|
|
14
|
+
if (fact.kind !== "evidence-expectation" ||
|
|
15
|
+
typeof fact.requirementId !== "string")
|
|
16
|
+
continue;
|
|
17
|
+
evidenceById.set(fact.requirementId, {
|
|
18
|
+
...evidenceById.get(fact.requirementId),
|
|
19
|
+
...object(fact.evidence),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return contractFacts.flatMap((fact) => fact.kind === "requirement" && typeof fact.id === "string"
|
|
23
|
+
? [{
|
|
24
|
+
...fact,
|
|
25
|
+
id: fact.id,
|
|
26
|
+
evidence: { ...object(fact.evidence), ...evidenceById.get(fact.id) },
|
|
27
|
+
}]
|
|
28
|
+
: []);
|
|
29
|
+
}
|
|
30
|
+
function names(value) {
|
|
31
|
+
return Array.isArray(value)
|
|
32
|
+
? value.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
33
|
+
: [];
|
|
34
|
+
}
|
|
35
|
+
const evidenceStatus = z.enum(["required", "optional", "not-applicable"]);
|
|
36
|
+
export const frontendEvidenceExpectationSchema = z.object({
|
|
37
|
+
requirementId: z.string().min(1),
|
|
38
|
+
evidence: z.object({
|
|
39
|
+
static: evidenceStatus.optional(),
|
|
40
|
+
behavior: evidenceStatus.optional(),
|
|
41
|
+
mock: evidenceStatus.optional(),
|
|
42
|
+
"real-integration": evidenceStatus.optional(),
|
|
43
|
+
}).strict().refine((evidence) => Object.keys(evidence).length > 0, "declare at least one evidence lane"),
|
|
44
|
+
}).strict();
|
|
45
|
+
const concretePath = z.string().min(1).refine((value) => value === value.trim() &&
|
|
46
|
+
!/^[A-Za-z]:/.test(value) &&
|
|
47
|
+
!/[\\*?\[\]{}\0]/.test(value) &&
|
|
48
|
+
value.split("/").every((part) => part !== "" && part !== "." && part !== ".."), "must be a concrete relative POSIX file path without traversal");
|
|
49
|
+
export const frontendRequiredDeliverablesSchema = z.object({
|
|
50
|
+
items: z.array(z.object({
|
|
51
|
+
path: concretePath,
|
|
52
|
+
requirementId: z.string().min(1),
|
|
53
|
+
sourceFragmentId: z.string().min(1),
|
|
54
|
+
}).strict()),
|
|
55
|
+
}).strict();
|
|
56
|
+
/** Validate structure and provenance; the Contract model owns the distinction
|
|
57
|
+
* between delivery obligations, permissions, prohibitions and references. */
|
|
58
|
+
export function validateFrontendRequiredDeliverables(value, requirements) {
|
|
59
|
+
const declaration = frontendRequiredDeliverablesSchema.parse(value);
|
|
60
|
+
for (const item of declaration.items) {
|
|
61
|
+
const requirement = requirements.get(item.requirementId);
|
|
62
|
+
if (!requirement?.sourceFragmentIds.includes(item.sourceFragmentId)) {
|
|
63
|
+
throw new Error(`required deliverable ${item.path} has an unknown requirement/fragment binding`);
|
|
64
|
+
}
|
|
65
|
+
const text = requirement.sourceFragments?.get(item.sourceFragmentId);
|
|
66
|
+
const escaped = item.path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
67
|
+
// A sentence-ending period is punctuation; a dot followed by filename
|
|
68
|
+
// characters is still part of the token (app.ts must not match app.ts.map).
|
|
69
|
+
const exactPath = new RegExp(`(^|[^\\p{L}\\p{N}_./-])${escaped}(?=$|[^\\p{L}\\p{N}_./-]|\\.(?=$|[^\\p{L}\\p{N}_./-]))`, "u");
|
|
70
|
+
if (!text || !exactPath.test(text)) {
|
|
71
|
+
throw new Error(`required deliverable ${item.path} is not an exact path in frozen source fragment ${item.sourceFragmentId}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return declaration;
|
|
75
|
+
}
|
|
76
|
+
/** Read the latest explicit inventory. Never infer obligations from prose. */
|
|
77
|
+
export function extractRequiredDeliverablePaths(facts) {
|
|
78
|
+
const declaration = facts.map(object).filter((fact) => fact?.origin === "contract" && fact.kind === "required-deliverables").at(-1);
|
|
79
|
+
if (!declaration)
|
|
80
|
+
return [];
|
|
81
|
+
const { items } = frontendRequiredDeliverablesSchema.parse({ items: declaration.items });
|
|
82
|
+
return [...new Set(items.map((item) => item.path))].sort();
|
|
83
|
+
}
|
|
84
|
+
/** Apply state-flow replacement/removal semantics exactly as the plan compiler does. */
|
|
85
|
+
export function collectCanonicalStateFlowNames(committedFacts, includeRegistry = false) {
|
|
86
|
+
const uiStateNames = new Set();
|
|
87
|
+
const interactionNames = new Set();
|
|
88
|
+
for (const value of committedFacts) {
|
|
89
|
+
const record = object(value);
|
|
90
|
+
if (!record || (record.phase !== undefined && record.phase !== "committed"))
|
|
91
|
+
continue;
|
|
92
|
+
const fact = object(record.fact) ?? record;
|
|
93
|
+
if (fact.origin !== "plan")
|
|
94
|
+
continue;
|
|
95
|
+
if (includeRegistry && fact.kind === "state-registry") {
|
|
96
|
+
uiStateNames.clear();
|
|
97
|
+
interactionNames.clear();
|
|
98
|
+
for (const name of names(fact.uiStateNames))
|
|
99
|
+
uiStateNames.add(name);
|
|
100
|
+
for (const name of names(fact.interactionNames))
|
|
101
|
+
interactionNames.add(name);
|
|
102
|
+
}
|
|
103
|
+
if (fact.kind !== "state-flow")
|
|
104
|
+
continue;
|
|
105
|
+
for (const name of names(fact.removeUiStateNames)) {
|
|
106
|
+
uiStateNames.delete(name);
|
|
107
|
+
}
|
|
108
|
+
for (const name of names(fact.removeInteractionNames)) {
|
|
109
|
+
interactionNames.delete(name);
|
|
110
|
+
}
|
|
111
|
+
for (const state of Array.isArray(fact.uiStates) ? fact.uiStates : []) {
|
|
112
|
+
if (!state || typeof state !== "object" || Array.isArray(state))
|
|
113
|
+
continue;
|
|
114
|
+
const name = state.name;
|
|
115
|
+
if (typeof name === "string" && name.trim())
|
|
116
|
+
uiStateNames.add(name);
|
|
117
|
+
}
|
|
118
|
+
for (const interaction of Array.isArray(fact.interactions)
|
|
119
|
+
? fact.interactions
|
|
120
|
+
: []) {
|
|
121
|
+
if (!interaction || typeof interaction !== "object" || Array.isArray(interaction)) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const name = interaction.name;
|
|
125
|
+
if (typeof name === "string" && name.trim())
|
|
126
|
+
interactionNames.add(name);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { uiStateNames, interactionNames };
|
|
130
|
+
}
|
|
@@ -28,31 +28,37 @@ function sortFindings(findings) {
|
|
|
28
28
|
* visual UI-state dimension (it cannot cover behavioural interactions). */
|
|
29
29
|
export function checkUiDesignCoverage(contract) {
|
|
30
30
|
const findings = [];
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
const
|
|
31
|
+
// Explicit per-id coverage (dogfood r-plan-quality): one choice covers an
|
|
32
|
+
// interaction/state when its purpose equals the name OR the choice lists
|
|
33
|
+
// the name in covers. The old wildcards are gone — a single reuse-existing
|
|
34
|
+
// row no longer passes every interaction, and a non-empty stylingStrategy
|
|
35
|
+
// no longer passes every state.
|
|
36
|
+
const covered = new Set();
|
|
37
|
+
for (const choice of contract.uiComponentChoices ?? []) {
|
|
38
|
+
if (choice.purpose.trim())
|
|
39
|
+
covered.add(choice.purpose);
|
|
40
|
+
for (const id of choice.covers ?? []) {
|
|
41
|
+
if (id.trim())
|
|
42
|
+
covered.add(id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
38
45
|
for (const state of contract.uiStates) {
|
|
39
46
|
if (!state.applicable)
|
|
40
47
|
continue;
|
|
41
|
-
|
|
42
|
-
if (!covered) {
|
|
48
|
+
if (!covered.has(state.name)) {
|
|
43
49
|
findings.push({
|
|
44
50
|
code: "ui-design-coverage-missing",
|
|
45
|
-
message: `applicable UI state "${state.name}"
|
|
51
|
+
message: `applicable UI state "${state.name}" is not covered by any uiComponentChoices purpose or covers entry`,
|
|
46
52
|
path: state.name,
|
|
47
53
|
});
|
|
48
54
|
}
|
|
49
55
|
}
|
|
50
56
|
for (const interaction of contract.interactions) {
|
|
51
|
-
if (
|
|
57
|
+
if (covered.has(interaction.name))
|
|
52
58
|
continue;
|
|
53
59
|
findings.push({
|
|
54
60
|
code: "ui-design-coverage-missing",
|
|
55
|
-
message: `interaction "${interaction.name}"
|
|
61
|
+
message: `interaction "${interaction.name}" is not covered by any uiComponentChoices purpose or covers entry`,
|
|
56
62
|
path: interaction.name,
|
|
57
63
|
});
|
|
58
64
|
}
|
|
@@ -158,7 +164,7 @@ export function checkMockStrategy(contract, allowedMockStrategies) {
|
|
|
158
164
|
* A component specReference must still not point into clearly non-component
|
|
159
165
|
* spec areas — templates and the ai_workspace governance subtree cannot be the
|
|
160
166
|
* authoritative component definition. */
|
|
161
|
-
export function checkComponentChoice(contract, componentSpecCandidatePaths) {
|
|
167
|
+
export function checkComponentChoice(contract, componentSpecCandidatePaths, options) {
|
|
162
168
|
const findings = [];
|
|
163
169
|
const candidates = new Set(componentSpecCandidatePaths.map(normalizeRelativePath));
|
|
164
170
|
const isNonComponentSpecArea = (path) => {
|
|
@@ -200,6 +206,23 @@ export function checkComponentChoice(contract, componentSpecCandidatePaths) {
|
|
|
200
206
|
});
|
|
201
207
|
}
|
|
202
208
|
}
|
|
209
|
+
else if (choice.decision === "reuse-existing") {
|
|
210
|
+
if (!(choice.evidencePath && choice.evidencePath.trim().length > 0)) {
|
|
211
|
+
findings.push({
|
|
212
|
+
code: "component-reuse-evidence-missing",
|
|
213
|
+
message: `reuse-existing component choice #${index} is missing evidencePath naming the existing repo file that proves the reuse; a greenfield path must use decision=new`,
|
|
214
|
+
path: choice.purpose,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
else if (options?.hasFreshPathEvidence &&
|
|
218
|
+
!options.hasFreshPathEvidence(choice.evidencePath)) {
|
|
219
|
+
findings.push({
|
|
220
|
+
code: "component-reuse-path-not-fresh",
|
|
221
|
+
message: `reuse-existing component choice #${index} evidencePath "${choice.evidencePath}" has no fresh existence evidence (path not found); reuse requires an existing file — use decision=new for greenfield paths`,
|
|
222
|
+
path: choice.evidencePath,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
203
226
|
else if (choice.decision === "new") {
|
|
204
227
|
if (!choice.specReference || !choice.specReference.path) {
|
|
205
228
|
findings.push({
|
|
@@ -371,6 +394,64 @@ export function checkDesignApproval(designApproval) {
|
|
|
371
394
|
}
|
|
372
395
|
return [];
|
|
373
396
|
}
|
|
397
|
+
/**
|
|
398
|
+
* UX vocabulary collisions (item ③, dogfood dag-1788504923861-0f7b17a9):
|
|
399
|
+
* prioritize-planner-task / prioritizePlannerTask / prioritize-planner-tasks
|
|
400
|
+
* are one behavior renamed three ways. Case/separator-insensitive duplicates
|
|
401
|
+
* inside interactions or inside uiStates are blocking.
|
|
402
|
+
*/
|
|
403
|
+
export function checkUxNameCollisions(contract) {
|
|
404
|
+
const findings = [];
|
|
405
|
+
const normalize = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
406
|
+
const collisions = (names, kind) => {
|
|
407
|
+
const byNormalized = new Map();
|
|
408
|
+
const found = [];
|
|
409
|
+
for (const name of names) {
|
|
410
|
+
const key = normalize(name);
|
|
411
|
+
if (!key)
|
|
412
|
+
continue;
|
|
413
|
+
const existing = byNormalized.get(key);
|
|
414
|
+
if (existing !== undefined && existing !== name) {
|
|
415
|
+
found.push({
|
|
416
|
+
code: "ux-name-collision",
|
|
417
|
+
message: `"${name}" and "${existing}" are the same ${kind} name up to case/separator — one behavior must keep one stable name`,
|
|
418
|
+
path: name,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
byNormalized.set(key, name);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
return found;
|
|
426
|
+
};
|
|
427
|
+
findings.push(...collisions(contract.interactions.map((interaction) => interaction.name), "interaction"));
|
|
428
|
+
findings.push(...collisions(contract.uiStates.map((state) => state.name), "UI state"));
|
|
429
|
+
return findings;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Deliverable completeness (item ⑦): every file explicitly declared by Contract
|
|
433
|
+
* from frozen source fragments (projected into targets.requiredDeliverables)
|
|
434
|
+
* must be planned — bound by some requirement's implementationTargets or a
|
|
435
|
+
* verification target's file, or listed in targets.files. allowedPaths is a
|
|
436
|
+
* write boundary and never implies coverage.
|
|
437
|
+
*/
|
|
438
|
+
export function checkRequiredDeliverables(contract) {
|
|
439
|
+
const required = contract.targets.requiredDeliverables ?? [];
|
|
440
|
+
if (required.length === 0)
|
|
441
|
+
return [];
|
|
442
|
+
const planned = new Set([
|
|
443
|
+
...contract.targets.files,
|
|
444
|
+
...contract.requirements.flatMap((requirement) => requirement.implementationTargets),
|
|
445
|
+
...contract.verificationTargets.map((target) => target.file),
|
|
446
|
+
]);
|
|
447
|
+
return required
|
|
448
|
+
.filter((path) => !planned.has(path))
|
|
449
|
+
.map((path) => ({
|
|
450
|
+
code: "required-deliverable-unplanned",
|
|
451
|
+
message: `Contract declares required deliverable "${path}" but no requirement implementationTarget, verification target, or targets.files entry plans it`,
|
|
452
|
+
path,
|
|
453
|
+
}));
|
|
454
|
+
}
|
|
374
455
|
/** Aggregate the deterministic design-policy checks into a single
|
|
375
456
|
* deterministic result. Any finding blocks; an empty finding list approves. */
|
|
376
457
|
export function evaluateFrontendDesignPolicy(input) {
|
|
@@ -379,10 +460,14 @@ export function evaluateFrontendDesignPolicy(input) {
|
|
|
379
460
|
...checkUiDesignCoverage(input.contract),
|
|
380
461
|
...checkSourceFreshness(input.contract, input.sourceFreshness),
|
|
381
462
|
...checkMockStrategy(input.contract, input.allowedMockStrategies),
|
|
382
|
-
...checkComponentChoice(input.contract, input.componentSpecCandidatePaths
|
|
463
|
+
...checkComponentChoice(input.contract, input.componentSpecCandidatePaths, input.hasFreshPathEvidence
|
|
464
|
+
? { hasFreshPathEvidence: input.hasFreshPathEvidence }
|
|
465
|
+
: undefined),
|
|
383
466
|
...checkTargetPaths(input.contract, input.writeSetPatterns),
|
|
384
467
|
...checkDependencies(input.contract, input.allowedDependencies),
|
|
385
468
|
...checkUiStateAttribution(input.contract),
|
|
469
|
+
...checkUxNameCollisions(input.contract),
|
|
470
|
+
...checkRequiredDeliverables(input.contract),
|
|
386
471
|
]);
|
|
387
472
|
const classification = findings.length > 0 ? "blocked" : "approved";
|
|
388
473
|
const result = {
|
|
@@ -9,10 +9,12 @@ import { findPackageRoot } from "../../shared/package-metadata.js";
|
|
|
9
9
|
import { pathMatchesPattern } from "../../shared/git-progress.js";
|
|
10
10
|
import { resolveDagTaskSourcePath } from "../../task/dag-source-paths.js";
|
|
11
11
|
import { readTypedEventStoreFromJsonl } from "./frontend-typed-event-store.js";
|
|
12
|
+
import { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
|
|
13
|
+
export { extractRequiredDeliverablePaths } from "./frontend-contract-facts.js";
|
|
12
14
|
// Cycle-safe value imports: the check functions are hoisted declarations used
|
|
13
15
|
// only at call time, so the contract module and the policy module can depend
|
|
14
16
|
// on each other without module-init ordering issues.
|
|
15
|
-
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, } from "./frontend-design-policy.js";
|
|
17
|
+
import { checkDependencies, checkTargetPaths, checkUiDesignCoverage, checkUiStateAttribution, checkRequiredDeliverables, checkUxNameCollisions, } from "./frontend-design-policy.js";
|
|
16
18
|
import { isDagSourceBindingV2 } from "./types.js";
|
|
17
19
|
import { canonicalContractRel, classifyStructuredContractFailure, formatStructuredArtifactPointer, persistRuntimeSkeleton, } from "./structured-output-repair.js";
|
|
18
20
|
import { renderFrontendPlanMarkdown } from "./frontend-plan-render.js";
|
|
@@ -600,12 +602,30 @@ const uiComponentChoiceSchema = z
|
|
|
600
602
|
decision: z.enum(["specified", "reuse-existing", "new"]),
|
|
601
603
|
specReference: uiComponentSpecReferenceSchema.nullable().optional(),
|
|
602
604
|
// Optional: the decision enum already carries the semantic (specified →
|
|
603
|
-
// specReference, new → task-source traceability, reuse-existing →
|
|
604
|
-
//
|
|
605
|
-
// Omit for reuse-existing to keep plan output small.
|
|
605
|
+
// specReference, new → task-source traceability, reuse-existing →
|
|
606
|
+
// evidencePath). Omit for reuse-existing to keep plan output small.
|
|
606
607
|
rationale: z.string().min(1).optional(),
|
|
608
|
+
// Multi-id coverage (dogfood r-plan-quality): ONE choice may cover many
|
|
609
|
+
// UI states/interactions explicitly instead of the old implicit
|
|
610
|
+
// "reuse-existing covers everything" wildcard.
|
|
611
|
+
covers: z.array(z.string().min(1)).optional(),
|
|
612
|
+
// reuse-existing evidence (dogfood dag-1788504923861-0f7b17a9: 18
|
|
613
|
+
// reuse-existing rows cited conventions in a not-yet-written
|
|
614
|
+
// src/planner.ts): a repo-relative path to the existing file that
|
|
615
|
+
// proves the reuse. Enforced by superRefine + fresh-existence checks.
|
|
616
|
+
evidencePath: safePath.optional(),
|
|
607
617
|
})
|
|
608
|
-
.strict()
|
|
618
|
+
.strict()
|
|
619
|
+
.superRefine((choice, ctx) => {
|
|
620
|
+
if (choice.decision === "reuse-existing" &&
|
|
621
|
+
!(choice.evidencePath && choice.evidencePath.trim().length > 0)) {
|
|
622
|
+
ctx.addIssue({
|
|
623
|
+
code: z.ZodIssueCode.custom,
|
|
624
|
+
path: ["evidencePath"],
|
|
625
|
+
message: "decision=reuse-existing requires evidencePath naming the existing repo file that proves the reuse; use decision=new for greenfield paths",
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
});
|
|
609
629
|
const uiComponentChoicesSchema = z.array(uiComponentChoiceSchema);
|
|
610
630
|
/**
|
|
611
631
|
* Normalize unstable model emissions before the strict schema sees them:
|
|
@@ -680,6 +700,11 @@ export const frontendImplementationContractSchema = z
|
|
|
680
700
|
files: z.array(safePath).min(1),
|
|
681
701
|
routes: z.array(z.string().startsWith("/")).optional(),
|
|
682
702
|
publicApiChanges: z.array(z.string().min(1)).optional(),
|
|
703
|
+
// Runtime-owned deliverable contract (dogfood r-plan-quality):
|
|
704
|
+
// explicit, source-bound file inventory committed by Contract.
|
|
705
|
+
// The design-policy completeness gate checks ONLY these —
|
|
706
|
+
// allowedPaths is a write boundary, not a coverage obligation.
|
|
707
|
+
requiredDeliverables: z.array(safePath).optional(),
|
|
683
708
|
})
|
|
684
709
|
.strict(),
|
|
685
710
|
requirements: z.array(z
|
|
@@ -1634,7 +1659,12 @@ export function compactFrontendImplementationContract(value) {
|
|
|
1634
1659
|
const targets = asRecord(record.targets);
|
|
1635
1660
|
if (targets) {
|
|
1636
1661
|
const compactTargets = { ...targets };
|
|
1637
|
-
for (const field of [
|
|
1662
|
+
for (const field of [
|
|
1663
|
+
"files",
|
|
1664
|
+
"routes",
|
|
1665
|
+
"publicApiChanges",
|
|
1666
|
+
"requiredDeliverables",
|
|
1667
|
+
]) {
|
|
1638
1668
|
if (Object.hasOwn(compactTargets, field)) {
|
|
1639
1669
|
compactTargets[field] = dedupeJsonArray(compactTargets[field]);
|
|
1640
1670
|
}
|
|
@@ -2425,6 +2455,16 @@ export async function analyzeFrontendImplementationContract(input) {
|
|
|
2425
2455
|
files: referencedConcretePaths,
|
|
2426
2456
|
};
|
|
2427
2457
|
}
|
|
2458
|
+
// Both Plan precheck and policy materialization consume this exact inventory.
|
|
2459
|
+
// Never trust a Plan patch or infer delivery obligations from prose.
|
|
2460
|
+
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, input.contractNodeId ?? "frontend-contract-pi", "contract-typed-facts.jsonl"));
|
|
2461
|
+
const requiredDeliverables = extractRequiredDeliverablePaths(contractRecords
|
|
2462
|
+
.filter((record) => record.phase === "committed")
|
|
2463
|
+
.map((record) => record.fact));
|
|
2464
|
+
delete result.data.targets.requiredDeliverables;
|
|
2465
|
+
if (requiredDeliverables.length > 0) {
|
|
2466
|
+
result.data.targets.requiredDeliverables = requiredDeliverables;
|
|
2467
|
+
}
|
|
2428
2468
|
const blockingGaps = [
|
|
2429
2469
|
...(result.data.evidenceGaps ?? []),
|
|
2430
2470
|
...result.data.requirements.flatMap((item) => item.evidenceGap ? [item.evidenceGap] : []),
|
|
@@ -2632,6 +2672,7 @@ const FRONTEND_PLAN_PATCH_PROTECTED_PATHS = [
|
|
|
2632
2672
|
"/sourceBinding",
|
|
2633
2673
|
"/riskLevel",
|
|
2634
2674
|
"/targets/files",
|
|
2675
|
+
"/targets/requiredDeliverables",
|
|
2635
2676
|
"/mockApi/productionDefaultOff",
|
|
2636
2677
|
];
|
|
2637
2678
|
const PLAN_LEDGER_FACT_KINDS_FOR_COMPILE = [
|
|
@@ -2680,6 +2721,8 @@ function frontendPlanPatchProtectedPathViolations(patch) {
|
|
|
2680
2721
|
const targets = asRecord(record.targets);
|
|
2681
2722
|
if (targets && Object.hasOwn(targets, "files"))
|
|
2682
2723
|
violations.push("/targets/files");
|
|
2724
|
+
if (targets && Object.hasOwn(targets, "requiredDeliverables"))
|
|
2725
|
+
violations.push("/targets/requiredDeliverables");
|
|
2683
2726
|
const mockApi = asRecord(record.mockApi);
|
|
2684
2727
|
if (mockApi && Object.hasOwn(mockApi, "productionDefaultOff"))
|
|
2685
2728
|
violations.push("/mockApi/productionDefaultOff");
|
|
@@ -2864,6 +2907,8 @@ export async function analyzeFrontendPlanPatchCandidate(input) {
|
|
|
2864
2907
|
const policyPreFindings = [
|
|
2865
2908
|
...checkUiDesignCoverage(analysis.canonical),
|
|
2866
2909
|
...checkUiStateAttribution(analysis.canonical),
|
|
2910
|
+
...checkRequiredDeliverables(analysis.canonical),
|
|
2911
|
+
...checkUxNameCollisions(analysis.canonical),
|
|
2867
2912
|
...checkDependencies(analysis.canonical, await deriveAllowedDependenciesFromRun(input.runDir)),
|
|
2868
2913
|
...checkTargetPaths(analysis.canonical, await deriveWriteSetFromRun(input.runDir)),
|
|
2869
2914
|
];
|
|
@@ -3082,9 +3127,11 @@ export async function buildContractFromCommittedFacts(input) {
|
|
|
3082
3127
|
}
|
|
3083
3128
|
// b. Committed contract terminal fact (contract-finalized).
|
|
3084
3129
|
const contractRecords = await readTypedEventStoreFromJsonl(path.join(input.runDir, contractNodeId, "contract-typed-facts.jsonl"));
|
|
3085
|
-
const
|
|
3130
|
+
const committedContractFacts = contractRecords
|
|
3086
3131
|
.filter((record) => record.phase === "committed")
|
|
3087
|
-
.map((record) => record.fact
|
|
3132
|
+
.map((record) => record.fact);
|
|
3133
|
+
const contractKinds = new Set(committedContractFacts
|
|
3134
|
+
.map((fact) => fact?.kind)
|
|
3088
3135
|
.filter((kind) => typeof kind === "string"));
|
|
3089
3136
|
if (!contractKinds.has("contract-finalized")) {
|
|
3090
3137
|
return {
|
|
@@ -3139,6 +3186,7 @@ export async function buildContractFromCommittedFacts(input) {
|
|
|
3139
3186
|
runDir: input.runDir,
|
|
3140
3187
|
rawContractText: serializeDeterministicJson(merged),
|
|
3141
3188
|
sourceBinding: input.sourceBinding,
|
|
3189
|
+
contractNodeId,
|
|
3142
3190
|
});
|
|
3143
3191
|
return {
|
|
3144
3192
|
ok: true,
|
|
@@ -12,8 +12,19 @@ function bulletList(items) {
|
|
|
12
12
|
return "";
|
|
13
13
|
return items.map((item) => `- ${item}`).join("\n");
|
|
14
14
|
}
|
|
15
|
+
/** The runtime's deterministic fallback for an outcome the plan did not
|
|
16
|
+
* author. It is bookkeeping for the schema's non-empty constraint, not user
|
|
17
|
+
* prose — the renderer must never present it as requirement content. */
|
|
18
|
+
const IMPLIED_OUTCOME_PREFIX = "implied by contract:";
|
|
15
19
|
function requirementsCoverage(contract) {
|
|
16
|
-
const lines = contract.requirements.map((requirement) =>
|
|
20
|
+
const lines = contract.requirements.map((requirement) => {
|
|
21
|
+
const targets = requirement.implementationTargets.join(", ");
|
|
22
|
+
const verificationTargets = requirement.verificationTargetIds.join(", ");
|
|
23
|
+
const authoredOutcome = requirement.expectedOutcome.startsWith(IMPLIED_OUTCOME_PREFIX)
|
|
24
|
+
? ""
|
|
25
|
+
: ` — ${requirement.expectedOutcome}`;
|
|
26
|
+
return `- ${requirement.id} → ${targets || "(no targets)"} [${verificationTargets || "no VT"}]${authoredOutcome}`;
|
|
27
|
+
});
|
|
17
28
|
return lines.length > 0 ? lines.join("\n") : "_(no requirements)_";
|
|
18
29
|
}
|
|
19
30
|
function uiStateHandling(contract) {
|
|
@@ -98,7 +109,7 @@ export function renderFrontendPlanMarkdown(contract) {
|
|
|
98
109
|
interactionNotes(contract),
|
|
99
110
|
"",
|
|
100
111
|
"## Mock & API Strategy",
|
|
101
|
-
contract.mockApi
|
|
112
|
+
contract.mockApi?.strategy || "_(not specified)_",
|
|
102
113
|
"",
|
|
103
114
|
"## Dependency Policy",
|
|
104
115
|
contract.dependencyPolicy || "_(not specified)_",
|
|
@@ -95,6 +95,8 @@ export const CONTRACT_FACT_KINDS = [
|
|
|
95
95
|
"open-question",
|
|
96
96
|
"split-proposal",
|
|
97
97
|
"openspec-selection",
|
|
98
|
+
"ui-state-declaration",
|
|
99
|
+
"required-deliverables",
|
|
98
100
|
"contract-finalized",
|
|
99
101
|
];
|
|
100
102
|
export const CONTRACT_TERMINAL_FACT_KINDS = ["contract-finalized"];
|
|
@@ -124,6 +126,7 @@ export const PLAN_RECORD_FACT_KINDS = [
|
|
|
124
126
|
"plan-requirement",
|
|
125
127
|
"plan-verification-target",
|
|
126
128
|
"plan-evidence-gap",
|
|
129
|
+
"state-registry",
|
|
127
130
|
];
|
|
128
131
|
/**
|
|
129
132
|
* A+B (AC-009): typed issue category shared by review and design change
|
|
@@ -3344,13 +3344,16 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3344
3344
|
allowedPaths: readOnlyPaths,
|
|
3345
3345
|
forbiddenPaths,
|
|
3346
3346
|
skills: FRONTEND_CONTRACT_SKILLS,
|
|
3347
|
-
outputContract: "Typed requirement facts plus a concise Markdown contract. Submit through the incremental typed tools record_requirement / record_constraint / record_evidence_expectation / record_handoff_intent / record_open_question / record_split_proposal / record_openspec_selection, then call finalize_contract exactly once.
|
|
3347
|
+
outputContract: "Typed requirement facts plus a concise Markdown contract. Submit through the incremental typed tools record_requirement / record_constraint / record_evidence_expectation / record_handoff_intent / record_open_question / record_split_proposal / record_ui_state / record_required_deliverables / record_openspec_selection, then call finalize_contract exactly once. record_requirement takes only the canonical ledger requirement id — the runtime owns the authoritative text, source spans, fragment bindings, and disposition. UI-visible or interactive requirements register a non-blocking frontend-test handoff intent, and any source-declared UI-state table is extracted verbatim through record_ui_state. End finalize_contract with a single contract disposition of ready | ready-with-assumptions | blocked. Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations; do not fix target files, components, or implementation methods as requirements. When OpenSpec candidates exist, classify only the ones you actually use: call record_openspec_selection once per required/relevant path; never enumerate irrelevant candidates (unmentioned defaults to irrelevant) and never emit a fenced selection JSON. No file writes.",
|
|
3348
3348
|
subtask_prompt: [
|
|
3349
3349
|
"OUTPUT BUDGET DISCIPLINE (hard requirement, extreme-environment safe): the provider output window is small — NEVER attempt to emit the whole contract in one response; a single large JSON dump will be truncated and rejected. Incremental submission through the typed tools is the ONLY supported output mode. Start submitting with the FIRST tool call: after each read, call record_requirement for the requirements you have already confirmed, one or a few per call. Every tool-call round MUST make progress by submitting at least one record_* fact. Do not re-read the same source file that is already materialized in this session; read each file at most once.",
|
|
3350
3350
|
"Read task source and produce a concise frontend implementation contract as typed requirement facts plus narrative Markdown.",
|
|
3351
|
-
"
|
|
3352
|
-
"
|
|
3351
|
+
"Confirm each requirement by the SAME id as the ledger canonical requirement it covers (sourceBinding.requirementIds, e.g. AC-001) — do NOT invent new REQ/BR prefixed ids for canonical requirements: the compiled contract must match the ledger canonical requirement ids exactly or schema validation rejects it (unknown requirement id). record_requirement takes ONLY the canonical id; the runtime commits the authoritative text and sourceFragmentIds from the frozen ledger. Never pass text/statement/sourceFragmentIds yourself — model rewrites and JSON-stringified fragment arrays are rejected.",
|
|
3352
|
+
"Requirement semantics, source spans, dispositions, and fragment bindings are ledger/runtime-owned. If a canonical requirement is genuinely blocked, say so in the Markdown contract narrative and finalize with the matching disposition instead of trying to encode it in the requirement fact.",
|
|
3353
3353
|
"Register evidence expectations for each requirement across static, behavior, Mock, and real integration as required | optional | not-applicable; required must follow from user requirements, task risk, or project governance, never from model convenience. For UI-visible or interactive requirements, register a non-blocking frontend-test handoff intent.",
|
|
3354
|
+
'Use record_evidence_expectation with {requirementId,evidence:{static,behavior,mock,"real-integration"}}; every lane is required | optional | not-applicable. Requirement text and provenance remain runtime-owned.',
|
|
3355
|
+
'Before finalize_contract ready, call record_required_deliverables once with the complete {items:[{path,requirementId,sourceFragmentId}]} inventory, or {items:[]} when no file delivery is mandatory. Interpret the original source, including lists and tables: allowedPaths/only-allowed-to-modify is permission, not an obligation; do not promote prohibited files, examples or references into deliverables. Paths must occur exactly in a frozen source fragment bound to that canonical requirement. Correct the whole inventory before finalizing if needed.',
|
|
3356
|
+
"Authoritative UI states: when the task source declares a UI-state table (state id / trigger / observable outcome), extract it VERBATIM through record_ui_state, one call per state, using the source's own state ids. The planner must bind these ids later — do not rename, merge, or invent states.",
|
|
3354
3357
|
"Cover scope, non-goals, acceptance criteria, UI states, target runtime environment, risks, and verification expectations. Do not fix target files, components, styling, or implementation methods as requirements; leave those to Scout and Plan.",
|
|
3355
3358
|
"If the task is too large for one bounded writer, record a task split proposal instead of silently widening scope.",
|
|
3356
3359
|
"End the contract with a single disposition: ready, ready-with-assumptions (bounded assumptions that do not change product behavior), or blocked.",
|
|
@@ -3408,6 +3411,7 @@ async function buildFrontendHybridDagFromTask(sources) {
|
|
|
3408
3411
|
"Record only: requirement-to-file/verification coverage; component/styling choices; applicable UI state and interaction behavior; data/Mock strategy; and a dependency policy or genuine evidence gap. Reuse Scout paths. If scope is missing, record a blocking gap instead of inventing a path.",
|
|
3409
3412
|
"Use the typed tool schemas as the field contract. Runtime owns schemaVersion, sourceBinding, riskLevel, targets.files, mockApi.productionDefaultOff, aliases, command allowlisting, path containment, and final validation; do not restate those rules or emit a full JSON contract.",
|
|
3410
3413
|
`Cover each frozen requirement ID exactly once: ${requirementIds.join(", ") || "(none)"}. Bind every verification target to a frozen commandId from the directory above plus a Scout-confirmed file. Behavior commands prove observable behavior: one target may cover multiple related requirementIds when one test behavior proves them together; do not mechanically create one target per requirement. A behavior target id is the stable machine trace token and its file must be a test file. Static commands are project-wide checks traced by file and command only.`,
|
|
3414
|
+
"UX vocabulary protocol: record_state_registry FIRST with the full global vocabulary — one stable kebab-case behavior-domain name per UI state/interaction (e.g. planner-task-edit, focus-queue-move), never one name per AC number and never a rename of an already-recorded concept. Coverage slices by requirement; UX does not. Then record_state_flow entries whose names all come from that registry; uiState names must use the contract's declared authoritative ids (declaredUiStates in the plan input) when present. Retry attempts see committedUx in this input — reuse those exact names. Components: one choice may cover many state/interaction ids via covers; reuse-existing requires evidencePath naming an existing repo file (greenfield must be decision=new).",
|
|
3411
3415
|
...(requiresOpenspecClassification ? ["When a component choice uses an OpenSpec selection, cite that selection; otherwise do not classify unrelated candidates."] : []),
|
|
3412
3416
|
"Call finalize_plan exactly once after the necessary typed facts. Return no Markdown narrative.",
|
|
3413
3417
|
"TOOL-ONLY PLAN: Do not read Contract/Scout stdout, task sources, or Scout-confirmed target files. Contract and Scout already own evidence discovery; use the injected upstream facts, record a genuine evidence gap when those facts are insufficient, and start committing record_* facts immediately. For decision=new, pass sourceRequirementIds to record_component_choice; runtime derives the exact PRD citation from the frozen ledger.",
|