Files
memind/page-data-delivery-assess.mjs
john 73b308af0a fix(page-data): enforce full workspace bind delivery and repair tooling
Prevent false-positive Finish Guard passes and sync-created fake bindings by assessing publication, policy, dataset, injection, and insert smoke before H5 delivery.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-12 15:30:52 +08:00

267 lines
8.1 KiB
JavaScript

import fs from 'node:fs/promises';
import {
detectPageDataDatasetUsageFromHtml,
inferPageDataBindAccessMode,
} from './page-data-html-detect.mjs';
import { listPageAccessPolicies, readPageAccessPolicy } from './page-data-policy-store.mjs';
import { createUserDataSpaceService } from './user-data-space-service.mjs';
const INJECTION_PAGE_ID_PATTERN =
/window\.__MINDSPACE_PAGE_DATA__\s*=\s*\{[^}]*"pageId"\s*:\s*"([^"]+)"/i;
const INJECTION_META_PAGE_ID_PATTERN =
/<meta[^>]+name=["']mindspace-page-data-page-id["'][^>]+content=["']([^"']+)["']/i;
export function extractInjectedPageIdFromHtml(html) {
const content = String(html ?? '');
const fromScript = content.match(INJECTION_PAGE_ID_PATTERN)?.[1]?.trim();
if (fromScript) return fromScript;
return content.match(INJECTION_META_PAGE_ID_PATTERN)?.[1]?.trim() ?? null;
}
export function buildSmokeInsertRow(policy, datasetName) {
const columns = policy?.datasets?.[datasetName]?.columns?.insert ?? [];
const row = {};
for (const column of columns) {
if (/rating|score|星级|评分/i.test(column)) {
row[column] = 3;
} else if (/phone|手机|电话/i.test(column)) {
row[column] = '13800000000';
} else {
row[column] = 'smoke-test';
}
}
return row;
}
export async function queryOnlinePublication(pool, pageId) {
if (!pool || !pageId) return null;
const [rows] = await pool.query(
`SELECT id, page_id, status, access_mode
FROM h5_publish_records
WHERE page_id = ? AND status = 'online'
ORDER BY published_at DESC
LIMIT 1`,
[pageId],
);
return rows[0] ?? null;
}
function findWorkspacePolicyForHtml({ publishDir, relativePath, html }) {
const usage = detectPageDataDatasetUsageFromHtml(html);
const htmlDatasetNames = [...usage.keys()].sort().join(',');
const expectedAccessMode = inferPageDataBindAccessMode(relativePath, html);
for (const policy of listPageAccessPolicies(publishDir)) {
const policyNames = Object.keys(policy?.datasets ?? {}).sort().join(',');
if (policyNames !== htmlDatasetNames) continue;
if (String(policy.accessMode ?? '').trim() !== expectedAccessMode) continue;
let matches = true;
for (const [datasetName, perms] of usage) {
const policyDataset = policy.datasets?.[datasetName];
if (!policyDataset) {
matches = false;
break;
}
if (perms.insert && !policyDataset.insert) matches = false;
if (perms.read && !policyDataset.read) matches = false;
}
if (matches) return policy;
}
return null;
}
export function assessWorkspacePageDataReadiness({ publishDir, relativePath, html }) {
const usage = detectPageDataDatasetUsageFromHtml(html);
if (!usage.size) {
return { ready: true, reasons: [] };
}
const reasons = [];
const dataSpace = createUserDataSpaceService({ workspaceRoot: publishDir });
for (const datasetName of usage.keys()) {
if (!dataSpace.getDataset(datasetName)) {
reasons.push(`dataset_not_registered:${datasetName}`);
}
}
const policy = findWorkspacePolicyForHtml({ publishDir, relativePath, html });
if (!policy) {
reasons.push('missing_workspace_policy');
}
return { ready: reasons.length === 0, reasons, policy };
}
export async function assessPageDataHtmlBinding({
pool,
userId,
publishDir,
relativePath,
html,
findPageByRelativePath,
}) {
const usage = detectPageDataDatasetUsageFromHtml(html);
if (!usage.size) {
return { bound: true, reasons: [], pageId: null };
}
const workspace = assessWorkspacePageDataReadiness({ publishDir, relativePath, html });
const reasons = [...workspace.reasons];
let pageId = null;
if (typeof findPageByRelativePath === 'function') {
const page = await findPageByRelativePath(userId, relativePath).catch(() => null);
pageId = page?.id ? String(page.id).trim() : null;
if (!pageId) {
reasons.push('missing_page_record');
}
} else {
reasons.push('missing_page_lookup');
}
if (pageId) {
const policy = readPageAccessPolicy(publishDir, pageId);
if (!policy) {
reasons.push('missing_policy_for_page');
} else {
const htmlDatasetNames = [...usage.keys()].sort().join(',');
const policyNames = Object.keys(policy.datasets ?? {}).sort().join(',');
if (htmlDatasetNames !== policyNames) {
reasons.push('policy_dataset_mismatch');
}
const expectedAccessMode = inferPageDataBindAccessMode(relativePath, html);
if (String(policy.accessMode ?? '').trim() !== expectedAccessMode) {
reasons.push('policy_access_mode_mismatch');
}
for (const [datasetName, perms] of usage) {
const policyDataset = policy.datasets?.[datasetName];
if (!policyDataset) continue;
if (perms.insert && !policyDataset.insert) reasons.push('missing_insert_permission');
if (perms.read && !policyDataset.read) reasons.push('missing_read_permission');
}
}
const publication = await queryOnlinePublication(pool, pageId);
if (!publication) {
reasons.push('missing_online_publication');
}
}
return {
bound: reasons.length === 0,
reasons: [...new Set(reasons)],
pageId,
policy: pageId ? readPageAccessPolicy(publishDir, pageId) : workspace.policy ?? null,
};
}
export async function fetchWorkspaceHtmlInjection({ url, fetchImpl = fetch }) {
const response = await fetchImpl(String(url));
if (!response.ok) {
return {
ok: false,
reason: `http_${response.status}`,
pageId: null,
html: '',
};
}
const html = await response.text();
const pageId = extractInjectedPageIdFromHtml(html);
if (!pageId) {
return { ok: false, reason: 'missing_page_data_injection', pageId: null, html };
}
return { ok: true, reason: null, pageId, html };
}
export async function smokeTestPageDataInsert({
apiBase,
pageId,
dataset,
policy,
fetchImpl = fetch,
}) {
const base = String(apiBase ?? '').replace(/\/$/, '') || '/api';
const row = buildSmokeInsertRow(policy, dataset);
if (!pageId || !dataset || !Object.keys(row).length) {
return { ok: false, reason: 'smoke_payload_unavailable', status: 0, body: null };
}
const response = await fetchImpl(
`${base}/public/pages/${encodeURIComponent(pageId)}/data/${encodeURIComponent(dataset)}/rows`,
{
method: 'POST',
headers: { 'content-type': 'application/json', accept: 'application/json' },
body: JSON.stringify(row),
},
);
const body = await response.json().catch(() => ({}));
if (!response.ok) {
return {
ok: false,
reason: body?.error?.code ?? 'insert_failed',
status: response.status,
body,
};
}
return { ok: true, reason: null, status: response.status, body };
}
export async function verifyPageDataDeliveryArtifacts({
artifacts = [],
publishDir,
apiBase,
pool,
userId,
findPageByRelativePath,
fetchImpl = fetch,
}) {
const failures = [];
for (const artifact of artifacts) {
if (artifact.isAdmin) continue;
const relativePath = artifact.relativePath;
const html = await fs.readFile(artifact.localPath, 'utf8').catch(() => '');
const usage = detectPageDataDatasetUsageFromHtml(html);
const insertDataset = [...usage.entries()].find(([, perms]) => perms.insert)?.[0] ?? null;
if (!insertDataset) continue;
const injection = await fetchWorkspaceHtmlInjection({ url: artifact.url, fetchImpl });
if (!injection.ok) {
failures.push({ relativePath, stage: 'injection', reason: injection.reason });
continue;
}
const assessment = await assessPageDataHtmlBinding({
pool,
userId,
publishDir,
relativePath,
html,
findPageByRelativePath,
});
if (!assessment.bound) {
failures.push({
relativePath,
stage: 'binding',
reason: assessment.reasons.join(','),
});
continue;
}
const smoke = await smokeTestPageDataInsert({
apiBase,
pageId: assessment.pageId,
dataset: insertDataset,
policy: assessment.policy,
fetchImpl,
});
if (!smoke.ok) {
failures.push({
relativePath,
stage: 'insert_smoke',
reason: smoke.reason,
status: smoke.status,
});
}
}
return failures;
}