dsh-continual-evolve 0.11.0 → 0.12.1

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.
Files changed (161) hide show
  1. package/README.md +42 -115
  2. package/README.zh.md +33 -93
  3. package/client/client.js +253 -0
  4. package/cordis.patch.yml +2 -2
  5. package/lib/card-routes.d.ts +32 -0
  6. package/lib/card-routes.js +92 -0
  7. package/lib/index.d.ts +65 -217
  8. package/lib/index.js +74 -234
  9. package/lib/known-workspaces.d.ts +22 -0
  10. package/lib/known-workspaces.js +42 -0
  11. package/lib/memory-guide.d.ts +13 -14
  12. package/lib/memory-guide.js +34 -21
  13. package/lib/memory-section.d.ts +77 -0
  14. package/lib/memory-section.js +189 -0
  15. package/lib/memory-snapshot.d.ts +54 -0
  16. package/lib/memory-snapshot.js +125 -0
  17. package/package.json +8 -17
  18. package/lib/apply.d.ts +0 -22
  19. package/lib/apply.js +0 -157
  20. package/lib/approval.d.ts +0 -73
  21. package/lib/approval.js +0 -127
  22. package/lib/audit-log.d.ts +0 -19
  23. package/lib/audit-log.js +0 -52
  24. package/lib/auto.d.ts +0 -236
  25. package/lib/auto.js +0 -773
  26. package/lib/autocase.d.ts +0 -38
  27. package/lib/autocase.js +0 -76
  28. package/lib/benchmark-command.d.ts +0 -9
  29. package/lib/benchmark-command.js +0 -357
  30. package/lib/benchmark.d.ts +0 -151
  31. package/lib/benchmark.js +0 -250
  32. package/lib/command-util.d.ts +0 -70
  33. package/lib/command-util.js +0 -123
  34. package/lib/command.d.ts +0 -13
  35. package/lib/command.js +0 -134
  36. package/lib/consolidate.d.ts +0 -90
  37. package/lib/consolidate.js +0 -180
  38. package/lib/copy.d.ts +0 -37
  39. package/lib/copy.js +0 -58
  40. package/lib/declines.d.ts +0 -103
  41. package/lib/declines.js +0 -179
  42. package/lib/evaluate.d.ts +0 -74
  43. package/lib/evaluate.js +0 -271
  44. package/lib/evolve-event.d.ts +0 -40
  45. package/lib/evolve-event.js +0 -60
  46. package/lib/extraction-phase.d.ts +0 -132
  47. package/lib/extraction-phase.js +0 -236
  48. package/lib/failures.d.ts +0 -42
  49. package/lib/failures.js +0 -171
  50. package/lib/fate.d.ts +0 -130
  51. package/lib/fate.js +0 -350
  52. package/lib/goal-command.d.ts +0 -7
  53. package/lib/goal-command.js +0 -37
  54. package/lib/goal.d.ts +0 -72
  55. package/lib/goal.js +0 -72
  56. package/lib/inject.d.ts +0 -90
  57. package/lib/inject.js +0 -164
  58. package/lib/injection-caps.d.ts +0 -4
  59. package/lib/injection-caps.js +0 -4
  60. package/lib/injection-directory.d.ts +0 -29
  61. package/lib/injection-directory.js +0 -78
  62. package/lib/injection-rank.d.ts +0 -85
  63. package/lib/injection-rank.js +0 -161
  64. package/lib/listener.d.ts +0 -45
  65. package/lib/listener.js +0 -199
  66. package/lib/llm-text.d.ts +0 -104
  67. package/lib/llm-text.js +0 -140
  68. package/lib/logfile.d.ts +0 -71
  69. package/lib/logfile.js +0 -159
  70. package/lib/maintenance-commands.d.ts +0 -23
  71. package/lib/maintenance-commands.js +0 -136
  72. package/lib/memory-agent.d.ts +0 -93
  73. package/lib/memory-agent.js +0 -300
  74. package/lib/memory-apply.d.ts +0 -44
  75. package/lib/memory-apply.js +0 -202
  76. package/lib/memory-benchmark.d.ts +0 -76
  77. package/lib/memory-benchmark.js +0 -170
  78. package/lib/memory-index.d.ts +0 -114
  79. package/lib/memory-index.js +0 -197
  80. package/lib/memory-manifest.d.ts +0 -16
  81. package/lib/memory-manifest.js +0 -70
  82. package/lib/message-source.d.ts +0 -23
  83. package/lib/message-source.js +0 -12
  84. package/lib/mount-command.d.ts +0 -10
  85. package/lib/mount-command.js +0 -48
  86. package/lib/mount.d.ts +0 -42
  87. package/lib/mount.js +0 -212
  88. package/lib/notify.d.ts +0 -48
  89. package/lib/notify.js +0 -57
  90. package/lib/plan-command.d.ts +0 -12
  91. package/lib/plan-command.js +0 -38
  92. package/lib/plan.d.ts +0 -16
  93. package/lib/plan.js +0 -126
  94. package/lib/planner.d.ts +0 -61
  95. package/lib/planner.js +0 -144
  96. package/lib/pool.d.ts +0 -7
  97. package/lib/pool.js +0 -25
  98. package/lib/prefix-cache.d.ts +0 -79
  99. package/lib/prefix-cache.js +0 -168
  100. package/lib/project.d.ts +0 -28
  101. package/lib/project.js +0 -66
  102. package/lib/projection.d.ts +0 -22
  103. package/lib/projection.js +0 -112
  104. package/lib/promotion.d.ts +0 -94
  105. package/lib/promotion.js +0 -171
  106. package/lib/recall.d.ts +0 -85
  107. package/lib/recall.js +0 -142
  108. package/lib/record-language.d.ts +0 -85
  109. package/lib/record-language.js +0 -152
  110. package/lib/render.d.ts +0 -13
  111. package/lib/render.js +0 -80
  112. package/lib/review-scheduler.d.ts +0 -38
  113. package/lib/review-scheduler.js +0 -183
  114. package/lib/review.d.ts +0 -60
  115. package/lib/review.js +0 -145
  116. package/lib/rollback.d.ts +0 -9
  117. package/lib/rollback.js +0 -68
  118. package/lib/rubric.d.ts +0 -29
  119. package/lib/rubric.js +0 -119
  120. package/lib/runtime-commands.d.ts +0 -14
  121. package/lib/runtime-commands.js +0 -115
  122. package/lib/runtime.d.ts +0 -24
  123. package/lib/runtime.js +0 -87
  124. package/lib/score.d.ts +0 -64
  125. package/lib/score.js +0 -191
  126. package/lib/search.d.ts +0 -83
  127. package/lib/search.js +0 -136
  128. package/lib/service.d.ts +0 -63
  129. package/lib/service.js +0 -253
  130. package/lib/skill-render.d.ts +0 -23
  131. package/lib/skill-render.js +0 -69
  132. package/lib/skill.d.ts +0 -15
  133. package/lib/skill.js +0 -80
  134. package/lib/skillquality.d.ts +0 -85
  135. package/lib/skillquality.js +0 -320
  136. package/lib/source.d.ts +0 -30
  137. package/lib/source.js +0 -43
  138. package/lib/state.d.ts +0 -26
  139. package/lib/state.js +0 -151
  140. package/lib/store.d.ts +0 -61
  141. package/lib/store.js +0 -164
  142. package/lib/token-usage.d.ts +0 -68
  143. package/lib/token-usage.js +0 -211
  144. package/lib/tool.d.ts +0 -34
  145. package/lib/tool.js +0 -320
  146. package/lib/turn-snapshot.d.ts +0 -78
  147. package/lib/turn-snapshot.js +0 -252
  148. package/lib/types.d.ts +0 -218
  149. package/lib/types.js +0 -133
  150. package/lib/usage.d.ts +0 -36
  151. package/lib/usage.js +0 -98
  152. package/lib/validate.d.ts +0 -28
  153. package/lib/validate.js +0 -181
  154. package/lib/wrapup-candidates.d.ts +0 -127
  155. package/lib/wrapup-candidates.js +0 -161
  156. package/lib/wrapup-command.d.ts +0 -10
  157. package/lib/wrapup-command.js +0 -243
  158. package/lib/wrapup-proposals.d.ts +0 -101
  159. package/lib/wrapup-proposals.js +0 -281
  160. package/lib/wrapup.d.ts +0 -56
  161. package/lib/wrapup.js +0 -105
