wayfind 2.0.28 → 2.0.30
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/bin/connectors/github.js +37 -1
- package/bin/content-store.js +127 -38
- package/bin/digest.js +118 -37
- package/bin/distill.js +355 -0
- package/bin/memory-compare.js +171 -0
- package/bin/memory-report.sh +41 -0
- package/bin/slack-bot.js +2 -1
- package/bin/storage/sqlite-backend.js +44 -2
- package/bin/team-context.js +106 -39
- package/package.json +1 -1
- package/plugin/scripts/session-end.sh +1 -1
- package/plugin/skills/init-folder/SKILL.md +93 -0
- package/plugin/skills/init-team/SKILL.md +8 -0
- package/plugin/skills/session-protocol/SKILL.md +2 -0
- package/specializations/claude-code/CLAUDE.md-global-fragment.md +6 -2
- package/specializations/claude-code/CLAUDE.md-repo-fragment.md +2 -0
- package/specializations/claude-code/commands/init-folder.md +91 -0
- package/specializations/claude-code/commands/init-team.md +8 -0
- package/specializations/claude-code/hooks/session-end.sh +1 -1
package/bin/connectors/github.js
CHANGED
|
@@ -303,7 +303,28 @@ async function pull(config, since) {
|
|
|
303
303
|
highlights.push(`${failedCount} CI failure(s)`);
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
repoHighlights.push({
|
|
306
|
+
repoHighlights.push({
|
|
307
|
+
repo: repoStr,
|
|
308
|
+
openPRs,
|
|
309
|
+
mergedPRs,
|
|
310
|
+
highlights,
|
|
311
|
+
topPRs: data.prs.slice(0, 5).map((pr) => ({
|
|
312
|
+
number: pr.number,
|
|
313
|
+
title: pr.title,
|
|
314
|
+
author: pr.user?.login || pr.user?.name || 'unknown',
|
|
315
|
+
state: pr.merged_at ? 'merged' : pr.state,
|
|
316
|
+
})),
|
|
317
|
+
topIssues: data.issues.slice(0, 5).map((iss) => ({
|
|
318
|
+
number: iss.number,
|
|
319
|
+
title: iss.title,
|
|
320
|
+
labels: (iss.labels || []).map((l) => (typeof l === 'string' ? l : l.name)).filter(Boolean),
|
|
321
|
+
state: iss.state,
|
|
322
|
+
})),
|
|
323
|
+
failedRuns: failed.map((r) => ({
|
|
324
|
+
name: r.name || r.workflow?.name || 'unknown',
|
|
325
|
+
branch: r.head_branch || '',
|
|
326
|
+
})),
|
|
327
|
+
});
|
|
307
328
|
}
|
|
308
329
|
|
|
309
330
|
// Generate rollup summary
|
|
@@ -489,6 +510,21 @@ function generateSummaryMarkdown(
|
|
|
489
510
|
for (const h of rh.highlights) {
|
|
490
511
|
lines.push(`- ${h}`);
|
|
491
512
|
}
|
|
513
|
+
if (rh.topPRs && rh.topPRs.length > 0) {
|
|
514
|
+
const prItems = rh.topPRs.map((pr) => `#${pr.number} "${pr.title}" (${pr.author}, ${pr.state})`);
|
|
515
|
+
lines.push(`**PRs:** ${prItems.join(' | ')}`);
|
|
516
|
+
}
|
|
517
|
+
if (rh.topIssues && rh.topIssues.length > 0) {
|
|
518
|
+
const issueItems = rh.topIssues.map((iss) => {
|
|
519
|
+
const labels = iss.labels && iss.labels.length > 0 ? ` [${iss.labels.join(', ')}]` : '';
|
|
520
|
+
return `#${iss.number} "${iss.title}"${labels} (${iss.state})`;
|
|
521
|
+
});
|
|
522
|
+
lines.push(`**Issues:** ${issueItems.join(' | ')}`);
|
|
523
|
+
}
|
|
524
|
+
if (rh.failedRuns && rh.failedRuns.length > 0) {
|
|
525
|
+
const runItems = rh.failedRuns.map((r) => `${r.name}${r.branch ? ' (' + r.branch + ')' : ''}`);
|
|
526
|
+
lines.push(`**Failed CI:** ${runItems.join(' | ')}`);
|
|
527
|
+
}
|
|
492
528
|
lines.push('');
|
|
493
529
|
}
|
|
494
530
|
|
package/bin/content-store.js
CHANGED
|
@@ -212,6 +212,22 @@ function generateEntryId(date, repo, title) {
|
|
|
212
212
|
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 12);
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Compute a quality score for an entry (0-3).
|
|
217
|
+
* +1 if has reasoning (explains WHY)
|
|
218
|
+
* +1 if has alternatives (what was rejected)
|
|
219
|
+
* +1 if substantive content (>500 chars)
|
|
220
|
+
* @param {Object} entry - Entry metadata
|
|
221
|
+
* @returns {number} 0-3
|
|
222
|
+
*/
|
|
223
|
+
function computeQualityScore(entry) {
|
|
224
|
+
let score = 0;
|
|
225
|
+
if (entry.hasReasoning) score++;
|
|
226
|
+
if (entry.hasAlternatives) score++;
|
|
227
|
+
if ((entry.contentLength || 0) > 500) score++;
|
|
228
|
+
return score;
|
|
229
|
+
}
|
|
230
|
+
|
|
215
231
|
/**
|
|
216
232
|
* Build the text content for embedding from an entry's fields.
|
|
217
233
|
* @param {Object} entry - Entry with date, repo, title, fields
|
|
@@ -329,7 +345,7 @@ async function indexJournals(options = {}) {
|
|
|
329
345
|
const content = buildContent({ ...entry, date, author });
|
|
330
346
|
const hash = contentHash(content);
|
|
331
347
|
|
|
332
|
-
|
|
348
|
+
const entryMeta = {
|
|
333
349
|
date,
|
|
334
350
|
repo: entry.repo,
|
|
335
351
|
title: entry.title,
|
|
@@ -339,8 +355,12 @@ async function indexJournals(options = {}) {
|
|
|
339
355
|
contentLength: content.length,
|
|
340
356
|
tags: extractTags(entry),
|
|
341
357
|
hasEmbedding: false,
|
|
358
|
+
hasReasoning: false,
|
|
359
|
+
hasAlternatives: false,
|
|
342
360
|
_content: content, // temporary, not saved to index
|
|
343
361
|
};
|
|
362
|
+
entryMeta.qualityScore = computeQualityScore(entryMeta);
|
|
363
|
+
newEntries[id] = entryMeta;
|
|
344
364
|
}
|
|
345
365
|
}
|
|
346
366
|
|
|
@@ -744,39 +764,50 @@ function getEntryContent(entryId, options = {}) {
|
|
|
744
764
|
// ── Signal entries ──────────────────────────────────────────────────────
|
|
745
765
|
if (entry.source === 'signal') {
|
|
746
766
|
if (!signalsDir) return null;
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
path.join(channelDir, `${entry.date}-summary.md`),
|
|
759
|
-
];
|
|
760
|
-
for (const filePath of dateCandidates) {
|
|
767
|
+
const repo = entry.repo || '';
|
|
768
|
+
|
|
769
|
+
// Determine file location based on repo format:
|
|
770
|
+
// - 'signals/channel' (summary files) → signalsDir/channel/
|
|
771
|
+
// - 'owner/repo' (per-repo files) → find the channel dir containing owner/repo/
|
|
772
|
+
let searchDirs = [];
|
|
773
|
+
if (repo.startsWith('signals/')) {
|
|
774
|
+
const channel = repo.replace(/^signals\//, '');
|
|
775
|
+
searchDirs = [path.join(signalsDir, channel)];
|
|
776
|
+
} else {
|
|
777
|
+
// Per-repo entry: search all channel dirs for owner/repo subdirectory
|
|
761
778
|
try {
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
779
|
+
const channels = fs.readdirSync(signalsDir, { withFileTypes: true })
|
|
780
|
+
.filter(d => d.isDirectory()).map(d => d.name);
|
|
781
|
+
for (const ch of channels) {
|
|
782
|
+
const repoDir = path.join(signalsDir, ch, repo);
|
|
783
|
+
if (fs.existsSync(repoDir)) {
|
|
784
|
+
searchDirs.push(repoDir);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
} catch { /* skip */ }
|
|
766
788
|
}
|
|
767
789
|
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
790
|
+
for (const dir of searchDirs) {
|
|
791
|
+
if (!fs.existsSync(dir)) continue;
|
|
792
|
+
// Try date-based filename first, then summary, then scan
|
|
793
|
+
const dateCandidates = [
|
|
794
|
+
path.join(dir, `${entry.date}.md`),
|
|
795
|
+
path.join(dir, `${entry.date}-summary.md`),
|
|
796
|
+
];
|
|
797
|
+
for (const filePath of dateCandidates) {
|
|
772
798
|
try {
|
|
773
|
-
return fs.readFileSync(
|
|
774
|
-
} catch {
|
|
775
|
-
continue;
|
|
776
|
-
}
|
|
799
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
800
|
+
} catch { /* try next */ }
|
|
777
801
|
}
|
|
778
|
-
|
|
779
|
-
|
|
802
|
+
// Scan for files matching the date
|
|
803
|
+
try {
|
|
804
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md') && f.includes(entry.date));
|
|
805
|
+
for (const file of files) {
|
|
806
|
+
try {
|
|
807
|
+
return fs.readFileSync(path.join(dir, file), 'utf8');
|
|
808
|
+
} catch { continue; }
|
|
809
|
+
}
|
|
810
|
+
} catch { /* dir not readable */ }
|
|
780
811
|
}
|
|
781
812
|
|
|
782
813
|
return null;
|
|
@@ -1346,7 +1377,7 @@ async function indexConversations(options = {}) {
|
|
|
1346
1377
|
|
|
1347
1378
|
const hash = contentHash(content);
|
|
1348
1379
|
|
|
1349
|
-
|
|
1380
|
+
const convEntry = {
|
|
1350
1381
|
date,
|
|
1351
1382
|
repo: transcript.repo,
|
|
1352
1383
|
title: decision.title,
|
|
@@ -1361,6 +1392,8 @@ async function indexConversations(options = {}) {
|
|
|
1361
1392
|
hasAlternatives: !!decision.has_alternatives,
|
|
1362
1393
|
_content: content,
|
|
1363
1394
|
};
|
|
1395
|
+
convEntry.qualityScore = computeQualityScore(convEntry);
|
|
1396
|
+
existingIndex.entries[id] = convEntry;
|
|
1364
1397
|
|
|
1365
1398
|
if (doEmbeddings) {
|
|
1366
1399
|
try {
|
|
@@ -1653,16 +1686,42 @@ async function indexSignals(options = {}) {
|
|
|
1653
1686
|
|
|
1654
1687
|
for (const channel of channels) {
|
|
1655
1688
|
const channelDir = path.join(signalsDir, channel);
|
|
1656
|
-
|
|
1689
|
+
|
|
1690
|
+
// Collect all .md files: channel root + recursive owner/repo subdirectories
|
|
1691
|
+
const signalFiles = [];
|
|
1657
1692
|
try {
|
|
1658
|
-
|
|
1693
|
+
const entries = fs.readdirSync(channelDir, { withFileTypes: true });
|
|
1694
|
+
// Channel-root .md files (summaries like YYYY-MM-DD-summary.md)
|
|
1695
|
+
for (const e of entries) {
|
|
1696
|
+
if (e.isFile() && e.name.endsWith('.md')) {
|
|
1697
|
+
signalFiles.push({ filePath: path.join(channelDir, e.name), file: e.name, repo: 'signals/' + channel });
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
// Walk owner/repo subdirectories (e.g., github/acme-corp/web-api/)
|
|
1701
|
+
for (const ownerEntry of entries) {
|
|
1702
|
+
if (!ownerEntry.isDirectory()) continue;
|
|
1703
|
+
const ownerDir = path.join(channelDir, ownerEntry.name);
|
|
1704
|
+
let repoEntries;
|
|
1705
|
+
try { repoEntries = fs.readdirSync(ownerDir, { withFileTypes: true }); } catch { continue; }
|
|
1706
|
+
for (const repoEntry of repoEntries) {
|
|
1707
|
+
if (!repoEntry.isDirectory()) continue;
|
|
1708
|
+
const repoDir = path.join(ownerDir, repoEntry.name);
|
|
1709
|
+
const repoStr = `${ownerEntry.name}/${repoEntry.name}`;
|
|
1710
|
+
let repoFiles;
|
|
1711
|
+
try { repoFiles = fs.readdirSync(repoDir).filter(f => f.endsWith('.md')); } catch { continue; }
|
|
1712
|
+
for (const f of repoFiles) {
|
|
1713
|
+
signalFiles.push({ filePath: path.join(repoDir, f), file: f, repo: repoStr });
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1659
1717
|
} catch {
|
|
1660
1718
|
continue;
|
|
1661
1719
|
}
|
|
1662
1720
|
|
|
1663
|
-
|
|
1721
|
+
signalFiles.sort((a, b) => a.file.localeCompare(b.file));
|
|
1722
|
+
|
|
1723
|
+
for (const { filePath, file, repo } of signalFiles) {
|
|
1664
1724
|
stats.fileCount++;
|
|
1665
|
-
const filePath = path.join(channelDir, file);
|
|
1666
1725
|
let content;
|
|
1667
1726
|
try {
|
|
1668
1727
|
content = fs.readFileSync(filePath, 'utf8');
|
|
@@ -1670,16 +1729,17 @@ async function indexSignals(options = {}) {
|
|
|
1670
1729
|
continue;
|
|
1671
1730
|
}
|
|
1672
1731
|
|
|
1673
|
-
// Extract date from filename (YYYY-MM-DD.md) or fall back to filename
|
|
1674
|
-
const dateMatch = file.match(/^(\d{4}-\d{2}-\d{2})
|
|
1732
|
+
// Extract date from filename (YYYY-MM-DD.md or YYYY-MM-DD-summary.md) or fall back to filename
|
|
1733
|
+
const dateMatch = file.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
1675
1734
|
const date = dateMatch ? dateMatch[1] : file.replace(/\.md$/, '');
|
|
1676
1735
|
|
|
1677
1736
|
// Extract title from first # heading, or fall back to filename
|
|
1678
1737
|
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
1679
1738
|
const title = titleMatch ? titleMatch[1].trim() : file.replace(/\.md$/, '');
|
|
1680
1739
|
|
|
1681
|
-
// Extract tags: channel name + any ## section headings
|
|
1740
|
+
// Extract tags: channel name + repo + any ## section headings
|
|
1682
1741
|
const tags = [channel];
|
|
1742
|
+
if (repo !== 'signals/' + channel) tags.push(repo);
|
|
1683
1743
|
const sectionRe = /^##\s+(.+)$/gm;
|
|
1684
1744
|
let sectionMatch;
|
|
1685
1745
|
while ((sectionMatch = sectionRe.exec(content)) !== null) {
|
|
@@ -1689,7 +1749,6 @@ async function indexSignals(options = {}) {
|
|
|
1689
1749
|
}
|
|
1690
1750
|
}
|
|
1691
1751
|
|
|
1692
|
-
const repo = 'signals/' + channel;
|
|
1693
1752
|
const id = generateEntryId(date, repo, file.replace(/\.md$/, ''));
|
|
1694
1753
|
const hash = contentHash(content);
|
|
1695
1754
|
|
|
@@ -1975,6 +2034,32 @@ function computeQualityProfile(options = {}) {
|
|
|
1975
2034
|
|
|
1976
2035
|
// ── Exports ─────────────────────────────────────────────────────────────────
|
|
1977
2036
|
|
|
2037
|
+
/**
|
|
2038
|
+
* Deduplicate search results by removing raw entries that have been absorbed
|
|
2039
|
+
* into distilled entries. If a distilled entry exists in the results, its
|
|
2040
|
+
* source entries (listed in distilledFrom) are removed.
|
|
2041
|
+
* @param {Array<{id: string, entry: Object, score?: number}>} results
|
|
2042
|
+
* @returns {Array} Deduplicated results
|
|
2043
|
+
*/
|
|
2044
|
+
function deduplicateResults(results) {
|
|
2045
|
+
if (!results || results.length === 0) return results;
|
|
2046
|
+
|
|
2047
|
+
// Collect all IDs that have been absorbed into distilled entries
|
|
2048
|
+
const absorbedIds = new Set();
|
|
2049
|
+
for (const r of results) {
|
|
2050
|
+
if (r.entry && r.entry.distilledFrom && Array.isArray(r.entry.distilledFrom)) {
|
|
2051
|
+
for (const id of r.entry.distilledFrom) {
|
|
2052
|
+
absorbedIds.add(id);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
if (absorbedIds.size === 0) return results;
|
|
2058
|
+
|
|
2059
|
+
// Filter out absorbed entries
|
|
2060
|
+
return results.filter(r => !absorbedIds.has(r.id));
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1978
2063
|
module.exports = {
|
|
1979
2064
|
// Parsing
|
|
1980
2065
|
parseJournalFile,
|
|
@@ -2005,6 +2090,10 @@ module.exports = {
|
|
|
2005
2090
|
isRepoExcluded,
|
|
2006
2091
|
applyFilters,
|
|
2007
2092
|
|
|
2093
|
+
// Quality & dedup
|
|
2094
|
+
computeQualityScore,
|
|
2095
|
+
deduplicateResults,
|
|
2096
|
+
|
|
2008
2097
|
// Core operations
|
|
2009
2098
|
indexJournals,
|
|
2010
2099
|
indexSignals,
|
package/bin/digest.js
CHANGED
|
@@ -269,13 +269,18 @@ function collectFromStore(sinceDate, options = {}) {
|
|
|
269
269
|
});
|
|
270
270
|
|
|
271
271
|
if (entries.length === 0) {
|
|
272
|
-
return { journals: '', signals: '', entryCount: 0 };
|
|
272
|
+
return { journals: '', signals: '', entryCount: 0, entryMeta: [] };
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
const journalParts = [];
|
|
276
276
|
const signalParts = [];
|
|
277
|
+
const journalMeta = [];
|
|
278
|
+
const signalMeta = [];
|
|
277
279
|
|
|
278
280
|
for (const { id, entry } of entries) {
|
|
281
|
+
// Skip raw entries that have been absorbed into a distilled entry
|
|
282
|
+
if (entry.distilledFrom) continue;
|
|
283
|
+
|
|
279
284
|
const content = contentStore.getEntryContent(id, { storePath, journalDir, signalsDir });
|
|
280
285
|
if (!content) continue;
|
|
281
286
|
|
|
@@ -287,10 +292,21 @@ function collectFromStore(sinceDate, options = {}) {
|
|
|
287
292
|
const meta = author ? `${header}\n${author}\n` : `${header}\n`;
|
|
288
293
|
const formatted = `${meta}\n${content}`;
|
|
289
294
|
|
|
295
|
+
const itemMeta = {
|
|
296
|
+
date: entry.date,
|
|
297
|
+
source: entry.source,
|
|
298
|
+
qualityScore: entry.qualityScore || 0,
|
|
299
|
+
hasReasoning: entry.hasReasoning,
|
|
300
|
+
hasAlternatives: entry.hasAlternatives,
|
|
301
|
+
distillTier: entry.distillTier || 'raw',
|
|
302
|
+
};
|
|
303
|
+
|
|
290
304
|
if (source === 'signal') {
|
|
291
305
|
signalParts.push(formatted);
|
|
306
|
+
signalMeta.push(itemMeta);
|
|
292
307
|
} else {
|
|
293
308
|
journalParts.push(formatted);
|
|
309
|
+
journalMeta.push(itemMeta);
|
|
294
310
|
}
|
|
295
311
|
}
|
|
296
312
|
|
|
@@ -298,6 +314,7 @@ function collectFromStore(sinceDate, options = {}) {
|
|
|
298
314
|
journals: journalParts.join('\n\n---\n\n'),
|
|
299
315
|
signals: signalParts.join('\n\n---\n\n'),
|
|
300
316
|
entryCount: entries.length,
|
|
317
|
+
entryMeta: { journal: journalMeta, signal: signalMeta },
|
|
301
318
|
};
|
|
302
319
|
}
|
|
303
320
|
|
|
@@ -618,56 +635,102 @@ function buildPrompt(personaId, signalContent, journalContent, dateRange, contex
|
|
|
618
635
|
}
|
|
619
636
|
|
|
620
637
|
/**
|
|
621
|
-
* Apply token budget constraints
|
|
622
|
-
*
|
|
638
|
+
* Apply token budget constraints with quality-weighted packing.
|
|
639
|
+
* Higher quality entries are kept preferentially over low-quality ones.
|
|
623
640
|
* @param {string} signalContent
|
|
624
641
|
* @param {string} journalContent
|
|
625
642
|
* @param {number} maxChars
|
|
626
|
-
* @
|
|
643
|
+
* @param {Object} [options] - Optional metadata for quality-weighted packing
|
|
644
|
+
* @param {Object} [options.entryMeta] - { journal: [{qualityScore, date, ...}], signal: [...] }
|
|
645
|
+
* @param {Array} [options.scores] - Intelligence scores from Haiku scoring
|
|
646
|
+
* @param {string} [options.personaId] - Current persona for score lookup
|
|
647
|
+
* @returns {{ signals: string, journals: string, truncated: boolean, stats: Object }}
|
|
627
648
|
*/
|
|
628
|
-
function applyTokenBudget(signalContent, journalContent, maxChars) {
|
|
649
|
+
function applyTokenBudget(signalContent, journalContent, maxChars, options = {}) {
|
|
629
650
|
const total = signalContent.length + journalContent.length;
|
|
630
651
|
if (total <= maxChars) {
|
|
631
|
-
return { signals: signalContent, journals: journalContent, truncated: false };
|
|
652
|
+
return { signals: signalContent, journals: journalContent, truncated: false, stats: { dropped: 0 } };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const { entryMeta, scores, personaId } = options;
|
|
656
|
+
|
|
657
|
+
// Split into sections
|
|
658
|
+
const signalSections = signalContent ? signalContent.split('\n\n---\n\n') : [];
|
|
659
|
+
const journalSections = journalContent ? journalContent.split('\n\n---\n\n') : [];
|
|
660
|
+
const signalMetaArr = (entryMeta && entryMeta.signal) || [];
|
|
661
|
+
const journalMetaArr = (entryMeta && entryMeta.journal) || [];
|
|
662
|
+
|
|
663
|
+
// Score each section with composite priority
|
|
664
|
+
const todayStr = today();
|
|
665
|
+
const yesterdayStr = (() => { const d = new Date(); d.setDate(d.getDate() - 1); return d.toISOString().split('T')[0]; })();
|
|
666
|
+
|
|
667
|
+
const allSections = [];
|
|
668
|
+
for (let i = 0; i < signalSections.length; i++) {
|
|
669
|
+
const meta = signalMetaArr[i] || {};
|
|
670
|
+
const quality = meta.qualityScore || 0;
|
|
671
|
+
const recency = (meta.date === todayStr || meta.date === yesterdayStr) ? 1 : 0;
|
|
672
|
+
const intel = (scores && scores[i] && personaId) ? (scores[i][personaId] || 0) : 0;
|
|
673
|
+
const distillBonus = (meta.distillTier && meta.distillTier !== 'raw') ? 1 : 0;
|
|
674
|
+
allSections.push({
|
|
675
|
+
text: signalSections[i],
|
|
676
|
+
type: 'signal',
|
|
677
|
+
priority: quality + recency + intel + distillBonus,
|
|
678
|
+
len: signalSections[i].length,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
for (let i = 0; i < journalSections.length; i++) {
|
|
682
|
+
const meta = journalMetaArr[i] || {};
|
|
683
|
+
const quality = meta.qualityScore || 0;
|
|
684
|
+
const recency = (meta.date === todayStr || meta.date === yesterdayStr) ? 1 : 0;
|
|
685
|
+
// Journal score indices start after signal count
|
|
686
|
+
const scoreIdx = signalSections.length + i;
|
|
687
|
+
const intel = (scores && scores[scoreIdx] && personaId) ? (scores[scoreIdx][personaId] || 0) : 0;
|
|
688
|
+
const distillBonus = (meta.distillTier && meta.distillTier !== 'raw') ? 1 : 0;
|
|
689
|
+
allSections.push({
|
|
690
|
+
text: journalSections[i],
|
|
691
|
+
type: 'journal',
|
|
692
|
+
priority: quality + recency + intel + distillBonus,
|
|
693
|
+
len: journalSections[i].length,
|
|
694
|
+
});
|
|
632
695
|
}
|
|
633
696
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const available = maxChars - noteLen;
|
|
697
|
+
// Sort by priority descending (highest quality first)
|
|
698
|
+
allSections.sort((a, b) => b.priority - a.priority);
|
|
637
699
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
700
|
+
// Greedily pack into budget
|
|
701
|
+
const truncationNote = '\n\n> Note: Input was truncated to fit within token budget. Lower-quality entries were dropped.\n';
|
|
702
|
+
const available = maxChars - truncationNote.length;
|
|
703
|
+
const keptSignals = [];
|
|
704
|
+
const keptJournals = [];
|
|
705
|
+
let used = 0;
|
|
706
|
+
let dropped = 0;
|
|
642
707
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
708
|
+
for (const section of allSections) {
|
|
709
|
+
const sectionCost = section.len + 7; // account for '\n\n---\n\n' separator
|
|
710
|
+
if (used + sectionCost <= available) {
|
|
711
|
+
if (section.type === 'signal') {
|
|
712
|
+
keptSignals.push(section.text);
|
|
713
|
+
} else {
|
|
714
|
+
keptJournals.push(section.text);
|
|
715
|
+
}
|
|
716
|
+
used += sectionCost;
|
|
717
|
+
} else {
|
|
718
|
+
dropped++;
|
|
650
719
|
}
|
|
651
720
|
}
|
|
652
721
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// Append truncation note to whichever content was actually trimmed
|
|
661
|
-
if (signalsTrimmed) {
|
|
662
|
-
trimmedSignals += truncationNote;
|
|
663
|
-
} else if (journalsTrimmed) {
|
|
664
|
-
trimmedJournals += truncationNote;
|
|
722
|
+
const truncated = dropped > 0;
|
|
723
|
+
let finalSignals = keptSignals.join('\n\n---\n\n');
|
|
724
|
+
let finalJournals = keptJournals.join('\n\n---\n\n');
|
|
725
|
+
if (truncated) {
|
|
726
|
+
finalJournals += truncationNote;
|
|
665
727
|
}
|
|
666
728
|
|
|
667
729
|
return {
|
|
668
|
-
signals:
|
|
669
|
-
journals:
|
|
670
|
-
truncated
|
|
730
|
+
signals: finalSignals,
|
|
731
|
+
journals: finalJournals,
|
|
732
|
+
truncated,
|
|
733
|
+
stats: { dropped, total: allSections.length, kept: allSections.length - dropped },
|
|
671
734
|
};
|
|
672
735
|
}
|
|
673
736
|
|
|
@@ -748,7 +811,11 @@ async function generateDigest(config, personaIds, sinceDate, onProgress) {
|
|
|
748
811
|
({ signals: pSignals, journals: pJournals } =
|
|
749
812
|
intelligence.filterForPersona(signalContent, journalContent, scores, personaId, threshold, allPersonaIds));
|
|
750
813
|
}
|
|
751
|
-
const budget = applyTokenBudget(pSignals, pJournals, maxInputChars
|
|
814
|
+
const budget = applyTokenBudget(pSignals, pJournals, maxInputChars, {
|
|
815
|
+
entryMeta: storeResult.entryMeta,
|
|
816
|
+
scores,
|
|
817
|
+
personaId,
|
|
818
|
+
});
|
|
752
819
|
pSignals = budget.signals;
|
|
753
820
|
pJournals = budget.journals;
|
|
754
821
|
|
|
@@ -805,7 +872,21 @@ async function generateDigest(config, personaIds, sinceDate, onProgress) {
|
|
|
805
872
|
fs.writeFileSync(combinedFile, combinedContent, 'utf8');
|
|
806
873
|
files.push(combinedFile);
|
|
807
874
|
|
|
808
|
-
|
|
875
|
+
// Compute input stats for preview mode
|
|
876
|
+
const entryMeta = storeResult.entryMeta || {};
|
|
877
|
+
const journalMeta = entryMeta.journal || [];
|
|
878
|
+
const signalMeta = entryMeta.signal || [];
|
|
879
|
+
const inputStats = {
|
|
880
|
+
journalEntries: journalMeta.length,
|
|
881
|
+
signalEntries: signalMeta.length,
|
|
882
|
+
qualityDistribution: {
|
|
883
|
+
rich: journalMeta.filter(m => m.qualityScore >= 2).length,
|
|
884
|
+
medium: journalMeta.filter(m => m.qualityScore === 1).length,
|
|
885
|
+
thin: journalMeta.filter(m => m.qualityScore === 0).length,
|
|
886
|
+
},
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
return { files, personas: personaIds, dateRange, scores, inputStats };
|
|
809
890
|
}
|
|
810
891
|
|
|
811
892
|
/**
|