@soleri/core 9.2.0 → 9.3.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/data/flows/build.flow.yaml +8 -9
- package/data/flows/deliver.flow.yaml +9 -10
- package/data/flows/design.flow.yaml +3 -4
- package/data/flows/enhance.flow.yaml +5 -6
- package/data/flows/explore.flow.yaml +3 -4
- package/data/flows/fix.flow.yaml +5 -6
- package/data/flows/plan.flow.yaml +4 -5
- package/data/flows/review.flow.yaml +3 -4
- package/dist/curator/curator.d.ts.map +1 -1
- package/dist/curator/curator.js +98 -22
- package/dist/curator/curator.js.map +1 -1
- package/dist/engine/bin/soleri-engine.js.map +1 -1
- package/dist/engine/module-manifest.d.ts.map +1 -1
- package/dist/engine/module-manifest.js +21 -1
- package/dist/engine/module-manifest.js.map +1 -1
- package/dist/engine/register-engine.d.ts.map +1 -1
- package/dist/engine/register-engine.js +25 -1
- package/dist/engine/register-engine.js.map +1 -1
- package/dist/flows/gate-evaluator.js.map +1 -1
- package/dist/operator/operator-profile.d.ts.map +1 -1
- package/dist/operator/operator-profile.js +11 -5
- package/dist/operator/operator-profile.js.map +1 -1
- package/dist/operator/operator-signals.d.ts.map +1 -1
- package/dist/operator/operator-signals.js.map +1 -1
- package/dist/planning/evidence-collector.js.map +1 -1
- package/dist/planning/gap-passes.d.ts.map +1 -1
- package/dist/planning/gap-passes.js +23 -6
- package/dist/planning/gap-passes.js.map +1 -1
- package/dist/planning/gap-patterns.d.ts.map +1 -1
- package/dist/planning/gap-patterns.js +57 -11
- package/dist/planning/gap-patterns.js.map +1 -1
- package/dist/planning/github-projection.d.ts.map +1 -1
- package/dist/planning/github-projection.js +39 -20
- package/dist/planning/github-projection.js.map +1 -1
- package/dist/planning/impact-analyzer.d.ts.map +1 -1
- package/dist/planning/impact-analyzer.js +20 -18
- package/dist/planning/impact-analyzer.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +22 -9
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner.d.ts.map +1 -1
- package/dist/planning/planner.js +60 -17
- package/dist/planning/planner.js.map +1 -1
- package/dist/planning/rationalization-detector.d.ts.map +1 -1
- package/dist/planning/rationalization-detector.js.map +1 -1
- package/dist/planning/reconciliation-engine.d.ts.map +1 -1
- package/dist/planning/reconciliation-engine.js.map +1 -1
- package/dist/planning/task-verifier.d.ts.map +1 -1
- package/dist/planning/task-verifier.js +14 -6
- package/dist/planning/task-verifier.js.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +2 -1
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/branching-ops.d.ts +12 -0
- package/dist/runtime/branching-ops.d.ts.map +1 -0
- package/dist/runtime/branching-ops.js +100 -0
- package/dist/runtime/branching-ops.js.map +1 -0
- package/dist/runtime/context-health.d.ts.map +1 -1
- package/dist/runtime/context-health.js.map +1 -1
- package/dist/runtime/facades/branching-facade.d.ts +7 -0
- package/dist/runtime/facades/branching-facade.d.ts.map +1 -0
- package/dist/runtime/facades/branching-facade.js +8 -0
- package/dist/runtime/facades/branching-facade.js.map +1 -0
- package/dist/runtime/facades/chat-service-ops.d.ts.map +1 -1
- package/dist/runtime/facades/chat-service-ops.js +3 -1
- package/dist/runtime/facades/chat-service-ops.js.map +1 -1
- package/dist/runtime/facades/chat-transport-ops.d.ts.map +1 -1
- package/dist/runtime/facades/chat-transport-ops.js.map +1 -1
- package/dist/runtime/facades/index.d.ts.map +1 -1
- package/dist/runtime/facades/index.js +42 -0
- package/dist/runtime/facades/index.js.map +1 -1
- package/dist/runtime/facades/intake-facade.d.ts +9 -0
- package/dist/runtime/facades/intake-facade.d.ts.map +1 -0
- package/dist/runtime/facades/intake-facade.js +11 -0
- package/dist/runtime/facades/intake-facade.js.map +1 -0
- package/dist/runtime/facades/links-facade.d.ts +9 -0
- package/dist/runtime/facades/links-facade.d.ts.map +1 -0
- package/dist/runtime/facades/links-facade.js +10 -0
- package/dist/runtime/facades/links-facade.js.map +1 -0
- package/dist/runtime/facades/operator-facade.d.ts.map +1 -1
- package/dist/runtime/facades/operator-facade.js.map +1 -1
- package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
- package/dist/runtime/facades/plan-facade.js +4 -1
- package/dist/runtime/facades/plan-facade.js.map +1 -1
- package/dist/runtime/facades/tier-facade.d.ts +7 -0
- package/dist/runtime/facades/tier-facade.d.ts.map +1 -0
- package/dist/runtime/facades/tier-facade.js +8 -0
- package/dist/runtime/facades/tier-facade.js.map +1 -0
- package/dist/runtime/facades/vault-facade.d.ts +9 -1
- package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
- package/dist/runtime/facades/vault-facade.js +44 -187
- package/dist/runtime/facades/vault-facade.js.map +1 -1
- package/dist/runtime/github-integration.d.ts.map +1 -1
- package/dist/runtime/github-integration.js +11 -4
- package/dist/runtime/github-integration.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +32 -10
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
- package/dist/runtime/planning-extra-ops.js.map +1 -1
- package/dist/runtime/runtime.d.ts.map +1 -1
- package/dist/runtime/runtime.js +3 -1
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/runtime/session-briefing.d.ts.map +1 -1
- package/dist/runtime/session-briefing.js +5 -1
- package/dist/runtime/session-briefing.js.map +1 -1
- package/dist/runtime/tier-ops.d.ts +13 -0
- package/dist/runtime/tier-ops.d.ts.map +1 -0
- package/dist/runtime/tier-ops.js +110 -0
- package/dist/runtime/tier-ops.js.map +1 -0
- package/dist/skills/sync-skills.d.ts.map +1 -1
- package/dist/skills/sync-skills.js +1 -1
- package/dist/skills/sync-skills.js.map +1 -1
- package/dist/vault/linking.d.ts.map +1 -1
- package/dist/vault/linking.js +41 -5
- package/dist/vault/linking.js.map +1 -1
- package/dist/vault/vault-entries.d.ts.map +1 -1
- package/dist/vault/vault-entries.js +68 -26
- package/dist/vault/vault-entries.js.map +1 -1
- package/dist/vault/vault-maintenance.d.ts.map +1 -1
- package/dist/vault/vault-maintenance.js +6 -2
- package/dist/vault/vault-maintenance.js.map +1 -1
- package/dist/vault/vault-markdown-sync.d.ts.map +1 -1
- package/dist/vault/vault-markdown-sync.js.map +1 -1
- package/dist/vault/vault-memories.d.ts.map +1 -1
- package/dist/vault/vault-memories.js +3 -1
- package/dist/vault/vault-memories.js.map +1 -1
- package/dist/vault/vault-schema.js +36 -10
- package/dist/vault/vault-schema.js.map +1 -1
- package/dist/vault/vault.d.ts.map +1 -1
- package/dist/vault/vault.js +5 -1
- package/dist/vault/vault.js.map +1 -1
- package/package.json +7 -7
- package/src/agency/agency-manager.test.ts +60 -40
- package/src/agency/default-rules.test.ts +17 -9
- package/src/capabilities/registry.test.ts +2 -12
- package/src/chat/agent-loop.test.ts +33 -43
- package/src/chat/mcp-bridge.test.ts +7 -2
- package/src/claudemd/inject.test.ts +2 -12
- package/src/context/context-engine.test.ts +96 -51
- package/src/control/intent-router.test.ts +3 -3
- package/src/curator/classifier.test.ts +14 -8
- package/src/curator/contradiction-detector.test.ts +30 -5
- package/src/curator/curator.ts +278 -56
- package/src/curator/duplicate-detector.test.ts +77 -15
- package/src/curator/quality-gate.test.ts +71 -31
- package/src/curator/tag-manager.test.ts +12 -4
- package/src/domain-packs/knowledge-installer.test.ts +2 -10
- package/src/domain-packs/token-resolver.test.ts +1 -3
- package/src/domain-packs/types.test.ts +16 -2
- package/src/enforcement/registry.test.ts +2 -8
- package/src/engine/bin/soleri-engine.ts +3 -1
- package/src/engine/module-manifest.test.ts +5 -4
- package/src/engine/module-manifest.ts +21 -1
- package/src/engine/register-engine.test.ts +6 -1
- package/src/engine/register-engine.ts +26 -3
- package/src/errors/classify.test.ts +6 -2
- package/src/errors/retry.test.ts +1 -4
- package/src/facades/facade-factory.test.ts +110 -64
- package/src/flows/epilogue.test.ts +16 -10
- package/src/flows/gate-evaluator.test.ts +12 -6
- package/src/flows/gate-evaluator.ts +1 -3
- package/src/governance/governance.test.ts +137 -21
- package/src/health/health-registry.test.ts +8 -1
- package/src/intake/content-classifier.test.ts +121 -51
- package/src/intake/dedup-gate.test.ts +38 -22
- package/src/intake/intake-pipeline.test.ts +5 -3
- package/src/intake/text-ingester.test.ts +26 -20
- package/src/llm/key-pool.test.ts +1 -3
- package/src/llm/llm-client.test.ts +1 -4
- package/src/llm/oauth-discovery.test.ts +16 -16
- package/src/llm/utils.test.ts +62 -18
- package/src/logging/logger.test.ts +4 -1
- package/src/loop/loop-manager.test.ts +2 -6
- package/src/migrations/migration-runner.edge-cases.test.ts +2 -7
- package/src/operator/operator-profile-extended.test.ts +15 -5
- package/src/operator/operator-profile.test.ts +26 -8
- package/src/operator/operator-profile.ts +38 -22
- package/src/operator/operator-signals-extended.test.ts +35 -23
- package/src/operator/operator-signals.test.ts +6 -10
- package/src/operator/operator-signals.ts +2 -1
- package/src/operator/prompts/hook-precompact-operator-dispatch.md +10 -6
- package/src/operator/prompts/subagent-soft-signal-extractor.md +5 -0
- package/src/operator/prompts/subagent-synthesis-cognition.md +19 -10
- package/src/operator/prompts/subagent-synthesis-communication.md +13 -7
- package/src/operator/prompts/subagent-synthesis-technical.md +19 -9
- package/src/operator/prompts/subagent-synthesis-trust.md +27 -21
- package/src/persona/defaults.test.ts +1 -5
- package/src/planning/evidence-collector.test.ts +147 -38
- package/src/planning/evidence-collector.ts +1 -4
- package/src/planning/gap-analysis-alternatives.test.ts +41 -11
- package/src/planning/gap-passes.test.ts +215 -33
- package/src/planning/gap-passes.ts +115 -46
- package/src/planning/gap-patterns.test.ts +87 -13
- package/src/planning/gap-patterns.ts +114 -31
- package/src/planning/github-projection.test.ts +6 -1
- package/src/planning/github-projection.ts +41 -20
- package/src/planning/impact-analyzer.test.ts +10 -23
- package/src/planning/impact-analyzer.ts +33 -46
- package/src/planning/plan-lifecycle.test.ts +103 -36
- package/src/planning/plan-lifecycle.ts +49 -18
- package/src/planning/planner.test.ts +12 -2
- package/src/planning/planner.ts +198 -58
- package/src/planning/rationalization-detector.test.ts +5 -20
- package/src/planning/rationalization-detector.ts +14 -16
- package/src/planning/reconciliation-engine.test.ts +20 -3
- package/src/planning/reconciliation-engine.ts +1 -2
- package/src/planning/task-verifier.test.ts +59 -27
- package/src/planning/task-verifier.ts +15 -9
- package/src/playbooks/playbook-executor.test.ts +1 -3
- package/src/plugins/plugin-loader.test.ts +19 -14
- package/src/plugins/plugin-registry.test.ts +45 -33
- package/src/project/project-registry.test.ts +23 -12
- package/src/prompts/template-manager.test.ts +4 -1
- package/src/queue/job-queue.test.ts +10 -14
- package/src/runtime/admin-extra-ops.test.ts +5 -19
- package/src/runtime/admin-ops.test.ts +1 -3
- package/src/runtime/admin-setup-ops.test.ts +3 -4
- package/src/runtime/admin-setup-ops.ts +9 -2
- package/src/runtime/archive-ops.test.ts +4 -1
- package/src/runtime/branching-ops.test.ts +144 -0
- package/src/runtime/branching-ops.ts +107 -0
- package/src/runtime/capture-ops.test.ts +7 -21
- package/src/runtime/chain-ops.test.ts +16 -6
- package/src/runtime/claude-md-helpers.test.ts +1 -3
- package/src/runtime/context-health.test.ts +1 -3
- package/src/runtime/context-health.ts +1 -3
- package/src/runtime/curator-extra-ops.test.ts +3 -1
- package/src/runtime/domain-ops.test.ts +46 -36
- package/src/runtime/facades/admin-facade.test.ts +1 -4
- package/src/runtime/facades/archive-facade.test.ts +21 -7
- package/src/runtime/facades/brain-facade.test.ts +176 -72
- package/src/runtime/facades/branching-facade.test.ts +43 -0
- package/src/runtime/facades/branching-facade.ts +11 -0
- package/src/runtime/facades/chat-facade.test.ts +81 -28
- package/src/runtime/facades/chat-service-ops.test.ts +178 -73
- package/src/runtime/facades/chat-service-ops.ts +3 -1
- package/src/runtime/facades/chat-session-ops.test.ts +25 -10
- package/src/runtime/facades/chat-transport-ops.test.ts +101 -34
- package/src/runtime/facades/chat-transport-ops.ts +0 -1
- package/src/runtime/facades/context-facade.test.ts +19 -4
- package/src/runtime/facades/control-facade.test.ts +3 -3
- package/src/runtime/facades/index.ts +42 -0
- package/src/runtime/facades/intake-facade.test.ts +215 -0
- package/src/runtime/facades/intake-facade.ts +14 -0
- package/src/runtime/facades/links-facade.test.ts +203 -0
- package/src/runtime/facades/links-facade.ts +13 -0
- package/src/runtime/facades/loop-facade.test.ts +22 -5
- package/src/runtime/facades/memory-facade.test.ts +19 -5
- package/src/runtime/facades/operator-facade.test.ts +17 -4
- package/src/runtime/facades/operator-facade.ts +11 -3
- package/src/runtime/facades/orchestrate-facade.test.ts +7 -1
- package/src/runtime/facades/plan-facade.test.ts +29 -12
- package/src/runtime/facades/plan-facade.ts +7 -2
- package/src/runtime/facades/tier-facade.test.ts +47 -0
- package/src/runtime/facades/tier-facade.ts +11 -0
- package/src/runtime/facades/vault-facade.test.ts +174 -242
- package/src/runtime/facades/vault-facade.ts +55 -199
- package/src/runtime/github-integration.ts +11 -8
- package/src/runtime/grading-ops.test.ts +39 -8
- package/src/runtime/intake-ops.test.ts +69 -16
- package/src/runtime/loop-ops.test.ts +16 -6
- package/src/runtime/memory-cross-project-ops.test.ts +25 -14
- package/src/runtime/orchestrate-ops.ts +54 -27
- package/src/runtime/pack-ops.test.ts +23 -6
- package/src/runtime/planning-extra-ops.test.ts +17 -7
- package/src/runtime/planning-extra-ops.ts +3 -1
- package/src/runtime/playbook-ops.test.ts +26 -3
- package/src/runtime/plugin-ops.test.ts +83 -25
- package/src/runtime/project-ops.test.ts +26 -6
- package/src/runtime/runtime.ts +3 -1
- package/src/runtime/session-briefing.test.ts +183 -54
- package/src/runtime/session-briefing.ts +8 -2
- package/src/runtime/sync-ops.test.ts +3 -12
- package/src/runtime/telemetry-ops.test.ts +31 -6
- package/src/runtime/tier-ops.test.ts +159 -0
- package/src/runtime/tier-ops.ts +119 -0
- package/src/runtime/vault-extra-ops.test.ts +32 -8
- package/src/runtime/vault-sharing-ops.test.ts +1 -4
- package/src/skills/sync-skills.ts +2 -12
- package/src/transport/ws-server.test.ts +7 -4
- package/src/vault/__tests__/vault-characterization.test.ts +492 -81
- package/src/vault/linking.test.ts +50 -17
- package/src/vault/linking.ts +48 -7
- package/src/vault/obsidian-sync.test.ts +6 -3
- package/src/vault/scope-detector.test.ts +1 -3
- package/src/vault/vault-branching.test.ts +9 -7
- package/src/vault/vault-entries.ts +209 -65
- package/src/vault/vault-maintenance.ts +7 -12
- package/src/vault/vault-manager.test.ts +10 -10
- package/src/vault/vault-markdown-sync.ts +4 -1
- package/src/vault/vault-memories.ts +7 -7
- package/src/vault/vault-schema.ts +72 -15
- package/src/vault/vault.ts +55 -9
- package/src/brain/strength-scorer.ts +0 -404
- package/src/engine/index.ts +0 -21
- package/src/persona/index.ts +0 -9
- package/src/vault/vault-interfaces.ts +0 -56
package/src/curator/curator.ts
CHANGED
|
@@ -16,8 +16,15 @@ import type {
|
|
|
16
16
|
HealthAuditResult,
|
|
17
17
|
} from './types.js';
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import {
|
|
20
|
+
detectDuplicates as detectDuplicatesPure,
|
|
21
|
+
DEFAULT_DUPLICATE_THRESHOLD,
|
|
22
|
+
} from './duplicate-detector.js';
|
|
23
|
+
import {
|
|
24
|
+
findContradictions,
|
|
25
|
+
DEFAULT_CONTRADICTION_THRESHOLD,
|
|
26
|
+
type ContradictionCandidate,
|
|
27
|
+
} from './contradiction-detector.js';
|
|
21
28
|
import {
|
|
22
29
|
normalizeTag as normalizeTagPure,
|
|
23
30
|
normalizeAndDedup,
|
|
@@ -53,13 +60,20 @@ export class Curator {
|
|
|
53
60
|
const p = this.provider;
|
|
54
61
|
return {
|
|
55
62
|
getAlias(lower: string) {
|
|
56
|
-
return
|
|
63
|
+
return (
|
|
64
|
+
p.get<{ canonical: string }>('SELECT canonical FROM curator_tag_alias WHERE alias = ?', [
|
|
65
|
+
lower,
|
|
66
|
+
])?.canonical ?? null
|
|
67
|
+
);
|
|
57
68
|
},
|
|
58
69
|
insertCanonical(tag: string) {
|
|
59
70
|
p.run('INSERT OR IGNORE INTO curator_tag_canonical (tag) VALUES (?)', [tag]);
|
|
60
71
|
},
|
|
61
72
|
upsertAlias(alias: string, canonical: string) {
|
|
62
|
-
p.run('INSERT OR REPLACE INTO curator_tag_alias (alias, canonical) VALUES (?, ?)', [
|
|
73
|
+
p.run('INSERT OR REPLACE INTO curator_tag_alias (alias, canonical) VALUES (?, ?)', [
|
|
74
|
+
alias,
|
|
75
|
+
canonical,
|
|
76
|
+
]);
|
|
63
77
|
},
|
|
64
78
|
getCanonicalRows() {
|
|
65
79
|
return p.all<{ tag: string; description: string | null; alias_count: number }>(
|
|
@@ -67,7 +81,11 @@ export class Curator {
|
|
|
67
81
|
);
|
|
68
82
|
},
|
|
69
83
|
countTagUsage(tag: string) {
|
|
70
|
-
return
|
|
84
|
+
return (
|
|
85
|
+
p.get<{ count: number }>('SELECT COUNT(*) as count FROM entries WHERE tags LIKE ?', [
|
|
86
|
+
`%"${tag}"%`,
|
|
87
|
+
])?.count ?? 0
|
|
88
|
+
);
|
|
71
89
|
},
|
|
72
90
|
};
|
|
73
91
|
}
|
|
@@ -76,34 +94,59 @@ export class Curator {
|
|
|
76
94
|
|
|
77
95
|
getStatus(): CuratorStatus {
|
|
78
96
|
const tableCount = (table: string): number =>
|
|
79
|
-
(
|
|
97
|
+
(
|
|
98
|
+
this.provider.get<{ count: number }>(`SELECT COUNT(*) as count FROM ${table}`) ?? {
|
|
99
|
+
count: 0,
|
|
100
|
+
}
|
|
101
|
+
).count;
|
|
80
102
|
const lastGroomed = this.provider.get<{ ts: number | null }>(
|
|
81
103
|
'SELECT MAX(last_groomed_at) as ts FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
|
|
82
104
|
) ?? { ts: null };
|
|
83
105
|
return {
|
|
84
106
|
initialized: true,
|
|
85
|
-
tables: {
|
|
107
|
+
tables: {
|
|
108
|
+
entry_state: tableCount('curator_entry_state'),
|
|
109
|
+
tag_canonical: tableCount('curator_tag_canonical'),
|
|
110
|
+
tag_alias: tableCount('curator_tag_alias'),
|
|
111
|
+
changelog: tableCount('curator_changelog'),
|
|
112
|
+
contradictions: tableCount('curator_contradictions'),
|
|
113
|
+
},
|
|
86
114
|
lastGroomedAt: lastGroomed.ts,
|
|
87
115
|
};
|
|
88
116
|
}
|
|
89
117
|
|
|
90
118
|
// ─── Tags (delegates to tag-manager) ──────────────────────────
|
|
91
119
|
|
|
92
|
-
normalizeTag(tag: string): TagNormalizationResult {
|
|
120
|
+
normalizeTag(tag: string): TagNormalizationResult {
|
|
121
|
+
return normalizeTagPure(tag, this.tagStore);
|
|
122
|
+
}
|
|
93
123
|
|
|
94
124
|
normalizeTags(entryId: string): TagNormalizationResult[] {
|
|
95
125
|
const entry = this.vault.get(entryId);
|
|
96
126
|
if (!entry) return [];
|
|
97
127
|
const { results, dedupedTags, changed } = normalizeAndDedup(entry.tags, this.tagStore);
|
|
98
128
|
if (changed) {
|
|
99
|
-
this.provider.run('UPDATE entries SET tags = ?, updated_at = unixepoch() WHERE id = ?', [
|
|
100
|
-
|
|
129
|
+
this.provider.run('UPDATE entries SET tags = ?, updated_at = unixepoch() WHERE id = ?', [
|
|
130
|
+
JSON.stringify(dedupedTags),
|
|
131
|
+
entryId,
|
|
132
|
+
]);
|
|
133
|
+
this.logChange(
|
|
134
|
+
'normalize_tags',
|
|
135
|
+
entryId,
|
|
136
|
+
JSON.stringify(entry.tags),
|
|
137
|
+
JSON.stringify(dedupedTags),
|
|
138
|
+
'Tag normalization',
|
|
139
|
+
);
|
|
101
140
|
}
|
|
102
141
|
return results;
|
|
103
142
|
}
|
|
104
143
|
|
|
105
|
-
addTagAlias(alias: string, canonical: string): void {
|
|
106
|
-
|
|
144
|
+
addTagAlias(alias: string, canonical: string): void {
|
|
145
|
+
addTagAliasPure(alias, canonical, this.tagStore);
|
|
146
|
+
}
|
|
147
|
+
getCanonicalTags(): CanonicalTag[] {
|
|
148
|
+
return getCanonicalTagsPure(this.tagStore);
|
|
149
|
+
}
|
|
107
150
|
|
|
108
151
|
// ─── Duplicates (delegates to duplicate-detector) ─────────────
|
|
109
152
|
|
|
@@ -114,24 +157,45 @@ export class Curator {
|
|
|
114
157
|
// ─── Contradictions (delegates to contradiction-detector) ─────
|
|
115
158
|
|
|
116
159
|
detectContradictions(threshold?: number): Contradiction[] {
|
|
117
|
-
const searchFn = (title: string) =>
|
|
118
|
-
|
|
160
|
+
const searchFn = (title: string) =>
|
|
161
|
+
this.vault.search(title, { type: 'pattern', limit: 20 }).map((r) => r.entry);
|
|
162
|
+
return this.persistContradictions(
|
|
163
|
+
findContradictions(this.vault.list({ limit: 100000 }), threshold, searchFn),
|
|
164
|
+
);
|
|
119
165
|
}
|
|
120
166
|
|
|
121
167
|
getContradictions(status?: ContradictionStatus): Contradiction[] {
|
|
122
|
-
const query = status
|
|
123
|
-
|
|
168
|
+
const query = status
|
|
169
|
+
? 'SELECT * FROM curator_contradictions WHERE status = ? ORDER BY similarity DESC'
|
|
170
|
+
: 'SELECT * FROM curator_contradictions ORDER BY similarity DESC';
|
|
171
|
+
return this.provider
|
|
172
|
+
.all<Record<string, unknown>>(query, status ? [status] : undefined)
|
|
173
|
+
.map((r) => this.rowToContradiction(r));
|
|
124
174
|
}
|
|
125
175
|
|
|
126
176
|
resolveContradiction(id: number, resolution: 'resolved' | 'dismissed'): Contradiction | null {
|
|
127
|
-
this.provider.run(
|
|
128
|
-
|
|
177
|
+
this.provider.run(
|
|
178
|
+
'UPDATE curator_contradictions SET status = ?, resolved_at = unixepoch() WHERE id = ?',
|
|
179
|
+
[resolution, id],
|
|
180
|
+
);
|
|
181
|
+
const row = this.provider.get<Record<string, unknown>>(
|
|
182
|
+
'SELECT * FROM curator_contradictions WHERE id = ?',
|
|
183
|
+
[id],
|
|
184
|
+
);
|
|
129
185
|
return row ? this.rowToContradiction(row) : null;
|
|
130
186
|
}
|
|
131
187
|
|
|
132
|
-
async detectContradictionsHybrid(
|
|
133
|
-
|
|
134
|
-
|
|
188
|
+
async detectContradictionsHybrid(
|
|
189
|
+
threshold?: number,
|
|
190
|
+
): Promise<{ contradictions: Contradiction[]; method: 'tfidf-only' }> {
|
|
191
|
+
const searchFn = (title: string) =>
|
|
192
|
+
this.vault.search(title, { type: 'pattern', limit: 20 }).map((r) => r.entry);
|
|
193
|
+
return {
|
|
194
|
+
contradictions: this.persistContradictions(
|
|
195
|
+
findContradictions(this.vault.list({ limit: 100000 }), threshold, searchFn),
|
|
196
|
+
),
|
|
197
|
+
method: 'tfidf-only',
|
|
198
|
+
};
|
|
135
199
|
}
|
|
136
200
|
|
|
137
201
|
// ─── Grooming ─────────────────────────────────────────────────
|
|
@@ -140,11 +204,17 @@ export class Curator {
|
|
|
140
204
|
const entry = this.vault.get(entryId);
|
|
141
205
|
if (!entry) return null;
|
|
142
206
|
const tagsNormalized = this.normalizeTags(entryId);
|
|
143
|
-
const row = this.provider.get<{ updated_at: number }>(
|
|
207
|
+
const row = this.provider.get<{ updated_at: number }>(
|
|
208
|
+
'SELECT updated_at FROM entries WHERE id = ?',
|
|
209
|
+
[entryId],
|
|
210
|
+
);
|
|
144
211
|
const now = Math.floor(Date.now() / 1000);
|
|
145
212
|
const stale = row ? now - row.updated_at > DEFAULT_STALE_DAYS * 86400 : false;
|
|
146
213
|
const status = stale ? 'stale' : 'active';
|
|
147
|
-
this.provider.run(
|
|
214
|
+
this.provider.run(
|
|
215
|
+
`INSERT INTO curator_entry_state (entry_id, status, last_groomed_at) VALUES (?, ?, unixepoch()) ON CONFLICT(entry_id) DO UPDATE SET status = excluded.status, last_groomed_at = unixepoch()`,
|
|
216
|
+
[entryId, status],
|
|
217
|
+
);
|
|
148
218
|
this.logChange('groom', entryId, null, `status=${status}`, 'Routine grooming');
|
|
149
219
|
return { entryId, tagsNormalized, stale, lastGroomedAt: now };
|
|
150
220
|
}
|
|
@@ -152,12 +222,22 @@ export class Curator {
|
|
|
152
222
|
groomAll(): GroomAllResult {
|
|
153
223
|
const start = Date.now();
|
|
154
224
|
const entries = this.vault.list({ limit: 100000 });
|
|
155
|
-
let tagsNormalized = 0,
|
|
225
|
+
let tagsNormalized = 0,
|
|
226
|
+
staleCount = 0;
|
|
156
227
|
for (const entry of entries) {
|
|
157
228
|
const result = this.groomEntry(entry.id);
|
|
158
|
-
if (result) {
|
|
229
|
+
if (result) {
|
|
230
|
+
tagsNormalized += result.tagsNormalized.filter((t) => t.wasAliased).length;
|
|
231
|
+
if (result.stale) staleCount++;
|
|
232
|
+
}
|
|
159
233
|
}
|
|
160
|
-
return {
|
|
234
|
+
return {
|
|
235
|
+
totalEntries: entries.length,
|
|
236
|
+
groomedCount: entries.length,
|
|
237
|
+
tagsNormalized,
|
|
238
|
+
staleCount,
|
|
239
|
+
durationMs: Date.now() - start,
|
|
240
|
+
};
|
|
161
241
|
}
|
|
162
242
|
|
|
163
243
|
// ─── Consolidation ───────────────────────────────────────────
|
|
@@ -167,17 +247,30 @@ export class Curator {
|
|
|
167
247
|
const dryRun = options?.dryRun ?? true;
|
|
168
248
|
const staleDaysThreshold = options?.staleDaysThreshold ?? DEFAULT_STALE_DAYS;
|
|
169
249
|
const duplicateThreshold = options?.duplicateThreshold ?? DEFAULT_DUPLICATE_THRESHOLD;
|
|
170
|
-
const contradictionThreshold =
|
|
250
|
+
const contradictionThreshold =
|
|
251
|
+
options?.contradictionThreshold ?? DEFAULT_CONTRADICTION_THRESHOLD;
|
|
171
252
|
const duplicates = this.detectDuplicates(undefined, duplicateThreshold);
|
|
172
253
|
const now = Math.floor(Date.now() / 1000);
|
|
173
|
-
const staleRows = this.provider.all<{ id: string }>(
|
|
254
|
+
const staleRows = this.provider.all<{ id: string }>(
|
|
255
|
+
'SELECT id FROM entries WHERE updated_at < ?',
|
|
256
|
+
[now - staleDaysThreshold * 86400],
|
|
257
|
+
);
|
|
174
258
|
const staleEntries = staleRows.map((r) => r.id);
|
|
175
259
|
const contradictions = this.detectContradictions(contradictionThreshold);
|
|
176
260
|
let mutations = 0;
|
|
177
261
|
if (!dryRun) {
|
|
178
262
|
for (const entryId of staleEntries) {
|
|
179
|
-
this.provider.run(
|
|
180
|
-
|
|
263
|
+
this.provider.run(
|
|
264
|
+
`INSERT INTO curator_entry_state (entry_id, status, last_groomed_at) VALUES (?, 'archived', unixepoch()) ON CONFLICT(entry_id) DO UPDATE SET status = 'archived', last_groomed_at = unixepoch()`,
|
|
265
|
+
[entryId],
|
|
266
|
+
);
|
|
267
|
+
this.logChange(
|
|
268
|
+
'archive',
|
|
269
|
+
entryId,
|
|
270
|
+
'active',
|
|
271
|
+
'archived',
|
|
272
|
+
'Stale entry archived during consolidation',
|
|
273
|
+
);
|
|
181
274
|
mutations++;
|
|
182
275
|
}
|
|
183
276
|
const removed = new Set<string>();
|
|
@@ -185,20 +278,38 @@ export class Curator {
|
|
|
185
278
|
for (const match of result.matches) {
|
|
186
279
|
if (!removed.has(match.entryId) && match.entryId !== result.entryId) {
|
|
187
280
|
this.vault.remove(match.entryId);
|
|
188
|
-
this.logChange(
|
|
281
|
+
this.logChange(
|
|
282
|
+
'remove_duplicate',
|
|
283
|
+
match.entryId,
|
|
284
|
+
null,
|
|
285
|
+
null,
|
|
286
|
+
`Duplicate of ${result.entryId} (similarity: ${match.similarity.toFixed(3)})`,
|
|
287
|
+
);
|
|
189
288
|
removed.add(match.entryId);
|
|
190
289
|
mutations++;
|
|
191
290
|
}
|
|
192
291
|
}
|
|
193
292
|
}
|
|
194
293
|
}
|
|
195
|
-
return {
|
|
294
|
+
return {
|
|
295
|
+
dryRun,
|
|
296
|
+
duplicates,
|
|
297
|
+
staleEntries,
|
|
298
|
+
contradictions,
|
|
299
|
+
mutations,
|
|
300
|
+
durationMs: Date.now() - start,
|
|
301
|
+
};
|
|
196
302
|
}
|
|
197
303
|
|
|
198
304
|
// ─── Changelog ────────────────────────────────────────────────
|
|
199
305
|
|
|
200
306
|
getEntryHistory(entryId: string, limit?: number): ChangelogEntry[] {
|
|
201
|
-
return this.provider
|
|
307
|
+
return this.provider
|
|
308
|
+
.all<Record<string, unknown>>(
|
|
309
|
+
'SELECT * FROM curator_changelog WHERE entry_id = ? ORDER BY created_at DESC, id DESC LIMIT ?',
|
|
310
|
+
[entryId, limit ?? 50],
|
|
311
|
+
)
|
|
312
|
+
.map((r) => this.rowToChangelog(r));
|
|
202
313
|
}
|
|
203
314
|
|
|
204
315
|
// ─── Health Audit (delegates to health-audit) ─────────────────
|
|
@@ -206,8 +317,19 @@ export class Curator {
|
|
|
206
317
|
healthAudit(): HealthAuditResult {
|
|
207
318
|
const entries = this.vault.list({ limit: 100000 });
|
|
208
319
|
const dataProvider: HealthDataProvider = {
|
|
209
|
-
getStaleCount: (threshold) =>
|
|
210
|
-
|
|
320
|
+
getStaleCount: (threshold) =>
|
|
321
|
+
(
|
|
322
|
+
this.provider.get<{ count: number }>(
|
|
323
|
+
'SELECT COUNT(*) as count FROM entries WHERE updated_at < ?',
|
|
324
|
+
[threshold],
|
|
325
|
+
) ?? { count: 0 }
|
|
326
|
+
).count,
|
|
327
|
+
getGroomedCount: () =>
|
|
328
|
+
(
|
|
329
|
+
this.provider.get<{ count: number }>(
|
|
330
|
+
'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
|
|
331
|
+
) ?? { count: 0 }
|
|
332
|
+
).count,
|
|
211
333
|
getDuplicates: () => this.detectDuplicates(),
|
|
212
334
|
getOpenContradictions: () => this.getContradictions('open'),
|
|
213
335
|
};
|
|
@@ -216,46 +338,115 @@ export class Curator {
|
|
|
216
338
|
|
|
217
339
|
// ─── Entry History (Version Snapshots) ────────────────────────
|
|
218
340
|
|
|
219
|
-
recordSnapshot(
|
|
341
|
+
recordSnapshot(
|
|
342
|
+
entryId: string,
|
|
343
|
+
changedBy?: string,
|
|
344
|
+
changeReason?: string,
|
|
345
|
+
): { recorded: boolean; historyId: number } {
|
|
220
346
|
const entry = this.vault.get(entryId);
|
|
221
347
|
if (!entry) return { recorded: false, historyId: -1 };
|
|
222
|
-
const result = this.provider.run(
|
|
348
|
+
const result = this.provider.run(
|
|
349
|
+
'INSERT INTO curator_entry_history (entry_id, snapshot, changed_by, change_reason, created_at) VALUES (?, ?, ?, ?, unixepoch())',
|
|
350
|
+
[entryId, JSON.stringify(entry), changedBy ?? 'system', changeReason ?? null],
|
|
351
|
+
);
|
|
223
352
|
return { recorded: true, historyId: Number(result.lastInsertRowid) };
|
|
224
353
|
}
|
|
225
354
|
|
|
226
|
-
getVersionHistory(entryId: string): Array<{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
355
|
+
getVersionHistory(entryId: string): Array<{
|
|
356
|
+
historyId: number;
|
|
357
|
+
entryId: string;
|
|
358
|
+
snapshot: IntelligenceEntry;
|
|
359
|
+
changedBy: string;
|
|
360
|
+
changeReason: string | null;
|
|
361
|
+
createdAt: number;
|
|
362
|
+
}> {
|
|
363
|
+
return this.provider
|
|
364
|
+
.all<Record<string, unknown>>(
|
|
365
|
+
'SELECT * FROM curator_entry_history WHERE entry_id = ? ORDER BY created_at ASC, id ASC',
|
|
366
|
+
[entryId],
|
|
367
|
+
)
|
|
368
|
+
.map((row) => ({
|
|
369
|
+
historyId: row.id as number,
|
|
370
|
+
entryId: row.entry_id as string,
|
|
371
|
+
snapshot: JSON.parse(row.snapshot as string) as IntelligenceEntry,
|
|
372
|
+
changedBy: row.changed_by as string,
|
|
373
|
+
changeReason: (row.change_reason as string) ?? null,
|
|
374
|
+
createdAt: row.created_at as number,
|
|
375
|
+
}));
|
|
230
376
|
}
|
|
231
377
|
|
|
232
378
|
// ─── Queue Stats ─────────────────────────────────────────────
|
|
233
379
|
|
|
234
|
-
getQueueStats(): {
|
|
380
|
+
getQueueStats(): {
|
|
381
|
+
totalEntries: number;
|
|
382
|
+
groomedEntries: number;
|
|
383
|
+
ungroomedEntries: number;
|
|
384
|
+
staleEntries: number;
|
|
385
|
+
freshEntries: number;
|
|
386
|
+
avgDaysSinceGroom: number;
|
|
387
|
+
} {
|
|
235
388
|
const p = this.provider;
|
|
236
|
-
const totalEntries = (
|
|
237
|
-
|
|
389
|
+
const totalEntries = (
|
|
390
|
+
p.get<{ count: number }>('SELECT COUNT(*) as count FROM entries') ?? { count: 0 }
|
|
391
|
+
).count;
|
|
392
|
+
const groomedEntries = (
|
|
393
|
+
p.get<{ count: number }>(
|
|
394
|
+
'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
|
|
395
|
+
) ?? { count: 0 }
|
|
396
|
+
).count;
|
|
238
397
|
const now = Math.floor(Date.now() / 1000);
|
|
239
|
-
const staleEntries = (
|
|
240
|
-
|
|
398
|
+
const staleEntries = (
|
|
399
|
+
p.get<{ count: number }>(
|
|
400
|
+
'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at < ?',
|
|
401
|
+
[now - 30 * 86400],
|
|
402
|
+
) ?? { count: 0 }
|
|
403
|
+
).count;
|
|
404
|
+
const freshEntries = (
|
|
405
|
+
p.get<{ count: number }>(
|
|
406
|
+
'SELECT COUNT(*) as count FROM curator_entry_state WHERE last_groomed_at IS NOT NULL AND last_groomed_at >= ?',
|
|
407
|
+
[now - 7 * 86400],
|
|
408
|
+
) ?? { count: 0 }
|
|
409
|
+
).count;
|
|
241
410
|
let avgDaysSinceGroom = 0;
|
|
242
411
|
if (groomedEntries > 0) {
|
|
243
|
-
const totalSeconds =
|
|
412
|
+
const totalSeconds =
|
|
413
|
+
(
|
|
414
|
+
p.get<{ total: number | null }>(
|
|
415
|
+
'SELECT SUM(? - last_groomed_at) as total FROM curator_entry_state WHERE last_groomed_at IS NOT NULL',
|
|
416
|
+
[now],
|
|
417
|
+
) ?? { total: 0 }
|
|
418
|
+
).total ?? 0;
|
|
244
419
|
avgDaysSinceGroom = Math.round((totalSeconds / groomedEntries / 86400) * 100) / 100;
|
|
245
420
|
}
|
|
246
|
-
return {
|
|
421
|
+
return {
|
|
422
|
+
totalEntries,
|
|
423
|
+
groomedEntries,
|
|
424
|
+
ungroomedEntries: totalEntries - groomedEntries,
|
|
425
|
+
staleEntries,
|
|
426
|
+
freshEntries,
|
|
427
|
+
avgDaysSinceGroom,
|
|
428
|
+
};
|
|
247
429
|
}
|
|
248
430
|
|
|
249
431
|
// ─── Metadata Enrichment (delegates to metadata-enricher) ────
|
|
250
432
|
|
|
251
|
-
enrichMetadata(entryId: string): {
|
|
433
|
+
enrichMetadata(entryId: string): {
|
|
434
|
+
enriched: boolean;
|
|
435
|
+
changes: Array<{ field: string; before: string; after: string }>;
|
|
436
|
+
} {
|
|
252
437
|
const entry = this.vault.get(entryId);
|
|
253
438
|
if (!entry) return { enriched: false, changes: [] };
|
|
254
439
|
const { changes, updates } = enrichEntryMetadata(entry);
|
|
255
440
|
if (changes.length === 0) return { enriched: false, changes: [] };
|
|
256
441
|
this.vault.update(entryId, updates);
|
|
257
442
|
this.recordSnapshot(entryId, 'curator', 'Metadata enrichment');
|
|
258
|
-
this.logChange(
|
|
443
|
+
this.logChange(
|
|
444
|
+
'enrich_metadata',
|
|
445
|
+
entryId,
|
|
446
|
+
JSON.stringify(changes.map((c) => c.field)),
|
|
447
|
+
JSON.stringify(changes.map((c) => c.after)),
|
|
448
|
+
'Rule-based metadata enrichment',
|
|
449
|
+
);
|
|
259
450
|
return { enriched: true, changes };
|
|
260
451
|
}
|
|
261
452
|
|
|
@@ -264,24 +455,55 @@ export class Curator {
|
|
|
264
455
|
private persistContradictions(candidates: ContradictionCandidate[]): Contradiction[] {
|
|
265
456
|
const detected: Contradiction[] = [];
|
|
266
457
|
for (const c of candidates) {
|
|
267
|
-
const result = this.provider.run(
|
|
458
|
+
const result = this.provider.run(
|
|
459
|
+
'INSERT OR IGNORE INTO curator_contradictions (pattern_id, antipattern_id, similarity) VALUES (?, ?, ?)',
|
|
460
|
+
[c.patternId, c.antipatternId, c.similarity],
|
|
461
|
+
);
|
|
268
462
|
if (result.changes > 0) {
|
|
269
|
-
const row = this.provider.get<Record<string, unknown>>(
|
|
463
|
+
const row = this.provider.get<Record<string, unknown>>(
|
|
464
|
+
'SELECT * FROM curator_contradictions WHERE pattern_id = ? AND antipattern_id = ?',
|
|
465
|
+
[c.patternId, c.antipatternId],
|
|
466
|
+
);
|
|
270
467
|
if (row) detected.push(this.rowToContradiction(row));
|
|
271
468
|
}
|
|
272
469
|
}
|
|
273
470
|
return detected;
|
|
274
471
|
}
|
|
275
472
|
|
|
276
|
-
private logChange(
|
|
277
|
-
|
|
473
|
+
private logChange(
|
|
474
|
+
action: string,
|
|
475
|
+
entryId: string,
|
|
476
|
+
beforeValue: string | null,
|
|
477
|
+
afterValue: string | null,
|
|
478
|
+
reason: string,
|
|
479
|
+
): void {
|
|
480
|
+
this.provider.run(
|
|
481
|
+
'INSERT INTO curator_changelog (action, entry_id, before_value, after_value, reason) VALUES (?, ?, ?, ?, ?)',
|
|
482
|
+
[action, entryId, beforeValue, afterValue, reason],
|
|
483
|
+
);
|
|
278
484
|
}
|
|
279
485
|
|
|
280
486
|
private rowToContradiction(row: Record<string, unknown>): Contradiction {
|
|
281
|
-
return {
|
|
487
|
+
return {
|
|
488
|
+
id: row.id as number,
|
|
489
|
+
patternId: row.pattern_id as string,
|
|
490
|
+
antipatternId: row.antipattern_id as string,
|
|
491
|
+
similarity: row.similarity as number,
|
|
492
|
+
status: row.status as ContradictionStatus,
|
|
493
|
+
createdAt: row.created_at as number,
|
|
494
|
+
resolvedAt: (row.resolved_at as number) ?? null,
|
|
495
|
+
};
|
|
282
496
|
}
|
|
283
497
|
|
|
284
498
|
private rowToChangelog(row: Record<string, unknown>): ChangelogEntry {
|
|
285
|
-
return {
|
|
499
|
+
return {
|
|
500
|
+
id: row.id as number,
|
|
501
|
+
action: row.action as string,
|
|
502
|
+
entryId: row.entry_id as string,
|
|
503
|
+
beforeValue: (row.before_value as string) ?? null,
|
|
504
|
+
afterValue: (row.after_value as string) ?? null,
|
|
505
|
+
reason: row.reason as string,
|
|
506
|
+
createdAt: row.created_at as number,
|
|
507
|
+
};
|
|
286
508
|
}
|
|
287
509
|
}
|
|
@@ -97,8 +97,16 @@ describe('duplicate-detector', () => {
|
|
|
97
97
|
|
|
98
98
|
it('detects duplicates with identical content', () => {
|
|
99
99
|
const entries = [
|
|
100
|
-
makeEntry({
|
|
101
|
-
|
|
100
|
+
makeEntry({
|
|
101
|
+
id: 'dup-1',
|
|
102
|
+
title: 'Validate user input',
|
|
103
|
+
description: 'Always validate user input before processing.',
|
|
104
|
+
}),
|
|
105
|
+
makeEntry({
|
|
106
|
+
id: 'dup-2',
|
|
107
|
+
title: 'Validate user input',
|
|
108
|
+
description: 'Always validate user input before processing.',
|
|
109
|
+
}),
|
|
102
110
|
];
|
|
103
111
|
const results = detectDuplicates(entries, undefined, 0.3);
|
|
104
112
|
expect(results.length).toBeGreaterThan(0);
|
|
@@ -107,8 +115,16 @@ describe('duplicate-detector', () => {
|
|
|
107
115
|
|
|
108
116
|
it('does not flag unrelated entries', () => {
|
|
109
117
|
const entries = [
|
|
110
|
-
makeEntry({
|
|
111
|
-
|
|
118
|
+
makeEntry({
|
|
119
|
+
id: 'a',
|
|
120
|
+
title: 'Database indexing strategies',
|
|
121
|
+
description: 'Create indices on columns.',
|
|
122
|
+
}),
|
|
123
|
+
makeEntry({
|
|
124
|
+
id: 'b',
|
|
125
|
+
title: 'React component lifecycle',
|
|
126
|
+
description: 'Use useEffect for side effects.',
|
|
127
|
+
}),
|
|
112
128
|
];
|
|
113
129
|
const results = detectDuplicates(entries, undefined, 0.8);
|
|
114
130
|
expect(results.length).toBe(0);
|
|
@@ -117,7 +133,11 @@ describe('duplicate-detector', () => {
|
|
|
117
133
|
it('filters by entryId when provided', () => {
|
|
118
134
|
const entries = [
|
|
119
135
|
makeEntry({ id: 'x', title: 'Authentication with JWT', description: 'Use JWT for auth.' }),
|
|
120
|
-
makeEntry({
|
|
136
|
+
makeEntry({
|
|
137
|
+
id: 'y',
|
|
138
|
+
title: 'JWT authentication pattern',
|
|
139
|
+
description: 'Implement JWT auth.',
|
|
140
|
+
}),
|
|
121
141
|
makeEntry({ id: 'z', title: 'Database pooling', description: 'Connection pools.' }),
|
|
122
142
|
];
|
|
123
143
|
const results = detectDuplicates(entries, 'x', 0.3);
|
|
@@ -129,8 +149,18 @@ describe('duplicate-detector', () => {
|
|
|
129
149
|
|
|
130
150
|
it('skips cross-domain pairs', () => {
|
|
131
151
|
const entries = [
|
|
132
|
-
makeEntry({
|
|
133
|
-
|
|
152
|
+
makeEntry({
|
|
153
|
+
id: '1',
|
|
154
|
+
domain: 'design',
|
|
155
|
+
title: 'Use semantic tokens',
|
|
156
|
+
description: 'Always use semantic tokens.',
|
|
157
|
+
}),
|
|
158
|
+
makeEntry({
|
|
159
|
+
id: '2',
|
|
160
|
+
domain: 'architecture',
|
|
161
|
+
title: 'Use semantic tokens',
|
|
162
|
+
description: 'Always use semantic tokens.',
|
|
163
|
+
}),
|
|
134
164
|
];
|
|
135
165
|
const results = detectDuplicates(entries, undefined, 0.3);
|
|
136
166
|
expect(results.length).toBe(0);
|
|
@@ -138,8 +168,18 @@ describe('duplicate-detector', () => {
|
|
|
138
168
|
|
|
139
169
|
it('flags same-domain similar entries', () => {
|
|
140
170
|
const entries = [
|
|
141
|
-
makeEntry({
|
|
142
|
-
|
|
171
|
+
makeEntry({
|
|
172
|
+
id: '1',
|
|
173
|
+
domain: 'design',
|
|
174
|
+
title: 'Use semantic tokens for colors',
|
|
175
|
+
description: 'Always use semantic tokens.',
|
|
176
|
+
}),
|
|
177
|
+
makeEntry({
|
|
178
|
+
id: '2',
|
|
179
|
+
domain: 'design',
|
|
180
|
+
title: 'Use semantic tokens for color values',
|
|
181
|
+
description: 'Prefer semantic color tokens.',
|
|
182
|
+
}),
|
|
143
183
|
];
|
|
144
184
|
const results = detectDuplicates(entries, undefined, 0.3);
|
|
145
185
|
expect(results.length).toBeGreaterThan(0);
|
|
@@ -147,8 +187,16 @@ describe('duplicate-detector', () => {
|
|
|
147
187
|
|
|
148
188
|
it('sets suggestMerge based on MERGE_SUGGESTION_THRESHOLD', () => {
|
|
149
189
|
const entries = [
|
|
150
|
-
makeEntry({
|
|
151
|
-
|
|
190
|
+
makeEntry({
|
|
191
|
+
id: 'a',
|
|
192
|
+
title: 'Exact same title',
|
|
193
|
+
description: 'Exact same description for merge test.',
|
|
194
|
+
}),
|
|
195
|
+
makeEntry({
|
|
196
|
+
id: 'b',
|
|
197
|
+
title: 'Exact same title',
|
|
198
|
+
description: 'Exact same description for merge test.',
|
|
199
|
+
}),
|
|
152
200
|
];
|
|
153
201
|
const results = detectDuplicates(entries, undefined, 0.3);
|
|
154
202
|
expect(results.length).toBeGreaterThan(0);
|
|
@@ -158,13 +206,27 @@ describe('duplicate-detector', () => {
|
|
|
158
206
|
|
|
159
207
|
it('sorts matches by descending similarity', () => {
|
|
160
208
|
const entries = [
|
|
161
|
-
makeEntry({
|
|
162
|
-
|
|
163
|
-
|
|
209
|
+
makeEntry({
|
|
210
|
+
id: 'base',
|
|
211
|
+
title: 'Use semantic tokens for colors',
|
|
212
|
+
description: 'Tokens for styling.',
|
|
213
|
+
}),
|
|
214
|
+
makeEntry({
|
|
215
|
+
id: 'close',
|
|
216
|
+
title: 'Use semantic tokens for color values',
|
|
217
|
+
description: 'Tokens for styling values.',
|
|
218
|
+
}),
|
|
219
|
+
makeEntry({
|
|
220
|
+
id: 'far',
|
|
221
|
+
title: 'Semantic approach to colors',
|
|
222
|
+
description: 'Use semantic color approach.',
|
|
223
|
+
}),
|
|
164
224
|
];
|
|
165
225
|
const results = detectDuplicates(entries, 'base', 0.1);
|
|
166
226
|
if (results.length > 0 && results[0].matches.length > 1) {
|
|
167
|
-
expect(results[0].matches[0].similarity).toBeGreaterThanOrEqual(
|
|
227
|
+
expect(results[0].matches[0].similarity).toBeGreaterThanOrEqual(
|
|
228
|
+
results[0].matches[1].similarity,
|
|
229
|
+
);
|
|
168
230
|
}
|
|
169
231
|
});
|
|
170
232
|
|