@aiwayds/dsh-topics-memory 0.23.0 → 0.24.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/lib/config.d.ts +18 -1
- package/lib/config.js +13 -0
- package/lib/digest.d.ts +38 -5
- package/lib/digest.js +57 -15
- package/lib/ilog.d.ts +14 -0
- package/lib/index.d.ts +9 -1
- package/lib/index.js +1 -0
- package/lib/okf.d.ts +34 -0
- package/lib/okf.js +41 -0
- package/lib/paths.d.ts +9 -0
- package/lib/paths.js +15 -0
- package/lib/pipeline-events.d.ts +4 -2
- package/lib/pipeline-events.js +2 -0
- package/lib/service.d.ts +88 -3
- package/lib/service.js +431 -15
- package/lib/store.d.ts +30 -3
- package/lib/store.js +28 -1
- package/package.json +2 -2
- package/src/config.ts +15 -0
- package/src/digest.ts +85 -12
- package/src/ilog.ts +9 -0
- package/src/index.ts +9 -0
- package/src/okf.ts +68 -0
- package/src/paths.ts +15 -0
- package/src/pipeline-events.ts +6 -2
- package/src/service.ts +472 -17
- package/src/store.ts +45 -3
package/lib/config.d.ts
CHANGED
|
@@ -118,6 +118,13 @@ export declare const TopicsConfig: z<Schemastery.ObjectS<NoInfer<{
|
|
|
118
118
|
* behavior change anywhere (byte-for-byte legacy); shadow = compute +
|
|
119
119
|
* bypass-ledger only, nothing lands in context; on = engage. */
|
|
120
120
|
packageMode: z<string, string, "volatile-defined">;
|
|
121
|
+
/** Lesson injection gate (B2, spec §7 C1 MJ-4): off | shadow | on, default
|
|
122
|
+
* off. off = established lessons neither inject nor shadow (zero injection-
|
|
123
|
+
* side behavior); shadow = the lesson priority seat double-computes into
|
|
124
|
+
* the bypass ledger (meta/lesson-shadow.jsonl), never lands in context;
|
|
125
|
+
* on = established lessons inject. Only the injection side lives here —
|
|
126
|
+
* the lesson loop (distill/approval) is the new repo's improveLoop gate. */
|
|
127
|
+
lessonInject: z<string, string, "volatile-defined">;
|
|
121
128
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
122
129
|
/** GitHub repo `owner/name`; empty = local-only mode (ADR 0008). */
|
|
123
130
|
repo: z<string, string, "volatile-defined">;
|
|
@@ -231,6 +238,13 @@ export declare const TopicsConfig: z<Schemastery.ObjectS<NoInfer<{
|
|
|
231
238
|
* behavior change anywhere (byte-for-byte legacy); shadow = compute +
|
|
232
239
|
* bypass-ledger only, nothing lands in context; on = engage. */
|
|
233
240
|
packageMode: z<string, string, "volatile-defined">;
|
|
241
|
+
/** Lesson injection gate (B2, spec §7 C1 MJ-4): off | shadow | on, default
|
|
242
|
+
* off. off = established lessons neither inject nor shadow (zero injection-
|
|
243
|
+
* side behavior); shadow = the lesson priority seat double-computes into
|
|
244
|
+
* the bypass ledger (meta/lesson-shadow.jsonl), never lands in context;
|
|
245
|
+
* on = established lessons inject. Only the injection side lives here —
|
|
246
|
+
* the lesson loop (distill/approval) is the new repo's improveLoop gate. */
|
|
247
|
+
lessonInject: z<string, string, "volatile-defined">;
|
|
234
248
|
}>>, "plain">;
|
|
235
249
|
export type TopicsConfigValue = {
|
|
236
250
|
repo: string;
|
|
@@ -281,8 +295,11 @@ export type TopicsConfigValue = {
|
|
|
281
295
|
embeddingProvider?: string;
|
|
282
296
|
embeddingModel?: string;
|
|
283
297
|
packageMode?: 'off' | 'shadow' | 'on';
|
|
298
|
+
/** Lesson injection gate (B2). Optional in this TS view only (sentinel
|
|
299
|
+
* pattern as above) — consumers treat undefined as 'off'. */
|
|
300
|
+
lessonInject?: 'off' | 'shadow' | 'on';
|
|
284
301
|
};
|
|
285
|
-
export declare const CONFIG_KEYS: readonly ["repo", "autoInject", "injectDedup", "suppressEcho", "topK", "perTopicBudget", "totalBudget", "matchThreshold", "tagBoost", "injectMode", "qualityLane", "graphDepth", "recencyWindowDays", "autoObserve", "includeSubagents", "observationMaxChars", "distillEveryTurns", "distillOnSessionEnd", "distillProvider", "distillModel", "distillBatchSize", "distillMaxModelCalls", "consolidateCadence", "deprecatedTtlDays", "usageBoost", "zeroOpenDecay", "pushDebounceSeconds", "jevEnabled", "jevBackend", "jevModel", "jevTimeoutMs", "jevSecretFile", "jevLayaFallback", "jevLayaUrl", "embeddingProvider", "embeddingModel", "packageMode"];
|
|
302
|
+
export declare const CONFIG_KEYS: readonly ["repo", "autoInject", "injectDedup", "suppressEcho", "topK", "perTopicBudget", "totalBudget", "matchThreshold", "tagBoost", "injectMode", "qualityLane", "graphDepth", "recencyWindowDays", "autoObserve", "includeSubagents", "observationMaxChars", "distillEveryTurns", "distillOnSessionEnd", "distillProvider", "distillModel", "distillBatchSize", "distillMaxModelCalls", "consolidateCadence", "deprecatedTtlDays", "usageBoost", "zeroOpenDecay", "pushDebounceSeconds", "jevEnabled", "jevBackend", "jevModel", "jevTimeoutMs", "jevSecretFile", "jevLayaFallback", "jevLayaUrl", "embeddingProvider", "embeddingModel", "packageMode", "lessonInject"];
|
|
286
303
|
export type ConfigKey = (typeof CONFIG_KEYS)[number];
|
|
287
304
|
/** CamelCase → dash-display (topK → top-k) for command output. */
|
|
288
305
|
export declare function displayKey(key: string): string;
|
package/lib/config.js
CHANGED
|
@@ -118,6 +118,13 @@ export const TopicsConfig = z.object({
|
|
|
118
118
|
* behavior change anywhere (byte-for-byte legacy); shadow = compute +
|
|
119
119
|
* bypass-ledger only, nothing lands in context; on = engage. */
|
|
120
120
|
packageMode: z.string().default('off').volatile(),
|
|
121
|
+
/** Lesson injection gate (B2, spec §7 C1 MJ-4): off | shadow | on, default
|
|
122
|
+
* off. off = established lessons neither inject nor shadow (zero injection-
|
|
123
|
+
* side behavior); shadow = the lesson priority seat double-computes into
|
|
124
|
+
* the bypass ledger (meta/lesson-shadow.jsonl), never lands in context;
|
|
125
|
+
* on = established lessons inject. Only the injection side lives here —
|
|
126
|
+
* the lesson loop (distill/approval) is the new repo's improveLoop gate. */
|
|
127
|
+
lessonInject: z.string().default('off').volatile(),
|
|
121
128
|
});
|
|
122
129
|
export const CONFIG_KEYS = [
|
|
123
130
|
'repo',
|
|
@@ -157,6 +164,7 @@ export const CONFIG_KEYS = [
|
|
|
157
164
|
'embeddingProvider',
|
|
158
165
|
'embeddingModel',
|
|
159
166
|
'packageMode',
|
|
167
|
+
'lessonInject',
|
|
160
168
|
];
|
|
161
169
|
/** CamelCase → dash-display (topK → top-k) for command output. */
|
|
162
170
|
export function displayKey(key) {
|
|
@@ -247,6 +255,11 @@ export function parseConfigValue(key, raw) {
|
|
|
247
255
|
return raw;
|
|
248
256
|
return { error: 'package-mode 取值 off|shadow|on' };
|
|
249
257
|
}
|
|
258
|
+
case 'lessonInject': {
|
|
259
|
+
if (raw === 'off' || raw === 'shadow' || raw === 'on')
|
|
260
|
+
return raw;
|
|
261
|
+
return { error: 'lesson-inject 取值 off|shadow|on' };
|
|
262
|
+
}
|
|
250
263
|
case 'jevLayaFallback': {
|
|
251
264
|
if (raw === 'on' || raw === 'true')
|
|
252
265
|
return true;
|
package/lib/digest.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ export interface AssembleResult {
|
|
|
45
45
|
}[];
|
|
46
46
|
/** Slow-lane pointers that entered the context (v4 §4.2). */
|
|
47
47
|
slowIncluded?: string[];
|
|
48
|
+
/** Established lessons that entered the context (B2, spec 卷一 §6.3). */
|
|
49
|
+
lessonIncluded?: string[];
|
|
48
50
|
}
|
|
49
51
|
export interface AssembleConfig {
|
|
50
52
|
perTopicBudget: number;
|
|
@@ -58,6 +60,27 @@ export interface SlowPointerInput {
|
|
|
58
60
|
description?: string;
|
|
59
61
|
why: string;
|
|
60
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* One established-lesson pointer (B2, spec 卷一 §6.3): flat-mode normal-entry
|
|
65
|
+
* rendering with the machine-recognizable source marker (MJ-C combination
|
|
66
|
+
* matrix — lessonInject=on without the A3 package injects lessons as plain
|
|
67
|
+
* entries, tagged `lesson source:<kind>`).
|
|
68
|
+
*/
|
|
69
|
+
export interface LessonPointerInput {
|
|
70
|
+
slug: string;
|
|
71
|
+
title: string;
|
|
72
|
+
status: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
score: number;
|
|
75
|
+
source: string;
|
|
76
|
+
}
|
|
77
|
+
/** The `(lesson source:x)` marker on lesson entries — flat AND package form. */
|
|
78
|
+
export declare function lessonSourceTag(source: string): string;
|
|
79
|
+
/** One flat lesson entry: same two-line pointer shape + the source marker. */
|
|
80
|
+
export declare function lessonEntryText(input: LessonPointerInput, budgetTokens: number): {
|
|
81
|
+
text: string;
|
|
82
|
+
usedTokens: number;
|
|
83
|
+
};
|
|
61
84
|
/**
|
|
62
85
|
* One slow-lane pointer: same shape as a fast pointer plus the mandatory why
|
|
63
86
|
* line (v4 §4.2 — 每条带 why 行). No score: the gate here was the LLM rerank,
|
|
@@ -69,14 +92,15 @@ export declare function slowEntryText(input: SlowPointerInput, budgetTokens: num
|
|
|
69
92
|
};
|
|
70
93
|
/**
|
|
71
94
|
* Greedily pack pointer entries (hits already score-ordered) under the total
|
|
72
|
-
* budget. Zero hits → empty text (ADR 0006: 零命中零注入).
|
|
95
|
+
* budget. Zero hits → empty text (ADR 0006: 零命中零注入). `lessons` (B2)
|
|
96
|
+
* packs after the slow pointers as flat entries with the source marker.
|
|
73
97
|
*/
|
|
74
|
-
export declare function assemblePointer(entries: readonly DigestInput[], cfg: AssembleConfig, slow?: readonly SlowPointerInput[]): AssembleResult;
|
|
98
|
+
export declare function assemblePointer(entries: readonly DigestInput[], cfg: AssembleConfig, slow?: readonly SlowPointerInput[], lessons?: readonly LessonPointerInput[]): AssembleResult;
|
|
75
99
|
/**
|
|
76
|
-
* Greedily pack per-topic digests (hits already score-ordered) under the
|
|
77
|
-
*
|
|
100
|
+
* Greedily pack per-topic digests (hits already score-ordered) under the total
|
|
101
|
+
* budget. Zero hits → empty text (ADR 0006: 零命中零注入). `lessons` (B2).
|
|
78
102
|
*/
|
|
79
|
-
export declare function assembleInjection(entries: readonly DigestInput[], cfg: AssembleConfig, slow?: readonly SlowPointerInput[]): AssembleResult;
|
|
103
|
+
export declare function assembleInjection(entries: readonly DigestInput[], cfg: AssembleConfig, slow?: readonly SlowPointerInput[], lessons?: readonly LessonPointerInput[]): AssembleResult;
|
|
80
104
|
/** One retrieval-side candidate, roster metadata pre-extracted by the caller. */
|
|
81
105
|
export interface PackageCandidate {
|
|
82
106
|
slug: string;
|
|
@@ -90,6 +114,15 @@ export interface PackageCandidate {
|
|
|
90
114
|
reasons: string[];
|
|
91
115
|
/** Edge/lane provenance: `depends:dN` | `slow-pick` | `semantic…` | raw. */
|
|
92
116
|
via?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Lesson priority seat (B2, spec 卷一 §6.3): sorts FIRST in the direct
|
|
119
|
+
* layer and is exempt from the ≤2 direct-seat cap (still budget-bound) —
|
|
120
|
+
* 「主层 ≤2 席中命中 lesson 至少 1 席」. Absent on every topic candidate.
|
|
121
|
+
*/
|
|
122
|
+
prioritySeat?: boolean;
|
|
123
|
+
/** Rendered as a `(${sourceTag})` suffix on the entry head — lesson source
|
|
124
|
+
* marker (`lesson source:correction`), MJ-C. */
|
|
125
|
+
sourceTag?: string;
|
|
93
126
|
}
|
|
94
127
|
export interface PackageInput {
|
|
95
128
|
query: string;
|
package/lib/digest.js
CHANGED
|
@@ -93,6 +93,20 @@ export function pointerEntry(input, budgetTokens) {
|
|
|
93
93
|
const text = lines.join('\n');
|
|
94
94
|
return { text, usedTokens: estimateTokens(text) };
|
|
95
95
|
}
|
|
96
|
+
/** The `(lesson source:x)` marker on lesson entries — flat AND package form. */
|
|
97
|
+
export function lessonSourceTag(source) {
|
|
98
|
+
return `lesson source:${source}`;
|
|
99
|
+
}
|
|
100
|
+
/** One flat lesson entry: same two-line pointer shape + the source marker. */
|
|
101
|
+
export function lessonEntryText(input, budgetTokens) {
|
|
102
|
+
const lines = [];
|
|
103
|
+
lines.push(`### ${input.title} [${input.status}] (topics:${input.slug} score:${input.score.toFixed(2)}) (${lessonSourceTag(input.source)})`);
|
|
104
|
+
const one = (input.description ?? '').replace(/\s+/g, ' ').trim();
|
|
105
|
+
if (one !== '')
|
|
106
|
+
lines.push(truncateToTokens(one, Math.max(16, budgetTokens - 32)).text);
|
|
107
|
+
const text = lines.join('\n');
|
|
108
|
+
return { text, usedTokens: estimateTokens(text) };
|
|
109
|
+
}
|
|
96
110
|
/**
|
|
97
111
|
* One slow-lane pointer: same shape as a fast pointer plus the mandatory why
|
|
98
112
|
* line (v4 §4.2 — 每条带 why 行). No score: the gate here was the LLM rerank,
|
|
@@ -116,16 +130,18 @@ function wrapBlocks(blocks) {
|
|
|
116
130
|
}
|
|
117
131
|
/**
|
|
118
132
|
* Shared greedy packing: fast entries first (score-ordered), then optional
|
|
119
|
-
* slow-lane pointers,
|
|
120
|
-
*
|
|
133
|
+
* slow-lane pointers, then optional lesson entries (B2: flat-mode normal
|
|
134
|
+
* entry slots), all under one total budget (v4 B2: shared 600 — the slow lane
|
|
135
|
+
* spends whatever the fast lane leaves; 分账终值 is a P3 question).
|
|
121
136
|
* Zero blocks → empty text (ADR 0006: 零命中零注入).
|
|
122
137
|
*/
|
|
123
|
-
function packInjection(entries, cfg, render, slow) {
|
|
124
|
-
if (entries.length === 0 && (slow === undefined || slow.length === 0)) {
|
|
138
|
+
function packInjection(entries, cfg, render, slow, lessons) {
|
|
139
|
+
if (entries.length === 0 && (slow === undefined || slow.length === 0) && (lessons === undefined || lessons.length === 0)) {
|
|
125
140
|
return { text: '', usedTokens: 0, included: [], dropped: [] };
|
|
126
141
|
}
|
|
127
142
|
const included = [];
|
|
128
143
|
const slowIncluded = [];
|
|
144
|
+
const lessonIncluded = [];
|
|
129
145
|
const dropped = [];
|
|
130
146
|
const blocks = [];
|
|
131
147
|
let used = estimateTokens(`${WRAPPER_OPEN}\n${WRAPPER_INTRO}\n\n${WRAPPER_CLOSE}`);
|
|
@@ -159,27 +175,47 @@ function packInjection(entries, cfg, render, slow) {
|
|
|
159
175
|
slowIncluded.push(item.slug);
|
|
160
176
|
used += block.usedTokens;
|
|
161
177
|
}
|
|
178
|
+
for (const item of lessons ?? []) {
|
|
179
|
+
const budget = Math.min(cfg.perTopicBudget, cfg.totalBudget - used);
|
|
180
|
+
if (budget < 24) {
|
|
181
|
+
dropped.push({ slug: item.slug, reason: 'lesson-budget' });
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const block = lessonEntryText(item, budget);
|
|
185
|
+
if (used + block.usedTokens > cfg.totalBudget) {
|
|
186
|
+
dropped.push({ slug: item.slug, reason: 'lesson-budget' });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
blocks.push(block.text);
|
|
190
|
+
lessonIncluded.push(item.slug);
|
|
191
|
+
used += block.usedTokens;
|
|
192
|
+
}
|
|
162
193
|
if (blocks.length === 0)
|
|
163
194
|
return { text: '', usedTokens: 0, included: [], dropped };
|
|
164
195
|
const text = wrapBlocks(blocks);
|
|
165
196
|
const result = { text, usedTokens: estimateTokens(text), included, dropped };
|
|
166
197
|
if (slowIncluded.length > 0)
|
|
167
198
|
result.slowIncluded = slowIncluded;
|
|
199
|
+
if (lessonIncluded.length > 0) {
|
|
200
|
+
result.lessonIncluded = lessonIncluded;
|
|
201
|
+
result.included = [...included, ...lessonIncluded];
|
|
202
|
+
}
|
|
168
203
|
return result;
|
|
169
204
|
}
|
|
170
205
|
/**
|
|
171
206
|
* Greedily pack pointer entries (hits already score-ordered) under the total
|
|
172
|
-
* budget. Zero hits → empty text (ADR 0006: 零命中零注入).
|
|
207
|
+
* budget. Zero hits → empty text (ADR 0006: 零命中零注入). `lessons` (B2)
|
|
208
|
+
* packs after the slow pointers as flat entries with the source marker.
|
|
173
209
|
*/
|
|
174
|
-
export function assemblePointer(entries, cfg, slow) {
|
|
175
|
-
return packInjection(entries, cfg, pointerEntry, slow);
|
|
210
|
+
export function assemblePointer(entries, cfg, slow, lessons) {
|
|
211
|
+
return packInjection(entries, cfg, pointerEntry, slow, lessons);
|
|
176
212
|
}
|
|
177
213
|
/**
|
|
178
|
-
* Greedily pack per-topic digests (hits already score-ordered) under the
|
|
179
|
-
*
|
|
214
|
+
* Greedily pack per-topic digests (hits already score-ordered) under the total
|
|
215
|
+
* budget. Zero hits → empty text (ADR 0006: 零命中零注入). `lessons` (B2).
|
|
180
216
|
*/
|
|
181
|
-
export function assembleInjection(entries, cfg, slow) {
|
|
182
|
-
return packInjection(entries, cfg, topicDigest, slow);
|
|
217
|
+
export function assembleInjection(entries, cfg, slow, lessons) {
|
|
218
|
+
return packInjection(entries, cfg, topicDigest, slow, lessons);
|
|
183
219
|
}
|
|
184
220
|
/** Total package cap — pinned, does not float with hit strength (A3 决议 3). */
|
|
185
221
|
export const PACKAGE_TOTAL = 600;
|
|
@@ -302,11 +338,15 @@ function isTrivialQuery(query, queryTokens) {
|
|
|
302
338
|
const stripped = query.replace(/\s+/g, '');
|
|
303
339
|
return stripped.length > 0 && stripped.length <= PACKAGE_TRIVIAL_QUERY_CHARS;
|
|
304
340
|
}
|
|
305
|
-
/** Stable within-layer order:
|
|
341
|
+
/** Stable within-layer order: prioritySeat first (lesson 优先席), then score
|
|
342
|
+
* desc, then usage, then input order. */
|
|
306
343
|
function rankStable(list, usage) {
|
|
307
344
|
return list
|
|
308
345
|
.map((c, i) => ({ c, i, u: usage?.get(c.slug) ?? 0 }))
|
|
309
|
-
.sort((a, b) => b.c.
|
|
346
|
+
.sort((a, b) => (b.c.prioritySeat === true ? 1 : 0) - (a.c.prioritySeat === true ? 1 : 0) ||
|
|
347
|
+
b.c.score - a.c.score ||
|
|
348
|
+
b.u - a.u ||
|
|
349
|
+
a.i - b.i)
|
|
310
350
|
.map((x) => x.c);
|
|
311
351
|
}
|
|
312
352
|
/** Machine-assembled why line (A3 决议 2): hit channel + edge source + score. */
|
|
@@ -359,7 +399,7 @@ function packageWhyLine(c, queryTokens) {
|
|
|
359
399
|
* 不再重复预告)。实质行是弹性段,超预算先截它(spec §2.4)。
|
|
360
400
|
*/
|
|
361
401
|
function directEntryText(c, queryTokens, budget) {
|
|
362
|
-
const head = `### ${c.title} [${c.status}]`;
|
|
402
|
+
const head = `### ${c.title} [${c.status}]${c.sourceTag !== undefined && c.sourceTag !== '' ? ` (${c.sourceTag})` : ''}`;
|
|
363
403
|
const why = packageWhyLine(c, queryTokens);
|
|
364
404
|
const warn = c.reasons.includes('conflicted-demoted') ? '⚠ 此条存在未合并冲突,结论可能过期。' : null;
|
|
365
405
|
const openBase = `细节: topic_open "${c.slug}"`;
|
|
@@ -478,7 +518,9 @@ export function assemblePackage(input, opts) {
|
|
|
478
518
|
const layers = { direct: [], neighborhood: [], weak: [] };
|
|
479
519
|
const directBlocks = [];
|
|
480
520
|
for (const c of direct) {
|
|
481
|
-
|
|
521
|
+
// prioritySeat (lesson 优先席) is exempt from the direct-seat cap — the
|
|
522
|
+
// budget is the only thing that can still cut it.
|
|
523
|
+
if (directBlocks.length >= PACKAGE_DIRECT_MAX && !isEntityAnchored(input.queryTokens, c) && c.prioritySeat !== true) {
|
|
482
524
|
dropped.push({ slug: c.slug, reason: 'direct-cap' });
|
|
483
525
|
continue;
|
|
484
526
|
}
|
package/lib/ilog.d.ts
CHANGED
|
@@ -85,6 +85,20 @@ export interface InjectionRecord {
|
|
|
85
85
|
* Written once with the record, never backfilled (ilog append-only).
|
|
86
86
|
*/
|
|
87
87
|
slowOutcome?: SlowOutcome;
|
|
88
|
+
/**
|
|
89
|
+
* Lesson gate fields (B2, spec 卷一 §6.3/§7) — present only on rounds where
|
|
90
|
+
* lessonInject is shadow|on AND at least one established lesson matched the
|
|
91
|
+
* query. `lessons` is the trigger-channel match list with whether the entry
|
|
92
|
+
* entered the context; shadow rounds always carry included:false there (the
|
|
93
|
+
* double-compute lands in meta/lesson-shadow.jsonl instead).
|
|
94
|
+
*/
|
|
95
|
+
lessonInject?: 'shadow' | 'on';
|
|
96
|
+
lessons?: {
|
|
97
|
+
slug: string;
|
|
98
|
+
score: number;
|
|
99
|
+
source: string;
|
|
100
|
+
included: boolean;
|
|
101
|
+
}[];
|
|
88
102
|
}
|
|
89
103
|
/**
|
|
90
104
|
* One topic_open event (pointer-open log, v4 §4.3). Everything beyond
|
package/lib/index.d.ts
CHANGED
|
@@ -124,6 +124,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
124
124
|
embeddingProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
125
125
|
embeddingModel: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
126
126
|
packageMode: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
127
|
+
lessonInject: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
127
128
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
128
129
|
repo: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
129
130
|
autoInject: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
|
|
@@ -162,6 +163,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
162
163
|
embeddingProvider: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
163
164
|
embeddingModel: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
164
165
|
packageMode: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
166
|
+
lessonInject: import("@deepseek-ai/schemastery").default<string, string, "volatile-defined">;
|
|
165
167
|
}>>, "plain">;
|
|
166
168
|
/** Live config reference as a 0.1.7 host passes it (cron / mcp-adapter
|
|
167
169
|
* spelling): `.get()` snapshots the current value — the host swaps the
|
|
@@ -174,6 +176,13 @@ interface VolatileRef<T> {
|
|
|
174
176
|
export type TopicsRuntimeConfig = Partial<{
|
|
175
177
|
[K in keyof TopicsConfigValue]: VolatileRef<TopicsConfigValue[K]> | TopicsConfigValue[K];
|
|
176
178
|
}>;
|
|
179
|
+
/**
|
|
180
|
+
* B-line dual-write seam contract (卷二 §6.2) — shared types are exported by
|
|
181
|
+
* THIS repo (C1 决议 3: no shared lib until a third consumer appears). The
|
|
182
|
+
* new repo calls TopicsService.feedObservations / setLessonStatus through the
|
|
183
|
+
* host process; direct bundle writes are forbidden on its side.
|
|
184
|
+
*/
|
|
185
|
+
export type { StructuredObservation, FeedResult, StoreResult } from './service.ts';
|
|
177
186
|
/**
|
|
178
187
|
* Strip a leading YAML frontmatter block (`---` / body / `---`) from a skill
|
|
179
188
|
* markdown file. `SkillDefinition.content` must be the instruction body after
|
|
@@ -186,4 +195,3 @@ export type TopicsRuntimeConfig = Partial<{
|
|
|
186
195
|
*/
|
|
187
196
|
export declare function stripFrontmatter(raw: string): string;
|
|
188
197
|
export declare function apply(ctx: Context, config?: TopicsRuntimeConfig): Promise<void>;
|
|
189
|
-
export {};
|
package/lib/index.js
CHANGED
package/lib/okf.d.ts
CHANGED
|
@@ -13,6 +13,32 @@
|
|
|
13
13
|
* @module okf
|
|
14
14
|
*/
|
|
15
15
|
export type TopicStatus = 'draft' | 'stable' | 'deprecated';
|
|
16
|
+
/**
|
|
17
|
+
* Bundle entry kinds (B2, spec 2026-10-self-improve-topics §6.1): `topic` =
|
|
18
|
+
* factual knowledge (「是什么」), `lesson` = behavioral lesson (「怎么做事」).
|
|
19
|
+
* The kind rides the OKF `type` frontmatter field — the canonical serialized
|
|
20
|
+
* value for a lesson is `Lesson` (matching the `Topic` capitalization
|
|
21
|
+
* convention); recognition is case-insensitive so bundles written by the
|
|
22
|
+
* new repo's loop parse regardless of casing drift.
|
|
23
|
+
*/
|
|
24
|
+
export type TopicKind = 'topic' | 'lesson';
|
|
25
|
+
/** lesson scope (B2 MJ-2, spec §6.2): workspace lessons inject only in the
|
|
26
|
+
* workspace that produced them (cwd-hash match); global need no filter. */
|
|
27
|
+
export type LessonScope = 'workspace' | 'global';
|
|
28
|
+
/** lesson confidence (B2, spec §6.2): the FACT source — `status` derives from
|
|
29
|
+
* it (hint↔draft, established↔stable); hint never injects (MJ-3). */
|
|
30
|
+
export type LessonConfidence = 'hint' | 'established';
|
|
31
|
+
/** lesson provenance (B2 BL-2, spec §6.2): machine-recognizable source tag;
|
|
32
|
+
* uninstall reclamation keys on source≠manual (卷二 §9). */
|
|
33
|
+
export type LessonSource = 'distilled' | 'correction' | 'manual';
|
|
34
|
+
/** kind from the raw `type` frontmatter value (case-insensitive `lesson`). */
|
|
35
|
+
export declare function topicKindOf(type: string): TopicKind;
|
|
36
|
+
/** The canonical serialized `type` value per kind. */
|
|
37
|
+
export declare function kindTypeValue(kind: TopicKind): string;
|
|
38
|
+
/** confidence 为事实源(B2 Minor):hint↔draft、established↔stable.
|
|
39
|
+
* `deprecated` is a lifecycle endpoint OUTSIDE the confidence axis — callers
|
|
40
|
+
* set it directly (setLessonStatus / consolidation / TTL sweep). */
|
|
41
|
+
export declare function deriveLessonStatus(confidence: LessonConfidence): TopicStatus;
|
|
16
42
|
export interface SourceEntry {
|
|
17
43
|
id?: string;
|
|
18
44
|
resource: string;
|
|
@@ -46,6 +72,14 @@ export interface TopicFrontmatter {
|
|
|
46
72
|
verified?: TrustStamp;
|
|
47
73
|
stale_after?: string;
|
|
48
74
|
sources?: SourceEntry[];
|
|
75
|
+
/** lesson 必带 scope(默认 workspace;global 需显式)。 */
|
|
76
|
+
scope?: LessonScope;
|
|
77
|
+
/** confidence 事实源;status 派生(deriveLessonStatus)。默认 hint。 */
|
|
78
|
+
confidence?: LessonConfidence;
|
|
79
|
+
/** provenance 标记(卸载回收按 source≠manual)。默认 manual。 */
|
|
80
|
+
source?: LessonSource;
|
|
81
|
+
/** 写入时的 workspace 标识(cwd hash,MJ-2);scope=global 时可空。 */
|
|
82
|
+
workspaceId?: string;
|
|
49
83
|
/** Producer-defined extras — preserved on round-trip per OKF §4.1. */
|
|
50
84
|
[extra: string]: unknown;
|
|
51
85
|
}
|
package/lib/okf.js
CHANGED
|
@@ -13,6 +13,20 @@
|
|
|
13
13
|
* @module okf
|
|
14
14
|
*/
|
|
15
15
|
import { parseYaml, stringifyYaml, YamlError } from "./yaml.js";
|
|
16
|
+
/** kind from the raw `type` frontmatter value (case-insensitive `lesson`). */
|
|
17
|
+
export function topicKindOf(type) {
|
|
18
|
+
return typeof type === 'string' && type.trim().toLowerCase() === 'lesson' ? 'lesson' : 'topic';
|
|
19
|
+
}
|
|
20
|
+
/** The canonical serialized `type` value per kind. */
|
|
21
|
+
export function kindTypeValue(kind) {
|
|
22
|
+
return kind === 'lesson' ? 'Lesson' : 'Topic';
|
|
23
|
+
}
|
|
24
|
+
/** confidence 为事实源(B2 Minor):hint↔draft、established↔stable.
|
|
25
|
+
* `deprecated` is a lifecycle endpoint OUTSIDE the confidence axis — callers
|
|
26
|
+
* set it directly (setLessonStatus / consolidation / TTL sweep). */
|
|
27
|
+
export function deriveLessonStatus(confidence) {
|
|
28
|
+
return confidence === 'established' ? 'stable' : 'draft';
|
|
29
|
+
}
|
|
16
30
|
export class OkfError extends Error {
|
|
17
31
|
name = 'OkfError';
|
|
18
32
|
}
|
|
@@ -106,8 +120,26 @@ export function parseTopicDoc(raw) {
|
|
|
106
120
|
return s;
|
|
107
121
|
});
|
|
108
122
|
}
|
|
123
|
+
// ---- lesson-kind fields (B2, spec §6.2) — parsed on lesson docs only ----
|
|
124
|
+
// Invalid enum values DEGRADE TO THE DEFAULT (triggers precedent: a producer
|
|
125
|
+
// slip must never invalidate an otherwise sound doc); the seam writers own
|
|
126
|
+
// the closed vocabulary. On lesson docs these four keys join the known set
|
|
127
|
+
// so the extras loop cannot double-store a raw unvalidated value.
|
|
128
|
+
if (topicKindOf(fmRaw.type) === 'lesson') {
|
|
129
|
+
fm.scope = fmRaw.scope === 'global' ? 'global' : 'workspace';
|
|
130
|
+
fm.confidence = fmRaw.confidence === 'established' ? 'established' : 'hint';
|
|
131
|
+
fm.source = fmRaw.source === 'distilled' || fmRaw.source === 'correction' ? fmRaw.source : 'manual';
|
|
132
|
+
if (typeof fmRaw.workspaceId === 'string' && fmRaw.workspaceId !== '')
|
|
133
|
+
fm.workspaceId = fmRaw.workspaceId;
|
|
134
|
+
}
|
|
109
135
|
// Preserve unknown producer-defined keys verbatim (OKF §4.1 extensions).
|
|
110
136
|
const known = new Set(['type', 'title', 'description', 'tags', 'triggers', 'depends', 'open_questions', 'impact', 'status', 'generated', 'verified', 'stale_after', 'sources']);
|
|
137
|
+
if (fm.scope !== undefined || fm.confidence !== undefined || fm.source !== undefined) {
|
|
138
|
+
known.add('scope');
|
|
139
|
+
known.add('confidence');
|
|
140
|
+
known.add('source');
|
|
141
|
+
known.add('workspaceId');
|
|
142
|
+
}
|
|
111
143
|
for (const [k, v] of Object.entries(fmRaw)) {
|
|
112
144
|
if (!known.has(k))
|
|
113
145
|
fm[k] = v;
|
|
@@ -137,6 +169,15 @@ export function serializeTopicDoc(doc) {
|
|
|
137
169
|
out.stale_after = fm.stale_after;
|
|
138
170
|
if (fm.sources !== undefined && fm.sources.length > 0)
|
|
139
171
|
out.sources = fm.sources;
|
|
172
|
+
// lesson fields serialize in a fixed slot (deterministic order, spec §6.2).
|
|
173
|
+
if (fm.scope !== undefined)
|
|
174
|
+
out.scope = fm.scope;
|
|
175
|
+
if (fm.confidence !== undefined)
|
|
176
|
+
out.confidence = fm.confidence;
|
|
177
|
+
if (fm.source !== undefined)
|
|
178
|
+
out.source = fm.source;
|
|
179
|
+
if (fm.workspaceId !== undefined)
|
|
180
|
+
out.workspaceId = fm.workspaceId;
|
|
140
181
|
for (const [k, v] of Object.entries(fm)) {
|
|
141
182
|
if (!(k in out))
|
|
142
183
|
out[k] = v;
|
package/lib/paths.d.ts
CHANGED
|
@@ -45,4 +45,13 @@ export declare function conflictsFile(root: string): string;
|
|
|
45
45
|
export declare function hostId(): string;
|
|
46
46
|
/** The actor string stamped into `generated.by` for machine-written topics. */
|
|
47
47
|
export declare function actorFor(): string;
|
|
48
|
+
/**
|
|
49
|
+
* Current workspace identifier for lesson scope filtering (B2 MJ-2, spec
|
|
50
|
+
* 卷一 §6.2): the sha256 prefix of the process cwd, overridable via
|
|
51
|
+
* $DSH_TOPICS_WORKSPACE_ID (tests / multi-root hosts; the override also lets
|
|
52
|
+
* a host pin one identity across chdirs). 拍板(spec 未钉死来源): cwd hash at
|
|
53
|
+
* read time is the minimal viable source — the same derivation the lesson
|
|
54
|
+
* write path stamps into frontmatter.workspaceId, so no config plumbing.
|
|
55
|
+
*/
|
|
56
|
+
export declare function currentWorkspaceId(): string;
|
|
48
57
|
export {};
|
package/lib/paths.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* @module paths
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, renameSync } from 'node:fs';
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
15
16
|
import { homedir } from 'node:os';
|
|
16
17
|
import { hostname as osHostname } from 'node:os';
|
|
17
18
|
import { join } from 'node:path';
|
|
@@ -82,3 +83,17 @@ export function hostId() {
|
|
|
82
83
|
export function actorFor() {
|
|
83
84
|
return `agent:dsh-topics-memory@${hostId()}`;
|
|
84
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Current workspace identifier for lesson scope filtering (B2 MJ-2, spec
|
|
88
|
+
* 卷一 §6.2): the sha256 prefix of the process cwd, overridable via
|
|
89
|
+
* $DSH_TOPICS_WORKSPACE_ID (tests / multi-root hosts; the override also lets
|
|
90
|
+
* a host pin one identity across chdirs). 拍板(spec 未钉死来源): cwd hash at
|
|
91
|
+
* read time is the minimal viable source — the same derivation the lesson
|
|
92
|
+
* write path stamps into frontmatter.workspaceId, so no config plumbing.
|
|
93
|
+
*/
|
|
94
|
+
export function currentWorkspaceId() {
|
|
95
|
+
const env = process.env.DSH_TOPICS_WORKSPACE_ID?.trim();
|
|
96
|
+
if (env !== undefined && env !== '')
|
|
97
|
+
return env;
|
|
98
|
+
return createHash('sha256').update(process.cwd()).digest('hex').slice(0, 16);
|
|
99
|
+
}
|
package/lib/pipeline-events.d.ts
CHANGED
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
/** Closed vocabulary, spec §8.1 + W-C wave start set. `slow-lane-outcome` is
|
|
17
17
|
* the slow lane's per-run disposition row (quality.ts); the distill/link
|
|
18
|
-
* kinds are the A2/A6 producers' contract — they import this module.
|
|
19
|
-
|
|
18
|
+
* kinds are the A2/A6 producers' contract — they import this module. The
|
|
19
|
+
* two `seam-*` kinds are the B-line dual-write audit trail (卷二 §6.2:
|
|
20
|
+
* caller-identity audit — opts.actor — rides every seam call). */
|
|
21
|
+
export type PipelineEventKind = 'slow-lane-outcome' | 'distill-op-invalid' | 'distill-op-retry-exhausted' | 'link-ref-rejected' | 'seam-observations-fed' | 'seam-lesson-status';
|
|
20
22
|
/** File NAME inside the bundle's meta dir (path via pipelineEventsFile). */
|
|
21
23
|
export declare const PIPELINE_EVENTS_FILE = "pipeline-events.jsonl";
|
|
22
24
|
export interface PipelineEvent {
|
package/lib/pipeline-events.js
CHANGED
|
@@ -31,6 +31,8 @@ const KNOWN_KINDS = new Set([
|
|
|
31
31
|
'distill-op-invalid',
|
|
32
32
|
'distill-op-retry-exhausted',
|
|
33
33
|
'link-ref-rejected',
|
|
34
|
+
'seam-observations-fed',
|
|
35
|
+
'seam-lesson-status',
|
|
34
36
|
]);
|
|
35
37
|
export function pipelineEventsFile(bundleDir) {
|
|
36
38
|
return join(metaDir(bundleDir), PIPELINE_EVENTS_FILE);
|