approval-log.mjs
147 lines 5.0 KB
Raw
sha256:8d46372e39d2d5a54fd93a8b1c27922fe0d9b22a72197345f1d2c71701cc4ce2 feat(auth): persistent login system + C7 session introspection Human minor ⚠ breaking 17 days ago
1 /**
2 * Materialized approval logs: thin vault markdown under approvals/ (indexed like notes).
3 * Default prefix avoids lib/config DEFAULT_IGNORE of folder name "meta".
4 */
5
6 export const APPROVAL_LOG_PREFIX = 'approvals';
7
8 /**
9 * @param {string} path - vault-relative path
10 * @returns {boolean}
11 */
12 export function isApprovalLogPath(path) {
13 if (path == null || typeof path !== 'string') return false;
14 const n = path.replace(/\\/g, '/');
15 return n === APPROVAL_LOG_PREFIX || n.startsWith(`${APPROVAL_LOG_PREFIX}/`);
16 }
17
18 /**
19 * @param {{ path?: string, frontmatter?: Record<string, unknown> } | null | undefined} note
20 * @returns {boolean}
21 */
22 export function isApprovalLogNote(note) {
23 if (!note) return false;
24 if (isApprovalLogPath(note.path)) return true;
25 const k = note.frontmatter && note.frontmatter.kind;
26 return String(k) === 'approval_log';
27 }
28
29 /**
30 * @param {string} proposalId
31 * @param {string} [approvedAtIso] - ISO timestamp (date prefix from first 10 chars)
32 * @returns {string} vault-relative path
33 */
34 export function approvalLogRelativePath(proposalId, approvedAtIso = new Date().toISOString()) {
35 const day = String(approvedAtIso).slice(0, 10) || '1970-01-01';
36 const safeId = String(proposalId)
37 .replace(/[^a-zA-Z0-9-]/g, '_')
38 .slice(0, 80);
39 return `${APPROVAL_LOG_PREFIX}/${day}-${safeId}.md`;
40 }
41
42 function trunc(s, max) {
43 if (s == null || typeof s !== 'string') return '';
44 const t = s.trim();
45 return t.length <= max ? t : t.slice(0, max) + '…';
46 }
47
48 /**
49 * Payload for writeNote (string frontmatter values).
50 * @param {{
51 * proposalId: string,
52 * targetPath: string,
53 * approvedAt: string,
54 * approvedBy?: string,
55 * proposedBy?: string,
56 * intent?: string,
57 * source?: string,
58 * proposedBodyExcerpt?: string,
59 * }} p
60 * @returns {{ relativePath: string, frontmatter: Record<string, string>, body: string }}
61 */
62 const MAX_PROPOSAL_EXCERPT = 4000;
63
64 export function buildApprovalLogWrite(p) {
65 const relativePath = approvalLogRelativePath(p.proposalId, p.approvedAt);
66 const targetPath = String(p.targetPath || '').trim() || 'unknown';
67 const frontmatter = {
68 kind: 'approval_log',
69 proposal_id: String(p.proposalId),
70 target_path: targetPath,
71 approved_at: String(p.approvedAt),
72 };
73 if (p.approvedBy) frontmatter.approved_by = String(p.approvedBy).trim().slice(0, 512);
74 if (p.proposedBy) frontmatter.proposed_by = String(p.proposedBy).trim().slice(0, 512);
75 const intentT = trunc(p.intent, 400);
76 if (intentT) frontmatter.intent = intentT;
77 const sourceT = trunc(p.source, 120);
78 if (sourceT) frontmatter.source = sourceT;
79
80 const safeTitle = targetPath.replace(/`/g, "'");
81 const bodyLines = [
82 `Approved vault change applied to \`${safeTitle}\`.`,
83 '',
84 `- **Proposal ID:** ${p.proposalId}`,
85 `- **Approved at:** ${p.approvedAt}`,
86 ];
87 if (intentT) bodyLines.push(`- **Intent (summary):** ${intentT}`);
88
89 let body = bodyLines.join('\n');
90 const ex =
91 p.proposedBodyExcerpt != null && String(p.proposedBodyExcerpt).trim()
92 ? String(p.proposedBodyExcerpt)
93 .replace(/\s+/g, ' ')
94 .trim()
95 .slice(0, MAX_PROPOSAL_EXCERPT)
96 : '';
97 if (ex) {
98 body += '\n\n## Proposal excerpt (for search)\n\n' + ex + '\n';
99 }
100
101 return {
102 relativePath,
103 frontmatter,
104 body,
105 };
106 }
107
108 /**
109 * Filter search hits / results by content scope (path-only; no disk read).
110 * @param {{ path: string }[]} hits
111 * @param {'all'|'notes'|'approval_logs'} scope
112 * @returns {{ path: string }[]}
113 */
114 export function filterHitsByContentScope(hits, scope) {
115 if (!hits || !scope || scope === 'all') return hits || [];
116 if (scope === 'approval_logs') return hits.filter((h) => h && isApprovalLogPath(h.path));
117 if (scope === 'notes') return hits.filter((h) => h && !isApprovalLogPath(h.path));
118 return hits;
119 }
120
121 /**
122 * Map content_scope to vector-store folder prefix so ANN runs include path-restricted chunks.
123 * Post-filtering global top-k alone drops approval-log chunks when their similarity ranks below k.
124 * @param {'all'|'notes'|'approval_logs'|undefined} content_scope
125 * @param {string|undefined} userFolder - Hub folder filter (vault-relative prefix)
126 * @returns {{ folder?: string, wideNotesFetch: boolean, impossible: boolean }}
127 */
128 export function resolveSearchFolderForContentScope(content_scope, userFolder) {
129 const cs = content_scope || 'all';
130 const uf =
131 userFolder != null && String(userFolder).trim()
132 ? String(userFolder).trim().replace(/\\/g, '/').replace(/\/$/, '')
133 : '';
134 if (cs === 'all') {
135 return { folder: uf || undefined, wideNotesFetch: false, impossible: false };
136 }
137 if (cs === 'approval_logs') {
138 if (uf && uf !== APPROVAL_LOG_PREFIX && !uf.startsWith(`${APPROVAL_LOG_PREFIX}/`)) {
139 return { wideNotesFetch: false, impossible: true };
140 }
141 return { folder: uf || APPROVAL_LOG_PREFIX, wideNotesFetch: false, impossible: false };
142 }
143 if (cs === 'notes') {
144 return { folder: uf || undefined, wideNotesFetch: true, impossible: false };
145 }
146 return { folder: uf || undefined, wideNotesFetch: false, impossible: false };
147 }
File History 2 commits
sha256:8d46372e39d2d5a54fd93a8b1c27922fe0d9b22a72197345f1d2c71701cc4ce2 feat(auth): persistent login system + C7 session introspection Human minor 17 days ago