Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions claims.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,16 +106,16 @@
},
{
"id": "test-count",
"claim": "356 tests pass on Node's built-in test runner, the count docs/ROADMAP.md records for `npm test`.",
"value": "356",
"metric": "passing node:test cases (# tests 356 / # pass 356 / # fail 0)",
"method": "npm test → node --test test/*.test.js, run in the claims job after npm ci, and the TAP summary is asserted. The roadmap sentence ('356 tests on Node's built-in runner (`npm test`, 2026-10-17)') is verified by the run, not read back from the prose.",
"claim": "358 tests pass on Node's built-in test runner, the count docs/ROADMAP.md records for `npm test`.",
"value": "358",
"metric": "passing node:test cases (# tests 358 / # pass 358 / # fail 0)",
"method": "npm test → node --test test/*.test.js, run in the claims job after npm ci, and the TAP summary is asserted. The roadmap sentence ('358 tests on Node's built-in runner (`npm test`, 2026-10-17)') is verified by the run, not read back from the prose.",
"repro": "npm test 2>&1 | grep -E '^# (tests|pass|fail)'",
"evidence": "docs/ROADMAP.md",
"as_of": "2026-10-17",
"check": {
"cmd": "npm test 2>&1 | grep -E '^# (tests|pass|fail)'",
"expect": { "contains": ["# tests 356", "# pass 356", "# fail 0"] },
"expect": { "contains": ["# tests 358", "# pass 358", "# fail 0"] },
"timeout": 120
}
},
Expand Down
2 changes: 1 addition & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
- **Session browser** with tool-call inspection, trace/waterfall view, spawn tracking and message timeline
- **Prompt tooling** — extraction (noise filtered), template clustering with outcome attribution, Claude-powered rewrites, and a prompt library that installs entries as native slash commands
- **Global search** across all platforms, insights dashboard, incremental session backup
- **React + Vite frontend** served by an Express backend; 356 tests on Node's built-in runner (`npm test`, 2026-10-17), CI on Node 22
- **React + Vite frontend** served by an Express backend; 358 tests on Node's built-in runner (`npm test`, 2026-10-17), CI on Node 22
- **Evidence-backed failure events and local review** with full-result invalidation, evidence navigation and narrow-screen session layout

## Current priorities
Expand Down
8 changes: 7 additions & 1 deletion lib/export.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,12 @@ function fenceFor(text) {
return '`'.repeat(longest + 1);
}

// Markdown table cell: escape backslashes first, then pipes, so a value ending
// in a backslash cannot turn our `\|` back into a live column separator.
function escapeTableCell(value) {
return String(value).replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
}

function renderSessionMarkdown(platform, payload, { maxToolBytes } = {}) {
const session = payload.session || {};
const lines = [];
Expand All @@ -179,7 +185,7 @@ function renderSessionMarkdown(platform, payload, { maxToolBytes } = {}) {
lines.push('| | |');
lines.push('|---|---|');
for (const [key, value] of metadataRows(platform, payload)) {
lines.push(`| ${key} | ${String(value).replace(/\|/g, '\\|')} |`);
lines.push(`| ${key} | ${escapeTableCell(value)} |`);
}
lines.push('');
lines.push(
Expand Down
12 changes: 10 additions & 2 deletions lib/library.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ function parseLibraryFile(raw) {
return { meta, content };
}

// Collapse any run of trailing newlines to exactly one. A linear scan rather
// than /\n*$/, which backtracks polynomially on long newline runs (ReDoS).
function withOneTrailingNewline(content) {
let end = content.length;
while (end > 0 && content.charCodeAt(end - 1) === 10) end--;
return `${content.slice(0, end)}\n`;
}

function serializeLibraryFile(meta, content) {
const lines = [
'---',
Expand All @@ -72,12 +80,12 @@ function serializeLibraryFile(meta, content) {
'---',
'',
];
return `${lines.join('\n')}${content.replace(/\n*$/, '\n')}`;
return `${lines.join('\n')}${withOneTrailingNewline(content)}`;
}

// Installed slash-command file: frontmatter with description only + prompt body
function serializeInstalledFile(description, content) {
return `---\ndescription: ${description || ''}\n---\n\n${content.replace(/\n*$/, '\n')}`;
return `---\ndescription: ${description || ''}\n---\n\n${withOneTrailingNewline(content)}`;
}

// Installed detection: file exists at the target path
Expand Down
2 changes: 1 addition & 1 deletion public/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -2898,7 +2898,7 @@ sessionList.addEventListener('scroll', () => {
// Ensure the selected card is visible; when virtualized it may not even be in the DOM
function scrollSessionIntoView(id) {
if (!id) return;
const esc = window.CSS && CSS.escape ? CSS.escape(id) : id.replace(/"/g, '\\"');
const esc = window.CSS && CSS.escape ? CSS.escape(id) : id.replace(/["\\]/g, '\\$&');
const el = sessionList.querySelector(`.session-item[data-session-id="${esc}"]`);
if (el) {
el.scrollIntoView({ block: 'nearest' });
Expand Down
10 changes: 10 additions & 0 deletions test/export.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ test('redactSecrets leaves ordinary text alone', () => {

// --- renderers (unit, tiny synthetic payload) ---

test('markdown metadata cells escape backslashes before pipes', () => {
const payload = { session: { id: 'synth-esc', cwd: 'C:\\proj|x\\' }, messages: [] };
const md = renderSessionMarkdown('codex', payload);
const row = md.split('\n').find((line) => line.startsWith('| Working directory |'));
assert.equal(row, '| Working directory | C:\\\\proj\\|x\\\\ |');
// Every pipe inside the value is preceded by an odd run of backslashes, i.e. escaped.
const cell = row.slice('| Working directory | '.length, -2);
for (const m of cell.matchAll(/(\\*)\|/g)) assert.equal(m[1].length % 2, 1);
});

const SYNTH = {
session: { id: 'synth-1', timestamp: '2026-01-01T00:00:00Z', cwd: '/tmp/proj', model: 'test-model' },
messages: [
Expand Down
14 changes: 14 additions & 0 deletions test/unit.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -429,3 +429,17 @@ test('resolveAllowedDir accepts absolute dirs under a root and rejects the rest'
assert.throws(() => safePath.resolveAllowedDir(bad, [root]), { name: 'PathEscapeError' });
}
});

const library = require(path.join(__dirname, '..', 'lib', 'library'));

test('serializeLibraryFile ends the body with exactly one newline, in linear time', () => {
const header = library.serializeLibraryFile({ name: 'x' }, '').slice(0, -1);
const body = (content) => library.serializeLibraryFile({ name: 'x' }, content).slice(header.length);
assert.equal(body('hello'), 'hello\n');
assert.equal(body('hello\n\n\n'), 'hello\n');
assert.equal(body('a\n\nb\n'), 'a\n\nb\n');
assert.equal(body(''), '\n');
const started = process.hrtime.bigint();
assert.equal(body(`${'\n'.repeat(200000)}x${'\n'.repeat(200000)}`).length, 200002);
assert.ok(Number(process.hrtime.bigint() - started) / 1e6 < 1000);
});
Loading