osborn 0.9.249 → 0.9.251
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/dist/claude-llm.js +13 -0
- package/dist/content/compaction-lens-worker.d.ts +23 -0
- package/dist/content/compaction-lens-worker.js +188 -0
- package/dist/content/compaction-lens.d.ts +106 -0
- package/dist/content/compaction-lens.js +222 -0
- package/dist/content/lens-audio.d.ts +38 -0
- package/dist/content/lens-audio.js +105 -0
- package/dist/content/lens-clip-arc.d.ts +80 -0
- package/dist/content/lens-clip-arc.js +148 -0
- package/dist/content/lens-clip-formats.d.ts +58 -0
- package/dist/content/lens-clip-formats.js +175 -0
- package/dist/content/lens-clip-search.d.ts +51 -0
- package/dist/content/lens-clip-search.js +138 -0
- package/dist/content/lens-clip-window.d.ts +72 -0
- package/dist/content/lens-clip-window.js +257 -0
- package/dist/content/lens-db.d.ts +70 -0
- package/dist/content/lens-db.js +170 -0
- package/dist/content/lens-hn.d.ts +19 -0
- package/dist/content/lens-hn.js +69 -0
- package/dist/content/lens-ingest.d.ts +67 -0
- package/dist/content/lens-ingest.js +155 -0
- package/dist/content/lens-launch.d.ts +46 -0
- package/dist/content/lens-launch.js +111 -0
- package/dist/content/lens-library-index.d.ts +30 -0
- package/dist/content/lens-library-index.js +79 -0
- package/dist/content/lens-library-select.d.ts +59 -0
- package/dist/content/lens-library-select.js +77 -0
- package/dist/content/lens-library.d.ts +92 -0
- package/dist/content/lens-library.js +179 -0
- package/dist/content/lens-model.d.ts +64 -0
- package/dist/content/lens-model.js +193 -0
- package/dist/content/lens-paths.d.ts +43 -0
- package/dist/content/lens-paths.js +166 -0
- package/dist/content/lens-period-block.d.ts +63 -0
- package/dist/content/lens-period-block.js +99 -0
- package/dist/content/lens-period-boundaries.d.ts +13 -0
- package/dist/content/lens-period-boundaries.js +40 -0
- package/dist/content/lens-period-render.d.ts +28 -0
- package/dist/content/lens-period-render.js +70 -0
- package/dist/content/lens-period.d.ts +106 -0
- package/dist/content/lens-period.js +205 -0
- package/dist/content/lens-profile.d.ts +47 -0
- package/dist/content/lens-profile.js +95 -0
- package/dist/content/lens-quotes.d.ts +103 -0
- package/dist/content/lens-quotes.js +186 -0
- package/dist/content/lens-redact.d.ts +61 -0
- package/dist/content/lens-redact.js +187 -0
- package/dist/content/lens-strip.d.ts +33 -0
- package/dist/content/lens-strip.js +132 -0
- package/dist/content/period-run.d.ts +30 -0
- package/dist/content/period-run.js +135 -0
- package/dist/content/render-ws-data.d.ts +8 -0
- package/dist/content/render-ws-data.js +108 -0
- package/dist/content/render-ws-screens.d.ts +25 -0
- package/dist/content/render-ws-screens.js +189 -0
- package/dist/content/render-ws-story.d.ts +1 -0
- package/dist/content/render-ws-story.js +233 -0
- package/dist/content/transcript-sanitizer.d.ts +195 -0
- package/dist/content/transcript-sanitizer.js +515 -0
- package/dist/index.js +34 -0
- package/package.json +1 -1
- package/test-results/.last-run.json +0 -4
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lens-period-render.ts — Markdown review output for a period-map run. Pure.
|
|
3
|
+
* high_leverage is rendered FIRST (strongest content candidates). The caller
|
|
4
|
+
* redacts the final string again (secrets + client) before writing it.
|
|
5
|
+
*/
|
|
6
|
+
const q = (x) => ` - > "${x.text.replace(/\n+/g, ' ')}" — ${x.speaker}, row #${x.row}, ${x.timestamp}${x.tsCorrected ? ' (ts corrected)' : ''}`;
|
|
7
|
+
const quotes = (xs) => xs.map(q);
|
|
8
|
+
export function renderPeriodMd(r) {
|
|
9
|
+
const L = [];
|
|
10
|
+
const b = r.block;
|
|
11
|
+
L.push(`# Period map — session ${r.sessionId}`, '');
|
|
12
|
+
L.push(`Model: ${r.model} (context ${r.contextLength.toLocaleString()} tok; block cap 25% = ${r.capTokens.toLocaleString()} tok). Review output only — profile and HWM untouched.`, '');
|
|
13
|
+
L.push('## Block');
|
|
14
|
+
if (b) {
|
|
15
|
+
L.push(`- Basis: ${b.basis}`, `- Rows in block: #${b.firstRowId}–#${b.lastRowId} (${b.periodRows - (b.leftOut?.rows ?? 0)} conversation rows of ${b.periodRows} in the period)`, `- Time: ${b.from} → ${b.to}`, `- Size: ${b.chars.toLocaleString()} chars ≈ ${b.estTokens.toLocaleString()} tokens (est. 4 chars/token)`, b.leftOut
|
|
16
|
+
? `- LEFT OUT (over the cap, oldest end): ${b.leftOut.rows} rows #${b.leftOut.firstRowId}–#${b.leftOut.lastRowId} (${b.leftOut.from} → ${b.leftOut.to}, ≈${b.leftOut.estTokens.toLocaleString()} tok)`
|
|
17
|
+
: '- Left out: nothing (whole period fits the cap)');
|
|
18
|
+
}
|
|
19
|
+
else
|
|
20
|
+
L.push('- (no block)');
|
|
21
|
+
L.push(`- Raw rows in the block's range (user+assistant, main thread): ${r.rowsRead}; kept after strip/sanitize: ${r.rowsKeptInSession}`);
|
|
22
|
+
L.push(`- Audio: ${r.audio}`, '');
|
|
23
|
+
L.push("### Stripped before the block (rows affected, within the block's row range)");
|
|
24
|
+
const kinds = Object.entries(r.stripped).sort((a, b2) => b2[1] - a[1]);
|
|
25
|
+
L.push(...(kinds.length ? kinds.map(([k, n]) => `- ${k}: ${n}`) : ['- none']), '');
|
|
26
|
+
L.push('## Cost', `- Calls: ${r.calls}; tokens ${r.promptTokens.toLocaleString()} in + ${r.completionTokens.toLocaleString()} out; $${r.costUsd.toFixed(4)} (cap $${r.capUsd.toFixed(2)}); finish=${r.finish || '-'}; provider=${r.provider || '-'}`, '');
|
|
27
|
+
if (r.errors.length)
|
|
28
|
+
L.push('## Errors', ...r.errors.map(e => `- ${e}`), '');
|
|
29
|
+
const m = r.map;
|
|
30
|
+
if (!m)
|
|
31
|
+
return L.join('\n') + '\n';
|
|
32
|
+
L.push('## High-leverage work (ranked)');
|
|
33
|
+
if (!m.highLeverage.length)
|
|
34
|
+
L.push('- (none verified)');
|
|
35
|
+
m.highLeverage.forEach((h, i) => L.push(`${i + 1}. **${h.title}** [${h.evidence}] — ${h.why}`, ...quotes(h.quotes)));
|
|
36
|
+
L.push('', '## Period goal');
|
|
37
|
+
if (m.periodGoal)
|
|
38
|
+
L.push(m.periodGoal.text, ...quotes(m.periodGoal.quotes));
|
|
39
|
+
else
|
|
40
|
+
L.push('- (none verified)');
|
|
41
|
+
L.push('', '## Arc');
|
|
42
|
+
m.arc.forEach((s, i) => L.push(`${i + 1}. [${s.step}] ${s.what}${s.why ? ` — ${s.why}` : ''}`, ...quotes(s.quotes)));
|
|
43
|
+
L.push('', '## What worked', '### Held up');
|
|
44
|
+
for (const w of m.whatWorked.heldUp)
|
|
45
|
+
L.push(`- **${w.name}** (${w.kind || '-'}) — ${w.why}`, ...quotes(w.quotes));
|
|
46
|
+
L.push('### Did not');
|
|
47
|
+
for (const w of m.whatWorked.didnt)
|
|
48
|
+
L.push(`- **${w.name}** (${w.kind || '-'}) — ${w.why}`, ...quotes(w.quotes));
|
|
49
|
+
L.push('', '## Stories');
|
|
50
|
+
for (const s of m.stories) {
|
|
51
|
+
L.push(`### ${s.title}`, `Why it matters: ${s.whyItMatters}`, `Rows: #${s.from}–#${s.to}`);
|
|
52
|
+
for (const tp of s.turningPoints)
|
|
53
|
+
L.push(`- Turning point: ${tp.moment}`, ...tp.userLines.map(x => ' ' + q(x)));
|
|
54
|
+
L.push('');
|
|
55
|
+
}
|
|
56
|
+
L.push('## Angles');
|
|
57
|
+
for (const a of m.angles)
|
|
58
|
+
L.push(`- **${a.title}** — demand question: ${a.demandQuestion}${a.story ? ` (story: ${a.story})` : ''}`, ...quotes(a.quotes));
|
|
59
|
+
L.push('', '## Anchors (row ranges per story)');
|
|
60
|
+
for (const a of m.anchors) {
|
|
61
|
+
const audio = a.audio ? `; audio ${a.audio.start ?? '?'}–${a.audio.end ?? '?'}${a.audio.approximate ? ' (approx.)' : ''}` : '';
|
|
62
|
+
L.push(`- ${a.story}: rows #${a.from}–#${a.to} (${a.rows} conversation rows, ${a.fromTs} → ${a.toTs}${audio})`);
|
|
63
|
+
}
|
|
64
|
+
L.push('', `## Quote verification`, `- Verified quotes kept: ${m.quotesKept}; quotes dropped: ${m.drops.length}; items dropped: ${m.itemsDropped.length}`);
|
|
65
|
+
for (const d of m.drops)
|
|
66
|
+
L.push(`- DROP [${d.section}] ${d.item} — row ${d.row || '?'}: "${d.text}" → ${d.reason}`);
|
|
67
|
+
for (const it of m.itemsDropped)
|
|
68
|
+
L.push(`- ITEM DROPPED ${it}`);
|
|
69
|
+
return L.join('\n') + '\n';
|
|
70
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lens-period.ts — the PERIOD MAP: one model call over one raw block
|
|
3
|
+
* (lens-period-block.ts) → what this stretch of work was, as a map.
|
|
4
|
+
*
|
|
5
|
+
* Sections: high_leverage (ranked first), period_goal, arc, what_worked,
|
|
6
|
+
* stories (with the user's own verbatim lines at the turning points), angles
|
|
7
|
+
* (each with the demand question it answers) and anchors (row ranges per story,
|
|
8
|
+
* for a later clip/library step).
|
|
9
|
+
*
|
|
10
|
+
* Every quote is verified DETERMINISTICALLY with lens-quotes against the block's
|
|
11
|
+
* rows; the speaker/row/timestamp written out are the matched row's. A failing
|
|
12
|
+
* quote is dropped (and logged with the reason); an item left with no verified
|
|
13
|
+
* quote is dropped. Story lines must be the user's own words (not pasted text).
|
|
14
|
+
*/
|
|
15
|
+
import { type DbRecord } from './lens-db.js';
|
|
16
|
+
import { type Quote } from './lens-quotes.js';
|
|
17
|
+
import { type RecordingAlignment } from './lens-audio.js';
|
|
18
|
+
export type LeverageEvidence = 'shipped' | 'root-caused' | 'decided';
|
|
19
|
+
export interface HighLeverage {
|
|
20
|
+
title: string;
|
|
21
|
+
why: string;
|
|
22
|
+
evidence: LeverageEvidence;
|
|
23
|
+
quotes: Quote[];
|
|
24
|
+
}
|
|
25
|
+
export interface ArcStep {
|
|
26
|
+
step: string;
|
|
27
|
+
what: string;
|
|
28
|
+
why: string;
|
|
29
|
+
quotes: Quote[];
|
|
30
|
+
}
|
|
31
|
+
export interface WorkedItem {
|
|
32
|
+
name: string;
|
|
33
|
+
kind: string;
|
|
34
|
+
why: string;
|
|
35
|
+
quotes: Quote[];
|
|
36
|
+
}
|
|
37
|
+
export interface TurningPoint {
|
|
38
|
+
moment: string;
|
|
39
|
+
userLines: Quote[];
|
|
40
|
+
}
|
|
41
|
+
export interface Story {
|
|
42
|
+
title: string;
|
|
43
|
+
whyItMatters: string;
|
|
44
|
+
turningPoints: TurningPoint[];
|
|
45
|
+
from: number;
|
|
46
|
+
to: number;
|
|
47
|
+
}
|
|
48
|
+
export interface Angle {
|
|
49
|
+
title: string;
|
|
50
|
+
demandQuestion: string;
|
|
51
|
+
story: string;
|
|
52
|
+
quotes: Quote[];
|
|
53
|
+
}
|
|
54
|
+
export interface Anchor {
|
|
55
|
+
story: string;
|
|
56
|
+
from: number;
|
|
57
|
+
to: number;
|
|
58
|
+
rows: number;
|
|
59
|
+
fromTs: string;
|
|
60
|
+
toTs: string;
|
|
61
|
+
/** Only when an AudioAdapter (lens-audio.ts) found a recording; transcript-only runs keep row ranges. */
|
|
62
|
+
audio?: {
|
|
63
|
+
path: string;
|
|
64
|
+
start: string | null;
|
|
65
|
+
end: string | null;
|
|
66
|
+
approximate: boolean;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export interface QuoteDrop {
|
|
70
|
+
section: string;
|
|
71
|
+
item: string;
|
|
72
|
+
text: string;
|
|
73
|
+
row: string;
|
|
74
|
+
reason: string;
|
|
75
|
+
}
|
|
76
|
+
export interface PeriodMap {
|
|
77
|
+
highLeverage: HighLeverage[];
|
|
78
|
+
periodGoal: {
|
|
79
|
+
text: string;
|
|
80
|
+
quotes: Quote[];
|
|
81
|
+
} | null;
|
|
82
|
+
arc: ArcStep[];
|
|
83
|
+
whatWorked: {
|
|
84
|
+
heldUp: WorkedItem[];
|
|
85
|
+
didnt: WorkedItem[];
|
|
86
|
+
};
|
|
87
|
+
stories: Story[];
|
|
88
|
+
angles: Angle[];
|
|
89
|
+
anchors: Anchor[];
|
|
90
|
+
drops: QuoteDrop[];
|
|
91
|
+
quotesKept: number;
|
|
92
|
+
itemsDropped: string[];
|
|
93
|
+
}
|
|
94
|
+
/** Transcript FIRST, instructions AFTER (measured on minimax-m3: the task gets lost otherwise). */
|
|
95
|
+
export declare function periodPrompt(blockText: string, meta: {
|
|
96
|
+
from: string;
|
|
97
|
+
to: string;
|
|
98
|
+
rows: number;
|
|
99
|
+
}): string;
|
|
100
|
+
export declare function normalizeLeverage(v: unknown): LeverageEvidence | '';
|
|
101
|
+
/** Parse + verify the model's map against the block rows. Null when the JSON is unusable. */
|
|
102
|
+
export declare function readPeriodMap(raw: string, records: DbRecord[]): PeriodMap | null;
|
|
103
|
+
/** Optional audio step: attach clip offsets to anchors when a recording alignment exists; no-op otherwise. */
|
|
104
|
+
export declare function attachAudio(map: PeriodMap, align: RecordingAlignment | null): PeriodMap;
|
|
105
|
+
/** Plain-text view of the block for debugging/tests. */
|
|
106
|
+
export declare const blockView: (records: DbRecord[]) => string;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lens-period.ts — the PERIOD MAP: one model call over one raw block
|
|
3
|
+
* (lens-period-block.ts) → what this stretch of work was, as a map.
|
|
4
|
+
*
|
|
5
|
+
* Sections: high_leverage (ranked first), period_goal, arc, what_worked,
|
|
6
|
+
* stories (with the user's own verbatim lines at the turning points), angles
|
|
7
|
+
* (each with the demand question it answers) and anchors (row ranges per story,
|
|
8
|
+
* for a later clip/library step).
|
|
9
|
+
*
|
|
10
|
+
* Every quote is verified DETERMINISTICALLY with lens-quotes against the block's
|
|
11
|
+
* rows; the speaker/row/timestamp written out are the matched row's. A failing
|
|
12
|
+
* quote is dropped (and logged with the reason); an item left with no verified
|
|
13
|
+
* quote is dropped. Story lines must be the user's own words (not pasted text).
|
|
14
|
+
*/
|
|
15
|
+
import { recordView } from './lens-db.js';
|
|
16
|
+
import { buildRecordIndex, explainQuote, parseJsonObject, verifyQuote } from './lens-quotes.js';
|
|
17
|
+
import { isPastedSpan } from './lens-clip-arc.js';
|
|
18
|
+
import { clipOffset } from './lens-audio.js';
|
|
19
|
+
const QUOTE_RULES = 'QUOTES: every "quotes"/"user_lines" entry = {"row": the record\'s row number from its [#row | timestamp | speaker] header ' +
|
|
20
|
+
'(digits only), "text": an exact verbatim substring (12-300 chars) copied character-for-character from THAT ONE record}. ' +
|
|
21
|
+
'ONE contiguous span: no ellipses, no skipped words, no merged sentences, no typo or grammar fixes — the person speaks by ' +
|
|
22
|
+
'voice, keep their disfluencies exactly. Never quote secrets, keys, tokens, emails, or personal details; keep masked tags ' +
|
|
23
|
+
'like [client] exactly as written.';
|
|
24
|
+
const SHAPE = '{"high_leverage": [{"title", "why": one line — why it is high-leverage, "evidence": "shipped"|"root-caused"|"decided", "quotes": [1-3]}],\n' +
|
|
25
|
+
' "period_goal": {"text": 1-2 sentences, "quotes": [1-3]},\n' +
|
|
26
|
+
' "arc": [{"step": "tried"|"failed"|"pivot"|"worked"|"outcome", "what": one line, "why": one line (why it failed / why it worked / what changed), "quotes": [1-2]}],\n' +
|
|
27
|
+
' "what_worked": {"held_up": [{"name", "kind": "tool"|"model"|"approach", "why", "quotes": [1-2]}], "didnt": [same shape]},\n' +
|
|
28
|
+
' "stories": [{"title", "why_it_matters": one line, "from_row": first row of the story, "to_row": last row, ' +
|
|
29
|
+
'"turning_points": [{"moment": one line, "user_lines": [1-2 USER quotes]}]}],\n' +
|
|
30
|
+
' "angles": [{"title", "demand_question": the question a real person is asking that this would answer, "story": story title or "", "quotes": [1-2]}]}';
|
|
31
|
+
/** Transcript FIRST, instructions AFTER (measured on minimax-m3: the task gets lost otherwise). */
|
|
32
|
+
export function periodPrompt(blockText, meta) {
|
|
33
|
+
return [
|
|
34
|
+
`Below is ONE raw, unedited, chronological stretch (${meta.rows} records, ${meta.from} to ${meta.to}) of a long voice work ` +
|
|
35
|
+
'session between a person (user) and their AI coding assistant (assistant). Conversation only — tool calls are not shown. ' +
|
|
36
|
+
'Read all of it, beginning to end; your task follows after the transcript.',
|
|
37
|
+
'<transcript>',
|
|
38
|
+
blockText,
|
|
39
|
+
'</transcript>',
|
|
40
|
+
'TASK: map this whole period. Do not summarise record by record — understand what the stretch of work was about and how ' +
|
|
41
|
+
'it unfolded, then fill every section:\n' +
|
|
42
|
+
'- high_leverage (0-6, strongest first): the high-value work of the period — something that unblocked a lot, saved real ' +
|
|
43
|
+
'money or time, a fix that took a whole class of problems off the table, a reusable technique others could use, or a ' +
|
|
44
|
+
'decision that changed direction. "evidence": shipped (built and shown working), root-caused (cause found/proven), ' +
|
|
45
|
+
'decided (a direction-changing decision the person made).\n' +
|
|
46
|
+
'- period_goal: what this stretch of work was about overall.\n' +
|
|
47
|
+
'- arc (4-12 steps, in order): what was tried, what failed and why, the pivots, what finally worked, and the outcome.\n' +
|
|
48
|
+
'- what_worked: tools, models and approaches that held up, and ones that did not (with why).\n' +
|
|
49
|
+
'- stories (1-6): self-contained sub-stories inside the period. Each with a title, why it matters, its row range, and the ' +
|
|
50
|
+
'PERSON\'s own verbatim lines (user records, their spoken words — not pasted logs/reviews) at the turning points.\n' +
|
|
51
|
+
'- angles (1-6): postable angles, each with the demand question it would answer.\n' +
|
|
52
|
+
'Keep masked tags like [client]; never reintroduce client names, IDs or amounts. No personal invoicing/money-owed items.',
|
|
53
|
+
QUOTE_RULES,
|
|
54
|
+
`Output ONLY one JSON object: ${SHAPE}`,
|
|
55
|
+
].join('\n\n');
|
|
56
|
+
}
|
|
57
|
+
const str = (v) => (typeof v === 'string' ? v.trim() : typeof v === 'number' ? String(v) : '');
|
|
58
|
+
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
59
|
+
export function normalizeLeverage(v) {
|
|
60
|
+
const s = str(v).toLowerCase().replace(/[\s_]+/g, '-');
|
|
61
|
+
if (s === 'shipped' || s === 'verified' || s === 'done')
|
|
62
|
+
return 'shipped';
|
|
63
|
+
if (s === 'root-caused' || s === 'root-cause' || s === 'rootcaused')
|
|
64
|
+
return 'root-caused';
|
|
65
|
+
if (s === 'decided' || s === 'decision')
|
|
66
|
+
return 'decided';
|
|
67
|
+
return '';
|
|
68
|
+
}
|
|
69
|
+
class Verifier {
|
|
70
|
+
idx;
|
|
71
|
+
byId;
|
|
72
|
+
ids;
|
|
73
|
+
drops = [];
|
|
74
|
+
kept = 0;
|
|
75
|
+
constructor(records) {
|
|
76
|
+
this.idx = buildRecordIndex(records);
|
|
77
|
+
this.byId = new Map(records.map(r => [r.id, r]));
|
|
78
|
+
this.ids = records.map(r => r.id);
|
|
79
|
+
}
|
|
80
|
+
/** Timestamp of the cited row (or the nearest row id) so lens-quotes can verify by row + time. */
|
|
81
|
+
tsFor(row) {
|
|
82
|
+
const r = this.byId.get(row);
|
|
83
|
+
if (r)
|
|
84
|
+
return r.timestamp;
|
|
85
|
+
let best = this.ids[0];
|
|
86
|
+
for (const id of this.ids)
|
|
87
|
+
if (Math.abs(id - row) < Math.abs(best - row))
|
|
88
|
+
best = id;
|
|
89
|
+
return this.byId.get(best)?.timestamp ?? '';
|
|
90
|
+
}
|
|
91
|
+
quotes(raw, section, item, max, userOnly = false) {
|
|
92
|
+
const out = [];
|
|
93
|
+
const seen = new Set();
|
|
94
|
+
for (const q of arr(raw).slice(0, max)) {
|
|
95
|
+
const row = Number(str(q?.row).replace(/^#/, ''));
|
|
96
|
+
const cand = { text: q?.text, row, timestamp: Number.isFinite(row) ? this.tsFor(row) : '' };
|
|
97
|
+
const v = verifyQuote(cand, this.idx);
|
|
98
|
+
const drop = (reason) => this.drops.push({ section, item, text: str(q?.text).slice(0, 160), row: str(q?.row), reason });
|
|
99
|
+
if (!v) {
|
|
100
|
+
drop(explainQuote(cand, this.idx));
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (userOnly && (v.speaker !== 'user' || isPastedSpan(this.byId.get(v.row)?.text ?? '', v.text))) {
|
|
104
|
+
drop(v.speaker !== 'user' ? 'not a user line' : 'pasted text, not the user\'s own words');
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const key = `${v.row}:${v.text}`;
|
|
108
|
+
if (seen.has(key))
|
|
109
|
+
continue;
|
|
110
|
+
seen.add(key);
|
|
111
|
+
out.push(v);
|
|
112
|
+
}
|
|
113
|
+
this.kept += out.length;
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Parse + verify the model's map against the block rows. Null when the JSON is unusable. */
|
|
118
|
+
export function readPeriodMap(raw, records) {
|
|
119
|
+
const o = parseJsonObject(raw);
|
|
120
|
+
if (!o)
|
|
121
|
+
return null;
|
|
122
|
+
const V = new Verifier(records);
|
|
123
|
+
const itemsDropped = [];
|
|
124
|
+
const keep = (section, label, it) => {
|
|
125
|
+
if (it.quotes.length)
|
|
126
|
+
return it;
|
|
127
|
+
itemsDropped.push(`${section}: ${label}`);
|
|
128
|
+
return null;
|
|
129
|
+
};
|
|
130
|
+
const hl = arr(o.high_leverage).slice(0, 6).map(h => {
|
|
131
|
+
const title = str(h?.title);
|
|
132
|
+
const evidence = normalizeLeverage(h?.evidence);
|
|
133
|
+
if (!title)
|
|
134
|
+
return null;
|
|
135
|
+
if (!evidence) {
|
|
136
|
+
itemsDropped.push(`high_leverage: ${title} (evidence "${str(h?.evidence)}" not shipped/root-caused/decided)`);
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return keep('high_leverage', title, { title, why: str(h?.why), evidence, quotes: V.quotes(h?.quotes, 'high_leverage', title, 3) });
|
|
140
|
+
});
|
|
141
|
+
const pg = o.period_goal;
|
|
142
|
+
const goalText = str(pg?.text) || str(pg);
|
|
143
|
+
const goal = goalText ? keep('period_goal', 'goal', { text: goalText, quotes: V.quotes(pg?.quotes, 'period_goal', 'goal', 3) }) : null;
|
|
144
|
+
const arc = arr(o.arc).slice(0, 14).map((s, i) => {
|
|
145
|
+
const what = str(s?.what);
|
|
146
|
+
if (!what)
|
|
147
|
+
return null;
|
|
148
|
+
return keep('arc', `${i + 1}. ${what}`, { step: str(s?.step) || 'step', what, why: str(s?.why), quotes: V.quotes(s?.quotes, 'arc', what, 2) });
|
|
149
|
+
});
|
|
150
|
+
const worked = (v, sec) => arr(v).slice(0, 8).map(w => {
|
|
151
|
+
const name = str(w?.name);
|
|
152
|
+
if (!name)
|
|
153
|
+
return null;
|
|
154
|
+
return keep(sec, name, { name, kind: str(w?.kind), why: str(w?.why), quotes: V.quotes(w?.quotes, sec, name, 2) });
|
|
155
|
+
}).filter((x) => !!x);
|
|
156
|
+
const ww = (o.what_worked ?? {});
|
|
157
|
+
const blockFirst = records[0]?.id ?? 0;
|
|
158
|
+
const blockLast = records[records.length - 1]?.id ?? 0;
|
|
159
|
+
const clamp = (n) => Math.min(blockLast, Math.max(blockFirst, n));
|
|
160
|
+
const stories = [];
|
|
161
|
+
for (const s of arr(o.stories).slice(0, 6)) {
|
|
162
|
+
const title = str(s?.title);
|
|
163
|
+
if (!title)
|
|
164
|
+
continue;
|
|
165
|
+
const tps = arr(s?.turning_points).slice(0, 5).map(tp => ({ moment: str(tp?.moment), userLines: V.quotes(tp?.user_lines, 'stories', title, 2, true) }))
|
|
166
|
+
.filter(tp => tp.userLines.length);
|
|
167
|
+
if (!tps.length) {
|
|
168
|
+
itemsDropped.push(`stories: ${title} (no verified user line)`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const rows = tps.flatMap(tp => tp.userLines.map(q => q.row));
|
|
172
|
+
const f = Number(str(s?.from_row).replace(/^#/, ''));
|
|
173
|
+
const t = Number(str(s?.to_row).replace(/^#/, ''));
|
|
174
|
+
// Anchor range = the model's range clamped to the block, always covering every verified line.
|
|
175
|
+
const from = Math.min(Number.isFinite(f) && f > 0 ? clamp(f) : Infinity, ...rows);
|
|
176
|
+
const to = Math.max(Number.isFinite(t) && t > 0 ? clamp(t) : -Infinity, ...rows);
|
|
177
|
+
stories.push({ title, whyItMatters: str(s?.why_it_matters), turningPoints: tps, from, to });
|
|
178
|
+
}
|
|
179
|
+
const angles = arr(o.angles).slice(0, 6).map(a => {
|
|
180
|
+
const title = str(a?.title);
|
|
181
|
+
if (!title)
|
|
182
|
+
return null;
|
|
183
|
+
return keep('angles', title, { title, demandQuestion: str(a?.demand_question), story: str(a?.story), quotes: V.quotes(a?.quotes, 'angles', title, 2) });
|
|
184
|
+
});
|
|
185
|
+
const anchors = stories.map(s => {
|
|
186
|
+
const inRange = records.filter(r => r.id >= s.from && r.id <= s.to);
|
|
187
|
+
return { story: s.title, from: s.from, to: s.to, rows: inRange.length, fromTs: inRange[0]?.timestamp ?? '', toTs: inRange[inRange.length - 1]?.timestamp ?? '' };
|
|
188
|
+
});
|
|
189
|
+
const nn = (xs) => xs.filter((x) => !!x);
|
|
190
|
+
return {
|
|
191
|
+
highLeverage: nn(hl), periodGoal: goal, arc: nn(arc), whatWorked: { heldUp: worked(ww.held_up, 'what_worked.held_up'), didnt: worked(ww.didnt, 'what_worked.didnt') },
|
|
192
|
+
stories, angles: nn(angles), anchors, drops: V.drops, quotesKept: V.kept, itemsDropped,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/** Optional audio step: attach clip offsets to anchors when a recording alignment exists; no-op otherwise. */
|
|
196
|
+
export function attachAudio(map, align) {
|
|
197
|
+
if (!align)
|
|
198
|
+
return map;
|
|
199
|
+
for (const a of map.anchors) {
|
|
200
|
+
a.audio = { path: align.path, start: a.fromTs ? clipOffset(a.fromTs, align.startIso) : null, end: a.toTs ? clipOffset(a.toTs, align.startIso) : null, approximate: align.approximate };
|
|
201
|
+
}
|
|
202
|
+
return map;
|
|
203
|
+
}
|
|
204
|
+
/** Plain-text view of the block for debugging/tests. */
|
|
205
|
+
export const blockView = (records) => records.map(recordView).join('\n\n');
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lens-profile.ts — format + APPEND (never rewrite) the per-project
|
|
3
|
+
* content-profile.md entry, and the worker's small redacted error log.
|
|
4
|
+
*/
|
|
5
|
+
import type { Evidence, Quote } from './lens-quotes.js';
|
|
6
|
+
import type { GapEvidence } from './lens-hn.js';
|
|
7
|
+
import { type RecordingAlignment } from './lens-audio.js';
|
|
8
|
+
export interface FinalAngle {
|
|
9
|
+
title: string;
|
|
10
|
+
why: string;
|
|
11
|
+
quotes: Quote[];
|
|
12
|
+
gap: GapEvidence;
|
|
13
|
+
}
|
|
14
|
+
export interface FinalCapability {
|
|
15
|
+
name: string;
|
|
16
|
+
did: string;
|
|
17
|
+
evidence: Evidence;
|
|
18
|
+
proof: string;
|
|
19
|
+
quotes: Quote[];
|
|
20
|
+
}
|
|
21
|
+
export interface EntryMeta {
|
|
22
|
+
sessionId: string;
|
|
23
|
+
source: 'recall-db';
|
|
24
|
+
mode: 'incremental' | 'backfill';
|
|
25
|
+
model: string;
|
|
26
|
+
records: number;
|
|
27
|
+
rowRange: [number, number];
|
|
28
|
+
windows: number;
|
|
29
|
+
windowsTotal: number;
|
|
30
|
+
modelCalls: number;
|
|
31
|
+
costUsd: number;
|
|
32
|
+
dropped: {
|
|
33
|
+
angles: number;
|
|
34
|
+
capabilities: number;
|
|
35
|
+
};
|
|
36
|
+
notDone: number;
|
|
37
|
+
billingDropped?: number;
|
|
38
|
+
/** True when no client denylist was loaded: header carries a review warning. */
|
|
39
|
+
noDenylist?: boolean;
|
|
40
|
+
recording: RecordingAlignment | null;
|
|
41
|
+
}
|
|
42
|
+
export declare function formatEntry(angles: FinalAngle[], caps: FinalCapability[], meta: EntryMeta): string;
|
|
43
|
+
export declare function appendProfile(projectDir: string, entry: string): string;
|
|
44
|
+
/** Scratch output (sample runs): overwrite a single file, never the profile. */
|
|
45
|
+
export declare function writeScratch(path: string, entry: string): string;
|
|
46
|
+
/** Small redacted log line in the project dir; truncated when it grows past 256 KB. */
|
|
47
|
+
export declare function logLine(projectDir: string | null, msg: string): void;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lens-profile.ts — format + APPEND (never rewrite) the per-project
|
|
3
|
+
* content-profile.md entry, and the worker's small redacted error log.
|
|
4
|
+
*/
|
|
5
|
+
import { appendFileSync, existsSync, mkdirSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { redactSecrets } from './transcript-sanitizer.js';
|
|
8
|
+
import { clipOffset } from './lens-audio.js';
|
|
9
|
+
import { LOG_FILE, PROFILE_FILE } from './lens-paths.js';
|
|
10
|
+
const PROFILE_HEADER = '# Content Profile\n\nAppend-only. Postable angles + demonstrated capabilities auto-extracted at each compaction ' +
|
|
11
|
+
'(disable with OSBORN_CONTENT_LENS=0). Every quote below was verified verbatim against the sanitized session ' +
|
|
12
|
+
'recall store (session.db row #, timestamp); capabilities are tagged shipped | root-caused. Review before posting.\n';
|
|
13
|
+
const oneLine = (s) => s.replace(/\s+/g, ' ').trim();
|
|
14
|
+
function quoteLines(quotes, rec, indent = ' ') {
|
|
15
|
+
return quotes.map(q => {
|
|
16
|
+
const clip = rec ? clipOffset(q.timestamp, rec.startIso) : null;
|
|
17
|
+
const clipTxt = clip ? ` · clip @${clip}${rec.approximate ? ' (approx)' : ''}` : '';
|
|
18
|
+
return `${indent}- > "${oneLine(q.text)}" — *${q.speaker}*, \`${q.timestamp}\` (row #${q.row})${clipTxt}`;
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function formatEntry(angles, caps, meta) {
|
|
22
|
+
const L = [];
|
|
23
|
+
L.push(`\n## ${new Date().toISOString()} — session ${meta.sessionId.substring(0, 8)}`);
|
|
24
|
+
L.push(`_source: ${meta.source} (${meta.mode}) · rows #${meta.rowRange[0]}–#${meta.rowRange[1]} · ${meta.records} conversation rows · ` +
|
|
25
|
+
`${meta.windows}/${meta.windowsTotal} window(s) · ${meta.model} · ${meta.modelCalls} call(s) · $${meta.costUsd.toFixed(4)} · ` +
|
|
26
|
+
`dropped by grounding check: ${meta.dropped.angles} angle(s), ${meta.dropped.capabilities} capability(ies) · ` +
|
|
27
|
+
`dropped as not-done (plan/speculation): ${meta.notDone}` +
|
|
28
|
+
`${meta.billingDropped !== undefined ? ` · dropped by finance-topic filter: ${meta.billingDropped}` : ''}_`);
|
|
29
|
+
if (meta.noDenylist) {
|
|
30
|
+
L.push('_WARNING: no client denylist was loaded — only shape-based redaction (emails, ids, amounts, legal-suffix company names) was applied. Check for client names before posting._');
|
|
31
|
+
}
|
|
32
|
+
if (meta.recording) {
|
|
33
|
+
const r = meta.recording;
|
|
34
|
+
L.push(`_recording: \`${r.path}\` · clip offsets relative to ${r.startIso} (${r.basis}` +
|
|
35
|
+
`${r.approximate ? '; alignment APPROXIMATE' : ''})_`);
|
|
36
|
+
}
|
|
37
|
+
L.push('');
|
|
38
|
+
if (angles.length) {
|
|
39
|
+
L.push('### Angles', '');
|
|
40
|
+
for (const a of angles) {
|
|
41
|
+
L.push(`#### ${oneLine(a.title)}`);
|
|
42
|
+
if (a.why)
|
|
43
|
+
L.push(`- **Why postable:** ${oneLine(a.why)}`);
|
|
44
|
+
L.push('- **Verified quotes:**', ...quoteLines(a.quotes, meta.recording));
|
|
45
|
+
L.push(`- **Gap evidence** (HN: ${a.gap.queries.map(q => `\`${q}\``).join(', ') || 'none'}):`);
|
|
46
|
+
for (const s of a.gap.stories) {
|
|
47
|
+
L.push(` - [${oneLine(s.title)}](${s.url}) — ${s.points} pts, ${s.num_comments} comments`);
|
|
48
|
+
}
|
|
49
|
+
L.push(` - ${a.gap.note}`, '');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (caps.length) {
|
|
53
|
+
L.push('### Capabilities', '');
|
|
54
|
+
for (const c of caps) {
|
|
55
|
+
L.push(`- **${oneLine(c.name)}** \`[${c.evidence}]\` — ${oneLine(c.did)}`);
|
|
56
|
+
if (c.proof)
|
|
57
|
+
L.push(` - _proof:_ ${oneLine(c.proof)}`);
|
|
58
|
+
L.push(...quoteLines(c.quotes, meta.recording, ' '));
|
|
59
|
+
}
|
|
60
|
+
L.push('');
|
|
61
|
+
}
|
|
62
|
+
// Defense in depth: everything is redacted (incl. high-entropy) before disk.
|
|
63
|
+
return redactSecrets(L.join('\n'), { assistant: true });
|
|
64
|
+
}
|
|
65
|
+
export function appendProfile(projectDir, entry) {
|
|
66
|
+
const path = join(projectDir, PROFILE_FILE);
|
|
67
|
+
mkdirSync(projectDir, { recursive: true });
|
|
68
|
+
if (!existsSync(path))
|
|
69
|
+
appendFileSync(path, PROFILE_HEADER, 'utf-8');
|
|
70
|
+
appendFileSync(path, entry, 'utf-8');
|
|
71
|
+
return path;
|
|
72
|
+
}
|
|
73
|
+
/** Scratch output (sample runs): overwrite a single file, never the profile. */
|
|
74
|
+
export function writeScratch(path, entry) {
|
|
75
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
76
|
+
writeFileSync(path, PROFILE_HEADER.replace('# Content Profile', '# Content Lens — scratch sample') + entry, 'utf-8');
|
|
77
|
+
return path;
|
|
78
|
+
}
|
|
79
|
+
const LOG_MAX_BYTES = 256 * 1024;
|
|
80
|
+
/** Small redacted log line in the project dir; truncated when it grows past 256 KB. */
|
|
81
|
+
export function logLine(projectDir, msg) {
|
|
82
|
+
if (!projectDir)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
const p = join(projectDir, LOG_FILE);
|
|
86
|
+
const line = `${new Date().toISOString()} ${redactSecrets(oneLine(msg), { assistant: true }).slice(0, 2000)}\n`;
|
|
87
|
+
if (existsSync(p) && statSync(p).size > LOG_MAX_BYTES)
|
|
88
|
+
writeFileSync(p, line, 'utf-8');
|
|
89
|
+
else
|
|
90
|
+
appendFileSync(p, line, 'utf-8');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
/* never throw from logging */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lens-quotes.ts — the DETERMINISTIC anti-hallucination guard (no model).
|
|
3
|
+
*
|
|
4
|
+
* Every quote the model returns must be an exact (whitespace-normalized)
|
|
5
|
+
* substring of a sanitized session.db conversation row carrying the SAME
|
|
6
|
+
* timestamp (rows the model actually saw). Items (angles or capabilities) with
|
|
7
|
+
* any unverifiable quote are dropped wholesale. The speaker and row id written
|
|
8
|
+
* to disk are taken from the matched row, never from the model.
|
|
9
|
+
*/
|
|
10
|
+
export interface Quote {
|
|
11
|
+
text: string;
|
|
12
|
+
timestamp: string;
|
|
13
|
+
speaker: 'user' | 'assistant';
|
|
14
|
+
/** session.db content.id of the row the quote was verified against. */
|
|
15
|
+
row: number;
|
|
16
|
+
/** The model cited a slightly wrong timestamp; `timestamp` is the source row's. */
|
|
17
|
+
tsCorrected?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Anything row-shaped: DbRecord from lens-db.ts (id = content.id). */
|
|
20
|
+
export interface QuotableRecord {
|
|
21
|
+
id?: number;
|
|
22
|
+
text: string;
|
|
23
|
+
timestamp: string;
|
|
24
|
+
speaker: 'user' | 'assistant';
|
|
25
|
+
}
|
|
26
|
+
export declare const normalizeWs: (s: string) => string;
|
|
27
|
+
export type RecordIndex = Map<string, string[]>;
|
|
28
|
+
type IndexEntry = {
|
|
29
|
+
text: string;
|
|
30
|
+
loose: string;
|
|
31
|
+
speaker: Quote['speaker'];
|
|
32
|
+
row: number;
|
|
33
|
+
ts: string;
|
|
34
|
+
ms: number;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Formatting-only normalization applied identically to quote AND row: markdown
|
|
38
|
+
* emphasis/code marks, typographic quotes/ellipsis, whitespace, letter case. Words, digits and
|
|
39
|
+
* punctuation that carry meaning are untouched, so this is still a substring check.
|
|
40
|
+
*/
|
|
41
|
+
export declare const looseText: (s: string) => string;
|
|
42
|
+
/** A cited timestamp may slip (neighbouring row, one digit); beyond this it's a different moment. */
|
|
43
|
+
export declare const TS_TOLERANCE_MS: number;
|
|
44
|
+
/** timestamp → rows carrying it (normalized + loose text, row id, parsed time). */
|
|
45
|
+
export declare function buildRecordIndex(records: QuotableRecord[]): Map<string, IndexEntry[]>;
|
|
46
|
+
export type QuoteIndex = ReturnType<typeof buildRecordIndex>;
|
|
47
|
+
/**
|
|
48
|
+
* Verified quote or null. Pure + deterministic. The quote text must be a
|
|
49
|
+
* substring (whitespace/markdown-normalized) of a SOURCE row. Exact timestamp
|
|
50
|
+
* match is preferred; otherwise the nearest row containing the text is accepted
|
|
51
|
+
* only if the cited timestamp is within TS_TOLERANCE_MS, or the cited row id is
|
|
52
|
+
* that row. The timestamp, speaker and row written out are ALWAYS the source
|
|
53
|
+
* row's, never the model's (tsCorrected marks a fixed citation).
|
|
54
|
+
*/
|
|
55
|
+
export declare function verifyQuote(q: {
|
|
56
|
+
text?: unknown;
|
|
57
|
+
timestamp?: unknown;
|
|
58
|
+
row?: unknown;
|
|
59
|
+
}, idx: QuoteIndex): Quote | null;
|
|
60
|
+
/** Why a quote failed (debug only; same rules as verifyQuote). */
|
|
61
|
+
export declare function explainQuote(q: {
|
|
62
|
+
text?: unknown;
|
|
63
|
+
timestamp?: unknown;
|
|
64
|
+
row?: unknown;
|
|
65
|
+
}, idx: QuoteIndex): string;
|
|
66
|
+
/**
|
|
67
|
+
* Keep items whose quotes ALL verify (1..maxQuotes required). Returns the kept
|
|
68
|
+
* items with verified quotes substituted, plus the drop count.
|
|
69
|
+
*/
|
|
70
|
+
export declare function verifyItems<T extends {
|
|
71
|
+
quotes: unknown[];
|
|
72
|
+
}>(items: T[], idx: QuoteIndex, maxQuotes?: number): {
|
|
73
|
+
kept: (Omit<T, 'quotes'> & {
|
|
74
|
+
quotes: Quote[];
|
|
75
|
+
})[];
|
|
76
|
+
dropped: number;
|
|
77
|
+
};
|
|
78
|
+
/** Tolerant JSON-object parse: accepts fences or leading prose. */
|
|
79
|
+
export declare function parseJsonObject(raw: string): Record<string, unknown> | null;
|
|
80
|
+
export interface RawAngle {
|
|
81
|
+
title: string;
|
|
82
|
+
why: string;
|
|
83
|
+
quotes: unknown[];
|
|
84
|
+
queries: string[];
|
|
85
|
+
}
|
|
86
|
+
export type Evidence = 'shipped' | 'root-caused';
|
|
87
|
+
export interface RawCapability {
|
|
88
|
+
name: string;
|
|
89
|
+
did: string;
|
|
90
|
+
/** Normalized evidence type; '' when the model gave none / something else (= not done). */
|
|
91
|
+
evidence: Evidence | '';
|
|
92
|
+
proof: string;
|
|
93
|
+
quotes: unknown[];
|
|
94
|
+
}
|
|
95
|
+
/** Done-vs-planned gate: only shipped/verified or proven-root-cause items count. */
|
|
96
|
+
export declare function normalizeEvidence(v: unknown): Evidence | '';
|
|
97
|
+
export declare function splitByEvidence(caps: RawCapability[]): {
|
|
98
|
+
done: RawCapability[];
|
|
99
|
+
notDone: number;
|
|
100
|
+
};
|
|
101
|
+
export declare function readAngles(v: unknown, max: number): RawAngle[];
|
|
102
|
+
export declare function readCapabilities(v: unknown, max: number): RawCapability[];
|
|
103
|
+
export {};
|