dsh-continual-evolve 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -115
- package/README.zh.md +26 -93
- package/cordis.patch.yml +2 -2
- package/lib/index.d.ts +44 -225
- package/lib/index.js +30 -239
- package/lib/memory-guide.d.ts +13 -14
- package/lib/memory-guide.js +32 -21
- package/lib/memory-section.d.ts +77 -0
- package/lib/memory-section.js +189 -0
- package/package.json +2 -17
- package/lib/apply.d.ts +0 -22
- package/lib/apply.js +0 -157
- package/lib/approval.d.ts +0 -73
- package/lib/approval.js +0 -127
- package/lib/audit-log.d.ts +0 -19
- package/lib/audit-log.js +0 -52
- package/lib/auto.d.ts +0 -236
- package/lib/auto.js +0 -773
- package/lib/autocase.d.ts +0 -38
- package/lib/autocase.js +0 -76
- package/lib/benchmark-command.d.ts +0 -9
- package/lib/benchmark-command.js +0 -357
- package/lib/benchmark.d.ts +0 -151
- package/lib/benchmark.js +0 -250
- package/lib/command-util.d.ts +0 -70
- package/lib/command-util.js +0 -123
- package/lib/command.d.ts +0 -13
- package/lib/command.js +0 -134
- package/lib/consolidate.d.ts +0 -90
- package/lib/consolidate.js +0 -180
- package/lib/copy.d.ts +0 -37
- package/lib/copy.js +0 -58
- package/lib/declines.d.ts +0 -103
- package/lib/declines.js +0 -179
- package/lib/evaluate.d.ts +0 -74
- package/lib/evaluate.js +0 -271
- package/lib/evolve-event.d.ts +0 -40
- package/lib/evolve-event.js +0 -60
- package/lib/extraction-phase.d.ts +0 -132
- package/lib/extraction-phase.js +0 -236
- package/lib/failures.d.ts +0 -42
- package/lib/failures.js +0 -171
- package/lib/fate.d.ts +0 -130
- package/lib/fate.js +0 -350
- package/lib/goal-command.d.ts +0 -7
- package/lib/goal-command.js +0 -37
- package/lib/goal.d.ts +0 -72
- package/lib/goal.js +0 -72
- package/lib/inject.d.ts +0 -90
- package/lib/inject.js +0 -164
- package/lib/injection-caps.d.ts +0 -4
- package/lib/injection-caps.js +0 -4
- package/lib/injection-directory.d.ts +0 -29
- package/lib/injection-directory.js +0 -78
- package/lib/injection-rank.d.ts +0 -85
- package/lib/injection-rank.js +0 -161
- package/lib/listener.d.ts +0 -45
- package/lib/listener.js +0 -199
- package/lib/llm-text.d.ts +0 -104
- package/lib/llm-text.js +0 -140
- package/lib/logfile.d.ts +0 -71
- package/lib/logfile.js +0 -159
- package/lib/maintenance-commands.d.ts +0 -23
- package/lib/maintenance-commands.js +0 -136
- package/lib/memory-agent.d.ts +0 -93
- package/lib/memory-agent.js +0 -300
- package/lib/memory-apply.d.ts +0 -44
- package/lib/memory-apply.js +0 -202
- package/lib/memory-benchmark.d.ts +0 -76
- package/lib/memory-benchmark.js +0 -170
- package/lib/memory-index.d.ts +0 -114
- package/lib/memory-index.js +0 -197
- package/lib/memory-manifest.d.ts +0 -16
- package/lib/memory-manifest.js +0 -70
- package/lib/message-source.d.ts +0 -23
- package/lib/message-source.js +0 -12
- package/lib/mount-command.d.ts +0 -10
- package/lib/mount-command.js +0 -48
- package/lib/mount.d.ts +0 -42
- package/lib/mount.js +0 -212
- package/lib/notify.d.ts +0 -48
- package/lib/notify.js +0 -57
- package/lib/plan-command.d.ts +0 -12
- package/lib/plan-command.js +0 -38
- package/lib/plan.d.ts +0 -16
- package/lib/plan.js +0 -126
- package/lib/planner.d.ts +0 -61
- package/lib/planner.js +0 -144
- package/lib/pool.d.ts +0 -7
- package/lib/pool.js +0 -25
- package/lib/prefix-cache.d.ts +0 -79
- package/lib/prefix-cache.js +0 -168
- package/lib/project.d.ts +0 -28
- package/lib/project.js +0 -66
- package/lib/projection.d.ts +0 -22
- package/lib/projection.js +0 -112
- package/lib/promotion.d.ts +0 -94
- package/lib/promotion.js +0 -171
- package/lib/recall.d.ts +0 -85
- package/lib/recall.js +0 -142
- package/lib/record-language.d.ts +0 -85
- package/lib/record-language.js +0 -152
- package/lib/render.d.ts +0 -13
- package/lib/render.js +0 -80
- package/lib/review-scheduler.d.ts +0 -38
- package/lib/review-scheduler.js +0 -183
- package/lib/review.d.ts +0 -60
- package/lib/review.js +0 -145
- package/lib/rollback.d.ts +0 -9
- package/lib/rollback.js +0 -68
- package/lib/rubric.d.ts +0 -29
- package/lib/rubric.js +0 -119
- package/lib/runtime-commands.d.ts +0 -14
- package/lib/runtime-commands.js +0 -115
- package/lib/runtime.d.ts +0 -24
- package/lib/runtime.js +0 -87
- package/lib/score.d.ts +0 -64
- package/lib/score.js +0 -191
- package/lib/search.d.ts +0 -83
- package/lib/search.js +0 -136
- package/lib/service.d.ts +0 -63
- package/lib/service.js +0 -253
- package/lib/skill-render.d.ts +0 -23
- package/lib/skill-render.js +0 -69
- package/lib/skill.d.ts +0 -15
- package/lib/skill.js +0 -80
- package/lib/skillquality.d.ts +0 -85
- package/lib/skillquality.js +0 -320
- package/lib/source.d.ts +0 -30
- package/lib/source.js +0 -43
- package/lib/state.d.ts +0 -26
- package/lib/state.js +0 -151
- package/lib/store.d.ts +0 -61
- package/lib/store.js +0 -164
- package/lib/token-usage.d.ts +0 -68
- package/lib/token-usage.js +0 -211
- package/lib/tool.d.ts +0 -34
- package/lib/tool.js +0 -320
- package/lib/turn-snapshot.d.ts +0 -78
- package/lib/turn-snapshot.js +0 -252
- package/lib/types.d.ts +0 -218
- package/lib/types.js +0 -133
- package/lib/usage.d.ts +0 -36
- package/lib/usage.js +0 -98
- package/lib/validate.d.ts +0 -28
- package/lib/validate.js +0 -181
- package/lib/wrapup-candidates.d.ts +0 -127
- package/lib/wrapup-candidates.js +0 -161
- package/lib/wrapup-command.d.ts +0 -10
- package/lib/wrapup-command.js +0 -243
- package/lib/wrapup-proposals.d.ts +0 -101
- package/lib/wrapup-proposals.js +0 -281
- package/lib/wrapup.d.ts +0 -56
- package/lib/wrapup.js +0 -105
package/lib/prefix-cache.js
DELETED
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Prefix-cache-aware planning input routing (Route A/B).
|
|
3
|
-
*
|
|
4
|
-
* The planner and the review gate re-send session context on every call as
|
|
5
|
-
* flat text. On providers with prompt caching that context is re-billed at
|
|
6
|
-
* full price each time. Route A instead prepends session-derived messages —
|
|
7
|
-
* the same evidence in structured form, eligible for cache-read pricing —
|
|
8
|
-
* and drops the redundant flat trajectory block. Route B keeps the legacy
|
|
9
|
-
* flat-text input byte-for-byte.
|
|
10
|
-
*
|
|
11
|
-
* Adapted from dsh-continual-harness (Route A/B + cache-detect): this port
|
|
12
|
-
* deliberately does NOT reproduce the host loop's byte-identical cache key
|
|
13
|
-
* (no deriveMessages/tools/sessionId plumbing on this seam), so a hit needs
|
|
14
|
-
* a provider that keys on the message-prefix content. Sources are stamped
|
|
15
|
-
* honestly and every reduction is documented below.
|
|
16
|
-
*/
|
|
17
|
-
import { createAssistantMessage, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
18
|
-
/** Default session-prefix budget for one Route A input, in characters. */
|
|
19
|
-
export const DEFAULT_PREFIX_MAX_CHARS = 12000;
|
|
20
|
-
/**
|
|
21
|
-
* Resolve per-call overrides against the defaults.
|
|
22
|
-
*
|
|
23
|
-
* @param opts - sparse per-call overrides.
|
|
24
|
-
* @returns the effective mode (default "auto") and char budget.
|
|
25
|
-
* @throws When maxChars is present but not a positive safe integer (fail loud on misconfiguration).
|
|
26
|
-
*/
|
|
27
|
-
export function resolvePrefixCache(opts) {
|
|
28
|
-
const mode = opts?.mode ?? "auto";
|
|
29
|
-
const maxChars = opts?.maxChars ?? DEFAULT_PREFIX_MAX_CHARS;
|
|
30
|
-
if (!Number.isSafeInteger(maxChars) || maxChars < 1) {
|
|
31
|
-
throw new Error(`evolve: prefix-cache maxChars must be a positive integer, got ${String(opts?.maxChars)}`);
|
|
32
|
-
}
|
|
33
|
-
return { mode, maxChars };
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* True when any assistant message in this session reported cache-read
|
|
37
|
-
* tokens — evidence the provider can serve a warm prefix.
|
|
38
|
-
*
|
|
39
|
-
* @param events - session log rows, newest last.
|
|
40
|
-
* @returns whether cache evidence exists (false on an empty/unreadable log).
|
|
41
|
-
*/
|
|
42
|
-
export function hasCacheEvidence(events) {
|
|
43
|
-
for (const raw of events) {
|
|
44
|
-
if (typeof raw !== "object" || raw === null)
|
|
45
|
-
continue;
|
|
46
|
-
const event = raw;
|
|
47
|
-
if (event.type !== "assistant/message")
|
|
48
|
-
continue;
|
|
49
|
-
const read = event.data?.usage?.cacheReadTokens;
|
|
50
|
-
if (typeof read === "number" && read > 0)
|
|
51
|
-
return true;
|
|
52
|
-
}
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Pick the planning input route for one call.
|
|
57
|
-
*
|
|
58
|
-
* @param events - session log rows, newest last.
|
|
59
|
-
* @param mode - routing mode (default "auto").
|
|
60
|
-
* @returns "A" when the session prefix should carry the context, else "B".
|
|
61
|
-
*/
|
|
62
|
-
export function detectPlannerRoute(events, mode) {
|
|
63
|
-
const resolved = mode ?? "auto";
|
|
64
|
-
if (resolved === "session")
|
|
65
|
-
return "A";
|
|
66
|
-
if (resolved === "off")
|
|
67
|
-
return "B";
|
|
68
|
-
return hasCacheEvidence(events) ? "A" : "B";
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Extract one message's text without depending on the dsh-llm ContentBlock
|
|
72
|
-
* type: string blocks pass through, object blocks contribute their `text`
|
|
73
|
-
* field when present. Non-text blocks (tool calls, images) are dropped.
|
|
74
|
-
*/
|
|
75
|
-
function blockText(content) {
|
|
76
|
-
if (typeof content === "string")
|
|
77
|
-
return content;
|
|
78
|
-
if (!Array.isArray(content))
|
|
79
|
-
return "";
|
|
80
|
-
const parts = [];
|
|
81
|
-
for (const block of content) {
|
|
82
|
-
if (typeof block === "string") {
|
|
83
|
-
parts.push(block);
|
|
84
|
-
}
|
|
85
|
-
else if (block !== null && typeof block === "object" && "text" in block && typeof block.text === "string") {
|
|
86
|
-
parts.push(block.text);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return parts.filter((text) => text.length > 0).join(" ");
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Rebuild a tail-biased session prefix from the event log.
|
|
93
|
-
*
|
|
94
|
-
* Inclusion mirrors the trajectory grounding (only direct human `user`
|
|
95
|
-
* speech plus the model's own assistant text), so injected plugin context
|
|
96
|
-
* and tool results never echo back into the planning input. Whole messages
|
|
97
|
-
* are dropped oldest-first over budget; a single over-budget message keeps
|
|
98
|
-
* its tail.
|
|
99
|
-
*
|
|
100
|
-
* @param events - session log rows, newest last.
|
|
101
|
-
* @param opts - provider/model stamps the assistant messages (the calling
|
|
102
|
-
* agent's own route); maxChars caps the prefix (default {@link DEFAULT_PREFIX_MAX_CHARS}).
|
|
103
|
-
* @returns the prefix messages, oldest first ([] when nothing qualifies —
|
|
104
|
-
* callers fall back to Route B).
|
|
105
|
-
* @throws When assistant text exists but provider/model is missing (an
|
|
106
|
-
* assistant message cannot be honestly stamped without its route).
|
|
107
|
-
*/
|
|
108
|
-
export function buildPrefixMessages(events, opts) {
|
|
109
|
-
const maxChars = opts.maxChars ?? DEFAULT_PREFIX_MAX_CHARS;
|
|
110
|
-
if (!Number.isSafeInteger(maxChars) || maxChars < 1) {
|
|
111
|
-
throw new Error(`evolve: prefix-cache maxChars must be a positive integer, got ${String(opts.maxChars)}`);
|
|
112
|
-
}
|
|
113
|
-
const pairs = [];
|
|
114
|
-
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
115
|
-
const raw = events[i];
|
|
116
|
-
if (typeof raw !== "object" || raw === null)
|
|
117
|
-
continue;
|
|
118
|
-
const event = raw;
|
|
119
|
-
const isUser = event.type === "user/message";
|
|
120
|
-
const isAssistant = event.type === "assistant/message";
|
|
121
|
-
if (!isUser && !isAssistant)
|
|
122
|
-
continue;
|
|
123
|
-
if (isUser) {
|
|
124
|
-
const kind = event.data?.source?.kind;
|
|
125
|
-
if (kind !== undefined && kind !== "user")
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
const text = blockText(event.data?.content).trim();
|
|
129
|
-
if (text.length === 0)
|
|
130
|
-
continue;
|
|
131
|
-
pairs.push({ role: isUser ? "user" : "assistant", text });
|
|
132
|
-
}
|
|
133
|
-
if (pairs.length === 0)
|
|
134
|
-
return [];
|
|
135
|
-
// Collected newest-first; restore conversation order before budgeting.
|
|
136
|
-
pairs.reverse();
|
|
137
|
-
// Tail-biased budget: drop oldest whole messages over budget; a lone
|
|
138
|
-
// over-budget message keeps its tail.
|
|
139
|
-
let total = pairs.reduce((sum, pair) => sum + pair.text.length, 0);
|
|
140
|
-
while (pairs.length > 1 && total > maxChars) {
|
|
141
|
-
const dropped = pairs.shift();
|
|
142
|
-
total -= dropped?.text.length ?? 0;
|
|
143
|
-
}
|
|
144
|
-
const oldest = pairs[0];
|
|
145
|
-
if (oldest !== undefined && total > maxChars) {
|
|
146
|
-
oldest.text = oldest.text.slice(-maxChars);
|
|
147
|
-
}
|
|
148
|
-
const messages = [];
|
|
149
|
-
for (const pair of pairs) {
|
|
150
|
-
if (pair.role === "user") {
|
|
151
|
-
messages.push(createUserMessage({
|
|
152
|
-
source: { kind: "user" },
|
|
153
|
-
content: [{ type: "text", text: pair.text }],
|
|
154
|
-
}));
|
|
155
|
-
}
|
|
156
|
-
else {
|
|
157
|
-
if (!opts.provider || !opts.model) {
|
|
158
|
-
throw new Error("evolve: prefix-cache needs the agent provider/model to stamp assistant messages");
|
|
159
|
-
}
|
|
160
|
-
messages.push(createAssistantMessage({
|
|
161
|
-
source: { provider: opts.provider, model: opts.model },
|
|
162
|
-
content: [{ type: "text", text: pair.text }],
|
|
163
|
-
}));
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return messages;
|
|
167
|
-
}
|
|
168
|
-
//# sourceMappingURL=prefix-cache.js.map
|
package/lib/project.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stable project key for a workspace. `workspaceIdentity` (when set)
|
|
3
|
-
* replaces the path as the hash source so renamed checkouts of the same
|
|
4
|
-
* logical project keep one store.
|
|
5
|
-
*/
|
|
6
|
-
export declare function resolveProjectKey(workspacePath: string, workspaceIdentity?: string): string;
|
|
7
|
-
/** Defense for externally supplied keys (explicit opts): single safe segment. */
|
|
8
|
-
export declare function sanitizeProjectKey(key: string): string;
|
|
9
|
-
/** Minimal agent shape the project-key derivation needs (duck-typed). */
|
|
10
|
-
export interface ProjectKeyAgentLike {
|
|
11
|
-
session?: {
|
|
12
|
-
header?: {
|
|
13
|
-
cwd?: unknown;
|
|
14
|
-
meta?: {
|
|
15
|
-
cwd?: unknown;
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Derive the project key from an assembling agent's session cwd. Returns
|
|
22
|
-
* undefined when no validated absolute cwd is present — the caller then
|
|
23
|
-
* skips the project layer rather than failing the whole assembly.
|
|
24
|
-
*/
|
|
25
|
-
export declare function projectKeyOf(agent: unknown, opts?: {
|
|
26
|
-
workspaceIdentity?: string;
|
|
27
|
-
}): string | undefined;
|
|
28
|
-
//# sourceMappingURL=project.d.ts.map
|
package/lib/project.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project scope identity: the third harness layer between session-local
|
|
3
|
-
* staging and the cross-project global store.
|
|
4
|
-
*
|
|
5
|
-
* A project is identified by the session's validated absolute `cwd`
|
|
6
|
-
* (stable slug + sha256, mirroring ZCode's `project-root.ts`), so every
|
|
7
|
-
* session opened in the same workspace shares one project store while
|
|
8
|
-
* different checkouts never collide. All helpers are pure and never throw —
|
|
9
|
-
* a missing cwd simply means "no project layer" (the caller degrades to
|
|
10
|
-
* global+local, the same shape as an empty project store).
|
|
11
|
-
*/
|
|
12
|
-
import { createHash } from "node:crypto";
|
|
13
|
-
import { basename, isAbsolute, resolve } from "node:path";
|
|
14
|
-
/**
|
|
15
|
-
* Stable project key for a workspace. `workspaceIdentity` (when set)
|
|
16
|
-
* replaces the path as the hash source so renamed checkouts of the same
|
|
17
|
-
* logical project keep one store.
|
|
18
|
-
*/
|
|
19
|
-
export function resolveProjectKey(workspacePath, workspaceIdentity) {
|
|
20
|
-
const identity = workspaceIdentity?.trim();
|
|
21
|
-
const normalized = resolve(workspacePath);
|
|
22
|
-
const keySource = identity && identity.length > 0 ? identity : process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
23
|
-
const hash = createHash("sha256").update(keySource).digest("hex").slice(0, 16);
|
|
24
|
-
const slug = identity && identity.length > 0 ? "project" : sanitizeSlug(basename(normalized) || "project");
|
|
25
|
-
return `${slug}-${hash}`;
|
|
26
|
-
}
|
|
27
|
-
function sanitizeSlug(value) {
|
|
28
|
-
const slug = value
|
|
29
|
-
.toLowerCase()
|
|
30
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
31
|
-
.replace(/^-+|-+$/g, "")
|
|
32
|
-
.slice(0, 48);
|
|
33
|
-
return slug.length > 0 ? slug : "project";
|
|
34
|
-
}
|
|
35
|
-
/** Defense for externally supplied keys (explicit opts): single safe segment. */
|
|
36
|
-
export function sanitizeProjectKey(key) {
|
|
37
|
-
const cleaned = key
|
|
38
|
-
.trim()
|
|
39
|
-
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
40
|
-
.replace(/^-+|-+$/g, "")
|
|
41
|
-
.slice(0, 64);
|
|
42
|
-
return cleaned.length > 0 ? cleaned : "project";
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Derive the project key from an assembling agent's session cwd. Returns
|
|
46
|
-
* undefined when no validated absolute cwd is present — the caller then
|
|
47
|
-
* skips the project layer rather than failing the whole assembly.
|
|
48
|
-
*/
|
|
49
|
-
export function projectKeyOf(agent, opts) {
|
|
50
|
-
try {
|
|
51
|
-
const header = agent?.session?.header;
|
|
52
|
-
const raw = header?.cwd ?? header?.meta?.cwd;
|
|
53
|
-
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
const cwd = raw.trim();
|
|
57
|
-
if (!isAbsolute(cwd)) {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
return resolveProjectKey(cwd, opts?.workspaceIdentity);
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
//# sourceMappingURL=project.js.map
|
package/lib/projection.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import type { HarnessState } from "./types.js";
|
|
2
|
-
/** Index file name at the store root. */
|
|
3
|
-
export declare const MEMORY_INDEX_FILE = "MEMORY.md";
|
|
4
|
-
/** Per-fact file directory inside the store. */
|
|
5
|
-
export declare const MEMORY_FACTS_DIR = "memory";
|
|
6
|
-
/** Map a memory id to a flat filename (`:` is legal on disk but hostile to tools). */
|
|
7
|
-
export declare function memoryFactFilename(id: string): string;
|
|
8
|
-
/**
|
|
9
|
-
* Rewrite the whole projection from the post-apply state: index, one file
|
|
10
|
-
* per memory entry (including archived ones, flagged), and deletion of
|
|
11
|
-
* files whose ids no longer exist. Deterministic — the same state always
|
|
12
|
-
* produces the same files.
|
|
13
|
-
*
|
|
14
|
-
* @param stateDir The store directory holding harness_state.json.
|
|
15
|
-
* @param state The freshly persisted state (post-apply, post-rollback).
|
|
16
|
-
*/
|
|
17
|
-
export declare function materializeMemoryProjection(stateDir: string, state: HarnessState): void;
|
|
18
|
-
/** Read back one projected fact file (round-trip helper for tests and tools). */
|
|
19
|
-
export declare function readMemoryFact(factsDir: string, filename: string): string;
|
|
20
|
-
/** True when the store directory carries a projection from a previous apply. */
|
|
21
|
-
export declare function hasMemoryProjection(stateDir: string): boolean;
|
|
22
|
-
//# sourceMappingURL=projection.d.ts.map
|
package/lib/projection.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Readable Markdown projection (P1): a ZCode-style `MEMORY.md` index plus
|
|
3
|
-
* one file per memory fact, materialized from the JSON store.
|
|
4
|
-
*
|
|
5
|
-
* The JSON harness state stays the single source of truth (versions,
|
|
6
|
-
* rollback, audit). The projection is a derived, human-trustable view:
|
|
7
|
-
* it is rewritten from the post-apply state inside the engine, so the
|
|
8
|
-
* model can never write it directly and rollback/archive stay in sync.
|
|
9
|
-
*
|
|
10
|
-
* Layout per store directory:
|
|
11
|
-
*
|
|
12
|
-
* <stateDir>/MEMORY.md one line per memory entry
|
|
13
|
-
* <stateDir>/memory/<file>.md one fact per file with frontmatter
|
|
14
|
-
*/
|
|
15
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
import { isArchived, isMemoryType, MEMORY_TYPE_KEY } from "./types.js";
|
|
18
|
-
/** Index file name at the store root. */
|
|
19
|
-
export const MEMORY_INDEX_FILE = "MEMORY.md";
|
|
20
|
-
/** Per-fact file directory inside the store. */
|
|
21
|
-
export const MEMORY_FACTS_DIR = "memory";
|
|
22
|
-
/** Map a memory id to a flat filename (`:` is legal on disk but hostile to tools). */
|
|
23
|
-
export function memoryFactFilename(id) {
|
|
24
|
-
return `${id.replace(/:/g, "_")}.md`;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Rewrite the whole projection from the post-apply state: index, one file
|
|
28
|
-
* per memory entry (including archived ones, flagged), and deletion of
|
|
29
|
-
* files whose ids no longer exist. Deterministic — the same state always
|
|
30
|
-
* produces the same files.
|
|
31
|
-
*
|
|
32
|
-
* @param stateDir The store directory holding harness_state.json.
|
|
33
|
-
* @param state The freshly persisted state (post-apply, post-rollback).
|
|
34
|
-
*/
|
|
35
|
-
export function materializeMemoryProjection(stateDir, state) {
|
|
36
|
-
const entries = Object.values(state.entries.memory).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
37
|
-
const factsDir = join(stateDir, MEMORY_FACTS_DIR);
|
|
38
|
-
mkdirSync(factsDir, { recursive: true });
|
|
39
|
-
const expected = new Map();
|
|
40
|
-
for (const entry of entries) {
|
|
41
|
-
let filename = memoryFactFilename(entry.id);
|
|
42
|
-
let suffix = 1;
|
|
43
|
-
while (expected.has(filename)) {
|
|
44
|
-
suffix += 1;
|
|
45
|
-
filename = `${memoryFactFilename(entry.id).replace(/\.md$/, "")}~${suffix}.md`;
|
|
46
|
-
}
|
|
47
|
-
expected.set(filename, entry);
|
|
48
|
-
}
|
|
49
|
-
for (const [filename, entry] of expected) {
|
|
50
|
-
writeFileSync(join(factsDir, filename), renderMemoryFact(entry), { encoding: "utf8", mode: 0o600 });
|
|
51
|
-
}
|
|
52
|
-
// Sweep facts whose entries vanished (delete); archived entries keep
|
|
53
|
-
// their file with archived:true frontmatter.
|
|
54
|
-
let existing;
|
|
55
|
-
try {
|
|
56
|
-
existing = readdirSync(factsDir).filter((file) => file.endsWith(".md"));
|
|
57
|
-
}
|
|
58
|
-
catch {
|
|
59
|
-
existing = [];
|
|
60
|
-
}
|
|
61
|
-
for (const file of existing) {
|
|
62
|
-
if (!expected.has(file)) {
|
|
63
|
-
try {
|
|
64
|
-
unlinkSync(join(factsDir, file));
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
// A stale fact file must not break the apply path; the next
|
|
68
|
-
// materialization retries the sweep.
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
writeFileSync(join(stateDir, MEMORY_INDEX_FILE), renderMemoryIndex([...expected]), { encoding: "utf8", mode: 0o600 });
|
|
73
|
-
}
|
|
74
|
-
/** Read back one projected fact file (round-trip helper for tests and tools). */
|
|
75
|
-
export function readMemoryFact(factsDir, filename) {
|
|
76
|
-
return readFileSync(join(factsDir, filename), "utf8");
|
|
77
|
-
}
|
|
78
|
-
/** True when the store directory carries a projection from a previous apply. */
|
|
79
|
-
export function hasMemoryProjection(stateDir) {
|
|
80
|
-
return existsSync(join(stateDir, MEMORY_INDEX_FILE)) && existsSync(join(stateDir, MEMORY_FACTS_DIR));
|
|
81
|
-
}
|
|
82
|
-
function renderMemoryFact(entry) {
|
|
83
|
-
const type = isMemoryType(entry.metadata[MEMORY_TYPE_KEY]) ? String(entry.metadata[MEMORY_TYPE_KEY]) : "untyped";
|
|
84
|
-
const frontmatter = [
|
|
85
|
-
"---",
|
|
86
|
-
`id: ${entry.id}`,
|
|
87
|
-
`scope: ${entry.scope}`,
|
|
88
|
-
`kind: memory`,
|
|
89
|
-
`memoryType: ${type}`,
|
|
90
|
-
`version: ${entry.version}`,
|
|
91
|
-
`path: ${entry.path}`,
|
|
92
|
-
`archived: ${isArchived(entry)}`,
|
|
93
|
-
`created_at: ${entry.created_at}`,
|
|
94
|
-
`updated_at: ${entry.updated_at}`,
|
|
95
|
-
"---",
|
|
96
|
-
].join("\n");
|
|
97
|
-
return `${frontmatter}\n\n# ${entry.title}\n\n${entry.content}\n`;
|
|
98
|
-
}
|
|
99
|
-
function renderMemoryIndex(files) {
|
|
100
|
-
const lines = ["# Memory Index", "", "Derived from the JSON harness state — do not edit by hand; it is rewritten on every memory apply.", ""];
|
|
101
|
-
if (files.length === 0) {
|
|
102
|
-
lines.push("(no memory entries yet)");
|
|
103
|
-
return `${lines.join("\n")}\n`;
|
|
104
|
-
}
|
|
105
|
-
for (const [file, entry] of files) {
|
|
106
|
-
const type = isMemoryType(entry.metadata[MEMORY_TYPE_KEY]) ? String(entry.metadata[MEMORY_TYPE_KEY]) : "untyped";
|
|
107
|
-
const archived = isArchived(entry) ? ", archived" : "";
|
|
108
|
-
lines.push(`- [${entry.scope}:${entry.id}] ${entry.title} (type=${type}, v${entry.version}${archived}) → ${MEMORY_FACTS_DIR}/${file}`);
|
|
109
|
-
}
|
|
110
|
-
return `${lines.join("\n")}\n`;
|
|
111
|
-
}
|
|
112
|
-
//# sourceMappingURL=projection.js.map
|
package/lib/promotion.d.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Promotion policy: mechanical, code-enforced gates that decide whether a
|
|
3
|
-
* local entry MAY be promoted to the cross-session global store ("模型提议,
|
|
4
|
-
* 代码保证"). The LLM classification proposes; these guards dispose.
|
|
5
|
-
*
|
|
6
|
-
* Rationale (2026-08-22 store audit): the global store is shared by every
|
|
7
|
-
* project on one profile, so promoting project-scoped knowledge taxes every
|
|
8
|
-
* future session in every project. Measured failure modes:
|
|
9
|
-
* - absolute paths / session ids in promoted content (project-scoped),
|
|
10
|
-
* - near-duplicates of existing global entries re-promoted from later
|
|
11
|
-
* sessions (title matching alone missed them),
|
|
12
|
-
* - one-line facts whose framing costs more than their content.
|
|
13
|
-
*
|
|
14
|
-
* Pure functions only — the callers (wrapup command, gate local-fate phase)
|
|
15
|
-
* supply the resolved {@link PromotionPolicy}.
|
|
16
|
-
*/
|
|
17
|
-
import type { HarnessEntry, HarnessState, RefinementKind } from "./types.js";
|
|
18
|
-
export interface PromotionPolicy {
|
|
19
|
-
/** Content matching any pattern is project-scoped and stays local. */
|
|
20
|
-
blockPatterns: RegExp[];
|
|
21
|
-
/** Whole promotions below this content length stay local (chars). */
|
|
22
|
-
minPromoteChars: number;
|
|
23
|
-
/** Content overlap above this against a global entry = duplicate. */
|
|
24
|
-
maxContentOverlap: number;
|
|
25
|
-
}
|
|
26
|
-
export declare const DEFAULT_PROMOTION_POLICY: PromotionPolicy;
|
|
27
|
-
/**
|
|
28
|
-
* Write-time conflict guard (R2): a global create whose similarity against an
|
|
29
|
-
* existing same-kind entry reaches this score is rejected outright — a
|
|
30
|
-
* near-duplicate adds zero information and the model should evolve_update
|
|
31
|
-
* the existing entry instead.
|
|
32
|
-
*/
|
|
33
|
-
export declare const CONFLICT_BLOCK_SCORE = 0.8;
|
|
34
|
-
/** Similarity at/above this stamps {@link CONFLICT_HINT_KEY} but lets the write proceed. */
|
|
35
|
-
export declare const CONFLICT_WARN_SCORE = 0.5;
|
|
36
|
-
/**
|
|
37
|
-
* First reason the content reads as carrying a live credential, or undefined
|
|
38
|
-
* when it screens clean. Unlike {@link projectScopedReason} this guard is
|
|
39
|
-
* NOT policy-configurable — it protects every sink at once (promotion path,
|
|
40
|
-
* global writes at the engine throat, mount materialization).
|
|
41
|
-
*/
|
|
42
|
-
export declare function secretLeakReason(content: string): string | undefined;
|
|
43
|
-
/**
|
|
44
|
-
* Build a policy from config values (schemastery strings compiled here so
|
|
45
|
-
* the config layer never touches RegExp). Invalid patterns are skipped —
|
|
46
|
-
* a broken user pattern must not disable the remaining guards.
|
|
47
|
-
*/
|
|
48
|
-
export declare function resolvePromotionPolicy(options: {
|
|
49
|
-
blockPatterns?: readonly string[] | undefined;
|
|
50
|
-
minPromoteChars?: number | undefined;
|
|
51
|
-
maxContentOverlap?: number | undefined;
|
|
52
|
-
}): PromotionPolicy;
|
|
53
|
-
/**
|
|
54
|
-
* First reason the content reads as project-scoped, or undefined when it
|
|
55
|
-
* looks portable. Returns the matched pattern source so skip reports stay
|
|
56
|
-
* explainable in reviews.jsonl rationales.
|
|
57
|
-
*/
|
|
58
|
-
export declare function projectScopedReason(content: string, policy: PromotionPolicy): string | undefined;
|
|
59
|
-
/**
|
|
60
|
-
* Token set for cheap similarity, built on the search module's tokenizer —
|
|
61
|
-
* one home for "how this project splits words" (ASCII words lowercased plus
|
|
62
|
-
* CJK character bigrams; bigrams survive segmentation-free Chinese text).
|
|
63
|
-
* The set form feeds Jaccard overlap (review audit 2026-08-28 C: the
|
|
64
|
-
* previous private tokenizer duplicated the definition; the calibrated
|
|
65
|
-
* 0.6/0.8 thresholds are judgment values, not data-fitted, so the merge
|
|
66
|
-
* costs at most a test re-pin).
|
|
67
|
-
*/
|
|
68
|
-
export declare function normalizedTokens(text: string): Set<string>;
|
|
69
|
-
/** Jaccard similarity of two texts' normalized token sets (0..1). */
|
|
70
|
-
export declare function contentOverlap(a: string, b: string): number;
|
|
71
|
-
export interface SimilarEntryHit {
|
|
72
|
-
id: string;
|
|
73
|
-
title: string;
|
|
74
|
-
score: number;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Human/LLM-readable description of a similarity hit, shared by the block
|
|
78
|
-
* error and the approval-question suffix so both surfaces explain the same
|
|
79
|
-
* way.
|
|
80
|
-
*/
|
|
81
|
-
export declare function buildConflictNotice(hit: SimilarEntryHit): string;
|
|
82
|
-
/**
|
|
83
|
-
* The most similar entry of the list, above `minScore`. Title and content
|
|
84
|
-
* both feed the comparison (titles are short; content carries the real
|
|
85
|
-
* signal). Generic form of {@link mostSimilarGlobalEntry} — callers decide
|
|
86
|
-
* which corpus (global store, merged view) the candidates come from.
|
|
87
|
-
*/
|
|
88
|
-
export declare function mostSimilarEntry(entries: readonly HarnessEntry[], title: string, content: string, minScore: number): SimilarEntryHit | undefined;
|
|
89
|
-
/**
|
|
90
|
-
* The most similar non-archived global entry of the same kind, above the
|
|
91
|
-
* policy threshold.
|
|
92
|
-
*/
|
|
93
|
-
export declare function mostSimilarGlobalEntry(globalState: HarnessState, kind: RefinementKind, title: string, content: string, policy: PromotionPolicy): SimilarEntryHit | undefined;
|
|
94
|
-
//# sourceMappingURL=promotion.d.ts.map
|
package/lib/promotion.js
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { isArchived } from "./types.js";
|
|
2
|
-
import { tokenize } from "./search.js";
|
|
3
|
-
/** Regex sources that mark content as project-scoped (never global). */
|
|
4
|
-
const DEFAULT_BLOCK_PATTERNS = [
|
|
5
|
-
String.raw `\/(?:mnt|home|Users)\/`, // absolute POSIX paths: "/home/…", "/mnt/…", "/Users/…"
|
|
6
|
-
String.raw `\bsession-[0-9a-f]{8}\b`, // session-scoped identifiers
|
|
7
|
-
String.raw `~/\.dsh\b`, // user harness home references
|
|
8
|
-
];
|
|
9
|
-
export const DEFAULT_PROMOTION_POLICY = {
|
|
10
|
-
blockPatterns: DEFAULT_BLOCK_PATTERNS.map((source) => new RegExp(source, "i")),
|
|
11
|
-
minPromoteChars: 100,
|
|
12
|
-
maxContentOverlap: 0.6,
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Write-time conflict guard (R2): a global create whose similarity against an
|
|
16
|
-
* existing same-kind entry reaches this score is rejected outright — a
|
|
17
|
-
* near-duplicate adds zero information and the model should evolve_update
|
|
18
|
-
* the existing entry instead.
|
|
19
|
-
*/
|
|
20
|
-
export const CONFLICT_BLOCK_SCORE = 0.8;
|
|
21
|
-
/** Similarity at/above this stamps {@link CONFLICT_HINT_KEY} but lets the write proceed. */
|
|
22
|
-
export const CONFLICT_WARN_SCORE = 0.5;
|
|
23
|
-
/**
|
|
24
|
-
* Fixed secret-detection patterns — a security invariant, deliberately NOT
|
|
25
|
-
* part of the configurable {@link PromotionPolicy}: a user pattern typo must
|
|
26
|
-
* never be able to disable secret screening. Each entry pairs a human label
|
|
27
|
-
* with a regex tuned for low false positives (placeholders like
|
|
28
|
-
* "YOUR_API_KEY_HERE" don't match; realistic mixed literals do).
|
|
29
|
-
*/
|
|
30
|
-
const SECRET_PATTERNS = [
|
|
31
|
-
{ label: "AnySearch API key", regex: /\bas_sk_[A-Za-z0-9]{8,}\b/ },
|
|
32
|
-
{ label: "Anthropic API key", regex: /\bsk-ant-[A-Za-z0-9_-]{16,}\b/ },
|
|
33
|
-
{ label: "OpenAI-style API key", regex: /\bsk-(?:proj-)?[A-Za-z0-9_-]{16,}\b/ },
|
|
34
|
-
{ label: "GitHub token", regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/ },
|
|
35
|
-
{ label: "GitHub fine-grained PAT", regex: /\bgithub_pat_[A-Za-z0-9_]{20,}\b/ },
|
|
36
|
-
{ label: "AWS access key", regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/ },
|
|
37
|
-
{ label: "Google API key", regex: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
38
|
-
{ label: "Slack token", regex: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
39
|
-
{ label: "npm grant token", regex: /\bnpm_[A-Za-z0-9]{36}\b/ },
|
|
40
|
-
{ label: "private key block", regex: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY(?: BLOCK)?-----/ },
|
|
41
|
-
{
|
|
42
|
-
label: "credential assignment",
|
|
43
|
-
// The credential-name group is manually case-insensitive ("apiKey" in
|
|
44
|
-
// JSON, "API_KEY" in env examples) so the placeholder lookahead stays
|
|
45
|
-
// case-sensitive: it excludes only ALL-CAPS placeholders
|
|
46
|
-
// ("YOUR_KEY_HERE"), not real mixed/lowercase literals.
|
|
47
|
-
regex: /\b(?:[Aa][Pp][Ii][_-]?[Kk][Ee][Yy]|[Ss][Ee][Cc][Rr][Ee][Tt]|[Tt][Oo][Kk][Ee][Nn]|[Pp][Aa][Ss][Ss][Ww][Oo][Rr][Dd]|[Pp][Aa][Ss][Ss][Ww][Dd]|[Pp][Ww][Dd])\b["']?\s*[:=]\s*["'](?!["']*[A-Z0-9_]+["'])[A-Za-z0-9+/_-]{16,}["']/,
|
|
48
|
-
},
|
|
49
|
-
];
|
|
50
|
-
/**
|
|
51
|
-
* Redact a matched secret for error/audit surfaces: enough to identify the
|
|
52
|
-
* credential family, never the full value (reasons land in reviews.jsonl,
|
|
53
|
-
* question texts, and logs).
|
|
54
|
-
*/
|
|
55
|
-
function redactSecret(matched) {
|
|
56
|
-
if (matched.length <= 10) {
|
|
57
|
-
return `${matched.slice(0, 3)}…`;
|
|
58
|
-
}
|
|
59
|
-
return `${matched.slice(0, 6)}…${matched.slice(-3)}`;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* First reason the content reads as carrying a live credential, or undefined
|
|
63
|
-
* when it screens clean. Unlike {@link projectScopedReason} this guard is
|
|
64
|
-
* NOT policy-configurable — it protects every sink at once (promotion path,
|
|
65
|
-
* global writes at the engine throat, mount materialization).
|
|
66
|
-
*/
|
|
67
|
-
export function secretLeakReason(content) {
|
|
68
|
-
for (const { label, regex } of SECRET_PATTERNS) {
|
|
69
|
-
const match = regex.exec(content);
|
|
70
|
-
if (match?.[0]) {
|
|
71
|
-
return `possible ${label} ("${redactSecret(match[0])}") — secrets must never be sedimented into harness state; rotate the credential and keep it out of the store`;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return undefined;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Build a policy from config values (schemastery strings compiled here so
|
|
78
|
-
* the config layer never touches RegExp). Invalid patterns are skipped —
|
|
79
|
-
* a broken user pattern must not disable the remaining guards.
|
|
80
|
-
*/
|
|
81
|
-
export function resolvePromotionPolicy(options) {
|
|
82
|
-
const patterns = [...(options.blockPatterns ?? [])]
|
|
83
|
-
.map((source) => {
|
|
84
|
-
try {
|
|
85
|
-
return new RegExp(source, "i");
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
.filter((pattern) => pattern !== undefined);
|
|
92
|
-
return {
|
|
93
|
-
blockPatterns: patterns.length > 0 ? patterns : DEFAULT_PROMOTION_POLICY.blockPatterns,
|
|
94
|
-
minPromoteChars: options.minPromoteChars ?? DEFAULT_PROMOTION_POLICY.minPromoteChars,
|
|
95
|
-
maxContentOverlap: options.maxContentOverlap ?? DEFAULT_PROMOTION_POLICY.maxContentOverlap,
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* First reason the content reads as project-scoped, or undefined when it
|
|
100
|
-
* looks portable. Returns the matched pattern source so skip reports stay
|
|
101
|
-
* explainable in reviews.jsonl rationales.
|
|
102
|
-
*/
|
|
103
|
-
export function projectScopedReason(content, policy) {
|
|
104
|
-
for (const pattern of policy.blockPatterns) {
|
|
105
|
-
if (pattern.test(content)) {
|
|
106
|
-
return `project-scoped content (matches /${pattern.source}/)`;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Token set for cheap similarity, built on the search module's tokenizer —
|
|
113
|
-
* one home for "how this project splits words" (ASCII words lowercased plus
|
|
114
|
-
* CJK character bigrams; bigrams survive segmentation-free Chinese text).
|
|
115
|
-
* The set form feeds Jaccard overlap (review audit 2026-08-28 C: the
|
|
116
|
-
* previous private tokenizer duplicated the definition; the calibrated
|
|
117
|
-
* 0.6/0.8 thresholds are judgment values, not data-fitted, so the merge
|
|
118
|
-
* costs at most a test re-pin).
|
|
119
|
-
*/
|
|
120
|
-
export function normalizedTokens(text) {
|
|
121
|
-
return new Set(tokenize(text));
|
|
122
|
-
}
|
|
123
|
-
/** Jaccard similarity of two texts' normalized token sets (0..1). */
|
|
124
|
-
export function contentOverlap(a, b) {
|
|
125
|
-
const left = normalizedTokens(a);
|
|
126
|
-
const right = normalizedTokens(b);
|
|
127
|
-
if (left.size === 0 || right.size === 0) {
|
|
128
|
-
return 0;
|
|
129
|
-
}
|
|
130
|
-
let intersection = 0;
|
|
131
|
-
for (const token of left) {
|
|
132
|
-
if (right.has(token)) {
|
|
133
|
-
intersection += 1;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return intersection / (left.size + right.size - intersection);
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Human/LLM-readable description of a similarity hit, shared by the block
|
|
140
|
-
* error and the approval-question suffix so both surfaces explain the same
|
|
141
|
-
* way.
|
|
142
|
-
*/
|
|
143
|
-
export function buildConflictNotice(hit) {
|
|
144
|
-
return `near-duplicate of ${hit.id} 「${hit.title}」 (similarity ${Math.round(hit.score * 100)}%)`;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* The most similar entry of the list, above `minScore`. Title and content
|
|
148
|
-
* both feed the comparison (titles are short; content carries the real
|
|
149
|
-
* signal). Generic form of {@link mostSimilarGlobalEntry} — callers decide
|
|
150
|
-
* which corpus (global store, merged view) the candidates come from.
|
|
151
|
-
*/
|
|
152
|
-
export function mostSimilarEntry(entries, title, content, minScore) {
|
|
153
|
-
let best;
|
|
154
|
-
for (const other of entries) {
|
|
155
|
-
if (isArchived(other))
|
|
156
|
-
continue;
|
|
157
|
-
const score = Math.max(contentOverlap(title, other.title), contentOverlap(content, other.content));
|
|
158
|
-
if (score >= minScore && (best === undefined || score > best.score)) {
|
|
159
|
-
best = { id: other.id, title: other.title, score };
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return best;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* The most similar non-archived global entry of the same kind, above the
|
|
166
|
-
* policy threshold.
|
|
167
|
-
*/
|
|
168
|
-
export function mostSimilarGlobalEntry(globalState, kind, title, content, policy) {
|
|
169
|
-
return mostSimilarEntry(Object.values(globalState.entries[kind]), title, content, policy.maxContentOverlap);
|
|
170
|
-
}
|
|
171
|
-
//# sourceMappingURL=promotion.js.map
|