@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/src/store.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  appendFile,
19
19
  access,
20
20
  rm,
21
+ copyFile,
21
22
  } from 'node:fs/promises'
22
23
  import { readFileSync } from 'node:fs'
23
24
  import { constants as FsConstants } from 'node:fs'
@@ -38,19 +39,34 @@ export interface TopicMeta {
38
39
  generatedAt: string
39
40
  }
40
41
 
41
- export type ObservationKind = 'decision' | 'finding' | 'constraint' | 'question' | 'turn'
42
+ /** Bundle entry kinds (B2, spec §6.5 落点表: store kind 枚举). Canonical
43
+ * model lives in okf.ts next to TopicStatus; re-exported here as the store
44
+ * surface the spec anchors on. */
45
+ export type TopicKind = okf.TopicKind
46
+ export const topicKindOf = okf.topicKindOf
47
+
48
+ export type ObservationKind = 'decision' | 'finding' | 'constraint' | 'question' | 'turn' | 'signal'
49
+
50
+ /**
51
+ * Where an observation came from. `fed` marks entries that arrived through
52
+ * the feedObservations write seam (B-line dual-write, spec 卷二 §6.2) — the
53
+ * provenance marker the distill lane and audits key on.
54
+ */
55
+ export type ObservationSource = 'model' | 'auto' | 'fed'
42
56
 
43
57
  export interface Observation {
44
58
  id: string
45
59
  at: string
46
60
  kind: ObservationKind
47
- source: 'model' | 'auto'
61
+ source: ObservationSource
48
62
  text: string
49
63
  sessionId?: string
50
64
  distilled: boolean
51
65
  distilledInto?: string[]
52
66
  /** Failed distill attempts: fed to the model but consumed by no op (GC counter). */
53
67
  attempts?: number
68
+ /** Write-seam audit: the feedObservations caller identity (opts.actor). */
69
+ by?: string
54
70
  }
55
71
 
56
72
  export interface SaveResult {
@@ -418,6 +434,30 @@ export class BundleStore {
418
434
  })
419
435
  }
420
436
 
437
+ /**
438
+ * Lesson removal seam (B2/卷二 §6.2 MJ-B, spec 红线④): archive BEFORE the
439
+ * roster removal — the file is copied byte-exact to `archive/lessons/`
440
+ * inside the bundle and only then deleted from `topics/`, both landing in
441
+ * ONE git commit so `git revert` restores the lesson exactly (自动写资产
442
+ * 必须可回滚). Returns undefined when the slug doesn't exist (idempotent).
443
+ */
444
+ async archiveLesson(slug: string, opts: { message: string }): Promise<{ archivedTo: string; committed: boolean } | undefined> {
445
+ return this.enqueue(async () => {
446
+ const clean = okf.slugify(slug)
447
+ const file = this.topicPath(clean)
448
+ if (!(await existsSync(file))) return undefined
449
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-')
450
+ const archivedTo = `archive/lessons/${clean}-${stamp}.md`
451
+ const absTo = join(this.root, archivedTo)
452
+ await mkdir(dirname(absTo), { recursive: true })
453
+ await copyFile(file, absTo)
454
+ await rm(file)
455
+ await this.regenerateIndex()
456
+ const committed = await this.commit([archivedTo, `topics/${clean}.md`, 'index.md'], opts.message)
457
+ return { archivedTo, committed }
458
+ })
459
+ }
460
+
421
461
  private async regenerateIndex(): Promise<void> { const metas = await this.listTopics()
422
462
  const entries: okf.IndexEntry[] = metas.map((m) => ({
423
463
  slug: m.slug,
@@ -483,9 +523,10 @@ export class BundleStore {
483
523
 
484
524
  async appendObservation(input: {
485
525
  kind: ObservationKind
486
- source: 'model' | 'auto'
526
+ source: ObservationSource
487
527
  text: string
488
528
  sessionId?: string
529
+ by?: string
489
530
  }): Promise<Observation> {
490
531
  const obs: Observation = {
491
532
  id: `obs-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`,
@@ -496,6 +537,7 @@ export class BundleStore {
496
537
  distilled: false,
497
538
  }
498
539
  if (input.sessionId !== undefined) obs.sessionId = input.sessionId
540
+ if (input.by !== undefined) obs.by = input.by
499
541
  // Behind the store-wide queue: markDistilled / recordUnconsumed rewrite
500
542
  // the whole JSONL file — an unsynchronized append here could interleave
501
543
  // with a rewrite and lose either the new observation or the rewrite.