From 1233884670015e38d77745b8517820d9f8679365 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Szab=C3=B3=20D=C3=A1vid?= Date: Sat, 27 Jun 2026 10:54:17 +0200 Subject: [PATCH] Add survey psychometric validity assistant --- .../README.md | 26 +++ .../demo.js | 29 +++ .../make-demo-video.js | 39 ++++ .../package.json | 13 ++ .../reports/clean-result.json | 13 ++ .../reports/demo.mp4 | Bin 0 -> 29032 bytes .../reports/risky-report.md | 30 +++ .../reports/risky-result.json | 188 ++++++++++++++++++ .../reports/summary.svg | 12 ++ .../src/guard.js | 177 +++++++++++++++++ .../src/samplePackets.js | 75 +++++++ .../test.js | 25 +++ 12 files changed, 627 insertions(+) create mode 100644 survey-psychometric-validity-assistant/README.md create mode 100644 survey-psychometric-validity-assistant/demo.js create mode 100644 survey-psychometric-validity-assistant/make-demo-video.js create mode 100644 survey-psychometric-validity-assistant/package.json create mode 100644 survey-psychometric-validity-assistant/reports/clean-result.json create mode 100644 survey-psychometric-validity-assistant/reports/demo.mp4 create mode 100644 survey-psychometric-validity-assistant/reports/risky-report.md create mode 100644 survey-psychometric-validity-assistant/reports/risky-result.json create mode 100644 survey-psychometric-validity-assistant/reports/summary.svg create mode 100644 survey-psychometric-validity-assistant/src/guard.js create mode 100644 survey-psychometric-validity-assistant/src/samplePackets.js create mode 100644 survey-psychometric-validity-assistant/test.js diff --git a/survey-psychometric-validity-assistant/README.md b/survey-psychometric-validity-assistant/README.md new file mode 100644 index 00000000..f25d32f1 --- /dev/null +++ b/survey-psychometric-validity-assistant/README.md @@ -0,0 +1,26 @@ +# Survey Psychometric Validity Assistant + +Self-contained SCIBASE issue #16 slice for AI-assisted peer review of survey manuscripts. The assistant blocks or escalates AI review output when a submitted survey scale has psychometric validity gaps that would make the downstream review misleading. + +The module uses synthetic packets only and has no network calls, external AI APIs, private manuscripts, credentials, or live data. + +## What it checks + +- reverse-coded item handling drift +- low Cronbach alpha or missing internal-consistency evidence +- factor-loading cross-loads and weak primary loadings +- confirmatory factor analysis sample-size shortfall +- missing or inconsistent Likert anchors +- construct-claim mismatch between manuscript claims and validated scales +- release readiness for AI-generated peer-review output + +## Run + +```bash +npm run check +npm test +npm run demo +npm run demo:video +``` + +Demo outputs are written to `reports/`. diff --git a/survey-psychometric-validity-assistant/demo.js b/survey-psychometric-validity-assistant/demo.js new file mode 100644 index 00000000..900d0367 --- /dev/null +++ b/survey-psychometric-validity-assistant/demo.js @@ -0,0 +1,29 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import { evaluateSurveyPacket, renderMarkdownReport } from "./src/guard.js"; +import { cleanPacket, riskyPacket } from "./src/samplePackets.js"; + +const reportsDir = new URL("./reports/", import.meta.url); +await mkdir(reportsDir, { recursive: true }); + +const clean = evaluateSurveyPacket(cleanPacket); +const risky = evaluateSurveyPacket(riskyPacket); + +await writeFile(new URL("clean-result.json", reportsDir), JSON.stringify(clean, null, 2)); +await writeFile(new URL("risky-result.json", reportsDir), JSON.stringify(risky, null, 2)); +await writeFile(new URL("risky-report.md", reportsDir), renderMarkdownReport(risky)); + +const svg = ` + + + Survey Psychometric Validity Assistant + Packet: ${risky.packetId} + Decision: ${risky.decision} + Severity score: ${risky.severityScore} | Findings: ${risky.findingCount} + Top gates: reverse coding, reliability, CFA sample size, factor cross-loads + Action: hold AI peer-review release for psychometric review + + +`; +await writeFile(new URL("summary.svg", reportsDir), svg); + +console.log(JSON.stringify({ clean: clean.decision, risky: risky.decision, reportsDir: reportsDir.pathname }, null, 2)); diff --git a/survey-psychometric-validity-assistant/make-demo-video.js b/survey-psychometric-validity-assistant/make-demo-video.js new file mode 100644 index 00000000..cf9432e0 --- /dev/null +++ b/survey-psychometric-validity-assistant/make-demo-video.js @@ -0,0 +1,39 @@ +import { spawnSync } from "node:child_process"; +import { mkdir } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +const reportsDir = new URL("./reports/", import.meta.url); +await mkdir(reportsDir, { recursive: true }); + +const output = new URL("demo.mp4", reportsDir); +const font = "C\\:/Windows/Fonts/arial.ttf"; +const filter = [ + "color=c=0xf8fafc:s=960x540:d=5:r=12", + "drawbox=x=48:y=52:w=864:h=436:color=0xcbd5e1:t=2", + `drawtext=fontfile='${font}':text='Survey Psychometric Validity Assistant':x=78:y=92:fontsize=34:fontcolor=0x111827`, + `drawtext=fontfile='${font}':text='AI review release gate for survey manuscripts':x=78:y=146:fontsize=23:fontcolor=0x334155`, + `drawtext=fontfile='${font}':text='Checks reverse coding, reliability, factor loadings, CFA sample size':x=78:y=204:fontsize=22:fontcolor=0x111827`, + `drawtext=fontfile='${font}':text='Risky packet decision HOLD':x=78:y=276:fontsize=34:fontcolor=0x991b1b`, + `drawtext=fontfile='${font}':text='Action - psychometric reviewer required before AI output is trusted':x=78:y=344:fontsize=22:fontcolor=0x111827`, + "drawbox=x=78:y=402:w=660:h=18:color=0xfee2e2:t=18", + "drawbox=x=78:y=402:w=540:h=18:color=0xef4444:t=18" +].join(","); + +const result = spawnSync("ffmpeg", [ + "-y", + "-f", + "lavfi", + "-i", + filter, + "-pix_fmt", + "yuv420p", + "-movflags", + "+faststart", + fileURLToPath(output) +], { stdio: "inherit" }); + +if (result.status !== 0) { + throw new Error(`ffmpeg failed with status ${result.status}`); +} + +console.log(`wrote ${fileURLToPath(output)}`); diff --git a/survey-psychometric-validity-assistant/package.json b/survey-psychometric-validity-assistant/package.json new file mode 100644 index 00000000..4d089e2d --- /dev/null +++ b/survey-psychometric-validity-assistant/package.json @@ -0,0 +1,13 @@ +{ + "name": "survey-psychometric-validity-assistant", + "version": "1.0.0", + "description": "Deterministic survey psychometric validity assistant for SCIBASE issue #16.", + "type": "module", + "scripts": { + "check": "node --check src/guard.js && node --check src/samplePackets.js && node --check test.js && node --check demo.js && node --check make-demo-video.js", + "test": "node test.js", + "demo": "node demo.js", + "demo:video": "node make-demo-video.js" + }, + "license": "MIT" +} diff --git a/survey-psychometric-validity-assistant/reports/clean-result.json b/survey-psychometric-validity-assistant/reports/clean-result.json new file mode 100644 index 00000000..8baa29d1 --- /dev/null +++ b/survey-psychometric-validity-assistant/reports/clean-result.json @@ -0,0 +1,13 @@ +{ + "packetId": "survey-clean-001", + "title": "Validated Remote Collaboration Burnout Scale", + "decision": "RELEASE", + "severityScore": 0, + "findingCount": 0, + "findings": [], + "releaseGate": { + "canReleaseAiReview": true, + "requiresPsychometricReviewer": false, + "rationale": "Psychometric evidence is sufficient for AI peer-review release." + } +} \ No newline at end of file diff --git a/survey-psychometric-validity-assistant/reports/demo.mp4 b/survey-psychometric-validity-assistant/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..27378144f0bfc4fd1b4050c924c7378428dcfbcf GIT binary patch literal 29032 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 >PLQv}pgHD%hbun)|-&8u&kY7(J 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 vs!M!8M#rBKYx4e#z66yUv=wf

