@aiwayds/dsh-topics-memory 0.22.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 +55 -1
- package/lib/config.js +42 -0
- package/lib/digest.d.ts +111 -5
- package/lib/digest.js +437 -11
- package/lib/distill.d.ts +39 -0
- package/lib/distill.js +250 -15
- package/lib/embedding.d.ts +113 -0
- package/lib/embedding.js +208 -0
- package/lib/ilog.d.ts +30 -0
- package/lib/index.d.ts +15 -1
- package/lib/index.js +24 -6
- 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 +38 -0
- package/lib/pipeline-events.js +123 -0
- package/lib/quality.d.ts +47 -7
- package/lib/quality.js +124 -20
- package/lib/retrieval.d.ts +106 -0
- package/lib/retrieval.js +280 -0
- package/lib/service.d.ts +103 -2
- package/lib/service.js +623 -16
- package/lib/store.d.ts +39 -3
- package/lib/store.js +60 -2
- package/package.json +2 -2
- package/src/config.ts +49 -0
- package/src/digest.ts +497 -8
- package/src/distill.ts +275 -14
- package/src/embedding.ts +294 -0
- package/src/ilog.ts +26 -0
- package/src/index.ts +32 -5
- package/src/okf.ts +68 -0
- package/src/paths.ts +15 -0
- package/src/pipeline-events.ts +136 -0
- package/src/quality.ts +145 -21
- package/src/retrieval.ts +335 -0
- package/src/service.ts +661 -19
- package/src/store.ts +77 -5
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
|
-
|
|
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:
|
|
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 {
|
|
@@ -93,6 +109,15 @@ function sameStrings(a: readonly string[], b: readonly string[]): boolean {
|
|
|
93
109
|
return a.length === b.length && a.every((v, i) => v === b[i])
|
|
94
110
|
}
|
|
95
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Body heading for caller-supplied semantic links (A2). Links are rendered as
|
|
114
|
+
* `[[wikilinks]]` under this section so the EXISTING link-edge machinery
|
|
115
|
+
* (okf.bodyLinkSlugs → computeBacklinks → retrieval graph walk) indexes them
|
|
116
|
+
* as `via: 'link'` edges — the edge representation gains no new type; the
|
|
117
|
+
* body landing spot is what makes them first-class links for free.
|
|
118
|
+
*/
|
|
119
|
+
const LINKS_HEADING = 'Links'
|
|
120
|
+
|
|
96
121
|
export interface BundleStoreOptions {
|
|
97
122
|
/** Override the stamped actor (tests). Defaults to `agent:dsh-topics-memory@<host>`. */
|
|
98
123
|
actor?: string
|
|
@@ -348,9 +373,17 @@ export class BundleStore {
|
|
|
348
373
|
* Write a topic (atomic), regenerate the index, and commit both — one
|
|
349
374
|
* commit per conclusion change (ADR 0003). `slug` in the doc's depends is
|
|
350
375
|
* NOT normalized here; callers pass bundle-relative paths.
|
|
376
|
+
*
|
|
377
|
+
* `links` (A2, optional): whitelist-surviving semantic targets from the
|
|
378
|
+
* distill lane. Applied INSIDE this one write transaction — the topic and
|
|
379
|
+
* its edges land together (先落 topic 再建边); the Links section lands as
|
|
380
|
+
* body wikilinks and the index/backlinks regeneration below (same
|
|
381
|
+
* transaction) recomputes the edge index over them. Normalized and
|
|
382
|
+
* self/dedup-guarded here too, so any caller benefits from the same guard
|
|
383
|
+
* the distill lane already ran.
|
|
351
384
|
*/
|
|
352
385
|
async saveTopic(
|
|
353
|
-
input: { slug: string; doc: okf.TopicDoc },
|
|
386
|
+
input: { slug: string; doc: okf.TopicDoc; links?: readonly string[] },
|
|
354
387
|
opts: { message: string; actor?: string; created?: boolean },
|
|
355
388
|
): Promise<SaveResult> {
|
|
356
389
|
return this.enqueue(async () => {
|
|
@@ -358,9 +391,22 @@ export class BundleStore {
|
|
|
358
391
|
const file = this.topicPath(slug)
|
|
359
392
|
const created = opts.created ?? !(await existsSync(file))
|
|
360
393
|
const now = new Date().toISOString()
|
|
394
|
+
let body = input.doc.body
|
|
395
|
+
if (input.links !== undefined && input.links.length > 0) {
|
|
396
|
+
const targets: string[] = []
|
|
397
|
+
for (const raw of input.links) {
|
|
398
|
+
if (typeof raw !== 'string') continue
|
|
399
|
+
const target = okf.normalizeLinkTarget(raw)
|
|
400
|
+
if (target === undefined || target === slug || targets.includes(target)) continue
|
|
401
|
+
targets.push(target)
|
|
402
|
+
}
|
|
403
|
+
if (targets.length > 0) {
|
|
404
|
+
body = okf.setSection(body, LINKS_HEADING, targets.map((t) => `- [[${t}]]`).join('\n'))
|
|
405
|
+
}
|
|
406
|
+
}
|
|
361
407
|
const doc: okf.TopicDoc = {
|
|
362
408
|
fm: { ...input.doc.fm, generated: { by: opts.actor ?? this.actor, at: now } },
|
|
363
|
-
body
|
|
409
|
+
body,
|
|
364
410
|
}
|
|
365
411
|
const raw = okf.serializeTopicDoc(doc)
|
|
366
412
|
await atomicWrite(file, raw)
|
|
@@ -388,6 +434,30 @@ export class BundleStore {
|
|
|
388
434
|
})
|
|
389
435
|
}
|
|
390
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
|
+
|
|
391
461
|
private async regenerateIndex(): Promise<void> { const metas = await this.listTopics()
|
|
392
462
|
const entries: okf.IndexEntry[] = metas.map((m) => ({
|
|
393
463
|
slug: m.slug,
|
|
@@ -453,9 +523,10 @@ export class BundleStore {
|
|
|
453
523
|
|
|
454
524
|
async appendObservation(input: {
|
|
455
525
|
kind: ObservationKind
|
|
456
|
-
source:
|
|
526
|
+
source: ObservationSource
|
|
457
527
|
text: string
|
|
458
528
|
sessionId?: string
|
|
529
|
+
by?: string
|
|
459
530
|
}): Promise<Observation> {
|
|
460
531
|
const obs: Observation = {
|
|
461
532
|
id: `obs-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`,
|
|
@@ -466,6 +537,7 @@ export class BundleStore {
|
|
|
466
537
|
distilled: false,
|
|
467
538
|
}
|
|
468
539
|
if (input.sessionId !== undefined) obs.sessionId = input.sessionId
|
|
540
|
+
if (input.by !== undefined) obs.by = input.by
|
|
469
541
|
// Behind the store-wide queue: markDistilled / recordUnconsumed rewrite
|
|
470
542
|
// the whole JSONL file — an unsynchronized append here could interleave
|
|
471
543
|
// with a rewrite and lose either the new observation or the rewrite.
|