diff --git a/page-data-html-detect.mjs b/page-data-html-detect.mjs index 623feab..3a6dfbf 100644 --- a/page-data-html-detect.mjs +++ b/page-data-html-detect.mjs @@ -47,13 +47,17 @@ export function detectPageDataDatasetUsageFromHtml(html) { // this API must therefore only require the safe, default capability. remember(match[1], { softDelete: true }); } - for (const match of text.matchAll(/\.insertRow\(\s*([A-Za-z_$][\w$]*)/g)) { + // `${...}` inside an HTML template literal is a row-id interpolation, not a + // JavaScript identifier passed as the dataset argument. Without this guard, + // snippets such as `onclick="deleteRow(${row.id})"` are detected as a `$` + // dataset and can make the whole workspace binding pass fail. + for (const match of text.matchAll(/\.insertRow\(\s*(?!\$\{)([A-Za-z_$][\w$]*)/g)) { remember(constants.get(match[1]) ?? match[1], { insert: true }); } - for (const match of text.matchAll(/\.listRows\(\s*([A-Za-z_$][\w$]*)/g)) { + for (const match of text.matchAll(/\.listRows\(\s*(?!\$\{)([A-Za-z_$][\w$]*)/g)) { remember(constants.get(match[1]) ?? match[1], { read: true }); } - for (const match of text.matchAll(/\.deleteRow\(\s*([A-Za-z_$][\w$]*)/g)) { + for (const match of text.matchAll(/\.deleteRow\(\s*(?!\$\{)([A-Za-z_$][\w$]*)/g)) { remember(constants.get(match[1]) ?? match[1], { softDelete: true }); } diff --git a/page-data-html-detect.test.mjs b/page-data-html-detect.test.mjs index 1ddcb1a..15a86b6 100644 --- a/page-data-html-detect.test.mjs +++ b/page-data-html-detect.test.mjs @@ -44,6 +44,17 @@ test('detectPageDataDatasetUsageFromHtml resolves dataset constants', () => { assert.deepEqual(usage.get('reading_survey'), { read: true }); }); +test('detectPageDataDatasetUsageFromHtml ignores template-literal row id interpolation', () => { + const html = ` + const DATASET = 'daily_log'; + await client.listRows(DATASET, { limit: 200 }); + rows.map((row) => \`\`); + `; + const usage = detectPageDataDatasetUsageFromHtml(html); + assert.deepEqual([...usage.entries()], [['daily_log', { read: true }]]); + assert.equal(usage.has('$'), false); +}); + test('assertPolicyMatchesHtmlDatasets rejects mismatched dataset names', () => { const html = `await c.insertRow('tkmind_exp_survey', {});`; assert.throws( diff --git a/page-data-workspace-ensure.mjs b/page-data-workspace-ensure.mjs index 25643d6..9ac54b3 100644 --- a/page-data-workspace-ensure.mjs +++ b/page-data-workspace-ensure.mjs @@ -137,20 +137,23 @@ export async function ensurePageDataHtmlPagesBound({ continue; } - const assessment = await assessPageDataHtmlBinding({ - pool, - userId, - publishDir: workspaceRoot, - relativePath: file.relativePath, - html: file.content, - findPageByRelativePath, - }); - if (assessment.bound) { - skipped.push({ relativePath: file.relativePath, reason: 'already_bound' }); - continue; - } - try { + // Keep every page's assessment and repair isolated. A malformed legacy + // page must be reported in `errors`, not reject the entire MindSpace page + // listing that invokes this best-effort maintenance pass. + const assessment = await assessPageDataHtmlBinding({ + pool, + userId, + publishDir: workspaceRoot, + relativePath: file.relativePath, + html: file.content, + findPageByRelativePath, + }); + if (assessment.bound) { + skipped.push({ relativePath: file.relativePath, reason: 'already_bound' }); + continue; + } + for (const datasetName of usage.keys()) { await ensureRegisteredDatasetFromHtml({ workspaceRoot,