@soleri/core 2.7.0 → 2.9.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.
Files changed (137) hide show
  1. package/dist/extensions/index.d.ts +3 -0
  2. package/dist/extensions/index.d.ts.map +1 -0
  3. package/dist/extensions/index.js +2 -0
  4. package/dist/extensions/index.js.map +1 -0
  5. package/dist/extensions/middleware.d.ts +13 -0
  6. package/dist/extensions/middleware.d.ts.map +1 -0
  7. package/dist/extensions/middleware.js +47 -0
  8. package/dist/extensions/middleware.js.map +1 -0
  9. package/dist/extensions/types.d.ts +64 -0
  10. package/dist/extensions/types.d.ts.map +1 -0
  11. package/dist/extensions/types.js +2 -0
  12. package/dist/extensions/types.js.map +1 -0
  13. package/dist/index.d.ts +8 -16
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +7 -16
  16. package/dist/index.js.map +1 -1
  17. package/dist/planning/gap-analysis.d.ts +2 -1
  18. package/dist/planning/gap-analysis.d.ts.map +1 -1
  19. package/dist/planning/gap-analysis.js +70 -1
  20. package/dist/planning/gap-analysis.js.map +1 -1
  21. package/dist/planning/gap-types.d.ts +8 -3
  22. package/dist/planning/gap-types.d.ts.map +1 -1
  23. package/dist/planning/gap-types.js +9 -1
  24. package/dist/planning/gap-types.js.map +1 -1
  25. package/dist/planning/planner.d.ts.map +1 -1
  26. package/dist/planning/planner.js +17 -5
  27. package/dist/planning/planner.js.map +1 -1
  28. package/dist/runtime/core-ops.d.ts +1 -1
  29. package/dist/runtime/core-ops.js +1 -1
  30. package/dist/runtime/facades/admin-facade.d.ts +8 -0
  31. package/dist/runtime/facades/admin-facade.d.ts.map +1 -0
  32. package/dist/runtime/facades/admin-facade.js +90 -0
  33. package/dist/runtime/facades/admin-facade.js.map +1 -0
  34. package/dist/runtime/facades/brain-facade.d.ts +8 -0
  35. package/dist/runtime/facades/brain-facade.d.ts.map +1 -0
  36. package/dist/runtime/facades/brain-facade.js +294 -0
  37. package/dist/runtime/facades/brain-facade.js.map +1 -0
  38. package/dist/runtime/facades/cognee-facade.d.ts +8 -0
  39. package/dist/runtime/facades/cognee-facade.d.ts.map +1 -0
  40. package/dist/runtime/facades/cognee-facade.js +154 -0
  41. package/dist/runtime/facades/cognee-facade.js.map +1 -0
  42. package/dist/runtime/facades/control-facade.d.ts +8 -0
  43. package/dist/runtime/facades/control-facade.d.ts.map +1 -0
  44. package/dist/runtime/facades/control-facade.js +244 -0
  45. package/dist/runtime/facades/control-facade.js.map +1 -0
  46. package/dist/runtime/facades/curator-facade.d.ts +8 -0
  47. package/dist/runtime/facades/curator-facade.d.ts.map +1 -0
  48. package/dist/runtime/facades/curator-facade.js +117 -0
  49. package/dist/runtime/facades/curator-facade.js.map +1 -0
  50. package/dist/runtime/facades/index.d.ts +10 -0
  51. package/dist/runtime/facades/index.d.ts.map +1 -0
  52. package/dist/runtime/facades/index.js +71 -0
  53. package/dist/runtime/facades/index.js.map +1 -0
  54. package/dist/runtime/facades/loop-facade.d.ts +8 -0
  55. package/dist/runtime/facades/loop-facade.d.ts.map +1 -0
  56. package/dist/runtime/facades/loop-facade.js +9 -0
  57. package/dist/runtime/facades/loop-facade.js.map +1 -0
  58. package/dist/runtime/facades/memory-facade.d.ts +8 -0
  59. package/dist/runtime/facades/memory-facade.d.ts.map +1 -0
  60. package/dist/runtime/facades/memory-facade.js +108 -0
  61. package/dist/runtime/facades/memory-facade.js.map +1 -0
  62. package/dist/runtime/facades/orchestrate-facade.d.ts +8 -0
  63. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -0
  64. package/dist/runtime/facades/orchestrate-facade.js +58 -0
  65. package/dist/runtime/facades/orchestrate-facade.js.map +1 -0
  66. package/dist/runtime/facades/plan-facade.d.ts +8 -0
  67. package/dist/runtime/facades/plan-facade.d.ts.map +1 -0
  68. package/dist/runtime/facades/plan-facade.js +110 -0
  69. package/dist/runtime/facades/plan-facade.js.map +1 -0
  70. package/dist/runtime/facades/vault-facade.d.ts +8 -0
  71. package/dist/runtime/facades/vault-facade.d.ts.map +1 -0
  72. package/dist/runtime/facades/vault-facade.js +194 -0
  73. package/dist/runtime/facades/vault-facade.js.map +1 -0
  74. package/dist/runtime/grading-ops.d.ts +1 -1
  75. package/dist/runtime/grading-ops.js +2 -2
  76. package/dist/runtime/grading-ops.js.map +1 -1
  77. package/dist/runtime/vault-extra-ops.d.ts +2 -2
  78. package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
  79. package/dist/runtime/vault-extra-ops.js +37 -2
  80. package/dist/runtime/vault-extra-ops.js.map +1 -1
  81. package/dist/streams/index.d.ts +4 -0
  82. package/dist/streams/index.d.ts.map +1 -0
  83. package/dist/streams/index.js +3 -0
  84. package/dist/streams/index.js.map +1 -0
  85. package/dist/streams/normalize.d.ts +14 -0
  86. package/dist/streams/normalize.d.ts.map +1 -0
  87. package/dist/streams/normalize.js +43 -0
  88. package/dist/streams/normalize.js.map +1 -0
  89. package/dist/streams/replayable-stream.d.ts +19 -0
  90. package/dist/streams/replayable-stream.d.ts.map +1 -0
  91. package/dist/streams/replayable-stream.js +90 -0
  92. package/dist/streams/replayable-stream.js.map +1 -0
  93. package/dist/vault/content-hash.d.ts +16 -0
  94. package/dist/vault/content-hash.d.ts.map +1 -0
  95. package/dist/vault/content-hash.js +21 -0
  96. package/dist/vault/content-hash.js.map +1 -0
  97. package/dist/vault/vault.d.ts +9 -0
  98. package/dist/vault/vault.d.ts.map +1 -1
  99. package/dist/vault/vault.js +49 -3
  100. package/dist/vault/vault.js.map +1 -1
  101. package/package.json +1 -1
  102. package/src/__tests__/content-hash.test.ts +60 -0
  103. package/src/__tests__/core-ops.test.ts +10 -7
  104. package/src/__tests__/extensions.test.ts +233 -0
  105. package/src/__tests__/grading-ops.test.ts +2 -2
  106. package/src/__tests__/memory-cross-project-ops.test.ts +2 -2
  107. package/src/__tests__/normalize.test.ts +75 -0
  108. package/src/__tests__/playbook.test.ts +4 -4
  109. package/src/__tests__/replayable-stream.test.ts +66 -0
  110. package/src/__tests__/vault-extra-ops.test.ts +1 -1
  111. package/src/__tests__/vault.test.ts +72 -0
  112. package/src/extensions/index.ts +2 -0
  113. package/src/extensions/middleware.ts +53 -0
  114. package/src/extensions/types.ts +64 -0
  115. package/src/index.ts +14 -17
  116. package/src/planning/gap-analysis.ts +95 -1
  117. package/src/planning/gap-types.ts +12 -2
  118. package/src/planning/planner.ts +17 -5
  119. package/src/runtime/facades/admin-facade.ts +101 -0
  120. package/src/runtime/facades/brain-facade.ts +331 -0
  121. package/src/runtime/facades/cognee-facade.ts +162 -0
  122. package/src/runtime/facades/control-facade.ts +279 -0
  123. package/src/runtime/facades/curator-facade.ts +132 -0
  124. package/src/runtime/facades/index.ts +74 -0
  125. package/src/runtime/facades/loop-facade.ts +12 -0
  126. package/src/runtime/facades/memory-facade.ts +114 -0
  127. package/src/runtime/facades/orchestrate-facade.ts +68 -0
  128. package/src/runtime/facades/plan-facade.ts +119 -0
  129. package/src/runtime/facades/vault-facade.ts +223 -0
  130. package/src/runtime/grading-ops.ts +2 -2
  131. package/src/runtime/vault-extra-ops.ts +38 -2
  132. package/src/streams/index.ts +3 -0
  133. package/src/streams/normalize.ts +56 -0
  134. package/src/streams/replayable-stream.ts +92 -0
  135. package/src/vault/content-hash.ts +31 -0
  136. package/src/vault/vault.ts +73 -3
  137. package/src/runtime/core-ops.ts +0 -1443
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Plan facade — plan lifecycle ops.
3
+ * create, approve, execute, reconcile, complete, grading.
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import type { OpDefinition } from '../../facades/types.js';
8
+ import type { AgentRuntime } from '../types.js';
9
+ import { createPlanningExtraOps } from '../planning-extra-ops.js';
10
+ import { createGradingOps } from '../grading-ops.js';
11
+
12
+ export function createPlanFacadeOps(runtime: AgentRuntime): OpDefinition[] {
13
+ const { planner } = runtime;
14
+
15
+ return [
16
+ // ─── Planning (inline from core-ops.ts) ─────────────────────
17
+ {
18
+ name: 'create_plan',
19
+ description:
20
+ 'Create a new plan in draft status. Plans track multi-step tasks with decisions and sub-tasks.',
21
+ auth: 'write',
22
+ schema: z.object({
23
+ objective: z.string().describe('What the plan aims to achieve'),
24
+ scope: z.string().describe('Which parts of the codebase are affected'),
25
+ decisions: z.array(z.string()).optional().default([]),
26
+ tasks: z
27
+ .array(z.object({ title: z.string(), description: z.string() }))
28
+ .optional()
29
+ .default([]),
30
+ }),
31
+ handler: async (params) => {
32
+ const plan = planner.create({
33
+ objective: params.objective as string,
34
+ scope: params.scope as string,
35
+ decisions: (params.decisions as string[]) ?? [],
36
+ tasks: (params.tasks as Array<{ title: string; description: string }>) ?? [],
37
+ });
38
+ return { created: true, plan };
39
+ },
40
+ },
41
+ {
42
+ name: 'get_plan',
43
+ description: 'Get a plan by ID, or list all active plans if no ID provided.',
44
+ auth: 'read',
45
+ schema: z.object({
46
+ planId: z.string().optional().describe('Plan ID. Omit to list all active plans.'),
47
+ }),
48
+ handler: async (params) => {
49
+ if (params.planId) {
50
+ const plan = planner.get(params.planId as string);
51
+ if (!plan) return { error: 'Plan not found: ' + params.planId };
52
+ return plan;
53
+ }
54
+ return { active: planner.getActive(), executing: planner.getExecuting() };
55
+ },
56
+ },
57
+ {
58
+ name: 'approve_plan',
59
+ description: 'Approve a draft plan and optionally start execution.',
60
+ auth: 'write',
61
+ schema: z.object({
62
+ planId: z.string(),
63
+ startExecution: z
64
+ .boolean()
65
+ .optional()
66
+ .default(false)
67
+ .describe('If true, immediately start execution after approval'),
68
+ }),
69
+ handler: async (params) => {
70
+ let plan = planner.approve(params.planId as string);
71
+ if (params.startExecution) {
72
+ plan = planner.startExecution(plan.id);
73
+ }
74
+ return { approved: true, executing: plan.status === 'executing', plan };
75
+ },
76
+ },
77
+ {
78
+ name: 'update_task',
79
+ description: 'Update a task status within an executing plan.',
80
+ auth: 'write',
81
+ schema: z.object({
82
+ planId: z.string(),
83
+ taskId: z.string(),
84
+ status: z.enum(['pending', 'in_progress', 'completed', 'skipped', 'failed']),
85
+ }),
86
+ handler: async (params) => {
87
+ const plan = planner.updateTask(
88
+ params.planId as string,
89
+ params.taskId as string,
90
+ params.status as 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed',
91
+ );
92
+ const task = plan.tasks.find((t) => t.id === params.taskId);
93
+ return { updated: true, task, plan: { id: plan.id, status: plan.status } };
94
+ },
95
+ },
96
+ {
97
+ name: 'complete_plan',
98
+ description: 'Mark an executing plan as completed.',
99
+ auth: 'write',
100
+ schema: z.object({
101
+ planId: z.string(),
102
+ }),
103
+ handler: async (params) => {
104
+ const plan = planner.complete(params.planId as string);
105
+ const taskSummary = {
106
+ completed: plan.tasks.filter((t) => t.status === 'completed').length,
107
+ skipped: plan.tasks.filter((t) => t.status === 'skipped').length,
108
+ failed: plan.tasks.filter((t) => t.status === 'failed').length,
109
+ total: plan.tasks.length,
110
+ };
111
+ return { completed: true, plan, taskSummary };
112
+ },
113
+ },
114
+
115
+ // ─── Satellite ops ───────────────────────────────────────────
116
+ ...createPlanningExtraOps(runtime),
117
+ ...createGradingOps(runtime),
118
+ ];
119
+ }
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Vault facade — knowledge management ops.
3
+ * search, CRUD, import/export, intake, archival.
4
+ */
5
+
6
+ import { z } from 'zod';
7
+ import type { OpDefinition } from '../../facades/types.js';
8
+ import type { IntelligenceEntry } from '../../intelligence/types.js';
9
+ import type { AgentRuntime } from '../types.js';
10
+ import { createVaultExtraOps } from '../vault-extra-ops.js';
11
+ import { createCaptureOps } from '../capture-ops.js';
12
+ import { createIntakeOps } from '../intake-ops.js';
13
+
14
+ export function createVaultFacadeOps(runtime: AgentRuntime): OpDefinition[] {
15
+ const { vault, brain, cognee, llmClient, syncManager, intakePipeline } = runtime;
16
+
17
+ return [
18
+ // ─── Search / Vault (inline from core-ops.ts) ───────────────
19
+ {
20
+ name: 'search',
21
+ description:
22
+ 'Search across all knowledge domains. Results ranked by TF-IDF + severity + recency + tag overlap + domain match.',
23
+ auth: 'read',
24
+ schema: z.object({
25
+ query: z.string(),
26
+ domain: z.string().optional(),
27
+ type: z.enum(['pattern', 'anti-pattern', 'rule', 'playbook']).optional(),
28
+ severity: z.enum(['critical', 'warning', 'suggestion']).optional(),
29
+ tags: z.array(z.string()).optional(),
30
+ limit: z.number().optional(),
31
+ }),
32
+ handler: async (params) => {
33
+ return brain.intelligentSearch(params.query as string, {
34
+ domain: params.domain as string | undefined,
35
+ type: params.type as string | undefined,
36
+ severity: params.severity as string | undefined,
37
+ tags: params.tags as string[] | undefined,
38
+ limit: (params.limit as number) ?? 10,
39
+ });
40
+ },
41
+ },
42
+ {
43
+ name: 'vault_stats',
44
+ description: 'Get vault statistics — entry counts by type, domain, severity.',
45
+ auth: 'read',
46
+ handler: async () => vault.stats(),
47
+ },
48
+ {
49
+ name: 'list_all',
50
+ description: 'List all knowledge entries with optional filters.',
51
+ auth: 'read',
52
+ schema: z.object({
53
+ domain: z.string().optional(),
54
+ type: z.enum(['pattern', 'anti-pattern', 'rule', 'playbook']).optional(),
55
+ severity: z.enum(['critical', 'warning', 'suggestion']).optional(),
56
+ tags: z.array(z.string()).optional(),
57
+ limit: z.number().optional(),
58
+ offset: z.number().optional(),
59
+ }),
60
+ handler: async (params) => {
61
+ return vault.list({
62
+ domain: params.domain as string | undefined,
63
+ type: params.type as string | undefined,
64
+ severity: params.severity as string | undefined,
65
+ tags: params.tags as string[] | undefined,
66
+ limit: (params.limit as number) ?? 50,
67
+ offset: (params.offset as number) ?? 0,
68
+ });
69
+ },
70
+ },
71
+ {
72
+ name: 'export',
73
+ description:
74
+ 'Export vault entries as JSON intelligence bundles — one per domain. Enables version control and sharing.',
75
+ auth: 'read',
76
+ schema: z.object({
77
+ domain: z.string().optional().describe('Export only this domain. Omit to export all.'),
78
+ }),
79
+ handler: async (params) => {
80
+ const stats = vault.stats();
81
+ const domains = params.domain ? [params.domain as string] : Object.keys(stats.byDomain);
82
+ const bundles: Array<{ domain: string; version: string; entries: IntelligenceEntry[] }> =
83
+ [];
84
+ for (const d of domains) {
85
+ const entries = vault.list({ domain: d, limit: 10000 });
86
+ bundles.push({ domain: d, version: '1.0.0', entries });
87
+ }
88
+ return {
89
+ exported: true,
90
+ bundles,
91
+ totalEntries: bundles.reduce((sum, b) => sum + b.entries.length, 0),
92
+ domains: bundles.map((b) => b.domain),
93
+ };
94
+ },
95
+ },
96
+
97
+ // ─── Enriched Capture ────────────────────────────────────────
98
+ {
99
+ name: 'capture_enriched',
100
+ description:
101
+ 'Unified LLM-enriched capture — accepts minimal fields (title, description, type), uses LLM to auto-infer tags, category, and severity.',
102
+ auth: 'write',
103
+ schema: z.object({
104
+ title: z.string().describe('Knowledge title'),
105
+ description: z.string().describe('Knowledge description'),
106
+ type: z
107
+ .enum(['pattern', 'anti-pattern', 'rule', 'playbook'])
108
+ .optional()
109
+ .describe('Entry type. If omitted, LLM infers from content.'),
110
+ domain: z.string().optional().describe('Domain. If omitted, LLM infers.'),
111
+ tags: z.array(z.string()).optional().describe('Tags. LLM adds more if needed.'),
112
+ }),
113
+ handler: async (params) => {
114
+ try {
115
+ const title = params.title as string;
116
+ const description = params.description as string;
117
+
118
+ // Try LLM enrichment for auto-tagging
119
+ let inferredTags: string[] = (params.tags as string[] | undefined) ?? [];
120
+ let inferredType = (params.type as IntelligenceEntry['type'] | undefined) ?? 'pattern';
121
+ const inferredDomain = (params.domain as string | undefined) ?? 'general';
122
+ let inferredSeverity: IntelligenceEntry['severity'] = 'suggestion';
123
+ const enriched = false;
124
+
125
+ try {
126
+ const captureId = `enriched-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
127
+ const enrichResult = brain.enrichAndCapture({
128
+ id: captureId,
129
+ title,
130
+ description,
131
+ type: inferredType,
132
+ domain: inferredDomain,
133
+ severity: inferredSeverity,
134
+ tags: inferredTags,
135
+ });
136
+
137
+ if (enrichResult.captured) {
138
+ return {
139
+ captured: true,
140
+ enriched: true,
141
+ entryId: enrichResult.id,
142
+ autoTags: enrichResult.autoTags,
143
+ duplicate: enrichResult.duplicate ?? null,
144
+ };
145
+ }
146
+ } catch {
147
+ // LLM enrichment failed — fall back to basic capture
148
+ }
149
+
150
+ // Fallback: basic capture without LLM enrichment
151
+ // Infer type from keywords
152
+ const lowerDesc = description.toLowerCase();
153
+ if (!params.type) {
154
+ if (
155
+ lowerDesc.includes('avoid') ||
156
+ lowerDesc.includes("don't") ||
157
+ lowerDesc.includes('anti-pattern')
158
+ )
159
+ inferredType = 'anti-pattern';
160
+ else if (
161
+ lowerDesc.includes('rule') ||
162
+ lowerDesc.includes('must') ||
163
+ lowerDesc.includes('always')
164
+ )
165
+ inferredType = 'rule';
166
+ }
167
+
168
+ // Infer severity from keywords
169
+ if (
170
+ lowerDesc.includes('critical') ||
171
+ lowerDesc.includes('security') ||
172
+ lowerDesc.includes('breaking')
173
+ )
174
+ inferredSeverity = 'critical';
175
+ else if (
176
+ lowerDesc.includes('warning') ||
177
+ lowerDesc.includes('careful') ||
178
+ lowerDesc.includes('avoid')
179
+ )
180
+ inferredSeverity = 'warning';
181
+
182
+ // Auto-generate tags from title words
183
+ if (inferredTags.length === 0) {
184
+ inferredTags = title
185
+ .toLowerCase()
186
+ .split(/\s+/)
187
+ .filter(
188
+ (w) =>
189
+ w.length > 3 && !['with', 'from', 'that', 'this', 'have', 'been'].includes(w),
190
+ )
191
+ .slice(0, 5);
192
+ }
193
+
194
+ const entry: IntelligenceEntry = {
195
+ id: `enriched-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
196
+ type: inferredType,
197
+ domain: inferredDomain,
198
+ title,
199
+ severity: inferredSeverity,
200
+ description,
201
+ tags: inferredTags,
202
+ };
203
+
204
+ vault.add(entry);
205
+
206
+ return {
207
+ captured: true,
208
+ enriched,
209
+ entry,
210
+ autoTags: inferredTags,
211
+ };
212
+ } catch (err) {
213
+ return { error: (err as Error).message };
214
+ }
215
+ },
216
+ },
217
+
218
+ // ─── Satellite ops ───────────────────────────────────────────
219
+ ...createVaultExtraOps(runtime),
220
+ ...createCaptureOps(runtime),
221
+ ...createIntakeOps(intakePipeline),
222
+ ];
223
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Plan grading operations — 5 ops for iterative plan quality scoring.
3
- * Uses 6-pass gap analysis with severity-weighted scoring (ported from Salvador).
3
+ * Uses 7-pass gap analysis with severity-weighted scoring + substance bonuses.
4
4
  *
5
5
  * Ops: plan_grade, plan_check_history, plan_latest_check,
6
6
  * plan_meets_grade, plan_auto_improve.
@@ -22,7 +22,7 @@ export function createGradingOps(runtime: AgentRuntime): OpDefinition[] {
22
22
  {
23
23
  name: 'plan_grade',
24
24
  description:
25
- 'Grade a plan using 6-pass gap analysis — severity-weighted scoring (critical=30, major=15, minor=2). Returns grade, score, gaps with recommendations, and iteration number.',
25
+ 'Grade a plan using 7-pass gap analysis — severity-weighted scoring (critical=30, major=15, minor=2) with substance bonuses for vault-informed depth. Returns grade, score, gaps with recommendations, and iteration number.',
26
26
  auth: 'read',
27
27
  schema: z.object({
28
28
  planId: z.string().describe('The plan ID to grade.'),
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Extra vault operations — 23 ops that extend the 4 base vault ops in core-ops.ts.
2
+ * Extra vault operations — 25 ops that extend the 4 base vault ops in core-ops.ts.
3
3
  *
4
4
  * Groups: single-entry CRUD (3), bulk (2), discovery (3), import/export (3),
5
5
  * analytics (1), seed canonical (1), knowledge lifecycle (4), temporal (3),
6
- * archival (3).
6
+ * archival (3), content hashing (2).
7
7
  */
8
8
 
9
9
  import { z } from 'zod';
@@ -586,6 +586,42 @@ export function createVaultExtraOps(runtime: AgentRuntime): OpDefinition[] {
586
586
  return vault.optimize();
587
587
  },
588
588
  },
589
+
590
+ // ── Content hashing (#166) ────────────────────────────────────────
591
+ {
592
+ name: 'vault_content_hash',
593
+ description: 'Compute content hash for an entry without inserting',
594
+ auth: 'read' as const,
595
+ schema: z.object({
596
+ type: z.string(),
597
+ domain: z.string(),
598
+ title: z.string(),
599
+ description: z.string(),
600
+ tags: z.array(z.string()).optional(),
601
+ example: z.string().optional(),
602
+ counterExample: z.string().optional(),
603
+ }),
604
+ handler: async (params) => {
605
+ const { computeContentHash: hashFn } = await import('../vault/content-hash.js');
606
+ const hash = hashFn(params as unknown as Parameters<typeof hashFn>[0]);
607
+ const existingId = vault.findByContentHash(hash);
608
+ return { hash, duplicate: existingId !== null, existingId };
609
+ },
610
+ },
611
+ {
612
+ name: 'vault_dedup_status',
613
+ description: 'Report content hash coverage and duplicate statistics',
614
+ auth: 'read' as const,
615
+ handler: async () => {
616
+ const stats = vault.contentHashStats();
617
+ const duplicates = stats.total - stats.uniqueHashes;
618
+ return {
619
+ ...stats,
620
+ duplicates,
621
+ coverage: stats.total > 0 ? Math.round((stats.hashed / stats.total) * 100) : 100,
622
+ };
623
+ },
624
+ },
589
625
  ];
590
626
  }
591
627
 
@@ -0,0 +1,3 @@
1
+ export { ReplayableStream } from './replayable-stream.js';
2
+ export { normalize, collect } from './normalize.js';
3
+ export type { NestableInput } from './normalize.js';
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Input type that supports arbitrary nesting of sync/async values.
3
+ */
4
+ export type NestableInput<T> =
5
+ | T
6
+ | T[]
7
+ | Promise<T | T[]>
8
+ | AsyncIterable<T>
9
+ | Iterable<NestableInput<T>>;
10
+
11
+ function isAsyncIterable<T>(value: unknown): value is AsyncIterable<T> {
12
+ return (
13
+ value !== null &&
14
+ value !== undefined &&
15
+ typeof value === 'object' &&
16
+ Symbol.asyncIterator in value
17
+ );
18
+ }
19
+
20
+ function isSyncIterable<T>(value: unknown): value is Iterable<T> {
21
+ return (
22
+ value !== null && value !== undefined && typeof value === 'object' && Symbol.iterator in value
23
+ );
24
+ }
25
+
26
+ /**
27
+ * Recursively flatten nested async/sync inputs into a flat AsyncIterable<T>.
28
+ * Discrimination: Promise → AsyncIterable → Iterable (excluding strings) → leaf T.
29
+ */
30
+ export async function* normalize<T>(input: NestableInput<T>): AsyncIterable<T> {
31
+ if (input instanceof Promise) {
32
+ const resolved = await input;
33
+ yield* normalize<T>(resolved as NestableInput<T>);
34
+ } else if (isAsyncIterable<T>(input)) {
35
+ for await (const item of input) {
36
+ yield item;
37
+ }
38
+ } else if (typeof input !== 'string' && isSyncIterable<NestableInput<T>>(input)) {
39
+ for (const item of input) {
40
+ yield* normalize<T>(item);
41
+ }
42
+ } else {
43
+ yield input as T;
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Collect an AsyncIterable<T> into a Promise<T[]>.
49
+ */
50
+ export async function collect<T>(source: AsyncIterable<T>): Promise<T[]> {
51
+ const items: T[] = [];
52
+ for await (const item of source) {
53
+ items.push(item);
54
+ }
55
+ return items;
56
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Multi-consumer async stream that replays from a buffer.
3
+ * Source executes exactly once — new iterators replay from buffer[0].
4
+ */
5
+ export class ReplayableStream<T> implements AsyncIterable<T> {
6
+ private buffer: T[] = [];
7
+ private source: AsyncIterator<T>;
8
+ private done = false;
9
+ private error: unknown = undefined;
10
+ private waiters: Array<{
11
+ resolve: (result: IteratorResult<T, undefined>) => void;
12
+ reject: (err: unknown) => void;
13
+ }> = [];
14
+ private advancing = false;
15
+
16
+ constructor(source: AsyncIterable<T>) {
17
+ this.source = source[Symbol.asyncIterator]();
18
+ }
19
+
20
+ private async advance(): Promise<void> {
21
+ if (this.advancing || this.done) return;
22
+ this.advancing = true;
23
+ try {
24
+ const result = await this.source.next();
25
+ if (result.done) {
26
+ this.done = true;
27
+ for (const waiter of this.waiters) {
28
+ waiter.resolve({ value: undefined, done: true });
29
+ }
30
+ this.waiters.length = 0;
31
+ } else {
32
+ this.buffer.push(result.value);
33
+ for (const waiter of this.waiters) {
34
+ waiter.resolve({ value: result.value, done: false });
35
+ }
36
+ this.waiters.length = 0;
37
+ }
38
+ } catch (err) {
39
+ this.done = true;
40
+ this.error = err;
41
+ for (const waiter of this.waiters) {
42
+ waiter.reject(err);
43
+ }
44
+ this.waiters.length = 0;
45
+ } finally {
46
+ this.advancing = false;
47
+ }
48
+ }
49
+
50
+ [Symbol.asyncIterator](): AsyncIterator<T> {
51
+ let index = 0;
52
+ return {
53
+ next: async (): Promise<IteratorResult<T>> => {
54
+ if (index < this.buffer.length) {
55
+ return { value: this.buffer[index++], done: false };
56
+ }
57
+ if (this.done) {
58
+ if (this.error) throw this.error;
59
+ return { value: undefined, done: true };
60
+ }
61
+ // Need to advance the source
62
+ return new Promise<IteratorResult<T, undefined>>((resolve, reject) => {
63
+ this.waiters.push({ resolve, reject });
64
+ if (!this.advancing) {
65
+ this.advance().catch(() => {
66
+ // Error already propagated to waiters via reject
67
+ });
68
+ }
69
+ }).then((result) => {
70
+ if (!result.done) index++;
71
+ return result;
72
+ });
73
+ },
74
+ };
75
+ }
76
+
77
+ async collect(): Promise<T[]> {
78
+ const items: T[] = [];
79
+ for await (const item of this) {
80
+ items.push(item);
81
+ }
82
+ return items;
83
+ }
84
+
85
+ get bufferedCount(): number {
86
+ return this.buffer.length;
87
+ }
88
+
89
+ get isDone(): boolean {
90
+ return this.done;
91
+ }
92
+ }
@@ -0,0 +1,31 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ export interface HashableEntry {
4
+ type: string;
5
+ domain: string;
6
+ title: string;
7
+ description: string;
8
+ tags?: string[];
9
+ example?: string;
10
+ counterExample?: string;
11
+ }
12
+
13
+ /**
14
+ * Compute a deterministic SHA-256 content hash for a vault entry.
15
+ * Normalizes fields (lowercase domain, trim, sort tags/keys) before hashing.
16
+ * Returns 40-char hex string. Excludes mutable fields (id, severity, timestamps).
17
+ */
18
+ export function computeContentHash(entry: HashableEntry): string {
19
+ const normalized = {
20
+ counterExample: (entry.counterExample ?? '').trim(),
21
+ description: entry.description.trim(),
22
+ domain: entry.domain.toLowerCase().trim(),
23
+ example: (entry.example ?? '').trim(),
24
+ tags: [...(entry.tags ?? [])].sort(),
25
+ title: entry.title.trim(),
26
+ type: entry.type.trim(),
27
+ };
28
+ // Keys already alphabetical — JSON.stringify preserves insertion order
29
+ const json = JSON.stringify(normalized);
30
+ return createHash('sha256').update(json, 'utf8').digest('hex').slice(0, 40);
31
+ }