proposal-llm-store.mjs
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠ breaking
1 day ago
| 1 | /** |
| 2 | * Hosted proposal LLM prefs: data/hosted_proposal_llm_prefs.json or Netlify Blob (gateway-billing store). |
| 3 | * Same blob accessor as billing; distinct key. |
| 4 | */ |
| 5 | import fs from 'fs/promises'; |
| 6 | import path from 'path'; |
| 7 | import { fileURLToPath } from 'url'; |
| 8 | import { getProposalEvaluationRequired } from '../../lib/hub-proposal-policy.mjs'; |
| 9 | |
| 10 | let projectRoot; |
| 11 | try { |
| 12 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); |
| 13 | projectRoot = path.resolve(__dirname, '..', '..'); |
| 14 | } catch (_) { |
| 15 | projectRoot = process.cwd(); |
| 16 | } |
| 17 | |
| 18 | const PREFS_FILE = path.join(projectRoot, 'data', 'hosted_proposal_llm_prefs.json'); |
| 19 | const BLOB_KEY = 'proposal-llm-prefs-v1'; |
| 20 | |
| 21 | function getBlobStore() { |
| 22 | return globalThis.__knowtation_gateway_blob; |
| 23 | } |
| 24 | |
| 25 | /** @param {unknown} v */ |
| 26 | function envTriState(v) { |
| 27 | if (v === '1' || v === 'true') return true; |
| 28 | if (v === '0' || v === 'false') return false; |
| 29 | return null; |
| 30 | } |
| 31 | |
| 32 | function emptyPrefs() { |
| 33 | return { |
| 34 | proposal_evaluation_required: false, |
| 35 | review_hints_enabled: false, |
| 36 | enrich_enabled: false, |
| 37 | }; |
| 38 | } |
| 39 | |
| 40 | function normalizePrefs(raw) { |
| 41 | const d = raw && typeof raw === 'object' ? raw : {}; |
| 42 | const out = emptyPrefs(); |
| 43 | if (typeof d.proposal_evaluation_required === 'boolean') out.proposal_evaluation_required = d.proposal_evaluation_required; |
| 44 | if (typeof d.review_hints_enabled === 'boolean') out.review_hints_enabled = d.review_hints_enabled; |
| 45 | if (typeof d.enrich_enabled === 'boolean') out.enrich_enabled = d.enrich_enabled; |
| 46 | return out; |
| 47 | } |
| 48 | |
| 49 | async function readFromBlob() { |
| 50 | const store = getBlobStore(); |
| 51 | if (!store) return null; |
| 52 | const raw = await store.get(BLOB_KEY, { type: 'json' }); |
| 53 | return normalizePrefs(raw); |
| 54 | } |
| 55 | |
| 56 | async function writeToBlob(prefs) { |
| 57 | const store = getBlobStore(); |
| 58 | if (!store) throw new Error('Netlify Blob store not configured'); |
| 59 | await store.setJSON(BLOB_KEY, prefs); |
| 60 | } |
| 61 | |
| 62 | async function readFromFile() { |
| 63 | try { |
| 64 | const raw = await fs.readFile(PREFS_FILE, 'utf8'); |
| 65 | return normalizePrefs(JSON.parse(raw)); |
| 66 | } catch (e) { |
| 67 | if (e.code === 'ENOENT') return emptyPrefs(); |
| 68 | throw e; |
| 69 | } |
| 70 | } |
| 71 | |
| 72 | async function writeToFile(prefs) { |
| 73 | await fs.mkdir(path.dirname(PREFS_FILE), { recursive: true }); |
| 74 | await fs.writeFile(PREFS_FILE, JSON.stringify(prefs, null, 2), 'utf8'); |
| 75 | } |
| 76 | |
| 77 | export async function loadHostedProposalLlmPrefs() { |
| 78 | if (getBlobStore()) { |
| 79 | const fromBlob = await readFromBlob(); |
| 80 | if (fromBlob) return fromBlob; |
| 81 | return emptyPrefs(); |
| 82 | } |
| 83 | return readFromFile(); |
| 84 | } |
| 85 | |
| 86 | export async function saveHostedProposalLlmPrefs(prefs) { |
| 87 | if (getBlobStore()) { |
| 88 | await writeToBlob(prefs); |
| 89 | } else { |
| 90 | await writeToFile(prefs); |
| 91 | } |
| 92 | } |
| 93 | |
| 94 | /** |
| 95 | * @param {Awaited<ReturnType<typeof loadHostedProposalLlmPrefs>>} prefs |
| 96 | * @param {string} dataDir - e.g. path.join(projectRoot, 'data') |
| 97 | */ |
| 98 | export function effectiveHostedEvaluationRequired(prefs, dataDir) { |
| 99 | const fromEnv = envTriState(process.env.HUB_PROPOSAL_EVALUATION_REQUIRED); |
| 100 | if (fromEnv !== null) return fromEnv; |
| 101 | if (typeof prefs?.proposal_evaluation_required === 'boolean') return prefs.proposal_evaluation_required; |
| 102 | return getProposalEvaluationRequired(dataDir); |
| 103 | } |
| 104 | |
| 105 | /** @param {Awaited<ReturnType<typeof loadHostedProposalLlmPrefs>>} prefs */ |
| 106 | export function effectiveHostedReviewHints(prefs) { |
| 107 | const fromEnv = envTriState(process.env.KNOWTATION_HUB_PROPOSAL_REVIEW_HINTS); |
| 108 | if (fromEnv !== null) return fromEnv; |
| 109 | return Boolean(prefs?.review_hints_enabled); |
| 110 | } |
| 111 | |
| 112 | /** @param {Awaited<ReturnType<typeof loadHostedProposalLlmPrefs>>} prefs */ |
| 113 | export function effectiveHostedEnrich(prefs) { |
| 114 | const fromEnv = envTriState(process.env.KNOWTATION_HUB_PROPOSAL_ENRICH); |
| 115 | if (fromEnv !== null) return fromEnv; |
| 116 | return Boolean(prefs?.enrich_enabled); |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * Merge partial into stored prefs (admin UI). Skips keys locked by env. |
| 121 | * @param {Partial<{ proposal_evaluation_required: boolean, review_hints_enabled: boolean, enrich_enabled: boolean }>} partial |
| 122 | */ |
| 123 | export async function mergeHostedProposalLlmPrefs(partial) { |
| 124 | const locks = { |
| 125 | proposal_evaluation_required: envTriState(process.env.HUB_PROPOSAL_EVALUATION_REQUIRED) !== null, |
| 126 | review_hints_enabled: envTriState(process.env.KNOWTATION_HUB_PROPOSAL_REVIEW_HINTS) !== null, |
| 127 | enrich_enabled: envTriState(process.env.KNOWTATION_HUB_PROPOSAL_ENRICH) !== null, |
| 128 | }; |
| 129 | const cur = await loadHostedProposalLlmPrefs(); |
| 130 | const next = { ...cur }; |
| 131 | if (partial.proposal_evaluation_required !== undefined && !locks.proposal_evaluation_required) { |
| 132 | next.proposal_evaluation_required = Boolean(partial.proposal_evaluation_required); |
| 133 | } |
| 134 | if (partial.review_hints_enabled !== undefined && !locks.review_hints_enabled) { |
| 135 | next.review_hints_enabled = Boolean(partial.review_hints_enabled); |
| 136 | } |
| 137 | if (partial.enrich_enabled !== undefined && !locks.enrich_enabled) { |
| 138 | next.enrich_enabled = Boolean(partial.enrich_enabled); |
| 139 | } |
| 140 | await saveHostedProposalLlmPrefs(next); |
| 141 | return next; |
| 142 | } |
File History
2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd
feat(calendar): enforce agent context tiers in retrieval AP…
Human
minor
⚠
1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6
docs: accept Calendar Events v0 spec with Phase 0 security …
Human
1 day ago