@aperdomoll90/ledger-ai 1.3.0 → 1.4.2
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/dist/cli.js +177 -221
- package/dist/commands/add.js +51 -100
- package/dist/commands/backfill.js +55 -0
- package/dist/commands/backup.js +10 -10
- package/dist/commands/check.js +21 -29
- package/dist/commands/config.js +13 -12
- package/dist/commands/delete.js +22 -17
- package/dist/commands/eval-judge.js +11 -0
- package/dist/commands/eval.js +321 -0
- package/dist/commands/export.js +8 -10
- package/dist/commands/get.js +9 -0
- package/dist/commands/hunt.js +206 -0
- package/dist/commands/ingest.js +15 -14
- package/dist/commands/init.js +18 -20
- package/dist/commands/list.js +21 -7
- package/dist/commands/migrate.js +11 -11
- package/dist/commands/onboard.js +2 -2
- package/dist/commands/pull.js +3 -2
- package/dist/commands/push.js +8 -8
- package/dist/commands/restore.js +38 -38
- package/dist/commands/show.js +13 -16
- package/dist/commands/sync.js +58 -19
- package/dist/commands/tag.js +20 -14
- package/dist/commands/update.js +50 -18
- package/dist/commands/wizard.js +3 -3
- package/dist/lib/ai-search.js +163 -0
- package/dist/lib/audit.js +19 -0
- package/dist/lib/backfill.js +60 -0
- package/dist/lib/config.js +19 -2
- package/dist/lib/document-classification.js +5 -0
- package/dist/lib/document-fetching.js +77 -0
- package/dist/lib/document-operations.js +150 -0
- package/dist/lib/documents/classification.js +5 -0
- package/dist/lib/documents/fetching.js +89 -0
- package/dist/lib/documents/operations.js +304 -0
- package/dist/lib/domains.js +116 -0
- package/dist/lib/embeddings.js +190 -0
- package/dist/lib/errors.js +3 -1
- package/dist/lib/eval/eval-advanced.js +289 -0
- package/dist/lib/eval/eval-judge-session.js +233 -0
- package/dist/lib/eval/eval-store.js +105 -0
- package/dist/lib/eval/eval.js +303 -0
- package/dist/lib/file-writer.js +23 -0
- package/dist/lib/generators.js +44 -45
- package/dist/lib/hunter-db.js +235 -0
- package/dist/lib/hunter-rss.js +30 -0
- package/dist/lib/hunter-scoring.js +55 -0
- package/dist/lib/hunter-types.js +36 -0
- package/dist/lib/lint-configs.js +20 -0
- package/dist/lib/migrate.js +2 -2
- package/dist/lib/notes.js +173 -59
- package/dist/lib/observability.js +296 -0
- package/dist/lib/op-add-note-types.test.js +7 -6
- package/dist/lib/prompt.js +8 -8
- package/dist/lib/rate-limiter.js +103 -0
- package/dist/lib/search/ai-search.js +396 -0
- package/dist/lib/search/chunk-context-enrichment.js +155 -0
- package/dist/lib/search/embeddings.js +293 -0
- package/dist/lib/search/reranker.js +120 -0
- package/dist/lib/search/semantic-cache.js +53 -0
- package/dist/lib/type-registry.test.js +6 -6
- package/dist/mcp-server.js +553 -66
- package/dist/migrations/migrations/005-audit-log.sql +22 -0
- package/dist/migrations/migrations/005_opportunities.sql +48 -0
- package/dist/migrations/migrations/006-audited-operations.sql +235 -0
- package/dist/migrations/migrations/006_hunt_analytics.sql +38 -0
- package/dist/migrations/migrations/007-eval-golden-judgments.sql +119 -0
- package/dist/migrations/migrations/008-drop-expected-doc-ids.sql +9 -0
- package/dist/migrations/migrations/008-judge-helpers.sql +21 -0
- package/dist/migrations/migrations/009-semantic-cache.sql +216 -0
- package/dist/scripts/batch-grade.js +344 -0
- package/dist/scripts/benchmark-ingestion.js +376 -0
- package/dist/scripts/convert-judgments-to-graded.js +88 -0
- package/dist/scripts/diagnose-first-result.js +333 -0
- package/dist/scripts/drop-golden-query.js +53 -0
- package/dist/scripts/eval-search.js +115 -0
- package/dist/scripts/grade-unjudged-top1.js +138 -0
- package/dist/scripts/hunter-analytics.js +38 -0
- package/dist/scripts/hunter-cron.js +63 -0
- package/dist/scripts/hunter-purge.js +25 -0
- package/dist/scripts/migrate-v2.js +140 -0
- package/dist/scripts/reindex.js +74 -0
- package/dist/scripts/sync-local-docs.js +153 -0
- package/package.json +7 -1
package/dist/lib/notes.js
CHANGED
|
@@ -1,41 +1,75 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import { fatal, ExitCode } from './errors.js';
|
|
3
3
|
import { contentHash } from './hash.js';
|
|
4
|
+
import { audit } from './audit.js';
|
|
4
5
|
import { loadConfigFile, saveConfigFile } from './config.js';
|
|
5
|
-
|
|
6
|
+
import { inferDomain as inferDomainFromType, getProtectionDefault, getAutoLoadDefault, resolveV1Type, isV2Type, } from './domains.js';
|
|
7
|
+
// --- Built-in Type Registry (v2: domain-based) ---
|
|
6
8
|
export const BUILTIN_TYPES = {
|
|
9
|
+
// v2 types mapped to legacy delivery tier for backward compat
|
|
10
|
+
'personality': 'persona',
|
|
11
|
+
'behavioral-rule': 'persona',
|
|
12
|
+
'preference': 'persona',
|
|
13
|
+
'skill': 'persona',
|
|
14
|
+
'claude-md': 'persona',
|
|
15
|
+
'hook': 'persona',
|
|
16
|
+
'plugin-config': 'persona',
|
|
17
|
+
'type-registry': 'persona',
|
|
18
|
+
'sync-rule': 'persona',
|
|
19
|
+
'dashboard': 'project',
|
|
20
|
+
'device-registry': 'project',
|
|
21
|
+
'environment': 'project',
|
|
22
|
+
'eval-result': 'project',
|
|
23
|
+
'architecture': 'project',
|
|
24
|
+
'project-status': 'project',
|
|
25
|
+
'event': 'project',
|
|
26
|
+
'error': 'project',
|
|
27
|
+
'reference': 'knowledge',
|
|
28
|
+
'knowledge': 'knowledge',
|
|
29
|
+
'general': 'knowledge',
|
|
30
|
+
// Legacy v1 names still accepted
|
|
7
31
|
'user-preference': 'persona',
|
|
8
32
|
'persona-rule': 'persona',
|
|
9
33
|
'system-rule': 'persona',
|
|
10
34
|
'code-craft': 'persona',
|
|
11
35
|
'architecture-decision': 'project',
|
|
12
|
-
'project-status': 'project',
|
|
13
|
-
'event': 'project',
|
|
14
|
-
'error': 'project',
|
|
15
|
-
'reference': 'knowledge',
|
|
16
36
|
'knowledge-guide': 'knowledge',
|
|
17
|
-
'
|
|
18
|
-
};
|
|
19
|
-
const TYPE_ALIASES = {
|
|
20
|
-
'feedback': 'general',
|
|
37
|
+
'skill-reference': 'persona',
|
|
21
38
|
};
|
|
22
39
|
function resolveTypeAlias(type) {
|
|
23
|
-
|
|
40
|
+
const migration = resolveV1Type(type);
|
|
41
|
+
if (migration)
|
|
42
|
+
return migration.type;
|
|
43
|
+
if (type === 'feedback')
|
|
44
|
+
return 'general';
|
|
45
|
+
return type;
|
|
24
46
|
}
|
|
25
47
|
export function getTypeRegistry() {
|
|
26
48
|
const config = loadConfigFile();
|
|
27
49
|
return { ...BUILTIN_TYPES, ...(config.types ?? {}) };
|
|
28
50
|
}
|
|
51
|
+
/** Infer domain from a note type. Handles both v1 and v2 type names. */
|
|
52
|
+
export function inferDomain(noteType) {
|
|
53
|
+
const v1 = resolveV1Type(noteType);
|
|
54
|
+
if (v1)
|
|
55
|
+
return v1.domain;
|
|
56
|
+
return inferDomainFromType(noteType);
|
|
57
|
+
}
|
|
58
|
+
/** Legacy: infer delivery tier from note type. Use inferDomain for new code. */
|
|
29
59
|
export function inferDelivery(noteType) {
|
|
60
|
+
const registry = getTypeRegistry();
|
|
61
|
+
// Check original name first (user overrides may use v1 names)
|
|
62
|
+
if (noteType in registry)
|
|
63
|
+
return registry[noteType];
|
|
30
64
|
const resolved = resolveTypeAlias(noteType);
|
|
31
|
-
return
|
|
65
|
+
return registry[resolved] ?? 'knowledge';
|
|
32
66
|
}
|
|
33
67
|
export function getRegisteredTypes() {
|
|
34
68
|
return Object.keys(getTypeRegistry());
|
|
35
69
|
}
|
|
36
70
|
export function isRegisteredType(noteType) {
|
|
37
71
|
const resolved = resolveTypeAlias(noteType);
|
|
38
|
-
return resolved in getTypeRegistry();
|
|
72
|
+
return isV2Type(resolved) || resolved in getTypeRegistry();
|
|
39
73
|
}
|
|
40
74
|
export function registerType(name, delivery) {
|
|
41
75
|
const config = loadConfigFile();
|
|
@@ -61,17 +95,31 @@ export async function fetchPersonaNotes(supabase) {
|
|
|
61
95
|
const { data, error } = await supabase
|
|
62
96
|
.from('notes')
|
|
63
97
|
.select('id, content, metadata, created_at, updated_at')
|
|
64
|
-
.eq('metadata->>
|
|
98
|
+
.eq('metadata->>domain', 'persona');
|
|
65
99
|
if (error) {
|
|
66
100
|
fatal(`Error querying persona notes: ${error.message}`, ExitCode.SUPABASE_ERROR);
|
|
67
101
|
}
|
|
68
102
|
return (data || []);
|
|
69
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Fetch notes that should sync to every machine.
|
|
106
|
+
* v2: domain IN (system, persona, workspace).
|
|
107
|
+
*/
|
|
108
|
+
export async function fetchSyncableNotes(supabase) {
|
|
109
|
+
const { data: domainNotes, error: domainError } = await supabase
|
|
110
|
+
.from('notes')
|
|
111
|
+
.select('id, content, metadata, created_at, updated_at')
|
|
112
|
+
.in('metadata->>domain', ['system', 'persona', 'workspace']);
|
|
113
|
+
if (!domainError && domainNotes && domainNotes.length > 0) {
|
|
114
|
+
return domainNotes;
|
|
115
|
+
}
|
|
116
|
+
return fetchPersonaNotes(supabase);
|
|
117
|
+
}
|
|
70
118
|
export async function findNoteByFile(supabase, filename) {
|
|
71
119
|
const { data: byFile } = await supabase
|
|
72
120
|
.from('notes')
|
|
73
121
|
.select('id, metadata')
|
|
74
|
-
.eq('metadata->>
|
|
122
|
+
.eq('metadata->>file_path', filename)
|
|
75
123
|
.single();
|
|
76
124
|
if (byFile)
|
|
77
125
|
return byFile;
|
|
@@ -120,10 +168,10 @@ export async function searchNotes(supabase, openai, query, threshold = 0.3, maxR
|
|
|
120
168
|
export async function fetchNoteHashes(supabase) {
|
|
121
169
|
const notes = await fetchPersonaNotes(supabase);
|
|
122
170
|
return notes
|
|
123
|
-
.filter(n => n.metadata.
|
|
171
|
+
.filter(n => n.metadata.file_path)
|
|
124
172
|
.map(n => ({
|
|
125
173
|
id: n.id,
|
|
126
|
-
localFile: n.metadata.
|
|
174
|
+
localFile: n.metadata.file_path,
|
|
127
175
|
contentHash: n.metadata.content_hash || contentHash(n.content),
|
|
128
176
|
content: n.content,
|
|
129
177
|
}));
|
|
@@ -198,6 +246,35 @@ function formatNotePreview(id, meta, content, maxLen = 300) {
|
|
|
198
246
|
const descLine = desc ? `\nDescription: ${desc}` : '';
|
|
199
247
|
return `"${label}" (id: ${id}) | type: ${noteType || '-'} | project: ${project || '-'}${descLine}\n ${preview}${truncated}`;
|
|
200
248
|
}
|
|
249
|
+
/**
|
|
250
|
+
* Check if an operation should be blocked or confirmed based on protection level.
|
|
251
|
+
* Returns null if operation is allowed, or an OperationResult if blocked/needs approval.
|
|
252
|
+
*/
|
|
253
|
+
function checkProtection(noteId, meta, operation, confirmed) {
|
|
254
|
+
const protection = meta.protection ?? 'open';
|
|
255
|
+
const uKey = meta.upsert_key;
|
|
256
|
+
const label = uKey || `id ${noteId}`;
|
|
257
|
+
const noteType = meta.type;
|
|
258
|
+
if (protection === 'immutable') {
|
|
259
|
+
return {
|
|
260
|
+
status: 'error',
|
|
261
|
+
message: `BLOCKED — "${label}" (type: ${noteType ?? 'unknown'}) is immutable and cannot be ${operation}d. Immutable notes are system-managed only.`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (protection === 'protected' && !confirmed) {
|
|
265
|
+
return {
|
|
266
|
+
status: 'confirm',
|
|
267
|
+
message: `PROTECTED NOTE — "${label}" (type: ${noteType ?? 'unknown'}) requires explicit user approval to ${operation}.\n\nTo proceed, re-call with confirmed: true.`,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (protection === 'guarded' && !confirmed) {
|
|
271
|
+
return {
|
|
272
|
+
status: 'confirm',
|
|
273
|
+
message: `GUARDED NOTE — "${label}" (type: ${noteType ?? 'unknown'}) requires confirmation to ${operation}.\n\nTo proceed, re-call with confirmed: true.`,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
201
278
|
// --- Naming Conventions ---
|
|
202
279
|
/** Valid type prefixes for upsert_key naming. */
|
|
203
280
|
const TYPE_PREFIXES = {
|
|
@@ -253,8 +330,8 @@ export function validateNaming(upsertKey, type, description) {
|
|
|
253
330
|
}
|
|
254
331
|
return null;
|
|
255
332
|
}
|
|
256
|
-
/** Derive
|
|
257
|
-
export function
|
|
333
|
+
/** Derive file_path from upsert_key: feedback-style → feedback_style.md */
|
|
334
|
+
export function deriveFilePath(upsertKey) {
|
|
258
335
|
return upsertKey.replace(/-/g, '_') + '.md';
|
|
259
336
|
}
|
|
260
337
|
/** Check if naming enforcement is enabled in config. */
|
|
@@ -281,7 +358,7 @@ export function checkMetadataCompleteness(metadata, type) {
|
|
|
281
358
|
if (!metadata.upsert_key) {
|
|
282
359
|
missing.push('upsert_key');
|
|
283
360
|
}
|
|
284
|
-
// Only ask for status on project-
|
|
361
|
+
// Only ask for status on project-tier types (those with delivery=project in the type registry)
|
|
285
362
|
if (inferDelivery(type) === 'project' && !metadata.status) {
|
|
286
363
|
missing.push('status');
|
|
287
364
|
}
|
|
@@ -300,9 +377,9 @@ export function checkMetadataCompleteness(metadata, type) {
|
|
|
300
377
|
return `METADATA NEEDED — ask the user for these fields before saving:\n\n${fields.join('\n')}\n\nIf the user wants to skip, re-call add_note with metadata field \`interactive_skip: true\` to use defaults.`;
|
|
301
378
|
}
|
|
302
379
|
// --- Shared Operations (called by both MCP and CLI) ---
|
|
303
|
-
export async function opSearchNotes(clients, query, threshold, limit, type, project) {
|
|
380
|
+
export async function opSearchNotes(clients, query, threshold, limit, type, project, domain) {
|
|
304
381
|
const embedding = await getEmbedding(clients.openai, query);
|
|
305
|
-
const fetchLimit = (type || project) ? limit * 3 : limit;
|
|
382
|
+
const fetchLimit = (type || project || domain) ? limit * 3 : limit;
|
|
306
383
|
const { data, error } = await clients.supabase.rpc('match_notes', {
|
|
307
384
|
q_emb: JSON.stringify(embedding),
|
|
308
385
|
threshold,
|
|
@@ -316,6 +393,8 @@ export async function opSearchNotes(clients, query, threshold, limit, type, proj
|
|
|
316
393
|
results = results.filter(n => n.metadata.type === type);
|
|
317
394
|
if (project)
|
|
318
395
|
results = results.filter(n => n.metadata.project === project);
|
|
396
|
+
if (domain)
|
|
397
|
+
results = results.filter(n => n.metadata.domain === domain);
|
|
319
398
|
results = results.slice(0, limit);
|
|
320
399
|
// Fallback: retry at 0.3 when requested threshold returns empty
|
|
321
400
|
if ((!results || results.length === 0) && threshold > 0.3) {
|
|
@@ -330,6 +409,8 @@ export async function opSearchNotes(clients, query, threshold, limit, type, proj
|
|
|
330
409
|
fallbackResults = fallbackResults.filter(n => n.metadata.type === type);
|
|
331
410
|
if (project)
|
|
332
411
|
fallbackResults = fallbackResults.filter(n => n.metadata.project === project);
|
|
412
|
+
if (domain)
|
|
413
|
+
fallbackResults = fallbackResults.filter(n => n.metadata.domain === domain);
|
|
333
414
|
fallbackResults = fallbackResults.slice(0, limit);
|
|
334
415
|
if (fallbackResults.length > 0) {
|
|
335
416
|
results = fallbackResults;
|
|
@@ -373,7 +454,7 @@ async function reassembleResults(supabase, results) {
|
|
|
373
454
|
}
|
|
374
455
|
return output.join('\n\n---\n\n');
|
|
375
456
|
}
|
|
376
|
-
export async function opListNotes(clients, limit, type, project) {
|
|
457
|
+
export async function opListNotes(clients, limit, type, project, domain) {
|
|
377
458
|
let query = clients.supabase
|
|
378
459
|
.from('notes')
|
|
379
460
|
.select('id, content, metadata, created_at')
|
|
@@ -383,6 +464,8 @@ export async function opListNotes(clients, limit, type, project) {
|
|
|
383
464
|
query = query.eq('metadata->>type', type);
|
|
384
465
|
if (project)
|
|
385
466
|
query = query.eq('metadata->>project', project);
|
|
467
|
+
if (domain)
|
|
468
|
+
query = query.eq('metadata->>domain', domain);
|
|
386
469
|
const { data, error } = await query;
|
|
387
470
|
if (error) {
|
|
388
471
|
return { status: 'error', message: `Error: ${error.message}` };
|
|
@@ -418,8 +501,10 @@ export async function opAddNote(clients, content, type, agent, metadata, force,
|
|
|
418
501
|
const nameError = validateTypeName(resolvedType);
|
|
419
502
|
if (nameError)
|
|
420
503
|
return { status: 'error', message: nameError };
|
|
421
|
-
const
|
|
422
|
-
|
|
504
|
+
const domainForRegistry = inferDomain(resolvedType);
|
|
505
|
+
// Map domain to legacy delivery tier for registry compatibility
|
|
506
|
+
const deliveryForRegistry = domainForRegistry === 'project' ? 'project' : domainForRegistry === 'persona' || domainForRegistry === 'system' ? 'persona' : 'knowledge';
|
|
507
|
+
registerType(resolvedType, deliveryForRegistry);
|
|
423
508
|
}
|
|
424
509
|
// If type is still unknown after potential registration, prompt
|
|
425
510
|
if (!isRegisteredType(resolvedType)) {
|
|
@@ -429,11 +514,32 @@ export async function opAddNote(clients, content, type, agent, metadata, force,
|
|
|
429
514
|
.join(', ');
|
|
430
515
|
return {
|
|
431
516
|
status: 'confirm',
|
|
432
|
-
message: `Type "${type}" is not registered.\n\nOptions:\n1. Register with
|
|
517
|
+
message: `Type "${type}" is not registered.\n\nOptions:\n1. Register with domain auto-inferred from type — re-call add_note with register_type: true\n2. Register with specific domain — re-call add_note with register_type: true AND set metadata.domain to "system", "persona", "workspace", or "project"\n3. Use an existing type instead — re-call add_note with one of: ${typeListStr}\n4. Cancel\n\nAsk the user which option they prefer.`,
|
|
433
518
|
};
|
|
434
519
|
}
|
|
435
520
|
// Use resolved type for the rest of the flow
|
|
436
521
|
type = resolvedType;
|
|
522
|
+
// --- Auto-set v2 metadata from type ---
|
|
523
|
+
if (!metadata.domain) {
|
|
524
|
+
metadata.domain = inferDomain(type);
|
|
525
|
+
}
|
|
526
|
+
if (!metadata.protection) {
|
|
527
|
+
metadata.protection = getProtectionDefault(type);
|
|
528
|
+
}
|
|
529
|
+
if (metadata.auto_load === undefined) {
|
|
530
|
+
metadata.auto_load = getAutoLoadDefault(metadata.domain, type);
|
|
531
|
+
}
|
|
532
|
+
if (!metadata.owner_type) {
|
|
533
|
+
metadata.owner_type = 'user';
|
|
534
|
+
metadata.owner_id = null;
|
|
535
|
+
}
|
|
536
|
+
if (!metadata.schema_version) {
|
|
537
|
+
metadata.schema_version = 1;
|
|
538
|
+
}
|
|
539
|
+
if (!metadata.embedding_model) {
|
|
540
|
+
metadata.embedding_model = 'openai/text-embedding-3-small';
|
|
541
|
+
metadata.embedding_dimensions = 1536;
|
|
542
|
+
}
|
|
437
543
|
// Naming enforcement (opt-in via config)
|
|
438
544
|
if (isNamingEnforced()) {
|
|
439
545
|
const namingError = validateNaming(upsertKey || '', type, description);
|
|
@@ -441,11 +547,11 @@ export async function opAddNote(clients, content, type, agent, metadata, force,
|
|
|
441
547
|
return { status: 'error', message: `Naming violation: ${namingError}` };
|
|
442
548
|
}
|
|
443
549
|
}
|
|
444
|
-
// Auto-derive
|
|
445
|
-
if (upsertKey && !metadata.
|
|
446
|
-
const
|
|
447
|
-
if (
|
|
448
|
-
metadata.
|
|
550
|
+
// Auto-derive file_path from upsert_key for persona/system domain notes with auto_load
|
|
551
|
+
if (upsertKey && !metadata.file_path) {
|
|
552
|
+
const noteDomain = metadata.domain || inferDomain(type);
|
|
553
|
+
if (noteDomain === 'persona' || noteDomain === 'system') {
|
|
554
|
+
metadata.file_path = deriveFilePath(upsertKey);
|
|
449
555
|
}
|
|
450
556
|
}
|
|
451
557
|
const fullMetadata = { ...metadata, type, agent, content_hash: contentHash(content) };
|
|
@@ -507,6 +613,7 @@ async function upsertExistingNote(clients, existing, content, fullMetadata) {
|
|
|
507
613
|
.single();
|
|
508
614
|
if (error)
|
|
509
615
|
return { status: 'error', message: `Error: ${error.message}` };
|
|
616
|
+
await audit(clients, data.id, fullMetadata, 'update', { content_hash: existing.metadata.content_hash, metadata: existing.metadata });
|
|
510
617
|
const preview = content.length > 100 ? content.slice(0, 100) + '...' : content;
|
|
511
618
|
return { status: 'ok', message: `Updated "${upsertKey}" (id: ${data.id}, ${content.length} chars)\nPreview: ${preview}` };
|
|
512
619
|
}
|
|
@@ -527,6 +634,7 @@ async function upsertExistingNote(clients, existing, content, fullMetadata) {
|
|
|
527
634
|
.single();
|
|
528
635
|
if (error)
|
|
529
636
|
return { status: 'error', message: `Error: ${error.message}` };
|
|
637
|
+
await audit(clients, data.id, fullMetadata, 'update', { content_hash: existing.metadata.content_hash, metadata: existing.metadata });
|
|
530
638
|
const preview = content.length > 100 ? content.slice(0, 100) + '...' : content;
|
|
531
639
|
return { status: 'ok', message: `Updated "${upsertKey}" (id: ${data.id}, ${content.length} chars)\nPreview: ${preview}` };
|
|
532
640
|
}
|
|
@@ -544,6 +652,7 @@ async function upsertExistingNote(clients, existing, content, fullMetadata) {
|
|
|
544
652
|
return { status: 'error', message: `Error saving chunk ${i + 1}/${newChunks.length}: ${error.message}` };
|
|
545
653
|
ids.push(data.id);
|
|
546
654
|
}
|
|
655
|
+
await audit(clients, ids[0], fullMetadata, 'update', { content_hash: existing.metadata.content_hash, metadata: existing.metadata });
|
|
547
656
|
return { status: 'ok', message: `Updated "${upsertKey}" as ${newChunks.length} chunks (ids: ${ids.join(', ')}, ${content.length} chars total)` };
|
|
548
657
|
}
|
|
549
658
|
async function createNewNote(clients, content, fullMetadata) {
|
|
@@ -558,6 +667,7 @@ async function createNewNote(clients, content, fullMetadata) {
|
|
|
558
667
|
.single();
|
|
559
668
|
if (error)
|
|
560
669
|
return { status: 'error', message: `Error: ${error.message}` };
|
|
670
|
+
await audit(clients, data.id, fullMetadata, 'create', null);
|
|
561
671
|
const label = upsertKey || `id ${data.id}`;
|
|
562
672
|
const preview = content.length > 100 ? content.slice(0, 100) + '...' : content;
|
|
563
673
|
return { status: 'ok', message: `Saved "${label}" (id: ${data.id}, type: ${fullMetadata.type}, ${content.length} chars)\nPreview: ${preview}` };
|
|
@@ -572,6 +682,7 @@ async function createNewNote(clients, content, fullMetadata) {
|
|
|
572
682
|
return { status: 'error', message: `Error saving chunk ${i + 1}/${chunks.length}: ${error.message}` };
|
|
573
683
|
ids.push(data.id);
|
|
574
684
|
}
|
|
685
|
+
await audit(clients, ids[0], fullMetadata, 'create', null);
|
|
575
686
|
return { status: 'ok', message: `Saved "${upsertKey || 'chunked'}" as ${chunks.length} chunks (ids: ${ids.join(', ')}, ${content.length} chars total)` };
|
|
576
687
|
}
|
|
577
688
|
export async function opUpdateNote(clients, id, content, metadata, confirmed = false) {
|
|
@@ -583,16 +694,9 @@ export async function opUpdateNote(clients, id, content, metadata, confirmed = f
|
|
|
583
694
|
if (fetchError || !existing) {
|
|
584
695
|
return { status: 'error', message: `Error: note ${id} not found.` };
|
|
585
696
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const existingType = existing.metadata.type;
|
|
590
|
-
const uKey = existing.metadata.upsert_key;
|
|
591
|
-
return {
|
|
592
|
-
status: 'confirm',
|
|
593
|
-
message: `⚠️ PROTECTED NOTE — "${uKey || `id ${id}`}" (type: ${existingType || 'unknown'}) has protected delivery.\n\nThis is a skill-reference note. Are you sure you want to update it? Skill reference documents are protected to prevent accidental overwrites.\n\nTo proceed, call update_note again with confirmed: true.`,
|
|
594
|
-
};
|
|
595
|
-
}
|
|
697
|
+
const protectionCheck = checkProtection(id, existing.metadata, 'update', confirmed);
|
|
698
|
+
if (protectionCheck)
|
|
699
|
+
return protectionCheck;
|
|
596
700
|
// Confirmation gate
|
|
597
701
|
if (!confirmed) {
|
|
598
702
|
const currentPreview = formatNotePreview(existing.id, existing.metadata, existing.content);
|
|
@@ -619,6 +723,7 @@ export async function opUpdateNote(clients, id, content, metadata, confirmed = f
|
|
|
619
723
|
.single();
|
|
620
724
|
if (error)
|
|
621
725
|
return { status: 'error', message: `Error: ${error.message}` };
|
|
726
|
+
await audit(clients, data.id, existingMeta, 'update', { content: existing.content, metadata: existingMeta });
|
|
622
727
|
const uKey = baseMeta.upsert_key;
|
|
623
728
|
const label = uKey || `id ${data.id}`;
|
|
624
729
|
const preview = content.length > 100 ? content.slice(0, 100) + '...' : content;
|
|
@@ -643,6 +748,7 @@ export async function opUpdateNote(clients, id, content, metadata, confirmed = f
|
|
|
643
748
|
.single();
|
|
644
749
|
if (error)
|
|
645
750
|
return { status: 'error', message: `Error: ${error.message}` };
|
|
751
|
+
await audit(clients, data.id, existingMeta, 'update', { content: existing.content, metadata: existingMeta });
|
|
646
752
|
const uKey = baseMeta.upsert_key;
|
|
647
753
|
const label = uKey || `id ${data.id}`;
|
|
648
754
|
const preview = content.length > 100 ? content.slice(0, 100) + '...' : content;
|
|
@@ -662,6 +768,7 @@ export async function opUpdateNote(clients, id, content, metadata, confirmed = f
|
|
|
662
768
|
return { status: 'error', message: `Error updating chunk ${i + 1}/${chunks.length}: ${error.message}` };
|
|
663
769
|
ids.push(data.id);
|
|
664
770
|
}
|
|
771
|
+
await audit(clients, ids[0], existingMeta, 'update', { content: existing.content, metadata: existingMeta });
|
|
665
772
|
return { status: 'ok', message: `Note updated as ${chunks.length} chunks (ids: ${ids.join(', ')}, group: ${newGroupId})` };
|
|
666
773
|
}
|
|
667
774
|
export async function opUpdateMetadata(clients, id, metadata, confirmed = false) {
|
|
@@ -673,23 +780,35 @@ export async function opUpdateMetadata(clients, id, metadata, confirmed = false)
|
|
|
673
780
|
if (fetchError || !existing) {
|
|
674
781
|
return { status: 'error', message: `Error: note ${id} not found.` };
|
|
675
782
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const existingType = existing.metadata.type;
|
|
680
|
-
const uKey = existing.metadata.upsert_key;
|
|
681
|
-
return {
|
|
682
|
-
status: 'confirm',
|
|
683
|
-
message: `⚠️ PROTECTED NOTE — "${uKey || `id ${id}`}" (type: ${existingType || 'unknown'}) has protected delivery.\n\nThis is a skill-reference note. Are you sure you want to update its metadata?\n\nTo proceed, call update_metadata again with confirmed: true.`,
|
|
684
|
-
};
|
|
685
|
-
}
|
|
783
|
+
const protectionCheck = checkProtection(id, existing.metadata, 'update', confirmed);
|
|
784
|
+
if (protectionCheck)
|
|
785
|
+
return protectionCheck;
|
|
686
786
|
const merged = { ...existing.metadata, ...metadata };
|
|
787
|
+
// Type change cascading — auto-update domain when type changes
|
|
788
|
+
const oldType = existing.metadata.type;
|
|
789
|
+
const newType = metadata.type;
|
|
790
|
+
if (newType && oldType && newType !== oldType) {
|
|
791
|
+
const oldExpectedDomain = inferDomain(oldType);
|
|
792
|
+
const currentDomain = existing.metadata.domain;
|
|
793
|
+
// Only auto-update if domain wasn't manually overridden
|
|
794
|
+
if (!currentDomain || currentDomain === oldExpectedDomain) {
|
|
795
|
+
merged.domain = inferDomain(newType);
|
|
796
|
+
}
|
|
797
|
+
// If domain was manually set to something different, leave it alone
|
|
798
|
+
}
|
|
687
799
|
const { error } = await clients.supabase
|
|
688
800
|
.from('notes')
|
|
689
801
|
.update({ metadata: merged })
|
|
690
802
|
.eq('id', id);
|
|
691
803
|
if (error)
|
|
692
804
|
return { status: 'error', message: `Error: ${error.message}` };
|
|
805
|
+
const changedFields = {};
|
|
806
|
+
for (const key of Object.keys(metadata)) {
|
|
807
|
+
const oldVal = existing.metadata[key];
|
|
808
|
+
if (oldVal !== metadata[key])
|
|
809
|
+
changedFields[key] = oldVal;
|
|
810
|
+
}
|
|
811
|
+
await audit(clients, id, merged, 'update_metadata', { metadata: changedFields });
|
|
693
812
|
const uKey = merged.upsert_key;
|
|
694
813
|
return { status: 'ok', message: `Updated metadata for "${uKey || `id ${id}`}"` };
|
|
695
814
|
}
|
|
@@ -704,15 +823,9 @@ export async function opDeleteNote(clients, id, confirmed = false) {
|
|
|
704
823
|
}
|
|
705
824
|
const meta = existing.metadata;
|
|
706
825
|
const groupId = meta.chunk_group;
|
|
707
|
-
|
|
708
|
-
if (
|
|
709
|
-
|
|
710
|
-
const existingType = meta.type;
|
|
711
|
-
return {
|
|
712
|
-
status: 'confirm',
|
|
713
|
-
message: `⚠️ PROTECTED NOTE — "${uKey || `id ${id}`}" (type: ${existingType || 'unknown'}) has protected delivery.\n\nThis is a skill-reference note. Are you sure you want to delete it? This action cannot be undone.\n\nTo proceed, call delete_note again with confirmed: true.`,
|
|
714
|
-
};
|
|
715
|
-
}
|
|
826
|
+
const protectionCheck = checkProtection(id, meta, 'delete', confirmed);
|
|
827
|
+
if (protectionCheck)
|
|
828
|
+
return protectionCheck;
|
|
716
829
|
// Confirmation gate
|
|
717
830
|
if (!confirmed) {
|
|
718
831
|
const chunkInfo = groupId ? ` (chunked, all chunks will be deleted)` : '';
|
|
@@ -722,6 +835,7 @@ export async function opDeleteNote(clients, id, confirmed = false) {
|
|
|
722
835
|
message: `CONFIRM DELETE — This note will be permanently removed${chunkInfo}:\n\n${preview}\n\nTo proceed, call delete_note again with confirmed: true.`,
|
|
723
836
|
};
|
|
724
837
|
}
|
|
838
|
+
await audit(clients, id, meta, 'delete', { content: existing.content, metadata: meta });
|
|
725
839
|
if (groupId) {
|
|
726
840
|
const { error } = await clients.supabase.from('notes').delete().eq('metadata->>chunk_group', groupId);
|
|
727
841
|
if (error)
|