+ + + Survey Psychometric Validity Assistant + Packet: survey-risky-017 + Decision: HOLD + Severity score: 39 | Findings: 17 + Top gates: reverse coding, reliability, CFA sample size, factor cross-loads + Action: hold AI peer-review release for psychometric review + + + \ No newline at end of file diff --git a/survey-psychometric-validity-assistant/src/guard.js b/survey-psychometric-validity-assistant/src/guard.js new file mode 100644 index 00000000..f178bb96 --- /dev/null +++ b/survey-psychometric-validity-assistant/src/guard.js @@ -0,0 +1,177 @@ +const DEFAULTS = { + minimumAlpha: 0.7, + minimumLoading: 0.5, + maximumCrossLoading: 0.32, + minimumLikertAnchors: 5, + cfaCasesPerParameter: 10 +}; + +function addFinding(findings, severity, code, message, evidence) { + findings.push({ severity, code, message, evidence }); +} + +function normalize(value) { + return String(value ?? "").trim().toLowerCase(); +} + +function evaluateScale(scale, findings, options) { + if (typeof scale.cronbachAlpha !== "number") { + addFinding( + findings, + "high", + "missing_internal_consistency", + `${scale.name} does not report Cronbach alpha or equivalent reliability evidence.`, + { scale: scale.name } + ); + } else if (scale.cronbachAlpha < options.minimumAlpha) { + addFinding( + findings, + "high", + "low_internal_consistency", + `${scale.name} reports alpha ${scale.cronbachAlpha}, below ${options.minimumAlpha}.`, + { scale: scale.name, alpha: scale.cronbachAlpha } + ); + } + + for (const item of scale.items ?? []) { + if (item.reverseCoded && !item.reverseScored) { + addFinding( + findings, + "critical", + "reverse_coding_drift", + `${item.id} is reverse coded but not reverse scored before scale scoring.`, + { scale: scale.name, itemId: item.id } + ); + } + + if (typeof item.loading === "number" && item.loading < options.minimumLoading) { + addFinding( + findings, + "medium", + "weak_primary_loading", + `${item.id} has primary loading ${item.loading}, below ${options.minimumLoading}.`, + { scale: scale.name, itemId: item.id, loading: item.loading } + ); + } + } + + for (const cross of scale.crossLoadings ?? []) { + if (cross.secondaryLoading >= options.maximumCrossLoading) { + addFinding( + findings, + "high", + "factor_cross_loading", + `${cross.itemId} has secondary loading ${cross.secondaryLoading}, above ${options.maximumCrossLoading}.`, + { scale: scale.name, itemId: cross.itemId, secondaryLoading: cross.secondaryLoading } + ); + } + } +} + +export function evaluateSurveyPacket(packet, customOptions = {}) { + const options = { ...DEFAULTS, ...customOptions }; + const findings = []; + + const anchors = packet.likertAnchors ?? []; + if (anchors.length < options.minimumLikertAnchors) { + addFinding( + findings, + "medium", + "thin_likert_anchor_set", + `Survey reports ${anchors.length} Likert anchors; expected at least ${options.minimumLikertAnchors}.`, + { anchors } + ); + } + + const minimumCfaSample = (packet.plannedCfaParameters ?? 0) * options.cfaCasesPerParameter; + if (packet.plannedCfaParameters && packet.sampleSize < minimumCfaSample) { + addFinding( + findings, + "high", + "cfa_sample_size_shortfall", + `CFA sample size ${packet.sampleSize} is below ${minimumCfaSample} for ${packet.plannedCfaParameters} parameters.`, + { + sampleSize: packet.sampleSize, + plannedCfaParameters: packet.plannedCfaParameters, + minimumCfaSample + } + ); + } + + for (const scale of packet.scales ?? []) { + evaluateScale(scale, findings, options); + } + + const scaleConstructs = new Set((packet.scales ?? []).map((scale) => normalize(scale.construct))); + for (const claim of packet.constructClaims ?? []) { + if (!scaleConstructs.has(normalize(claim))) { + addFinding( + findings, + "high", + "construct_claim_mismatch", + `Manuscript claims ${claim}, but no submitted scale validates that construct directly.`, + { claim, availableConstructs: [...scaleConstructs] } + ); + } + } + + const severityScore = findings.reduce((sum, finding) => { + if (finding.severity === "critical") return sum + 5; + if (finding.severity === "high") return sum + 3; + if (finding.severity === "medium") return sum + 1; + return sum; + }, 0); + + const decision = findings.some((finding) => finding.severity === "critical") || severityScore >= 9 + ? "HOLD" + : severityScore >= 3 + ? "REVIEW" + : "RELEASE"; + + return { + packetId: packet.id, + title: packet.title, + decision, + severityScore, + findingCount: findings.length, + findings, + releaseGate: { + canReleaseAiReview: decision === "RELEASE", + requiresPsychometricReviewer: decision !== "RELEASE", + rationale: decision === "RELEASE" + ? "Psychometric evidence is sufficient for AI peer-review release." + : "Psychometric validity issues must be reviewed before AI output is trusted." + } + }; +} + +export function renderMarkdownReport(result) { + const lines = [ + `# Survey Psychometric Validity Report`, + ``, + `Packet: ${result.packetId}`, + `Decision: ${result.decision}`, + `Severity score: ${result.severityScore}`, + `Findings: ${result.findingCount}`, + ``, + `## Findings` + ]; + + if (result.findings.length === 0) { + lines.push(`- No blocking psychometric validity findings.`); + } else { + for (const finding of result.findings) { + lines.push(`- [${finding.severity}] ${finding.code}: ${finding.message}`); + } + } + + lines.push( + ``, + `## Release gate`, + `- Can release AI review: ${result.releaseGate.canReleaseAiReview}`, + `- Requires psychometric reviewer: ${result.releaseGate.requiresPsychometricReviewer}`, + `- Rationale: ${result.releaseGate.rationale}` + ); + + return `${lines.join("\n")}\n`; +} diff --git a/survey-psychometric-validity-assistant/src/samplePackets.js b/survey-psychometric-validity-assistant/src/samplePackets.js new file mode 100644 index 00000000..76253d5a --- /dev/null +++ b/survey-psychometric-validity-assistant/src/samplePackets.js @@ -0,0 +1,75 @@ +export const cleanPacket = { + id: "survey-clean-001", + title: "Validated Remote Collaboration Burnout Scale", + decisionContext: "pre-release AI peer review", + constructClaims: ["burnout", "remote collaboration fatigue"], + sampleSize: 420, + plannedCfaParameters: 32, + likertAnchors: ["Strongly disagree", "Disagree", "Neutral", "Agree", "Strongly agree"], + scales: [ + { + name: "Remote Collaboration Fatigue", + construct: "remote collaboration fatigue", + cronbachAlpha: 0.86, + items: [ + { id: "rcf_1", loading: 0.74 }, + { id: "rcf_2", loading: 0.69 }, + { id: "rcf_3", loading: 0.71 }, + { id: "rcf_4", loading: 0.66, reverseCoded: true, reverseScored: true } + ], + crossLoadings: [ + { itemId: "rcf_2", secondaryLoading: 0.18 } + ] + }, + { + name: "Burnout Short Form", + construct: "burnout", + cronbachAlpha: 0.82, + items: [ + { id: "bsf_1", loading: 0.72 }, + { id: "bsf_2", loading: 0.77 }, + { id: "bsf_3", loading: 0.68 } + ], + crossLoadings: [] + } + ] +}; + +export const riskyPacket = { + id: "survey-risky-017", + title: "AI Workload Anxiety and Productivity Claims", + decisionContext: "pre-release AI peer review", + constructClaims: ["anxiety", "productivity", "clinical burnout"], + sampleSize: 96, + plannedCfaParameters: 28, + likertAnchors: ["Never", "Sometimes", "Often"], + scales: [ + { + name: "Workload Anxiety Draft Scale", + construct: "workload anxiety", + cronbachAlpha: 0.58, + items: [ + { id: "wa_1", loading: 0.42 }, + { id: "wa_2", loading: 0.39, reverseCoded: true, reverseScored: false }, + { id: "wa_3", loading: 0.44 }, + { id: "wa_4", loading: 0.31 } + ], + crossLoadings: [ + { itemId: "wa_1", secondaryLoading: 0.41 }, + { itemId: "wa_3", secondaryLoading: 0.37 } + ] + }, + { + name: "Productivity Perception Index", + construct: "productivity perception", + cronbachAlpha: null, + items: [ + { id: "ppi_1", loading: 0.47 }, + { id: "ppi_2", loading: 0.36 } + ], + crossLoadings: [ + { itemId: "ppi_2", secondaryLoading: 0.34 } + ] + } + ] +}; diff --git a/survey-psychometric-validity-assistant/test.js b/survey-psychometric-validity-assistant/test.js new file mode 100644 index 00000000..819ed1ea --- /dev/null +++ b/survey-psychometric-validity-assistant/test.js @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import { evaluateSurveyPacket, renderMarkdownReport } from "./src/guard.js"; +import { cleanPacket, riskyPacket } from "./src/samplePackets.js"; + +const clean = evaluateSurveyPacket(cleanPacket); +assert.equal(clean.decision, "RELEASE"); +assert.equal(clean.findingCount, 0); +assert.equal(clean.releaseGate.canReleaseAiReview, true); + +const risky = evaluateSurveyPacket(riskyPacket); +assert.equal(risky.decision, "HOLD"); +assert.equal(risky.releaseGate.requiresPsychometricReviewer, true); +assert.ok(risky.findings.some((finding) => finding.code === "reverse_coding_drift")); +assert.ok(risky.findings.some((finding) => finding.code === "low_internal_consistency")); +assert.ok(risky.findings.some((finding) => finding.code === "missing_internal_consistency")); +assert.ok(risky.findings.some((finding) => finding.code === "factor_cross_loading")); +assert.ok(risky.findings.some((finding) => finding.code === "cfa_sample_size_shortfall")); +assert.ok(risky.findings.some((finding) => finding.code === "construct_claim_mismatch")); + +const report = renderMarkdownReport(risky); +assert.match(report, /Survey Psychometric Validity Report/); +assert.match(report, /Decision: HOLD/); +assert.match(report, /reverse_coding_drift/); + +console.log("survey psychometric validity assistant tests passed");