companion-artifact-writer.mjs
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb
reconcile: import GitHub-direct RBAC/OAuth/companion and ho…
Human
minor
⚠ breaking
4 hours ago
| 1 | /** |
| 2 | * Phase 6 — DerivedArtifactWriter: single write + delete path (D6.6). |
| 3 | * |
| 4 | * This is the ONLY module that may persist or delete a derived artifact in any store: |
| 5 | * - Note frontmatter (ai_summary + provenance sidecar) via writeNoteFn |
| 6 | * - Vector store (embeddings) via vectorStore.upsert / deleteByPath |
| 7 | * - Memory insight events via MemoryManager.store('insight', ...) |
| 8 | * |
| 9 | * AUTHORITY GROUP ONLY (D6.4.5, D6.6.3): |
| 10 | * This module MUST NEVER be imported by: |
| 11 | * - lib/companion-runtime-manager.mjs (runtime group) |
| 12 | * - lib/companion-spawn-adapter.mjs / companion-download-adapter.mjs (runtime adapters) |
| 13 | * - Any other module in the runtime/inference group |
| 14 | * An architecture test in the security test tier enforces this. |
| 15 | * |
| 16 | * WRITE PIPELINE (D6.6.1 — all steps must succeed, or no write occurs): |
| 17 | * 1. Provenance validation (D6.2 — validateProvenance) |
| 18 | * 2. Tier resolution (D6.1 — resolveTier) |
| 19 | * 3. Consent gate (D6.3 — enforceConsentPolicy, Phase 1 reuse) |
| 20 | * 4. Encryption routing (D6.4 — ClientEncryptor.isAvailable + encrypt) |
| 21 | * 5. Terminal-state store (D6.1.2 — dispatch to the correct physical store) |
| 22 | * |
| 23 | * DELETE PIPELINE (D6.5.4): |
| 24 | * Single call removes from ALL stores for a note/scope. Partial removal is surfaced |
| 25 | * as an error, not silently treated as complete. |
| 26 | * |
| 27 | * RE-ENRICHMENT (D6.7): |
| 28 | * checkReEnrichmentEligibility is advisory only — never forces recompute, |
| 29 | * never routes to the proposal pipeline (D6.7.4). |
| 30 | */ |
| 31 | |
| 32 | import { validateProvenance } from './companion-provenance-validator.mjs'; |
| 33 | import { resolveTier, TERMINAL_STATES } from './companion-tier-resolver.mjs'; |
| 34 | import { enforceConsentPolicy } from './model-runtime-lane.mjs'; |
| 35 | import { |
| 36 | ENCRYPTOR_REASONS, |
| 37 | ENCRYPTOR_SCOPES, |
| 38 | UNAVAILABLE_CLIENT_ENCRYPTOR, |
| 39 | } from './companion-client-encryptor.mjs'; |
| 40 | |
| 41 | /** |
| 42 | * Fixed reason codes for writer failures. |
| 43 | * Only these codes are logged/surfaced — never the raw exception message or any secrets. |
| 44 | * @readonly |
| 45 | */ |
| 46 | export const WRITER_REASONS = Object.freeze({ |
| 47 | /** Provenance record failed D6.2 validation. */ |
| 48 | PROVENANCE_INVALID: 'writer_provenance_invalid', |
| 49 | /** Tier resolution failed — unknown tier or privacy_max without registry. */ |
| 50 | TIER_UNRESOLVABLE: 'writer_tier_unresolvable', |
| 51 | /** enforceConsentPolicy returned lane_policy_denied or cloud_consent_required. */ |
| 52 | CONSENT_DENIED: 'writer_consent_denied', |
| 53 | /** privacy_max + ClientEncryptor.isAvailable === false → fail closed (D6.4.3). */ |
| 54 | ENCRYPTION_UNAVAILABLE: 'writer_encryption_unavailable', |
| 55 | /** ClientEncryptor.encrypt threw or returned a malformed result. */ |
| 56 | ENCRYPTION_FAILED: 'writer_encryption_failed', |
| 57 | /** Physical store operation failed. */ |
| 58 | STORE_FAILED: 'writer_store_failed', |
| 59 | /** All delete stores failed. */ |
| 60 | DELETE_FAILED: 'writer_delete_failed', |
| 61 | /** Some delete stores succeeded, some failed — partial removal. */ |
| 62 | DELETE_PARTIAL: 'writer_delete_partial', |
| 63 | /** |
| 64 | * Cross-partition (delegated) write requested before the tenancy gate exists (D6.3.6). |
| 65 | * Only self-partition writes are enabled until the tenancy identity lands. |
| 66 | */ |
| 67 | SELF_PARTITION_ONLY: 'writer_self_partition_only', |
| 68 | }); |
| 69 | |
| 70 | /** |
| 71 | * @typedef {{ |
| 72 | * writeNoteFn: (vaultPath: string, notePath: string, opts: object) => void, |
| 73 | * vaultPath: string, |
| 74 | * vectorStore?: ({ upsert: (points: object[]) => Promise<void>, deleteByPath?: (notePath: string) => Promise<void> }) | null, |
| 75 | * mm?: import('./memory.mjs').MemoryManager | null, |
| 76 | * encryptor?: import('./companion-client-encryptor.mjs').ClientEncryptorLike, |
| 77 | * vaultRegistryAvailable?: boolean, |
| 78 | * }} WriterDeps |
| 79 | */ |
| 80 | |
| 81 | /** |
| 82 | * @typedef {{ |
| 83 | * write: (artifact: object, provenance: object, context: WriteContext) => Promise<WriteResult>, |
| 84 | * deleteArtifacts: (target: DeleteTarget) => Promise<DeleteResult>, |
| 85 | * checkReEnrichmentEligibility: (storedProvenance: object, currentVersions: object) => EligibilityResult, |
| 86 | * }} DerivedArtifactWriter |
| 87 | */ |
| 88 | |
| 89 | /** |
| 90 | * @typedef {{ |
| 91 | * lane: string, |
| 92 | * containsPrivateData: boolean, |
| 93 | * consentId?: string, |
| 94 | * isDelegate?: boolean, |
| 95 | * delegatedManagedAllowed?: boolean, |
| 96 | * enrichesDelegatedPartition?: boolean, |
| 97 | * delegatedEnrichmentAllowed?: boolean, |
| 98 | * }} WriteContext |
| 99 | */ |
| 100 | |
| 101 | /** |
| 102 | * @typedef {{ ok: true; terminalState: string } | { ok: false; reason: string; detail?: string }} WriteResult |
| 103 | */ |
| 104 | |
| 105 | /** |
| 106 | * @typedef {{ notePath?: string; scope?: string }} DeleteTarget |
| 107 | */ |
| 108 | |
| 109 | /** |
| 110 | * @typedef {{ ok: true; stores: string[] } | { ok: false; reason: string; failed: string[] }} DeleteResult |
| 111 | */ |
| 112 | |
| 113 | /** |
| 114 | * @typedef {{ eligible: boolean; reason: string }} EligibilityResult |
| 115 | */ |
| 116 | |
| 117 | /** |
| 118 | * Create a DerivedArtifactWriter capability. |
| 119 | * |
| 120 | * Must be held ONLY by the authority group. Pass it explicitly to callers that |
| 121 | * need to persist derived artifacts (enrichIndexedNotes, runDiscoverPass) — never |
| 122 | * import it from within the runtime/inference group. |
| 123 | * |
| 124 | * @param {WriterDeps} deps |
| 125 | * @returns {DerivedArtifactWriter} |
| 126 | */ |
| 127 | export function createDerivedArtifactWriter(deps) { |
| 128 | const { |
| 129 | writeNoteFn, |
| 130 | vaultPath, |
| 131 | vectorStore = null, |
| 132 | mm = null, |
| 133 | encryptor = UNAVAILABLE_CLIENT_ENCRYPTOR, |
| 134 | vaultRegistryAvailable = false, |
| 135 | } = deps; |
| 136 | |
| 137 | if (typeof writeNoteFn !== 'function') { |
| 138 | throw new TypeError('DerivedArtifactWriter: writeNoteFn must be a function.'); |
| 139 | } |
| 140 | if (typeof vaultPath !== 'string' || !vaultPath.trim()) { |
| 141 | throw new TypeError('DerivedArtifactWriter: vaultPath must be a non-empty string.'); |
| 142 | } |
| 143 | |
| 144 | /** |
| 145 | * Execute the 5-step write pipeline (D6.6.1). |
| 146 | * |
| 147 | * @param {object} artifact - The derived artifact payload (e.g. { summary: '...' }). |
| 148 | * @param {object} provenance - The canonical provenance record per D6.2.1. |
| 149 | * @param {WriteContext} context - Consent/authorization context. |
| 150 | * @returns {Promise<WriteResult>} |
| 151 | */ |
| 152 | async function write(artifact, provenance, context) { |
| 153 | // D6.3.6 — Cross-partition writes disabled until tenancy gate exists. |
| 154 | // enrichesDelegatedPartition=true means actor ≠ partition owner (D6.3.2). |
| 155 | if (context.enrichesDelegatedPartition === true) { |
| 156 | return { ok: false, reason: WRITER_REASONS.SELF_PARTITION_ONLY }; |
| 157 | } |
| 158 | |
| 159 | // ── Step 1: Provenance validation (D6.2) ───────────────────────────────── |
| 160 | const provenanceCheck = validateProvenance(provenance, artifact); |
| 161 | if (!provenanceCheck.ok) { |
| 162 | return { |
| 163 | ok: false, |
| 164 | reason: WRITER_REASONS.PROVENANCE_INVALID, |
| 165 | detail: provenanceCheck.reason, |
| 166 | }; |
| 167 | } |
| 168 | |
| 169 | // ── Step 2: Tier resolution (D6.1) ─────────────────────────────────────── |
| 170 | const tierResult = resolveTier( |
| 171 | /** @type {string} */ (provenance.artifact_type), |
| 172 | /** @type {string} */ (provenance.privacy_tier), |
| 173 | { vaultRegistryAvailable }, |
| 174 | ); |
| 175 | if (!tierResult.ok) { |
| 176 | return { ok: false, reason: WRITER_REASONS.TIER_UNRESOLVABLE, detail: tierResult.reason }; |
| 177 | } |
| 178 | const { terminalState } = tierResult; |
| 179 | |
| 180 | // ── Step 3: Consent gate (D6.3 — Phase 1 enforceConsentPolicy reuse) ───── |
| 181 | const consentDecision = enforceConsentPolicy({ |
| 182 | lane: context.lane, |
| 183 | containsPrivateData: context.containsPrivateData, |
| 184 | consentId: context.consentId, |
| 185 | isDelegate: context.isDelegate ?? false, |
| 186 | delegatedManagedAllowed: context.delegatedManagedAllowed ?? false, |
| 187 | enrichesDelegatedPartition: context.enrichesDelegatedPartition ?? false, |
| 188 | delegatedEnrichmentAllowed: context.delegatedEnrichmentAllowed ?? false, |
| 189 | }); |
| 190 | if (consentDecision !== 'allow') { |
| 191 | return { ok: false, reason: WRITER_REASONS.CONSENT_DENIED, detail: consentDecision }; |
| 192 | } |
| 193 | |
| 194 | // ── Step 4: Encryption routing (D6.4) ──────────────────────────────────── |
| 195 | let encryptedPayload = null; |
| 196 | if (terminalState === TERMINAL_STATES.CLIENT_ENCRYPTED) { |
| 197 | const scope = _scopeForArtifact(/** @type {string} */ (provenance.artifact_type)); |
| 198 | |
| 199 | // Require ClientEncryptor with user-held key — fail closed if absent (D6.4.2, D6.4.3) |
| 200 | if (!encryptor.isAvailable(/** @type {string} */ (provenance.privacy_tier), scope)) { |
| 201 | return { ok: false, reason: WRITER_REASONS.ENCRYPTION_UNAVAILABLE }; |
| 202 | } |
| 203 | |
| 204 | try { |
| 205 | const plaintext = new TextEncoder().encode(JSON.stringify({ artifact, provenance: _safeProvenanceFields(provenance) })); |
| 206 | encryptedPayload = encryptor.encrypt(plaintext, { scope }); |
| 207 | } catch (err) { |
| 208 | // Surface a fixed reason code — never the raw exception (may contain key material) |
| 209 | const detail = |
| 210 | err instanceof Error && err.message === ENCRYPTOR_REASONS.UNAVAILABLE |
| 211 | ? ENCRYPTOR_REASONS.UNAVAILABLE |
| 212 | : ENCRYPTOR_REASONS.ENCRYPT_FAILED; |
| 213 | return { ok: false, reason: WRITER_REASONS.ENCRYPTION_FAILED, detail }; |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | // ── Step 5: Terminal-state store (D6.1.2) ──────────────────────────────── |
| 218 | try { |
| 219 | await _storeArtifact({ |
| 220 | artifact, |
| 221 | provenance, |
| 222 | terminalState, |
| 223 | encryptedPayload, |
| 224 | writeNoteFn, |
| 225 | vaultPath, |
| 226 | vectorStore, |
| 227 | mm, |
| 228 | }); |
| 229 | } catch { |
| 230 | // Never re-throw the raw error (may contain path or data details) |
| 231 | return { ok: false, reason: WRITER_REASONS.STORE_FAILED }; |
| 232 | } |
| 233 | |
| 234 | return { ok: true, terminalState }; |
| 235 | } |
| 236 | |
| 237 | /** |
| 238 | * Delete all derived artifacts for a note path from ALL stores (D6.5.4). |
| 239 | * A single deletion call removes frontmatter ai_summary, vector entry, |
| 240 | * and stale-flags aggregate insights. Partial removal is an error. |
| 241 | * |
| 242 | * @param {DeleteTarget} target |
| 243 | * @returns {Promise<DeleteResult>} |
| 244 | */ |
| 245 | async function deleteArtifacts(target) { |
| 246 | const failed = []; |
| 247 | const succeeded = []; |
| 248 | |
| 249 | // 1. Remove ai_summary + provenance sidecar from note frontmatter (D6.5.1) |
| 250 | if (target.notePath) { |
| 251 | try { |
| 252 | writeNoteFn(vaultPath, target.notePath, { |
| 253 | frontmatter: { |
| 254 | ai_summary: null, |
| 255 | ai_summary_provenance: null, |
| 256 | ai_summary_ciphertext: null, |
| 257 | }, |
| 258 | }); |
| 259 | succeeded.push('frontmatter'); |
| 260 | } catch { |
| 261 | failed.push('frontmatter'); |
| 262 | } |
| 263 | } |
| 264 | |
| 265 | // 2. Purge vector entry keyed by note path (D6.5.1) |
| 266 | if (target.notePath && vectorStore != null && typeof vectorStore.deleteByPath === 'function') { |
| 267 | try { |
| 268 | await vectorStore.deleteByPath(target.notePath); |
| 269 | succeeded.push('vector'); |
| 270 | } catch { |
| 271 | failed.push('vector'); |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | // 3. Stale-flag aggregate insights referencing this note (D6.5.2). |
| 276 | // Aggregate insights are NOT deleted when a source note is deleted — instead, |
| 277 | // a maintenance event records the deletion so runVerifyPass marks them stale |
| 278 | // and D6.7 re-enrichment is triggered. |
| 279 | if (target.notePath && mm != null) { |
| 280 | try { |
| 281 | mm.store('maintenance', { |
| 282 | deleted_note_path: target.notePath, |
| 283 | action: 'artifact_delete_requested', |
| 284 | }); |
| 285 | succeeded.push('memory_stale_flag'); |
| 286 | } catch { |
| 287 | failed.push('memory_stale_flag'); |
| 288 | } |
| 289 | } |
| 290 | |
| 291 | if (failed.length === 0) { |
| 292 | return { ok: true, stores: succeeded }; |
| 293 | } |
| 294 | |
| 295 | return { |
| 296 | ok: false, |
| 297 | reason: succeeded.length === 0 ? WRITER_REASONS.DELETE_FAILED : WRITER_REASONS.DELETE_PARTIAL, |
| 298 | failed, |
| 299 | }; |
| 300 | } |
| 301 | |
| 302 | /** |
| 303 | * Advisory re-enrichment eligibility check (D6.7.1). |
| 304 | * |
| 305 | * Returns eligible=true when the active model/runtime version is newer than the |
| 306 | * stored artifact's recorded version. This is a FLAG only — never forces recompute, |
| 307 | * never routes the note to the proposal pipeline (D6.7.4). |
| 308 | * |
| 309 | * Fail-closed (D6.7 §fail-closed): unknown/unparseable version → eligible=true |
| 310 | * (safe: recompute rather than trust potentially stale artifact). |
| 311 | * |
| 312 | * @param {{ model_version?: string | null, runtime_version?: string | null }} storedProvenance |
| 313 | * @param {{ model_version?: string | null, runtime_version?: string | null }} currentVersions |
| 314 | * @returns {EligibilityResult} |
| 315 | */ |
| 316 | function checkReEnrichmentEligibility(storedProvenance, currentVersions) { |
| 317 | if (!storedProvenance || !currentVersions) { |
| 318 | return { eligible: true, reason: 're_enrichment_unknown_stored_version' }; |
| 319 | } |
| 320 | |
| 321 | const storedMV = typeof storedProvenance.model_version === 'string' && storedProvenance.model_version.trim() |
| 322 | ? storedProvenance.model_version |
| 323 | : null; |
| 324 | const storedRV = typeof storedProvenance.runtime_version === 'string' && storedProvenance.runtime_version.trim() |
| 325 | ? storedProvenance.runtime_version |
| 326 | : null; |
| 327 | const currentMV = typeof currentVersions.model_version === 'string' && currentVersions.model_version.trim() |
| 328 | ? currentVersions.model_version |
| 329 | : null; |
| 330 | const currentRV = typeof currentVersions.runtime_version === 'string' && currentVersions.runtime_version.trim() |
| 331 | ? currentVersions.runtime_version |
| 332 | : null; |
| 333 | |
| 334 | // Both stored versions absent → eligible (stale by default) |
| 335 | if (!storedMV && !storedRV) { |
| 336 | return { eligible: true, reason: 're_enrichment_stored_version_absent' }; |
| 337 | } |
| 338 | |
| 339 | // Current version unknown → eligible (safe conservative default) |
| 340 | if (!currentMV && !currentRV) { |
| 341 | return { eligible: true, reason: 're_enrichment_current_version_unknown' }; |
| 342 | } |
| 343 | |
| 344 | // model_version mismatch |
| 345 | if (storedMV && currentMV && currentMV !== storedMV) { |
| 346 | return { eligible: true, reason: 're_enrichment_model_version_newer' }; |
| 347 | } |
| 348 | |
| 349 | // runtime_version mismatch |
| 350 | if (storedRV && currentRV && currentRV !== storedRV) { |
| 351 | return { eligible: true, reason: 're_enrichment_runtime_version_newer' }; |
| 352 | } |
| 353 | |
| 354 | return { eligible: false, reason: 're_enrichment_version_current' }; |
| 355 | } |
| 356 | |
| 357 | return Object.freeze({ write, deleteArtifacts, checkReEnrichmentEligibility }); |
| 358 | } |
| 359 | |
| 360 | // ── Internal helpers ────────────────────────────────────────────────────────── |
| 361 | |
| 362 | /** |
| 363 | * Map artifact type to the encryptor scope (D6.4.1 / ENCRYPTOR_SCOPES). |
| 364 | * ai_summary + embedding live in the vault; insight + discovery_facet in memory. |
| 365 | * @param {string} artifactType |
| 366 | * @returns {'vault'|'memory'} |
| 367 | */ |
| 368 | function _scopeForArtifact(artifactType) { |
| 369 | return artifactType === 'ai_summary' || artifactType === 'embedding' |
| 370 | ? ENCRYPTOR_SCOPES.VAULT |
| 371 | : ENCRYPTOR_SCOPES.MEMORY; |
| 372 | } |
| 373 | |
| 374 | /** |
| 375 | * Return only the safe, non-secret provenance fields for storage in frontmatter/sidecar. |
| 376 | * Defense-in-depth: provenance validator already rejects secrets, but we whitelist here |
| 377 | * to guarantee no novel field with a secret ever appears in a stored artifact. |
| 378 | * @param {object} provenance |
| 379 | * @returns {object} |
| 380 | */ |
| 381 | function _safeProvenanceFields(provenance) { |
| 382 | const SAFE = [ |
| 383 | 'generated_by', 'source', 'model', 'model_version', 'runtime_version', |
| 384 | 'lane', 'privacy_tier', 'source_note_path', 'source_event_id', |
| 385 | 'created_at', 'artifact_type', 'schema_version', |
| 386 | ]; |
| 387 | const result = /** @type {Record<string, unknown>} */ ({}); |
| 388 | for (const k of SAFE) { |
| 389 | if (k in provenance) result[k] = /** @type {any} */ (provenance)[k]; |
| 390 | } |
| 391 | return result; |
| 392 | } |
| 393 | |
| 394 | /** |
| 395 | * Dispatch terminal-state storage to the correct physical store. |
| 396 | * Called ONLY by write() after all gates pass. |
| 397 | * |
| 398 | * @param {{ |
| 399 | * artifact: object, |
| 400 | * provenance: object, |
| 401 | * terminalState: string, |
| 402 | * encryptedPayload: import('./companion-client-encryptor.mjs').EncryptResult | null, |
| 403 | * writeNoteFn: Function, |
| 404 | * vaultPath: string, |
| 405 | * vectorStore: object | null, |
| 406 | * mm: object | null, |
| 407 | * }} params |
| 408 | * @returns {Promise<void>} |
| 409 | */ |
| 410 | async function _storeArtifact({ artifact, provenance, terminalState, encryptedPayload, writeNoteFn, vaultPath, vectorStore, mm }) { |
| 411 | const { artifact_type, source_note_path } = /** @type {any} */ (provenance); |
| 412 | const safeProv = _safeProvenanceFields(provenance); |
| 413 | |
| 414 | if (terminalState === TERMINAL_STATES.HOST_READABLE) { |
| 415 | switch (artifact_type) { |
| 416 | case 'ai_summary': { |
| 417 | if (!source_note_path) throw new Error('ai_summary requires source_note_path'); |
| 418 | writeNoteFn(vaultPath, source_note_path, { |
| 419 | frontmatter: { |
| 420 | ai_summary: artifact.summary, |
| 421 | ai_summary_provenance: safeProv, |
| 422 | }, |
| 423 | }); |
| 424 | break; |
| 425 | } |
| 426 | case 'embedding': { |
| 427 | if (!vectorStore) throw new Error('embedding requires vectorStore'); |
| 428 | await vectorStore.upsert([{ |
| 429 | path: source_note_path, |
| 430 | vector: artifact.vector, |
| 431 | payload: { provenance: safeProv, ...(artifact.payload ?? {}) }, |
| 432 | }]); |
| 433 | break; |
| 434 | } |
| 435 | case 'insight': |
| 436 | case 'discovery_facet': { |
| 437 | if (!mm) throw new Error('insight/discovery_facet requires MemoryManager'); |
| 438 | mm.store('insight', { |
| 439 | connections: artifact.connections ?? [], |
| 440 | contradictions: artifact.contradictions ?? [], |
| 441 | open_questions: artifact.open_questions ?? [], |
| 442 | topic_count: artifact.topic_count ?? 0, |
| 443 | provenance: safeProv, |
| 444 | }); |
| 445 | break; |
| 446 | } |
| 447 | default: |
| 448 | throw new Error(`Unknown artifact_type: ${artifact_type}`); |
| 449 | } |
| 450 | |
| 451 | } else if (terminalState === TERMINAL_STATES.CLIENT_ENCRYPTED) { |
| 452 | // privacy_max: store ciphertext + wrappedDekRef only — host never sees plaintext (D6.4.2 step 3) |
| 453 | if (!encryptedPayload) throw new Error('CLIENT_ENCRYPTED requires encryptedPayload'); |
| 454 | const ciphertextB64 = Buffer.from(encryptedPayload.ciphertext).toString('base64'); |
| 455 | const provenanceMeta = { |
| 456 | wrapped_dek_ref: encryptedPayload.wrappedDekRef, |
| 457 | alg: encryptedPayload.alg, |
| 458 | schema_version: provenance.schema_version, |
| 459 | artifact_type, |
| 460 | }; |
| 461 | |
| 462 | switch (artifact_type) { |
| 463 | case 'ai_summary': { |
| 464 | if (!source_note_path) throw new Error('ai_summary requires source_note_path'); |
| 465 | writeNoteFn(vaultPath, source_note_path, { |
| 466 | frontmatter: { |
| 467 | ai_summary_ciphertext: ciphertextB64, |
| 468 | ai_summary_provenance: provenanceMeta, |
| 469 | }, |
| 470 | }); |
| 471 | break; |
| 472 | } |
| 473 | case 'embedding': { |
| 474 | if (!vectorStore) throw new Error('embedding requires vectorStore'); |
| 475 | await vectorStore.upsert([{ |
| 476 | path: source_note_path, |
| 477 | ciphertext: ciphertextB64, |
| 478 | wrapped_dek_ref: encryptedPayload.wrappedDekRef, |
| 479 | alg: encryptedPayload.alg, |
| 480 | provenance_schema_version: provenance.schema_version, |
| 481 | }]); |
| 482 | break; |
| 483 | } |
| 484 | case 'insight': |
| 485 | case 'discovery_facet': { |
| 486 | if (!mm) throw new Error('insight/discovery_facet requires MemoryManager'); |
| 487 | mm.store('insight', { |
| 488 | ciphertext: ciphertextB64, |
| 489 | wrapped_dek_ref: encryptedPayload.wrappedDekRef, |
| 490 | alg: encryptedPayload.alg, |
| 491 | schema_version: provenance.schema_version, |
| 492 | }); |
| 493 | break; |
| 494 | } |
| 495 | default: |
| 496 | throw new Error(`Unknown artifact_type: ${artifact_type}`); |
| 497 | } |
| 498 | |
| 499 | } |
| 500 | // TERMINAL_STATES.LOCAL_ONLY: nothing persisted to remote stores. |
| 501 | // The caller handles local-only caching outside this writer — the writer |
| 502 | // simply returns { ok: true, terminalState: 'local_only' } as confirmation. |
| 503 | } |
File History
1 commit
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb
reconcile: import GitHub-direct RBAC/OAuth/companion and ho…
Human
minor
⚠
4 hours ago