@dzhechkov/harness-core 0.8.6 → 0.8.11
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/.dz-manifest.json +361 -97
- package/README.md +48 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +12 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/codex-invoke.d.ts +73 -0
- package/dist/codex-invoke.d.ts.map +1 -0
- package/dist/codex-invoke.js +80 -0
- package/dist/codex-invoke.js.map +1 -0
- package/dist/compounding.d.ts +54 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +221 -1
- package/dist/compounding.js.map +1 -1
- package/dist/discrimination-gate.d.ts +63 -3
- package/dist/discrimination-gate.d.ts.map +1 -1
- package/dist/discrimination-gate.js +113 -16
- package/dist/discrimination-gate.js.map +1 -1
- package/dist/eta.d.ts +92 -0
- package/dist/eta.d.ts.map +1 -0
- package/dist/eta.js +488 -0
- package/dist/eta.js.map +1 -0
- package/dist/event-chain.d.ts +30 -0
- package/dist/event-chain.d.ts.map +1 -1
- package/dist/event-chain.js +24 -0
- package/dist/event-chain.js.map +1 -1
- package/dist/feature-adr-checkpoints.js +1 -1
- package/dist/feature-adr-decision-recall.d.ts +167 -0
- package/dist/feature-adr-decision-recall.d.ts.map +1 -0
- package/dist/feature-adr-decision-recall.js +519 -0
- package/dist/feature-adr-decision-recall.js.map +1 -0
- package/dist/feature-adr-landing.d.ts +37 -0
- package/dist/feature-adr-landing.d.ts.map +1 -0
- package/dist/feature-adr-landing.js +59 -0
- package/dist/feature-adr-landing.js.map +1 -0
- package/dist/feature-adr-routing.d.ts +2 -2
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +7 -11
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/guard-promotion.d.ts +41 -0
- package/dist/guard-promotion.d.ts.map +1 -1
- package/dist/guard-promotion.js +218 -4
- package/dist/guard-promotion.js.map +1 -1
- package/dist/guard-volume.d.ts +108 -0
- package/dist/guard-volume.d.ts.map +1 -0
- package/dist/guard-volume.js +536 -0
- package/dist/guard-volume.js.map +1 -0
- package/dist/guard.d.ts +17 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +92 -4
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +24 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -5
- package/dist/index.js.map +1 -1
- package/dist/integration-apply.d.ts +25 -0
- package/dist/integration-apply.d.ts.map +1 -0
- package/dist/integration-apply.js +299 -0
- package/dist/integration-apply.js.map +1 -0
- package/dist/integration-evidence.d.ts +46 -0
- package/dist/integration-evidence.d.ts.map +1 -0
- package/dist/integration-evidence.js +44 -0
- package/dist/integration-evidence.js.map +1 -0
- package/dist/integration-probe-worker.d.ts +22 -0
- package/dist/integration-probe-worker.d.ts.map +1 -0
- package/dist/integration-probe-worker.js +334 -0
- package/dist/integration-probe-worker.js.map +1 -0
- package/dist/integrations-verify.d.ts +60 -0
- package/dist/integrations-verify.d.ts.map +1 -0
- package/dist/integrations-verify.js +194 -0
- package/dist/integrations-verify.js.map +1 -0
- package/dist/lesson-generalization.d.ts +29 -0
- package/dist/lesson-generalization.d.ts.map +1 -0
- package/dist/lesson-generalization.js +84 -0
- package/dist/lesson-generalization.js.map +1 -0
- package/dist/mutation-gate.d.ts +39 -36
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +111 -5
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts +27 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +186 -7
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +27 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +211 -45
- package/dist/patterns.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +27 -5
- package/dist/plugin.js.map +1 -1
- package/dist/recommend.d.ts +4 -5
- package/dist/recommend.d.ts.map +1 -1
- package/dist/recommend.js +110 -45
- package/dist/recommend.js.map +1 -1
- package/dist/registry.d.ts +32 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +165 -9
- package/dist/registry.js.map +1 -1
- package/dist/run-records.d.ts +3 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +18 -0
- package/dist/run-records.js.map +1 -1
- package/dist/score.d.ts +95 -0
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +274 -2
- package/dist/score.js.map +1 -1
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +20 -17
- package/dist/setup.js.map +1 -1
- package/dist/skill-selection.d.ts +72 -0
- package/dist/skill-selection.d.ts.map +1 -0
- package/dist/skill-selection.js +76 -0
- package/dist/skill-selection.js.map +1 -0
- package/dist/stem.d.ts +12 -0
- package/dist/stem.d.ts.map +1 -0
- package/dist/stem.js +89 -0
- package/dist/stem.js.map +1 -0
- package/dist/target-integrations.d.ts +65 -0
- package/dist/target-integrations.d.ts.map +1 -0
- package/dist/target-integrations.js +152 -0
- package/dist/target-integrations.js.map +1 -0
- package/dist/telemetry-vocabulary.d.ts +7 -0
- package/dist/telemetry-vocabulary.d.ts.map +1 -1
- package/dist/telemetry-vocabulary.js +29 -0
- package/dist/telemetry-vocabulary.js.map +1 -1
- package/dist/vector-tier.d.ts +6 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +32 -7
- package/dist/vector-tier.js.map +1 -1
- package/package.json +7 -6
- package/sbom.json +772 -112
- package/src/amendment-trace.ts +12 -1
- package/src/codex-invoke.ts +138 -0
- package/src/compounding.ts +300 -1
- package/src/discrimination-gate.ts +183 -19
- package/src/eta.ts +590 -0
- package/src/event-chain.ts +41 -0
- package/src/feature-adr-checkpoints.ts +1 -1
- package/src/feature-adr-decision-recall.ts +652 -0
- package/src/feature-adr-landing.ts +109 -0
- package/src/feature-adr-routing.ts +7 -11
- package/src/guard-promotion.ts +245 -4
- package/src/guard-volume.ts +752 -0
- package/src/guard.ts +110 -4
- package/src/index.ts +73 -6
- package/src/integration-apply.ts +332 -0
- package/src/integration-evidence.ts +89 -0
- package/src/integration-probe-worker.ts +310 -0
- package/src/integration-receipts/claude-code/mcp/2.1.235.json +35 -0
- package/src/integrations-verify.ts +258 -0
- package/src/lesson-generalization.ts +115 -0
- package/src/mutation-gate.ts +165 -5
- package/src/operations.ts +207 -7
- package/src/patterns.ts +252 -43
- package/src/plugin.ts +27 -5
- package/src/recommend.ts +116 -46
- package/src/registry.ts +144 -11
- package/src/run-records.ts +23 -0
- package/src/score.ts +361 -3
- package/src/setup.ts +20 -17
- package/src/skill-selection.ts +111 -0
- package/src/stem.ts +87 -0
- package/src/target-integrations.ts +225 -0
- package/src/telemetry-vocabulary.ts +36 -0
- package/src/vector-tier.ts +44 -14
package/src/patterns.ts
CHANGED
|
@@ -32,6 +32,13 @@ import type { VectorEntry } from './vector-tier.js';
|
|
|
32
32
|
import { rankLessonsByDelta, type LessonHistory } from './safla-delta.js';
|
|
33
33
|
import { withStoreLock, withStoreLockSync, StoreLockTimeoutError, StoreLockCompromisedError } from './store-lock.js';
|
|
34
34
|
import { describeNativeDep, exerciseSqliteOpen, probeNativeDep } from './native-dep-probe.js';
|
|
35
|
+
import {
|
|
36
|
+
lessonPairIdOf,
|
|
37
|
+
mergeLessonFormHits,
|
|
38
|
+
validateClassTemplate,
|
|
39
|
+
type LessonForm,
|
|
40
|
+
type LessonMatchedForm,
|
|
41
|
+
} from './lesson-generalization.js';
|
|
35
42
|
|
|
36
43
|
/** A learned pattern as written by `dz teach` to `.dz/patterns.jsonl`. */
|
|
37
44
|
export interface PatternRecord {
|
|
@@ -47,6 +54,12 @@ export interface PatternRecord {
|
|
|
47
54
|
readonly ts: string;
|
|
48
55
|
/** Origin of the record (e.g. "dz-teach"). */
|
|
49
56
|
readonly source: string;
|
|
57
|
+
/** Durable role in an optional linked lesson pair. */
|
|
58
|
+
readonly lessonForm?: LessonForm;
|
|
59
|
+
/** Stable identity shared by the specific and class rows. */
|
|
60
|
+
readonly lessonPairId?: string;
|
|
61
|
+
/** Read-model companion; derived from the linked class row, never persisted on this row. */
|
|
62
|
+
readonly classForm?: string;
|
|
50
63
|
}
|
|
51
64
|
|
|
52
65
|
/** A session lifecycle event as written by the session hooks to `.dz/sessions.jsonl`. */
|
|
@@ -371,6 +384,25 @@ function sqlitePath(projectRoot: string): string {
|
|
|
371
384
|
return join(projectRoot, '.dz', 'memory', 'patterns.sqlite');
|
|
372
385
|
}
|
|
373
386
|
|
|
387
|
+
function classFormMarkerPath(projectRoot: string): string {
|
|
388
|
+
return join(projectRoot, '.dz', 'memory', '.class-forms-present');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function ensureClassFormMarker(projectRoot: string): void {
|
|
392
|
+
const marker = classFormMarkerPath(projectRoot);
|
|
393
|
+
if (existsSync(marker)) return;
|
|
394
|
+
mkdirSync(dirname(marker), { recursive: true });
|
|
395
|
+
try {
|
|
396
|
+
writeFileSync(marker, 'lesson-class-forms/1\n', { flag: 'wx' });
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function classFormIndexPresent(projectRoot: string): boolean {
|
|
403
|
+
return existsSync(classFormMarkerPath(projectRoot));
|
|
404
|
+
}
|
|
405
|
+
|
|
374
406
|
/**
|
|
375
407
|
* Open the SQLite store backend if the configured mode allows it AND the native
|
|
376
408
|
* `better-sqlite3` is loadable. Returns `undefined` to fall back to JSON (the
|
|
@@ -404,7 +436,11 @@ function tryOpenSqlite(projectRoot: string, mode: SqliteBackendMode): SqliteBack
|
|
|
404
436
|
* identity, keeping migration/import an idempotent upsert.
|
|
405
437
|
*/
|
|
406
438
|
function patternIdentity(p: PatternRecord): string {
|
|
407
|
-
|
|
439
|
+
const legacy = `${p.pattern}|${p.ts}|${p.reward}|${p.domain}|${p.type}`;
|
|
440
|
+
// Existing rows must retain their historical ids; only a complete pair identity changes the key.
|
|
441
|
+
return p.lessonPairId !== undefined && p.lessonForm !== undefined
|
|
442
|
+
? `${legacy}|${p.lessonForm}|${p.lessonPairId}`
|
|
443
|
+
: legacy;
|
|
408
444
|
}
|
|
409
445
|
|
|
410
446
|
/** Deterministic store id derived from the full-content {@link patternIdentity}. */
|
|
@@ -434,6 +470,9 @@ const PATTERN_TYPES: ReadonlySet<string> = new Set(['rule', 'success-pattern', '
|
|
|
434
470
|
|
|
435
471
|
/** Anti-corruption mapping: harness `PatternRecord` → canonical `MemoryRecord`. */
|
|
436
472
|
export function patternToRecord(p: PatternRecord): MemoryRecord {
|
|
473
|
+
const pairMetadata = p.lessonPairId !== undefined && p.lessonForm !== undefined
|
|
474
|
+
? { lessonForm: p.lessonForm, lessonPairId: p.lessonPairId }
|
|
475
|
+
: {};
|
|
437
476
|
return {
|
|
438
477
|
id: recordId(p),
|
|
439
478
|
skillId: '', // taught patterns are free-text, not tied to one skill
|
|
@@ -447,12 +486,18 @@ export function patternToRecord(p: PatternRecord): MemoryRecord {
|
|
|
447
486
|
// the learn-loop write path.
|
|
448
487
|
outcome: PATTERN_TYPES.has(p.type) ? p.type : 'lesson-learned',
|
|
449
488
|
timestamp: p.ts,
|
|
450
|
-
metadata: { domain: p.domain ?? 'general', source: p.source ?? 'dz-teach' },
|
|
489
|
+
metadata: { domain: p.domain ?? 'general', source: p.source ?? 'dz-teach', ...pairMetadata },
|
|
451
490
|
};
|
|
452
491
|
}
|
|
453
492
|
|
|
454
493
|
/** Anti-corruption mapping: canonical `MemoryRecord` → harness `PatternRecord`. */
|
|
455
494
|
export function recordToPattern(r: MemoryRecord): PatternRecord {
|
|
495
|
+
const lessonForm = r.metadata?.['lessonForm'];
|
|
496
|
+
const lessonPairId = r.metadata?.['lessonPairId'];
|
|
497
|
+
const pair: { lessonForm?: LessonForm; lessonPairId?: string } = (lessonForm === 'specific' || lessonForm === 'class')
|
|
498
|
+
&& typeof lessonPairId === 'string' && lessonPairId !== ''
|
|
499
|
+
? { lessonForm, lessonPairId }
|
|
500
|
+
: {};
|
|
456
501
|
return {
|
|
457
502
|
pattern: r.text,
|
|
458
503
|
type: PATTERN_TYPES.has(r.outcome) ? (r.outcome as PatternRecord['type']) : 'lesson-learned',
|
|
@@ -460,6 +505,7 @@ export function recordToPattern(r: MemoryRecord): PatternRecord {
|
|
|
460
505
|
domain: r.metadata?.['domain'] ?? 'general',
|
|
461
506
|
ts: r.timestamp,
|
|
462
507
|
source: r.metadata?.['source'] ?? 'dz-teach',
|
|
508
|
+
...pair,
|
|
463
509
|
};
|
|
464
510
|
}
|
|
465
511
|
|
|
@@ -652,6 +698,10 @@ async function reinforcePatternLocked(projectRoot: string, dzIdOrText: string, o
|
|
|
652
698
|
const records = loadStoreRecords(projectRoot);
|
|
653
699
|
const rec = records.find((r) => r.id === dzIdOrText || r.text === dzIdOrText);
|
|
654
700
|
if (rec === undefined) return { ok: false, error: `no learned pattern matches ${JSON.stringify(dzIdOrText)}` };
|
|
701
|
+
const pairId = typeof rec.metadata?.['lessonPairId'] === 'string' ? rec.metadata['lessonPairId'] : undefined;
|
|
702
|
+
const targets = pairId === undefined
|
|
703
|
+
? [rec]
|
|
704
|
+
: records.filter((record) => record.metadata?.['lessonPairId'] === pairId);
|
|
655
705
|
const prev = readReinforcementState(rec);
|
|
656
706
|
const ts = opts.ts ?? new Date().toISOString();
|
|
657
707
|
const observed = opts.reward !== undefined && Number.isFinite(opts.reward) ? Math.max(0, Math.min(1, opts.reward)) : rec.score;
|
|
@@ -667,17 +717,15 @@ async function reinforcePatternLocked(projectRoot: string, dzIdOrText: string, o
|
|
|
667
717
|
// EXPOSURE (a recall-hit sample) is not confirmation — stats update, quarantine stays (the
|
|
668
718
|
// no-promotion-by-exposure invariant; found by cross-model QE as a live hole: recall-hit
|
|
669
719
|
// flushes were routed through this same function and silently promoted every viewed lesson).
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
720
|
+
for (const target of targets) {
|
|
721
|
+
const promotedMeta = { ...(target.metadata ?? {}), ...encodeReinforcementState(nextState) };
|
|
722
|
+
if (opts.exposure !== true) {
|
|
723
|
+
delete promotedMeta['qStatus'];
|
|
724
|
+
delete promotedMeta['quarantinedAt'];
|
|
725
|
+
}
|
|
726
|
+
const put = await putStoreRecord(projectRoot, { ...target, metadata: promotedMeta });
|
|
727
|
+
if ('error' in put) return { ok: false, dzId: rec.id, error: put.error };
|
|
674
728
|
}
|
|
675
|
-
const next: MemoryRecord = {
|
|
676
|
-
...rec,
|
|
677
|
-
metadata: promotedMeta,
|
|
678
|
-
};
|
|
679
|
-
const put = await putStoreRecord(projectRoot, next);
|
|
680
|
-
if ('error' in put) return { ok: false, dzId: rec.id, error: put.error };
|
|
681
729
|
try {
|
|
682
730
|
appendFileSync(join(projectRoot, '.dz', 'sessions.jsonl'), JSON.stringify({ event: 'reinforce', ts, dzId: rec.id, uses: nextState.uses }) + '\n');
|
|
683
731
|
} catch { /* best-effort */ }
|
|
@@ -687,14 +735,18 @@ async function reinforcePatternLocked(projectRoot: string, dzIdOrText: string, o
|
|
|
687
735
|
export async function updateReinforcementState(projectRoot: string, dzId: string, state: ReinforcementState): Promise<ReinforcePatternResult> {
|
|
688
736
|
try {
|
|
689
737
|
return await withStoreLock(projectRoot, async () => {
|
|
690
|
-
const
|
|
738
|
+
const records = loadStoreRecords(projectRoot);
|
|
739
|
+
const rec = records.find((r) => r.id === dzId);
|
|
691
740
|
if (rec === undefined) return { ok: false, error: `no learned pattern matches ${JSON.stringify(dzId)}` };
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
741
|
+
const pairId = typeof rec.metadata?.['lessonPairId'] === 'string' ? rec.metadata['lessonPairId'] : undefined;
|
|
742
|
+
const targets = pairId === undefined ? [rec] : records.filter((r) => r.metadata?.['lessonPairId'] === pairId);
|
|
743
|
+
for (const target of targets) {
|
|
744
|
+
const put = await putStoreRecord(projectRoot, {
|
|
745
|
+
...target,
|
|
746
|
+
metadata: { ...(target.metadata ?? {}), ...encodeReinforcementState(state) },
|
|
747
|
+
});
|
|
748
|
+
if ('error' in put) return { ok: false, dzId, error: put.error };
|
|
749
|
+
}
|
|
698
750
|
return { ok: true, dzId, uses: state.uses };
|
|
699
751
|
});
|
|
700
752
|
} catch (err) {
|
|
@@ -724,16 +776,23 @@ export async function promotePatterns(projectRoot: string, dzIds: readonly strin
|
|
|
724
776
|
const promoted: string[] = [];
|
|
725
777
|
const notFound: string[] = [];
|
|
726
778
|
const notQuarantined: string[] = [];
|
|
779
|
+
const handled = new Set<string>();
|
|
727
780
|
for (const id of dzIds) {
|
|
728
781
|
const rec = records.find((r) => r.id === id);
|
|
729
782
|
if (rec === undefined) { notFound.push(id); continue; }
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
783
|
+
const pairId = typeof rec.metadata?.['lessonPairId'] === 'string' ? rec.metadata['lessonPairId'] : undefined;
|
|
784
|
+
const targets = pairId === undefined ? [rec] : records.filter((r) => r.metadata?.['lessonPairId'] === pairId);
|
|
785
|
+
const pending = targets.filter((target) => !handled.has(target.id) && readQuarantineState(target).quarantined);
|
|
786
|
+
if (pending.length === 0) { notQuarantined.push(id); continue; }
|
|
787
|
+
for (const target of pending) {
|
|
788
|
+
const meta = { ...(target.metadata ?? {}) };
|
|
789
|
+
delete meta['qStatus'];
|
|
790
|
+
delete meta['quarantinedAt'];
|
|
791
|
+
const put = await putStoreRecord(projectRoot, { ...target, metadata: meta });
|
|
792
|
+
if ('error' in put) return { ok: false, promoted, notFound, notQuarantined, error: put.error };
|
|
793
|
+
handled.add(target.id);
|
|
794
|
+
promoted.push(target.id);
|
|
795
|
+
}
|
|
737
796
|
}
|
|
738
797
|
return { ok: true, promoted, notFound, notQuarantined };
|
|
739
798
|
});
|
|
@@ -969,6 +1028,7 @@ function migrationRecords(projectRoot: string): MemoryRecord[] {
|
|
|
969
1028
|
* JSON file is never deleted. Returns the total record count after the write.
|
|
970
1029
|
*/
|
|
971
1030
|
export async function recordPattern(projectRoot: string, p: PatternRecord, opts: { quarantine?: boolean } = {}): Promise<number> {
|
|
1031
|
+
if (p.lessonForm === 'class') ensureClassFormMarker(projectRoot);
|
|
972
1032
|
const { sqliteBackend } = readLearningConfig(projectRoot);
|
|
973
1033
|
// lesson-quarantine: a fresh lesson is a HYPOTHESIS — mark it when the feature is on. Folded
|
|
974
1034
|
// legacy records are NEVER marked (they predate the feature: grandfathered as promoted).
|
|
@@ -1001,6 +1061,61 @@ export async function recordPattern(projectRoot: string, p: PatternRecord, opts:
|
|
|
1001
1061
|
});
|
|
1002
1062
|
}
|
|
1003
1063
|
|
|
1064
|
+
export interface RecordLessonFormsResult {
|
|
1065
|
+
readonly count: number;
|
|
1066
|
+
readonly specific: 'stored';
|
|
1067
|
+
readonly class: 'absent' | 'stored' | 'rejected' | 'failed';
|
|
1068
|
+
readonly reason?: string;
|
|
1069
|
+
readonly records: readonly PatternRecord[];
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
export async function recordLessonForms(
|
|
1073
|
+
projectRoot: string,
|
|
1074
|
+
specific: PatternRecord,
|
|
1075
|
+
classTemplate?: string,
|
|
1076
|
+
opts: {
|
|
1077
|
+
readonly quarantine?: boolean;
|
|
1078
|
+
readonly writeRecord?: (root: string, record: PatternRecord, options: { quarantine?: boolean }) => Promise<number>;
|
|
1079
|
+
} = {},
|
|
1080
|
+
): Promise<RecordLessonFormsResult> {
|
|
1081
|
+
const persist = opts.writeRecord ?? recordPattern;
|
|
1082
|
+
const persistOptions = opts.quarantine === true ? { quarantine: true } : {};
|
|
1083
|
+
const proposed = classTemplate?.trim();
|
|
1084
|
+
if (proposed === undefined || proposed === '') {
|
|
1085
|
+
const count = await persist(projectRoot, specific, persistOptions);
|
|
1086
|
+
return { count, specific: 'stored', class: 'absent', records: [specific] };
|
|
1087
|
+
}
|
|
1088
|
+
const validation = proposed === specific.pattern
|
|
1089
|
+
? { ok: true as const }
|
|
1090
|
+
: validateClassTemplate(specific.pattern, proposed);
|
|
1091
|
+
if (!validation.ok) {
|
|
1092
|
+
const count = await persist(projectRoot, specific, persistOptions);
|
|
1093
|
+
return { count, specific: 'stored', class: 'rejected', reason: validation.reason, records: [specific] };
|
|
1094
|
+
}
|
|
1095
|
+
const lessonPairId = lessonPairIdOf(specific.pattern, proposed, specific.ts);
|
|
1096
|
+
const specificRow: PatternRecord = { ...specific, lessonForm: 'specific', lessonPairId };
|
|
1097
|
+
const classRow: PatternRecord = {
|
|
1098
|
+
...specific,
|
|
1099
|
+
pattern: proposed,
|
|
1100
|
+
source: 'dz-teach-class',
|
|
1101
|
+
lessonForm: 'class',
|
|
1102
|
+
lessonPairId,
|
|
1103
|
+
};
|
|
1104
|
+
const firstCount = await persist(projectRoot, specificRow, persistOptions);
|
|
1105
|
+
try {
|
|
1106
|
+
const count = await persist(projectRoot, classRow, persistOptions);
|
|
1107
|
+
return { count, specific: 'stored', class: 'stored', records: [specificRow, classRow] };
|
|
1108
|
+
} catch (error) {
|
|
1109
|
+
return {
|
|
1110
|
+
count: firstCount,
|
|
1111
|
+
specific: 'stored',
|
|
1112
|
+
class: 'failed',
|
|
1113
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1114
|
+
records: [specificRow],
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1004
1119
|
/** Outcome of a {@link recallPatterns} ranked search (or a hybrid merge — see `vector-tier.ts`). */
|
|
1005
1120
|
export interface RecallHit {
|
|
1006
1121
|
/** The matched pattern. */
|
|
@@ -1013,6 +1128,76 @@ export interface RecallHit {
|
|
|
1013
1128
|
readonly backend: 'sqlite' | 'json' | 'vector' | 'both';
|
|
1014
1129
|
/** lesson-quarantine: set (true) only for a quarantined hit — display marks it ⚠q and ranking damps/sinks it. */
|
|
1015
1130
|
readonly quarantined?: boolean;
|
|
1131
|
+
/** Which stored formulation caused this logical lesson to rank. */
|
|
1132
|
+
readonly matchedForm?: LessonMatchedForm;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
export interface RecallPatternsOptions {
|
|
1136
|
+
readonly onClassDegraded?: (message: string) => void;
|
|
1137
|
+
readonly classMatcher?: typeof validateClassTemplate;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function mergeRecallRows(
|
|
1141
|
+
records: readonly MemoryRecord[],
|
|
1142
|
+
ranked: readonly MemoryRecord[],
|
|
1143
|
+
backend: 'sqlite' | 'json',
|
|
1144
|
+
query: string,
|
|
1145
|
+
limit: number,
|
|
1146
|
+
opts: RecallPatternsOptions,
|
|
1147
|
+
): RecallHit[] {
|
|
1148
|
+
const pairRows = new Map<string, { specific?: MemoryRecord; classRow?: MemoryRecord }>();
|
|
1149
|
+
for (const record of records) {
|
|
1150
|
+
const pattern = recordToPattern(record);
|
|
1151
|
+
if (pattern.lessonPairId === undefined || pattern.lessonForm === undefined) continue;
|
|
1152
|
+
const pair = pairRows.get(pattern.lessonPairId) ?? {};
|
|
1153
|
+
if (pattern.lessonForm === 'specific') pair.specific = record;
|
|
1154
|
+
else pair.classRow = record;
|
|
1155
|
+
pairRows.set(pattern.lessonPairId, pair);
|
|
1156
|
+
}
|
|
1157
|
+
const rankedIds = new Set(ranked.map((record) => record.id));
|
|
1158
|
+
const specificHits: Array<{ key: string; value: RecallHit; matchedForm: 'specific' }> = [];
|
|
1159
|
+
const rankedClassRows: MemoryRecord[] = [];
|
|
1160
|
+
const logicalHit = (record: MemoryRecord): RecallHit => {
|
|
1161
|
+
const source = recordToPattern(record);
|
|
1162
|
+
const pair = source.lessonPairId === undefined ? undefined : pairRows.get(source.lessonPairId);
|
|
1163
|
+
const specificRecord = source.lessonForm === 'class' ? pair?.specific : record;
|
|
1164
|
+
const classRecord = source.lessonForm === 'class' ? record : pair?.classRow;
|
|
1165
|
+
const base = recordToPattern(specificRecord ?? record);
|
|
1166
|
+
const pattern = classRecord === undefined ? base : { ...base, classForm: classRecord.text };
|
|
1167
|
+
const quarantined = readQuarantineState(specificRecord ?? record).quarantined
|
|
1168
|
+
|| (classRecord !== undefined && readQuarantineState(classRecord).quarantined);
|
|
1169
|
+
return { pattern, backend, ...(quarantined ? { quarantined: true as const } : {}) };
|
|
1170
|
+
};
|
|
1171
|
+
for (const record of ranked) {
|
|
1172
|
+
const pattern = recordToPattern(record);
|
|
1173
|
+
const key = pattern.lessonPairId ?? record.id;
|
|
1174
|
+
if (pattern.lessonForm === 'class') rankedClassRows.push(record);
|
|
1175
|
+
else specificHits.push({ key, value: logicalHit(record), matchedForm: 'specific' });
|
|
1176
|
+
}
|
|
1177
|
+
const finish = (classHits: Array<{ key: string; value: RecallHit; matchedForm: 'class' }>): RecallHit[] =>
|
|
1178
|
+
sinkQuarantined(
|
|
1179
|
+
mergeLessonFormHits(specificHits, classHits, limit * 2).map((hit) => ({ ...hit.value, matchedForm: hit.matchedForm })),
|
|
1180
|
+
limit,
|
|
1181
|
+
);
|
|
1182
|
+
try {
|
|
1183
|
+
const classHits: Array<{ key: string; value: RecallHit; matchedForm: 'class' }> = rankedClassRows.map((record) => {
|
|
1184
|
+
const pattern = recordToPattern(record);
|
|
1185
|
+
return { key: pattern.lessonPairId ?? record.id, value: logicalHit(record), matchedForm: 'class' };
|
|
1186
|
+
});
|
|
1187
|
+
const classMatcher = opts.classMatcher ?? validateClassTemplate;
|
|
1188
|
+
for (const record of records) {
|
|
1189
|
+
const pattern = recordToPattern(record);
|
|
1190
|
+
if (pattern.lessonForm !== 'class' || rankedIds.has(record.id)) continue;
|
|
1191
|
+
if (classMatcher(query, pattern.pattern).ok) {
|
|
1192
|
+
classHits.push({ key: pattern.lessonPairId ?? record.id, value: logicalHit(record), matchedForm: 'class' });
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return finish(classHits);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
const message = `class-form search degraded: ${error instanceof Error ? error.message : String(error)}`;
|
|
1198
|
+
try { opts.onClassDegraded?.(message); } catch { /* an advisory sink may not suppress specific recall */ }
|
|
1199
|
+
return finish([]);
|
|
1200
|
+
}
|
|
1016
1201
|
}
|
|
1017
1202
|
|
|
1018
1203
|
/**
|
|
@@ -1021,36 +1206,48 @@ export interface RecallHit {
|
|
|
1021
1206
|
* recall — note it is *lexical*, not vector similarity (true embedding/HNSW recall
|
|
1022
1207
|
* lives in the `agentdb-memory` MCP skill, MCP-host only). Graceful: `[]` on failure.
|
|
1023
1208
|
*/
|
|
1024
|
-
export function recallPatterns(
|
|
1209
|
+
export function recallPatterns(
|
|
1210
|
+
projectRoot: string,
|
|
1211
|
+
query: string,
|
|
1212
|
+
limit = 10,
|
|
1213
|
+
opts: RecallPatternsOptions = {},
|
|
1214
|
+
): RecallHit[] {
|
|
1025
1215
|
const { sqliteBackend } = readLearningConfig(projectRoot);
|
|
1026
1216
|
if (sqliteBackend !== 'json' && existsSync(sqlitePath(projectRoot))) {
|
|
1027
1217
|
try {
|
|
1028
1218
|
const db = SqliteBackend.open(sqlitePath(projectRoot));
|
|
1029
1219
|
try {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1220
|
+
if (!classFormIndexPresent(projectRoot)) {
|
|
1221
|
+
return sinkQuarantined(
|
|
1222
|
+
db.querySync({ text: query, limit: limit * 2 }).map((r) => ({
|
|
1223
|
+
pattern: recordToPattern(r),
|
|
1224
|
+
backend: 'sqlite' as const,
|
|
1225
|
+
...(readQuarantineState(r).quarantined ? { quarantined: true as const } : {}),
|
|
1226
|
+
})),
|
|
1227
|
+
limit,
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
const records = db.allSync();
|
|
1231
|
+
return mergeRecallRows(records, db.querySync({ text: query, limit: limit * 4 }), 'sqlite', query, limit, opts);
|
|
1038
1232
|
} finally {
|
|
1039
1233
|
db.close();
|
|
1040
1234
|
}
|
|
1041
1235
|
} catch { /* fall through to JSON */ }
|
|
1042
1236
|
}
|
|
1043
1237
|
try {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
.map((r) => ({
|
|
1238
|
+
const backend = JsonFileBackend.openSync(storePath(projectRoot));
|
|
1239
|
+
if (!classFormIndexPresent(projectRoot)) {
|
|
1240
|
+
return sinkQuarantined(
|
|
1241
|
+
backend.querySync({ text: query, limit: limit * 2 }).map((r) => ({
|
|
1048
1242
|
pattern: recordToPattern(r),
|
|
1049
1243
|
backend: 'json' as const,
|
|
1050
1244
|
...(readQuarantineState(r).quarantined ? { quarantined: true as const } : {}),
|
|
1051
1245
|
})),
|
|
1052
|
-
|
|
1053
|
-
|
|
1246
|
+
limit,
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
const records = backend.allSync();
|
|
1250
|
+
return mergeRecallRows(records, backend.querySync({ text: query, limit: limit * 4 }), 'json', query, limit, opts);
|
|
1054
1251
|
} catch {
|
|
1055
1252
|
return [];
|
|
1056
1253
|
}
|
|
@@ -1430,6 +1627,18 @@ export interface RemovePatternsResult {
|
|
|
1430
1627
|
*/
|
|
1431
1628
|
export function removePatternsByIds(projectRoot: string, ids: ReadonlySet<string>): RemovePatternsResult {
|
|
1432
1629
|
if (ids.size === 0) return { removed: 0 };
|
|
1630
|
+
const expandedIds = new Set(ids);
|
|
1631
|
+
const records = loadStoreRecords(projectRoot);
|
|
1632
|
+
const selectedPairs = new Set(
|
|
1633
|
+
records
|
|
1634
|
+
.filter((record) => ids.has(record.id) && typeof record.metadata?.['lessonPairId'] === 'string')
|
|
1635
|
+
.map((record) => record.metadata?.['lessonPairId'] as string),
|
|
1636
|
+
);
|
|
1637
|
+
for (const record of records) {
|
|
1638
|
+
if (typeof record.metadata?.['lessonPairId'] === 'string' && selectedPairs.has(record.metadata['lessonPairId'])) {
|
|
1639
|
+
expandedIds.add(record.id);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1433
1642
|
let removed = 0;
|
|
1434
1643
|
const errors: string[] = [];
|
|
1435
1644
|
|
|
@@ -1440,7 +1649,7 @@ export function removePatternsByIds(projectRoot: string, ids: ReadonlySet<string
|
|
|
1440
1649
|
const db = SqliteBackend.open(sqlitePath(projectRoot));
|
|
1441
1650
|
try {
|
|
1442
1651
|
for (const r of db.allSync()) {
|
|
1443
|
-
if (
|
|
1652
|
+
if (expandedIds.has(r.id)) {
|
|
1444
1653
|
db.removeSync(r.id);
|
|
1445
1654
|
removed += 1;
|
|
1446
1655
|
}
|
|
@@ -1463,7 +1672,7 @@ export function removePatternsByIds(projectRoot: string, ids: ReadonlySet<string
|
|
|
1463
1672
|
const backend = JsonFileBackend.openSync(storePath(projectRoot));
|
|
1464
1673
|
let localRemoved = 0;
|
|
1465
1674
|
for (const r of backend.allSync()) {
|
|
1466
|
-
if (
|
|
1675
|
+
if (expandedIds.has(r.id)) {
|
|
1467
1676
|
backend.removeSync(r.id);
|
|
1468
1677
|
localRemoved += 1;
|
|
1469
1678
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -27,6 +27,13 @@ export interface PluginManifest {
|
|
|
27
27
|
readonly skills: readonly string[];
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** id→path index built from the registry, so the generator never guesses a layout. */
|
|
31
|
+
function skillPathIndex(registry: { entries: readonly { id: string; pack: string; path?: string }[] }): Map<string, string> {
|
|
32
|
+
const m = new Map<string, string>();
|
|
33
|
+
for (const e of registry.entries) if (e.path !== undefined && e.path !== '') m.set(`${e.pack}/${e.id}`, e.path);
|
|
34
|
+
return m;
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
/** Generate .claude-plugin/ directory from registry. */
|
|
31
38
|
export function generatePlugin(
|
|
32
39
|
projectRoot: string,
|
|
@@ -61,16 +68,26 @@ export function generatePlugin(
|
|
|
61
68
|
};
|
|
62
69
|
|
|
63
70
|
const skillPacks = [...packMap.entries()].map(([name, count]) => ({
|
|
71
|
+
// `pack` is the REAL directory name; `name` is only the display short form. Deriving the key
|
|
72
|
+
// back by re-adding the `skills-` prefix broke the moment the catalogue learned packs that
|
|
73
|
+
// never had one (health-advisor, keysarium, …): the lookup missed and their entries listed
|
|
74
|
+
// zero skills.
|
|
75
|
+
pack: name,
|
|
64
76
|
name: name.replace('skills-', ''),
|
|
65
77
|
skills: count,
|
|
66
78
|
description: packDescriptions[name] ?? `${count} skills`,
|
|
67
79
|
}));
|
|
68
80
|
|
|
81
|
+
const pathById = skillPathIndex(registry);
|
|
82
|
+
|
|
69
83
|
// Explicit skill paths for the full-suite plugin (source `./`): every skill
|
|
70
84
|
// dir across every pack, relative to the repo root. This is what makes
|
|
71
85
|
// `claude plugin details` report the real skill count instead of 0.
|
|
72
86
|
const allSkillPaths = [...packSkillIds.entries()]
|
|
73
|
-
|
|
87
|
+
// Prefer the path the catalogue recorded; `<pack>/<id>` is only a fallback for entries built
|
|
88
|
+
// before the field existed. Reconstructing it is what produced unresolvable paths for skills
|
|
89
|
+
// that live under `skills/` or `templates/.claude/skills/`.
|
|
90
|
+
.flatMap(([pack, ids]) => ids.map((id) => pathById.get(`${pack}/${id}`) ?? `packages/@dzhechkov/${pack}/${id}`))
|
|
74
91
|
.sort();
|
|
75
92
|
|
|
76
93
|
const pluginJson = {
|
|
@@ -103,12 +120,17 @@ export function generatePlugin(
|
|
|
103
120
|
...skillPacks.map((pack) => ({
|
|
104
121
|
name: `dz-${pack.name}`,
|
|
105
122
|
displayName: `DZ ${pack.name.charAt(0).toUpperCase() + pack.name.slice(1)} Skills`,
|
|
106
|
-
source: `packages/@dzhechkov
|
|
123
|
+
source: `packages/@dzhechkov/${pack.pack}`,
|
|
107
124
|
description: `${pack.skills} ${pack.name} skills — ${pack.description}`,
|
|
108
125
|
keywords: [pack.name],
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
126
|
+
// Listed relative to `source` — required for Claude Code to discover them. The path comes
|
|
127
|
+
// from the catalogue rather than from the assumption "skill dirs sit at the pack root",
|
|
128
|
+
// which holds for `skills-*` packs and for neither of the two other layouts.
|
|
129
|
+
skills: (packSkillIds.get(pack.pack) ?? []).slice().sort().map((id) => {
|
|
130
|
+
const full = pathById.get(`${pack.pack}/${id}`);
|
|
131
|
+
const base = `packages/@dzhechkov/${pack.pack}/`;
|
|
132
|
+
return full !== undefined && full.startsWith(base) ? `./${full.slice(base.length)}` : `./${id}`;
|
|
133
|
+
}),
|
|
112
134
|
})),
|
|
113
135
|
],
|
|
114
136
|
};
|