@cccarv82/freya 1.0.29 → 1.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/web-ui.js +58 -4
- package/cli/web.js +173 -4
- package/package.json +1 -1
package/cli/web-ui.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
lastPlan: '',
|
|
13
13
|
lastApplied: null,
|
|
14
14
|
autoApply: true,
|
|
15
|
-
autoRunReports: false
|
|
15
|
+
autoRunReports: false,
|
|
16
|
+
prettyPublish: true
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
function applyTheme(theme) {
|
|
@@ -205,6 +206,7 @@
|
|
|
205
206
|
localStorage.setItem('freya.dir', $('dir').value);
|
|
206
207
|
try { localStorage.setItem('freya.autoApply', state.autoApply ? '1' : '0'); } catch {}
|
|
207
208
|
try { localStorage.setItem('freya.autoRunReports', state.autoRunReports ? '1' : '0'); } catch {}
|
|
209
|
+
try { localStorage.setItem('freya.prettyPublish', state.prettyPublish ? '1' : '0'); } catch {}
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
function loadLocal() {
|
|
@@ -218,6 +220,11 @@
|
|
|
218
220
|
if (v2 !== null) state.autoRunReports = v2 === '1';
|
|
219
221
|
const cb2 = $('autoRunReports');
|
|
220
222
|
if (cb2) cb2.checked = !!state.autoRunReports;
|
|
223
|
+
|
|
224
|
+
const v3 = localStorage.getItem('freya.prettyPublish');
|
|
225
|
+
if (v3 !== null) state.prettyPublish = v3 === '1';
|
|
226
|
+
const cb3 = $('prettyPublish');
|
|
227
|
+
if (cb3) cb3.checked = !!state.prettyPublish;
|
|
221
228
|
} catch {}
|
|
222
229
|
|
|
223
230
|
const def = (window.__FREYA_DEFAULT_DIR && window.__FREYA_DEFAULT_DIR !== '__FREYA_DEFAULT_DIR__')
|
|
@@ -347,7 +354,9 @@
|
|
|
347
354
|
const pri = (t.priority || '').toUpperCase();
|
|
348
355
|
row.innerHTML = '<div style="display:flex; justify-content:space-between; gap:10px; align-items:center">'
|
|
349
356
|
+ '<div style="min-width:0"><div style="font-weight:700">' + escapeHtml(t.description || '') + '</div>'
|
|
350
|
-
+ '<div style="opacity:.7; font-size:11px; margin-top:4px">' + escapeHtml(String(t.category || ''))
|
|
357
|
+
+ '<div style="opacity:.7; font-size:11px; margin-top:4px">' + escapeHtml(String(t.category || ''))
|
|
358
|
+
+ (t.projectSlug ? (' · <span style="font-family:var(--mono); opacity:.9">[' + escapeHtml(String(t.projectSlug)) + ']</span>') : '')
|
|
359
|
+
+ (pri ? (' · ' + escapeHtml(pri)) : '') + '</div></div>'
|
|
351
360
|
+ '<button class="btn small" type="button">Complete</button>'
|
|
352
361
|
+ '</div>';
|
|
353
362
|
const btn = row.querySelector('button');
|
|
@@ -383,7 +392,9 @@
|
|
|
383
392
|
const sev = String(b.severity || '').toUpperCase();
|
|
384
393
|
row.innerHTML = '<div style="display:flex; justify-content:space-between; gap:10px; align-items:center">'
|
|
385
394
|
+ '<div style="min-width:0"><div style="font-weight:800">' + escapeHtml(sev) + '</div>'
|
|
386
|
-
+ '<div style="margin-top:4px">' + escapeHtml(b.title || '')
|
|
395
|
+
+ '<div style="margin-top:4px">' + escapeHtml(b.title || '')
|
|
396
|
+
+ (b.projectSlug ? (' <span style="font-family:var(--mono); opacity:.8">[' + escapeHtml(String(b.projectSlug)) + ']</span>') : '')
|
|
397
|
+
+ '</div>'
|
|
387
398
|
+ '</div>'
|
|
388
399
|
+ '<div style="opacity:.7; font-size:11px; white-space:nowrap">' + escapeHtml(fmtWhen(new Date(b.createdAt || Date.now()).getTime())) + '</div>'
|
|
389
400
|
+ '</div>';
|
|
@@ -515,6 +526,39 @@
|
|
|
515
526
|
}
|
|
516
527
|
}
|
|
517
528
|
|
|
529
|
+
async function reloadSlugRules() {
|
|
530
|
+
try {
|
|
531
|
+
const r = await api('/api/project-slug-map/get', { dir: dirOrDefault() });
|
|
532
|
+
const el = $('slugRules');
|
|
533
|
+
if (el) el.value = JSON.stringify(r.map || { rules: [] }, null, 2);
|
|
534
|
+
setPill('ok', 'rules loaded');
|
|
535
|
+
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
536
|
+
} catch (e) {
|
|
537
|
+
setPill('err', 'rules load failed');
|
|
538
|
+
setOut(String(e && e.message ? e.message : e));
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
async function saveSlugRules() {
|
|
543
|
+
try {
|
|
544
|
+
const el = $('slugRules');
|
|
545
|
+
if (!el) return;
|
|
546
|
+
const raw = String(el.value || '').trim();
|
|
547
|
+
if (!raw) throw new Error('Rules JSON is empty');
|
|
548
|
+
let map;
|
|
549
|
+
try { map = JSON.parse(raw); } catch (e) { throw new Error('Invalid JSON: ' + (e.message || e)); }
|
|
550
|
+
|
|
551
|
+
setPill('run', 'saving rules…');
|
|
552
|
+
const r = await api('/api/project-slug-map/save', { dir: dirOrDefault(), map });
|
|
553
|
+
if (el) el.value = JSON.stringify(r.map || map, null, 2);
|
|
554
|
+
setPill('ok', 'rules saved');
|
|
555
|
+
setTimeout(() => setPill('ok', 'idle'), 800);
|
|
556
|
+
} catch (e) {
|
|
557
|
+
setPill('err', 'rules save failed');
|
|
558
|
+
setOut(String(e && e.message ? e.message : e));
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
518
562
|
async function saveSettings() {
|
|
519
563
|
try {
|
|
520
564
|
saveLocal();
|
|
@@ -556,7 +600,8 @@
|
|
|
556
600
|
const webhookUrl = target === 'discord' ? $('discord').value.trim() : $('teams').value.trim();
|
|
557
601
|
if (!webhookUrl) throw new Error('Configure o webhook antes.');
|
|
558
602
|
setPill('run', 'publish…');
|
|
559
|
-
|
|
603
|
+
const mode = state.prettyPublish ? 'pretty' : 'chunks';
|
|
604
|
+
await api('/api/publish', { webhookUrl, text: state.lastText, mode, allowSecrets: true });
|
|
560
605
|
setPill('ok', 'published');
|
|
561
606
|
} catch (e) {
|
|
562
607
|
setPill('err', 'publish failed');
|
|
@@ -583,6 +628,12 @@
|
|
|
583
628
|
}
|
|
584
629
|
}
|
|
585
630
|
|
|
631
|
+
function togglePrettyPublish() {
|
|
632
|
+
const cb = $('prettyPublish');
|
|
633
|
+
state.prettyPublish = cb ? !!cb.checked : true;
|
|
634
|
+
try { localStorage.setItem('freya.prettyPublish', state.prettyPublish ? '1' : '0'); } catch {}
|
|
635
|
+
}
|
|
636
|
+
|
|
586
637
|
function toggleAutoRunReports() {
|
|
587
638
|
const cb = $('autoRunReports');
|
|
588
639
|
state.autoRunReports = cb ? !!cb.checked : false;
|
|
@@ -754,6 +805,8 @@
|
|
|
754
805
|
window.saveSettings = saveSettings;
|
|
755
806
|
window.refreshReports = refreshReports;
|
|
756
807
|
window.refreshToday = refreshToday;
|
|
808
|
+
window.reloadSlugRules = reloadSlugRules;
|
|
809
|
+
window.saveSlugRules = saveSlugRules;
|
|
757
810
|
window.renderReportsList = renderReportsList;
|
|
758
811
|
window.copyOut = copyOut;
|
|
759
812
|
window.copyPath = copyPath;
|
|
@@ -765,6 +818,7 @@
|
|
|
765
818
|
window.saveAndPlan = saveAndPlan;
|
|
766
819
|
window.toggleAutoApply = toggleAutoApply;
|
|
767
820
|
window.toggleAutoRunReports = toggleAutoRunReports;
|
|
821
|
+
window.togglePrettyPublish = togglePrettyPublish;
|
|
768
822
|
window.applyPlan = applyPlan;
|
|
769
823
|
window.runSuggestedReports = runSuggestedReports;
|
|
770
824
|
})();
|
package/cli/web.js
CHANGED
|
@@ -91,6 +91,66 @@ function writeSettings(workspaceDir, settings) {
|
|
|
91
91
|
return out;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function projectSlugMapPath(workspaceDir) {
|
|
95
|
+
return path.join(workspaceDir, 'data', 'settings', 'project-slug-map.json');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readProjectSlugMap(workspaceDir) {
|
|
99
|
+
const p = projectSlugMapPath(workspaceDir);
|
|
100
|
+
try {
|
|
101
|
+
if (!exists(p)) {
|
|
102
|
+
ensureDir(path.dirname(p));
|
|
103
|
+
const defaults = {
|
|
104
|
+
schemaVersion: 1,
|
|
105
|
+
updatedAt: new Date().toISOString(),
|
|
106
|
+
rules: [
|
|
107
|
+
{ contains: 'fideliza', slug: 'vivo/fidelizacao' },
|
|
108
|
+
{ contains: 'bnpl', slug: 'vivo/bnpl' },
|
|
109
|
+
{ contains: 'dpgc', slug: 'vivo/bnpl/dpgc' },
|
|
110
|
+
{ contains: 'vivo+', slug: 'vivo/vivoplus' }
|
|
111
|
+
]
|
|
112
|
+
};
|
|
113
|
+
fs.writeFileSync(p, JSON.stringify(defaults, null, 2) + '\n', 'utf8');
|
|
114
|
+
return defaults;
|
|
115
|
+
}
|
|
116
|
+
const json = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
117
|
+
if (!json || !Array.isArray(json.rules)) return { schemaVersion: 1, rules: [] };
|
|
118
|
+
return json;
|
|
119
|
+
} catch {
|
|
120
|
+
return { schemaVersion: 1, rules: [] };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function inferProjectSlug(text, map) {
|
|
125
|
+
const t = String(text || '').toLowerCase();
|
|
126
|
+
if (!t.trim()) return '';
|
|
127
|
+
|
|
128
|
+
let base = '';
|
|
129
|
+
const rules = (map && Array.isArray(map.rules)) ? map.rules : [];
|
|
130
|
+
for (const r of rules) {
|
|
131
|
+
if (!r) continue;
|
|
132
|
+
const needle = String(r.contains || '').toLowerCase().trim();
|
|
133
|
+
const slug = String(r.slug || '').trim();
|
|
134
|
+
if (!needle || !slug) continue;
|
|
135
|
+
if (t.includes(needle)) { base = slug; break; }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// CHG tags
|
|
139
|
+
const chg = (t.match(/\bchg\s*0*\d{4,}\b/i) || [])[0];
|
|
140
|
+
const chgNorm = chg ? chg.toLowerCase().replace(/\s+/g, '') : '';
|
|
141
|
+
|
|
142
|
+
// If no base but looks like Vivo context, at least prefix vivo
|
|
143
|
+
if (!base && (t.includes('vivo') || t.includes('vivo+'))) base = 'vivo';
|
|
144
|
+
|
|
145
|
+
if (base && chgNorm) {
|
|
146
|
+
// keep numeric id
|
|
147
|
+
const id = chgNorm.replace(/[^0-9]/g, '');
|
|
148
|
+
if (id) base = base.replace(/\/+$/g, '') + '/chg' + id;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return base;
|
|
152
|
+
}
|
|
153
|
+
|
|
94
154
|
function listReports(workspaceDir) {
|
|
95
155
|
const dir = path.join(workspaceDir, 'docs', 'reports');
|
|
96
156
|
if (!exists(dir)) return [];
|
|
@@ -121,6 +181,36 @@ function listReports(workspaceDir) {
|
|
|
121
181
|
}));
|
|
122
182
|
}
|
|
123
183
|
|
|
184
|
+
function extractTitleFromMarkdown(md) {
|
|
185
|
+
const t = String(md || '');
|
|
186
|
+
const m = /^#\s+(.+)$/m.exec(t);
|
|
187
|
+
if (m && m[1]) return m[1].trim();
|
|
188
|
+
// fallback: first non-empty line
|
|
189
|
+
const line = t.split(/\r?\n/).map((l) => l.trim()).find((l) => l);
|
|
190
|
+
return line ? line.slice(0, 80) : 'Freya report';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function stripFirstH1(md) {
|
|
194
|
+
const t = String(md || '');
|
|
195
|
+
return t.replace(/^#\s+.+\r?\n/, '').trim();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function splitForEmbed(text, limit = 3900) {
|
|
199
|
+
const t = String(text || '');
|
|
200
|
+
if (t.length <= limit) return [t];
|
|
201
|
+
const chunks = [];
|
|
202
|
+
let i = 0;
|
|
203
|
+
while (i < t.length) {
|
|
204
|
+
let end = Math.min(t.length, i + limit);
|
|
205
|
+
// prefer splitting at newline
|
|
206
|
+
const nl = t.lastIndexOf('\n', end);
|
|
207
|
+
if (nl > i + 200) end = nl;
|
|
208
|
+
chunks.push(t.slice(i, end));
|
|
209
|
+
i = end;
|
|
210
|
+
}
|
|
211
|
+
return chunks;
|
|
212
|
+
}
|
|
213
|
+
|
|
124
214
|
function splitForDiscord(text, limit = 1900) {
|
|
125
215
|
const t = String(text || '');
|
|
126
216
|
if (t.length <= limit) return [t];
|
|
@@ -174,14 +264,19 @@ function postJson(url, bodyObj) {
|
|
|
174
264
|
});
|
|
175
265
|
}
|
|
176
266
|
|
|
177
|
-
function postDiscordWebhook(url,
|
|
178
|
-
return postJson(url, { content });
|
|
267
|
+
function postDiscordWebhook(url, payload) {
|
|
268
|
+
if (typeof payload === 'string') return postJson(url, { content: payload });
|
|
269
|
+
return postJson(url, payload);
|
|
179
270
|
}
|
|
180
271
|
|
|
181
272
|
function postTeamsWebhook(url, text) {
|
|
182
273
|
return postJson(url, { text });
|
|
183
274
|
}
|
|
184
275
|
|
|
276
|
+
function postTeamsCard(url, card) {
|
|
277
|
+
return postJson(url, card);
|
|
278
|
+
}
|
|
279
|
+
|
|
185
280
|
function escapeJsonControlChars(jsonText) {
|
|
186
281
|
// Replace unescaped control chars inside JSON string literals with safe escapes.
|
|
187
282
|
// Handles Copilot outputs where newlines/tabs leak into string values.
|
|
@@ -329,6 +424,42 @@ async function publishRobust(webhookUrl, text, opts = {}) {
|
|
|
329
424
|
const u = new URL(webhookUrl);
|
|
330
425
|
const isDiscord = u.hostname.includes('discord.com') || u.hostname.includes('discordapp.com');
|
|
331
426
|
|
|
427
|
+
const mode = String(opts.mode || 'chunks');
|
|
428
|
+
|
|
429
|
+
if (mode === 'pretty') {
|
|
430
|
+
const title = extractTitleFromMarkdown(text);
|
|
431
|
+
const body = stripFirstH1(text);
|
|
432
|
+
|
|
433
|
+
if (isDiscord) {
|
|
434
|
+
const parts = splitForEmbed(body, 3900);
|
|
435
|
+
for (let i = 0; i < parts.length; i++) {
|
|
436
|
+
const payload = {
|
|
437
|
+
embeds: [
|
|
438
|
+
{
|
|
439
|
+
title: i === 0 ? title : undefined,
|
|
440
|
+
description: parts[i],
|
|
441
|
+
color: 0x5865F2
|
|
442
|
+
}
|
|
443
|
+
]
|
|
444
|
+
};
|
|
445
|
+
await postDiscordWebhook(webhookUrl, payload);
|
|
446
|
+
}
|
|
447
|
+
return { ok: true, chunks: parts.length, mode: 'pretty' };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Teams (MessageCard)
|
|
451
|
+
const card = {
|
|
452
|
+
'@type': 'MessageCard',
|
|
453
|
+
'@context': 'http://schema.org/extensions',
|
|
454
|
+
summary: title,
|
|
455
|
+
themeColor: '0078D7',
|
|
456
|
+
title,
|
|
457
|
+
text: body
|
|
458
|
+
};
|
|
459
|
+
await postTeamsCard(webhookUrl, card);
|
|
460
|
+
return { ok: true, chunks: 1, mode: 'pretty' };
|
|
461
|
+
}
|
|
462
|
+
|
|
332
463
|
const chunks = isDiscord ? splitForDiscord(text, 1900) : splitForDiscord(text, 1800);
|
|
333
464
|
|
|
334
465
|
for (const chunk of chunks) {
|
|
@@ -622,6 +753,11 @@ function buildHtml(safeDefault) {
|
|
|
622
753
|
<div class="help">Os webhooks ficam salvos na workspace em <code>data/settings/settings.json</code>.</div>
|
|
623
754
|
|
|
624
755
|
<div style="height:10px"></div>
|
|
756
|
+
<label style="display:flex; align-items:center; gap:10px; user-select:none; margin: 6px 0 12px 0">
|
|
757
|
+
<input id="prettyPublish" type="checkbox" checked style="width:auto" onchange="togglePrettyPublish()" />
|
|
758
|
+
Pretty publish (cards/embeds)
|
|
759
|
+
</label>
|
|
760
|
+
|
|
625
761
|
<div class="stack">
|
|
626
762
|
<button class="btn" onclick="saveSettings()">Save settings</button>
|
|
627
763
|
<button class="btn" onclick="publish('discord')">Publish selected → Discord</button>
|
|
@@ -631,6 +767,15 @@ function buildHtml(safeDefault) {
|
|
|
631
767
|
<div style="height:14px"></div>
|
|
632
768
|
|
|
633
769
|
<div class="help"><b>Dica:</b> clique em um relatório em <i>Reports</i> para ver o preview e habilitar publish/copy.</div>
|
|
770
|
+
|
|
771
|
+
<div style="height:14px"></div>
|
|
772
|
+
<label>Project slug rules</label>
|
|
773
|
+
<textarea id="slugRules" rows="8" placeholder="{ \"rules\": [ { \"contains\": \"fideliza\", \"slug\": \"vivo/fidelizacao\" } ] }" style="width:100%; padding:10px 12px; border-radius:12px; border:1px solid var(--line); background: rgba(255,255,255,.72); color: var(--text); outline:none; resize: vertical; font-family: var(--mono);"></textarea>
|
|
774
|
+
<div class="help">Regras usadas pra inferir <code>projectSlug</code>. Formato JSON (objeto com <code>rules</code>). Editável no estilo Obsidian-friendly.</div>
|
|
775
|
+
<div class="stack" style="margin-top:10px">
|
|
776
|
+
<button class="btn" onclick="reloadSlugRules()">Reload rules</button>
|
|
777
|
+
<button class="btn" onclick="saveSlugRules()">Save rules</button>
|
|
778
|
+
</div>
|
|
634
779
|
</div>
|
|
635
780
|
</div>
|
|
636
781
|
|
|
@@ -934,6 +1079,29 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
934
1079
|
return safeJson(res, 200, { ok: true, settings: { discordWebhookUrl: saved.discordWebhookUrl, teamsWebhookUrl: saved.teamsWebhookUrl } });
|
|
935
1080
|
}
|
|
936
1081
|
|
|
1082
|
+
|
|
1083
|
+
if (req.url === '/api/project-slug-map/get') {
|
|
1084
|
+
const map = readProjectSlugMap(workspaceDir);
|
|
1085
|
+
return safeJson(res, 200, { ok: true, map });
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
if (req.url === '/api/project-slug-map/save') {
|
|
1089
|
+
const map = payload.map;
|
|
1090
|
+
if (!map || typeof map !== 'object') return safeJson(res, 400, { error: 'Missing map' });
|
|
1091
|
+
if (!Array.isArray(map.rules)) return safeJson(res, 400, { error: 'map.rules must be an array' });
|
|
1092
|
+
|
|
1093
|
+
// normalize + basic validation
|
|
1094
|
+
const rules = map.rules
|
|
1095
|
+
.map((r) => ({ contains: String(r.contains || '').trim(), slug: String(r.slug || '').trim() }))
|
|
1096
|
+
.filter((r) => r.contains && r.slug);
|
|
1097
|
+
|
|
1098
|
+
const out = { schemaVersion: 1, updatedAt: new Date().toISOString(), rules };
|
|
1099
|
+
const p = projectSlugMapPath(workspaceDir);
|
|
1100
|
+
ensureDir(require('path').dirname(p));
|
|
1101
|
+
fs.writeFileSync(p, JSON.stringify(out, null, 2) + '\n', 'utf8');
|
|
1102
|
+
return safeJson(res, 200, { ok: true, map: out });
|
|
1103
|
+
}
|
|
1104
|
+
|
|
937
1105
|
if (req.url === '/api/reports/list') {
|
|
938
1106
|
const reports = listReports(workspaceDir);
|
|
939
1107
|
return safeJson(res, 200, { reports });
|
|
@@ -1222,6 +1390,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1222
1390
|
const now = new Date().toISOString();
|
|
1223
1391
|
const applyMode = String(payload.mode || 'all').trim();
|
|
1224
1392
|
const applied = { tasks: 0, blockers: 0, tasksSkipped: 0, blockersSkipped: 0, reportsSuggested: [], oracleQueries: [], mode: applyMode };
|
|
1393
|
+
const slugMap = readProjectSlugMap(workspaceDir);
|
|
1225
1394
|
|
|
1226
1395
|
function makeId(prefix) {
|
|
1227
1396
|
const rand = Math.random().toString(16).slice(2, 8);
|
|
@@ -1256,7 +1425,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1256
1425
|
if (applyMode !== 'all' && applyMode !== 'tasks') continue;
|
|
1257
1426
|
const description = normalizeWhitespace(a.description);
|
|
1258
1427
|
if (!description) continue;
|
|
1259
|
-
const projectSlug = String(a.projectSlug || '').trim();
|
|
1428
|
+
const projectSlug = String(a.projectSlug || '').trim() || inferProjectSlug(description, slugMap);
|
|
1260
1429
|
const key = sha1(normalizeTextForKey((projectSlug ? projectSlug + ' ' : '') + description));
|
|
1261
1430
|
if (existingTaskKeys24h.has(key)) { applied.tasksSkipped++; continue; }
|
|
1262
1431
|
const category = validTaskCats.has(String(a.category || '').trim()) ? String(a.category).trim() : 'DO_NOW';
|
|
@@ -1278,7 +1447,7 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1278
1447
|
if (type === 'create_blocker') {
|
|
1279
1448
|
if (applyMode !== 'all' && applyMode !== 'blockers') continue;
|
|
1280
1449
|
const title = normalizeWhitespace(a.title);
|
|
1281
|
-
const projectSlug = String(a.projectSlug || '').trim();
|
|
1450
|
+
const projectSlug = String(a.projectSlug || '').trim() || inferProjectSlug(title + ' ' + normalizeWhitespace(a.notes), slugMap);
|
|
1282
1451
|
const key = sha1(normalizeTextForKey((projectSlug ? projectSlug + ' ' : '') + title));
|
|
1283
1452
|
if (existingBlockerKeys24h.has(key)) { applied.blockersSkipped++; continue; }
|
|
1284
1453
|
const notes = normalizeWhitespace(a.notes);
|