@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/lib/store.d.ts
CHANGED
|
@@ -19,18 +19,31 @@ export interface TopicMeta {
|
|
|
19
19
|
depends: string[];
|
|
20
20
|
generatedAt: string;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
/** Bundle entry kinds (B2, spec §6.5 落点表: store kind 枚举). Canonical
|
|
23
|
+
* model lives in okf.ts next to TopicStatus; re-exported here as the store
|
|
24
|
+
* surface the spec anchors on. */
|
|
25
|
+
export type TopicKind = okf.TopicKind;
|
|
26
|
+
export declare const topicKindOf: typeof okf.topicKindOf;
|
|
27
|
+
export type ObservationKind = 'decision' | 'finding' | 'constraint' | 'question' | 'turn' | 'signal';
|
|
28
|
+
/**
|
|
29
|
+
* Where an observation came from. `fed` marks entries that arrived through
|
|
30
|
+
* the feedObservations write seam (B-line dual-write, spec 卷二 §6.2) — the
|
|
31
|
+
* provenance marker the distill lane and audits key on.
|
|
32
|
+
*/
|
|
33
|
+
export type ObservationSource = 'model' | 'auto' | 'fed';
|
|
23
34
|
export interface Observation {
|
|
24
35
|
id: string;
|
|
25
36
|
at: string;
|
|
26
37
|
kind: ObservationKind;
|
|
27
|
-
source:
|
|
38
|
+
source: ObservationSource;
|
|
28
39
|
text: string;
|
|
29
40
|
sessionId?: string;
|
|
30
41
|
distilled: boolean;
|
|
31
42
|
distilledInto?: string[];
|
|
32
43
|
/** Failed distill attempts: fed to the model but consumed by no op (GC counter). */
|
|
33
44
|
attempts?: number;
|
|
45
|
+
/** Write-seam audit: the feedObservations caller identity (opts.actor). */
|
|
46
|
+
by?: string;
|
|
34
47
|
}
|
|
35
48
|
export interface SaveResult {
|
|
36
49
|
slug: string;
|
|
@@ -110,10 +123,19 @@ export declare class BundleStore {
|
|
|
110
123
|
* Write a topic (atomic), regenerate the index, and commit both — one
|
|
111
124
|
* commit per conclusion change (ADR 0003). `slug` in the doc's depends is
|
|
112
125
|
* NOT normalized here; callers pass bundle-relative paths.
|
|
126
|
+
*
|
|
127
|
+
* `links` (A2, optional): whitelist-surviving semantic targets from the
|
|
128
|
+
* distill lane. Applied INSIDE this one write transaction — the topic and
|
|
129
|
+
* its edges land together (先落 topic 再建边); the Links section lands as
|
|
130
|
+
* body wikilinks and the index/backlinks regeneration below (same
|
|
131
|
+
* transaction) recomputes the edge index over them. Normalized and
|
|
132
|
+
* self/dedup-guarded here too, so any caller benefits from the same guard
|
|
133
|
+
* the distill lane already ran.
|
|
113
134
|
*/
|
|
114
135
|
saveTopic(input: {
|
|
115
136
|
slug: string;
|
|
116
137
|
doc: okf.TopicDoc;
|
|
138
|
+
links?: readonly string[];
|
|
117
139
|
}, opts: {
|
|
118
140
|
message: string;
|
|
119
141
|
actor?: string;
|
|
@@ -126,6 +148,19 @@ export declare class BundleStore {
|
|
|
126
148
|
* safe write path. Returns false when the slug doesn't exist (idempotent).
|
|
127
149
|
*/
|
|
128
150
|
deleteTopic(slug: string, message: string): Promise<boolean>;
|
|
151
|
+
/**
|
|
152
|
+
* Lesson removal seam (B2/卷二 §6.2 MJ-B, spec 红线④): archive BEFORE the
|
|
153
|
+
* roster removal — the file is copied byte-exact to `archive/lessons/`
|
|
154
|
+
* inside the bundle and only then deleted from `topics/`, both landing in
|
|
155
|
+
* ONE git commit so `git revert` restores the lesson exactly (自动写资产
|
|
156
|
+
* 必须可回滚). Returns undefined when the slug doesn't exist (idempotent).
|
|
157
|
+
*/
|
|
158
|
+
archiveLesson(slug: string, opts: {
|
|
159
|
+
message: string;
|
|
160
|
+
}): Promise<{
|
|
161
|
+
archivedTo: string;
|
|
162
|
+
committed: boolean;
|
|
163
|
+
} | undefined>;
|
|
129
164
|
private regenerateIndex;
|
|
130
165
|
/**
|
|
131
166
|
* Reverse-reference index: slug → who references it and how (`depends`
|
|
@@ -140,9 +175,10 @@ export declare class BundleStore {
|
|
|
140
175
|
}[]>>;
|
|
141
176
|
appendObservation(input: {
|
|
142
177
|
kind: ObservationKind;
|
|
143
|
-
source:
|
|
178
|
+
source: ObservationSource;
|
|
144
179
|
text: string;
|
|
145
180
|
sessionId?: string;
|
|
181
|
+
by?: string;
|
|
146
182
|
}): Promise<Observation>;
|
|
147
183
|
allObservations(limit?: number): Promise<Observation[]>;
|
|
148
184
|
undistilledObservations(limit?: number): Promise<Observation[]>;
|
package/lib/store.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module store
|
|
10
10
|
*/
|
|
11
|
-
import { readdir, readFile, writeFile, rename, mkdir, appendFile, access, rm, } from 'node:fs/promises';
|
|
11
|
+
import { readdir, readFile, writeFile, rename, mkdir, appendFile, access, rm, copyFile, } from 'node:fs/promises';
|
|
12
12
|
import { readFileSync } from 'node:fs';
|
|
13
13
|
import { constants as FsConstants } from 'node:fs';
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
@@ -16,6 +16,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
16
16
|
import * as okf from "./okf.js";
|
|
17
17
|
import * as gitmod from "./git.js";
|
|
18
18
|
import { actorFor } from "./paths.js";
|
|
19
|
+
export const topicKindOf = okf.topicKindOf;
|
|
19
20
|
export class StoreError extends Error {
|
|
20
21
|
name = 'StoreError';
|
|
21
22
|
}
|
|
@@ -42,6 +43,14 @@ function dedupePaths(paths) {
|
|
|
42
43
|
function sameStrings(a, b) {
|
|
43
44
|
return a.length === b.length && a.every((v, i) => v === b[i]);
|
|
44
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Body heading for caller-supplied semantic links (A2). Links are rendered as
|
|
48
|
+
* `[[wikilinks]]` under this section so the EXISTING link-edge machinery
|
|
49
|
+
* (okf.bodyLinkSlugs → computeBacklinks → retrieval graph walk) indexes them
|
|
50
|
+
* as `via: 'link'` edges — the edge representation gains no new type; the
|
|
51
|
+
* body landing spot is what makes them first-class links for free.
|
|
52
|
+
*/
|
|
53
|
+
const LINKS_HEADING = 'Links';
|
|
45
54
|
export class BundleStore {
|
|
46
55
|
root;
|
|
47
56
|
actor;
|
|
@@ -285,6 +294,14 @@ export class BundleStore {
|
|
|
285
294
|
* Write a topic (atomic), regenerate the index, and commit both — one
|
|
286
295
|
* commit per conclusion change (ADR 0003). `slug` in the doc's depends is
|
|
287
296
|
* NOT normalized here; callers pass bundle-relative paths.
|
|
297
|
+
*
|
|
298
|
+
* `links` (A2, optional): whitelist-surviving semantic targets from the
|
|
299
|
+
* distill lane. Applied INSIDE this one write transaction — the topic and
|
|
300
|
+
* its edges land together (先落 topic 再建边); the Links section lands as
|
|
301
|
+
* body wikilinks and the index/backlinks regeneration below (same
|
|
302
|
+
* transaction) recomputes the edge index over them. Normalized and
|
|
303
|
+
* self/dedup-guarded here too, so any caller benefits from the same guard
|
|
304
|
+
* the distill lane already ran.
|
|
288
305
|
*/
|
|
289
306
|
async saveTopic(input, opts) {
|
|
290
307
|
return this.enqueue(async () => {
|
|
@@ -292,9 +309,24 @@ export class BundleStore {
|
|
|
292
309
|
const file = this.topicPath(slug);
|
|
293
310
|
const created = opts.created ?? !(await existsSync(file));
|
|
294
311
|
const now = new Date().toISOString();
|
|
312
|
+
let body = input.doc.body;
|
|
313
|
+
if (input.links !== undefined && input.links.length > 0) {
|
|
314
|
+
const targets = [];
|
|
315
|
+
for (const raw of input.links) {
|
|
316
|
+
if (typeof raw !== 'string')
|
|
317
|
+
continue;
|
|
318
|
+
const target = okf.normalizeLinkTarget(raw);
|
|
319
|
+
if (target === undefined || target === slug || targets.includes(target))
|
|
320
|
+
continue;
|
|
321
|
+
targets.push(target);
|
|
322
|
+
}
|
|
323
|
+
if (targets.length > 0) {
|
|
324
|
+
body = okf.setSection(body, LINKS_HEADING, targets.map((t) => `- [[${t}]]`).join('\n'));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
295
327
|
const doc = {
|
|
296
328
|
fm: { ...input.doc.fm, generated: { by: opts.actor ?? this.actor, at: now } },
|
|
297
|
-
body
|
|
329
|
+
body,
|
|
298
330
|
};
|
|
299
331
|
const raw = okf.serializeTopicDoc(doc);
|
|
300
332
|
await atomicWrite(file, raw);
|
|
@@ -321,6 +353,30 @@ export class BundleStore {
|
|
|
321
353
|
return true;
|
|
322
354
|
});
|
|
323
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Lesson removal seam (B2/卷二 §6.2 MJ-B, spec 红线④): archive BEFORE the
|
|
358
|
+
* roster removal — the file is copied byte-exact to `archive/lessons/`
|
|
359
|
+
* inside the bundle and only then deleted from `topics/`, both landing in
|
|
360
|
+
* ONE git commit so `git revert` restores the lesson exactly (自动写资产
|
|
361
|
+
* 必须可回滚). Returns undefined when the slug doesn't exist (idempotent).
|
|
362
|
+
*/
|
|
363
|
+
async archiveLesson(slug, opts) {
|
|
364
|
+
return this.enqueue(async () => {
|
|
365
|
+
const clean = okf.slugify(slug);
|
|
366
|
+
const file = this.topicPath(clean);
|
|
367
|
+
if (!(await existsSync(file)))
|
|
368
|
+
return undefined;
|
|
369
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
370
|
+
const archivedTo = `archive/lessons/${clean}-${stamp}.md`;
|
|
371
|
+
const absTo = join(this.root, archivedTo);
|
|
372
|
+
await mkdir(dirname(absTo), { recursive: true });
|
|
373
|
+
await copyFile(file, absTo);
|
|
374
|
+
await rm(file);
|
|
375
|
+
await this.regenerateIndex();
|
|
376
|
+
const committed = await this.commit([archivedTo, `topics/${clean}.md`, 'index.md'], opts.message);
|
|
377
|
+
return { archivedTo, committed };
|
|
378
|
+
});
|
|
379
|
+
}
|
|
324
380
|
async regenerateIndex() {
|
|
325
381
|
const metas = await this.listTopics();
|
|
326
382
|
const entries = metas.map((m) => ({
|
|
@@ -400,6 +456,8 @@ export class BundleStore {
|
|
|
400
456
|
};
|
|
401
457
|
if (input.sessionId !== undefined)
|
|
402
458
|
obs.sessionId = input.sessionId;
|
|
459
|
+
if (input.by !== undefined)
|
|
460
|
+
obs.by = input.by;
|
|
403
461
|
// Behind the store-wide queue: markDistilled / recordUnconsumed rewrite
|
|
404
462
|
// the whole JSONL file — an unsynchronized append here could interleave
|
|
405
463
|
// with a rewrite and lose either the new observation or the rewrite.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwayds/dsh-topics-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "dsh-plugin: quality-first topic memory for dsh — a topic keeps the starting question, conclusion, impact and dependencies, not the process; local-first git-traced OKF bundle, budgeted LLM-free injection",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"build": "tsc -p tsconfig.json",
|
|
43
43
|
"check": "tsc --noEmit -p tsconfig.json",
|
|
44
44
|
"pretest": "tsc -p tsconfig.json",
|
|
45
|
-
"test": "node --test test/yaml.test.mjs test/okf.test.mjs test/retrieval.test.mjs test/digest.test.mjs test/ilog.test.mjs test/store.test.mjs test/paths.test.mjs test/git.test.mjs test/sync.test.mjs test/tools.test.mjs test/dedup.test.mjs test/commands.test.mjs test/onboard.test.mjs test/delegation.test.mjs test/observer.test.mjs test/distill.test.mjs test/consolidate.test.mjs test/viz.test.mjs test/corpus.test.mjs test/llm-scope.test.mjs test/quality.test.mjs test/replay-gate.test.mjs test/depends-repair.test.mjs test/skill.test.mjs test/fast-lane.test.mjs test/legacy-import.test.mjs test/jev-config.test.mjs test/jev-gate.test.mjs test/jev-client.test.mjs test/jev-log.test.mjs test/jev-dual.test.mjs test/jev-fastgate.test.mjs test/zero-open-decay.test.mjs",
|
|
45
|
+
"test": "node --test test/yaml.test.mjs test/okf.test.mjs test/retrieval.test.mjs test/digest.test.mjs test/digest-package.test.mjs test/ilog.test.mjs test/store.test.mjs test/paths.test.mjs test/git.test.mjs test/sync.test.mjs test/tools.test.mjs test/dedup.test.mjs test/commands.test.mjs test/onboard.test.mjs test/delegation.test.mjs test/observer.test.mjs test/distill.test.mjs test/consolidate.test.mjs test/viz.test.mjs test/corpus.test.mjs test/llm-scope.test.mjs test/quality.test.mjs test/replay-gate.test.mjs test/depends-repair.test.mjs test/skill.test.mjs test/fast-lane.test.mjs test/legacy-import.test.mjs test/jev-config.test.mjs test/jev-gate.test.mjs test/jev-client.test.mjs test/jev-log.test.mjs test/jev-dual.test.mjs test/jev-fastgate.test.mjs test/zero-open-decay.test.mjs test/embedding.test.mjs test/embedding-index.test.mjs test/query-sanitize.test.mjs test/semantic-rerank.test.mjs test/inject-config.test.mjs test/distill-links.test.mjs test/store-links.test.mjs test/lesson-seams.test.mjs",
|
|
46
46
|
"smoke": "node scripts/smoke-boot.mjs"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {},
|
package/src/config.ts
CHANGED
|
@@ -105,6 +105,28 @@ export const TopicsConfig = z.object({
|
|
|
105
105
|
/** laya-serve systemone endpoint for the pace-maker (default: the local
|
|
106
106
|
* laya-serve bind from the laya repo's serve mode). */
|
|
107
107
|
jevLayaUrl: z.string().default('http://127.0.0.1:8000/v1/systemone').volatile(),
|
|
108
|
+
/** Local embedding provider (A4 semantic reranker, spec §3.1): ''
|
|
109
|
+
* sentinel = the whole semantic layer is OFF (pure lexical); 'ollama' =
|
|
110
|
+
* the local ollama embed API (localhost is the ADR 0017 local exception).
|
|
111
|
+
* v1 ships no remote provider — a remote embedding may only ever enter a
|
|
112
|
+
* slow-lane/offline path, never the fast lane (red line ①). */
|
|
113
|
+
embeddingProvider: z.string().default('').volatile(),
|
|
114
|
+
/** Embedding model; '' = provider default (ollama → nomic-embed-text),
|
|
115
|
+
* same sentinel pattern as distillModel. */
|
|
116
|
+
embeddingModel: z.string().default('').volatile(),
|
|
117
|
+
/** Injection-package master gate (A-line, spec §2/§3): off | shadow | on.
|
|
118
|
+
* Carries the WHOLE new-behavior surface — query sanitization, the
|
|
119
|
+
* three-layer package, nearFloor semantic rerank, degradation. off = zero
|
|
120
|
+
* behavior change anywhere (byte-for-byte legacy); shadow = compute +
|
|
121
|
+
* bypass-ledger only, nothing lands in context; on = engage. */
|
|
122
|
+
packageMode: z.string().default('off').volatile(),
|
|
123
|
+
/** Lesson injection gate (B2, spec §7 C1 MJ-4): off | shadow | on, default
|
|
124
|
+
* off. off = established lessons neither inject nor shadow (zero injection-
|
|
125
|
+
* side behavior); shadow = the lesson priority seat double-computes into
|
|
126
|
+
* the bypass ledger (meta/lesson-shadow.jsonl), never lands in context;
|
|
127
|
+
* on = established lessons inject. Only the injection side lives here —
|
|
128
|
+
* the lesson loop (distill/approval) is the new repo's improveLoop gate. */
|
|
129
|
+
lessonInject: z.string().default('off').volatile(),
|
|
108
130
|
})
|
|
109
131
|
|
|
110
132
|
export type TopicsConfigValue = {
|
|
@@ -149,6 +171,16 @@ export type TopicsConfigValue = {
|
|
|
149
171
|
jevSecretFile?: string
|
|
150
172
|
jevLayaFallback?: boolean
|
|
151
173
|
jevLayaUrl?: string
|
|
174
|
+
/** Injection-quality line keys (spec §7). Optional in this TS view only
|
|
175
|
+
* (same sentinel pattern as the jev keys): index.ts's bare-harness
|
|
176
|
+
* DEFAULTS literal predates them — consumers must treat undefined as the
|
|
177
|
+
* documented default ('' / '' / 'off' = every new behavior zeroed). */
|
|
178
|
+
embeddingProvider?: string
|
|
179
|
+
embeddingModel?: string
|
|
180
|
+
packageMode?: 'off' | 'shadow' | 'on'
|
|
181
|
+
/** Lesson injection gate (B2). Optional in this TS view only (sentinel
|
|
182
|
+
* pattern as above) — consumers treat undefined as 'off'. */
|
|
183
|
+
lessonInject?: 'off' | 'shadow' | 'on'
|
|
152
184
|
}
|
|
153
185
|
|
|
154
186
|
export const CONFIG_KEYS = [
|
|
@@ -186,6 +218,10 @@ export const CONFIG_KEYS = [
|
|
|
186
218
|
'jevSecretFile',
|
|
187
219
|
'jevLayaFallback',
|
|
188
220
|
'jevLayaUrl',
|
|
221
|
+
'embeddingProvider',
|
|
222
|
+
'embeddingModel',
|
|
223
|
+
'packageMode',
|
|
224
|
+
'lessonInject',
|
|
189
225
|
] as const
|
|
190
226
|
|
|
191
227
|
export type ConfigKey = (typeof CONFIG_KEYS)[number]
|
|
@@ -257,10 +293,23 @@ export function parseConfigValue(key: ConfigKey, raw: string): boolean | number
|
|
|
257
293
|
}
|
|
258
294
|
case 'distillProvider':
|
|
259
295
|
case 'distillModel':
|
|
296
|
+
case 'embeddingModel':
|
|
260
297
|
case 'jevModel':
|
|
261
298
|
case 'jevSecretFile':
|
|
262
299
|
case 'jevLayaUrl':
|
|
263
300
|
return raw
|
|
301
|
+
case 'embeddingProvider': {
|
|
302
|
+
if (raw === '' || raw === 'ollama') return raw
|
|
303
|
+
return { error: 'embedding-provider 仅支持 ollama(留空=语义层关;远程 provider 不进快道)' }
|
|
304
|
+
}
|
|
305
|
+
case 'packageMode': {
|
|
306
|
+
if (raw === 'off' || raw === 'shadow' || raw === 'on') return raw
|
|
307
|
+
return { error: 'package-mode 取值 off|shadow|on' }
|
|
308
|
+
}
|
|
309
|
+
case 'lessonInject': {
|
|
310
|
+
if (raw === 'off' || raw === 'shadow' || raw === 'on') return raw
|
|
311
|
+
return { error: 'lesson-inject 取值 off|shadow|on' }
|
|
312
|
+
}
|
|
264
313
|
case 'jevLayaFallback': {
|
|
265
314
|
if (raw === 'on' || raw === 'true') return true
|
|
266
315
|
if (raw === 'off' || raw === 'false') return false
|