@aiwayds/dsh-topics-memory 0.6.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/README.md +112 -0
- package/README.zh.md +112 -0
- package/cordis.patch.yml +8 -0
- package/lib/commands.d.ts +21 -0
- package/lib/commands.js +368 -0
- package/lib/config.d.ts +122 -0
- package/lib/config.js +123 -0
- package/lib/delegation.d.ts +23 -0
- package/lib/delegation.js +21 -0
- package/lib/digest.d.ts +42 -0
- package/lib/digest.js +101 -0
- package/lib/distill.d.ts +186 -0
- package/lib/distill.js +759 -0
- package/lib/git.d.ts +66 -0
- package/lib/git.js +169 -0
- package/lib/ilog.d.ts +53 -0
- package/lib/ilog.js +70 -0
- package/lib/index.d.ts +73 -0
- package/lib/index.js +504 -0
- package/lib/observer.d.ts +41 -0
- package/lib/observer.js +128 -0
- package/lib/okf.d.ts +112 -0
- package/lib/okf.js +279 -0
- package/lib/onboard.d.ts +205 -0
- package/lib/onboard.js +664 -0
- package/lib/paths.d.ts +48 -0
- package/lib/paths.js +84 -0
- package/lib/retrieval.d.ts +61 -0
- package/lib/retrieval.js +178 -0
- package/lib/service.d.ts +96 -0
- package/lib/service.js +375 -0
- package/lib/store.d.ts +153 -0
- package/lib/store.js +489 -0
- package/lib/sync.d.ts +52 -0
- package/lib/sync.js +154 -0
- package/lib/tools.d.ts +8 -0
- package/lib/tools.js +208 -0
- package/lib/viz.d.ts +33 -0
- package/lib/viz.js +310 -0
- package/lib/yaml.d.ts +18 -0
- package/lib/yaml.js +334 -0
- package/package.json +97 -0
- package/src/commands.ts +397 -0
- package/src/config.ts +149 -0
- package/src/delegation.ts +27 -0
- package/src/digest.ts +125 -0
- package/src/distill.ts +885 -0
- package/src/git.ts +209 -0
- package/src/ilog.ts +99 -0
- package/src/index.ts +574 -0
- package/src/observer.ts +150 -0
- package/src/okf.ts +317 -0
- package/src/onboard.ts +762 -0
- package/src/paths.ts +104 -0
- package/src/retrieval.ts +219 -0
- package/src/service.ts +400 -0
- package/src/store.ts +567 -0
- package/src/sync.ts +170 -0
- package/src/tools.ts +225 -0
- package/src/viz.ts +333 -0
- package/src/yaml.ts +322 -0
package/lib/git.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git CLI wrapper for the bundle Cache (ADR 0002/0003).
|
|
3
|
+
*
|
|
4
|
+
* Every command is an argv-array child process (no shell), with a timeout and
|
|
5
|
+
* a plugin identity for commits (`user.name`/`user.email` via -c, never
|
|
6
|
+
* touching the user's git config). In GitHub mode the token is attached
|
|
7
|
+
* per-invocation as an http.extraHeader — it never lands in the remote URL,
|
|
8
|
+
* .git/config, or any file (ADR 0002).
|
|
9
|
+
*
|
|
10
|
+
* @module git
|
|
11
|
+
*/
|
|
12
|
+
export interface GitResult {
|
|
13
|
+
code: number;
|
|
14
|
+
stdout: string;
|
|
15
|
+
stderr: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class GitError extends Error {
|
|
18
|
+
name: string;
|
|
19
|
+
readonly result: GitResult;
|
|
20
|
+
constructor(message: string, result: GitResult);
|
|
21
|
+
}
|
|
22
|
+
export interface GitRunOptions {
|
|
23
|
+
cwd: string;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
/** Token for http extra header (push/pull in GitHub mode). */
|
|
26
|
+
token?: string;
|
|
27
|
+
/** Throw GitError on non-zero exit (default true). */
|
|
28
|
+
mayFail?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function runGit(args: readonly string[], opts: GitRunOptions): Promise<GitResult>;
|
|
31
|
+
export declare function isRepo(cwd: string): Promise<boolean>;
|
|
32
|
+
export declare function initRepo(cwd: string, branch?: string): Promise<void>;
|
|
33
|
+
export declare function addAndCommit(cwd: string, paths: readonly string[], message: string): Promise<boolean>;
|
|
34
|
+
export declare function headRev(cwd: string): Promise<string | undefined>;
|
|
35
|
+
export interface HistoryEntry {
|
|
36
|
+
hash: string;
|
|
37
|
+
date: string;
|
|
38
|
+
message: string;
|
|
39
|
+
}
|
|
40
|
+
/** Per-file history — the toolized exit of 「git 可追溯」(need 3). */
|
|
41
|
+
export declare function fileHistory(cwd: string, path: string, limit?: number): Promise<HistoryEntry[]>;
|
|
42
|
+
/** The full content of a topic file at a given revision (conclusion diffing). */
|
|
43
|
+
export declare function fileAtRev(cwd: string, path: string, rev: string): Promise<string | undefined>;
|
|
44
|
+
export interface PullOutcome {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
/** Paths left in conflicted state (empty when ok). */
|
|
47
|
+
conflicted: string[];
|
|
48
|
+
output: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* `pull --rebase` with conflict containment: on conflict we abort the rebase
|
|
52
|
+
* (ADR 0003 — no automatic smart merging) and report the conflicted paths so
|
|
53
|
+
* the caller can mark those topics. Remote and branch are explicit — the
|
|
54
|
+
* bundle never depends on upstream tracking config.
|
|
55
|
+
*/
|
|
56
|
+
export declare function pullRebase(cwd: string, token?: string, remote?: string, branch?: string): Promise<PullOutcome>;
|
|
57
|
+
export declare function push(cwd: string, token?: string, remote?: string, branch?: string): Promise<{
|
|
58
|
+
ok: boolean;
|
|
59
|
+
output: string;
|
|
60
|
+
}>;
|
|
61
|
+
export declare function setRemote(cwd: string, url: string, remote?: string): Promise<void>;
|
|
62
|
+
export declare function removeRemote(cwd: string, remote?: string): Promise<void>;
|
|
63
|
+
export declare function hasRemote(cwd: string, remote?: string): Promise<boolean>;
|
|
64
|
+
export declare function unpushedCount(cwd: string, remote?: string, branch?: string): Promise<number>;
|
|
65
|
+
/** True when the remote advertises the branch — empty remotes skip the pull step. */
|
|
66
|
+
export declare function remoteBranchExists(cwd: string, token?: string, remote?: string, branch?: string): Promise<boolean>;
|
package/lib/git.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git CLI wrapper for the bundle Cache (ADR 0002/0003).
|
|
3
|
+
*
|
|
4
|
+
* Every command is an argv-array child process (no shell), with a timeout and
|
|
5
|
+
* a plugin identity for commits (`user.name`/`user.email` via -c, never
|
|
6
|
+
* touching the user's git config). In GitHub mode the token is attached
|
|
7
|
+
* per-invocation as an http.extraHeader — it never lands in the remote URL,
|
|
8
|
+
* .git/config, or any file (ADR 0002).
|
|
9
|
+
*
|
|
10
|
+
* @module git
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
export class GitError extends Error {
|
|
14
|
+
name = 'GitError';
|
|
15
|
+
result;
|
|
16
|
+
constructor(message, result) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.result = result;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
22
|
+
export function runGit(args, opts) {
|
|
23
|
+
const extra = [];
|
|
24
|
+
if (opts.token !== undefined && opts.token !== '') {
|
|
25
|
+
const basic = Buffer.from(`x-access-token:${opts.token}`, 'utf8').toString('base64');
|
|
26
|
+
extra.push('-c', `http.extraHeader=Authorization: Basic ${basic}`);
|
|
27
|
+
}
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const child = spawn('git', [...extra, ...args], {
|
|
30
|
+
cwd: opts.cwd,
|
|
31
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
|
|
32
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
33
|
+
});
|
|
34
|
+
let stdout = '';
|
|
35
|
+
let stderr = '';
|
|
36
|
+
let settled = false;
|
|
37
|
+
const timer = setTimeout(() => {
|
|
38
|
+
if (settled)
|
|
39
|
+
return;
|
|
40
|
+
settled = true;
|
|
41
|
+
child.kill('SIGKILL');
|
|
42
|
+
reject(new GitError(`git ${args[0]} timed out after ${opts.timeoutMs ?? DEFAULT_TIMEOUT_MS}ms`, { code: -1, stdout, stderr }));
|
|
43
|
+
}, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
44
|
+
child.stdout.on('data', (d) => {
|
|
45
|
+
stdout += d.toString('utf8');
|
|
46
|
+
});
|
|
47
|
+
child.stderr.on('data', (d) => {
|
|
48
|
+
stderr += d.toString('utf8');
|
|
49
|
+
});
|
|
50
|
+
child.on('error', (err) => {
|
|
51
|
+
if (settled)
|
|
52
|
+
return;
|
|
53
|
+
settled = true;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
reject(err);
|
|
56
|
+
});
|
|
57
|
+
child.on('close', (code) => {
|
|
58
|
+
if (settled)
|
|
59
|
+
return;
|
|
60
|
+
settled = true;
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
const result = { code: code ?? -1, stdout, stderr };
|
|
63
|
+
if ((code ?? -1) !== 0 && opts.mayFail !== true) {
|
|
64
|
+
reject(new GitError(`git ${args[0]} failed (${code}): ${stderr.trim().split('\n').at(-1) ?? ''}`, result));
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
resolve(result);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
/** Commit identity so bundle history never depends on the machine's git config. */
|
|
73
|
+
const IDENTITY = ['-c', 'user.name=dsh-topics-memory', '-c', 'user.email=dsh-topics-memory@localhost'];
|
|
74
|
+
export async function isRepo(cwd) {
|
|
75
|
+
const r = await runGit(['rev-parse', '--is-inside-work-tree'], { cwd, mayFail: true }).catch(() => undefined);
|
|
76
|
+
return r !== undefined && r.code === 0 && r.stdout.trim() === 'true';
|
|
77
|
+
}
|
|
78
|
+
export async function initRepo(cwd, branch = 'main') {
|
|
79
|
+
await runGit(['init', '-b', branch], { cwd });
|
|
80
|
+
}
|
|
81
|
+
export async function addAndCommit(cwd, paths, message) {
|
|
82
|
+
await runGit(['add', '--', ...paths], { cwd });
|
|
83
|
+
// Commit only when something is staged (write-through must not spam empty commits).
|
|
84
|
+
const staged = await runGit(['diff', '--cached', '--name-only'], { cwd });
|
|
85
|
+
if (staged.stdout.trim() === '')
|
|
86
|
+
return false;
|
|
87
|
+
const r = await runGit([...IDENTITY, 'commit', '-m', message], { cwd });
|
|
88
|
+
return r.code === 0;
|
|
89
|
+
}
|
|
90
|
+
export async function headRev(cwd) {
|
|
91
|
+
const r = await runGit(['rev-parse', 'HEAD'], { cwd, mayFail: true }).catch(() => undefined);
|
|
92
|
+
if (r === undefined || r.code !== 0)
|
|
93
|
+
return undefined;
|
|
94
|
+
return r.stdout.trim();
|
|
95
|
+
}
|
|
96
|
+
/** Per-file history — the toolized exit of 「git 可追溯」(need 3). */
|
|
97
|
+
export async function fileHistory(cwd, path, limit = 50) {
|
|
98
|
+
const r = await runGit(['log', '--follow', `--max-count=${limit}`, '--date=iso-strict', '--pretty=%H%x09%ad%x09%s', '--', path], { cwd, mayFail: true }).catch(() => undefined);
|
|
99
|
+
if (r === undefined || r.code !== 0)
|
|
100
|
+
return [];
|
|
101
|
+
return r.stdout
|
|
102
|
+
.split('\n')
|
|
103
|
+
.filter((l) => l.trim() !== '')
|
|
104
|
+
.map((l) => {
|
|
105
|
+
const [hash, date, ...rest] = l.split('\t');
|
|
106
|
+
return { hash: hash ?? '', date: date ?? '', message: rest.join('\t') };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** The full content of a topic file at a given revision (conclusion diffing). */
|
|
110
|
+
export async function fileAtRev(cwd, path, rev) {
|
|
111
|
+
const r = await runGit(['show', `${rev}:${path}`], { cwd, mayFail: true }).catch(() => undefined);
|
|
112
|
+
if (r === undefined || r.code !== 0)
|
|
113
|
+
return undefined;
|
|
114
|
+
return r.stdout;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* `pull --rebase` with conflict containment: on conflict we abort the rebase
|
|
118
|
+
* (ADR 0003 — no automatic smart merging) and report the conflicted paths so
|
|
119
|
+
* the caller can mark those topics. Remote and branch are explicit — the
|
|
120
|
+
* bundle never depends on upstream tracking config.
|
|
121
|
+
*/
|
|
122
|
+
export async function pullRebase(cwd, token, remote = 'origin', branch = 'main') {
|
|
123
|
+
const r = await runGit(['pull', '--rebase', '--autostash', remote, branch], { cwd, token, mayFail: true, timeoutMs: 60_000 }).catch((e) => {
|
|
124
|
+
return { code: -1, stdout: '', stderr: String(e instanceof Error ? e.message : e) };
|
|
125
|
+
});
|
|
126
|
+
if (r.code === 0)
|
|
127
|
+
return { ok: true, conflicted: [], output: r.stdout + r.stderr };
|
|
128
|
+
const conflicted = (await runGit(['diff', '--name-only', '--diff-filter=U'], { cwd, mayFail: true }).catch(() => ({ code: -1, stdout: '', stderr: '' }))).stdout
|
|
129
|
+
.split('\n')
|
|
130
|
+
.map((l) => l.trim())
|
|
131
|
+
.filter((l) => l !== '');
|
|
132
|
+
if (conflicted.length > 0) {
|
|
133
|
+
await runGit(['rebase', '--abort'], { cwd, mayFail: true }).catch(() => undefined);
|
|
134
|
+
}
|
|
135
|
+
return { ok: false, conflicted, output: r.stdout + r.stderr };
|
|
136
|
+
}
|
|
137
|
+
export async function push(cwd, token, remote = 'origin', branch = 'main') {
|
|
138
|
+
const r = await runGit(['push', remote, branch], { cwd, token, mayFail: true, timeoutMs: 60_000 }).catch((e) => {
|
|
139
|
+
return { code: -1, stdout: '', stderr: String(e instanceof Error ? e.message : e) };
|
|
140
|
+
});
|
|
141
|
+
return { ok: r.code === 0, output: (r.stdout + r.stderr).trim() };
|
|
142
|
+
}
|
|
143
|
+
export async function setRemote(cwd, url, remote = 'origin') {
|
|
144
|
+
const existing = await runGit(['remote', 'get-url', remote], { cwd, mayFail: true }).catch(() => undefined);
|
|
145
|
+
if (existing !== undefined && existing.code === 0 && existing.stdout.trim() !== '') {
|
|
146
|
+
await runGit(['remote', 'set-url', remote, url], { cwd });
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
await runGit(['remote', 'add', remote, url], { cwd });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export async function removeRemote(cwd, remote = 'origin') {
|
|
153
|
+
await runGit(['remote', 'remove', remote], { cwd, mayFail: true }).catch(() => undefined);
|
|
154
|
+
}
|
|
155
|
+
export async function hasRemote(cwd, remote = 'origin') {
|
|
156
|
+
const r = await runGit(['remote', 'get-url', remote], { cwd, mayFail: true }).catch(() => undefined);
|
|
157
|
+
return r !== undefined && r.code === 0 && r.stdout.trim() !== '';
|
|
158
|
+
}
|
|
159
|
+
export async function unpushedCount(cwd, remote = 'origin', branch = 'main') {
|
|
160
|
+
const r = await runGit(['rev-list', '--count', `${remote}/${branch}..${branch}`], { cwd, mayFail: true }).catch(() => undefined);
|
|
161
|
+
if (r === undefined || r.code !== 0)
|
|
162
|
+
return 0;
|
|
163
|
+
return Number(r.stdout.trim()) || 0;
|
|
164
|
+
}
|
|
165
|
+
/** True when the remote advertises the branch — empty remotes skip the pull step. */
|
|
166
|
+
export async function remoteBranchExists(cwd, token, remote = 'origin', branch = 'main') {
|
|
167
|
+
const r = await runGit(['ls-remote', '--heads', remote, branch], { cwd, token, mayFail: true, timeoutMs: 60_000 }).catch(() => undefined);
|
|
168
|
+
return r !== undefined && r.code === 0 && r.stdout.trim() !== '';
|
|
169
|
+
}
|
package/lib/ilog.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injection log (ADR 0007) — one record per retrieval round, no conversation
|
|
3
|
+
* text, only features and decisions. Aggregations power `/topics stats` and the
|
|
4
|
+
* near-miss evidence that makes threshold tuning measurable.
|
|
5
|
+
*
|
|
6
|
+
* @module ilog
|
|
7
|
+
*/
|
|
8
|
+
export interface InjectionRecord {
|
|
9
|
+
at: string;
|
|
10
|
+
sessionId?: string;
|
|
11
|
+
/** Query shape: token count and a bounded sample (features, not content). */
|
|
12
|
+
queryTokenCount: number;
|
|
13
|
+
querySample?: string;
|
|
14
|
+
rosterSize: number;
|
|
15
|
+
hits: {
|
|
16
|
+
slug: string;
|
|
17
|
+
score: number;
|
|
18
|
+
reasons: string[];
|
|
19
|
+
viaGraph: boolean;
|
|
20
|
+
}[];
|
|
21
|
+
nearMisses: {
|
|
22
|
+
slug: string;
|
|
23
|
+
score: number;
|
|
24
|
+
}[];
|
|
25
|
+
injected: boolean;
|
|
26
|
+
why?: string;
|
|
27
|
+
dropped?: {
|
|
28
|
+
slug: string;
|
|
29
|
+
reason: string;
|
|
30
|
+
}[];
|
|
31
|
+
/** Slugs blocked this round by session-level injection dedup (never assembled). */
|
|
32
|
+
deduped?: string[];
|
|
33
|
+
usedTokens?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface AggregateStats {
|
|
36
|
+
rounds: number;
|
|
37
|
+
injectedRounds: number;
|
|
38
|
+
hitRate: number;
|
|
39
|
+
zeroHitRounds: number;
|
|
40
|
+
avgHitsPerRound: number;
|
|
41
|
+
topTopics: {
|
|
42
|
+
slug: string;
|
|
43
|
+
count: number;
|
|
44
|
+
}[];
|
|
45
|
+
nearMissHistogram: {
|
|
46
|
+
bucket: string;
|
|
47
|
+
count: number;
|
|
48
|
+
}[];
|
|
49
|
+
avgBudgetUtilization: number;
|
|
50
|
+
}
|
|
51
|
+
export declare function aggregateStats(records: readonly InjectionRecord[]): AggregateStats;
|
|
52
|
+
/** Bounded sample of the query kept for debugging — words only, max 40 chars. */
|
|
53
|
+
export declare function querySample(query: string): string;
|
package/lib/ilog.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Injection log (ADR 0007) — one record per retrieval round, no conversation
|
|
3
|
+
* text, only features and decisions. Aggregations power `/topics stats` and the
|
|
4
|
+
* near-miss evidence that makes threshold tuning measurable.
|
|
5
|
+
*
|
|
6
|
+
* @module ilog
|
|
7
|
+
*/
|
|
8
|
+
export function aggregateStats(records) {
|
|
9
|
+
const rounds = records.length;
|
|
10
|
+
let injectedRounds = 0;
|
|
11
|
+
let zeroHitRounds = 0;
|
|
12
|
+
let hitsSum = 0;
|
|
13
|
+
let budgetSamples = 0;
|
|
14
|
+
let budgetSum = 0;
|
|
15
|
+
const topicCounts = new Map();
|
|
16
|
+
const buckets = new Map();
|
|
17
|
+
for (const r of records) {
|
|
18
|
+
if (r.injected)
|
|
19
|
+
injectedRounds += 1;
|
|
20
|
+
if (r.hits.length === 0)
|
|
21
|
+
zeroHitRounds += 1;
|
|
22
|
+
hitsSum += r.hits.length;
|
|
23
|
+
if (typeof r.usedTokens === 'number' && r.usedTokens > 0) {
|
|
24
|
+
budgetSamples += 1;
|
|
25
|
+
budgetSum += r.usedTokens;
|
|
26
|
+
}
|
|
27
|
+
// topTopics feeds 「Top-N 被注入 Topic」— deduped hits were NOT injected,
|
|
28
|
+
// so they must not inflate the per-slug injection counts. Retrieval-shape
|
|
29
|
+
// metrics above (hits/rounds, zero-hit rounds) keep counting raw hits.
|
|
30
|
+
const deduped = r.deduped === undefined ? undefined : new Set(r.deduped);
|
|
31
|
+
for (const h of r.hits) {
|
|
32
|
+
if (deduped?.has(h.slug))
|
|
33
|
+
continue;
|
|
34
|
+
topicCounts.set(h.slug, (topicCounts.get(h.slug) ?? 0) + 1);
|
|
35
|
+
}
|
|
36
|
+
for (const nm of r.nearMisses) {
|
|
37
|
+
const bucket = nmBucket(nm.score);
|
|
38
|
+
buckets.set(bucket, (buckets.get(bucket) ?? 0) + 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const topTopics = [...topicCounts.entries()]
|
|
42
|
+
.map(([slug, count]) => ({ slug, count }))
|
|
43
|
+
.sort((a, b) => b.count - a.count)
|
|
44
|
+
.slice(0, 10);
|
|
45
|
+
const nearMissHistogram = [...buckets.entries()]
|
|
46
|
+
.sort((a, b) => bucketOrder(a[0]) - bucketOrder(b[0]))
|
|
47
|
+
.map(([bucket, count]) => ({ bucket, count }));
|
|
48
|
+
return {
|
|
49
|
+
rounds,
|
|
50
|
+
injectedRounds,
|
|
51
|
+
hitRate: rounds === 0 ? 0 : Math.round((injectedRounds / rounds) * 1000) / 1000,
|
|
52
|
+
zeroHitRounds,
|
|
53
|
+
avgHitsPerRound: rounds === 0 ? 0 : Math.round((hitsSum / rounds) * 100) / 100,
|
|
54
|
+
topTopics,
|
|
55
|
+
nearMissHistogram,
|
|
56
|
+
avgBudgetUtilization: budgetSamples === 0 ? 0 : Math.round((budgetSum / budgetSamples) * 100) / 100,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function nmBucket(score) {
|
|
60
|
+
const floor = Math.floor(score * 20) / 20;
|
|
61
|
+
return `${floor.toFixed(2)}–${(floor + 0.05).toFixed(2)}`;
|
|
62
|
+
}
|
|
63
|
+
function bucketOrder(label) {
|
|
64
|
+
return Number(label.split('–')[0]);
|
|
65
|
+
}
|
|
66
|
+
/** Bounded sample of the query kept for debugging — words only, max 40 chars. */
|
|
67
|
+
export function querySample(query) {
|
|
68
|
+
const words = query.replace(/\s+/g, ' ').trim();
|
|
69
|
+
return words.length <= 40 ? words : `${words.slice(0, 40)}…`;
|
|
70
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-topics-memory — OKF topic memory for DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* Wiring (per ADR 0001–0008):
|
|
5
|
+
* - `topics` settings namespace (user-tunable via /topics set)
|
|
6
|
+
* - static systemPrompt section teaching the topic tools (never volatile
|
|
7
|
+
* content — the provider cache prefix stays byte-stable)
|
|
8
|
+
* - same-turn injection: retrieval runs synchronously at inbox-claim time
|
|
9
|
+
* (`agent/inbox/spliced` live event, which dispatches BEFORE prompt
|
|
10
|
+
* assembly — the only seam early enough, per the dsh-llmwiki upstream's
|
|
11
|
+
* validated recipe) and a systemPrompt.context() provider serves the assembled
|
|
12
|
+
* digest for this turn
|
|
13
|
+
* - model tools: topic_save / topic_search / topic_observe / topic_history
|
|
14
|
+
* - `/topics` command via the shared dsh-commands registry (optional peer)
|
|
15
|
+
* - observer (M2): turn capture + distill triggers over session events
|
|
16
|
+
* - sync (ADR 0003): pull on session start, debounced write-through push
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
export declare const name = "dsh-topics-memory";
|
|
20
|
+
/**
|
|
21
|
+
* Bounded wait for the process-exit distill. Session teardown is the last
|
|
22
|
+
* distill trigger, but the disposer used to fire it fire-and-forget — process
|
|
23
|
+
* exit always won the race and the run died unwritten. The disposer now waits
|
|
24
|
+
* for the run, capped so a pathological (hanging) model call can never wedge
|
|
25
|
+
* the host's exit.
|
|
26
|
+
*/
|
|
27
|
+
export declare const EXIT_DISTILL_TIMEOUT_MS = 90000;
|
|
28
|
+
/**
|
|
29
|
+
* Resolve when `p` settles (rejection swallowed) or after `ms`, whichever
|
|
30
|
+
* comes first. The cap timer is unref'd: an otherwise-idle event loop exits
|
|
31
|
+
* naturally instead of being kept alive purely by the bound.
|
|
32
|
+
*/
|
|
33
|
+
export declare function settleBounded(p: Promise<unknown> | undefined, ms: number): Promise<void>;
|
|
34
|
+
/** Services consumed at apply time; llm joins via guarded ctx.inject. */
|
|
35
|
+
export declare const inject: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Settings-provider surface the legacy-namespace migration needs. Structurally
|
|
38
|
+
* typed like every other host seam in this file, so bare test harnesses
|
|
39
|
+
* without dsh-settings still load the module: a host lacking `describe` or
|
|
40
|
+
* `mutate` simply skips the migration.
|
|
41
|
+
*/
|
|
42
|
+
interface SettingsNsLike {
|
|
43
|
+
register(n: unknown, s: unknown): {
|
|
44
|
+
get(): unknown;
|
|
45
|
+
};
|
|
46
|
+
describe?(): {
|
|
47
|
+
ns: unknown;
|
|
48
|
+
user?: unknown;
|
|
49
|
+
}[];
|
|
50
|
+
mutate?(ns: unknown, ops: readonly {
|
|
51
|
+
op: 'set';
|
|
52
|
+
path: string[];
|
|
53
|
+
value?: unknown;
|
|
54
|
+
}[]): Promise<void>;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* One-time carry-over of user-tuned values from the pre-rename `llmwiki`
|
|
58
|
+
* settings namespace into `topics` (ADR 0013). Constraints:
|
|
59
|
+
* - one-time: only fires while the new namespace holds NO raw user overrides —
|
|
60
|
+
* describe().user is the only way to tell user keys from schema defaults,
|
|
61
|
+
* because scope.get() folds the defaults in;
|
|
62
|
+
* - idempotent: after the copy (or any config written on the new name) the
|
|
63
|
+
* override check short-circuits every later boot;
|
|
64
|
+
* - fail-open: registering the legacy namespace can throw (duplicate or
|
|
65
|
+
* schema-invalid stored section) and the write can reject — both contained,
|
|
66
|
+
* so plugin startup never depends on the migration.
|
|
67
|
+
* Keys whose stored value equals the schema default are not copied (nothing
|
|
68
|
+
* user-tuned to preserve). Returns the in-flight write for tests; apply()
|
|
69
|
+
* fire-and-forgets it.
|
|
70
|
+
*/
|
|
71
|
+
export declare function migrateLegacySettings(settings: SettingsNsLike, warn?: (message: string) => void): Promise<void> | undefined;
|
|
72
|
+
export declare function apply(ctx: Context): void;
|
|
73
|
+
export {};
|