@@ -1,243 +0,0 @@
1
- import { questionServiceOf, requireGlobalApproval } from "./approval.js";
2
- import { wrapupArchiveCopy } from "./copy.js";
3
- import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, valenceStampProposal, wholePromoteProposals } from "./wrapup.js";
4
- import { DEFAULT_PROMOTION_POLICY } from "./promotion.js";
5
- import { normalizeRecordLanguage, resolveRecordLanguage } from "./record-language.js";
6
- import { recentUserText } from "./inject.js";
7
- function success(text) {
8
- return { kind: "success", text };
9
- }
10
- export async function executeWrapupCommand(ctx, engine, invocation, policy = DEFAULT_PROMOTION_POLICY, recordLanguage) {
11
- const sessionId = invocation.agent.id;
12
- const localState = engine.load("local", sessionId);
13
- const globalState = engine.load("global", undefined);
14
- const candidates = listLocalCandidates(localState, globalState, engine.baseDir);
15
- if (candidates.length === 0) {
16
- return success(`(nothing to wrap up: ${sessionId}'s local store has no active, un-promoted entries — use /evolve list to inspect it)`);
17
- }
18
- // 1. Classify: the model judges each audited candidate's fate.
19
- const assessment = await assessLocalEntries(ctx, invocation.agent, candidates, {
20
- signal: invocation.signal,
21
- tokenUsage: {
22
- baseDir: engine.baseDir,
23
- sessionId,
24
- retain: engine.retention.tokenUsage,
25
- onError: (cause) => ctx.logger("continual-evolve").warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
26
- },
27
- tokenUsagePhase: "wrapup",
28
- });
29
- const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
30
- // 2. Partition by action. Deterministic guards re-check the LIVE global
31
- // store right before anything lands (state may have changed mid-call).
32
- const { promotable, skipped } = filterPromotable(assessment.items, globalState, candidates, policy);
33
- const promoteItems = promotable.filter((item) => item.verdict === "promote");
34
- const archiveItems = assessment.items.filter((item) => item.verdict === "archive");
35
- // Split promotion (A-form): archive a mixed entry but promote ONLY the
36
- // cleaned durable part the model extracted. Guarded the same way as whole
37
- // promotes — a split that would duplicate a globally covered topic is
38
- // dropped and the entry archives plain.
39
- const splitItems = [];
40
- const splitSkipped = [];
41
- for (const item of archiveItems) {
42
- if (!item.promote)
43
- continue;
44
- const candidate = byKey.get(item.key);
45
- if (!candidate) {
46
- splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
47
- continue;
48
- }
49
- const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy, candidate.metadata);
50
- if (blocked) {
51
- splitSkipped.push({ key: item.key, reason: blocked });
52
- continue;
53
- }
54
- splitItems.push({ item, candidate });
55
- }
56
- // Plain archives (no split payload): the symmetric guard — an archive that
57
- // is NOT globally covered AND was distilled from real user messages must
58
- // not proceed silently.
59
- const plainArchives = archiveItems.filter((item) => !item.promote);
60
- const { silent: silentArchives, review: reviewArchives } = splitArchiveGuards(plainArchives, candidates);
61
- const keepItems = assessment.items.filter((item) => item.verdict === "keep");
62
- // 3. Report the assessment before touching anything.
63
- const lines = [
64
- `wrapup assessment (${sessionId}): ${candidates.length} candidates${candidates.some((c) => c.coveredGlobally) ? `, ${candidates.filter((c) => c.coveredGlobally).length} covered globally` : ""}`,
65
- `${assessment.rationale}`,
66
- ];
67
- for (const [heading, items] of [
68
- ["PROMOTE (to global)", promoteItems],
69
- ["SPLIT (archive + promote durable part)", splitItems.map((split) => split.item)],
70
- ["ARCHIVE", silentArchives],
71
- ["ARCHIVE (needs review)", reviewArchives],
72
- ["KEEP", keepItems],
73
- ]) {
74
- lines.push(`${heading}: ${items.length}`);
75
- for (const item of items) {
76
- const candidate = byKey.get(item.key);
77
- const title = candidate ? candidate.title : item.key;
78
- const splitNote = item.promote ? ` → 拆出提升「${item.promote.title}」` : "";
79
- lines.push(`- ${item.key} "${title}"${splitNote} — ${item.reason}`);
80
- }
81
- }
82
- for (const skip of skipped) {
83
- lines.push(`- promote skipped: ${skip.key} — ${skip.reason}`);
84
- }
85
- for (const skip of splitSkipped) {
86
- lines.push(`- split skipped: ${skip.key} — ${skip.reason}`);
87
- }
88
- lines.push("");
89
- const applied = [];
90
- // 4. Global writes: governed resource — ONE human approval gate covers
91
- // every create (whole promotes AND split promotions). On approval:
92
- // - whole promote → create global copy + stamp local promotedTo+archivedAt;
93
- // - split → create the cleaned durable part + archive the original with
94
- // promotedTo. On rejection: whole promotes are not written, and each
95
- // split's original STILL archives plain (its snapshot half deserves
96
- // the archive; the durable half is reported for manual handling).
97
- const wholeCreates = promoteItems.map((item) => ({ item, candidate: byKey.get(item.key) }));
98
- const splitCreates = splitItems;
99
- const allCreates = new Set([...wholeCreates.map((c) => c.item.key), ...splitCreates.map((c) => c.item.key)]);
100
- if (allCreates.size > 0) {
101
- const what = `wrapup 将写入跨会话 global store(共 ${allCreates.size} 条:${promoteItems.length} 条整条提升 + ${splitItems.length} 条拆解提升):\n${[
102
- ...promoteItems.map((item) => `- 整条提升 ${item.key} "${byKey.get(item.key)?.title ?? item.key}"`),
103
- ...splitItems.map((split) => `- 拆解提升 ${split.item.key} → 清洗「${split.item.promote?.title}」(原条目随之归档)`),
104
- ].join("\n")}`;
105
- let promoteAllowed = true;
106
- try {
107
- await requireGlobalApproval(ctx, invocation.agent, invocation.signal, what, recordLanguage);
108
- }
109
- catch (cause) {
110
- promoteAllowed = false;
111
- const message = `global 写入未批准 — 整条提升与拆解提升均未写入 (${cause instanceof Error ? cause.message : String(cause)})`;
112
- applied.push(message);
113
- lines.push(message);
114
- }
115
- if (promoteAllowed) {
116
- // Whole promotes: create global entry, retire the local copy.
117
- // Shared proposal builders keep the wrap-up command and the gate's
118
- // local-fate dimension writing IDENTICAL edits.
119
- for (const { item, candidate } of wholeCreates) {
120
- if (!candidate)
121
- continue;
122
- const proposals = wholePromoteProposals(item, candidate, sessionId);
123
- const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
124
- const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
125
- const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
126
- scope: "local",
127
- baselineState: localState,
128
- });
129
- applied.push(`promoted ${item.key} → global:${createdId} (${globalResult.id}; local stamped ${localResult.id})`);
130
- }
131
- // Split promotions: create the cleaned durable part, retire the
132
- // original local entry (its snapshot half is archived along).
133
- for (const { item, candidate } of splitCreates) {
134
- if (!item.promote)
135
- continue;
136
- const proposals = splitPromoteProposals(item, candidate, sessionId);
137
- const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
138
- const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
139
- const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
140
- scope: "local",
141
- baselineState: localState,
142
- });
143
- applied.push(`split ${item.key}: promoted cleaned part → global:${createdId} (${globalResult.id}); original archived (${localResult.id})`);
144
- }
145
- }
146
- else {
147
- // Rejected: whole promotes stay un-written; each split's original
148
- // still archives plain (reported, data restorable).
149
- for (const { item, candidate } of splitCreates) {
150
- if (!candidate)
151
- continue;
152
- const result = engine.apply("local", sessionId, {
153
- summary: `wrapup: split promotion not approved — archive original ${item.key} plain`,
154
- rationale: item.reason,
155
- expectedOutcome: `The original leaves injection; the cleaned part was NOT written (reported for manual handling).`,
156
- edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
157
- }, { scope: "local", baselineState: localState });
158
- applied.push(`split ${item.key}: promotion not approved — original archived plain (${result.id})`);
159
- }
160
- }
161
- }
162
- // 5. Silent archives: deterministic local action (hidden from injection,
163
- // data kept restorable) — covered topics and operational entries need no
164
- // confirmation, matching the original behavior.
165
- for (const item of silentArchives) {
166
- const candidate = byKey.get(item.key);
167
- if (!candidate)
168
- continue;
169
- const result = engine.apply("local", sessionId, {
170
- summary: `wrapup: archive local ${item.key} — ${item.reason}`,
171
- rationale: item.reason,
172
- expectedOutcome: `The entry stops being injected but stays restorable.`,
173
- edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
174
- }, { scope: "local", baselineState: localState });
175
- applied.push(`archived ${item.key} (${result.id})`);
176
- }
177
- // 6. Review archives (symmetric guard): not covered globally + distilled
178
- // from real user messages — the user decides before this content is
179
- // hidden from future sessions. No question service → conservative keep.
180
- const userQuestions = questionServiceOf(ctx);
181
- for (const item of reviewArchives) {
182
- const candidate = byKey.get(item.key);
183
- if (!candidate)
184
- continue;
185
- if (!userQuestions) {
186
- applied.push(`kept ${item.key} — archive pending user confirmation (no question service)`);
187
- continue;
188
- }
189
- const questionId = "evolve-wrapup-archive-review";
190
- let archiveConfirmed = false;
191
- try {
192
- const copy = wrapupArchiveCopy(candidate.title, normalizeRecordLanguage(recordLanguage) ??
193
- resolveRecordLanguage({ ctx, trajectoryText: recentUserText(invocation.agent) }));
194
- const answer = await userQuestions.ask({
195
- questions: [
196
- {
197
- id: questionId,
198
- question: copy.question,
199
- options: copy.options,
200
- },
201
- ],
202
- agent: invocation.agent,
203
- signal: invocation.signal,
204
- });
205
- const selected = answer.answers?.find((entry) => entry.id === questionId)?.selected ?? [];
206
- archiveConfirmed = selected.includes("归档") || selected.includes("Archive");
207
- }
208
- catch {
209
- archiveConfirmed = false;
210
- }
211
- if (archiveConfirmed) {
212
- const result = engine.apply("local", sessionId, {
213
- summary: `wrapup: archive local ${item.key} (user-confirmed) — ${item.reason}`,
214
- rationale: item.reason,
215
- expectedOutcome: `The entry stops being injected but stays restorable.`,
216
- edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
217
- }, { scope: "local", baselineState: localState });
218
- applied.push(`archived ${item.key} (user-confirmed, ${result.id})`);
219
- }
220
- else {
221
- applied.push(`kept ${item.key} — user declined the archive`);
222
- }
223
- }
224
- // 7. Valence stamps (P1 效价反馈): keep-verdict entries the assessor marked
225
- // contradicted get their negative counter bumped — the entry stays live
226
- // but sinks in injection ranking and the next assessment prefers
227
- // archiving it. Deterministic local action, no approval needed.
228
- for (const item of keepItems) {
229
- if (!item.contradicted)
230
- continue;
231
- const candidate = byKey.get(item.key);
232
- if (!candidate)
233
- continue;
234
- const proposal = valenceStampProposal(item, candidate);
235
- if (!proposal)
236
- continue;
237
- const result = engine.apply("local", sessionId, proposal, { scope: "local", baselineState: localState });
238
- applied.push(`contradicted stamp ${item.key} → valenceNegative=${candidate.negativeCount + 1} (${result.id})`);
239
- }
240
- lines.push(...(applied.length > 0 ? applied : ["(no changes applied — all entries kept)"]));
241
- return success(lines.join("\n"));
242
- }
243
- //# sourceMappingURL=wrapup-command.js.map
@@ -1,101 +0,0 @@
1
- /**
2
- * Wrap-up proposal builders: turn assessed verdicts into deterministic,
3
- * evidence-carrying refinement proposals — whole promotes, split promotes,
4
- * archive reviews, and valence stamps. Pure logic over the candidate types
5
- * in `wrapup-candidates.ts`; the user approves, the engine applies.
6
- */
7
- import type { HarnessState, RefinementKind, RefinementProposal } from "./types.js";
8
- import { type PromotionPolicy } from "./promotion.js";
9
- import { type WrapupCandidate, type WrapupItem } from "./wrapup-candidates.js";
10
- export interface PromotableSplit {
11
- /** Items that may be promoted: classified promote AND not covered globally. */
12
- promotable: WrapupItem[];
13
- /** Items classified promote but blocked by the deterministic guard, with why. */
14
- skipped: {
15
- key: string;
16
- reason: string;
17
- }[];
18
- }
19
- /**
20
- * Apply-time deterministic guard: re-check every promote verdict against the
21
- * global store right before it lands. The LLM classification may be stale
22
- * (a gate ran while assessing) or wrong; this ensures a promote never writes
23
- * a duplicate, project-scoped, or too-thin global entry. Pure and unit-tested.
24
- *
25
- * Guards (2026-08-22 promotion policy):
26
- * - audited candidate list + title coverage (pre-existing),
27
- * - project-scoped content markers (absolute paths / session ids) — the
28
- * global store is shared across projects and must stay portable,
29
- * - thin content below the policy floor (framing outweighs the fact),
30
- * - near-duplicate of an existing global entry by content overlap.
31
- */
32
- export declare function filterPromotable(items: readonly WrapupItem[], globalState: HarnessState, candidates: readonly WrapupCandidate[], policy?: PromotionPolicy): PromotableSplit;
33
- export interface ArchiveReviewSplit {
34
- /** Archives that may proceed silently: topic already covered, no real
35
- * distillation source, or the archive half of an already-approved split. */
36
- silent: WrapupItem[];
37
- /**
38
- * Archives that would bury possibly-reusable content: not covered
39
- * globally AND distilled from real user messages (sourceSeqs present).
40
- * These MAY NOT archive silently — the command must get user
41
- * confirmation first (the symmetric guard to filterPromotable: it stops
42
- * over-archiving, not just over-writing).
43
- */
44
- review: WrapupItem[];
45
- }
46
- /**
47
- * The symmetric archive guard. `filterPromotable` is one-directional: it
48
- * stops the model from WRITING duplicate global entries, but nothing stopped
49
- * an unfounded ARCHIVE from hiding content that was actually only local.
50
- * Guard criteria: an archive needs user confirmation when it is NOT covered
51
- * globally AND the entry carries a real distillation source (sourceSeqs /
52
- * sourceSession — i.e. it was distilled from actual user messages, so it
53
- * may hold reusable value). Operational/empty entries archive silently as
54
- * before. Split archives (archive + promote sub-object) skip this check:
55
- * their promotion already crosses a human approval gate, so the archive is
56
- * the completion of an approved action, not a silent burial.
57
- */
58
- export declare function needsArchiveReview(item: WrapupItem, candidate: WrapupCandidate): boolean;
59
- /** Partition archive items into silent vs review-required (see needsArchiveReview). */
60
- export declare function splitArchiveGuards(items: readonly WrapupItem[], candidates: readonly WrapupCandidate[]): ArchiveReviewSplit;
61
- /**
62
- * Apply-time guard for a split promotion (archive + promote sub-object):
63
- * the cleaned payload must pass the same promotion policy as a whole
64
- * promote — no global coverage duplicate, no project-scoped content, not
65
- * too thin, no near-duplicate global entry. A blocked split is dropped (the
66
- * entry still archives plain) rather than half-promoting a redundancy.
67
- */
68
- export declare function splitPromoteBlocked(item: WrapupItem, globalState: HarnessState, kind: RefinementKind, policy?: PromotionPolicy, candidateMetadata?: Record<string, unknown>): string | undefined;
69
- /**
70
- * Shared proposal builders for a WHOLE promotion — used by both the
71
- * `/evolve wrapup` command and the gate's local-fate dimension so the two
72
- * paths apply IDENTICAL edits (global create + local retirement stamp).
73
- *
74
- * The local stamp is a factory: the `promotedTo` id is only known after the
75
- * global create lands (validation may slugify the id), so the caller applies
76
- * the global proposal first and stamps the local copy with the created id.
77
- */
78
- export declare function wholePromoteProposals(item: WrapupItem, candidate: WrapupCandidate, sessionId: string): {
79
- global: RefinementProposal;
80
- localStamp: (createdId: string) => RefinementProposal;
81
- };
82
- /**
83
- * Shared proposal builders for a SPLIT promotion (A-form): archive a mixed
84
- * local entry but promote ONLY the cleaned durable part the model extracted.
85
- * Same usage contract as {@link wholePromoteProposals}: apply the global
86
- * create, then stamp the original local entry with the created id.
87
- */
88
- export declare function splitPromoteProposals(item: WrapupItem, candidate: WrapupCandidate, sessionId: string): {
89
- global: RefinementProposal;
90
- localStamp: (createdId: string) => RefinementProposal;
91
- };
92
- /**
93
- * Valence stamp (P1 效价反馈): a keep-verdict item the assessor marked
94
- * contradicted gets its negative-valence counter bumped — the entry stays
95
- * live but sinks in injection ranking and the next assessment prefers
96
- * archiving it. Promotes pass untouched (the human-approved global copy is
97
- * the live truth); archives don't need the stamp (they already leave
98
- * injection). Undefined when nothing applies.
99
- */
100
- export declare function valenceStampProposal(item: WrapupItem, candidate: WrapupCandidate): RefinementProposal | undefined;
101
- //# sourceMappingURL=wrapup-proposals.d.ts.map
@@ -1,281 +0,0 @@
1
- import { ARCHIVED_AT_KEY, PROMOTED_AT_KEY, PROMOTED_TO_KEY, SOURCE_SEQS_KEY, SOURCE_SESSION_KEY, SOURCED_FROM_KEY, VALENCE_NEGATIVE_KEY } from "./types.js";
2
- import { MEMORY_TYPE_KEY, isMemoryType } from "./types.js";
3
- import { DEFAULT_PROMOTION_POLICY, mostSimilarGlobalEntry, projectScopedReason, secretLeakReason } from "./promotion.js";
4
- import { globalCoverageDetected } from "./wrapup-candidates.js";
5
- import { candidateKey } from "./wrapup-candidates.js";
6
- /**
7
- * Apply-time deterministic guard: re-check every promote verdict against the
8
- * global store right before it lands. The LLM classification may be stale
9
- * (a gate ran while assessing) or wrong; this ensures a promote never writes
10
- * a duplicate, project-scoped, or too-thin global entry. Pure and unit-tested.
11
- *
12
- * Guards (2026-08-22 promotion policy):
13
- * - audited candidate list + title coverage (pre-existing),
14
- * - project-scoped content markers (absolute paths / session ids) — the
15
- * global store is shared across projects and must stay portable,
16
- * - thin content below the policy floor (framing outweighs the fact),
17
- * - near-duplicate of an existing global entry by content overlap.
18
- */
19
- export function filterPromotable(items, globalState, candidates, policy = DEFAULT_PROMOTION_POLICY) {
20
- const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
21
- const promotable = [];
22
- const skipped = [];
23
- for (const item of items) {
24
- if (item.verdict !== "promote")
25
- continue;
26
- const candidate = byKey.get(item.key);
27
- if (!candidate) {
28
- skipped.push({ key: item.key, reason: "not in the audited candidate list" });
29
- continue;
30
- }
31
- if (candidate.coveredGlobally || globalCoverageDetected(globalState, candidate.kind, candidate)) {
32
- skipped.push({ key: item.key, reason: "already covered globally" });
33
- continue;
34
- }
35
- if (candidate.kind === "memory" && !isMemoryType(candidate.metadata[MEMORY_TYPE_KEY])) {
36
- skipped.push({
37
- key: item.key,
38
- reason: `memory has no recall type (metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference) — update the local entry to classify it first, then promote`,
39
- });
40
- continue;
41
- }
42
- const scoped = projectScopedReason(`${candidate.title}\n${candidate.content}`, policy);
43
- if (scoped) {
44
- skipped.push({ key: item.key, reason: scoped });
45
- continue;
46
- }
47
- // Metadata screens too: wholePromoteProposals copies candidate.metadata
48
- // verbatim into the global entry, so a credential planted in local
49
- // metadata must not ride the promotion path into the shared store
50
- // (review audit 2026-08-28 B2).
51
- const secret = secretLeakReason([candidate.title, candidate.content, JSON.stringify(candidate.metadata ?? {})].join("\n"));
52
- if (secret) {
53
- skipped.push({ key: item.key, reason: secret });
54
- continue;
55
- }
56
- if (candidate.content.length < policy.minPromoteChars) {
57
- skipped.push({
58
- key: item.key,
59
- reason: `too thin to promote (${candidate.content.length} < ${policy.minPromoteChars} chars) — keep local or merge`,
60
- });
61
- continue;
62
- }
63
- const similar = mostSimilarGlobalEntry(globalState, candidate.kind, candidate.title, candidate.content, policy);
64
- if (similar) {
65
- skipped.push({
66
- key: item.key,
67
- reason: `near-duplicate of global ${candidate.kind}:${similar.id} "${similar.title}" (overlap ${similar.score.toFixed(2)}) — update that entry instead`,
68
- });
69
- continue;
70
- }
71
- promotable.push(item);
72
- }
73
- return { promotable, skipped };
74
- }
75
- /**
76
- * The symmetric archive guard. `filterPromotable` is one-directional: it
77
- * stops the model from WRITING duplicate global entries, but nothing stopped
78
- * an unfounded ARCHIVE from hiding content that was actually only local.
79
- * Guard criteria: an archive needs user confirmation when it is NOT covered
80
- * globally AND the entry carries a real distillation source (sourceSeqs /
81
- * sourceSession — i.e. it was distilled from actual user messages, so it
82
- * may hold reusable value). Operational/empty entries archive silently as
83
- * before. Split archives (archive + promote sub-object) skip this check:
84
- * their promotion already crosses a human approval gate, so the archive is
85
- * the completion of an approved action, not a silent burial.
86
- */
87
- export function needsArchiveReview(item, candidate) {
88
- if (item.verdict !== "archive")
89
- return false;
90
- if (item.promote)
91
- return false;
92
- if (candidate.coveredGlobally)
93
- return false;
94
- const seqs = candidate.metadata[SOURCE_SEQS_KEY];
95
- const session = candidate.metadata[SOURCE_SESSION_KEY];
96
- return (Array.isArray(seqs) && seqs.length > 0) || (typeof session === "string" && session.length > 0);
97
- }
98
- /** Partition archive items into silent vs review-required (see needsArchiveReview). */
99
- export function splitArchiveGuards(items, candidates) {
100
- const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
101
- const silent = [];
102
- const review = [];
103
- for (const item of items) {
104
- if (item.verdict !== "archive")
105
- continue;
106
- const candidate = byKey.get(item.key);
107
- if (candidate && needsArchiveReview(item, candidate)) {
108
- review.push(item);
109
- }
110
- else {
111
- silent.push(item);
112
- }
113
- }
114
- return { silent, review };
115
- }
116
- /**
117
- * Apply-time guard for a split promotion (archive + promote sub-object):
118
- * the cleaned payload must pass the same promotion policy as a whole
119
- * promote — no global coverage duplicate, no project-scoped content, not
120
- * too thin, no near-duplicate global entry. A blocked split is dropped (the
121
- * entry still archives plain) rather than half-promoting a redundancy.
122
- */
123
- export function splitPromoteBlocked(item, globalState, kind, policy = DEFAULT_PROMOTION_POLICY, candidateMetadata) {
124
- if (!item.promote)
125
- return "no split payload";
126
- if (kind === "memory" && !isMemoryType(candidateMetadata?.[MEMORY_TYPE_KEY])) {
127
- return `split promotion needs the source entry classified (metadata.${MEMORY_TYPE_KEY} one of user|feedback|project|reference) — update it first`;
128
- }
129
- if (globalCoverageDetected(globalState, kind, { id: "", title: item.promote.title })) {
130
- return "split promotion duplicates a globally covered topic";
131
- }
132
- const scoped = projectScopedReason(`${item.promote.title}\n${item.promote.content}`, policy);
133
- if (scoped)
134
- return `split promotion is ${scoped}`;
135
- const secret = secretLeakReason([item.promote.title, item.promote.content].join("\n"));
136
- if (secret)
137
- return `split promotion blocked: ${secret}`;
138
- if (item.promote.content.length < policy.minPromoteChars) {
139
- return `split promotion too thin (${item.promote.content.length} < ${policy.minPromoteChars} chars)`;
140
- }
141
- const similar = mostSimilarGlobalEntry(globalState, kind, item.promote.title, item.promote.content, policy);
142
- if (similar) {
143
- return `split promotion near-duplicates global ${kind}:${similar.id} "${similar.title}" (overlap ${similar.score.toFixed(2)})`;
144
- }
145
- return undefined;
146
- }
147
- /**
148
- * Shared proposal builders for a WHOLE promotion — used by both the
149
- * `/evolve wrapup` command and the gate's local-fate dimension so the two
150
- * paths apply IDENTICAL edits (global create + local retirement stamp).
151
- *
152
- * The local stamp is a factory: the `promotedTo` id is only known after the
153
- * global create lands (validation may slugify the id), so the caller applies
154
- * the global proposal first and stamps the local copy with the created id.
155
- */
156
- export function wholePromoteProposals(item, candidate, sessionId) {
157
- const now = new Date().toISOString();
158
- return {
159
- global: {
160
- summary: `wrapup: promote local ${item.key} to the global store`,
161
- rationale: item.reason,
162
- expectedOutcome: `The entry is now visible to every session via the global store (sourcedFromLocal=${sessionId}:${candidate.id}).`,
163
- edits: [
164
- {
165
- action: "create",
166
- kind: candidate.kind,
167
- id: candidate.id,
168
- title: candidate.title,
169
- content: candidate.content,
170
- path: candidate.path,
171
- metadata: {
172
- ...candidate.metadata,
173
- [SOURCED_FROM_KEY]: `${sessionId}:${candidate.id}`,
174
- [PROMOTED_AT_KEY]: now,
175
- },
176
- },
177
- ],
178
- },
179
- localStamp: (createdId) => ({
180
- summary: `wrapup: stamp local ${item.key} as promoted to ${createdId} and retire it from injection`,
181
- rationale: item.reason,
182
- expectedOutcome: `The local copy keeps its data but stops being injected; the global copy is the live one.`,
183
- edits: [
184
- {
185
- action: "update",
186
- kind: candidate.kind,
187
- id: candidate.id,
188
- title: candidate.title,
189
- content: candidate.content,
190
- metadata: {
191
- ...candidate.metadata,
192
- [PROMOTED_TO_KEY]: createdId,
193
- [PROMOTED_AT_KEY]: now,
194
- [ARCHIVED_AT_KEY]: now,
195
- },
196
- },
197
- ],
198
- }),
199
- };
200
- }
201
- /**
202
- * Shared proposal builders for a SPLIT promotion (A-form): archive a mixed
203
- * local entry but promote ONLY the cleaned durable part the model extracted.
204
- * Same usage contract as {@link wholePromoteProposals}: apply the global
205
- * create, then stamp the original local entry with the created id.
206
- */
207
- export function splitPromoteProposals(item, candidate, sessionId) {
208
- if (!item.promote)
209
- throw new Error("split promote proposals require a promote payload");
210
- const now = new Date().toISOString();
211
- return {
212
- global: {
213
- summary: `wrapup: split — promote cleaned part of ${item.key} to the global store`,
214
- rationale: item.reason,
215
- expectedOutcome: `Only the durable part becomes visible globally; the snapshot half stays archived with the original.`,
216
- edits: [
217
- {
218
- action: "create",
219
- kind: candidate.kind,
220
- id: candidate.id,
221
- title: item.promote.title,
222
- content: item.promote.content,
223
- path: candidate.path,
224
- metadata: {
225
- ...candidate.metadata,
226
- [SOURCED_FROM_KEY]: `${sessionId}:${candidate.id}`,
227
- [PROMOTED_AT_KEY]: now,
228
- },
229
- },
230
- ],
231
- },
232
- localStamp: (createdId) => ({
233
- summary: `wrapup: split — archive original ${item.key}, stamped as promoted to ${createdId}`,
234
- rationale: item.reason,
235
- expectedOutcome: `The original leaves injection (data kept, restorable); the cleaned global copy is the live one.`,
236
- edits: [
237
- {
238
- action: "update",
239
- kind: candidate.kind,
240
- id: candidate.id,
241
- title: candidate.title,
242
- content: candidate.content,
243
- metadata: {
244
- ...candidate.metadata,
245
- [PROMOTED_TO_KEY]: createdId,
246
- [PROMOTED_AT_KEY]: now,
247
- [ARCHIVED_AT_KEY]: now,
248
- },
249
- },
250
- ],
251
- }),
252
- };
253
- }
254
- /**
255
- * Valence stamp (P1 效价反馈): a keep-verdict item the assessor marked
256
- * contradicted gets its negative-valence counter bumped — the entry stays
257
- * live but sinks in injection ranking and the next assessment prefers
258
- * archiving it. Promotes pass untouched (the human-approved global copy is
259
- * the live truth); archives don't need the stamp (they already leave
260
- * injection). Undefined when nothing applies.
261
- */
262
- export function valenceStampProposal(item, candidate) {
263
- if (item.verdict !== "keep" || !item.contradicted)
264
- return undefined;
265
- return {
266
- summary: `wrapup: stamp contradicted valence on ${item.key} (${candidate.negativeCount + 1})`,
267
- rationale: item.reason,
268
- expectedOutcome: "The entry's negative-valence counter rises; injection downranks it and the next assessment prefers archiving it.",
269
- edits: [
270
- {
271
- action: "update",
272
- kind: candidate.kind,
273
- id: candidate.id,
274
- title: candidate.title,
275
- content: candidate.content,
276
- metadata: { ...candidate.metadata, [VALENCE_NEGATIVE_KEY]: candidate.negativeCount + 1 },
277
- },
278
- ],
279
- };
280
- }
281
- //# sourceMappingURL=wrapup-proposals.js.map