@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 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
- * total budget. Zero hits → empty text (ADR 0006: 零命中零注入).
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, all under one total budget (v4 B2: shared 600 — the
120
- * slow lane spends whatever the fast lane leaves; 分账终值 is a P3 question).
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
- * total budget. Zero hits → empty text (ADR 0006: 零命中零注入).
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: score desc, then usage, then input 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.score - a.c.score || b.u - a.u || a.i - b.i)
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
- if (directBlocks.length >= PACKAGE_DIRECT_MAX && !isEntityAnchored(input.queryTokens, c)) {
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
@@ -1035,6 +1035,7 @@ const DEFAULTS = {
1035
1035
  embeddingProvider: '',
1036
1036
  embeddingModel: '',
1037
1037
  packageMode: 'off',
1038
+ lessonInject: 'off',
1038
1039
  usageBoost: 0.15,
1039
1040
  pushDebounceSeconds: 45,
1040
1041
  };
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
+ }
@@ -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
- export type PipelineEventKind = 'slow-lane-outcome' | 'distill-op-invalid' | 'distill-op-retry-exhausted' | 'link-ref-rejected';
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 {
@@ -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);