@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/retrieval.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @module retrieval
|
|
13
13
|
*/
|
|
14
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
14
16
|
import { dependsSlugs } from "./okf.js";
|
|
15
17
|
/** Hard cap for the usage bonus — below threshold 0.3 so usage can never
|
|
16
18
|
* carry a candidate across the gate on its own (ADR 0015). */
|
|
@@ -256,3 +258,281 @@ export function searchTopics(query, roster, cfg = {}) {
|
|
|
256
258
|
}
|
|
257
259
|
return { hits, nearMisses, rosterSize: roster.length };
|
|
258
260
|
}
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// Injection-quality line (wave/injection-quality, spec §2.1/§3.1/§3.3) —
|
|
263
|
+
// CAPABILITY ONLY. None of this is wired into searchTopics or any service
|
|
264
|
+
// path; the caller gates it on packageMode ≠ 'off' (Phase 2 wiring). With
|
|
265
|
+
// the config keys at their defaults every code path below is unreachable
|
|
266
|
+
// from the hot lane, so default-off behavior stays byte-for-byte.
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
// --- ① Query sanitization (A4 决议 4, spec §3.3) ---------------------------
|
|
269
|
+
// Pure, LLM-free, millisecond-scale. Treats the queryTokenCount p90=462
|
|
270
|
+
// paste-pollution tail: fenced code blocks, URLs, line-leading shell
|
|
271
|
+
// prompts, over-long lines, and a hard token cap.
|
|
272
|
+
/**
|
|
273
|
+
* Token cap in tokenize() units — the SAME unit injections.jsonl logs as
|
|
274
|
+
* queryTokenCount (p90=462 baseline; real-query p50=153 stays untouched).
|
|
275
|
+
* Raised or lowered via SanitizeOptions.maxTokens.
|
|
276
|
+
*/
|
|
277
|
+
export const QUERY_TOKEN_CAP = 96;
|
|
278
|
+
/**
|
|
279
|
+
* Lines longer than this are folded to their head (log-paste guard). Sized
|
|
280
|
+
* so the 96-token cap is the binding constraint, not the fold: 96 latin
|
|
281
|
+
* tokens ≈ 650+ chars, so a legitimate long single-line query passes the
|
|
282
|
+
* fold intact and only the token cap trims it. (实现期定 constant.)
|
|
283
|
+
*/
|
|
284
|
+
export const MAX_LINE_CHARS = 720;
|
|
285
|
+
const FENCE_OPEN = /^[ \t]*(```|~~~)/;
|
|
286
|
+
// Line-leading shell prompt: `user@host:~/repo$ `, `C:\> `, or a bare
|
|
287
|
+
// `$ `/`# `/`% `/`> `/`❯ ` marker. Requires trailing whitespace so words
|
|
288
|
+
// like `$PATH` or `#tag` survive untouched.
|
|
289
|
+
const LINE_PROMPT = /^[ \t]*(?:[\w.@:/\\~-]*[$#%>❯➜])[ \t]+/;
|
|
290
|
+
const URL_RE = /\b(?:https?:\/\/|www\.)[^\s<>"'`)\]]+/gi;
|
|
291
|
+
// Chunks the cap walks: one latin/digit word (or hyphenated compound) per
|
|
292
|
+
// match, or one CJK run (sliced when a run alone busts the cap).
|
|
293
|
+
const CHUNK_RUN = new RegExp(`${WORD_RUN.source}|${CJK_RUN.source}`, 'gu');
|
|
294
|
+
/** Remove fenced code blocks wholesale (markers AND body): fenced code is
|
|
295
|
+
* the paste-pollution payload; the prose around it is the actual query. */
|
|
296
|
+
function stripFencedBlocks(text) {
|
|
297
|
+
const out = [];
|
|
298
|
+
let open = null;
|
|
299
|
+
for (const line of text.split('\n')) {
|
|
300
|
+
const m = line.match(FENCE_OPEN);
|
|
301
|
+
if (open === null) {
|
|
302
|
+
if (m !== null)
|
|
303
|
+
open = m[1];
|
|
304
|
+
else
|
|
305
|
+
out.push(line);
|
|
306
|
+
}
|
|
307
|
+
else if (m !== null && m[1] === open) {
|
|
308
|
+
open = null; // closing fence — block fully dropped
|
|
309
|
+
}
|
|
310
|
+
// inside an open fence: drop the line
|
|
311
|
+
}
|
|
312
|
+
return out.join('\n');
|
|
313
|
+
}
|
|
314
|
+
/** Cap the sanitized text to ≤ cap distinct retrieval tokens. Fast path:
|
|
315
|
+
* at or under the cap the text passes through VERBATIM (no re-chunking —
|
|
316
|
+
* hyphens, punctuation and spacing survive byte-exact; retrieval scores
|
|
317
|
+
* tokens, not bytes, but the string may be echoed into logs/injection).
|
|
318
|
+
* Only an over-cap text rebuilds from chunks: head-first (a query's intent
|
|
319
|
+
* leads; pollution trails), with CJK runs sliced at the character level so
|
|
320
|
+
* a single giant run truncates instead of being dropped whole. */
|
|
321
|
+
function capTokens(text, cap) {
|
|
322
|
+
const total = tokenize(text).length;
|
|
323
|
+
if (total === 0)
|
|
324
|
+
return ''; // nothing lexical survives — caller sees a zero-token query
|
|
325
|
+
if (total <= cap)
|
|
326
|
+
return text;
|
|
327
|
+
const seen = new Set();
|
|
328
|
+
const kept = [];
|
|
329
|
+
for (const chunk of text.match(CHUNK_RUN) ?? []) {
|
|
330
|
+
const fresh = tokenize(chunk).filter((t) => !seen.has(t));
|
|
331
|
+
if (fresh.length === 0)
|
|
332
|
+
continue; // already-covered chunk adds nothing
|
|
333
|
+
if (seen.size + fresh.length <= cap) {
|
|
334
|
+
for (const t of fresh)
|
|
335
|
+
seen.add(t);
|
|
336
|
+
kept.push(chunk);
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
const room = cap - seen.size;
|
|
340
|
+
if (room <= 0)
|
|
341
|
+
break;
|
|
342
|
+
// Over budget: CJK runs truncate exactly (slice room+1 chars → ≤ room
|
|
343
|
+
// fresh bigrams, so the cap invariant holds). Latin chunks are single
|
|
344
|
+
// tokens by construction — they never reach here with room > 0, but if
|
|
345
|
+
// one ever does, dropping it beats busting the cap.
|
|
346
|
+
if (/^[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]/u.test(chunk)) {
|
|
347
|
+
kept.push(chunk.slice(0, room + 1));
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
return kept.join(' ');
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Sanitize a raw user query for retrieval (spec §3.3): strip fenced code
|
|
355
|
+
* blocks and URLs, fold line-leading shell prompts, fold over-long lines to
|
|
356
|
+
* their head, cap the distinct-token count. Returns '' when nothing lexical
|
|
357
|
+
* survives — the caller treats that as a zero-token query (natural skip).
|
|
358
|
+
* Pure function; the hot lane only reaches it behind packageMode.
|
|
359
|
+
*/
|
|
360
|
+
export function sanitizeQuery(raw, opts = {}) {
|
|
361
|
+
const cap = opts.maxTokens ?? QUERY_TOKEN_CAP;
|
|
362
|
+
const maxLineChars = opts.maxLineChars ?? MAX_LINE_CHARS;
|
|
363
|
+
if (raw === '')
|
|
364
|
+
return '';
|
|
365
|
+
let text = stripFencedBlocks(raw);
|
|
366
|
+
text = text.replace(URL_RE, ' ');
|
|
367
|
+
text = text
|
|
368
|
+
.split('\n')
|
|
369
|
+
.map((line) => {
|
|
370
|
+
let l = line.replace(LINE_PROMPT, '');
|
|
371
|
+
if (l.length > maxLineChars)
|
|
372
|
+
l = l.slice(0, maxLineChars);
|
|
373
|
+
return l;
|
|
374
|
+
})
|
|
375
|
+
.join('\n');
|
|
376
|
+
text = text.replace(/\s+/g, ' ').trim();
|
|
377
|
+
if (text === '')
|
|
378
|
+
return '';
|
|
379
|
+
return capTokens(text, cap);
|
|
380
|
+
}
|
|
381
|
+
export function embeddingIndexPath(bundleDir) {
|
|
382
|
+
return join(bundleDir, 'meta', 'embeddings.json');
|
|
383
|
+
}
|
|
384
|
+
function parseEmbeddingIndexEntry(value) {
|
|
385
|
+
if (typeof value !== 'object' || value === null)
|
|
386
|
+
return null;
|
|
387
|
+
const e = value;
|
|
388
|
+
if (typeof e.model !== 'string' || e.model === '')
|
|
389
|
+
return null;
|
|
390
|
+
if (typeof e.dim !== 'number' || !Number.isInteger(e.dim) || e.dim <= 0)
|
|
391
|
+
return null;
|
|
392
|
+
if (!Array.isArray(e.vec) || e.vec.length !== e.dim)
|
|
393
|
+
return null;
|
|
394
|
+
if (!e.vec.every((n) => typeof n === 'number' && Number.isFinite(n)))
|
|
395
|
+
return null;
|
|
396
|
+
return { model: e.model, dim: e.dim, vec: e.vec };
|
|
397
|
+
}
|
|
398
|
+
/** Load meta/embeddings.json from a bundle dir. Missing file, corrupt JSON,
|
|
399
|
+
* or malformed entries are skipped/empty — never throws (a broken index
|
|
400
|
+
* must degrade to the lexical path, not break retrieval). */
|
|
401
|
+
export function loadEmbeddingIndex(bundleDir) {
|
|
402
|
+
let raw;
|
|
403
|
+
try {
|
|
404
|
+
raw = readFileSync(embeddingIndexPath(bundleDir), 'utf8');
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
return {};
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const json = JSON.parse(raw);
|
|
411
|
+
if (typeof json !== 'object' || json === null || Array.isArray(json))
|
|
412
|
+
return {};
|
|
413
|
+
const out = {};
|
|
414
|
+
for (const [slug, entry] of Object.entries(json)) {
|
|
415
|
+
if (slug === '')
|
|
416
|
+
continue;
|
|
417
|
+
const parsed = parseEmbeddingIndexEntry(entry);
|
|
418
|
+
if (parsed !== null)
|
|
419
|
+
out[slug] = parsed;
|
|
420
|
+
}
|
|
421
|
+
return out;
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return {};
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/** Persist the index in the canonical shape (the rebuild's write side —
|
|
428
|
+
* embedding/rebuilding itself stays with the caller). Offline rebuild
|
|
429
|
+
* path, not the hot lane. */
|
|
430
|
+
export function saveEmbeddingIndex(bundleDir, index, mkdir = true) {
|
|
431
|
+
if (mkdir)
|
|
432
|
+
mkdirSync(join(bundleDir, 'meta'), { recursive: true });
|
|
433
|
+
writeFileSync(embeddingIndexPath(bundleDir), JSON.stringify(index));
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Freshness audit: which slugs need (re)embedding — missing from the index,
|
|
437
|
+
* built by a different model, or shape-broken (dim/vec disagree). Returns
|
|
438
|
+
* them in roster order; an EMPTY list means the index covers the roster.
|
|
439
|
+
* A '' embedder model means the semantic layer is off → nothing to maintain.
|
|
440
|
+
*
|
|
441
|
+
* NOTE the freshness boundary: content-change invalidation needs a stored
|
|
442
|
+
* content hash, which the pinned {model,dim,vec} entry shape does not carry
|
|
443
|
+
* — the rebuild trigger policy is spec-实现期定 and stays with the caller.
|
|
444
|
+
*/
|
|
445
|
+
export function ensureIndexFresh(index, topics, embedFn) {
|
|
446
|
+
if ((embedFn.model ?? '') === '')
|
|
447
|
+
return [];
|
|
448
|
+
const stale = [];
|
|
449
|
+
for (const t of topics) {
|
|
450
|
+
const e = index[t.slug];
|
|
451
|
+
if (e === undefined || e.model !== embedFn.model || e.dim !== e.vec.length || !(e.dim > 0))
|
|
452
|
+
stale.push(t.slug);
|
|
453
|
+
}
|
|
454
|
+
return stale;
|
|
455
|
+
}
|
|
456
|
+
// --- ③ nearFloor semantic rerank (A4 决议 2, spec §3.1) ---------------------
|
|
457
|
+
// Cosine reordering of the nearFloor band ONLY (0.20–0.30 — the 93%
|
|
458
|
+
// near-miss mass). Thresholds, gateScore and the structural gate are
|
|
459
|
+
// untouched: the reranker reorders and tags, never re-scores.
|
|
460
|
+
export const RERANK_FLOOR = 0.2;
|
|
461
|
+
export const RERANK_CEILING = 0.3;
|
|
462
|
+
export const SEMANTIC_RERANK_REASON = 'semantic-rerank';
|
|
463
|
+
/** Cosine similarity; 0 for dim mismatch or zero vectors (never throws). */
|
|
464
|
+
export function cosineSimilarity(a, b) {
|
|
465
|
+
if (a.length === 0 || a.length !== b.length)
|
|
466
|
+
return 0;
|
|
467
|
+
let dot = 0;
|
|
468
|
+
let na = 0;
|
|
469
|
+
let nb = 0;
|
|
470
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
471
|
+
dot += a[i] * b[i];
|
|
472
|
+
na += a[i] * a[i];
|
|
473
|
+
nb += b[i] * b[i];
|
|
474
|
+
}
|
|
475
|
+
if (na === 0 || nb === 0)
|
|
476
|
+
return 0;
|
|
477
|
+
return dot / Math.sqrt(na * nb);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Reorder the nearFloor band (0.20–0.30 by gateScore) by cosine similarity
|
|
481
|
+
* against the query vector. Band members with a usable index vector sort by
|
|
482
|
+
* similarity (stable — ties keep input order) and gain the
|
|
483
|
+
* `semantic-rerank` reason plus their similarity; everyone else — out of
|
|
484
|
+
* band, unindexed, model-mismatched — passes through UNCHANGED at the tail
|
|
485
|
+
* in input order. Scores are never touched.
|
|
486
|
+
*
|
|
487
|
+
* Degradation is explicit and total: provider/model unconfigured, probe
|
|
488
|
+
* failure, missing query vector or missing index all return the input as-is
|
|
489
|
+
* with `reranked:false` and a passthroughReason (原序透传,不许静默 — the
|
|
490
|
+
* caller logs it into the degraded status). `queryText` rides the signature
|
|
491
|
+
* for provenance/debug parity with the caller's logging; the v1 math is
|
|
492
|
+
* vector-only.
|
|
493
|
+
*/
|
|
494
|
+
export function rerankNearFloor(queryText, queryVec, nearFloorCandidates, index, opts = {}) {
|
|
495
|
+
void queryText; // v1 math is vector-only — see doc above
|
|
496
|
+
if (nearFloorCandidates.length === 0) {
|
|
497
|
+
return { hits: [], reranked: false, passthroughReason: 'no-candidates' };
|
|
498
|
+
}
|
|
499
|
+
if (!Array.isArray(queryVec) || queryVec.length === 0) {
|
|
500
|
+
return { hits: [...nearFloorCandidates], reranked: false, passthroughReason: 'no-query-vector' };
|
|
501
|
+
}
|
|
502
|
+
if (Object.keys(index).length === 0) {
|
|
503
|
+
return { hits: [...nearFloorCandidates], reranked: false, passthroughReason: 'empty-index' };
|
|
504
|
+
}
|
|
505
|
+
const floor = opts.floor ?? RERANK_FLOOR;
|
|
506
|
+
const ceiling = opts.ceiling ?? RERANK_CEILING;
|
|
507
|
+
const simOf = new Map();
|
|
508
|
+
for (const hit of nearFloorCandidates) {
|
|
509
|
+
const entry = index[hit.slug];
|
|
510
|
+
const usable = entry !== undefined &&
|
|
511
|
+
entry.dim === entry.vec.length &&
|
|
512
|
+
entry.dim > 0 &&
|
|
513
|
+
entry.dim === queryVec.length &&
|
|
514
|
+
(opts.expectedModel === undefined || entry.model === opts.expectedModel);
|
|
515
|
+
simOf.set(hit, usable ? cosineSimilarity(queryVec, entry.vec) : null);
|
|
516
|
+
}
|
|
517
|
+
const band = nearFloorCandidates.filter((h) => {
|
|
518
|
+
const sim = simOf.get(h);
|
|
519
|
+
return sim !== null && sim !== undefined && h.gateScore !== undefined && h.gateScore >= floor && h.gateScore < ceiling;
|
|
520
|
+
});
|
|
521
|
+
if (band.length === 0) {
|
|
522
|
+
return { hits: [...nearFloorCandidates], reranked: false, passthroughReason: 'no-rerankable-candidates' };
|
|
523
|
+
}
|
|
524
|
+
const rerankedSet = new Set(band);
|
|
525
|
+
const tail = nearFloorCandidates.filter((h) => !rerankedSet.has(h));
|
|
526
|
+
// Stable sort: V8's Array#sort keeps input order for equal similarities.
|
|
527
|
+
const ordered = [...band].sort((x, y) => simOf.get(y) - simOf.get(x));
|
|
528
|
+
return {
|
|
529
|
+
hits: [
|
|
530
|
+
...ordered.map((h) => {
|
|
531
|
+
const sim = simOf.get(h);
|
|
532
|
+
return { ...h, reasons: [...h.reasons, SEMANTIC_RERANK_REASON], similarity: Math.round(sim * 10_000) / 10_000 };
|
|
533
|
+
}),
|
|
534
|
+
...tail.map((h) => ({ ...h })),
|
|
535
|
+
],
|
|
536
|
+
reranked: true,
|
|
537
|
+
};
|
|
538
|
+
}
|
package/lib/service.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { type RetrievableTopic, type SearchOutcome } from './retrieval.ts';
|
|
|
10
10
|
import { type AssembleResult } from './digest.ts';
|
|
11
11
|
import type { BundleStore, Observation, SaveResult } from './store.ts';
|
|
12
12
|
import type { TopicsConfigValue } from './config.ts';
|
|
13
|
-
import { type AggregateStats, type QueryBuildShape, type UsageSignal } from './ilog.ts';
|
|
13
|
+
import { type AggregateStats, type QueryBuildShape, type SlowOutcome, type UsageSignal } from './ilog.ts';
|
|
14
14
|
import type { Sync } from './sync.ts';
|
|
15
15
|
export interface RetrieveOutcome {
|
|
16
16
|
outcome: SearchOutcome;
|
|
@@ -33,6 +33,42 @@ export interface SlowDelivery {
|
|
|
33
33
|
queryBuild: QueryBuildShape;
|
|
34
34
|
model: string;
|
|
35
35
|
ms: number;
|
|
36
|
+
/** Producing run's disposition (A4 全留痕) — copied into ilog.slowOutcome. */
|
|
37
|
+
outcome?: SlowOutcome;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* One structured observation from the new repo's signal layer (B1 §2.1
|
|
41
|
+
* whitelist): FEATURES ONLY, never conversation text (红线⑤). Unknown caller
|
|
42
|
+
* fields cannot pass — the whitelist copy in feedObservations is the gate.
|
|
43
|
+
*/
|
|
44
|
+
export interface StructuredObservation {
|
|
45
|
+
/** Signal type from the B1 closed list (`tool-error`, `turn-end-negative`,
|
|
46
|
+
* `unused-injection`, `repeat-call`, `interruption-pair`, `jev-calibration`). */
|
|
47
|
+
type: string;
|
|
48
|
+
/** ISO instant; defaults to now. */
|
|
49
|
+
at?: string;
|
|
50
|
+
sessionId?: string;
|
|
51
|
+
/** Bounded scalar feature fields (toolName/errorName/count/band/...). */
|
|
52
|
+
detail?: Record<string, string | number | boolean>;
|
|
53
|
+
}
|
|
54
|
+
export interface FeedResult {
|
|
55
|
+
accepted: number;
|
|
56
|
+
duplicates: number;
|
|
57
|
+
rejected: number;
|
|
58
|
+
ids: string[];
|
|
59
|
+
/** Present when the call itself was refused (missing/blank actor). */
|
|
60
|
+
error?: 'unauthorized';
|
|
61
|
+
}
|
|
62
|
+
export interface StoreResult {
|
|
63
|
+
ok: boolean;
|
|
64
|
+
slug: string;
|
|
65
|
+
/** Refusal reason; absent on success. */
|
|
66
|
+
reason?: 'unauthorized' | 'not-found' | 'not-a-lesson' | 'invalid-state';
|
|
67
|
+
/** confidence before the transition (when the lesson was read). */
|
|
68
|
+
from?: okf.LessonConfidence;
|
|
69
|
+
/** removed only: bundle-relative archive path (git-revertable). */
|
|
70
|
+
archivedTo?: string;
|
|
71
|
+
committed?: boolean;
|
|
36
72
|
}
|
|
37
73
|
export declare class TopicsService {
|
|
38
74
|
private cache;
|
|
@@ -101,6 +137,22 @@ export declare class TopicsService {
|
|
|
101
137
|
* the slow lane's recall band stays untouched, mirroring usageBoost).
|
|
102
138
|
*/
|
|
103
139
|
private zeroOpenCfg;
|
|
140
|
+
/** mtime-cached view of meta/embeddings.json (sync read — hot-lane caller).
|
|
141
|
+
* Missing/corrupt index = {} → the reranker degrades to lexical order. */
|
|
142
|
+
private embedIndexCache;
|
|
143
|
+
private embeddingIndexCacheGet;
|
|
144
|
+
/** Shadow bypass ledger (spec §8.3): one line per shadow round — feature
|
|
145
|
+
* fields only, never query text (jev-log discipline). Silent on failure;
|
|
146
|
+
* 512KB truncation keeps the newest three quarters. Shared by the package
|
|
147
|
+
* shadow (meta/package-shadow.jsonl) and the lesson shadow (B2,
|
|
148
|
+
* meta/lesson-shadow.jsonl). */
|
|
149
|
+
private appendShadowLedger;
|
|
150
|
+
private packageShadowLedgerPath;
|
|
151
|
+
private lessonShadowLedgerPath;
|
|
152
|
+
/** Boot-time embedding index maintenance (spec §3: boot 校验+增量重建).
|
|
153
|
+
* Provider '' or packageMode off → no-op (default-off byte-for-byte).
|
|
154
|
+
* Full-roster embed on rebuild (A1 §5: ~9.5s for 137 topics). */
|
|
155
|
+
refreshEmbeddingIndex(): Promise<void>;
|
|
104
156
|
/**
|
|
105
157
|
* Full retrieval round: search → assemble → log (ADR 0006/0007). The
|
|
106
158
|
* injection record is written even for zero-hit rounds — the hit-rate
|
|
@@ -140,6 +192,7 @@ export declare class TopicsService {
|
|
|
140
192
|
echoed: string[];
|
|
141
193
|
included: string[];
|
|
142
194
|
slowIncluded: string[];
|
|
195
|
+
lessonIncluded: string[];
|
|
143
196
|
};
|
|
144
197
|
/**
|
|
145
198
|
* topic_open tool path (v4 §4.1): full conclusion / open questions /
|
|
@@ -167,9 +220,25 @@ export declare class TopicsService {
|
|
|
167
220
|
private attributeOpen;
|
|
168
221
|
/** Sync roster read (same mtime cache as roster()). */
|
|
169
222
|
rosterSync(): RetrievableTopic[];
|
|
223
|
+
/** Established, scope-passing lesson entries (B2, spec 卷一 §6.3) — sync,
|
|
224
|
+
* sharing the roster's mtime cache. Injection reads ONLY established
|
|
225
|
+
* lessons here (MJ-3: hint 仅供审批队列展示,永不注入); scope filter =
|
|
226
|
+
* workspace lessons must carry the current workspace id (MJ-2); scope=global
|
|
227
|
+
* and legacy entries without a workspaceId pass (fail-open — the filter's
|
|
228
|
+
* default is permissive, the confidence gate is the hard one). */
|
|
229
|
+
lessonsSync(): (RetrievableTopic & {
|
|
230
|
+
confidence: okf.LessonConfidence;
|
|
231
|
+
source: okf.LessonSource;
|
|
232
|
+
scope: okf.LessonScope;
|
|
233
|
+
workspaceId?: string;
|
|
234
|
+
})[];
|
|
170
235
|
private readDocSync;
|
|
171
236
|
private readDoc;
|
|
172
|
-
/** topic_save tool path: create or update with actor stamping.
|
|
237
|
+
/** topic_save tool path: create or update with actor stamping. `kind:
|
|
238
|
+
* 'lesson'` (B2 data plane) switches the OKF type to Lesson and stamps the
|
|
239
|
+
* lesson frontmatter (scope/confidence/source/workspaceId) with the
|
|
240
|
+
* confidence↔status derivation (hint↔draft, established↔stable —
|
|
241
|
+
* confidence is the fact source). */
|
|
173
242
|
saveTopic(input: {
|
|
174
243
|
title: string;
|
|
175
244
|
description?: string;
|
|
@@ -178,11 +247,19 @@ export declare class TopicsService {
|
|
|
178
247
|
depends?: string[];
|
|
179
248
|
openQuestions?: string[];
|
|
180
249
|
impact?: string[];
|
|
250
|
+
/** A2 semantic-link targets; store.saveTopic lands them as body wikilinks. */
|
|
251
|
+
links?: string[];
|
|
181
252
|
status?: okf.TopicStatus;
|
|
182
253
|
conclusion?: string;
|
|
183
254
|
recommendations?: string;
|
|
184
255
|
source?: 'model' | 'distill';
|
|
185
256
|
slug?: string;
|
|
257
|
+
/** B2 lesson data plane: entry kind (default topic). */
|
|
258
|
+
kind?: okf.TopicKind;
|
|
259
|
+
scope?: okf.LessonScope;
|
|
260
|
+
confidence?: okf.LessonConfidence;
|
|
261
|
+
/** Lesson provenance (input.source above is the tool-path tag). */
|
|
262
|
+
lessonSource?: okf.LessonSource;
|
|
186
263
|
}): Promise<SaveResult & {
|
|
187
264
|
slug: string;
|
|
188
265
|
}>;
|
|
@@ -192,6 +269,30 @@ export declare class TopicsService {
|
|
|
192
269
|
sessionId?: string;
|
|
193
270
|
source?: 'model' | 'auto';
|
|
194
271
|
}): Promise<Observation>;
|
|
272
|
+
/**
|
|
273
|
+
* ① 观察投喂: structured observations (B1 §2.1 whitelist, FEATURES ONLY —
|
|
274
|
+
* 对话原文禁入, 红线⑤) appended to the observations stream with the `fed`
|
|
275
|
+
* provenance marker and `kind: 'signal'`; each line carries `by: actor`.
|
|
276
|
+
* The whitelist copy drops unknown caller fields, caps detail entries and
|
|
277
|
+
* string sizes, and renders a sorted feature line as the observation text.
|
|
278
|
+
* Duplicates (same type+session+detail in the recent tail) are counted, not
|
|
279
|
+
* appended (重复观察由原仓去重). Fail-soft: one malformed entry is counted
|
|
280
|
+
* as rejected, never sinks the batch.
|
|
281
|
+
*/
|
|
282
|
+
feedObservations(obs: readonly StructuredObservation[], opts: {
|
|
283
|
+
actor: string;
|
|
284
|
+
}): Promise<FeedResult>;
|
|
285
|
+
/**
|
|
286
|
+
* ② lesson 状态变更 (B2/卷二 §6.2): hint→established / →deprecated /
|
|
287
|
+
* →removed. established flips confidence (the FACT source) and derives
|
|
288
|
+
* status (established↔stable); deprecated flips status only (the TTL sweep
|
|
289
|
+
* owns the rest); removed = archive FIRST (byte-exact copy to
|
|
290
|
+
* `archive/lessons/` inside the bundle, git-revertable — 红线④) then out of
|
|
291
|
+
* the roster, one commit. Each call lands a `seam-lesson-status` audit row.
|
|
292
|
+
*/
|
|
293
|
+
setLessonStatus(slug: string, to: 'established' | 'deprecated' | 'removed', opts: {
|
|
294
|
+
actor: string;
|
|
295
|
+
}): Promise<StoreResult>;
|
|
195
296
|
search(query: string, topK?: number): Promise<SearchOutcome>;
|
|
196
297
|
history(slug: string, limit?: number): Promise<{
|
|
197
298
|
entries: {
|