hub-proposal-review-triggers.mjs
166 lines 5.9 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Deterministic review triggers: phrases, path prefixes, labels → pending + queue metadata.
3 * Override: data/hub_proposal_review_triggers.json (same shape as packaged default).
4 */
5
6 import fs from 'fs';
7 import path from 'path';
8 import { fileURLToPath } from 'url';
9 import { normalizePathPrefix, notePathMatchesPrefix } from './write.mjs';
10
11 // Netlify gateway bundle: import.meta.url may be missing at load time — never throw here.
12 function libDirname() {
13 try {
14 const u = typeof import.meta !== 'undefined' ? import.meta.url : '';
15 if (u) return path.dirname(fileURLToPath(u));
16 } catch (_) {}
17 return path.join(process.cwd(), 'lib');
18 }
19
20 const __dirname = libDirname();
21 const PACKAGED_DEFAULT = path.join(__dirname, '..', 'hub', 'proposal-review-triggers-default.json');
22
23 const MAX_PHRASES = 200;
24 const MAX_PHRASE_LEN = 128;
25 const MAX_PREFIX_RULES = 64;
26 const MAX_LABEL_RULES = 64;
27
28 /**
29 * @returns {{
30 * literal_phrases: { match: string, review_queue?: string, review_severity?: string }[],
31 * path_prefixes: { prefix: string, review_queue?: string, review_severity?: string }[],
32 * label_any: { labels: string[], review_queue?: string, review_severity?: string }[],
33 * }}
34 */
35 export function loadReviewTriggers(dataDir) {
36 const overridePath = path.join(dataDir, 'hub_proposal_review_triggers.json');
37 let raw;
38 if (fs.existsSync(overridePath)) {
39 try {
40 raw = JSON.parse(fs.readFileSync(overridePath, 'utf8'));
41 } catch {
42 raw = null;
43 }
44 }
45 if (!raw || typeof raw !== 'object') {
46 try {
47 raw = JSON.parse(fs.readFileSync(PACKAGED_DEFAULT, 'utf8'));
48 } catch {
49 raw = { literal_phrases: [], path_prefixes: [], label_any: [] };
50 }
51 }
52 return normalizeTriggers(raw);
53 }
54
55 function normalizeTriggers(raw) {
56 const literal_phrases = [];
57 const arr = Array.isArray(raw.literal_phrases) ? raw.literal_phrases : [];
58 for (const row of arr.slice(0, MAX_PHRASES)) {
59 if (!row || typeof row !== 'object') continue;
60 const m = typeof row.match === 'string' ? row.match.trim().slice(0, MAX_PHRASE_LEN) : '';
61 if (!m) continue;
62 literal_phrases.push({
63 match: m,
64 review_queue: normQueue(row.review_queue),
65 review_severity: normSeverity(row.review_severity),
66 });
67 }
68 const path_prefixes = [];
69 const pfx = Array.isArray(raw.path_prefixes) ? raw.path_prefixes : [];
70 for (const row of pfx.slice(0, MAX_PREFIX_RULES)) {
71 if (!row || typeof row !== 'object') continue;
72 const pr = typeof row.prefix === 'string' ? row.prefix.trim().replace(/\\/g, '/') : '';
73 if (!pr) continue;
74 let prefixNorm;
75 try {
76 prefixNorm = normalizePathPrefix(pr);
77 } catch {
78 continue;
79 }
80 path_prefixes.push({
81 prefix: prefixNorm,
82 review_queue: normQueue(row.review_queue),
83 review_severity: normSeverity(row.review_severity),
84 });
85 }
86 const label_any = [];
87 const lab = Array.isArray(raw.label_any) ? raw.label_any : [];
88 for (const row of lab.slice(0, MAX_LABEL_RULES)) {
89 if (!row || typeof row !== 'object') continue;
90 const labels = Array.isArray(row.labels)
91 ? [...new Set(row.labels.map((x) => String(x).trim().toLowerCase()).filter(Boolean))].slice(0, 32)
92 : [];
93 if (!labels.length) continue;
94 label_any.push({
95 labels,
96 review_queue: normQueue(row.review_queue),
97 review_severity: normSeverity(row.review_severity),
98 });
99 }
100 return { literal_phrases, path_prefixes, label_any };
101 }
102
103 function normQueue(v) {
104 if (v == null || typeof v !== 'string') return undefined;
105 const s = v.trim().slice(0, 64);
106 return s || undefined;
107 }
108
109 function normSeverity(v) {
110 if (v === 'elevated' || v === 'standard') return v;
111 return undefined;
112 }
113
114 /**
115 * @param {ReturnType<typeof loadReviewTriggers>} triggers
116 * @param {{ path: string, body: string, intent?: string, labels: string[] }} input
117 * @returns {{ forcePending: boolean, review_queue?: string, review_severity?: 'standard'|'elevated', auto_flag_reasons: string[] }}
118 */
119 export function applyReviewTriggers(triggers, input) {
120 const reasons = [];
121 let forcePending = false;
122 /** @type {string|undefined} */
123 let review_queue;
124 /** @type {'standard'|'elevated'|undefined} */
125 let review_severity;
126
127 const pathNorm = String(input.path || '').replace(/\\/g, '/');
128 const body = String(input.body || '');
129 const intent = String(input.intent || '');
130 const labelSet = new Set((input.labels || []).map((x) => String(x).trim().toLowerCase()).filter(Boolean));
131 const haystack = `${pathNorm}\n${body}\n${intent}`.toLowerCase();
132
133 for (const rule of triggers.literal_phrases) {
134 const needle = rule.match.toLowerCase();
135 if (needle && haystack.includes(needle)) {
136 forcePending = true;
137 reasons.push(`phrase:${rule.match.slice(0, 48)}`);
138 if (rule.review_queue) review_queue = rule.review_queue;
139 if (rule.review_severity === 'elevated') review_severity = 'elevated';
140 else if (rule.review_severity === 'standard' && review_severity !== 'elevated') review_severity = 'standard';
141 }
142 }
143
144 for (const rule of triggers.path_prefixes) {
145 if (notePathMatchesPrefix(pathNorm, rule.prefix)) {
146 forcePending = true;
147 reasons.push(`path_prefix:${rule.prefix}`);
148 if (rule.review_queue) review_queue = rule.review_queue;
149 if (rule.review_severity === 'elevated') review_severity = 'elevated';
150 else if (rule.review_severity === 'standard' && review_severity !== 'elevated') review_severity = 'standard';
151 }
152 }
153
154 for (const rule of triggers.label_any) {
155 const hit = rule.labels.some((l) => labelSet.has(l));
156 if (hit) {
157 forcePending = true;
158 reasons.push(`label_any:${rule.labels.join(',')}`);
159 if (rule.review_queue) review_queue = rule.review_queue;
160 if (rule.review_severity === 'elevated') review_severity = 'elevated';
161 else if (rule.review_severity === 'standard' && review_severity !== 'elevated') review_severity = 'standard';
162 }
163 }
164
165 return { forcePending, review_queue, review_severity, auto_flag_reasons: reasons };
166 }
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 2 days ago