@tea-agent/loop-agent 0.44.0-next.10 → 0.44.0-next.11

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.44.0-next.10",
4
- "gitSha": "9ab06d59ee69d2e55b671bba52ee83e0362ec56f",
5
- "builtAt": "2026-09-17T03:17:20.149Z"
3
+ "version": "0.44.0-next.11",
4
+ "gitSha": "332db6a894b640eef5c495e3a3ba92b383fcf259",
5
+ "builtAt": "2026-09-17T05:51:06.283Z"
6
6
  }
@@ -0,0 +1,6 @@
1
+ /** DAG Pi 会话族兼容转发:既有测试从 entry 导入的符号统一经此转发,逐步迁移后移除。 */
2
+ export { estimateFrontendPlanRequirementRecordCalls, batchFrontendPlanRequirements, FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY, FRONTEND_PLAN_BATCH_MAX_SESSIONS, mapWithConcurrency, buildFrontendPlanWorkload, collectFrontendPlanRequirementIds, loadOrCreateFrontendPlanCoverageLayout, collectFrontendVerificationCommandFiles } from "./plan-batches.js";
3
+ export { compactFrontendPlanLedgerContext, compactFrontendPlanPromptForRequirementSlice } from "./plan-prompts.js";
4
+ export { isPlannerThinkingExhausted, isWriterThinkingExhausted, readWriterThinkingExhaustionEvidence, WRITER_BUDGET_EXHAUSTED_CATEGORY, WRITER_TOKEN_BUDGET, WRITER_THINKING_EXHAUSTED_CATEGORY, PLANNER_THINKING_EXHAUSTED_CATEGORY } from "./writer-evidence.js";
5
+ export { aggregateParallelPiResults, combineSequentialPiResults, runFrontendScoutParallelSessions } from "./scout-parallel.js";
6
+ export { runFrontendContractSegmentedSessions, runFrontendPlanSegmentedSessions, runFrontendReviewSegmentedSessions, runFrontendScoutSegmentedSessions } from "./segmented-plan.js";
@@ -0,0 +1,280 @@
1
+ /** DAG Pi 分段会话的批次规划:work 装配、批次上限、覆盖布局与并发映射。覆盖布局会读写 coverage-layout.json;禁止反向依赖执行器入口。 */
2
+ import path from "node:path";
3
+ import { readFile, stat } from "node:fs/promises";
4
+ import { z } from "zod";
5
+ import { FRONTEND_SCOPE_TARGET_BYTES, packFrontendInputUnits, parseFrontendInputBlock } from "../../../workflows/dag/frontend-input-projection.js";
6
+ import { sha256OfCanonicalJson } from "../../../task/contract/hash.js";
7
+ import { writeDagNodeJsonArtifact } from "../../../infrastructure/harness/artifact-store.js";
8
+ import { isRecordObject } from "../guards.js";
9
+ export const FRONTEND_PLAN_SEGMENTS = [
10
+ {
11
+ id: "coverage",
12
+ toolNames: new Set([
13
+ "record_plan_requirement",
14
+ "record_plan_group_coverage",
15
+ "record_plan_verification_target",
16
+ "record_plan_evidence_gap",
17
+ "adopt_staged_fact",
18
+ ]),
19
+ instruction: [
20
+ "PLAN PHASE — requirement coverage only.",
21
+ "Your ONLY job: for every frozen requirement, emit record_plan_requirement (requirement → implementation files) and record_plan_verification_target facts (verification target bound to requirement ids and files). Group related requirements under one non-static behavior target when one observable test behavior proves them together; do not mechanically create one target per requirement. Target ids identify contract entries, not test-title markers. Reuse affected existing test files and their names; do not add tests or rename titles just to carry generated ids. Never submit prose as a symbol. Do NOT record components, UI states, mock, dependency, or routes — a follow-up session owns those.",
22
+ "A coverage session is complete only when EVERY requirement assigned to this session (the full inventory, or the exact COVERAGE BATCH / shard list when present) has committed coverage facts: a record_plan_requirement entry plus verification targets, or a committed evidence gap. Keep committing in batches of up to 4 record_* calls per assistant message until then; do not write a concluding summary while any assigned requirement is still uncommitted — an early stop strands the remainder into a MISSING-FACT repair session and doubles the sessions needed.",
23
+ "If a requirement genuinely cannot have a verification target, record a non-empty record_plan_evidence_gap. Do not call finalize_plan; it is not available in this phase.",
24
+ ].join(" "),
25
+ },
26
+ {
27
+ id: "ux-registry",
28
+ toolNames: new Set(["record_state_registry", "adopt_staged_fact"]),
29
+ instruction: [
30
+ "PLAN PHASE — global UX vocabulary.",
31
+ "Bootstrap the global UX vocabulary from the execution-group index and authoritative declared states. This is navigation, not permission to decide unseen behavior. Detailed complete scopes may extend the registry with replace:true while preserving live names. UI states use declaredUiStates ids when present. Interaction names are stable kebab-case behavior domains; merge requirements that describe the same behavior instead of renaming it per AC slice. Empty arrays explicitly declare that no UX vocabulary applies. Do not record component choices or state-flow details in this phase.",
32
+ "Do not call finalize_plan; it is not available in this phase.",
33
+ ].join(" "),
34
+ },
35
+ {
36
+ id: "ux-local",
37
+ toolNames: new Set([
38
+ "record_state_registry",
39
+ "record_component_choice",
40
+ "record_state_flow",
41
+ "record_plan_verification_target",
42
+ "adopt_staged_fact",
43
+ ]),
44
+ instruction: [
45
+ "PLAN PHASE — global UX decisions.",
46
+ "Requirements and verification targets are already committed in the ledger; do not re-record unchanged facts. Review the current complete execution-group scope and the committed global UX registry together, then record each component choice, UI state and interaction exactly once. Bind each applicable state to its verificationTargetIds; the runtime derives the reverse VT.uiStates relation. If a VT requires correction, record_plan_verification_target with replace:true is available after declaring its states; preserve its requirement coverage. Multiple requirements describing one behavior share one registry name and state-flow entry; never repeat or rename it per AC. Cross-cutting data flow belongs to the global Mock/data phase. Do not record routes, Mock/API policy, dependencies, or design deviations here.",
47
+ "Do not call finalize_plan; it is not available in this phase.",
48
+ ].join(" "),
49
+ },
50
+ {
51
+ id: "global-route",
52
+ toolNames: new Set(["record_route_selection", "adopt_staged_fact"]),
53
+ instruction: [
54
+ "PLAN PHASE — global route decision.",
55
+ "Use the Scout target surface and record only the selected route(s). Do not record requirement-local UX, Mock/data, dependency, or deviation facts. Do not call finalize_plan.",
56
+ ].join(" "),
57
+ },
58
+ {
59
+ id: "global-mock-data",
60
+ toolNames: new Set(["record_data_flow", "record_mock_api", "record_mock_endpoint", "adopt_staged_fact"]),
61
+ instruction: [
62
+ "PLAN PHASE — global Mock/API and data policy.",
63
+ "Record the cross-cutting interaction-to-endpoint data flow and Mock/API strategy only. Keep this decision set separate from route, component, state, dependency, and deviation facts. Do not call finalize_plan.",
64
+ ].join(" "),
65
+ },
66
+ {
67
+ id: "global-dependency-deviation",
68
+ toolNames: new Set(["record_dependency", "record_design_deviation", "adopt_staged_fact"]),
69
+ instruction: [
70
+ "PLAN PHASE — global dependency and design-deviation policy.",
71
+ "Record only dependency policy and design-evidence conflicts. Do not record route, Mock/data, or requirement-local UX facts. Do not call finalize_plan.",
72
+ ].join(" "),
73
+ },
74
+ ];
75
+ export function estimateFrontendPlanRequirementRecordCalls(fact) {
76
+ const record = fact && typeof fact === "object" && !Array.isArray(fact)
77
+ ? fact
78
+ : {};
79
+ const declaredTargetCount = Array.isArray(record.verificationTargetIds)
80
+ ? record.verificationTargetIds.filter((value) => typeof value === "string" && value.trim()).length
81
+ : Array.isArray(record.verificationTargets)
82
+ ? record.verificationTargets.length
83
+ : 0;
84
+ const evidence = record.evidence && typeof record.evidence === "object"
85
+ ? record.evidence
86
+ : undefined;
87
+ const targetCount = Math.max(declaredTargetCount, evidence?.behavior === "required" ? 2 : 1);
88
+ return 1 + targetCount;
89
+ }
90
+ export function batchFrontendPlanRequirements(input) {
91
+ const batches = [];
92
+ let current = [];
93
+ let currentCost = 0;
94
+ for (const id of input.requirementIds) {
95
+ const cost = Math.max(1, input.requirementCosts?.get(id) ?? 2);
96
+ if (current.length > 0 &&
97
+ (currentCost + cost > input.maxEstimatedRecordCalls ||
98
+ (input.maxRequirements !== undefined &&
99
+ current.length >= input.maxRequirements))) {
100
+ batches.push(current);
101
+ current = [];
102
+ currentCost = 0;
103
+ }
104
+ current.push(id);
105
+ currentCost += cost;
106
+ }
107
+ if (current.length > 0)
108
+ batches.push(current);
109
+ return batches;
110
+ }
111
+ export const FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS = 10;
112
+ export const FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY = 4;
113
+ export const FRONTEND_PLAN_BATCH_MAX_SESSIONS = 128;
114
+ export const FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS = 8;
115
+ export const FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS = 24;
116
+ export async function mapWithConcurrency(items, limit, worker, shouldReduceConcurrency) {
117
+ const results = new Array(items.length);
118
+ let nextIndex = 0;
119
+ let concurrency = Math.max(1, limit);
120
+ const running = new Map();
121
+ try {
122
+ while (nextIndex < items.length || running.size > 0) {
123
+ while (nextIndex < items.length && running.size < concurrency) {
124
+ const index = nextIndex++;
125
+ running.set(index, worker(items[index], index).then(result => ({ index, result })));
126
+ }
127
+ const { index, result } = await Promise.race(running.values());
128
+ running.delete(index);
129
+ results[index] = result;
130
+ // Drain existing work; only pending shards use the reduced cap.
131
+ // Failed shards remain failed and receive no extra retry allowance.
132
+ if (shouldReduceConcurrency(result))
133
+ concurrency = Math.max(1, Math.floor(concurrency / 2));
134
+ }
135
+ }
136
+ catch (error) {
137
+ await Promise.allSettled(running.values());
138
+ throw error;
139
+ }
140
+ return results;
141
+ }
142
+ export function collectFrontendVerificationCommandFiles(basePrompt) {
143
+ const files = new Set();
144
+ const configRe = /--config\s+([\w@./-]+\.(?:js|mjs|cjs|ts|json))/g;
145
+ const checkRe = /node\s+--check\s+([\w@./-]+\.(?:js|mjs|cjs))/g;
146
+ for (const re of [configRe, checkRe]) {
147
+ for (const match of basePrompt.matchAll(re)) {
148
+ const file = match[1];
149
+ if (file && file.includes("/"))
150
+ files.add(file);
151
+ }
152
+ }
153
+ return [...files].sort();
154
+ }
155
+ export function countFrontendPlanTargetSurfaces(basePrompt) {
156
+ const match = /<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/.exec(basePrompt);
157
+ if (!match)
158
+ return undefined;
159
+ for (const line of match[0].split(/\r?\n/)) {
160
+ try {
161
+ const payload = JSON.parse(line);
162
+ if (Array.isArray(payload.targetSurface))
163
+ return payload.targetSurface.length;
164
+ }
165
+ catch {
166
+ // surrounding lines are prose
167
+ }
168
+ }
169
+ return undefined;
170
+ }
171
+ export function buildFrontendPlanWorkload(input) {
172
+ const allRequirementIds = input.requirementIds;
173
+ const compiledInput = parseFrontendInputBlock(input.basePrompt, "plan")?.payload;
174
+ const fullUnits = new Map(compiledInput?.requirements.map(r => [r.id, r]) ?? []);
175
+ const declaredGroups = Array.isArray(compiledInput?.executionGroups)
176
+ ? compiledInput.executionGroups.filter(isRecordObject) : [];
177
+ const workGroups = declaredGroups.map(g => ({
178
+ id: String(g.id), kind: String(g.kind),
179
+ requirementIds: Array.isArray(g.requirementIds)
180
+ ? g.requirementIds.filter((id) => typeof id === "string" && allRequirementIds.includes(id)) : [],
181
+ })).filter(g => g.requirementIds.length);
182
+ const groupedIds = new Set(workGroups.flatMap(g => g.requirementIds));
183
+ for (const id of allRequirementIds) {
184
+ if (!groupedIds.has(id))
185
+ workGroups.push({ id, kind: "unclassified", requirementIds: [id] });
186
+ }
187
+ const workCost = (ids) => 1 + ids.reduce((total, id) => total + Math.max(1, (input.requirementCosts?.get(id) ?? 2) - 1), 0);
188
+ const policy = input.sessionOptions.frontendExecutionPolicy;
189
+ const targetBytes = policy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
190
+ const buildWorkBatches = (ids) => {
191
+ const work = workGroups.flatMap((g, index) => {
192
+ const members = g.requirementIds.filter(id => ids.includes(id));
193
+ return members.length ? [{ id: `${index}:${g.id}`, requirementIds: members,
194
+ requirements: members.map(id => fullUnits.get(id) ?? { id }), estimatedCalls: workCost(members) }] : [];
195
+ });
196
+ // Pack complete work units, not the repeated request scaffold. Deducting
197
+ // fixed context/tools can leave a one-byte budget and force one AC per
198
+ // session without reducing that overhead. The SDK checks the actual
199
+ // request against model capacity; capacity recovery splits unfinished work.
200
+ return packFrontendInputUnits(work, {
201
+ targetBytes,
202
+ maxUnits: policy?.maxScopeUnits ?? 4,
203
+ maxCost: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS, cost: g => g.estimatedCalls,
204
+ }).map(batch => batch.flatMap(g => g.requirementIds));
205
+ };
206
+ // This renderer also carries the Plan's evolving UX checkpoint on resume.
207
+ // Only the Contract/Scout-derived input participates in ownership binding.
208
+ const { committedUx: _committedUx, ...frozenInput } = compiledInput ?? {};
209
+ return {
210
+ frozenInput, workGroups, buildWorkBatches,
211
+ compactEligible: input.requirementCosts !== undefined &&
212
+ workGroups.length <= FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS &&
213
+ workGroups.reduce((total, group) => total + workCost(group.requirementIds), 0) <= FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS &&
214
+ countFrontendPlanTargetSurfaces(input.basePrompt) === 1,
215
+ };
216
+ }
217
+ export function collectFrontendPlanRequirementIds(contractFacts) {
218
+ return [
219
+ ...new Set(contractFacts
220
+ .filter((record) => record.fact?.kind ===
221
+ "requirement")
222
+ .map((record) => record.fact?.id)
223
+ .filter((id) => typeof id === "string")),
224
+ ];
225
+ }
226
+ export const frontendPlanCoverageLayoutSchema = z.object({
227
+ schemaVersion: z.literal(1),
228
+ bindingSha256: z.string(),
229
+ coverageBatches: z.array(z.array(z.string().min(1)).min(1)).min(1),
230
+ layoutSha256: z.string(),
231
+ }).strict();
232
+ export async function loadOrCreateFrontendPlanCoverageLayout(input) {
233
+ const fileName = "coverage-layout.json";
234
+ const file = path.join(input.runDir, input.nodeId, fileName);
235
+ const bindingSha256 = sha256OfCanonicalJson(input.binding);
236
+ let raw;
237
+ try {
238
+ raw = await readFile(file, "utf8");
239
+ }
240
+ catch (error) {
241
+ if (error.code !== "ENOENT")
242
+ throw error;
243
+ }
244
+ let layout;
245
+ if (raw !== undefined) {
246
+ layout = frontendPlanCoverageLayoutSchema.parse(JSON.parse(raw));
247
+ }
248
+ else {
249
+ // Losing the ownership receipt must never repartition acknowledged facts.
250
+ for (const relative of ["plan-typed-facts.jsonl", "parallel"]) {
251
+ const existing = await stat(path.join(input.runDir, input.nodeId, relative)).catch(error => {
252
+ if (error.code !== "ENOENT")
253
+ throw error;
254
+ return undefined;
255
+ });
256
+ if (existing && (existing.isDirectory() || existing.size > 0)) {
257
+ throw Error("FRONTEND_PLAN_LAYOUT_MISSING: preserve the existing ledger and restart its owning phase");
258
+ }
259
+ }
260
+ const descriptor = {
261
+ schemaVersion: 1, bindingSha256,
262
+ coverageBatches: input.workload.compactEligible ? [input.requirementIds] : input.workload.buildWorkBatches(input.requirementIds),
263
+ };
264
+ layout = { ...descriptor, layoutSha256: sha256OfCanonicalJson(descriptor) };
265
+ }
266
+ const { layoutSha256, ...descriptor } = layout;
267
+ if (layout.bindingSha256 !== bindingSha256 || layoutSha256 !== sha256OfCanonicalJson(descriptor)) {
268
+ throw Error("FRONTEND_PLAN_LAYOUT_MISMATCH: frozen coverage ownership or input binding changed");
269
+ }
270
+ const members = layout.coverageBatches.flat();
271
+ const owners = new Map(layout.coverageBatches.flatMap((batch, index) => batch.map(id => [id, index])));
272
+ if (members.length !== owners.size || members.length !== input.requirementIds.length ||
273
+ input.requirementIds.some(id => !owners.has(id)) ||
274
+ input.workload.workGroups.some(group => new Set(group.requirementIds.map(id => owners.get(id))).size !== 1)) {
275
+ throw Error("FRONTEND_PLAN_LAYOUT_INVALID: coverage must partition complete execution groups exactly once");
276
+ }
277
+ if (raw === undefined)
278
+ await writeDagNodeJsonArtifact(input.runDir, input.nodeId, fileName, layout);
279
+ return layout.coverageBatches;
280
+ }
@@ -0,0 +1,367 @@
1
+ /** DAG Pi 分段会话的提示词投影:覆盖/UX/全局阶段 prompt 与账本上下文压缩,禁止反向依赖执行器入口。 */
2
+ import { projectFrontendInputScope } from "../../../workflows/dag/frontend-input-projection.js";
3
+ import { isRecordObject } from "../guards.js";
4
+ import { committedFactFromPlanRecord, planFactScopeIntersects, planFactStringList } from "../plan/facts.js";
5
+ export function compactPromptString(value, _maxChars) {
6
+ return typeof value === "string" && value.trim().length ? value.trim() : undefined;
7
+ }
8
+ export function compactPromptStringArray(value, _maxEntries = 12, maxChars = 180) {
9
+ if (!Array.isArray(value))
10
+ return [];
11
+ return value
12
+ .map((item) => compactPromptString(item, maxChars))
13
+ .filter((item) => item !== undefined);
14
+ }
15
+ export function compactFrontendPlanPromptForRequirementSlice(basePrompt, requirementIds, options = {}) {
16
+ const slice = [...new Set(requirementIds.filter((id) => id.trim().length > 0))];
17
+ if (slice.length === 0)
18
+ return basePrompt;
19
+ const planInputPattern = /<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/;
20
+ const planInputMatch = planInputPattern.exec(basePrompt);
21
+ if (!planInputMatch)
22
+ return basePrompt;
23
+ const blockLines = planInputMatch[0].split(/\r?\n/);
24
+ let payload;
25
+ for (const line of blockLines) {
26
+ try {
27
+ const parsed = JSON.parse(line);
28
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
29
+ payload = parsed;
30
+ break;
31
+ }
32
+ }
33
+ catch {
34
+ // The surrounding block contains prose; only its JSON line is data.
35
+ }
36
+ }
37
+ if (!payload || !Array.isArray(payload.requirements))
38
+ throw Error("FRONTEND_INPUT_INVALID: plan inventory is not parseable");
39
+ const requirementsById = new Map();
40
+ for (const value of payload.requirements) {
41
+ if (!value || typeof value !== "object" || Array.isArray(value))
42
+ continue;
43
+ const requirement = value;
44
+ if (typeof requirement.id === "string") {
45
+ requirementsById.set(requirement.id, requirement);
46
+ }
47
+ }
48
+ const requirements = slice.map((id) => requirementsById.get(id));
49
+ if (requirements.some((requirement) => requirement === undefined)) {
50
+ throw Error(`FRONTEND_INPUT_SCOPE_MISSING: ${slice.filter(id => !requirementsById.has(id)).join(", ")}`);
51
+ }
52
+ const compactRequirements = requirements.map((requirement) => ({
53
+ id: compactPromptString(requirement.id, 80),
54
+ ...(options.includeRequirementText !== false && compactPromptString(requirement.text, 240)
55
+ ? { text: compactPromptString(requirement.text, 240) }
56
+ : {}),
57
+ sourceFragmentIds: compactPromptStringArray(requirement.sourceFragmentIds, 20, 80),
58
+ }));
59
+ const sliceSet = new Set(slice);
60
+ const compactVerificationTargets = options.includeVerificationTargets === false
61
+ ? []
62
+ : Array.isArray(payload.verificationTargets)
63
+ ? payload.verificationTargets.flatMap((value) => {
64
+ if (!value || typeof value !== "object" || Array.isArray(value))
65
+ return [];
66
+ const target = value;
67
+ if (options.verificationTargetIds && !options.verificationTargetIds.includes(String(target.id)))
68
+ return [];
69
+ const targetRequirementIds = planFactStringList(target.requirementIds);
70
+ const relatedRequirementIds = targetRequirementIds.filter((id) => sliceSet.has(id));
71
+ if (relatedRequirementIds.length === 0)
72
+ return [];
73
+ return [
74
+ {
75
+ ...(typeof target.id === "string"
76
+ ? { id: target.id }
77
+ : {}),
78
+ ...(typeof target.commandId === "string"
79
+ ? { commandId: target.commandId }
80
+ : {}),
81
+ ...(compactPromptString(target.commandLabel, 180)
82
+ ? { commandLabel: compactPromptString(target.commandLabel, 180) }
83
+ : {}),
84
+ ...(typeof target.file === "string"
85
+ ? { file: target.file }
86
+ : {}),
87
+ requirementIds: relatedRequirementIds,
88
+ uiStates: planFactStringList(target.uiStates),
89
+ },
90
+ ];
91
+ })
92
+ : [];
93
+ const compactTargetSurface = Array.isArray(payload.targetSurface)
94
+ ? payload.targetSurface.flatMap((value) => {
95
+ if (!value || typeof value !== "object" || Array.isArray(value))
96
+ return [];
97
+ const surface = value;
98
+ return [
99
+ {
100
+ ...(compactPromptString(surface.completeness, 32)
101
+ ? { completeness: compactPromptString(surface.completeness, 32) }
102
+ : {}),
103
+ ...(compactPromptString(surface.entrypoint, 180)
104
+ ? { entrypoint: compactPromptString(surface.entrypoint, 180) }
105
+ : {}),
106
+ ...(compactPromptString(surface.routeOrMount, 180)
107
+ ? { routeOrMount: compactPromptString(surface.routeOrMount, 180) }
108
+ : {}),
109
+ implementationPaths: compactPromptStringArray(surface.implementationPaths),
110
+ proposedPaths: compactPromptStringArray(surface.proposedPaths),
111
+ testPaths: compactPromptStringArray(surface.testPaths),
112
+ ...(compactPromptString(surface.dataSource, 180)
113
+ ? { dataSource: compactPromptString(surface.dataSource, 180) }
114
+ : {}),
115
+ allowedPathConflicts: compactPromptStringArray(surface.allowedPathConflicts),
116
+ unresolvedPaths: compactPromptStringArray(surface.unresolvedPaths),
117
+ },
118
+ ];
119
+ })
120
+ : [];
121
+ const compactDesignEvidence = options.includeDesignEvidence === true &&
122
+ Array.isArray(payload.designEvidence)
123
+ ? payload.designEvidence.flatMap((value) => {
124
+ if (!value || typeof value !== "object" || Array.isArray(value))
125
+ return [];
126
+ const evidence = value;
127
+ return [{
128
+ ...(compactPromptString(evidence.source, 180)
129
+ ? { source: compactPromptString(evidence.source, 180) }
130
+ : {}),
131
+ paths: compactPromptStringArray(evidence.paths, 12, 180),
132
+ conflicts: compactPromptStringArray(evidence.conflicts, 24, 180),
133
+ }];
134
+ })
135
+ : [];
136
+ const compactDeclaredUiStates = Array.isArray(payload.declaredUiStates)
137
+ ? payload.declaredUiStates.flatMap((value) => {
138
+ if (!value || typeof value !== "object" || Array.isArray(value))
139
+ return [];
140
+ const state = value;
141
+ const id = typeof state.id === "string" ? state.id : undefined;
142
+ if (!id)
143
+ return [];
144
+ return [{
145
+ id,
146
+ ...(compactPromptString(state.trigger, 180)
147
+ ? { trigger: compactPromptString(state.trigger, 180) }
148
+ : {}),
149
+ ...(compactPromptString(state.observableOutcome, 240)
150
+ ? { observableOutcome: compactPromptString(state.observableOutcome, 240) }
151
+ : {}),
152
+ }];
153
+ })
154
+ : [];
155
+ const committedUx = payload.committedUx &&
156
+ typeof payload.committedUx === "object" &&
157
+ !Array.isArray(payload.committedUx)
158
+ ? payload.committedUx
159
+ : undefined;
160
+ const compactPayload = {
161
+ inputManifest: { ...projectFrontendInputScope({ ...payload, requirements: [...requirementsById.values()] }, slice).inputManifest, semantics: options.includeRequirementText === false ? "navigation-only" : "full" },
162
+ constraints: payload.constraints,
163
+ executionGroups: Array.isArray(payload.executionGroups) ? payload.executionGroups.filter(g => isRecordObject(g) && Array.isArray(g.requirementIds) && g.requirementIds.some(id => slice.includes(String(id)))) : [],
164
+ requirements: compactRequirements,
165
+ requiredDeliverables: Array.isArray(payload.requiredDeliverables) ? payload.requiredDeliverables : [],
166
+ ...(compactTargetSurface.length > 0
167
+ ? { targetSurface: compactTargetSurface }
168
+ : {}),
169
+ ...(compactVerificationTargets.length > 0
170
+ ? { verificationTargets: compactVerificationTargets }
171
+ : {}),
172
+ ...(compactDesignEvidence.length > 0
173
+ ? { designEvidence: compactDesignEvidence }
174
+ : {}),
175
+ ...(compactDeclaredUiStates.length > 0
176
+ ? { declaredUiStates: compactDeclaredUiStates }
177
+ : {}),
178
+ ...(committedUx
179
+ ? {
180
+ committedUx: {
181
+ uiStateNames: compactPromptStringArray(committedUx.uiStateNames, 40, 100),
182
+ interactionNames: compactPromptStringArray(committedUx.interactionNames, 60, 120),
183
+ },
184
+ }
185
+ : {}),
186
+ };
187
+ const compactBlock = [
188
+ "<frontend_plan_input>",
189
+ `Committed Contract/Scout facts for ONLY the current requirement slice (${slice.join(", ")}); do not infer or record requirements outside this slice.`,
190
+ JSON.stringify(compactPayload),
191
+ "Do not read upstream artifacts, task sources, or repository files. If this slice cannot support a decision, record a genuine evidence gap.",
192
+ "</frontend_plan_input>",
193
+ ].join("\n");
194
+ let compactPrompt = basePrompt.replace(planInputPattern, compactBlock);
195
+ compactPrompt = compactPrompt.replace(/<upstream_context>[\s\S]*?<\/upstream_context>/, "<upstream_context>\n(full upstream prose omitted; use only the slice-specific typed facts above)\n</upstream_context>");
196
+ compactPrompt = compactPrompt.replace(/Cover each frozen requirement ID exactly once:[^\n]*/, `Cover ONLY the current requirement slice: ${slice.join(", ")}. Record each slice requirement exactly once.`);
197
+ const checklistPattern = /<plan_review_checklist>([\s\S]*?)<\/plan_review_checklist>/;
198
+ const checklistMatch = checklistPattern.exec(compactPrompt);
199
+ if (checklistMatch) {
200
+ if (options.includeChecklist === false) {
201
+ compactPrompt = compactPrompt.replace(checklistPattern, "");
202
+ return compactPrompt;
203
+ }
204
+ const checklist = checklistMatch[1]
205
+ .split(/\r?\n/)
206
+ .filter((line) => {
207
+ const requirementLine = /^Requirements requiring behavioural coverage:\s*(.*)$/.exec(line.trim());
208
+ if (requirementLine) {
209
+ return false;
210
+ }
211
+ const citationId = /^([^\s]+)\s+\+/.exec(line.trim())?.[1];
212
+ return citationId === undefined || sliceSet.has(citationId);
213
+ })
214
+ .concat([`Requirements requiring behavioural coverage: ${slice.join(", ") || "(none)"}`])
215
+ .join("\n");
216
+ compactPrompt = compactPrompt.replace(checklistPattern, `<plan_review_checklist>${checklist}</plan_review_checklist>`);
217
+ }
218
+ return compactPrompt;
219
+ }
220
+ export function compactFrontendPlanLedgerContext(input) {
221
+ const slice = new Set(input.requirementIds);
222
+ const allowed = new Set(input.kinds);
223
+ const scopedKinds = new Set(input.scopedKinds ?? []);
224
+ const compactFacts = [];
225
+ for (const value of input.committedFacts) {
226
+ const fact = committedFactFromPlanRecord(value);
227
+ if (!fact || fact.origin !== "plan" || typeof fact.kind !== "string" || !allowed.has(fact.kind))
228
+ continue;
229
+ if (scopedKinds.has(fact.kind) && !planFactScopeIntersects(fact, slice)) {
230
+ continue;
231
+ }
232
+ const entry = fact.entry && typeof fact.entry === "object" && !Array.isArray(fact.entry)
233
+ ? fact.entry
234
+ : undefined;
235
+ if (fact.kind === "plan-requirement") {
236
+ if (!entry || typeof entry.id !== "string" || !slice.has(entry.id))
237
+ continue;
238
+ compactFacts.push({
239
+ kind: fact.kind,
240
+ entry: {
241
+ id: entry.id,
242
+ implementationTargets: planFactStringList(entry.implementationTargets).slice(0, 12),
243
+ verificationTargetIds: planFactStringList(entry.verificationTargetIds).slice(0, 12),
244
+ ...(compactPromptString(entry.expectedOutcome, 240)
245
+ ? { expectedOutcome: compactPromptString(entry.expectedOutcome, 240) }
246
+ : {}),
247
+ ...(entry.evidenceGap && typeof entry.evidenceGap === "object"
248
+ ? { evidenceGap: entry.evidenceGap }
249
+ : {}),
250
+ },
251
+ });
252
+ continue;
253
+ }
254
+ if (fact.kind === "plan-verification-target") {
255
+ if (!entry)
256
+ continue;
257
+ // Scope against the complete canonical binding before compacting.
258
+ // A shared target may bind a late requirement beyond the preview cap.
259
+ const requirementIds = planFactStringList(entry.requirementIds).filter((id) => slice.has(id));
260
+ if (requirementIds.length === 0)
261
+ continue;
262
+ compactFacts.push({
263
+ kind: fact.kind,
264
+ entry: {
265
+ id: entry.id,
266
+ commandId: entry.commandId,
267
+ file: entry.file,
268
+ requirementIds,
269
+ uiStates: planFactStringList(entry.uiStates).slice(0, 12),
270
+ },
271
+ });
272
+ continue;
273
+ }
274
+ if (fact.kind === "component-choice") {
275
+ const choices = Array.isArray(fact.uiComponentChoices)
276
+ ? fact.uiComponentChoices.flatMap((choice) => {
277
+ if (!choice || typeof choice !== "object" || Array.isArray(choice))
278
+ return [];
279
+ const item = choice;
280
+ return [{
281
+ purpose: compactPromptString(item.purpose, 120),
282
+ component: compactPromptString(item.component, 120),
283
+ decision: compactPromptString(item.decision, 40),
284
+ covers: compactPromptStringArray(item.covers, 40, 120),
285
+ evidencePath: compactPromptString(item.evidencePath, 180),
286
+ }];
287
+ })
288
+ : [];
289
+ if (choices.length > 0)
290
+ compactFacts.push({ kind: fact.kind, uiComponentChoices: choices });
291
+ continue;
292
+ }
293
+ if (fact.kind === "state-registry") {
294
+ compactFacts.push({
295
+ kind: fact.kind,
296
+ uiStateNames: planFactStringList(fact.uiStateNames).slice(0, 40),
297
+ interactionNames: planFactStringList(fact.interactionNames).slice(0, 60),
298
+ });
299
+ continue;
300
+ }
301
+ if (fact.kind === "state-flow") {
302
+ compactFacts.push({
303
+ kind: fact.kind,
304
+ uiStates: Array.isArray(fact.uiStates) ? fact.uiStates : [],
305
+ interactions: Array.isArray(fact.interactions) ? fact.interactions : [],
306
+ removeUiStateNames: compactPromptStringArray(fact.removeUiStateNames, 24, 100),
307
+ removeInteractionNames: compactPromptStringArray(fact.removeInteractionNames, 24, 100),
308
+ });
309
+ continue;
310
+ }
311
+ if (fact.kind === "data-flow") {
312
+ compactFacts.push({
313
+ kind: fact.kind,
314
+ interactions: compactPromptStringArray(fact.interactions, 32, 120),
315
+ endpoints: compactPromptStringArray(fact.endpoints, 32, 180),
316
+ });
317
+ continue;
318
+ }
319
+ if (fact.kind === "mock-api") {
320
+ const mockApi = fact.mockApi && typeof fact.mockApi === "object" && !Array.isArray(fact.mockApi)
321
+ ? fact.mockApi
322
+ : {};
323
+ compactFacts.push({
324
+ kind: fact.kind,
325
+ mockApi: {
326
+ strategy: compactPromptString(mockApi.strategy, 40),
327
+ activation: compactPromptString(mockApi.activation, 180),
328
+ endpoints: Array.isArray(mockApi.endpoints) ? mockApi.endpoints : [],
329
+ },
330
+ });
331
+ continue;
332
+ }
333
+ if (fact.kind === "design-deviation") {
334
+ compactFacts.push({ kind: fact.kind, conflicts: compactPromptStringArray(fact.conflicts, 32, 180) });
335
+ continue;
336
+ }
337
+ if (fact.kind === "dependency") {
338
+ compactFacts.push({ kind: fact.kind, policy: compactPromptString(fact.policy, 300) });
339
+ continue;
340
+ }
341
+ if (fact.kind === "target-surface") {
342
+ compactFacts.push({ kind: fact.kind, routes: compactPromptStringArray(fact.routes, 24, 180), implementationPaths: compactPromptStringArray(fact.implementationPaths, 24, 180), proposedPaths: compactPromptStringArray(fact.proposedPaths, 24, 180), testPaths: compactPromptStringArray(fact.testPaths, 24, 180) });
343
+ }
344
+ }
345
+ if (compactFacts.length === 0)
346
+ return "";
347
+ const priorityFacts = compactFacts.filter((fact) => fact.kind === "plan-requirement" || fact.kind === "plan-verification-target");
348
+ const otherFacts = compactFacts.filter((fact) => fact.kind !== "plan-requirement" && fact.kind !== "plan-verification-target");
349
+ let previewBytes = 0;
350
+ const boundedFacts = [
351
+ ...priorityFacts.slice(0, 64),
352
+ ...otherFacts.slice(-32),
353
+ ].slice(0, 96).filter(fact => {
354
+ const bytes = Buffer.byteLength(JSON.stringify(fact));
355
+ if (previewBytes + bytes > 24_000)
356
+ return false;
357
+ previewBytes += bytes;
358
+ return true;
359
+ });
360
+ return [
361
+ "<frontend_plan_ledger>",
362
+ "Preview of committed plan facts; not a replacement payload. Fields and lists may be omitted or shortened. Use read_plan_facts with kind, entryId/eventId and optional field to retrieve complete values in bounded pages before corrections; preserve all other bindings. Absence from this preview is not absence from the ledger.",
363
+ JSON.stringify({ complete: false, matchingFacts: compactFacts.length, omittedFacts: compactFacts.length - boundedFacts.length }),
364
+ JSON.stringify(boundedFacts),
365
+ "</frontend_plan_ledger>",
366
+ ].join("\n");
367
+ }