proposal-llm-store.mjs
142 lines 4.8 KB
Raw
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