Files
memind/health-p0-experiment.mjs
T
john f96fdcb3b9 feat(health): add P0 health channel kernel and encrypted health zone.
Lock health recording behind confirm-only state machines, reject public publishes for the health category, and ship the 14-day experiment Page Data templates with tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 18:04:26 +08:00

119 lines
3.3 KiB
JavaScript

export const HEALTH_P0_DATASET_NAME = 'health_p0_daily_log';
export const HEALTH_P0_INSERT_COLUMNS = Object.freeze([
'participant_code',
'log_date',
'am_measured_at',
'am_systolic',
'am_diastolic',
'am_pulse',
'pm_measured_at',
'pm_systolic',
'pm_diastolic',
'pm_pulse',
'sleep_start',
'sleep_end',
'sleep_minutes',
'sleep_source',
'weight_kg',
'symptoms',
'alcohol',
'stress',
'medication_change',
'medication_note',
'other_note',
'entry_channel',
'photo_sent',
]);
export const HEALTH_P0_READ_COLUMNS = Object.freeze([
'id',
...HEALTH_P0_INSERT_COLUMNS,
'created_at',
]);
export const HEALTH_P0_CREATE_TABLE_SQL = `
CREATE TABLE IF NOT EXISTS health_p0_daily_log (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
participant_code TEXT NOT NULL,
log_date DATE NOT NULL,
am_measured_at TEXT NULL,
am_systolic INT NULL,
am_diastolic INT NULL,
am_pulse INT NULL,
pm_measured_at TEXT NULL,
pm_systolic INT NULL,
pm_diastolic INT NULL,
pm_pulse INT NULL,
sleep_start TEXT NULL,
sleep_end TEXT NULL,
sleep_minutes INT NULL,
sleep_source TEXT NULL,
weight_kg NUMERIC(5,1) NULL,
symptoms JSONB NOT NULL DEFAULT '[]'::jsonb,
alcohol TEXT NULL,
stress TEXT NULL,
medication_change TEXT NULL,
medication_note TEXT NULL,
other_note TEXT NULL,
entry_channel TEXT NULL,
photo_sent BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMPTZ NULL
);
`.trim();
export function buildHealthP0DatasetRegistration() {
return {
name: HEALTH_P0_DATASET_NAME,
table: HEALTH_P0_DATASET_NAME,
description: 'MeMind Health P0 前置实验每日记录',
actions: ['read', 'insert'],
columns: {
read: [...HEALTH_P0_READ_COLUMNS],
insert: [...HEALTH_P0_INSERT_COLUMNS],
},
};
}
export function buildHealthP0BindSpecs({ password } = {}) {
return {
participant: {
relativePath: 'public/health-p0-log.html',
accessMode: 'public',
datasets: {
[HEALTH_P0_DATASET_NAME]: { insert: true, read: false },
},
},
researcher: {
relativePath: 'public/health-p0-admin.html',
accessMode: 'password',
password,
datasets: {
[HEALTH_P0_DATASET_NAME]: { insert: false, read: true },
},
},
};
}
export function assertHealthP0HtmlContract(html, { role } = {}) {
const source = String(html ?? '');
const issues = [];
if (!source.includes('/assets/page-data-client.js')) issues.push('missing_page_data_client');
if (/\bon(?:click|input|submit|change)\s*=/i.test(source)) issues.push('inline_event_handler');
if (/localStorage|sessionStorage|IndexedDB/i.test(source)) issues.push('browser_storage');
if (/127\.0\.0\.1:\d+/.test(source)) issues.push('hardcoded_localhost');
if (role === 'participant') {
if (!source.includes(`insertRow('${HEALTH_P0_DATASET_NAME}'`)
&& !source.includes(`insertRow("${HEALTH_P0_DATASET_NAME}"`)) {
issues.push('missing_insert');
}
if (source.includes('listRows(')) issues.push('participant_must_not_read');
}
if (role === 'researcher') {
if (!source.includes('authenticate(')) issues.push('missing_authenticate');
if (!source.includes('listRows(')) issues.push('missing_list_rows');
}
return { ok: issues.length === 0, issues };
}