From 25cac601792611ce54a045edf7bf7f9fd81a3273 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:47:45 -0400 Subject: [PATCH 001/100] docs: start protected Case Study delivery --- docs/classroom-workstream.md | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/classroom-workstream.md b/docs/classroom-workstream.md index a3052e2..39e70dd 100644 --- a/docs/classroom-workstream.md +++ b/docs/classroom-workstream.md @@ -6,7 +6,7 @@ This is the live restart document for the Classroom Experience program (#288). ## Current phase -#294 coaching feedback is implemented in draft PR #306 on `feature/classroom-coaching-feedback`. Runtime and focused regression validation are green; final documented-head gates and merge are the remaining work. +#294 is merged and production READY. #295 Protected Case Study delivery is active on `feature/classroom-protected-cases`. ## Program issues @@ -18,8 +18,8 @@ This is the live restart document for the Classroom Experience program (#288). | Class domain/API | #291 | Complete | PR #300 / `feature/classroom-domain-api` | Combined #290+#291 head: 183 tests, 182 pass, 0 fail, 1 skip; CI/CodeQL/dependency/template guard green | | Student join/resume | #292 | Complete | PR #302 / `feature/classroom-student-experience` | 194 tests: 193 pass, 0 fail, 1 skip; CI/CodeQL/dependency/template guard green | | Instructor observer | #293 | Complete | PR #305 merged | 208 tests: 207 pass, 0 fail, 1 skip; repository gates green | -| Coaching | #294 | Implementation complete | PR #306 / `feature/classroom-coaching-feedback` | Validated implementation head: 224 tests, 223 pass, 0 fail, 1 skip; final docs/head checks pending merge | -| Protected cases | #295 | Not started | — | Requires #290 + #291 | +| Coaching | #294 | Complete | PR #306 merged | 224 tests: 223 pass, 0 fail, 1 skip; all repository gates green; production deployment READY | +| Protected cases | #295 | In progress | `feature/classroom-protected-cases` | Public manifest leak confirmed; split contract being implemented | | Browser E2E/CI | #296 | Not started | — | Uses #279 infrastructure | | Browser test foundation | #279 | Existing open issue | — | Shared Playwright/accessibility foundation | @@ -56,15 +56,15 @@ See `docs/classroom-architecture.md` for the full contract. ## Active work -#297–#305 are merged to `main`. #294 is active on `feature/classroom-coaching-feedback`. +#297–#306 are merged to `main`. #295 is active on `feature/classroom-protected-cases`. ## Last completed action -Completed #294 runtime/API/registry/UI implementation and focused regression coverage on PR #306. The validated implementation head completed 224 tests (223 pass, 0 fail, 1 intentional skip) with the canonical quality gate green; Dependency Review and Template Manifest Guard are green on the current documented head. +Merged PR #306 / completed #294 as `4d0ab953fbdde32dde3441fbe2e415f0bf886634`. Production Vercel deployment reached READY and a post-deploy runtime-error scan found no errors. ## Next recommended action -Inspect PR #306 first. If it is still open, confirm the final documented head has CI, CodeQL, Dependency Review, and Template Manifest Guard green; review the complete diff; then mark ready, squash-merge, close #294, and update #288. If #306 is already merged, start #295 Protected Case Study delivery from current `main`. Do not broaden #294 to ephemeral Possible Cause cards. +Continue #295 on `feature/classroom-protected-cases`. The confirmed leak is the generated `src/templates.manifest.js`: `scripts/build-templates-manifest.mjs` currently compiles Standard Templates and Case Studies, including full Case Study state, into one public static module. Split the public generated manifest to Standard Templates only; keep validating all authored JSON; then add server-authorized Case Study catalog/payload delivery for Student and Instructor contexts. Do not treat the rotating Case Study password as authentication. ## Completed #300 implementation @@ -120,7 +120,7 @@ Inspect PR #306 first. If it is still open, confirm the final documented head ha - Student → non-Student role changes now restore the Student pre-class local recovery without clearing Student resume. -## Active #294 implementation +## Completed #294 implementation - Coaching persistence is separate from `collaboration_workspaces.snapshot_json` and its revision. - Table: `classroom_coaching_feedback`, keyed by class/workspace/target. @@ -146,6 +146,21 @@ Inspect PR #306 first. If it is still open, confirm the final documented head ha - GitHub Advanced Security flagged the original coaching target-ID regex for potential exponential backtracking; it was replaced with a linear-time dot-segment validator in `63bc782d885eb9d57bb74c09389db4c9955e52d7` with grammar regressions in `1e25af79c97a4c60f9a091bd41d9c5b8074c188e`. - Exact next action: confirm the post-review-fix head is green, resolve the CodeQL review thread, update PR/issue final SHA, then merge #306. +## Active #295 implementation + +- Branch: `feature/classroom-protected-cases`. +- Issue: #295. +- Confirmed current leak: `scripts/build-templates-manifest.mjs` reads every JSON file under `templates/` and writes metadata + full state into public `src/templates.manifest.js`. +- `src/templates.js` statically imports that manifest and assumes every listed resource has an in-bundle payload; role filtering therefore controls visibility only, not confidentiality. +- Target architecture: + - authoring JSON remains in-repo and all resources stay build-validated; + - public generated manifest contains Standard Templates only; + - protected Case Study catalog/payload is loaded server-side after Classroom authorization; + - existing mode projection/pedagogical unlock behavior is preserved after authorization; + - static-bundle regression tests prove protected Case Study content is absent from public generated assets. +- Delivery-safe parts: generator/runtime split → server authorization/API → Student/Instructor drawer integration → bundle guard/docs/final gates. +- Exact next action: inspect drawer + authoring/build contracts, then implement the generator/public-runtime split as the first durable checkpoint. + ## Known risks / watch items - Existing Case Study payloads are client-bundled; role-based hiding is not a confidentiality control. From c373cce833ce74a29c768e11939c563832edb3d4 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:51:30 -0400 Subject: [PATCH 002/100] feat: split public and protected template manifests --- scripts/build-templates-manifest.mjs | 40 +++++++++++++++++++++------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/scripts/build-templates-manifest.mjs b/scripts/build-templates-manifest.mjs index da7b713..76668de 100755 --- a/scripts/build-templates-manifest.mjs +++ b/scripts/build-templates-manifest.mjs @@ -10,7 +10,8 @@ import { TEMPLATE_MODE_IDS } from '../src/templateModes.js'; import { TEMPLATE_KINDS } from '../src/templateKinds.js'; const templatesDir = new URL('../templates/', import.meta.url); -const manifestPath = new URL('../src/templates.manifest.js', import.meta.url); +const publicManifestPath = new URL('../src/templates.manifest.js', import.meta.url); +const protectedManifestPath = new URL('../api/protected-case-studies.manifest.js', import.meta.url); const MODE_VALUES = Object.freeze(Object.values(TEMPLATE_MODE_IDS)); const KIND_VALUES = Object.freeze(Object.values(TEMPLATE_KINDS)); @@ -191,10 +192,10 @@ async function readTemplateFile(fileName) { }; } -function createManifestSource(manifest) { - const banner = `/**\n * AUTO-GENERATED FILE. DO NOT EDIT DIRECTLY.\n * Generated by scripts/build-templates-manifest.mjs\n */`; +function createManifestSource(manifest, { exportName, boundary }) { + const banner = `/**\n * AUTO-GENERATED FILE. DO NOT EDIT DIRECTLY.\n * Generated by scripts/build-templates-manifest.mjs\n * ${boundary}\n */`; const serialized = JSON.stringify(manifest, null, 2); - return `${banner}\n\nfunction deepFreeze(value) {\n if (Array.isArray(value)) {\n value.forEach(deepFreeze);\n } else if (value && typeof value === 'object') {\n Object.values(value).forEach(deepFreeze);\n }\n return Object.freeze(value);\n}\n\nconst manifest = ${serialized};\n\nexport const TEMPLATE_MANIFEST = deepFreeze(manifest);\n`; + return `${banner}\n\nfunction deepFreeze(value) {\n if (Array.isArray(value)) {\n value.forEach(deepFreeze);\n } else if (value && typeof value === 'object') {\n Object.values(value).forEach(deepFreeze);\n }\n return Object.freeze(value);\n}\n\nconst manifest = ${serialized};\n\nexport const ${exportName} = deepFreeze(manifest);\n`; } async function main() { @@ -211,14 +212,33 @@ async function main() { if (entries.length === 0) { throw new Error('No JSON templates found in templates/'); } - const manifest = []; + const templates = []; for (const fileName of entries) { const template = await readTemplateFile(fileName); - manifest.push(template); - } - const output = createManifestSource(manifest); - await fs.writeFile(manifestPath, output, 'utf8'); - console.log(`Wrote ${manifest.length} template(s) to ${fileURLToPath(manifestPath)}.`); + templates.push(template); + } + + const publicTemplates = templates.filter(template => template.templateKind === TEMPLATE_KINDS.STANDARD); + const protectedCaseStudies = templates.filter(template => template.templateKind === TEMPLATE_KINDS.CASE_STUDY); + + const publicOutput = createManifestSource(publicTemplates, { + exportName: 'TEMPLATE_MANIFEST', + boundary: 'PUBLIC CLIENT MANIFEST: Standard Templates only. Protected Case Studies must never be added here.' + }); + const protectedOutput = createManifestSource(protectedCaseStudies, { + exportName: 'PROTECTED_CASE_STUDY_MANIFEST', + boundary: 'SERVER-ONLY MANIFEST: protected Case Study metadata and payloads. Never import from browser runtime modules.' + }); + + await Promise.all([ + fs.writeFile(publicManifestPath, publicOutput, 'utf8'), + fs.writeFile(protectedManifestPath, protectedOutput, 'utf8') + ]); + + console.log( + `Wrote ${publicTemplates.length} public template(s) to ${fileURLToPath(publicManifestPath)} ` + + `and ${protectedCaseStudies.length} protected Case Study record(s) to ${fileURLToPath(protectedManifestPath)}.` + ); } main().catch(error => { From 64ae718768a39d492691e42676c92d16a9c08bb3 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:51:37 -0400 Subject: [PATCH 003/100] build: emit public Standard Template manifest only --- src/templates.manifest.js | 2796 +++---------------------------------- 1 file changed, 188 insertions(+), 2608 deletions(-) diff --git a/src/templates.manifest.js b/src/templates.manifest.js index bcbee92..76607fe 100644 --- a/src/templates.manifest.js +++ b/src/templates.manifest.js @@ -1,6 +1,7 @@ /** * AUTO-GENERATED FILE. DO NOT EDIT DIRECTLY. * Generated by scripts/build-templates-manifest.mjs + * PUBLIC CLIENT MANIFEST: Standard Templates only. Protected Case Studies must never be added here. */ function deepFreeze(value) { @@ -14,2725 +15,304 @@ function deepFreeze(value) { const manifest = [ { - "id": "microcomputer-cabinets", - "name": "Microcomputer Cabinets", - "description": "Final Inspection Reports that cabinets are being rejected periodically", + "id": "checkout-latency", + "name": "Checkout Latency Spike", + "description": "EU checkout slowdown triggered by a CDN header rewrite.", + "templateKind": "standard", "supportedModes": [ "intake", "is-is-not", "dc", "full" ], - "templateKind": "case-study", "state": { "meta": { "version": 1, - "savedAt": "2025-11-18T18:57:08.496Z" + "savedAt": null }, "pre": { - "oneLine": "Cabinets are being rejected", - "proof": "Final Inspection Reports that cabinets are being rejected periodically", - "objectPrefill": "1025 Microcomputer Cabinets", - "healthy": "Flat, even coat of paint on the outside surface", - "now": "Paint Gaps" + "oneLine": "EU checkout requests timing out after CDN rule change.", + "proof": "APM shows p95 latency at 18s with widespread gateway retries.", + "objectPrefill": "Checkout service / EU tenants", + "healthy": "Authorizations complete under 3s with <1% retry.", + "now": "Cards time out, fallback queue saturates, and conversions drop 35%." }, "impact": { - "now": "Total reject rate is hovering at 6% \n\nwe are manually re-working cabinets with paint gaps", - "future": "production delays and out-of-stock notices may have to go out due to shortage of 1025 Cabinets", - "time": "Next Week sales are expected to be increasing along with production rate" + "now": "Merchants losing €1.2M/hr, customers double-purchasing, and support volume spiking.", + "future": "Merchants will disable promos and regulators expect notification if >60m.", + "time": "Started 2024-03-08 07:38Z as CDN header rewrite pushed globally." }, "ops": { - "bridgeOpenedUtc": "2025-11-18T09:04:02.398Z", - "icName": "", - "bcName": "", - "semOpsName": "", - "severity": "", - "detectMonitoring": false, - "detectUserReport": true, - "detectAutomation": false, + "bridgeOpenedUtc": "2024-03-08 07:42Z", + "icName": "Jonas Reed", + "bcName": "Eva Mendez", + "semOpsName": "Luis Martin", + "severity": "SEV-1", + "detectMonitoring": true, + "detectUserReport": false, + "detectAutomation": true, "detectOther": false, "evScreenshot": false, - "evLogs": false, - "evMetrics": false, - "evRepro": false, - "evOther": false, + "evLogs": true, + "evMetrics": true, + "evRepro": true, + "evOther": true, "containStatus": "stabilized", - "containDesc": "Manually reworking current cabinets", - "commCadence": "", - "commLog": [], - "commNextDueIso": "", - "commNextUpdateTime": "", + "containDesc": "Rolled back EU POPs to last-known-good CDN config and rate-limited loyalty header.", + "commCadence": "15", + "commLog": [ + { + "type": "internal", + "ts": "2024-03-08T07:45:00Z", + "message": "War room formed, diffing CDN configs." + }, + { + "type": "external", + "ts": "2024-03-08T08:00:00Z", + "message": "Status page updated with EU latency advisory." + }, + { + "type": "internal", + "ts": "2024-03-08T08:10:00Z", + "message": "Containment applied via POP rollback; monitoring drop." + } + ], + "commNextDueIso": "2024-03-08T08:15:00Z", + "commNextUpdateTime": "08:15", "tableFocusMode": "comprehensive" }, "table": [ { - "band": "WHAT — Define the problem precisely (Object & Deviation)." - }, - { - "q": "WHAT — Specific Object/Thing is having the “Visual Defects”", - "is": "1025 Microcomputer Cabinets", - "no": "1035, 1045 Cabinets", - "di": "1025s are cleaned in Line 1 Wash Tanks\n\n1025s are assembled in Line 1 Final Assembly\n\n1025s have a higher production rate than the 1035 and 1045 combined", - "ch": "More Cleaned on Line 1 (Weekly over last 3 weeks)\n\nWash tanks Flushed Every 30 Minutes\n\nMore assembled on Line 1 (Weekly over last 3 weeks) \n\nNew Employees (5 Weeks Ago)", - "questionId": "what-object" - }, - { - "q": "WHAT — Specific Deviation does the “1025 Microcomputer Cabinets” have?", - "is": "Visual Defects\n\nPaint Gaps", - "no": "Functional, Electronic Defects\n\nPaint Peeling, Discolouring, Running, Fading", - "di": "", - "ch": "", - "questionId": "what-deviation" + "band": "WHAT", + "note": "Define the problem precisely (Object & Deviation)." }, { - "band": "WHERE — Locate the problem (geography/topology and on the object)." + "questionId": "what-object", + "q": "WHAT — Specific Object/Thing is having the {DEVIATION}", + "is": "Checkout API for EU tenants hitting FRA/AMS POPs.", + "no": "US checkout tenants plus non-card wallets remain normal.", + "di": "Only card payments routed through CDN header rewrite show latency.", + "ch": "Impacted traffic contains new loyalty header added this morning." }, { - "q": "WHERE — is the “1025 Microcomputer Cabinets” geographically/topology when the “Visual Defects” occurs?", - "is": "Reported at Final Inspection", - "no": "Reported at Final Assembly", - "di": "", - "ch": "", - "questionId": "where-location" + "questionId": "what-deviation", + "q": "WHAT — Specific Deviation does the {OBJECT} have?", + "is": "Auth calls take 15-20s and time out, forcing retries.", + "no": "Async webhook callbacks and idempotency layer stay nominal.", + "di": "Latency spikes only on POPs where header rewrite is enabled.", + "ch": "Deviation began minutes after config promotion." }, { - "q": "WHERE — On the “1025 Microcomputer Cabinets” is the “Visual Defects” observed?", - "is": "All Panels\nOutside Panels\nRandomly", - "no": "Some Panels\n(NMD - Inside?)\nIn Specific Spots or Pattern", - "di": "", - "ch": "", - "questionId": "where-on-object" + "band": "WHERE", + "note": "Locate the problem (geography/topology and on the object)." }, { - "band": "WHEN — Timing and Description" + "questionId": "where-location", + "q": "WHERE — is the {OBJECT} geographically/topology when the {DEVIATION} occurs?", + "is": "FRA and AMS POPs fronting EU tenants.", + "no": "DUB and LHR POPs on legacy routing stay healthy.", + "di": "Only POPs that honor loyalty header override misbehave.", + "ch": "Same POPs received header rewrite at 07:38Z." }, { - "q": "WHEN — Was the “Visual Defects” First observed for “1025 Microcomputer Cabinets”", - "is": "2 Weeks Ago", - "no": "Before 2 Weeks Ago", - "di": "Production Rate", - "ch": "Production Increased (Weekly over last 3 weeks)", - "questionId": "when-first-observed" + "questionId": "where-on-object", + "q": "WHERE — On the {OBJECT} is the {DEVIATION} observed?", + "is": "Ingress CDN tier before requests reach checkout pods.", + "no": "Checkout pods inside EU clusters show normal CPU/mem.", + "di": "Only TLS handshake plus origin selection step regresses.", + "ch": "Header rewrite manipulates origin cache key for loyalty path." }, { - "q": "WHEN — Since the first occurrence has “Visual Defects” been logged? What Pattern?", - "is": "NMD - Time\n\"Periodic\"\n", - "no": "NMD - Time\nSporadic, Continuous", - "di": "", - "ch": "", - "questionId": "when-pattern" + "band": "WHEN", + "note": "Timing and Description" }, { - "q": "WHEN — Describe using words When the “Visual Defects” was first seen", - "is": "During Visual Inspection", - "no": "During Final Assembly, Electronic, or Operational Inspection", - "di": "", - "ch": "", - "questionId": "when-description" + "questionId": "when-pattern", + "q": "WHEN — Since the first occurrence has {DEVIATION} been logged? What Pattern?", + "is": "Continuous since 07:38Z; no relief between retries.", + "no": "Earlier in the day latency followed normal diurnal pattern.", + "di": "Traffic during loyalty promo hours is hardest hit.", + "ch": "Pattern maps to config roll at 07:38Z then steady state." }, { - "band": "EXTENT — How big is it? Magnitude, count, scope, trend." + "questionId": "when-description", + "q": "WHEN — Describe using words When the {DEVIATION} was first seen", + "is": "Appeared immediately during CDN deploy validation (before user escalation).", + "no": "No deviations overnight despite heavy load tests.", + "di": "Only requests including loyalty header see rewrite and delay.", + "ch": "New config rewrites Accept headers and bypasses warm cache." }, { - "q": "EXTENT — What is the population or size of “1025 Microcomputer Cabinets” affected?", - "is": "Currently Over 5.85%\n\nIncreasing", - "no": "0.16% - 5.84% and\n5.86% - 100%", - "di": "", - "ch": "", - "questionId": "extent-population" + "band": "EXTENT", + "note": "How big is it? Magnitude, count, scope, trend." }, { - "q": "EXTENT — What is the size of a single “Visual Defects”?", - "is": "", - "no": "", - "di": "", - "ch": "", - "questionId": "extent-size" + "questionId": "extent-population", + "q": "EXTENT — What is the population or size of {OBJECT} affected?", + "is": "All EU card traffic (~2.1k rps) routed through FRA/AMS POPs.", + "no": "Merchants pinned to DUB POP unaffected.", + "di": "Premium merchants on loyalty pilot are 2x more impacted.", + "ch": "Population exactly matches tenants moved to new header rule." }, { - "q": "EXTENT — How many “Visual Defects” are occuring on each “1025 Microcomputer Cabinets”?", - "is": "Up to 11 Gaps per cabinet\n\nIncreasing", - "no": "Any other range or single value", - "di": "", - "ch": "", - "questionId": "extent-count" + "questionId": "extent-size", + "q": "EXTENT — What is the size of a single {DEVIATION}?", + "is": "Single checkout attempts incur 18s additional latency and multiple retries.", + "no": "Baseline deviation remained <500ms before change.", + "di": "Latency delta grows with each loyalty header mutation.", + "ch": "Size stabilized once rollback applied." } ], "causes": [ { - "id": "cause-2h25uu-mi4xnini", - "suspect": "Employees", - "accusation": "Wearing Unapproved Hand Cream", - "impact": "Leaving a Film on the Cabinet that prevents paint from adhering", - "summaryText": "We suspect Employees because Wearing Unapproved Hand Cream. This could lead to Leaving a Film on the Cabinet that prevents paint from adhering.", - "confidence": "", - "evidence": "", - "findings": {}, - "editing": false, - "testingOpen": false - }, - { - "id": "cause-2u9pmq-mi4xo7ef", - "suspect": "Wash Tanks", - "accusation": "Contaminated", - "impact": "Leaving Dirt on Cabinets", - "summaryText": "We suspect Wash Tanks because Contaminated. This could lead to Leaving Dirt on Cabinets.", - "confidence": "", - "evidence": "", - "findings": {}, + "id": "cause-cdn-rule", + "suspect": "CDN header rewrite broke cache keying for loyalty traffic.", + "accusation": "Rewrite adds Accept header fragment forcing origin revalidation on every request.", + "impact": "POP queues build, TLS renegotiates, and checkout pods receive bursts.", + "summaryText": "Misconfigured CDN rule for loyalty header invalidates cache and adds 15s delay.", + "confidence": "high", + "evidence": "Disabling rewrite on FRA instantly returns latency to baseline.", + "findings": { + "what-object": { + "mode": "yes", + "note": "Only checkout API traffic on loyalty header is impacted." + }, + "where-location": { + "mode": "yes", + "note": "Matches FRA/AMS POPs that received rewrite." + }, + "when-pattern": { + "mode": "yes", + "note": "Timeline aligns with header deployment." + } + }, "editing": false, "testingOpen": false }, { - "id": "cause-wjmudz-mi4xohef", - "suspect": "Grounding Hooks", - "accusation": "Not Properly Grounded", - "impact": "Preventing Paint Adhesion", - "summaryText": "We suspect Grounding Hooks because Not Properly Grounded. This could lead to Preventing Paint Adhesion.", - "confidence": "", - "evidence": "", - "findings": {}, + "id": "cause-db-hot", + "suspect": "Multi-region DB hot partition.", + "accusation": "EU tenant data co-located causing partition hotspot and slow auth.", + "impact": "Would explain retries if DB were throttling connections.", + "summaryText": "Potential DB hot partition causing cascading latency.", + "confidence": "low", + "evidence": "DB metrics remain green; theory kept for completeness.", + "findings": { + "what-deviation": { + "mode": "fail", + "note": "DB tier shows no latency delta." + }, + "extent-size": { + "mode": "fail", + "note": "Does not explain 18s TLS stall before origin." + } + }, "editing": false, "testingOpen": false } ], - "likelyCauseId": "cause-2h25uu-mi4xnini", + "likelyCauseId": "cause-cdn-rule", "steps": { "items": [ - { - "id": "1", - "label": "Pre-analysis completed", - "checked": false - }, { "id": "2", "label": "Incident Commander assigned", - "checked": false - }, - { - "id": "4", - "label": "Problem statement created", - "checked": false - }, - { - "id": "5", - "label": "Bridge options considered", - "checked": false - }, - { - "id": "6", - "label": "Bridges opened and responders invited", - "checked": false - }, - { - "id": "7", - "label": "Bridge etiquette, roles, and guidelines outlined", - "checked": false + "checked": true }, { "id": "8", "label": "Current actions documented (Who/What/When)", - "checked": false - }, - { - "id": "9", - "label": "Quick spec answers captured", - "checked": false - }, - { - "id": "10", - "label": "Last changes, monitoring, and dependencies investigated", - "checked": false - }, - { - "id": "11", - "label": "Attendees optimized", - "checked": false - }, - { - "id": "12", - "label": "Possible causes developed", - "checked": false + "checked": true }, { "id": "13", "label": "Testing actions documented (Who/What/When)", - "checked": false - }, - { - "id": "14", - "label": "Micro-experiments/tests conducted", - "checked": false - }, - { - "id": "15", - "label": "Containment options identified", - "checked": false - }, - { - "id": "16", - "label": "Comms written, reviewed, and sent", - "checked": false - }, - { - "id": "17", - "label": "Attendees optimized for next action", - "checked": false - }, - { - "id": "18", - "label": "Possible causes evaluated and distinctions identified", - "checked": false - }, - { - "id": "19", - "label": "Most probable cause identified", - "checked": false + "checked": true }, { "id": "20", "label": "Restoration/rollback/workaround selected", - "checked": false - }, - { - "id": "21", - "label": "Attendees optimized for decision", - "checked": false - }, - { - "id": "22", - "label": "Verification and risk plan created", - "checked": false - }, - { - "id": "23", - "label": "Restoration actions documented (Who/What/When)", - "checked": false + "checked": true }, { "id": "24", "label": "Service validated internally and externally", "checked": false - }, - { - "id": "25", - "label": "Restoration comms sent", - "checked": false - }, - { - "id": "26", - "label": "Handover template prepared (PIR/problem)", - "checked": false - }, - { - "id": "27", - "label": "Downstream issues from fix assessed", - "checked": false - }, - { - "id": "28", - "label": "Participants released and bridge closed", - "checked": false } ], "drawerOpen": false }, "actions": { - "analysisId": "analysis-ea2a2d69-11c9-4bf7-8c6f-301d64b430cf", + "analysisId": "template-checkout", "items": [ { - "id": "51870a36-3501-4af0-aa28-c5e0eca4b9b6", - "analysisId": "analysis-ea2a2d69-11c9-4bf7-8c6f-301d64b430cf", - "createdAt": "2025-11-18T18:55:55.571Z", - "createdBy": "local", - "summary": "Start Data logging to understand the period of the issue", - "detail": "", + "id": "action-cdn-freeze", + "analysisId": "template-checkout", + "createdAt": "2024-03-08T07:50:00Z", + "createdBy": "TemplateBot", + "summary": "Freeze CDN config and audit loyalty rewrite.", + "detail": "Diff last-known-good config vs current, revert EU POPs, and prepare lock file.", "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" + "name": "CDN Engineering", + "category": "TECHNOLOGY_PLATFORM", + "subOwner": "TECHNOLOGY_PLATFORM__CLOUD", + "notes": "On-call pair running diff script.", + "lastAssignedBy": "TemplateBot", + "lastAssignedAt": "2024-03-08T07:52:00Z", + "source": "Template" }, - "role": "", - "status": "Planned", + "role": "Networking", + "status": "In-Progress", "priority": "High", - "dueAt": "", - "startedAt": "", + "dueAt": "2024-03-08T08:20:00Z", + "startedAt": "2024-03-08T07:52:00Z", "completedAt": "", "dependencies": [], - "risk": "None", + "risk": "High", "changeControl": { - "required": false + "required": true, + "rollbackPlan": "CDN config checkpoint restore." }, "verification": { - "required": false + "required": true, + "result": "" }, "links": {}, - "notes": "", + "notes": "Need approval from BC before global push.", "auditTrail": [] }, { - "id": "3ab35131-55ca-4661-b847-45a034649c18", - "analysisId": "analysis-ea2a2d69-11c9-4bf7-8c6f-301d64b430cf", - "createdAt": "2025-11-18T18:56:18.157Z", - "createdBy": "local", - "summary": "Purchase Gloves and Begin Mandate for All Employees to be Gloved", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Med", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "cause-2h25uu-mi4xnini" - }, - "notes": "", - "auditTrail": [] - }, - { - "id": "669ca6a2-4b0a-421c-84fa-e54e16097e56", - "analysisId": "analysis-ea2a2d69-11c9-4bf7-8c6f-301d64b430cf", - "createdAt": "2025-11-18T18:56:03.381Z", - "createdBy": "local", - "summary": "Examine Wash Tanks for Contaminents", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Med", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "cause-2u9pmq-mi4xo7ef" - }, - "notes": "", - "auditTrail": [] - } - ] - } - } - }, - { - "id": "paytrix", - "name": "Paytrix", - "description": "Chaos at Paytrix", - "supportedModes": [ - "intake", - "is-is-not", - "dc", - "full" - ], - "templateKind": "case-study", - "state": { - "meta": { - "version": 1, - "savedAt": "2025-12-04T21:37:48.348Z" - }, - "appearance": { - "theme": "light" - }, - "pre": { - "oneLine": "AuthorizationService (NA-East) is experiencing sustained elevated p95 latency above baseline.", - "proof": "Detection Sources:\nMonitoring (Datadog)\nUser Report (Amazon, Delta Airlines)\nAutomation (xMatters Sev-1 rule)\n\nEvidence Collected:\nMetrics screenshots from Datadog\nxMatters high-priority alert\nMerchant emails (Amazon, Delta)\nInternal Teams chatter confirming latency and 504s\nServiceNow auto-Sev1 ticket\n", - "objectPrefill": "AuthService — NA-East regional shard (Authorization path only).", - "healthy": "Authorization p95 latency ~95–150ms\n\nTimeout rate < 0.3%\n\nNo sustained regional deviation\n\nMajor merchants reporting normal operation\n\nHealth checks passing within expected thresholds", - "now": "p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior." - }, - "impact": { - "now": "Merchants in NA-East reporting payment delays/timeouts\n\nAmazon retry rate 5–7%\n\nDelta kiosks unable to process payments\n\nMerchantGateway experiencing elevated 504 responses\n\nIncreased abandonment in retail checkouts", - "future": "Retry storms may propagate load and degrade other regions (NA-West, SA)\n\nSLO/SLA breach likely within minutes\n\nPotential throttling by downstream issuers\n\nEscalation to global merchant base (churn & reputational risk)\n\nOn-call fatigue and SME overload if the incident expands", - "time": "Impact is active now\n\nExpected to worsen within 5–10 minutes if no containment initiated\n\nMerchant dissatisfaction already emerging" - }, - "ops": { - "bridgeOpenedUtc": "2025-12-03T16:40:01.816Z", - "icName": "Jordan Reyes", - "bcName": "Shane C", - "semOpsName": "", - "severity": "S1", - "detectMonitoring": true, - "detectUserReport": true, - "detectAutomation": true, - "detectOther": false, - "evScreenshot": false, - "evLogs": false, - "evMetrics": false, - "evRepro": false, - "evOther": false, - "containStatus": "assessing", - "containDesc": "Pause non-critical MerchantGateway transactions impacting Auth until traffic stabilizes. Validate with SME before execution.", - "commCadence": "", - "commLog": [], - "commNextDueIso": "", - "commNextUpdateTime": "", - "tableFocusMode": "comprehensive" - }, - "table": [ - { - "band": "WHAT — Define the problem precisely (Object & Deviation)." - }, - { - "q": "WHAT — Specific Object/Thing is having the “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.”", - "is": "AuthService — NA-East regional shard (Authorization path only).", - "no": "AuthService — NA-West\nAuthService — EU shard\nFraudEdge precheck service\nMerchantGateway service\nRedis cluster\nNetwork transport layer", - "di": "NA-East Authorization uses a different logic branch due to region-specific config.", - "ch": "Region-specific override for adaptive risk scoring was enabled yesterday afternoon, but only for NA-East.", - "questionId": "what-object" - }, - { - "q": "WHAT — Specific Deviation does the “AuthService — NA-East regional shard (Authorization path only).” have?", - "is": "p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.", - "no": "Authorization service is down\nAuthorization service is returning zero responses\nAuthorization errors are increasing across regions\nFraudEdge latency is elevated\nMerchantGateway latency is elevated", - "di": "", - "ch": "", - "questionId": "what-deviation" - }, - { - "band": "WHERE — Locate the problem (geography/topology and on the object)." - }, - { - "q": "WHERE — is the “AuthService — NA-East regional shard (Authorization path only).” geographically/topology when the “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” occurs?", - "is": "NA-East region (all AZs).", - "no": "NA-West\nEU region\nSouth America region\nSandbox / non-production", - "di": "NA-East is uniquely under promo-concentrated load, making it the only region able to trigger the new logic branch.", - "ch": "Traffic steering policy shifted heavier Amazon promo traffic to NA-East earlier this week.", - "questionId": "where-location" - }, - { - "q": "WHERE — On the “AuthService — NA-East regional shard (Authorization path only).” is the “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” observed?", - "is": "Authorization path within AuthService.\nPoints where MerchantGateway calls AuthService (responses originating from AuthService in NA-East).", - "no": "TokenService path\nFraudEdge decision path\nRedis lookup path\nKafka publish/subscribe layers", - "di": "", - "ch": "", - "questionId": "where-on-object" - }, - { - "band": "WHEN — Timing and Description" - }, - { - "q": "WHEN — Was the “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” First observed for “AuthService — NA-East regional shard (Authorization path only).”", - "is": "Around 10:03 AM ET.", - "no": "10:01 AM Dev Datadog annotation\nEarlier EU event referenced in Slack\n9:55 AM MerchantGateway config push", - "di": "", - "ch": "", - "questionId": "when-first-observed" - }, - { - "q": "WHEN — Since the first occurrence has “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” been logged? What Pattern?", - "is": "Sustained elevation for several minutes (3–4+).\nNot intermittent.", - "no": "Periodic pattern\nImproving trend\nLimited to a single AZ\nAlternating across regions", - "di": "", - "ch": "", - "questionId": "when-pattern" - }, - { - "q": "WHEN — Describe using words When the “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” was first seen", - "is": "During active authorization traffic.\nDuring morning merchant peak (Amazon promo surge).", - "no": "During a deploy window\nDuring planned maintenance\nDuring a failover event", - "di": "The deviation starts when the traffic threshold is crossed, not when the deploy or config change happened.", - "ch": "Threshold parameters updated at 9:58; threshold crossed at 10:03.", - "questionId": "when-description" - }, - { - "band": "EXTENT — How big is it? Magnitude, count, scope, trend." - }, - { - "q": "EXTENT — What is the population or size of “AuthService — NA-East regional shard (Authorization path only).” affected?", - "is": "All AuthService nodes in NA-East.\nAll AZs in NA-East.", - "no": "Nodes in NA-West\nNodes in EU\nNodes in staging/sandbox", - "di": "", - "ch": "", - "questionId": "extent-population" - }, - { - "q": "EXTENT — What is the size of a single “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.”?", - "is": "p95 latency elevated to 500–700ms.", - "no": "Multi-second total stalls (e.g., 3–5 seconds unresponsive)\nTotal outage (zero responses)\nCPU/memory exhaustion", - "di": "", - "ch": "", - "questionId": "extent-size" - }, - { - "q": "EXTENT — How many “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” are occuring on each “AuthService — NA-East regional shard (Authorization path only).”?", - "is": "Continuous elevated latency across all NA-East nodes.\nPresent across multiple merchant flows (Amazon, Delta).", - "no": "Single-node issue\nSingle-merchant issue\nShort-lived blip", - "di": "", - "ch": "", - "questionId": "extent-count" - } - ], - "causes": [ - { - "id": "cause-upi5es-mirvp5em", - "suspect": "Adaptive risk scoring logic in NA-East Authorization path", - "accusation": "adds overhead under threshold-triggered conditions", - "impact": "load causing latency to spike", - "summaryText": "We suspect Adaptive risk scoring logic in NA-East Authorization path is adds overhead under threshold-triggered conditions. This could lead to load causing latency to spike.", - "confidence": "", - "evidence": "", - "findings": { - "WHAT — Specific Object/Thing is having the {DEVIATION}": { - "mode": "yes", - "note": "The region-specific config override exists only in NA-East Authorization path (matches IS). No other region or service uses that branch (matches IS NOT)." - }, - "WHAT — Specific Deviation does the {OBJECT} have?": { - "mode": "yes", - "note": "Extra processing overhead from the logic branch explains latency elevation without causing outages or multi-region failures." - }, - "WHERE — is the {OBJECT} geographically/topology when the {DEVIATION} occurs?": { - "mode": "yes", - "note": "Region-specific override applies to NA-East only (matches IS). Other regions do not use override or hit threshold (matches IS NOT)." - }, - "WHERE — On the {OBJECT} is the {DEVIATION} observed?": { - "mode": "yes", - "note": "Only the Authorization handler uses the adaptive scoring logic; other handlers do not. Exact alignment." - }, - "WHEN — Was the {DEVIATION} First observed for {OBJECT}": { - "mode": "yes", - "note": "Threshold for adaptive scoring activated only once traffic crossed at 10:03. Prior times show no effect (aligned to IS NOT)." - }, - "WHEN — Since the first occurrence has {DEVIATION} been logged? What Pattern?": { - "mode": "yes", - "note": "Flag logic would increase compute consistently under load; region-wide config makes pattern uniform across AZs." - }, - "WHEN — Describe using words When the {DEVIATION} was first seen": { - "mode": "yes", - "note": "Threshold-based scoring triggers under high traffic (IS). It is not tied to deploy/maintenance/failover (IS NOT)." - }, - "EXTENT — What is the population or size of {OBJECT} affected?": { - "mode": "yes", - "note": "Region-wide override applied to every node in NA-East. Other regions not affected." - }, - "EXTENT — What is the size of a single {DEVIATION}?": { - "mode": "yes", - "note": "Feature overhead produces sub-second increases, not multisecond stalls or outages." - }, - "EXTENT — How many {DEVIATION} are occuring on each {OBJECT}?": { - "mode": "yes", - "note": "Region-wide threshold logic affects all AZs and all merchants routed through NA-East. No contradictions." - } - }, - "editing": false, - "testingOpen": false - }, - { - "id": "cause-vakmx1-mirvrepn", - "suspect": "MerchantGateway retry configuration", - "accusation": "too aggressive", - "impact": "amplifies latency during authorization degradation", - "summaryText": "We suspect MerchantGateway retry configuration because of too aggressive. This could lead to amplifies latency during authorization degradation.", - "confidence": "", - "evidence": "", - "findings": { - "WHAT — Specific Object/Thing is having the {DEVIATION}": { - "mode": "assumption", - "note": "Retries could amplify latency on the auth path only if AuthService is already slow (assumption required). Cannot explain NA-East-specific config difference." - }, - "WHAT — Specific Deviation does the {OBJECT} have?": { - "mode": "assumption", - "note": "Retries can push latency to 500–700ms only if upstream (AuthService) slowdown begins first." - }, - "WHERE — is the {OBJECT} geographically/topology when the {DEVIATION} occurs?": { - "mode": "assumption", - "note": "MGW retry behavior is global; only NA-East sees impact if NA-East responds slower than other regions. Assumption needed." - }, - "WHERE — On the {OBJECT} is the {DEVIATION} observed?": { - "mode": "assumption", - "note": "MGW retries affect only paths calling AuthService; but cannot independently explain why only Authorization handler is slow. Requires assumption." - }, - "WHEN — Was the {DEVIATION} First observed for {OBJECT}": { - "mode": "assumption", - "note": "Retries spike at exactly 10:03 because AuthService slowed. This fits only if another cause degraded AuthService first." - }, - "WHEN — Since the first occurrence has {DEVIATION} been logged? What Pattern?": { - "mode": "yes", - "note": "Retry storms can produce sustained latency (fits IS)." - }, - "WHEN — Describe using words When the {DEVIATION} was first seen": { - "mode": "assumption", - "note": "Retries align to promo surge only if AuthService degraded at the same time." - }, - "EXTENT — What is the population or size of {OBJECT} affected?": { - "mode": "assumption", - "note": "Would affect all NA-East nodes if MGW retries hit all endpoints uniformly; requires assumption." - }, - "EXTENT — What is the size of a single {DEVIATION}?": { - "mode": "assumption", - "note": "Retry amplification can produce 500–700ms range, but requires AuthService to be marginally slow first." - }, - "EXTENT — How many {DEVIATION} are occuring on each {OBJECT}?": { - "mode": "assumption", - "note": "Continuous retries align with continuous upstream latency if AuthService remains slow." - } - }, - "editing": false, - "testingOpen": false - }, - { - "id": "cause-gcz67n-mirvtxi6", - "suspect": "Redis routing", - "accusation": "intermittently resolves to a cross-region shard under load", - "impact": "the round-trip time delay to request and get a response from the remote shard increases latency", - "summaryText": "We suspect Redis routing is intermittently resolves to a cross-region shard under load. This could lead to the round-trip time delay to request and get a response from the remote shard increases latency.", - "confidence": "", - "evidence": "", - "findings": { - "WHAT — Specific Object/Thing is having the {DEVIATION}": { - "mode": "fail", - "note": "Redis misrouting would affect TokenService, FraudEdge, and other paths — but IS says they’re normal. Contradiction." - }, - "WHAT — Specific Deviation does the {OBJECT} have?": { - "mode": "fail", - "note": "Cross-region Redis calls cause multi-second stalls or intermittent spikes — NOT uniform 500–700ms elevation." - }, - "WHERE — is the {OBJECT} geographically/topology when the {DEVIATION} occurs?": { - "mode": "fail", - "note": "Redis routing bugs rarely hit an entire region uniformly; would show uneven AZ impacts, contradicting IS." - }, - "WHERE — On the {OBJECT} is the {DEVIATION} observed?": { - "mode": "fail", - "note": "Redis routing would affect multiple paths beyond Authorization (contradicting IS NOT entries)." - }, - "WHEN — Was the {DEVIATION} First observed for {OBJECT}": { - "mode": "fail", - "note": "Redis issues do not align precisely with a 10:03 AM threshold event; mismatch with IS." - }, - "WHEN — Since the first occurrence has {DEVIATION} been logged? What Pattern?": { - "mode": "fail", - "note": "Redis problems are intermittent or jittery — contradicts sustained pattern." - }, - "WHEN — Describe using words When the {DEVIATION} was first seen": { - "mode": "fail", - "note": "Redis issues do not correlate to promo surges (IS), nor to deploy/maintenance windows (IS NOT). No alignment." - }, - "EXTENT — What is the population or size of {OBJECT} affected?": { - "mode": "fail", - "note": "Cross-region Redis calls would not hit all NA-East nodes uniformly — contradicts IS." - }, - "EXTENT — What is the size of a single {DEVIATION}?": { - "mode": "fail", - "note": "Remote shard resolution would exceed 1–3 seconds or time out entirely — contradicts IS 500–700ms envelope." - }, - "EXTENT — How many {DEVIATION} are occuring on each {OBJECT}?": { - "mode": "fail", - "note": "Redis issues would be uneven, merchant-specific, or node-specific — contradicts continuous all-node pattern." - } - }, - "editing": false, - "testingOpen": false - } - ], - "likelyCauseId": "cause-upi5es-mirvp5em", - "steps": { - "items": [ - { - "id": "1", - "label": "Pre-analysis completed", - "checked": false - }, - { - "id": "2", - "label": "Incident Commander assigned", - "checked": false - }, - { - "id": "4", - "label": "Problem statement created", - "checked": false - }, - { - "id": "5", - "label": "Bridge options considered", - "checked": false - }, - { - "id": "6", - "label": "Bridges opened and responders invited", - "checked": false - }, - { - "id": "7", - "label": "Bridge etiquette, roles, and guidelines outlined", - "checked": false - }, - { - "id": "8", - "label": "Current actions documented (Who/What/When)", - "checked": false - }, - { - "id": "9", - "label": "Quick spec answers captured", - "checked": false - }, - { - "id": "10", - "label": "Last changes, monitoring, and dependencies investigated", - "checked": false - }, - { - "id": "11", - "label": "Attendees optimized", - "checked": false - }, - { - "id": "12", - "label": "Possible causes developed", - "checked": false - }, - { - "id": "13", - "label": "Testing actions documented (Who/What/When)", - "checked": false - }, - { - "id": "14", - "label": "Micro-experiments/tests conducted", - "checked": false - }, - { - "id": "15", - "label": "Containment options identified", - "checked": false - }, - { - "id": "16", - "label": "Comms written, reviewed, and sent", - "checked": false - }, - { - "id": "17", - "label": "Attendees optimized for next action", - "checked": false - }, - { - "id": "18", - "label": "Possible causes evaluated and distinctions identified", - "checked": false - }, - { - "id": "19", - "label": "Most probable cause identified", - "checked": false - }, - { - "id": "20", - "label": "Restoration/rollback/workaround selected", - "checked": false - }, - { - "id": "21", - "label": "Attendees optimized for decision", - "checked": false - }, - { - "id": "22", - "label": "Verification and risk plan created", - "checked": false - }, - { - "id": "23", - "label": "Restoration actions documented (Who/What/When)", - "checked": false - }, - { - "id": "24", - "label": "Service validated internally and externally", - "checked": false - }, - { - "id": "25", - "label": "Restoration comms sent", - "checked": false - }, - { - "id": "26", - "label": "Handover template prepared (PIR/problem)", - "checked": false - }, - { - "id": "27", - "label": "Downstream issues from fix assessed", - "checked": false - }, - { - "id": "28", - "label": "Participants released and bridge closed", - "checked": false - } - ], - "drawerOpen": false - }, - "actions": { - "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", - "items": [ - { - "id": "729fc1ee-6d38-4a38-b6ad-513aea372c69", - "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", - "createdAt": "2025-12-03T17:08:52.403Z", - "createdBy": "local", - "summary": "Verify latency across all AZs in NA-East (are all AZs affected or only one?)", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "High", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": { - "level": "None", - "impactIfFails": "", - "prevent": "", - "ifHappens": "" - }, - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": {}, - "notes": "", - "auditTrail": [] - }, - { - "id": "7d1a2e39-f578-497e-9c76-9f403ceb7048", - "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", - "createdAt": "2025-12-03T17:08:58.929Z", - "createdBy": "local", - "summary": "Check regional parity (NA-West, EU-Central): confirm this is isolated.", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "High", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": { - "level": "None", - "impactIfFails": "", - "prevent": "", - "ifHappens": "" - }, - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": {}, - "notes": "", - "auditTrail": [] - }, - { - "id": "b6d6fe21-e590-4784-abc6-6cb24a16d0f8", - "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", - "createdAt": "2025-12-03T17:09:41.392Z", - "createdBy": "local", - "summary": "Retrieve last-known-good window for AuthService from Datadog.", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "High", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": { - "level": "None", - "impactIfFails": "", - "prevent": "", - "ifHappens": "" - }, - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": {}, - "notes": "", - "auditTrail": [] - }, - { - "id": "bea8d3c3-d520-4a2f-ac21-abe4c15b5ed6", - "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", - "createdAt": "2025-12-03T17:10:00.723Z", - "createdBy": "local", - "summary": "Gather the exact merchant impact metrics (retry %, timeouts, abandonment).", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "High", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": { - "level": "None", - "impactIfFails": "", - "prevent": "", - "ifHappens": "" - }, - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": {}, - "notes": "", - "auditTrail": [] - }, - { - "id": "24e20fac-524a-49c8-b4ce-16fd3fde4db9", - "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", - "createdAt": "2025-12-03T17:09:37.024Z", - "createdBy": "local", - "summary": "Check FraudEdge queues to confirm not contributing.", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Med", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": { - "level": "None", - "impactIfFails": "", - "prevent": "", - "ifHappens": "" - }, - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": {}, - "notes": "", - "auditTrail": [] - }, - { - "id": "683f6c33-67b6-4fc1-8f3a-4e597fd2e7c8", - "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", - "createdAt": "2025-12-03T17:09:13.163Z", - "createdBy": "local", - "summary": "Validate Redis routing update status in case SMEs attempt to blame it.", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Med", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": { - "level": "None", - "impactIfFails": "", - "prevent": "", - "ifHappens": "" - }, - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": {}, - "notes": "", - "auditTrail": [] - }, - { - "id": "388fda77-c081-4ae9-93fc-206275dce281", - "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", - "createdAt": "2025-12-03T17:09:04.059Z", - "createdBy": "local", - "summary": "Confirm recent MerchantGateway config change timing vs spike onset.", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Med", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": { - "level": "None", - "impactIfFails": "", - "prevent": "", - "ifHappens": "" - }, - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": {}, - "notes": "", - "auditTrail": [] - }, - { - "id": "ff855c1c-ca68-4ed0-b99d-a96737f02ff2", - "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", - "createdAt": "2025-12-03T17:09:53.441Z", - "createdBy": "local", - "summary": "Coordinate with MCNO to determine whether a Broadscope is now required.", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Low", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": { - "level": "None", - "impactIfFails": "", - "prevent": "", - "ifHappens": "" - }, - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": {}, - "notes": "", - "auditTrail": [] - }, - { - "id": "afbc20c5-18d8-44aa-ad7b-5b414486e829", - "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", - "createdAt": "2025-12-03T17:09:47.392Z", - "createdBy": "local", - "summary": "Confirm actual timeout error codes (504 vs 408 vs 429).", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Low", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": { - "level": "None", - "impactIfFails": "", - "prevent": "", - "ifHappens": "" - }, - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": {}, - "notes": "", - "auditTrail": [] - } - ] - } - } - }, - { - "id": "checkout-latency", - "name": "Checkout Latency Spike", - "description": "EU checkout slowdown triggered by a CDN header rewrite.", - "supportedModes": [ - "intake", - "is-is-not", - "dc", - "full" - ], - "templateKind": "standard", - "state": { - "meta": { - "version": 1, - "savedAt": null - }, - "pre": { - "oneLine": "EU checkout requests timing out after CDN rule change.", - "proof": "APM shows p95 latency at 18s with widespread gateway retries.", - "objectPrefill": "Checkout service / EU tenants", - "healthy": "Authorizations complete under 3s with <1% retry.", - "now": "Cards time out, fallback queue saturates, and conversions drop 35%." - }, - "impact": { - "now": "Merchants losing €1.2M/hr, customers double-purchasing, and support volume spiking.", - "future": "Merchants will disable promos and regulators expect notification if >60m.", - "time": "Started 2024-03-08 07:38Z as CDN header rewrite pushed globally." - }, - "ops": { - "bridgeOpenedUtc": "2024-03-08 07:42Z", - "icName": "Jonas Reed", - "bcName": "Eva Mendez", - "semOpsName": "Luis Martin", - "severity": "SEV-1", - "detectMonitoring": true, - "detectUserReport": false, - "detectAutomation": true, - "detectOther": false, - "evScreenshot": false, - "evLogs": true, - "evMetrics": true, - "evRepro": true, - "evOther": true, - "containStatus": "stabilized", - "containDesc": "Rolled back EU POPs to last-known-good CDN config and rate-limited loyalty header.", - "commCadence": "15", - "commLog": [ - { - "type": "internal", - "ts": "2024-03-08T07:45:00Z", - "message": "War room formed, diffing CDN configs." - }, - { - "type": "external", - "ts": "2024-03-08T08:00:00Z", - "message": "Status page updated with EU latency advisory." - }, - { - "type": "internal", - "ts": "2024-03-08T08:10:00Z", - "message": "Containment applied via POP rollback; monitoring drop." - } - ], - "commNextDueIso": "2024-03-08T08:15:00Z", - "commNextUpdateTime": "08:15", - "tableFocusMode": "comprehensive" - }, - "table": [ - { - "band": "WHAT", - "note": "Define the problem precisely (Object & Deviation)." - }, - { - "questionId": "what-object", - "q": "WHAT — Specific Object/Thing is having the {DEVIATION}", - "is": "Checkout API for EU tenants hitting FRA/AMS POPs.", - "no": "US checkout tenants plus non-card wallets remain normal.", - "di": "Only card payments routed through CDN header rewrite show latency.", - "ch": "Impacted traffic contains new loyalty header added this morning." - }, - { - "questionId": "what-deviation", - "q": "WHAT — Specific Deviation does the {OBJECT} have?", - "is": "Auth calls take 15-20s and time out, forcing retries.", - "no": "Async webhook callbacks and idempotency layer stay nominal.", - "di": "Latency spikes only on POPs where header rewrite is enabled.", - "ch": "Deviation began minutes after config promotion." - }, - { - "band": "WHERE", - "note": "Locate the problem (geography/topology and on the object)." - }, - { - "questionId": "where-location", - "q": "WHERE — is the {OBJECT} geographically/topology when the {DEVIATION} occurs?", - "is": "FRA and AMS POPs fronting EU tenants.", - "no": "DUB and LHR POPs on legacy routing stay healthy.", - "di": "Only POPs that honor loyalty header override misbehave.", - "ch": "Same POPs received header rewrite at 07:38Z." - }, - { - "questionId": "where-on-object", - "q": "WHERE — On the {OBJECT} is the {DEVIATION} observed?", - "is": "Ingress CDN tier before requests reach checkout pods.", - "no": "Checkout pods inside EU clusters show normal CPU/mem.", - "di": "Only TLS handshake plus origin selection step regresses.", - "ch": "Header rewrite manipulates origin cache key for loyalty path." - }, - { - "band": "WHEN", - "note": "Timing and Description" - }, - { - "questionId": "when-pattern", - "q": "WHEN — Since the first occurrence has {DEVIATION} been logged? What Pattern?", - "is": "Continuous since 07:38Z; no relief between retries.", - "no": "Earlier in the day latency followed normal diurnal pattern.", - "di": "Traffic during loyalty promo hours is hardest hit.", - "ch": "Pattern maps to config roll at 07:38Z then steady state." - }, - { - "questionId": "when-description", - "q": "WHEN — Describe using words When the {DEVIATION} was first seen", - "is": "Appeared immediately during CDN deploy validation (before user escalation).", - "no": "No deviations overnight despite heavy load tests.", - "di": "Only requests including loyalty header see rewrite and delay.", - "ch": "New config rewrites Accept headers and bypasses warm cache." - }, - { - "band": "EXTENT", - "note": "How big is it? Magnitude, count, scope, trend." - }, - { - "questionId": "extent-population", - "q": "EXTENT — What is the population or size of {OBJECT} affected?", - "is": "All EU card traffic (~2.1k rps) routed through FRA/AMS POPs.", - "no": "Merchants pinned to DUB POP unaffected.", - "di": "Premium merchants on loyalty pilot are 2x more impacted.", - "ch": "Population exactly matches tenants moved to new header rule." - }, - { - "questionId": "extent-size", - "q": "EXTENT — What is the size of a single {DEVIATION}?", - "is": "Single checkout attempts incur 18s additional latency and multiple retries.", - "no": "Baseline deviation remained <500ms before change.", - "di": "Latency delta grows with each loyalty header mutation.", - "ch": "Size stabilized once rollback applied." - } - ], - "causes": [ - { - "id": "cause-cdn-rule", - "suspect": "CDN header rewrite broke cache keying for loyalty traffic.", - "accusation": "Rewrite adds Accept header fragment forcing origin revalidation on every request.", - "impact": "POP queues build, TLS renegotiates, and checkout pods receive bursts.", - "summaryText": "Misconfigured CDN rule for loyalty header invalidates cache and adds 15s delay.", - "confidence": "high", - "evidence": "Disabling rewrite on FRA instantly returns latency to baseline.", - "findings": { - "what-object": { - "mode": "yes", - "note": "Only checkout API traffic on loyalty header is impacted." - }, - "where-location": { - "mode": "yes", - "note": "Matches FRA/AMS POPs that received rewrite." - }, - "when-pattern": { - "mode": "yes", - "note": "Timeline aligns with header deployment." - } - }, - "editing": false, - "testingOpen": false - }, - { - "id": "cause-db-hot", - "suspect": "Multi-region DB hot partition.", - "accusation": "EU tenant data co-located causing partition hotspot and slow auth.", - "impact": "Would explain retries if DB were throttling connections.", - "summaryText": "Potential DB hot partition causing cascading latency.", - "confidence": "low", - "evidence": "DB metrics remain green; theory kept for completeness.", - "findings": { - "what-deviation": { - "mode": "fail", - "note": "DB tier shows no latency delta." - }, - "extent-size": { - "mode": "fail", - "note": "Does not explain 18s TLS stall before origin." - } - }, - "editing": false, - "testingOpen": false - } - ], - "likelyCauseId": "cause-cdn-rule", - "steps": { - "items": [ - { - "id": "2", - "label": "Incident Commander assigned", - "checked": true - }, - { - "id": "8", - "label": "Current actions documented (Who/What/When)", - "checked": true - }, - { - "id": "13", - "label": "Testing actions documented (Who/What/When)", - "checked": true - }, - { - "id": "20", - "label": "Restoration/rollback/workaround selected", - "checked": true - }, - { - "id": "24", - "label": "Service validated internally and externally", - "checked": false - } - ], - "drawerOpen": false - }, - "actions": { - "analysisId": "template-checkout", - "items": [ - { - "id": "action-cdn-freeze", - "analysisId": "template-checkout", - "createdAt": "2024-03-08T07:50:00Z", - "createdBy": "TemplateBot", - "summary": "Freeze CDN config and audit loyalty rewrite.", - "detail": "Diff last-known-good config vs current, revert EU POPs, and prepare lock file.", - "owner": { - "name": "CDN Engineering", - "category": "TECHNOLOGY_PLATFORM", - "subOwner": "TECHNOLOGY_PLATFORM__CLOUD", - "notes": "On-call pair running diff script.", - "lastAssignedBy": "TemplateBot", - "lastAssignedAt": "2024-03-08T07:52:00Z", - "source": "Template" - }, - "role": "Networking", - "status": "In-Progress", - "priority": "High", - "dueAt": "2024-03-08T08:20:00Z", - "startedAt": "2024-03-08T07:52:00Z", - "completedAt": "", - "dependencies": [], - "risk": "High", - "changeControl": { - "required": true, - "rollbackPlan": "CDN config checkpoint restore." - }, - "verification": { - "required": true, - "result": "" - }, - "links": {}, - "notes": "Need approval from BC before global push.", - "auditTrail": [] - }, - { - "id": "action-merchant-brief", - "analysisId": "template-checkout", - "createdAt": "2024-03-08T08:05:00Z", - "createdBy": "TemplateBot", - "summary": "Brief top merchants and regulators.", - "detail": "Share impact summary, containment ETA, and request extension on EU SLA.", - "owner": { - "name": "Merchant Success", - "category": "BUSINESS_CUSTOMER", - "subOwner": "BUSINESS_CUSTOMER__BUSINESS_RELATIONSHIP", - "notes": "Coordinating with exec sponsor.", - "lastAssignedBy": "TemplateBot", - "lastAssignedAt": "2024-03-08T08:06:00Z", - "source": "Template" - }, - "role": "BC Comms", - "status": "Planned", - "priority": "Med", - "dueAt": "2024-03-08T08:30:00Z", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": "Medium", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": {}, - "notes": "Prep template for EU regulator alias.", - "auditTrail": [] - } - ] - } - } - }, - { - "id": "mrs-walthers-sugar-donuts", - "name": "Mrs Walthers Sugar Donuts", - "description": "Quality control has reported bad donuts at their inspection station", - "supportedModes": [ - "intake", - "is-is-not", - "dc", - "full" - ], - "templateKind": "case-study", - "state": { - "meta": { - "version": 1, - "savedAt": "2025-11-18T07:33:01.521Z" - }, - "pre": { - "oneLine": "Donuts are bad", - "proof": "Quality control has reported bad donuts at their inspection station", - "objectPrefill": "Sugar Donuts", - "healthy": "All edges should be square. Center hole should be blown out fully, and the center hole is also square. Donuts should be cooked correctly, and golden in color. Thickness should be within specified parameters", - "now": "Round Edges" - }, - "impact": { - "now": "25% Reject rate for Sugar Donuts (1 out of 4 rows).\nProduction demands are not being met", - "future": "Unable to meet production demand", - "time": "End of the Day" - }, - "ops": { - "bridgeOpenedUtc": "2025-11-18T04:41:35.797Z", - "icName": "", - "bcName": "", - "semOpsName": "", - "severity": "", - "detectMonitoring": true, - "detectUserReport": false, - "detectAutomation": false, - "detectOther": false, - "evScreenshot": true, - "evLogs": false, - "evMetrics": false, - "evRepro": false, - "evOther": false, - "containStatus": "assessing", - "containDesc": "", - "commCadence": "", - "commLog": [], - "commNextDueIso": "", - "commNextUpdateTime": "", - "tableFocusMode": "comprehensive" - }, - "table": [ - { - "band": "WHAT — Define the problem precisely (Object & Deviation)." - }, - { - "q": "WHAT — Specific Object/Thing is having the “Round Edges”", - "is": "Sugar Donuts", - "no": "Crullers\nPlain, Frosted", - "di": "Donuts are made on a separate line\n\nDonuts are larger in size\n\nSugar Donuts are sugar coated\n\nMade on Rows 1-4", - "ch": "New Sugaring Equipment (1 Week Ago)", - "questionId": "what-object" - }, - { - "q": "WHAT — Specific Deviation does the “Sugar Donuts” have?", - "is": "Round Edges", - "no": "Too thick, too thin, overcooked, undercooked, center holes not blown out", - "di": "", - "ch": "", - "questionId": "what-deviation" - }, - { - "band": "WHERE — Locate the problem (geography/topology and on the object)." - }, - { - "q": "WHERE — is the “Sugar Donuts” geographically/topology when the “Round Edges” occurs?", - "is": "Fast Line, Mesh Belt, QC\nRow 1", - "no": "Packaging, Customer\nRows 2,3,4 or combination", - "di": "Row 1 is furthest from the air source\n\nRow 1 has no outside cutter blade\n\nRow 1 edges are formed by edgers", - "ch": "Air System Modified (1 Week Ago)\n\nStripped Edger Screw (1 Week Ago)\n\nReplaced Edger Screw (1 Week Ago)", - "questionId": "where-location" - }, - { - "q": "WHERE — On the “Sugar Donuts” is the “Round Edges” observed?", - "is": "Outside Edge", - "no": "Inside, leading, trailing or combination of edges", - "di": "No Blades on outside edge", - "ch": "", - "questionId": "where-on-object" - }, - { - "band": "WHEN — Timing and Description" - }, - { - "q": "WHEN — Was the “Round Edges” First observed for “Sugar Donuts”", - "is": "~1 Hr Ago", - "no": "Before 1 Hour Ago", - "di": "We installed a new chain around this time", - "ch": "Chain Broke (55 Mins Ago)\n\nChain Installed (1 Hr Ago)", - "questionId": "when-first-observed" - }, - { - "q": "WHEN — Since the first occurrence has “Round Edges” been logged? What Pattern?", - "is": "Every Minute Since\nContinuous", - "no": "Any Other Times\nPeriodic, Sporadic, One-Time Event", - "di": "", - "ch": "", - "questionId": "when-pattern" - }, - { - "q": "WHEN — Describe using words When the “Round Edges” was first seen", - "is": "After Cutting the donuts", - "no": "After cooking, being transported on mesh belt, during cooling, after topping, or at quality control", - "di": "", - "ch": "", - "questionId": "when-description" - }, - { - "band": "EXTENT — How big is it? Magnitude, count, scope, trend." - }, - { - "q": "EXTENT — What is the population or size of “Sugar Donuts” affected?", - "is": "25% Reject Rate\n1 of 4 Rows", - "no": "More or Less than 25%\n2 or More Rows", - "di": "", - "ch": "", - "questionId": "extent-population" - }, - { - "q": "EXTENT — What is the size of a single “Round Edges”?", - "is": "Entire Edge", - "no": "Partial Edge", - "di": "", - "ch": "", - "questionId": "extent-size" - }, - { - "q": "EXTENT — How many “Round Edges” are occuring on each “Sugar Donuts”?", - "is": "1 Per Donut", - "no": "Between 2-4", - "di": "", - "ch": "", - "questionId": "extent-count" - } - ], - "causes": [ - { - "id": "cause-4giuxz-mi43kb9e", - "suspect": "Blades", - "accusation": "Overheating", - "impact": "and causing dough to not properly release from blades", - "summaryText": "We suspect Blades because Overheating. This could lead to and causing dough to not properly release from blades.", - "confidence": "", - "evidence": "", - "findings": { - "WHAT — Specific Object/Thing is having the {DEVIATION}": { - "mode": "assumption", - "note": "Only if the sugar donuts blades (1-4) are overheating" - }, - "WHAT — Specific Deviation does the {OBJECT} have?": { - "mode": "assumption", - "note": "The center hold blades are releasing just fine" - }, - "WHERE — is the {OBJECT} geographically/topology when the {DEVIATION} occurs?": { - "mode": "assumption", - "note": "Row 1-4 blades are overheating" - }, - "WHERE — On the {OBJECT} is the {DEVIATION} observed?": { - "mode": "fail", - "note": "We do not have blades on the outside edge of row 1" - } - }, - "editing": false, - "testingOpen": false - }, - { - "id": "cause-auefoi-mi43le61", - "suspect": "Hot Oil Bath", - "accusation": "at Incorrect Temperature", - "impact": "Cooking one side of the donuts improperly", - "summaryText": "We suspect Hot Oil Bath because at Incorrect Temperature. This could lead to Cooking one side of the donuts improperly.", - "confidence": "", - "evidence": "", - "findings": { - "WHAT — Specific Object/Thing is having the {DEVIATION}": { - "mode": "fail", - "note": "This does not explain why other donuts would be affected" - } - }, - "editing": false, - "testingOpen": false - }, - { - "id": "cause-dtnho5-mi43m5a9", - "suspect": "Edgers", - "accusation": "is loose", - "impact": "and not holding outside edge at correct specifications", - "summaryText": "We suspect Edgers because is loose. This could lead to and not holding outside edge at correct specifications.", - "confidence": "", - "evidence": "", - "findings": {}, - "editing": false, - "testingOpen": false - } - ], - "likelyCauseId": "cause-dtnho5-mi43m5a9", - "steps": { - "items": [ - { - "id": "1", - "label": "Pre-analysis completed", - "checked": false - }, - { - "id": "2", - "label": "Incident Commander assigned", - "checked": false - }, - { - "id": "4", - "label": "Problem statement created", - "checked": false - }, - { - "id": "5", - "label": "Bridge options considered", - "checked": false - }, - { - "id": "6", - "label": "Bridges opened and responders invited", - "checked": false - }, - { - "id": "7", - "label": "Bridge etiquette, roles, and guidelines outlined", - "checked": false - }, - { - "id": "8", - "label": "Current actions documented (Who/What/When)", - "checked": false - }, - { - "id": "9", - "label": "Quick spec answers captured", - "checked": false - }, - { - "id": "10", - "label": "Last changes, monitoring, and dependencies investigated", - "checked": false - }, - { - "id": "11", - "label": "Attendees optimized", - "checked": false - }, - { - "id": "12", - "label": "Possible causes developed", - "checked": false - }, - { - "id": "13", - "label": "Testing actions documented (Who/What/When)", - "checked": false - }, - { - "id": "14", - "label": "Micro-experiments/tests conducted", - "checked": false - }, - { - "id": "15", - "label": "Containment options identified", - "checked": false - }, - { - "id": "16", - "label": "Comms written, reviewed, and sent", - "checked": false - }, - { - "id": "17", - "label": "Attendees optimized for next action", - "checked": false - }, - { - "id": "18", - "label": "Possible causes evaluated and distinctions identified", - "checked": false - }, - { - "id": "19", - "label": "Most probable cause identified", - "checked": false - }, - { - "id": "20", - "label": "Restoration/rollback/workaround selected", - "checked": false - }, - { - "id": "21", - "label": "Attendees optimized for decision", - "checked": false - }, - { - "id": "22", - "label": "Verification and risk plan created", - "checked": false - }, - { - "id": "23", - "label": "Restoration actions documented (Who/What/When)", - "checked": false - }, - { - "id": "24", - "label": "Service validated internally and externally", - "checked": false - }, - { - "id": "25", - "label": "Restoration comms sent", - "checked": false - }, - { - "id": "26", - "label": "Handover template prepared (PIR/problem)", - "checked": false - }, - { - "id": "27", - "label": "Downstream issues from fix assessed", - "checked": false - }, - { - "id": "28", - "label": "Participants released and bridge closed", - "checked": false - } - ], - "drawerOpen": false - }, - "actions": { - "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", - "items": [ - { - "id": "cb0076ab-2a5f-4508-8672-73f1ef9ff49f", - "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", - "createdAt": "2025-11-18T04:57:40.397Z", - "createdBy": "local", - "summary": "Understand vibration issues leading to Edger Failure/Release", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "High", - "dueAt": "", - "startedAt": "2025-11-18T04:57:42.073Z", - "completedAt": "2025-11-18T04:57:42.621Z", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "cause-dtnho5-mi43m5a9" - }, - "notes": "", - "auditTrail": [] - }, - { - "id": "7a883644-57d3-4204-99ad-c42b7312b27b", - "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", - "createdAt": "2025-11-18T04:57:53.067Z", - "createdBy": "local", - "summary": "Provide Options for a perm fix", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Med", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "cause-dtnho5-mi43m5a9" - }, - "notes": "", - "auditTrail": [] - }, - { - "id": "c933507a-d82e-4d17-b70e-de6b16148761", - "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", - "createdAt": "2025-11-18T04:57:13.778Z", - "createdBy": "local", - "summary": "Inspect the Proofing Tunnel", - "detail": "", - "owner": { - "name": "Engineering Assistant", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "local-user", - "lastAssignedAt": "2025-11-18T04:57:21.808Z", - "source": "Manual" - }, - "role": "", - "status": "Done", - "priority": "Med", - "dueAt": "", - "startedAt": "2025-11-18T04:57:26.030Z", - "completedAt": "2025-11-18T04:57:26.704Z", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "cause-dtnho5-mi43m5a9" - }, - "notes": "", - "auditTrail": [ - { - "type": "ownerChanged", - "at": "2025-11-18T04:57:21.808Z", - "by": "local-user", - "before": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "after": { - "name": "Engineering Assistant", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "local-user", - "lastAssignedAt": "2025-11-18T04:57:21.808Z", - "source": "Manual" - } - } - ] - }, - { - "id": "8086ff81-2004-4951-b875-2dd4328055b9", - "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", - "createdAt": "2025-11-18T04:56:57.888Z", - "createdBy": "local", - "summary": "Replace the screws on Row 1", - "detail": "", - "owner": { - "name": "Engineering Assistant", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "local-user", - "lastAssignedAt": "2025-11-18T04:57:04.726Z", - "source": "Manual" - }, - "role": "", - "status": "Done", - "priority": "Med", - "dueAt": "", - "startedAt": "2025-11-18T04:57:05.833Z", - "completedAt": "2025-11-18T04:57:06.404Z", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "cause-dtnho5-mi43m5a9" - }, - "notes": "", - "auditTrail": [ - { - "type": "ownerChanged", - "at": "2025-11-18T04:57:04.726Z", - "by": "local-user", - "before": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "after": { - "name": "Engineering Assistant", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "local-user", - "lastAssignedAt": "2025-11-18T04:57:04.726Z", - "source": "Manual" - } - } - ] - }, - { - "id": "27d65310-b2c4-4a59-b49a-bcbf52c30a1f", - "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", - "createdAt": "2025-11-18T04:56:16.699Z", - "createdBy": "local", - "summary": "Attempt to tighten the edgers on row 1", - "detail": "", - "owner": { - "name": "Engineering Assistant", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "local-user", - "lastAssignedAt": "2025-11-18T04:56:42.269Z", - "source": "Manual" - }, - "role": "", - "status": "Done", - "priority": "Med", - "dueAt": "", - "startedAt": "2025-11-18T04:56:18.930Z", - "completedAt": "2025-11-18T04:56:19.456Z", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "cause-dtnho5-mi43m5a9" - }, - "notes": "", - "auditTrail": [ - { - "type": "ownerChanged", - "at": "2025-11-18T04:56:42.269Z", - "by": "local-user", - "before": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "after": { - "name": "Engineering Assistant", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "local-user", - "lastAssignedAt": "2025-11-18T04:56:42.269Z", - "source": "Manual" - } - } - ] - } - ] - } - } - }, - { - "id": "tamworth-petrochemical", - "name": "Tamworth Petrochemical", - "description": "Customers are reporting an even faint fog across our sheet film products. \n\nWe have reproduced this with X-Ray successfully", - "supportedModes": [ - "intake", - "is-is-not", - "dc", - "full" - ], - "templateKind": "case-study", - "state": { - "meta": { - "version": 1, - "savedAt": "2025-11-19T00:43:10.430Z" - }, - "pre": { - "oneLine": "Tamworth Film is foggy", - "proof": "Customers are reporting an even faint fog across our sheet film products. \n\nWe have reproduced this with X-Ray successfully", - "objectPrefill": "Sheet Film", - "healthy": "The photograph as taken to be clear of any additional elements that were not captured at the time of photograph", - "now": "Fogged" - }, - "impact": { - "now": "682 out of 7000 Dealers are complaining\n\nSales Down\n\nReputation Lost\n\n2-for-1 Exchange Program in Effect", - "future": "We may risk being permanently branded as a poor-quality supplier\n\nDistributors may stop carrying our products", - "time": "" - }, - "ops": { - "bridgeOpenedUtc": "2025-11-19T00:27:46.306Z", - "icName": "", - "bcName": "", - "semOpsName": "", - "severity": "", - "detectMonitoring": false, - "detectUserReport": false, - "detectAutomation": false, - "detectOther": false, - "evScreenshot": true, - "evLogs": false, - "evMetrics": false, - "evRepro": true, - "evOther": false, - "containStatus": "stoppingImpact", - "containDesc": "", - "commCadence": "", - "commLog": [], - "commNextDueIso": "", - "commNextUpdateTime": "", - "tableFocusMode": "comprehensive" - }, - "table": [ - { - "band": "WHAT — Define the problem precisely (Object & Deviation)." - }, - { - "q": "WHAT — Specific Object/Thing is having the “Fogged”", - "is": "Hi-Grade 202\n\nSheet Film", - "no": "NMD - Competitors?\n\nRoll or Pack", - "di": "Must be handled in total darkness\n\nUses different packaging\n\nUses Interleaf paper", - "ch": "NKC\n\nNew Zip Top Package (5 Weeks ago)\n\nNew Interleaf Supplier (6 Months Ago)", - "questionId": "what-object" - }, - { - "q": "WHAT — Specific Deviation does the “Hi-Grade 202” have?", - "is": "Fogged", - "no": "Overexposed, underexposed, spots, streaks", - "di": "", - "ch": "", - "questionId": "what-deviation" - }, - { - "band": "WHERE — Locate the problem (geography/topology and on the object)." - }, - { - "q": "WHERE — is the “Hi-Grade 202” geographically/topology when the “Fogged” occurs?", - "is": "USA\n\nAll Sales Districts\n\nSmall Dealers\n\nDevelopment Rooms", - "no": "N/A\n\nSome Sales Districts\n\nLarge Dealers\n\nN/A", - "di": "Small dealers have slower inventory turnover", - "ch": "NKC", - "questionId": "where-location" - }, - { - "q": "WHERE — On the “Hi-Grade 202” is the “Fogged” observed?", - "is": "Even across the entire surface of the negative", - "no": "On edges, spots, or partial parts of the negative", - "di": "", - "ch": "", - "questionId": "where-on-object" - }, - { - "band": "WHEN — Timing and Description" - }, - { - "q": "WHEN — Was the “Fogged” First observed for “Hi-Grade 202”", - "is": "10+ Days Ago\n\nNMD - Reporting Delay", - "no": "Before 10+ Days Ago", - "di": "", - "ch": "", - "questionId": "when-first-observed" - }, - { - "q": "WHEN — Since the first occurrence has “Fogged” been logged? What Pattern?", - "is": "Daily / Continuous", - "no": "\nRandom, One-Time Event, Periodic", - "di": "", - "ch": "", - "questionId": "when-pattern" - }, - { - "q": "WHEN — Describe using words When the “Fogged” was first seen", - "is": "Immediately when developed", - "no": "Some time after the initial development", - "di": "", - "ch": "", - "questionId": "when-description" - }, - { - "band": "EXTENT — How big is it? Magnitude, count, scope, trend." - }, - { - "q": "EXTENT — What is the population or size of “Hi-Grade 202” affected?", - "is": "682 Dealers\n\nAll negatives in a box\n\nIncreasing", - "no": "> or < 682 Dealers\n\nSome of the Negatives in a box", - "di": "", - "ch": "", - "questionId": "extent-population" - }, - { - "q": "EXTENT — What is the size of a single “Fogged”?", - "is": "100% of the Negative is visibly fogged\n\nStable", - "no": "Less than 100%", - "di": "", - "ch": "", - "questionId": "extent-size" - }, - { - "q": "EXTENT — How many “Fogged” are occuring on each “Hi-Grade 202”?", - "is": "NMD - Seems like 1 Instance of Fogging per Sheet", - "no": "", - "di": "", - "ch": "", - "questionId": "extent-count" - } - ], - "causes": [ - { - "id": "cause-u2f2c1-mi59wwx8", - "suspect": "Doctors and Dentists", - "accusation": "Leaving their X-Ray Equipment On", - "impact": "causing X-Ray Radiation to contaminate our nearby products", - "summaryText": "We suspect Doctors and Dentists because Leaving their X-Ray Equipment On. This could lead to causing X-Ray Radiation to contaminate our nearby products.", - "confidence": "", - "evidence": "", - "findings": {}, - "editing": false, - "testingOpen": false - }, - { - "id": "cause-ix4hu6-mi59xkch", - "suspect": "Nuclear", - "accusation": "Fallout", - "impact": "causing radiation exposure to our sensitive products (Hi-Grade 202)", - "summaryText": "We suspect Nuclear because Fallout. This could lead to causing radiation exposure to our sensitive products (Hi-Grade 202).", - "confidence": "", - "evidence": "", - "findings": {}, - "editing": false, - "testingOpen": false - }, - { - "id": "cause-xlxgqi-mi59y76z", - "suspect": "Shipping Company", - "accusation": "Route Change", - "impact": "Leading to exposure to radiation prone areas of the US", - "summaryText": "We suspect Shipping Company because Route Change. This could lead to Leading to exposure to radiation prone areas of the US.", - "confidence": "", - "evidence": "", - "findings": {}, - "editing": false, - "testingOpen": false - }, - { - "id": "cause-dv4oln-mi59ymxw", - "suspect": "Interleaf Paper", - "accusation": "Contaminated", - "impact": "Exposing the film prematurely", - "summaryText": "We suspect Interleaf Paper because Contaminated. This could lead to Exposing the film prematurely.", - "confidence": "", - "evidence": "", - "findings": {}, - "editing": false, - "testingOpen": false - }, - { - "id": "cause-fgg1y5-mi59z56k", - "suspect": "Amateurs", - "accusation": "Unable to handle the film correctly", - "impact": "Exposing it incorrectly when putting it in camera", - "summaryText": "We suspect Amateurs because Unable to handle the film correctly. This could lead to Exposing it incorrectly when putting it in camera.", - "confidence": "", - "evidence": "", - "findings": {}, - "editing": false, - "testingOpen": false - } - ], - "likelyCauseId": "cause-dv4oln-mi59ymxw", - "steps": { - "items": [ - { - "id": "1", - "label": "Pre-analysis completed", - "checked": false - }, - { - "id": "2", - "label": "Incident Commander assigned", - "checked": false - }, - { - "id": "4", - "label": "Problem statement created", - "checked": false - }, - { - "id": "5", - "label": "Bridge options considered", - "checked": false - }, - { - "id": "6", - "label": "Bridges opened and responders invited", - "checked": false - }, - { - "id": "7", - "label": "Bridge etiquette, roles, and guidelines outlined", - "checked": false - }, - { - "id": "8", - "label": "Current actions documented (Who/What/When)", - "checked": false - }, - { - "id": "9", - "label": "Quick spec answers captured", - "checked": false - }, - { - "id": "10", - "label": "Last changes, monitoring, and dependencies investigated", - "checked": false - }, - { - "id": "11", - "label": "Attendees optimized", - "checked": false - }, - { - "id": "12", - "label": "Possible causes developed", - "checked": false - }, - { - "id": "13", - "label": "Testing actions documented (Who/What/When)", - "checked": false - }, - { - "id": "14", - "label": "Micro-experiments/tests conducted", - "checked": false - }, - { - "id": "15", - "label": "Containment options identified", - "checked": false - }, - { - "id": "16", - "label": "Comms written, reviewed, and sent", - "checked": false - }, - { - "id": "17", - "label": "Attendees optimized for next action", - "checked": false - }, - { - "id": "18", - "label": "Possible causes evaluated and distinctions identified", - "checked": false - }, - { - "id": "19", - "label": "Most probable cause identified", - "checked": false - }, - { - "id": "20", - "label": "Restoration/rollback/workaround selected", - "checked": false - }, - { - "id": "21", - "label": "Attendees optimized for decision", - "checked": false - }, - { - "id": "22", - "label": "Verification and risk plan created", - "checked": false - }, - { - "id": "23", - "label": "Restoration actions documented (Who/What/When)", - "checked": false - }, - { - "id": "24", - "label": "Service validated internally and externally", - "checked": false - }, - { - "id": "25", - "label": "Restoration comms sent", - "checked": false - }, - { - "id": "26", - "label": "Handover template prepared (PIR/problem)", - "checked": false - }, - { - "id": "27", - "label": "Downstream issues from fix assessed", - "checked": false - }, - { - "id": "28", - "label": "Participants released and bridge closed", - "checked": false - } - ], - "drawerOpen": false - }, - "actions": { - "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", - "items": [ - { - "id": "8158d4af-a8ac-4f3a-a5a5-a9dc1264a6ee", - "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", - "createdAt": "2025-11-19T00:42:34.771Z", - "createdBy": "local", - "summary": "Double the Marketing Budget", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Med", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "" - }, - "notes": "", - "auditTrail": [] - }, - { - "id": "6db1d3f4-71a6-4826-8fdb-072f18ab77a9", - "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", - "createdAt": "2025-11-19T00:42:01.031Z", - "createdBy": "local", - "summary": "Perform Chemical Analysis on Interleaf Paper to see if it is contaminated with something", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Med", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "cause-dv4oln-mi59ymxw" - }, - "notes": "", - "auditTrail": [] - }, - { - "id": "b6f59196-26fe-4e24-b448-95051efc7eba", - "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", - "createdAt": "2025-11-19T00:41:31.726Z", - "createdBy": "local", - "summary": "Ask shipping company about any security screenings they are now doing", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Med", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "cause-xlxgqi-mi59y76z" - }, - "notes": "", - "auditTrail": [] - }, - { - "id": "955ebc23-c7ed-4f07-82d2-e2cae57c9c52", - "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", - "createdAt": "2025-11-19T00:41:12.174Z", - "createdBy": "local", - "summary": "Map out precipitation across the USA and determine if it matches the dealer pattern of complaints", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Med", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "cause-ix4hu6-mi59xkch" - }, - "notes": "", - "auditTrail": [] - }, - { - "id": "4561a608-36c7-42fe-aae1-461baa33c060", - "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", - "createdAt": "2025-11-19T00:40:35.413Z", - "createdBy": "local", - "summary": "Visit Local Doctors and Dentist offices to verify that their equipment is not malfunctioning", - "detail": "", - "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" - }, - "role": "", - "status": "Planned", - "priority": "Med", - "dueAt": "", - "startedAt": "", - "completedAt": "", - "dependencies": [], - "risk": "None", - "changeControl": { - "required": false - }, - "verification": { - "required": false - }, - "links": { - "hypothesisId": "cause-u2f2c1-mi59wwx8" - }, - "notes": "", - "auditTrail": [] - }, - { - "id": "2b9023c5-2758-4652-88cf-f1091de72637", - "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", - "createdAt": "2025-11-19T00:40:10.792Z", - "createdBy": "local", - "summary": "Watch someone try to use our film and see if they are doing it incorrectly", - "detail": "", + "id": "action-merchant-brief", + "analysisId": "template-checkout", + "createdAt": "2024-03-08T08:05:00Z", + "createdBy": "TemplateBot", + "summary": "Brief top merchants and regulators.", + "detail": "Share impact summary, containment ETA, and request extension on EU SLA.", "owner": { - "name": "", - "category": "", - "subOwner": "", - "notes": "", - "lastAssignedBy": "", - "lastAssignedAt": "", - "source": "Manual" + "name": "Merchant Success", + "category": "BUSINESS_CUSTOMER", + "subOwner": "BUSINESS_CUSTOMER__BUSINESS_RELATIONSHIP", + "notes": "Coordinating with exec sponsor.", + "lastAssignedBy": "TemplateBot", + "lastAssignedAt": "2024-03-08T08:06:00Z", + "source": "Template" }, - "role": "", + "role": "BC Comms", "status": "Planned", "priority": "Med", - "dueAt": "", + "dueAt": "2024-03-08T08:30:00Z", "startedAt": "", "completedAt": "", "dependencies": [], - "risk": "None", + "risk": "Medium", "changeControl": { "required": false }, "verification": { "required": false }, - "links": { - "hypothesisId": "cause-fgg1y5-mi59z56k" - }, - "notes": "", + "links": {}, + "notes": "Prep template for EU regulator alias.", "auditTrail": [] } ] From 1d4cbe67ff35161cc25808eb293f761f026d07aa Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:52:33 -0400 Subject: [PATCH 004/100] build: add protected Case Study manifest --- api/protected-case-studies.manifest.js | 2440 ++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 2441 insertions(+), 1 deletion(-) create mode 100644 api/protected-case-studies.manifest.js diff --git a/api/protected-case-studies.manifest.js b/api/protected-case-studies.manifest.js new file mode 100644 index 0000000..629c743 --- /dev/null +++ b/api/protected-case-studies.manifest.js @@ -0,0 +1,2440 @@ +/** + * AUTO-GENERATED FILE. DO NOT EDIT DIRECTLY. + * Generated by scripts/build-templates-manifest.mjs + * SERVER-ONLY MANIFEST: protected Case Study metadata and payloads. Never import from browser runtime modules. + */ + +function deepFreeze(value) { + if (Array.isArray(value)) { + value.forEach(deepFreeze); + } else if (value && typeof value === 'object') { + Object.values(value).forEach(deepFreeze); + } + return Object.freeze(value); +} + +const manifest = [ + { + "id": "microcomputer-cabinets", + "name": "Microcomputer Cabinets", + "description": "Final Inspection Reports that cabinets are being rejected periodically", + "templateKind": "case-study", + "supportedModes": [ + "intake", + "is-is-not", + "dc", + "full" + ], + "state": { + "meta": { + "version": 1, + "savedAt": "2025-11-18T18:57:08.496Z" + }, + "pre": { + "oneLine": "Cabinets are being rejected", + "proof": "Final Inspection Reports that cabinets are being rejected periodically", + "objectPrefill": "1025 Microcomputer Cabinets", + "healthy": "Flat, even coat of paint on the outside surface", + "now": "Paint Gaps" + }, + "impact": { + "now": "Total reject rate is hovering at 6% \n\nwe are manually re-working cabinets with paint gaps", + "future": "production delays and out-of-stock notices may have to go out due to shortage of 1025 Cabinets", + "time": "Next Week sales are expected to be increasing along with production rate" + }, + "ops": { + "bridgeOpenedUtc": "2025-11-18T09:04:02.398Z", + "icName": "", + "bcName": "", + "semOpsName": "", + "severity": "", + "detectMonitoring": false, + "detectUserReport": true, + "detectAutomation": false, + "detectOther": false, + "evScreenshot": false, + "evLogs": false, + "evMetrics": false, + "evRepro": false, + "evOther": false, + "containStatus": "stabilized", + "containDesc": "Manually reworking current cabinets", + "commCadence": "", + "commLog": [], + "commNextDueIso": "", + "commNextUpdateTime": "", + "tableFocusMode": "comprehensive" + }, + "table": [ + { + "band": "WHAT — Define the problem precisely (Object & Deviation)." + }, + { + "q": "WHAT — Specific Object/Thing is having the “Visual Defects”", + "is": "1025 Microcomputer Cabinets", + "no": "1035, 1045 Cabinets", + "di": "1025s are cleaned in Line 1 Wash Tanks\n\n1025s are assembled in Line 1 Final Assembly\n\n1025s have a higher production rate than the 1035 and 1045 combined", + "ch": "More Cleaned on Line 1 (Weekly over last 3 weeks)\n\nWash tanks Flushed Every 30 Minutes\n\nMore assembled on Line 1 (Weekly over last 3 weeks) \n\nNew Employees (5 Weeks Ago)", + "questionId": "what-object" + }, + { + "q": "WHAT — Specific Deviation does the “1025 Microcomputer Cabinets” have?", + "is": "Visual Defects\n\nPaint Gaps", + "no": "Functional, Electronic Defects\n\nPaint Peeling, Discolouring, Running, Fading", + "di": "", + "ch": "", + "questionId": "what-deviation" + }, + { + "band": "WHERE — Locate the problem (geography/topology and on the object)." + }, + { + "q": "WHERE — is the “1025 Microcomputer Cabinets” geographically/topology when the “Visual Defects” occurs?", + "is": "Reported at Final Inspection", + "no": "Reported at Final Assembly", + "di": "", + "ch": "", + "questionId": "where-location" + }, + { + "q": "WHERE — On the “1025 Microcomputer Cabinets” is the “Visual Defects” observed?", + "is": "All Panels\nOutside Panels\nRandomly", + "no": "Some Panels\n(NMD - Inside?)\nIn Specific Spots or Pattern", + "di": "", + "ch": "", + "questionId": "where-on-object" + }, + { + "band": "WHEN — Timing and Description" + }, + { + "q": "WHEN — Was the “Visual Defects” First observed for “1025 Microcomputer Cabinets”", + "is": "2 Weeks Ago", + "no": "Before 2 Weeks Ago", + "di": "Production Rate", + "ch": "Production Increased (Weekly over last 3 weeks)", + "questionId": "when-first-observed" + }, + { + "q": "WHEN — Since the first occurrence has “Visual Defects” been logged? What Pattern?", + "is": "NMD - Time\n\"Periodic\"\n", + "no": "NMD - Time\nSporadic, Continuous", + "di": "", + "ch": "", + "questionId": "when-pattern" + }, + { + "q": "WHEN — Describe using words When the “Visual Defects” was first seen", + "is": "During Visual Inspection", + "no": "During Final Assembly, Electronic, or Operational Inspection", + "di": "", + "ch": "", + "questionId": "when-description" + }, + { + "band": "EXTENT — How big is it? Magnitude, count, scope, trend." + }, + { + "q": "EXTENT — What is the population or size of “1025 Microcomputer Cabinets” affected?", + "is": "Currently Over 5.85%\n\nIncreasing", + "no": "0.16% - 5.84% and\n5.86% - 100%", + "di": "", + "ch": "", + "questionId": "extent-population" + }, + { + "q": "EXTENT — What is the size of a single “Visual Defects”?", + "is": "", + "no": "", + "di": "", + "ch": "", + "questionId": "extent-size" + }, + { + "q": "EXTENT — How many “Visual Defects” are occuring on each “1025 Microcomputer Cabinets”?", + "is": "Up to 11 Gaps per cabinet\n\nIncreasing", + "no": "Any other range or single value", + "di": "", + "ch": "", + "questionId": "extent-count" + } + ], + "causes": [ + { + "id": "cause-2h25uu-mi4xnini", + "suspect": "Employees", + "accusation": "Wearing Unapproved Hand Cream", + "impact": "Leaving a Film on the Cabinet that prevents paint from adhering", + "summaryText": "We suspect Employees because Wearing Unapproved Hand Cream. This could lead to Leaving a Film on the Cabinet that prevents paint from adhering.", + "confidence": "", + "evidence": "", + "findings": {}, + "editing": false, + "testingOpen": false + }, + { + "id": "cause-2u9pmq-mi4xo7ef", + "suspect": "Wash Tanks", + "accusation": "Contaminated", + "impact": "Leaving Dirt on Cabinets", + "summaryText": "We suspect Wash Tanks because Contaminated. This could lead to Leaving Dirt on Cabinets.", + "confidence": "", + "evidence": "", + "findings": {}, + "editing": false, + "testingOpen": false + }, + { + "id": "cause-wjmudz-mi4xohef", + "suspect": "Grounding Hooks", + "accusation": "Not Properly Grounded", + "impact": "Preventing Paint Adhesion", + "summaryText": "We suspect Grounding Hooks because Not Properly Grounded. This could lead to Preventing Paint Adhesion.", + "confidence": "", + "evidence": "", + "findings": {}, + "editing": false, + "testingOpen": false + } + ], + "likelyCauseId": "cause-2h25uu-mi4xnini", + "steps": { + "items": [ + { + "id": "1", + "label": "Pre-analysis completed", + "checked": false + }, + { + "id": "2", + "label": "Incident Commander assigned", + "checked": false + }, + { + "id": "4", + "label": "Problem statement created", + "checked": false + }, + { + "id": "5", + "label": "Bridge options considered", + "checked": false + }, + { + "id": "6", + "label": "Bridges opened and responders invited", + "checked": false + }, + { + "id": "7", + "label": "Bridge etiquette, roles, and guidelines outlined", + "checked": false + }, + { + "id": "8", + "label": "Current actions documented (Who/What/When)", + "checked": false + }, + { + "id": "9", + "label": "Quick spec answers captured", + "checked": false + }, + { + "id": "10", + "label": "Last changes, monitoring, and dependencies investigated", + "checked": false + }, + { + "id": "11", + "label": "Attendees optimized", + "checked": false + }, + { + "id": "12", + "label": "Possible causes developed", + "checked": false + }, + { + "id": "13", + "label": "Testing actions documented (Who/What/When)", + "checked": false + }, + { + "id": "14", + "label": "Micro-experiments/tests conducted", + "checked": false + }, + { + "id": "15", + "label": "Containment options identified", + "checked": false + }, + { + "id": "16", + "label": "Comms written, reviewed, and sent", + "checked": false + }, + { + "id": "17", + "label": "Attendees optimized for next action", + "checked": false + }, + { + "id": "18", + "label": "Possible causes evaluated and distinctions identified", + "checked": false + }, + { + "id": "19", + "label": "Most probable cause identified", + "checked": false + }, + { + "id": "20", + "label": "Restoration/rollback/workaround selected", + "checked": false + }, + { + "id": "21", + "label": "Attendees optimized for decision", + "checked": false + }, + { + "id": "22", + "label": "Verification and risk plan created", + "checked": false + }, + { + "id": "23", + "label": "Restoration actions documented (Who/What/When)", + "checked": false + }, + { + "id": "24", + "label": "Service validated internally and externally", + "checked": false + }, + { + "id": "25", + "label": "Restoration comms sent", + "checked": false + }, + { + "id": "26", + "label": "Handover template prepared (PIR/problem)", + "checked": false + }, + { + "id": "27", + "label": "Downstream issues from fix assessed", + "checked": false + }, + { + "id": "28", + "label": "Participants released and bridge closed", + "checked": false + } + ], + "drawerOpen": false + }, + "actions": { + "analysisId": "analysis-ea2a2d69-11c9-4bf7-8c6f-301d64b430cf", + "items": [ + { + "id": "51870a36-3501-4af0-aa28-c5e0eca4b9b6", + "analysisId": "analysis-ea2a2d69-11c9-4bf7-8c6f-301d64b430cf", + "createdAt": "2025-11-18T18:55:55.571Z", + "createdBy": "local", + "summary": "Start Data logging to understand the period of the issue", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "High", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": {}, + "notes": "", + "auditTrail": [] + }, + { + "id": "3ab35131-55ca-4661-b847-45a034649c18", + "analysisId": "analysis-ea2a2d69-11c9-4bf7-8c6f-301d64b430cf", + "createdAt": "2025-11-18T18:56:18.157Z", + "createdBy": "local", + "summary": "Purchase Gloves and Begin Mandate for All Employees to be Gloved", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-2h25uu-mi4xnini" + }, + "notes": "", + "auditTrail": [] + }, + { + "id": "669ca6a2-4b0a-421c-84fa-e54e16097e56", + "analysisId": "analysis-ea2a2d69-11c9-4bf7-8c6f-301d64b430cf", + "createdAt": "2025-11-18T18:56:03.381Z", + "createdBy": "local", + "summary": "Examine Wash Tanks for Contaminents", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-2u9pmq-mi4xo7ef" + }, + "notes": "", + "auditTrail": [] + } + ] + } + } + }, + { + "id": "paytrix", + "name": "Paytrix", + "description": "Chaos at Paytrix", + "templateKind": "case-study", + "supportedModes": [ + "intake", + "is-is-not", + "dc", + "full" + ], + "state": { + "meta": { + "version": 1, + "savedAt": "2025-12-04T21:37:48.348Z" + }, + "appearance": { + "theme": "light" + }, + "pre": { + "oneLine": "AuthorizationService (NA-East) is experiencing sustained elevated p95 latency above baseline.", + "proof": "Detection Sources:\nMonitoring (Datadog)\nUser Report (Amazon, Delta Airlines)\nAutomation (xMatters Sev-1 rule)\n\nEvidence Collected:\nMetrics screenshots from Datadog\nxMatters high-priority alert\nMerchant emails (Amazon, Delta)\nInternal Teams chatter confirming latency and 504s\nServiceNow auto-Sev1 ticket\n", + "objectPrefill": "AuthService — NA-East regional shard (Authorization path only).", + "healthy": "Authorization p95 latency ~95–150ms\n\nTimeout rate < 0.3%\n\nNo sustained regional deviation\n\nMajor merchants reporting normal operation\n\nHealth checks passing within expected thresholds", + "now": "p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior." + }, + "impact": { + "now": "Merchants in NA-East reporting payment delays/timeouts\n\nAmazon retry rate 5–7%\n\nDelta kiosks unable to process payments\n\nMerchantGateway experiencing elevated 504 responses\n\nIncreased abandonment in retail checkouts", + "future": "Retry storms may propagate load and degrade other regions (NA-West, SA)\n\nSLO/SLA breach likely within minutes\n\nPotential throttling by downstream issuers\n\nEscalation to global merchant base (churn & reputational risk)\n\nOn-call fatigue and SME overload if the incident expands", + "time": "Impact is active now\n\nExpected to worsen within 5–10 minutes if no containment initiated\n\nMerchant dissatisfaction already emerging" + }, + "ops": { + "bridgeOpenedUtc": "2025-12-03T16:40:01.816Z", + "icName": "Jordan Reyes", + "bcName": "Shane C", + "semOpsName": "", + "severity": "S1", + "detectMonitoring": true, + "detectUserReport": true, + "detectAutomation": true, + "detectOther": false, + "evScreenshot": false, + "evLogs": false, + "evMetrics": false, + "evRepro": false, + "evOther": false, + "containStatus": "assessing", + "containDesc": "Pause non-critical MerchantGateway transactions impacting Auth until traffic stabilizes. Validate with SME before execution.", + "commCadence": "", + "commLog": [], + "commNextDueIso": "", + "commNextUpdateTime": "", + "tableFocusMode": "comprehensive" + }, + "table": [ + { + "band": "WHAT — Define the problem precisely (Object & Deviation)." + }, + { + "q": "WHAT — Specific Object/Thing is having the “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.”", + "is": "AuthService — NA-East regional shard (Authorization path only).", + "no": "AuthService — NA-West\nAuthService — EU shard\nFraudEdge precheck service\nMerchantGateway service\nRedis cluster\nNetwork transport layer", + "di": "NA-East Authorization uses a different logic branch due to region-specific config.", + "ch": "Region-specific override for adaptive risk scoring was enabled yesterday afternoon, but only for NA-East.", + "questionId": "what-object" + }, + { + "q": "WHAT — Specific Deviation does the “AuthService — NA-East regional shard (Authorization path only).” have?", + "is": "p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.", + "no": "Authorization service is down\nAuthorization service is returning zero responses\nAuthorization errors are increasing across regions\nFraudEdge latency is elevated\nMerchantGateway latency is elevated", + "di": "", + "ch": "", + "questionId": "what-deviation" + }, + { + "band": "WHERE — Locate the problem (geography/topology and on the object)." + }, + { + "q": "WHERE — is the “AuthService — NA-East regional shard (Authorization path only).” geographically/topology when the “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” occurs?", + "is": "NA-East region (all AZs).", + "no": "NA-West\nEU region\nSouth America region\nSandbox / non-production", + "di": "NA-East is uniquely under promo-concentrated load, making it the only region able to trigger the new logic branch.", + "ch": "Traffic steering policy shifted heavier Amazon promo traffic to NA-East earlier this week.", + "questionId": "where-location" + }, + { + "q": "WHERE — On the “AuthService — NA-East regional shard (Authorization path only).” is the “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” observed?", + "is": "Authorization path within AuthService.\nPoints where MerchantGateway calls AuthService (responses originating from AuthService in NA-East).", + "no": "TokenService path\nFraudEdge decision path\nRedis lookup path\nKafka publish/subscribe layers", + "di": "", + "ch": "", + "questionId": "where-on-object" + }, + { + "band": "WHEN — Timing and Description" + }, + { + "q": "WHEN — Was the “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” First observed for “AuthService — NA-East regional shard (Authorization path only).”", + "is": "Around 10:03 AM ET.", + "no": "10:01 AM Dev Datadog annotation\nEarlier EU event referenced in Slack\n9:55 AM MerchantGateway config push", + "di": "", + "ch": "", + "questionId": "when-first-observed" + }, + { + "q": "WHEN — Since the first occurrence has “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” been logged? What Pattern?", + "is": "Sustained elevation for several minutes (3–4+).\nNot intermittent.", + "no": "Periodic pattern\nImproving trend\nLimited to a single AZ\nAlternating across regions", + "di": "", + "ch": "", + "questionId": "when-pattern" + }, + { + "q": "WHEN — Describe using words When the “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” was first seen", + "is": "During active authorization traffic.\nDuring morning merchant peak (Amazon promo surge).", + "no": "During a deploy window\nDuring planned maintenance\nDuring a failover event", + "di": "The deviation starts when the traffic threshold is crossed, not when the deploy or config change happened.", + "ch": "Threshold parameters updated at 9:58; threshold crossed at 10:03.", + "questionId": "when-description" + }, + { + "band": "EXTENT — How big is it? Magnitude, count, scope, trend." + }, + { + "q": "EXTENT — What is the population or size of “AuthService — NA-East regional shard (Authorization path only).” affected?", + "is": "All AuthService nodes in NA-East.\nAll AZs in NA-East.", + "no": "Nodes in NA-West\nNodes in EU\nNodes in staging/sandbox", + "di": "", + "ch": "", + "questionId": "extent-population" + }, + { + "q": "EXTENT — What is the size of a single “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.”?", + "is": "p95 latency elevated to 500–700ms.", + "no": "Multi-second total stalls (e.g., 3–5 seconds unresponsive)\nTotal outage (zero responses)\nCPU/memory exhaustion", + "di": "", + "ch": "", + "questionId": "extent-size" + }, + { + "q": "EXTENT — How many “p95 authorization latency is elevated to 500–700ms and sustained beyond normal behavior.” are occuring on each “AuthService — NA-East regional shard (Authorization path only).”?", + "is": "Continuous elevated latency across all NA-East nodes.\nPresent across multiple merchant flows (Amazon, Delta).", + "no": "Single-node issue\nSingle-merchant issue\nShort-lived blip", + "di": "", + "ch": "", + "questionId": "extent-count" + } + ], + "causes": [ + { + "id": "cause-upi5es-mirvp5em", + "suspect": "Adaptive risk scoring logic in NA-East Authorization path", + "accusation": "adds overhead under threshold-triggered conditions", + "impact": "load causing latency to spike", + "summaryText": "We suspect Adaptive risk scoring logic in NA-East Authorization path is adds overhead under threshold-triggered conditions. This could lead to load causing latency to spike.", + "confidence": "", + "evidence": "", + "findings": { + "WHAT — Specific Object/Thing is having the {DEVIATION}": { + "mode": "yes", + "note": "The region-specific config override exists only in NA-East Authorization path (matches IS). No other region or service uses that branch (matches IS NOT)." + }, + "WHAT — Specific Deviation does the {OBJECT} have?": { + "mode": "yes", + "note": "Extra processing overhead from the logic branch explains latency elevation without causing outages or multi-region failures." + }, + "WHERE — is the {OBJECT} geographically/topology when the {DEVIATION} occurs?": { + "mode": "yes", + "note": "Region-specific override applies to NA-East only (matches IS). Other regions do not use override or hit threshold (matches IS NOT)." + }, + "WHERE — On the {OBJECT} is the {DEVIATION} observed?": { + "mode": "yes", + "note": "Only the Authorization handler uses the adaptive scoring logic; other handlers do not. Exact alignment." + }, + "WHEN — Was the {DEVIATION} First observed for {OBJECT}": { + "mode": "yes", + "note": "Threshold for adaptive scoring activated only once traffic crossed at 10:03. Prior times show no effect (aligned to IS NOT)." + }, + "WHEN — Since the first occurrence has {DEVIATION} been logged? What Pattern?": { + "mode": "yes", + "note": "Flag logic would increase compute consistently under load; region-wide config makes pattern uniform across AZs." + }, + "WHEN — Describe using words When the {DEVIATION} was first seen": { + "mode": "yes", + "note": "Threshold-based scoring triggers under high traffic (IS). It is not tied to deploy/maintenance/failover (IS NOT)." + }, + "EXTENT — What is the population or size of {OBJECT} affected?": { + "mode": "yes", + "note": "Region-wide override applied to every node in NA-East. Other regions not affected." + }, + "EXTENT — What is the size of a single {DEVIATION}?": { + "mode": "yes", + "note": "Feature overhead produces sub-second increases, not multisecond stalls or outages." + }, + "EXTENT — How many {DEVIATION} are occuring on each {OBJECT}?": { + "mode": "yes", + "note": "Region-wide threshold logic affects all AZs and all merchants routed through NA-East. No contradictions." + } + }, + "editing": false, + "testingOpen": false + }, + { + "id": "cause-vakmx1-mirvrepn", + "suspect": "MerchantGateway retry configuration", + "accusation": "too aggressive", + "impact": "amplifies latency during authorization degradation", + "summaryText": "We suspect MerchantGateway retry configuration because of too aggressive. This could lead to amplifies latency during authorization degradation.", + "confidence": "", + "evidence": "", + "findings": { + "WHAT — Specific Object/Thing is having the {DEVIATION}": { + "mode": "assumption", + "note": "Retries could amplify latency on the auth path only if AuthService is already slow (assumption required). Cannot explain NA-East-specific config difference." + }, + "WHAT — Specific Deviation does the {OBJECT} have?": { + "mode": "assumption", + "note": "Retries can push latency to 500–700ms only if upstream (AuthService) slowdown begins first." + }, + "WHERE — is the {OBJECT} geographically/topology when the {DEVIATION} occurs?": { + "mode": "assumption", + "note": "MGW retry behavior is global; only NA-East sees impact if NA-East responds slower than other regions. Assumption needed." + }, + "WHERE — On the {OBJECT} is the {DEVIATION} observed?": { + "mode": "assumption", + "note": "MGW retries affect only paths calling AuthService; but cannot independently explain why only Authorization handler is slow. Requires assumption." + }, + "WHEN — Was the {DEVIATION} First observed for {OBJECT}": { + "mode": "assumption", + "note": "Retries spike at exactly 10:03 because AuthService slowed. This fits only if another cause degraded AuthService first." + }, + "WHEN — Since the first occurrence has {DEVIATION} been logged? What Pattern?": { + "mode": "yes", + "note": "Retry storms can produce sustained latency (fits IS)." + }, + "WHEN — Describe using words When the {DEVIATION} was first seen": { + "mode": "assumption", + "note": "Retries align to promo surge only if AuthService degraded at the same time." + }, + "EXTENT — What is the population or size of {OBJECT} affected?": { + "mode": "assumption", + "note": "Would affect all NA-East nodes if MGW retries hit all endpoints uniformly; requires assumption." + }, + "EXTENT — What is the size of a single {DEVIATION}?": { + "mode": "assumption", + "note": "Retry amplification can produce 500–700ms range, but requires AuthService to be marginally slow first." + }, + "EXTENT — How many {DEVIATION} are occuring on each {OBJECT}?": { + "mode": "assumption", + "note": "Continuous retries align with continuous upstream latency if AuthService remains slow." + } + }, + "editing": false, + "testingOpen": false + }, + { + "id": "cause-gcz67n-mirvtxi6", + "suspect": "Redis routing", + "accusation": "intermittently resolves to a cross-region shard under load", + "impact": "the round-trip time delay to request and get a response from the remote shard increases latency", + "summaryText": "We suspect Redis routing is intermittently resolves to a cross-region shard under load. This could lead to the round-trip time delay to request and get a response from the remote shard increases latency.", + "confidence": "", + "evidence": "", + "findings": { + "WHAT — Specific Object/Thing is having the {DEVIATION}": { + "mode": "fail", + "note": "Redis misrouting would affect TokenService, FraudEdge, and other paths — but IS says they’re normal. Contradiction." + }, + "WHAT — Specific Deviation does the {OBJECT} have?": { + "mode": "fail", + "note": "Cross-region Redis calls cause multi-second stalls or intermittent spikes — NOT uniform 500–700ms elevation." + }, + "WHERE — is the {OBJECT} geographically/topology when the {DEVIATION} occurs?": { + "mode": "fail", + "note": "Redis routing bugs rarely hit an entire region uniformly; would show uneven AZ impacts, contradicting IS." + }, + "WHERE — On the {OBJECT} is the {DEVIATION} observed?": { + "mode": "fail", + "note": "Redis routing would affect multiple paths beyond Authorization (contradicting IS NOT entries)." + }, + "WHEN — Was the {DEVIATION} First observed for {OBJECT}": { + "mode": "fail", + "note": "Redis issues do not align precisely with a 10:03 AM threshold event; mismatch with IS." + }, + "WHEN — Since the first occurrence has {DEVIATION} been logged? What Pattern?": { + "mode": "fail", + "note": "Redis problems are intermittent or jittery — contradicts sustained pattern." + }, + "WHEN — Describe using words When the {DEVIATION} was first seen": { + "mode": "fail", + "note": "Redis issues do not correlate to promo surges (IS), nor to deploy/maintenance windows (IS NOT). No alignment." + }, + "EXTENT — What is the population or size of {OBJECT} affected?": { + "mode": "fail", + "note": "Cross-region Redis calls would not hit all NA-East nodes uniformly — contradicts IS." + }, + "EXTENT — What is the size of a single {DEVIATION}?": { + "mode": "fail", + "note": "Remote shard resolution would exceed 1–3 seconds or time out entirely — contradicts IS 500–700ms envelope." + }, + "EXTENT — How many {DEVIATION} are occuring on each {OBJECT}?": { + "mode": "fail", + "note": "Redis issues would be uneven, merchant-specific, or node-specific — contradicts continuous all-node pattern." + } + }, + "editing": false, + "testingOpen": false + } + ], + "likelyCauseId": "cause-upi5es-mirvp5em", + "steps": { + "items": [ + { + "id": "1", + "label": "Pre-analysis completed", + "checked": false + }, + { + "id": "2", + "label": "Incident Commander assigned", + "checked": false + }, + { + "id": "4", + "label": "Problem statement created", + "checked": false + }, + { + "id": "5", + "label": "Bridge options considered", + "checked": false + }, + { + "id": "6", + "label": "Bridges opened and responders invited", + "checked": false + }, + { + "id": "7", + "label": "Bridge etiquette, roles, and guidelines outlined", + "checked": false + }, + { + "id": "8", + "label": "Current actions documented (Who/What/When)", + "checked": false + }, + { + "id": "9", + "label": "Quick spec answers captured", + "checked": false + }, + { + "id": "10", + "label": "Last changes, monitoring, and dependencies investigated", + "checked": false + }, + { + "id": "11", + "label": "Attendees optimized", + "checked": false + }, + { + "id": "12", + "label": "Possible causes developed", + "checked": false + }, + { + "id": "13", + "label": "Testing actions documented (Who/What/When)", + "checked": false + }, + { + "id": "14", + "label": "Micro-experiments/tests conducted", + "checked": false + }, + { + "id": "15", + "label": "Containment options identified", + "checked": false + }, + { + "id": "16", + "label": "Comms written, reviewed, and sent", + "checked": false + }, + { + "id": "17", + "label": "Attendees optimized for next action", + "checked": false + }, + { + "id": "18", + "label": "Possible causes evaluated and distinctions identified", + "checked": false + }, + { + "id": "19", + "label": "Most probable cause identified", + "checked": false + }, + { + "id": "20", + "label": "Restoration/rollback/workaround selected", + "checked": false + }, + { + "id": "21", + "label": "Attendees optimized for decision", + "checked": false + }, + { + "id": "22", + "label": "Verification and risk plan created", + "checked": false + }, + { + "id": "23", + "label": "Restoration actions documented (Who/What/When)", + "checked": false + }, + { + "id": "24", + "label": "Service validated internally and externally", + "checked": false + }, + { + "id": "25", + "label": "Restoration comms sent", + "checked": false + }, + { + "id": "26", + "label": "Handover template prepared (PIR/problem)", + "checked": false + }, + { + "id": "27", + "label": "Downstream issues from fix assessed", + "checked": false + }, + { + "id": "28", + "label": "Participants released and bridge closed", + "checked": false + } + ], + "drawerOpen": false + }, + "actions": { + "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", + "items": [ + { + "id": "729fc1ee-6d38-4a38-b6ad-513aea372c69", + "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", + "createdAt": "2025-12-03T17:08:52.403Z", + "createdBy": "local", + "summary": "Verify latency across all AZs in NA-East (are all AZs affected or only one?)", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "High", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": { + "level": "None", + "impactIfFails": "", + "prevent": "", + "ifHappens": "" + }, + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": {}, + "notes": "", + "auditTrail": [] + }, + { + "id": "7d1a2e39-f578-497e-9c76-9f403ceb7048", + "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", + "createdAt": "2025-12-03T17:08:58.929Z", + "createdBy": "local", + "summary": "Check regional parity (NA-West, EU-Central): confirm this is isolated.", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "High", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": { + "level": "None", + "impactIfFails": "", + "prevent": "", + "ifHappens": "" + }, + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": {}, + "notes": "", + "auditTrail": [] + }, + { + "id": "b6d6fe21-e590-4784-abc6-6cb24a16d0f8", + "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", + "createdAt": "2025-12-03T17:09:41.392Z", + "createdBy": "local", + "summary": "Retrieve last-known-good window for AuthService from Datadog.", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "High", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": { + "level": "None", + "impactIfFails": "", + "prevent": "", + "ifHappens": "" + }, + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": {}, + "notes": "", + "auditTrail": [] + }, + { + "id": "bea8d3c3-d520-4a2f-ac21-abe4c15b5ed6", + "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", + "createdAt": "2025-12-03T17:10:00.723Z", + "createdBy": "local", + "summary": "Gather the exact merchant impact metrics (retry %, timeouts, abandonment).", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "High", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": { + "level": "None", + "impactIfFails": "", + "prevent": "", + "ifHappens": "" + }, + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": {}, + "notes": "", + "auditTrail": [] + }, + { + "id": "24e20fac-524a-49c8-b4ce-16fd3fde4db9", + "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", + "createdAt": "2025-12-03T17:09:37.024Z", + "createdBy": "local", + "summary": "Check FraudEdge queues to confirm not contributing.", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": { + "level": "None", + "impactIfFails": "", + "prevent": "", + "ifHappens": "" + }, + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": {}, + "notes": "", + "auditTrail": [] + }, + { + "id": "683f6c33-67b6-4fc1-8f3a-4e597fd2e7c8", + "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", + "createdAt": "2025-12-03T17:09:13.163Z", + "createdBy": "local", + "summary": "Validate Redis routing update status in case SMEs attempt to blame it.", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": { + "level": "None", + "impactIfFails": "", + "prevent": "", + "ifHappens": "" + }, + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": {}, + "notes": "", + "auditTrail": [] + }, + { + "id": "388fda77-c081-4ae9-93fc-206275dce281", + "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", + "createdAt": "2025-12-03T17:09:04.059Z", + "createdBy": "local", + "summary": "Confirm recent MerchantGateway config change timing vs spike onset.", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": { + "level": "None", + "impactIfFails": "", + "prevent": "", + "ifHappens": "" + }, + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": {}, + "notes": "", + "auditTrail": [] + }, + { + "id": "ff855c1c-ca68-4ed0-b99d-a96737f02ff2", + "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", + "createdAt": "2025-12-03T17:09:53.441Z", + "createdBy": "local", + "summary": "Coordinate with MCNO to determine whether a Broadscope is now required.", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Low", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": { + "level": "None", + "impactIfFails": "", + "prevent": "", + "ifHappens": "" + }, + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": {}, + "notes": "", + "auditTrail": [] + }, + { + "id": "afbc20c5-18d8-44aa-ad7b-5b414486e829", + "analysisId": "analysis-6bef92d9-3d60-48ed-9e67-e641620d9513", + "createdAt": "2025-12-03T17:09:47.392Z", + "createdBy": "local", + "summary": "Confirm actual timeout error codes (504 vs 408 vs 429).", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Low", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": { + "level": "None", + "impactIfFails": "", + "prevent": "", + "ifHappens": "" + }, + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": {}, + "notes": "", + "auditTrail": [] + } + ] + } + } + }, + { + "id": "mrs-walthers-sugar-donuts", + "name": "Mrs Walthers Sugar Donuts", + "description": "Quality control has reported bad donuts at their inspection station", + "templateKind": "case-study", + "supportedModes": [ + "intake", + "is-is-not", + "dc", + "full" + ], + "state": { + "meta": { + "version": 1, + "savedAt": "2025-11-18T07:33:01.521Z" + }, + "pre": { + "oneLine": "Donuts are bad", + "proof": "Quality control has reported bad donuts at their inspection station", + "objectPrefill": "Sugar Donuts", + "healthy": "All edges should be square. Center hole should be blown out fully, and the center hole is also square. Donuts should be cooked correctly, and golden in color. Thickness should be within specified parameters", + "now": "Round Edges" + }, + "impact": { + "now": "25% Reject rate for Sugar Donuts (1 out of 4 rows).\nProduction demands are not being met", + "future": "Unable to meet production demand", + "time": "End of the Day" + }, + "ops": { + "bridgeOpenedUtc": "2025-11-18T04:41:35.797Z", + "icName": "", + "bcName": "", + "semOpsName": "", + "severity": "", + "detectMonitoring": true, + "detectUserReport": false, + "detectAutomation": false, + "detectOther": false, + "evScreenshot": true, + "evLogs": false, + "evMetrics": false, + "evRepro": false, + "evOther": false, + "containStatus": "assessing", + "containDesc": "", + "commCadence": "", + "commLog": [], + "commNextDueIso": "", + "commNextUpdateTime": "", + "tableFocusMode": "comprehensive" + }, + "table": [ + { + "band": "WHAT — Define the problem precisely (Object & Deviation)." + }, + { + "q": "WHAT — Specific Object/Thing is having the “Round Edges”", + "is": "Sugar Donuts", + "no": "Crullers\nPlain, Frosted", + "di": "Donuts are made on a separate line\n\nDonuts are larger in size\n\nSugar Donuts are sugar coated\n\nMade on Rows 1-4", + "ch": "New Sugaring Equipment (1 Week Ago)", + "questionId": "what-object" + }, + { + "q": "WHAT — Specific Deviation does the “Sugar Donuts” have?", + "is": "Round Edges", + "no": "Too thick, too thin, overcooked, undercooked, center holes not blown out", + "di": "", + "ch": "", + "questionId": "what-deviation" + }, + { + "band": "WHERE — Locate the problem (geography/topology and on the object)." + }, + { + "q": "WHERE — is the “Sugar Donuts” geographically/topology when the “Round Edges” occurs?", + "is": "Fast Line, Mesh Belt, QC\nRow 1", + "no": "Packaging, Customer\nRows 2,3,4 or combination", + "di": "Row 1 is furthest from the air source\n\nRow 1 has no outside cutter blade\n\nRow 1 edges are formed by edgers", + "ch": "Air System Modified (1 Week Ago)\n\nStripped Edger Screw (1 Week Ago)\n\nReplaced Edger Screw (1 Week Ago)", + "questionId": "where-location" + }, + { + "q": "WHERE — On the “Sugar Donuts” is the “Round Edges” observed?", + "is": "Outside Edge", + "no": "Inside, leading, trailing or combination of edges", + "di": "No Blades on outside edge", + "ch": "", + "questionId": "where-on-object" + }, + { + "band": "WHEN — Timing and Description" + }, + { + "q": "WHEN — Was the “Round Edges” First observed for “Sugar Donuts”", + "is": "~1 Hr Ago", + "no": "Before 1 Hour Ago", + "di": "We installed a new chain around this time", + "ch": "Chain Broke (55 Mins Ago)\n\nChain Installed (1 Hr Ago)", + "questionId": "when-first-observed" + }, + { + "q": "WHEN — Since the first occurrence has “Round Edges” been logged? What Pattern?", + "is": "Every Minute Since\nContinuous", + "no": "Any Other Times\nPeriodic, Sporadic, One-Time Event", + "di": "", + "ch": "", + "questionId": "when-pattern" + }, + { + "q": "WHEN — Describe using words When the “Round Edges” was first seen", + "is": "After Cutting the donuts", + "no": "After cooking, being transported on mesh belt, during cooling, after topping, or at quality control", + "di": "", + "ch": "", + "questionId": "when-description" + }, + { + "band": "EXTENT — How big is it? Magnitude, count, scope, trend." + }, + { + "q": "EXTENT — What is the population or size of “Sugar Donuts” affected?", + "is": "25% Reject Rate\n1 of 4 Rows", + "no": "More or Less than 25%\n2 or More Rows", + "di": "", + "ch": "", + "questionId": "extent-population" + }, + { + "q": "EXTENT — What is the size of a single “Round Edges”?", + "is": "Entire Edge", + "no": "Partial Edge", + "di": "", + "ch": "", + "questionId": "extent-size" + }, + { + "q": "EXTENT — How many “Round Edges” are occuring on each “Sugar Donuts”?", + "is": "1 Per Donut", + "no": "Between 2-4", + "di": "", + "ch": "", + "questionId": "extent-count" + } + ], + "causes": [ + { + "id": "cause-4giuxz-mi43kb9e", + "suspect": "Blades", + "accusation": "Overheating", + "impact": "and causing dough to not properly release from blades", + "summaryText": "We suspect Blades because Overheating. This could lead to and causing dough to not properly release from blades.", + "confidence": "", + "evidence": "", + "findings": { + "WHAT — Specific Object/Thing is having the {DEVIATION}": { + "mode": "assumption", + "note": "Only if the sugar donuts blades (1-4) are overheating" + }, + "WHAT — Specific Deviation does the {OBJECT} have?": { + "mode": "assumption", + "note": "The center hold blades are releasing just fine" + }, + "WHERE — is the {OBJECT} geographically/topology when the {DEVIATION} occurs?": { + "mode": "assumption", + "note": "Row 1-4 blades are overheating" + }, + "WHERE — On the {OBJECT} is the {DEVIATION} observed?": { + "mode": "fail", + "note": "We do not have blades on the outside edge of row 1" + } + }, + "editing": false, + "testingOpen": false + }, + { + "id": "cause-auefoi-mi43le61", + "suspect": "Hot Oil Bath", + "accusation": "at Incorrect Temperature", + "impact": "Cooking one side of the donuts improperly", + "summaryText": "We suspect Hot Oil Bath because at Incorrect Temperature. This could lead to Cooking one side of the donuts improperly.", + "confidence": "", + "evidence": "", + "findings": { + "WHAT — Specific Object/Thing is having the {DEVIATION}": { + "mode": "fail", + "note": "This does not explain why other donuts would be affected" + } + }, + "editing": false, + "testingOpen": false + }, + { + "id": "cause-dtnho5-mi43m5a9", + "suspect": "Edgers", + "accusation": "is loose", + "impact": "and not holding outside edge at correct specifications", + "summaryText": "We suspect Edgers because is loose. This could lead to and not holding outside edge at correct specifications.", + "confidence": "", + "evidence": "", + "findings": {}, + "editing": false, + "testingOpen": false + } + ], + "likelyCauseId": "cause-dtnho5-mi43m5a9", + "steps": { + "items": [ + { + "id": "1", + "label": "Pre-analysis completed", + "checked": false + }, + { + "id": "2", + "label": "Incident Commander assigned", + "checked": false + }, + { + "id": "4", + "label": "Problem statement created", + "checked": false + }, + { + "id": "5", + "label": "Bridge options considered", + "checked": false + }, + { + "id": "6", + "label": "Bridges opened and responders invited", + "checked": false + }, + { + "id": "7", + "label": "Bridge etiquette, roles, and guidelines outlined", + "checked": false + }, + { + "id": "8", + "label": "Current actions documented (Who/What/When)", + "checked": false + }, + { + "id": "9", + "label": "Quick spec answers captured", + "checked": false + }, + { + "id": "10", + "label": "Last changes, monitoring, and dependencies investigated", + "checked": false + }, + { + "id": "11", + "label": "Attendees optimized", + "checked": false + }, + { + "id": "12", + "label": "Possible causes developed", + "checked": false + }, + { + "id": "13", + "label": "Testing actions documented (Who/What/When)", + "checked": false + }, + { + "id": "14", + "label": "Micro-experiments/tests conducted", + "checked": false + }, + { + "id": "15", + "label": "Containment options identified", + "checked": false + }, + { + "id": "16", + "label": "Comms written, reviewed, and sent", + "checked": false + }, + { + "id": "17", + "label": "Attendees optimized for next action", + "checked": false + }, + { + "id": "18", + "label": "Possible causes evaluated and distinctions identified", + "checked": false + }, + { + "id": "19", + "label": "Most probable cause identified", + "checked": false + }, + { + "id": "20", + "label": "Restoration/rollback/workaround selected", + "checked": false + }, + { + "id": "21", + "label": "Attendees optimized for decision", + "checked": false + }, + { + "id": "22", + "label": "Verification and risk plan created", + "checked": false + }, + { + "id": "23", + "label": "Restoration actions documented (Who/What/When)", + "checked": false + }, + { + "id": "24", + "label": "Service validated internally and externally", + "checked": false + }, + { + "id": "25", + "label": "Restoration comms sent", + "checked": false + }, + { + "id": "26", + "label": "Handover template prepared (PIR/problem)", + "checked": false + }, + { + "id": "27", + "label": "Downstream issues from fix assessed", + "checked": false + }, + { + "id": "28", + "label": "Participants released and bridge closed", + "checked": false + } + ], + "drawerOpen": false + }, + "actions": { + "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", + "items": [ + { + "id": "cb0076ab-2a5f-4508-8672-73f1ef9ff49f", + "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", + "createdAt": "2025-11-18T04:57:40.397Z", + "createdBy": "local", + "summary": "Understand vibration issues leading to Edger Failure/Release", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "High", + "dueAt": "", + "startedAt": "2025-11-18T04:57:42.073Z", + "completedAt": "2025-11-18T04:57:42.621Z", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-dtnho5-mi43m5a9" + }, + "notes": "", + "auditTrail": [] + }, + { + "id": "7a883644-57d3-4204-99ad-c42b7312b27b", + "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", + "createdAt": "2025-11-18T04:57:53.067Z", + "createdBy": "local", + "summary": "Provide Options for a perm fix", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-dtnho5-mi43m5a9" + }, + "notes": "", + "auditTrail": [] + }, + { + "id": "c933507a-d82e-4d17-b70e-de6b16148761", + "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", + "createdAt": "2025-11-18T04:57:13.778Z", + "createdBy": "local", + "summary": "Inspect the Proofing Tunnel", + "detail": "", + "owner": { + "name": "Engineering Assistant", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "local-user", + "lastAssignedAt": "2025-11-18T04:57:21.808Z", + "source": "Manual" + }, + "role": "", + "status": "Done", + "priority": "Med", + "dueAt": "", + "startedAt": "2025-11-18T04:57:26.030Z", + "completedAt": "2025-11-18T04:57:26.704Z", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-dtnho5-mi43m5a9" + }, + "notes": "", + "auditTrail": [ + { + "type": "ownerChanged", + "at": "2025-11-18T04:57:21.808Z", + "by": "local-user", + "before": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "after": { + "name": "Engineering Assistant", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "local-user", + "lastAssignedAt": "2025-11-18T04:57:21.808Z", + "source": "Manual" + } + } + ] + }, + { + "id": "8086ff81-2004-4951-b875-2dd4328055b9", + "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", + "createdAt": "2025-11-18T04:56:57.888Z", + "createdBy": "local", + "summary": "Replace the screws on Row 1", + "detail": "", + "owner": { + "name": "Engineering Assistant", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "local-user", + "lastAssignedAt": "2025-11-18T04:57:04.726Z", + "source": "Manual" + }, + "role": "", + "status": "Done", + "priority": "Med", + "dueAt": "", + "startedAt": "2025-11-18T04:57:05.833Z", + "completedAt": "2025-11-18T04:57:06.404Z", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-dtnho5-mi43m5a9" + }, + "notes": "", + "auditTrail": [ + { + "type": "ownerChanged", + "at": "2025-11-18T04:57:04.726Z", + "by": "local-user", + "before": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "after": { + "name": "Engineering Assistant", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "local-user", + "lastAssignedAt": "2025-11-18T04:57:04.726Z", + "source": "Manual" + } + } + ] + }, + { + "id": "27d65310-b2c4-4a59-b49a-bcbf52c30a1f", + "analysisId": "analysis-c1befdf8-c060-4e88-b944-10d8f2293eac", + "createdAt": "2025-11-18T04:56:16.699Z", + "createdBy": "local", + "summary": "Attempt to tighten the edgers on row 1", + "detail": "", + "owner": { + "name": "Engineering Assistant", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "local-user", + "lastAssignedAt": "2025-11-18T04:56:42.269Z", + "source": "Manual" + }, + "role": "", + "status": "Done", + "priority": "Med", + "dueAt": "", + "startedAt": "2025-11-18T04:56:18.930Z", + "completedAt": "2025-11-18T04:56:19.456Z", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-dtnho5-mi43m5a9" + }, + "notes": "", + "auditTrail": [ + { + "type": "ownerChanged", + "at": "2025-11-18T04:56:42.269Z", + "by": "local-user", + "before": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "after": { + "name": "Engineering Assistant", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "local-user", + "lastAssignedAt": "2025-11-18T04:56:42.269Z", + "source": "Manual" + } + } + ] + } + ] + } + } + }, + { + "id": "tamworth-petrochemical", + "name": "Tamworth Petrochemical", + "description": "Customers are reporting an even faint fog across our sheet film products. \n\nWe have reproduced this with X-Ray successfully", + "templateKind": "case-study", + "supportedModes": [ + "intake", + "is-is-not", + "dc", + "full" + ], + "state": { + "meta": { + "version": 1, + "savedAt": "2025-11-19T00:43:10.430Z" + }, + "pre": { + "oneLine": "Tamworth Film is foggy", + "proof": "Customers are reporting an even faint fog across our sheet film products. \n\nWe have reproduced this with X-Ray successfully", + "objectPrefill": "Sheet Film", + "healthy": "The photograph as taken to be clear of any additional elements that were not captured at the time of photograph", + "now": "Fogged" + }, + "impact": { + "now": "682 out of 7000 Dealers are complaining\n\nSales Down\n\nReputation Lost\n\n2-for-1 Exchange Program in Effect", + "future": "We may risk being permanently branded as a poor-quality supplier\n\nDistributors may stop carrying our products", + "time": "" + }, + "ops": { + "bridgeOpenedUtc": "2025-11-19T00:27:46.306Z", + "icName": "", + "bcName": "", + "semOpsName": "", + "severity": "", + "detectMonitoring": false, + "detectUserReport": false, + "detectAutomation": false, + "detectOther": false, + "evScreenshot": true, + "evLogs": false, + "evMetrics": false, + "evRepro": true, + "evOther": false, + "containStatus": "stoppingImpact", + "containDesc": "", + "commCadence": "", + "commLog": [], + "commNextDueIso": "", + "commNextUpdateTime": "", + "tableFocusMode": "comprehensive" + }, + "table": [ + { + "band": "WHAT — Define the problem precisely (Object & Deviation)." + }, + { + "q": "WHAT — Specific Object/Thing is having the “Fogged”", + "is": "Hi-Grade 202\n\nSheet Film", + "no": "NMD - Competitors?\n\nRoll or Pack", + "di": "Must be handled in total darkness\n\nUses different packaging\n\nUses Interleaf paper", + "ch": "NKC\n\nNew Zip Top Package (5 Weeks ago)\n\nNew Interleaf Supplier (6 Months Ago)", + "questionId": "what-object" + }, + { + "q": "WHAT — Specific Deviation does the “Hi-Grade 202” have?", + "is": "Fogged", + "no": "Overexposed, underexposed, spots, streaks", + "di": "", + "ch": "", + "questionId": "what-deviation" + }, + { + "band": "WHERE — Locate the problem (geography/topology and on the object)." + }, + { + "q": "WHERE — is the “Hi-Grade 202” geographically/topology when the “Fogged” occurs?", + "is": "USA\n\nAll Sales Districts\n\nSmall Dealers\n\nDevelopment Rooms", + "no": "N/A\n\nSome Sales Districts\n\nLarge Dealers\n\nN/A", + "di": "Small dealers have slower inventory turnover", + "ch": "NKC", + "questionId": "where-location" + }, + { + "q": "WHERE — On the “Hi-Grade 202” is the “Fogged” observed?", + "is": "Even across the entire surface of the negative", + "no": "On edges, spots, or partial parts of the negative", + "di": "", + "ch": "", + "questionId": "where-on-object" + }, + { + "band": "WHEN — Timing and Description" + }, + { + "q": "WHEN — Was the “Fogged” First observed for “Hi-Grade 202”", + "is": "10+ Days Ago\n\nNMD - Reporting Delay", + "no": "Before 10+ Days Ago", + "di": "", + "ch": "", + "questionId": "when-first-observed" + }, + { + "q": "WHEN — Since the first occurrence has “Fogged” been logged? What Pattern?", + "is": "Daily / Continuous", + "no": "\nRandom, One-Time Event, Periodic", + "di": "", + "ch": "", + "questionId": "when-pattern" + }, + { + "q": "WHEN — Describe using words When the “Fogged” was first seen", + "is": "Immediately when developed", + "no": "Some time after the initial development", + "di": "", + "ch": "", + "questionId": "when-description" + }, + { + "band": "EXTENT — How big is it? Magnitude, count, scope, trend." + }, + { + "q": "EXTENT — What is the population or size of “Hi-Grade 202” affected?", + "is": "682 Dealers\n\nAll negatives in a box\n\nIncreasing", + "no": "> or < 682 Dealers\n\nSome of the Negatives in a box", + "di": "", + "ch": "", + "questionId": "extent-population" + }, + { + "q": "EXTENT — What is the size of a single “Fogged”?", + "is": "100% of the Negative is visibly fogged\n\nStable", + "no": "Less than 100%", + "di": "", + "ch": "", + "questionId": "extent-size" + }, + { + "q": "EXTENT — How many “Fogged” are occuring on each “Hi-Grade 202”?", + "is": "NMD - Seems like 1 Instance of Fogging per Sheet", + "no": "", + "di": "", + "ch": "", + "questionId": "extent-count" + } + ], + "causes": [ + { + "id": "cause-u2f2c1-mi59wwx8", + "suspect": "Doctors and Dentists", + "accusation": "Leaving their X-Ray Equipment On", + "impact": "causing X-Ray Radiation to contaminate our nearby products", + "summaryText": "We suspect Doctors and Dentists because Leaving their X-Ray Equipment On. This could lead to causing X-Ray Radiation to contaminate our nearby products.", + "confidence": "", + "evidence": "", + "findings": {}, + "editing": false, + "testingOpen": false + }, + { + "id": "cause-ix4hu6-mi59xkch", + "suspect": "Nuclear", + "accusation": "Fallout", + "impact": "causing radiation exposure to our sensitive products (Hi-Grade 202)", + "summaryText": "We suspect Nuclear because Fallout. This could lead to causing radiation exposure to our sensitive products (Hi-Grade 202).", + "confidence": "", + "evidence": "", + "findings": {}, + "editing": false, + "testingOpen": false + }, + { + "id": "cause-xlxgqi-mi59y76z", + "suspect": "Shipping Company", + "accusation": "Route Change", + "impact": "Leading to exposure to radiation prone areas of the US", + "summaryText": "We suspect Shipping Company because Route Change. This could lead to Leading to exposure to radiation prone areas of the US.", + "confidence": "", + "evidence": "", + "findings": {}, + "editing": false, + "testingOpen": false + }, + { + "id": "cause-dv4oln-mi59ymxw", + "suspect": "Interleaf Paper", + "accusation": "Contaminated", + "impact": "Exposing the film prematurely", + "summaryText": "We suspect Interleaf Paper because Contaminated. This could lead to Exposing the film prematurely.", + "confidence": "", + "evidence": "", + "findings": {}, + "editing": false, + "testingOpen": false + }, + { + "id": "cause-fgg1y5-mi59z56k", + "suspect": "Amateurs", + "accusation": "Unable to handle the film correctly", + "impact": "Exposing it incorrectly when putting it in camera", + "summaryText": "We suspect Amateurs because Unable to handle the film correctly. This could lead to Exposing it incorrectly when putting it in camera.", + "confidence": "", + "evidence": "", + "findings": {}, + "editing": false, + "testingOpen": false + } + ], + "likelyCauseId": "cause-dv4oln-mi59ymxw", + "steps": { + "items": [ + { + "id": "1", + "label": "Pre-analysis completed", + "checked": false + }, + { + "id": "2", + "label": "Incident Commander assigned", + "checked": false + }, + { + "id": "4", + "label": "Problem statement created", + "checked": false + }, + { + "id": "5", + "label": "Bridge options considered", + "checked": false + }, + { + "id": "6", + "label": "Bridges opened and responders invited", + "checked": false + }, + { + "id": "7", + "label": "Bridge etiquette, roles, and guidelines outlined", + "checked": false + }, + { + "id": "8", + "label": "Current actions documented (Who/What/When)", + "checked": false + }, + { + "id": "9", + "label": "Quick spec answers captured", + "checked": false + }, + { + "id": "10", + "label": "Last changes, monitoring, and dependencies investigated", + "checked": false + }, + { + "id": "11", + "label": "Attendees optimized", + "checked": false + }, + { + "id": "12", + "label": "Possible causes developed", + "checked": false + }, + { + "id": "13", + "label": "Testing actions documented (Who/What/When)", + "checked": false + }, + { + "id": "14", + "label": "Micro-experiments/tests conducted", + "checked": false + }, + { + "id": "15", + "label": "Containment options identified", + "checked": false + }, + { + "id": "16", + "label": "Comms written, reviewed, and sent", + "checked": false + }, + { + "id": "17", + "label": "Attendees optimized for next action", + "checked": false + }, + { + "id": "18", + "label": "Possible causes evaluated and distinctions identified", + "checked": false + }, + { + "id": "19", + "label": "Most probable cause identified", + "checked": false + }, + { + "id": "20", + "label": "Restoration/rollback/workaround selected", + "checked": false + }, + { + "id": "21", + "label": "Attendees optimized for decision", + "checked": false + }, + { + "id": "22", + "label": "Verification and risk plan created", + "checked": false + }, + { + "id": "23", + "label": "Restoration actions documented (Who/What/When)", + "checked": false + }, + { + "id": "24", + "label": "Service validated internally and externally", + "checked": false + }, + { + "id": "25", + "label": "Restoration comms sent", + "checked": false + }, + { + "id": "26", + "label": "Handover template prepared (PIR/problem)", + "checked": false + }, + { + "id": "27", + "label": "Downstream issues from fix assessed", + "checked": false + }, + { + "id": "28", + "label": "Participants released and bridge closed", + "checked": false + } + ], + "drawerOpen": false + }, + "actions": { + "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", + "items": [ + { + "id": "8158d4af-a8ac-4f3a-a5a5-a9dc1264a6ee", + "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", + "createdAt": "2025-11-19T00:42:34.771Z", + "createdBy": "local", + "summary": "Double the Marketing Budget", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "" + }, + "notes": "", + "auditTrail": [] + }, + { + "id": "6db1d3f4-71a6-4826-8fdb-072f18ab77a9", + "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", + "createdAt": "2025-11-19T00:42:01.031Z", + "createdBy": "local", + "summary": "Perform Chemical Analysis on Interleaf Paper to see if it is contaminated with something", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-dv4oln-mi59ymxw" + }, + "notes": "", + "auditTrail": [] + }, + { + "id": "b6f59196-26fe-4e24-b448-95051efc7eba", + "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", + "createdAt": "2025-11-19T00:41:31.726Z", + "createdBy": "local", + "summary": "Ask shipping company about any security screenings they are now doing", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-xlxgqi-mi59y76z" + }, + "notes": "", + "auditTrail": [] + }, + { + "id": "955ebc23-c7ed-4f07-82d2-e2cae57c9c52", + "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", + "createdAt": "2025-11-19T00:41:12.174Z", + "createdBy": "local", + "summary": "Map out precipitation across the USA and determine if it matches the dealer pattern of complaints", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-ix4hu6-mi59xkch" + }, + "notes": "", + "auditTrail": [] + }, + { + "id": "4561a608-36c7-42fe-aae1-461baa33c060", + "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", + "createdAt": "2025-11-19T00:40:35.413Z", + "createdBy": "local", + "summary": "Visit Local Doctors and Dentist offices to verify that their equipment is not malfunctioning", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-u2f2c1-mi59wwx8" + }, + "notes": "", + "auditTrail": [] + }, + { + "id": "2b9023c5-2758-4652-88cf-f1091de72637", + "analysisId": "analysis-0ae04de1-747a-414b-805f-82f3e79220f1", + "createdAt": "2025-11-19T00:40:10.792Z", + "createdBy": "local", + "summary": "Watch someone try to use our film and see if they are doing it incorrectly", + "detail": "", + "owner": { + "name": "", + "category": "", + "subOwner": "", + "notes": "", + "lastAssignedBy": "", + "lastAssignedAt": "", + "source": "Manual" + }, + "role": "", + "status": "Planned", + "priority": "Med", + "dueAt": "", + "startedAt": "", + "completedAt": "", + "dependencies": [], + "risk": "None", + "changeControl": { + "required": false + }, + "verification": { + "required": false + }, + "links": { + "hypothesisId": "cause-fgg1y5-mi59z56k" + }, + "notes": "", + "auditTrail": [] + } + ] + } + } + } +]; + +export const PROTECTED_CASE_STUDY_MANIFEST = deepFreeze(manifest); diff --git a/package.json b/package.json index 3a80a6d..2c78ec4 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "verify:summary": "node scripts/ensure-summary-integrations.js", "verify:persistence": "node scripts/ensure-persistence-integrations.js", "verify:lockfile": "node scripts/verify-lockfile.js", - "check:templates": "npm run build:templates && git diff --exit-code -- src/templates.manifest.js", + "check:templates": "npm run build:templates && git diff --exit-code -- src/templates.manifest.js api/protected-case-studies.manifest.js", "repo:doctor": "node scripts/repo-doctor.js", "quality": "npm run verify:lockfile && npm run repo:doctor && npm run verify:tests && npm run verify:summary && npm run verify:persistence && npm run lint && npm run check:templates && npm run check:storage-docs && npm test" }, From 5d8ecb756de179276164d575cfc4ea1a03ef7115 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:54:49 -0400 Subject: [PATCH 005/100] test: guard protected Case Studies from public manifest --- api/protected-case-studies.manifest.js | 8 ++-- src/templates.manifest.js | 2 +- tests/protectedCaseManifest.unit.test.mjs | 55 +++++++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 tests/protectedCaseManifest.unit.test.mjs diff --git a/api/protected-case-studies.manifest.js b/api/protected-case-studies.manifest.js index 629c743..bdcfc53 100644 --- a/api/protected-case-studies.manifest.js +++ b/api/protected-case-studies.manifest.js @@ -18,13 +18,13 @@ const manifest = [ "id": "microcomputer-cabinets", "name": "Microcomputer Cabinets", "description": "Final Inspection Reports that cabinets are being rejected periodically", - "templateKind": "case-study", "supportedModes": [ "intake", "is-is-not", "dc", "full" ], + "templateKind": "case-study", "state": { "meta": { "version": 1, @@ -455,13 +455,13 @@ const manifest = [ "id": "paytrix", "name": "Paytrix", "description": "Chaos at Paytrix", - "templateKind": "case-study", "supportedModes": [ "intake", "is-is-not", "dc", "full" ], + "templateKind": "case-study", "state": { "meta": { "version": 1, @@ -1263,13 +1263,13 @@ const manifest = [ "id": "mrs-walthers-sugar-donuts", "name": "Mrs Walthers Sugar Donuts", "description": "Quality control has reported bad donuts at their inspection station", - "templateKind": "case-study", "supportedModes": [ "intake", "is-is-not", "dc", "full" ], + "templateKind": "case-study", "state": { "meta": { "version": 1, @@ -1868,13 +1868,13 @@ const manifest = [ "id": "tamworth-petrochemical", "name": "Tamworth Petrochemical", "description": "Customers are reporting an even faint fog across our sheet film products. \n\nWe have reproduced this with X-Ray successfully", - "templateKind": "case-study", "supportedModes": [ "intake", "is-is-not", "dc", "full" ], + "templateKind": "case-study", "state": { "meta": { "version": 1, diff --git a/src/templates.manifest.js b/src/templates.manifest.js index 76607fe..515afda 100644 --- a/src/templates.manifest.js +++ b/src/templates.manifest.js @@ -18,13 +18,13 @@ const manifest = [ "id": "checkout-latency", "name": "Checkout Latency Spike", "description": "EU checkout slowdown triggered by a CDN header rewrite.", - "templateKind": "standard", "supportedModes": [ "intake", "is-is-not", "dc", "full" ], + "templateKind": "standard", "state": { "meta": { "version": 1, diff --git a/tests/protectedCaseManifest.unit.test.mjs b/tests/protectedCaseManifest.unit.test.mjs new file mode 100644 index 0000000..3911a1f --- /dev/null +++ b/tests/protectedCaseManifest.unit.test.mjs @@ -0,0 +1,55 @@ +/** + * Security regression coverage for the public/protected template build boundary. + */ +import assert from 'node:assert/strict'; +import { readFile, readdir } from 'node:fs/promises'; +import { test } from 'node:test'; + +import { PROTECTED_CASE_STUDY_MANIFEST } from '../api/protected-case-studies.manifest.js'; +import { TEMPLATE_MANIFEST } from '../src/templates.manifest.js'; +import { TEMPLATE_KINDS } from '../src/templateKinds.js'; + +test('public manifest contains only Standard Templates while protected cases stay server-side', async () => { + assert.ok(TEMPLATE_MANIFEST.length > 0, 'at least one public Standard Template should exist'); + assert.ok(PROTECTED_CASE_STUDY_MANIFEST.length > 0, 'protected Case Studies should exist server-side'); + + assert.ok( + TEMPLATE_MANIFEST.every(entry => entry.templateKind === TEMPLATE_KINDS.STANDARD), + 'public manifest must contain only Standard Templates' + ); + assert.ok( + PROTECTED_CASE_STUDY_MANIFEST.every(entry => entry.templateKind === TEMPLATE_KINDS.CASE_STUDY), + 'server-only manifest must contain only Case Studies' + ); + + const publicIds = new Set(TEMPLATE_MANIFEST.map(entry => entry.id)); + for (const caseStudy of PROTECTED_CASE_STUDY_MANIFEST) { + assert.equal(publicIds.has(caseStudy.id), false, `${caseStudy.id} must not be public`); + } + + const publicSource = await readFile(new URL('../src/templates.manifest.js', import.meta.url), 'utf8'); + for (const caseStudy of PROTECTED_CASE_STUDY_MANIFEST) { + assert.equal(publicSource.includes(caseStudy.id), false, `${caseStudy.id} must not occur in the public generated module`); + assert.equal(publicSource.includes(caseStudy.name), false, `${caseStudy.name} must not occur in the public generated module`); + } +}); + +test('every authored JSON resource is emitted to exactly one generated boundary', async () => { + const templatesDir = new URL('../templates/', import.meta.url); + const names = (await readdir(templatesDir)).filter(name => name.endsWith('.json')).sort(); + const authored = []; + for (const name of names) { + authored.push(JSON.parse(await readFile(new URL(name, templatesDir), 'utf8'))); + } + + const generated = [ + ...TEMPLATE_MANIFEST.map(entry => ({ id: entry.id, templateKind: entry.templateKind })), + ...PROTECTED_CASE_STUDY_MANIFEST.map(entry => ({ id: entry.id, templateKind: entry.templateKind })) + ].sort((a, b) => a.id.localeCompare(b.id)); + + const expected = authored + .map(entry => ({ id: entry.id.trim(), templateKind: entry.templateKind })) + .sort((a, b) => a.id.localeCompare(b.id)); + + assert.deepEqual(generated, expected); +}); From 5949db8ebea94517ae8c6c7fa5dfa0b4e620adb0 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:56:50 -0400 Subject: [PATCH 006/100] refactor: expose Student classroom authorization context --- api/_classroom.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/api/_classroom.js b/api/_classroom.js index c00436f..91040c7 100644 --- a/api/_classroom.js +++ b/api/_classroom.js @@ -485,7 +485,7 @@ async function initializeClassroomRepository() { }; }, - async listFeedbackForStudent(accessHash) { + async getStudentContext(accessHash) { const rows = await sql`SELECT c.id AS "classInternalId", c.public_id AS "classId", @@ -507,6 +507,16 @@ async function initializeClassroomRepository() { AND w.expires_at > NOW()`; const scope = rows[0]; if (!scope) return null; + return { + classroom: { id: scope.classId, title: scope.classTitle, expiresAt: scope.classExpiresAt }, + workspace: { id: scope.workspaceId, kind: scope.workspaceKind, label: scope.workspaceLabel }, + internal: { classId: scope.classInternalId, workspaceId: scope.workspaceInternalId } + }; + }, + + async listFeedbackForStudent(accessHash) { + const scope = await this.getStudentContext(accessHash); + if (!scope) return null; const feedback = await sql`SELECT target_id AS "targetId", status, @@ -517,12 +527,12 @@ async function initializeClassroomRepository() { created_at AS "createdAt", updated_at AS "updatedAt" FROM classroom_coaching_feedback - WHERE class_id = ${scope.classInternalId} - AND workspace_id = ${scope.workspaceInternalId} + WHERE class_id = ${scope.internal.classId} + AND workspace_id = ${scope.internal.workspaceId} ORDER BY target_id`; return { - classroom: { id: scope.classId, title: scope.classTitle, expiresAt: scope.classExpiresAt }, - workspace: { id: scope.workspaceId, kind: scope.workspaceKind, label: scope.workspaceLabel }, + classroom: scope.classroom, + workspace: scope.workspace, feedback }; }, From 8f4d684358308bd1e333fc52e8c271aefc880557 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:56:53 -0400 Subject: [PATCH 007/100] test: model Student classroom authorization context --- tests/helpers/classroom-test-repositories.mjs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/helpers/classroom-test-repositories.mjs b/tests/helpers/classroom-test-repositories.mjs index 16e8d63..5be0650 100644 --- a/tests/helpers/classroom-test-repositories.mjs +++ b/tests/helpers/classroom-test-repositories.mjs @@ -310,7 +310,7 @@ export function createClassroomRepository() { const cleared = coaching.delete(key); return { classroom: scope.classroom, workspace: scope.workspace, cleared, targetId }; }, - async listFeedbackForStudent(accessHash) { + async getStudentContext(accessHash) { const member = [...memberships.values()].find(value => value.accessHash === accessHash); if (!member) return null; const item = classes.find(candidate => candidate.internalId === member.classInternalId && !candidate.revoked); @@ -319,10 +319,19 @@ export function createClassroomRepository() { candidate.classInternalId === item.internalId && candidate.workspaceId === member.workspaceId && !candidate.revoked )); if (!workspace) return null; - const prefix = item.internalId + ':' + workspace.workspaceId + ':'; return { classroom: publicClass(item), workspace: { id: workspace.id, kind: workspace.kind, label: workspace.label }, + internal: { classId: item.internalId, workspaceId: workspace.workspaceId } + }; + }, + async listFeedbackForStudent(accessHash) { + const scope = await this.getStudentContext(accessHash); + if (!scope) return null; + const prefix = scope.internal.classId + ':' + scope.internal.workspaceId + ':'; + return { + classroom: scope.classroom, + workspace: scope.workspace, feedback: [...coaching.entries()] .filter(([key]) => key.startsWith(prefix)) .map(([, value]) => ({ ...value })) From 5dfd2718a2fefc2357afb22a5a906cd3f045e970 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:58:27 -0400 Subject: [PATCH 008/100] feat: add authorized protected Case Study API --- api/_protectedCaseStudies.js | 123 +++++++++++++++++ api/classes/case-studies.js | 7 + api/classes/case-studies/student.js | 7 + tests/protectedCaseStudies.api.test.mjs | 172 ++++++++++++++++++++++++ 4 files changed, 309 insertions(+) create mode 100644 api/_protectedCaseStudies.js create mode 100644 api/classes/case-studies.js create mode 100644 api/classes/case-studies/student.js create mode 100644 tests/protectedCaseStudies.api.test.mjs diff --git a/api/_protectedCaseStudies.js b/api/_protectedCaseStudies.js new file mode 100644 index 0000000..5093529 --- /dev/null +++ b/api/_protectedCaseStudies.js @@ -0,0 +1,123 @@ +/** + * @module api/protectedCaseStudies + * @description + * Server-only protected Case Study catalog and payload delivery. Authorization + * uses the existing Classroom Instructor capability or issued Student workspace + * capability; protected content never travels in URLs or public client assets. + */ + +import { getClassroomRepository } from './_classroom.js'; +import { PROTECTED_CASE_STUDY_MANIFEST } from './protected-case-studies.manifest.js'; +import { hashWorkspaceToken, parseAuthorizationToken } from './_workspace.js'; + +function send(res, status, body) { + res.setHeader('Cache-Control', 'no-store'); + res.setHeader('Referrer-Policy', 'no-referrer'); + return res.status(status).json(body); +} + +function methodNotAllowed(res) { + res.setHeader('Allow', 'GET, POST'); + return send(res, 405, { error: 'Method not allowed.' }); +} + +function requireBearer(req) { + const token = parseAuthorizationToken(req?.headers?.authorization); + return token || null; +} + +function catalog(manifest) { + return manifest.map(entry => ({ + id: entry.id, + name: entry.name, + description: entry.description, + templateKind: entry.templateKind, + supportedModes: [...entry.supportedModes] + })); +} + +function requestedCaseStudy(manifest, body) { + const id = typeof body?.caseStudyId === 'string' ? body.caseStudyId.trim() : ''; + if (!id) return null; + return manifest.find(entry => entry.id === id) || null; +} + +function createProtectedCaseStudyHandler({ + getRepository, + manifest, + authorize, + unauthorizedMessage +}) { + return async (req, res) => { + if (!['GET', 'POST'].includes(req.method)) return methodNotAllowed(res); + const token = requireBearer(req); + if (!token) return send(res, 401, { error: 'Missing or invalid authorization.' }); + + try { + const repository = await getRepository(); + const context = await authorize(repository, hashWorkspaceToken(token)); + if (!context) return send(res, 404, { error: unauthorizedMessage }); + + if (req.method === 'GET') { + return send(res, 200, { + class: context.classroom, + caseStudies: catalog(manifest) + }); + } + + const caseStudy = requestedCaseStudy(manifest, req.body); + if (!caseStudy) return send(res, 404, { error: 'Case Study not found.' }); + + return send(res, 200, { + class: context.classroom, + workspace: context.workspace || null, + caseStudy + }); + } catch { + return send(res, 500, { error: 'Unable to load protected Case Studies.' }); + } + }; +} + +/** + * Create the Instructor protected Case Study handler. + * + * @param {object} [dependencies] Injectable dependencies. + * @param {() => Promise} [dependencies.getRepository] Classroom repository provider. + * @param {ReadonlyArray} [dependencies.manifest] Protected manifest override for tests. + * @returns {Function} Vercel handler. + */ +export function instructorCaseStudiesHandler({ + getRepository = getClassroomRepository, + manifest = PROTECTED_CASE_STUDY_MANIFEST +} = {}) { + return createProtectedCaseStudyHandler({ + getRepository, + manifest, + authorize: async (repository, tokenHash) => { + const classroom = await repository.getClassByInstructor(tokenHash); + return classroom ? { classroom } : null; + }, + unauthorizedMessage: 'Class not found.' + }); +} + +/** + * Create the Student protected Case Study handler. + * + * @param {object} [dependencies] Injectable dependencies. + * @param {() => Promise} [dependencies.getRepository] Classroom repository provider. + * @param {ReadonlyArray} [dependencies.manifest] Protected manifest override for tests. + * @returns {Function} Vercel handler. + */ +export function studentCaseStudiesHandler({ + getRepository = getClassroomRepository, + manifest = PROTECTED_CASE_STUDY_MANIFEST +} = {}) { + return createProtectedCaseStudyHandler({ + getRepository, + manifest, + authorize: (repository, tokenHash) => repository.getStudentContext(tokenHash), + unauthorizedMessage: 'Class resources not found.' + }); +} diff --git a/api/classes/case-studies.js b/api/classes/case-studies.js new file mode 100644 index 0000000..d753a76 --- /dev/null +++ b/api/classes/case-studies.js @@ -0,0 +1,7 @@ +/** + * @module api/classes/case-studies + * @description Instructor-authorized protected Case Study catalog and payload delivery. + */ +import { instructorCaseStudiesHandler } from '../_protectedCaseStudies.js'; + +export default instructorCaseStudiesHandler(); diff --git a/api/classes/case-studies/student.js b/api/classes/case-studies/student.js new file mode 100644 index 0000000..3f4b1e4 --- /dev/null +++ b/api/classes/case-studies/student.js @@ -0,0 +1,7 @@ +/** + * @module api/classes/case-studies/student + * @description Student-authorized protected Case Study catalog and payload delivery. + */ +import { studentCaseStudiesHandler } from '../../_protectedCaseStudies.js'; + +export default studentCaseStudiesHandler(); diff --git a/tests/protectedCaseStudies.api.test.mjs b/tests/protectedCaseStudies.api.test.mjs new file mode 100644 index 0000000..8bdc6d3 --- /dev/null +++ b/tests/protectedCaseStudies.api.test.mjs @@ -0,0 +1,172 @@ +/** + * Authorization coverage for protected Case Study catalog and payload delivery. + */ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { + instructorCaseStudiesHandler, + studentCaseStudiesHandler +} from '../api/_protectedCaseStudies.js'; +import { hashWorkspaceToken } from '../api/_workspace.js'; +import { + createClassroomRepository, + createWorkspaceRepository, + response +} from './helpers/classroom-test-repositories.mjs'; + +const INSTRUCTOR = 'i'.repeat(43); +const JOIN = 'j'.repeat(43); +const CLAIM = 'c'.repeat(43); +const STUDENT = 's'.repeat(43); +const PRIMARY_WORKSPACE = 'p'.repeat(43); +const CLASS_ID = '11111111-1111-4111-8111-111111111111'; +const WORKSPACE_ID = '22222222-2222-4222-8222-222222222222'; +const PARTICIPANT_ID = '33333333-3333-4333-8333-333333333333'; + +const CASES = Object.freeze([ + Object.freeze({ + id: 'training-case', + name: 'Training Case', + description: 'Protected classroom material.', + templateKind: 'case-study', + supportedModes: Object.freeze(['intake', 'full']), + state: Object.freeze({ secretEvidence: 'answer-key-content' }) + }) +]); + +function request(method, token, body = undefined) { + return { + method, + headers: token ? { authorization: `Bearer ${token}` } : {}, + body + }; +} + +async function setupClassroom() { + const classrooms = createClassroomRepository(); + const workspaces = createWorkspaceRepository(); + const instructorHash = hashWorkspaceToken(INSTRUCTOR); + const joinHash = hashWorkspaceToken(JOIN); + const claimHash = hashWorkspaceToken(CLAIM); + + await classrooms.createClass({ + publicId: CLASS_ID, + title: 'Protected Cases Class', + instructorHash, + studentJoinHash: joinHash + }); + const workspace = await workspaces.createUntil( + hashWorkspaceToken(PRIMARY_WORKSPACE), + { meta: { version: 1 } }, + 'future', + 'Student workspace' + ); + await classrooms.addWorkspace(instructorHash, { + publicId: WORKSPACE_ID, + workspaceId: workspace.id, + kind: 'individual', + label: 'Student workspace', + claimHash + }); + await classrooms.joinWorkspace({ + studentJoinHash: joinHash, + claimHash, + participantId: PARTICIPANT_ID, + accessHash: hashWorkspaceToken(STUDENT), + workspaceRepository: workspaces + }); + + return { classrooms, workspaces }; +} + +test('Instructor catalog requires the represented class and omits protected payload state', async () => { + const { classrooms } = await setupClassroom(); + const handler = instructorCaseStudiesHandler({ + getRepository: async () => classrooms, + manifest: CASES + }); + + const unauthorized = response(); + await handler(request('GET', JOIN), unauthorized); + assert.equal(unauthorized.statusCode, 404); + assert.deepEqual(unauthorized.body, { error: 'Class not found.' }); + + const res = response(); + await handler(request('GET', INSTRUCTOR), res); + assert.equal(res.statusCode, 200); + assert.equal(res.headers['Cache-Control'], 'no-store'); + assert.equal(res.headers['Referrer-Policy'], 'no-referrer'); + assert.equal(res.body.class.id, CLASS_ID); + assert.deepEqual(res.body.caseStudies, [{ + id: 'training-case', + name: 'Training Case', + description: 'Protected classroom material.', + templateKind: 'case-study', + supportedModes: ['intake', 'full'] + }]); + assert.equal('state' in res.body.caseStudies[0], false); +}); + +test('Instructor payload uses POST body and returns protected content only after authorization', async () => { + const { classrooms } = await setupClassroom(); + const handler = instructorCaseStudiesHandler({ + getRepository: async () => classrooms, + manifest: CASES + }); + + const res = response(); + await handler(request('POST', INSTRUCTOR, { caseStudyId: 'training-case' }), res); + assert.equal(res.statusCode, 200); + assert.equal(res.body.caseStudy.id, 'training-case'); + assert.equal(res.body.caseStudy.state.secretEvidence, 'answer-key-content'); + + const missing = response(); + await handler(request('POST', INSTRUCTOR, { caseStudyId: 'missing' }), missing); + assert.equal(missing.statusCode, 404); + assert.equal(missing.body.error, 'Case Study not found.'); +}); + +test('Student access is membership-bound and rejects legacy collaboration capabilities', async () => { + const { classrooms } = await setupClassroom(); + const handler = studentCaseStudiesHandler({ + getRepository: async () => classrooms, + manifest: CASES + }); + + const legacy = response(); + await handler(request('GET', PRIMARY_WORKSPACE), legacy); + assert.equal(legacy.statusCode, 404); + assert.deepEqual(legacy.body, { error: 'Class resources not found.' }); + + const catalog = response(); + await handler(request('GET', STUDENT), catalog); + assert.equal(catalog.statusCode, 200); + assert.equal(catalog.body.class.id, CLASS_ID); + assert.equal(catalog.body.caseStudies[0].id, 'training-case'); + assert.equal('state' in catalog.body.caseStudies[0], false); + + const payload = response(); + await handler(request('POST', STUDENT, { caseStudyId: 'training-case' }), payload); + assert.equal(payload.statusCode, 200); + assert.equal(payload.body.workspace.id, WORKSPACE_ID); + assert.equal(payload.body.caseStudy.state.secretEvidence, 'answer-key-content'); +}); + +test('Protected Case Study handlers reject unauthenticated and unsupported methods without caching', async () => { + const { classrooms } = await setupClassroom(); + const handler = studentCaseStudiesHandler({ + getRepository: async () => classrooms, + manifest: CASES + }); + + const missingAuth = response(); + await handler(request('GET', null), missingAuth); + assert.equal(missingAuth.statusCode, 401); + assert.equal(missingAuth.headers['Cache-Control'], 'no-store'); + + const write = response(); + await handler(request('PUT', STUDENT, { caseStudyId: 'training-case' }), write); + assert.equal(write.statusCode, 405); + assert.equal(write.headers.Allow, 'GET, POST'); +}); From 83e278b87392c7be5eb8ba8ec4f19d84a0b94def Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:02:11 -0400 Subject: [PATCH 009/100] feat: add authorized Classroom Case Study client --- main.js | 21 ++- src/classroomCaseStudies.js | 209 +++++++++++++++++++++ src/classroomInstructor.js | 8 + tests/classroomCaseStudies.unit.test.mjs | 108 +++++++++++ tests/classroomInstructor.feature.test.mjs | 25 ++- 5 files changed, 366 insertions(+), 5 deletions(-) create mode 100644 src/classroomCaseStudies.js create mode 100644 tests/classroomCaseStudies.unit.test.mjs diff --git a/main.js b/main.js index 3bb777c..484ac97 100644 --- a/main.js +++ b/main.js @@ -62,15 +62,19 @@ import { initExperienceRoleController } from './src/experienceRoleController.js' import { initStudentClassroom } from './src/classroomStudent.js'; import { initInstructorClassroom } from './src/classroomInstructor.js'; import { initClassroomCoaching } from './src/classroomCoaching.js'; +import { initClassroomCaseStudies } from './src/classroomCaseStudies.js'; /** Active shared-session controller, initialized during boot. @type {object|null} */ let collaborationController = null; let studentClassroomController = null; let instructorClassroomController = null; let classroomCoachingController = null; +let classroomCaseStudiesController = null; /** Destroys collaboration resources during application or test teardown. @returns {void} */ export function destroyCollaboration() { + classroomCaseStudiesController?.destroy?.(); + classroomCaseStudiesController = null; classroomCoachingController?.destroy?.(); classroomCoachingController = null; instructorClassroomController?.destroy?.(); @@ -194,7 +198,8 @@ function boot() { initPreface({ onSave: saveAppState }); initCommsDrawer(); - initTemplatesDrawer(); + classroomCaseStudiesController = initClassroomCaseStudies({ windowRef: window }); + initTemplatesDrawer({ protectedCaseStudies: classroomCaseStudiesController }); initNotesWorkspace({ onSave: saveAppState, showToast }); initializeCommunications({ onSave: saveAppState, showToast }); initStepsFeature({ onSave: saveAppState, onLog: logCommunication }); @@ -233,8 +238,14 @@ function boot() { apply: applyAppState, saveLocal: saveToStorage, toast: showToast, - onClassConnected: token => classroomCoachingController?.connectStudent?.(token), - onClassDisconnected: () => classroomCoachingController?.disconnectStudent?.() + onClassConnected: token => { + classroomCoachingController?.connectStudent?.(token); + void classroomCaseStudiesController?.connectStudent?.(token); + }, + onClassDisconnected: () => { + classroomCoachingController?.disconnectStudent?.(); + classroomCaseStudiesController?.disconnect?.(); + } }); instructorClassroomController = initInstructorClassroom({ collaboration: collaborationController, @@ -242,7 +253,9 @@ function boot() { apply: applyAppState, toast: showToast, onObservation: context => classroomCoachingController?.showInstructorWorkspace?.(context), - onObservationEnd: () => classroomCoachingController?.hideInstructorWorkspace?.() + onObservationEnd: () => classroomCoachingController?.hideInstructorWorkspace?.(), + onClassConnected: token => { void classroomCaseStudiesController?.connectInstructor?.(token); }, + onClassDisconnected: () => classroomCaseStudiesController?.disconnect?.() }); wireThemeToggle(); wireSummaryEvents(); diff --git a/src/classroomCaseStudies.js b/src/classroomCaseStudies.js new file mode 100644 index 0000000..ccd4401 --- /dev/null +++ b/src/classroomCaseStudies.js @@ -0,0 +1,209 @@ +/** + * @module classroomCaseStudies + * @summary Owns authorized in-memory Classroom Case Study catalog and payload access. + * @description + * Credentials remain owned by the Student/Instructor classroom controllers. + * This module receives the active capability only while that classroom role is + * connected, fetches protected resources with Authorization headers, and never + * persists capabilities, catalog content, or payloads to Intake state/storage. + */ + +export const INSTRUCTOR_CASE_STUDIES_ENDPOINT = '/api/classes/case-studies'; +export const STUDENT_CASE_STUDIES_ENDPOINT = '/api/classes/case-studies/student'; + +const CAPABILITY_PATTERN = /^[A-Za-z0-9_-]{43}$/; + +function clone(value) { + if (Array.isArray(value)) return value.map(clone); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, clone(item)])); + } + return value; +} + +function validCapability(value) { + return typeof value === 'string' && CAPABILITY_PATTERN.test(value.trim()); +} + +function sanitizeCatalog(records) { + if (!Array.isArray(records)) return []; + return records + .filter(record => ( + record + && typeof record.id === 'string' + && typeof record.name === 'string' + && typeof record.description === 'string' + && record.templateKind === 'case-study' + && Array.isArray(record.supportedModes) + )) + .map(record => ({ + id: record.id, + name: record.name, + description: record.description, + templateKind: 'case-study', + supportedModes: record.supportedModes.filter(mode => typeof mode === 'string') + })); +} + +/** + * Create the protected Classroom Case Study controller. + * + * @param {object} [options] Dependency overrides. + * @returns {object} In-memory resource controller. + */ +export function createClassroomCaseStudiesController({ + fetchImpl = globalThis.fetch?.bind(globalThis), + windowRef = globalThis.window +} = {}) { + let context = null; + let catalog = []; + let loading = false; + let lastError = ''; + let epoch = 0; + let destroyed = false; + + const emit = () => { + try { + if (typeof windowRef?.dispatchEvent !== 'function' || typeof windowRef?.CustomEvent !== 'function') return; + windowRef.dispatchEvent(new windowRef.CustomEvent('intake:protected-case-studies-changed')); + } catch {} + }; + + const endpointFor = role => role === 'instructor' + ? INSTRUCTOR_CASE_STUDIES_ENDPOINT + : STUDENT_CASE_STUDIES_ENDPOINT; + + const clear = () => { + epoch += 1; + context = null; + catalog = []; + loading = false; + lastError = ''; + emit(); + }; + + const connect = async (role, capability) => { + const token = typeof capability === 'string' ? capability.trim() : ''; + if (destroyed || !['student', 'instructor'].includes(role) || !validCapability(token) || typeof fetchImpl !== 'function') { + clear(); + return false; + } + + const localEpoch = ++epoch; + context = { role, capability: token }; + catalog = []; + loading = true; + lastError = ''; + emit(); + + try { + const response = await fetchImpl(endpointFor(role), { + method: 'GET', + headers: { Authorization: `Bearer ${token}` } + }); + const body = await response.json().catch(() => ({})); + if (destroyed || localEpoch !== epoch || context?.capability !== token || context?.role !== role) return false; + if (!response.ok || !Array.isArray(body.caseStudies)) { + catalog = []; + lastError = response.status === 401 || response.status === 404 + ? 'Class resources are no longer available.' + : 'Could not load class resources.'; + return false; + } + catalog = sanitizeCatalog(body.caseStudies); + return true; + } catch { + if (destroyed || localEpoch !== epoch) return false; + catalog = []; + lastError = 'Could not load class resources.'; + return false; + } finally { + if (!destroyed && localEpoch === epoch) { + loading = false; + emit(); + } + } + }; + + const getPayload = async caseStudyId => { + if ( + destroyed + || !context + || typeof caseStudyId !== 'string' + || !caseStudyId.trim() + || typeof fetchImpl !== 'function' + ) return null; + + const requestedId = caseStudyId.trim(); + const localContext = { ...context }; + const localEpoch = epoch; + try { + const response = await fetchImpl(endpointFor(localContext.role), { + method: 'POST', + headers: { + Authorization: `Bearer ${localContext.capability}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ caseStudyId: requestedId }) + }); + const body = await response.json().catch(() => ({})); + if ( + destroyed + || localEpoch !== epoch + || context?.capability !== localContext.capability + || context?.role !== localContext.role + ) return null; + const record = body.caseStudy; + if ( + !response.ok + || !record + || record.id !== requestedId + || record.templateKind !== 'case-study' + || !record.state + || typeof record.state !== 'object' + || !Array.isArray(record.supportedModes) + ) { + lastError = 'Could not load this Case Study.'; + emit(); + return null; + } + lastError = ''; + return clone(record); + } catch { + if (!destroyed && localEpoch === epoch) { + lastError = 'Could not load this Case Study.'; + emit(); + } + return null; + } + }; + + return { + connectStudent: capability => connect('student', capability), + connectInstructor: capability => connect('instructor', capability), + disconnect: clear, + destroy: () => { + destroyed = true; + clear(); + }, + getCatalog: () => catalog.map(record => ({ ...record, supportedModes: [...record.supportedModes] })), + getPayload, + getState: () => ({ + role: context?.role || null, + connected: Boolean(context), + loading, + lastError, + catalogCount: catalog.length + }) + }; +} + +/** + * Initialize the protected Case Study controller. + * + * @param {Parameters[0]} options Dependencies. + * @returns {ReturnType} Controller. + */ +export function initClassroomCaseStudies(options) { + return createClassroomCaseStudiesController(options); +} diff --git a/src/classroomInstructor.js b/src/classroomInstructor.js index a31fb8e..3cb6777 100644 --- a/src/classroomInstructor.js +++ b/src/classroomInstructor.js @@ -101,6 +101,8 @@ export function createInstructorClassroomController({ toast = () => {}, onObservation = () => {}, onObservationEnd = () => {}, + onClassConnected = () => {}, + onClassDisconnected = () => {}, setTimeoutImpl = globalThis.setTimeout?.bind(globalThis), clearTimeoutImpl = globalThis.clearTimeout?.bind(globalThis), AbortControllerImpl = globalThis.AbortController @@ -480,6 +482,7 @@ export function createInstructorClassroomController({ }; const terminalClass = message => { + onClassDisconnected(); stopLive(); clearInstructorSession(storage); activeSession = null; @@ -571,6 +574,7 @@ export function createInstructorClassroomController({ restoreLocal(); } saveSession(); + onClassConnected(token); renderDashboard(); renderRoster(); if (!selectedWorkspaceId && workspaces[0]) await selectWorkspace(workspaces[0].id); @@ -589,6 +593,7 @@ export function createInstructorClassroomController({ }; const activateClass = async (token, preferredWorkspaceId = '') => { + onClassDisconnected(); disconnectStandaloneCollaboration(); setBusy(true); setStatus('connecting'); @@ -662,6 +667,7 @@ export function createInstructorClassroomController({ }; const leaveClass = () => { + onClassDisconnected(); stopLive(); clearInstructorSession(storage); activeSession = null; @@ -675,6 +681,7 @@ export function createInstructorClassroomController({ }; const pause = () => { + onClassDisconnected(); stopLive(); restoreLocal(); setConnectedLayout(false); @@ -724,6 +731,7 @@ export function createInstructorClassroomController({ const destroy = () => { if (destroyed) return; destroyed = true; + onClassDisconnected(); stopLive(); onObservationEnd(); restoreReadonlyProjection(); diff --git a/tests/classroomCaseStudies.unit.test.mjs b/tests/classroomCaseStudies.unit.test.mjs new file mode 100644 index 0000000..82ee0bf --- /dev/null +++ b/tests/classroomCaseStudies.unit.test.mjs @@ -0,0 +1,108 @@ +/** + * Client contract tests for authorized Classroom Case Study delivery. + */ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { + INSTRUCTOR_CASE_STUDIES_ENDPOINT, + STUDENT_CASE_STUDIES_ENDPOINT, + createClassroomCaseStudiesController +} from '../src/classroomCaseStudies.js'; + +const STUDENT = 's'.repeat(43); +const INSTRUCTOR = 'i'.repeat(43); + +function response(status, body) { + return { + ok: status >= 200 && status < 300, + status, + json: async () => body + }; +} + +test('Student connection loads protected metadata with bearer auth and persists no credential state', async () => { + const requests = []; + const controller = createClassroomCaseStudiesController({ + fetchImpl: async (url, options) => { + requests.push([url, options]); + return response(200, { + caseStudies: [{ + id: 'case-a', + name: 'Case A', + description: 'Protected', + templateKind: 'case-study', + supportedModes: ['intake', 'full'] + }] + }); + } + }); + + assert.equal(await controller.connectStudent(STUDENT), true); + assert.deepEqual(controller.getCatalog().map(item => item.id), ['case-a']); + assert.equal(requests[0][0], STUDENT_CASE_STUDIES_ENDPOINT); + assert.equal(requests[0][1].headers.Authorization, `Bearer ${STUDENT}`); + assert.deepEqual(controller.getState(), { + role: 'student', + connected: true, + loading: false, + lastError: '', + catalogCount: 1 + }); + assert.equal(JSON.stringify(controller.getState()).includes(STUDENT), false); +}); + +test('Case Study payload uses POST body and is discarded when classroom context changes', async () => { + const requests = []; + let resolvePayload; + const controller = createClassroomCaseStudiesController({ + fetchImpl: async (url, options) => { + requests.push([url, options]); + if (options.method === 'GET') return response(200, { caseStudies: [] }); + return new Promise(resolve => { resolvePayload = resolve; }); + } + }); + + await controller.connectStudent(STUDENT); + const payloadPromise = controller.getPayload('case-a'); + assert.equal(requests.at(-1)[0], STUDENT_CASE_STUDIES_ENDPOINT); + assert.equal(requests.at(-1)[1].body, JSON.stringify({ caseStudyId: 'case-a' })); + assert.equal(String(requests.at(-1)[0]).includes('case-a'), false); + + controller.disconnect(); + resolvePayload(response(200, { + caseStudy: { + id: 'case-a', + name: 'Case A', + description: 'Protected', + templateKind: 'case-study', + supportedModes: ['full'], + state: { secret: 'answer' } + } + })); + assert.equal(await payloadPromise, null, 'late protected payload must not survive disconnect'); +}); + +test('Instructor connection uses the Instructor route and disconnect clears metadata', async () => { + const controller = createClassroomCaseStudiesController({ + fetchImpl: async (url, options) => { + assert.equal(url, INSTRUCTOR_CASE_STUDIES_ENDPOINT); + assert.equal(options.headers.Authorization, `Bearer ${INSTRUCTOR}`); + return response(200, { + caseStudies: [{ + id: 'case-b', + name: 'Case B', + description: 'Teaching resource', + templateKind: 'case-study', + supportedModes: ['full'] + }] + }); + } + }); + + assert.equal(await controller.connectInstructor(INSTRUCTOR), true); + assert.equal(controller.getCatalog().length, 1); + controller.disconnect(); + assert.equal(controller.getCatalog().length, 0); + assert.equal(controller.getState().connected, false); +}); diff --git a/tests/classroomInstructor.feature.test.mjs b/tests/classroomInstructor.feature.test.mjs index f0d3111..8a14978 100644 --- a/tests/classroomInstructor.feature.test.mjs +++ b/tests/classroomInstructor.feature.test.mjs @@ -56,7 +56,7 @@ function observeBody(id, statement) { }; } -function setup(fetchImpl, { onObservation = () => {}, onObservationEnd = () => {} } = {}) { +function setup(fetchImpl, { onObservation = () => {}, onObservationEnd = () => {}, onClassConnected = () => {}, onClassDisconnected = () => {} } = {}) { dom = new JSDOM(INDEX_HTML, { url: 'https://intake.test/' }); persistExperienceRolePreference(EXPERIENCE_ROLE_IDS.INSTRUCTOR, dom.window.localStorage); initExperienceRoleController({ @@ -95,6 +95,8 @@ function setup(fetchImpl, { onObservation = () => {}, onObservationEnd = () => { toast: () => {}, onObservation, onObservationEnd, + onClassConnected, + onClassDisconnected, setTimeoutImpl: fn => { calls.timers.push(fn); return calls.timers.length; @@ -258,3 +260,24 @@ test('Instructor observer lifecycle emits coaching integration hooks without gra await settle(); assert.ok(ended >= 1, 'leaving Instructor ends the coaching observation context'); }); + + +test('Instructor class lifecycle exposes protected-resource capability hooks', async () => { + const connected = []; + let disconnected = 0; + const env = setup(async url => { + if (url === '/api/classes/workspaces') return response(200, rosterBody()); + if (url.includes(W1)) return response(200, observeBody(W1, 'First')); + return response(404, {}); + }, { + onClassConnected: token => connected.push(token), + onClassDisconnected: () => { disconnected += 1; } + }); + + await env.controller.openClass(TOKEN); + assert.deepEqual(connected, [TOKEN]); + + applyExperienceRole(EXPERIENCE_ROLE_IDS.STANDALONE); + await settle(); + assert.ok(disconnected >= 2, 'activation reset and role pause both clear protected-resource context'); +}); From 35acd62d35a0826e8e350507a16c187ffceafbc1 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:04:05 -0400 Subject: [PATCH 010/100] feat: load authorized Case Studies in resource drawer --- src/templates.js | 16 +++++ src/templatesDrawer.js | 70 +++++++++++++++---- tests/templates.unit.test.mjs | 20 ++++++ ...templatesRoleAvailability.feature.test.mjs | 43 +++++++++++- 4 files changed, 136 insertions(+), 13 deletions(-) diff --git a/src/templates.js b/src/templates.js index c760a92..9b28e8b 100644 --- a/src/templates.js +++ b/src/templates.js @@ -278,6 +278,22 @@ export function listTemplateModes() { return TEMPLATE_MODES.map(mode => ({ ...mode })); } +/** + * Project an authorized external template/Case Study state through the canonical mode rules. + * + * @param {import('./storage.js').SerializedAppState} fullState Full authorized state. + * @param {keyof typeof MODE_RULES|string} modeId Requested presentation mode. + * @param {string[]} [supportedModes] Modes authorized by the resource metadata. + * @returns {import('./storage.js').SerializedAppState|null} Projected payload or null. + */ +export function projectTemplateState(fullState, modeId, supportedModes = Array.from(MODE_INDEX.keys())) { + if (!fullState || typeof fullState !== 'object' || typeof modeId !== 'string') return null; + const normalizedMode = MODE_INDEX.has(modeId.trim()) ? modeId.trim() : null; + const normalizedSupported = sanitizeSupportedModes(supportedModes); + if (!normalizedMode || !normalizedSupported.includes(normalizedMode)) return null; + return projectState(normalizeManifestState(fullState), /** @type {keyof typeof MODE_RULES} */ (normalizedMode)); +} + /** * Retrieve the serialized payload for a given template and mode. * @param {string} templateId - Template identifier from {@link listTemplates}. diff --git a/src/templatesDrawer.js b/src/templatesDrawer.js index d64bc8a..d070629 100644 --- a/src/templatesDrawer.js +++ b/src/templatesDrawer.js @@ -16,6 +16,7 @@ import { listTemplates, listTemplateModes, getTemplatePayload, + projectTemplateState, TEMPLATE_MODE_IDS, TEMPLATE_KINDS } from './templates.js'; @@ -28,8 +29,17 @@ import { } from './templateAvailability.js'; import { showToast } from './toast.js'; -const templateRecords = listTemplates(); -const templateIndex = new Map(templateRecords.map(template => [template.id, template])); +const publicTemplateRecords = listTemplates(); +let protectedCaseStudiesProvider = null; + +function getTemplateRecords() { + const protectedRecords = protectedCaseStudiesProvider?.getCatalog?.() || []; + const seen = new Set(publicTemplateRecords.map(record => record.id)); + return [ + ...publicTemplateRecords, + ...protectedRecords.filter(record => record && !seen.has(record.id)) + ]; +} const modeRecords = listTemplateModes(); const modeIndex = new Map(modeRecords.map(mode => [mode.id, mode])); @@ -58,7 +68,7 @@ let templatesDrawerOpen = false; let templatesDrawerReady = false; let templatesReturnFocus = null; -let selectedTemplateId = templateRecords.length ? templateRecords[0].id : null; +let selectedTemplateId = publicTemplateRecords.length ? publicTemplateRecords[0].id : null; let selectedModeId = modeIndex.has(TEMPLATE_MODE_IDS.FULL) ? TEMPLATE_MODE_IDS.FULL : modeRecords.length @@ -69,7 +79,7 @@ function getSelectedTemplateMeta() { if (!selectedTemplateId) { return null; } - return templateIndex.get(selectedTemplateId) || null; + return getTemplateRecords().find(template => template.id === selectedTemplateId) || null; } /** @@ -87,7 +97,7 @@ function getResourcePolicy() { * @returns {Array} Available template metadata records. */ function getAvailableTemplateRecords() { - return filterTemplatesForExperience(getActiveExperienceRole(), templateRecords); + return filterTemplatesForExperience(getActiveExperienceRole(), getTemplateRecords()); } /** @@ -449,7 +459,7 @@ function handleTemplateClick(event) { } event.preventDefault(); const id = target.dataset.templateId; - if (!id || id === selectedTemplateId || !templateIndex.has(id)) { + if (!id || id === selectedTemplateId || !getTemplateRecords().some(template => template.id === id)) { return; } selectedTemplateId = id; @@ -511,7 +521,7 @@ function validatePassword() { return true; } -function applySelectedTemplate() { +async function applySelectedTemplate() { if (!templatesDrawerReady || !selectedTemplateId) { return; } @@ -527,11 +537,36 @@ function applySelectedTemplate() { if (requiresPassword && !validatePassword()) { return; } - const payload = getTemplatePayload(selectedTemplateId, modeToApply); + + let payload = null; + if (isStandardTemplate(templateMeta)) { + payload = getTemplatePayload(selectedTemplateId, modeToApply); + } else { + if (!protectedCaseStudiesProvider?.getPayload) { + setPasswordError('This Case Study is not available in the current class.'); + return; + } + if (templatesApplyBtn) templatesApplyBtn.disabled = true; + const protectedRecord = await protectedCaseStudiesProvider.getPayload(selectedTemplateId); + if (protectedRecord) { + payload = projectTemplateState( + protectedRecord.state, + modeToApply, + protectedRecord.supportedModes + ); + } + } + if (!payload) { - setPasswordError('That mode is not available for the selected template.'); + setPasswordError( + requiresPassword + ? 'This Case Study could not be loaded for the selected mode.' + : 'That mode is not available for the selected template.' + ); + updateApplyButtonState(); return; } + const rollbackSnapshot = collectAppState(); try { applyAppState(payload); @@ -545,12 +580,14 @@ function applySelectedTemplate() { } } setPasswordError('Unable to apply the template. Please try again.'); + updateApplyButtonState(); return; } if (templatesPasswordInput) { templatesPasswordInput.value = ''; } clearPasswordError(); + updateApplyButtonState(); closeTemplatesDrawer(); const modeMeta = modeIndex.get(modeToApply); const templateName = templateMeta ? templateMeta.name : 'Template'; @@ -646,7 +683,7 @@ function wireDrawerInternalEvents() { templatesModeGroup.addEventListener('click', handleModeClick); } if (templatesApplyBtn) { - templatesApplyBtn.addEventListener('click', applySelectedTemplate); + templatesApplyBtn.addEventListener('click', () => { void applySelectedTemplate(); }); } if (templatesPasswordInput) { templatesPasswordInput.addEventListener('input', () => { @@ -664,6 +701,9 @@ function wireDrawerInternalEvents() { closeTemplatesDrawer({ skipFocus: true }); refreshResourcePresentation(); }); + window.addEventListener('intake:protected-case-studies-changed', () => { + refreshResourcePresentation(); + }); } /** @@ -672,8 +712,14 @@ function wireDrawerInternalEvents() { * * @returns {void} */ -export function initTemplatesDrawer() { - if (templatesDrawerReady) return; +export function initTemplatesDrawer({ protectedCaseStudies = null } = {}) { + if (protectedCaseStudies) { + protectedCaseStudiesProvider = protectedCaseStudies; + } + if (templatesDrawerReady) { + refreshResourcePresentation(); + return; + } templatesBtn = document.getElementById('templatesBtn'); templatesDrawer = document.getElementById('templatesDrawer'); templatesBackdrop = document.getElementById('templatesBackdrop'); diff --git a/tests/templates.unit.test.mjs b/tests/templates.unit.test.mjs index 683985f..735366c 100644 --- a/tests/templates.unit.test.mjs +++ b/tests/templates.unit.test.mjs @@ -7,6 +7,7 @@ import { __dangerousSetTemplateManifestForTests as setTemplateManifest, getTemplateMetadata, getTemplatePayload, + projectTemplateState, listTemplateModes, listTemplates } from '../src/templates.js'; @@ -254,3 +255,22 @@ test('mode projections filter KT table columns per mode', () => { assert.equal(dcQuestion.di, 'Differentiator', 'd&c mode surfaces distinctions'); assert.equal(dcQuestion.ch, 'Changes', 'd&c mode surfaces changes'); }); + + +test('projectTemplateState applies canonical mode rules to authorized external payloads', () => { + const payload = projectTemplateState( + buildState(), + TEMPLATE_MODE_IDS.IS_IS_NOT, + [TEMPLATE_MODE_IDS.IS_IS_NOT, TEMPLATE_MODE_IDS.FULL] + ); + assert.ok(payload); + const row = payload.table.find(item => item.questionId === 'what-object'); + assert.equal(row.is, 'Object is failing'); + assert.equal(row.no, 'Objects not failing'); + assert.equal(row.di, ''); + assert.equal(row.ch, ''); + assert.equal( + projectTemplateState(buildState(), TEMPLATE_MODE_IDS.DC, [TEMPLATE_MODE_IDS.FULL]), + null + ); +}); diff --git a/tests/templatesRoleAvailability.feature.test.mjs b/tests/templatesRoleAvailability.feature.test.mjs index d4a6cf4..e996b59 100644 --- a/tests/templatesRoleAvailability.feature.test.mjs +++ b/tests/templatesRoleAvailability.feature.test.mjs @@ -13,6 +13,7 @@ import { } from '../src/experienceRoleController.js'; import { EXPERIENCE_ROLE_IDS } from '../src/experienceRoles.js'; import { initTemplatesDrawer } from '../src/templatesDrawer.js'; +import { collectAppState } from '../src/appState.js'; import { installJsdomGlobals, restoreJsdomGlobals } from './helpers/jsdom-globals.js'; const INDEX_HTML = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); @@ -29,7 +30,29 @@ initExperienceRoleController({ storage: dom.window.localStorage, location: dom.window.location }); -initTemplatesDrawer(); +const protectedProvider = { + getCatalog: () => [{ + id: 'authorized-case', + name: 'Authorized Case', + description: 'Loaded only after classroom authorization.', + templateKind: 'case-study', + supportedModes: ['intake', 'is-is-not', 'dc', 'full'] + }], + getPayload: async caseStudyId => { + if (caseStudyId !== 'authorized-case') return null; + const state = collectAppState(); + state.pre.oneLine = 'Protected Case Study applied'; + return { + id: 'authorized-case', + name: 'Authorized Case', + description: 'Loaded only after classroom authorization.', + templateKind: 'case-study', + supportedModes: ['intake', 'is-is-not', 'dc', 'full'], + state + }; + } +}; +initTemplatesDrawer({ protectedCaseStudies: protectedProvider }); after(() => { restoreJsdomGlobals(globals); @@ -76,3 +99,21 @@ test('drawer projects normal resources across Standalone, Student, and Instructo assert.equal(resourceKinds().includes('case-study'), false); assert.equal(dom.window.document.getElementById('templatesDrawerTitle').textContent, 'Templates'); }); + + +test('authorized Case Study payload is fetched only when Student applies it', async () => { + applyExperienceRole(EXPERIENCE_ROLE_IDS.STUDENT); + const caseButton = dom.window.document.querySelector('#templatesList [data-template-id="authorized-case"]'); + assert.ok(caseButton); + caseButton.click(); + + const password = dom.window.document.getElementById('templatesPassword'); + const minutes = String(new Date().getMinutes()).padStart(2, '0'); + password.value = `full${minutes}`; + dom.window.document.getElementById('templatesApplyBtn').click(); + + await new Promise(resolve => setImmediate(resolve)); + await new Promise(resolve => setImmediate(resolve)); + + assert.equal(dom.window.document.getElementById('oneLine').value, 'Protected Case Study applied'); +}); From 1b4371c35d70ddb399b9755852f99ed7d0e51d5c Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:04:31 -0400 Subject: [PATCH 011/100] docs: checkpoint protected Case Study delivery --- docs/classroom-workstream.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/classroom-workstream.md b/docs/classroom-workstream.md index 39e70dd..c36415e 100644 --- a/docs/classroom-workstream.md +++ b/docs/classroom-workstream.md @@ -159,7 +159,12 @@ Continue #295 on `feature/classroom-protected-cases`. The confirmed leak is the - existing mode projection/pedagogical unlock behavior is preserved after authorization; - static-bundle regression tests prove protected Case Study content is absent from public generated assets. - Delivery-safe parts: generator/runtime split → server authorization/API → Student/Instructor drawer integration → bundle guard/docs/final gates. -- Exact next action: inspect drawer + authoring/build contracts, then implement the generator/public-runtime split as the first durable checkpoint. +- Public/server manifest split checkpoint: `5d8ecb756de179276164d575cfc4ea1a03ef7115`; Template Manifest Guard green. +- Authorized server API checkpoint: `5dfd2718a2fefc2357afb22a5a906cd3f045e970`. +- Authorized client/lifecycle checkpoint: `83e278b87392c7be5eb8ba8ec4f19d84a0b94def`. +- Drawer integration checkpoint: `35acd62d35a0826e8e350507a16c187ffceafbc1`. +- Student/Instructor protected catalog is in-memory only; Case Study payload fetch uses authenticated POST after the pedagogical unlock; Standalone has no protected provider context. +- Exact next action: inspect canonical gates on `35acd62...`, fix findings, then add final import/static-bundle guard + docs/security review. ## Known risks / watch items From b3160264adfac5d500711c22515bf21f27dff75d Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:08:31 -0400 Subject: [PATCH 012/100] security: exclude authored Case Studies from Vercel public output --- .vercelignore | 4 + package.json | 3 +- scripts/verify-protected-case-boundary.mjs | 98 ++++++++++++++++++++++ vercel.json | 2 +- 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 .vercelignore create mode 100644 scripts/verify-protected-case-boundary.mjs diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..84b0c4a --- /dev/null +++ b/.vercelignore @@ -0,0 +1,4 @@ +# Authored template JSON is build-time source only. +# Generated Standard Templates are committed under src/; protected Case Studies +# are committed only into the server-side api manifest. +templates/*.json diff --git a/package.json b/package.json index 2c78ec4..b8d91b3 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "verify:lockfile": "node scripts/verify-lockfile.js", "check:templates": "npm run build:templates && git diff --exit-code -- src/templates.manifest.js api/protected-case-studies.manifest.js", "repo:doctor": "node scripts/repo-doctor.js", - "quality": "npm run verify:lockfile && npm run repo:doctor && npm run verify:tests && npm run verify:summary && npm run verify:persistence && npm run lint && npm run check:templates && npm run check:storage-docs && npm test" + "quality": "npm run verify:lockfile && npm run repo:doctor && npm run verify:tests && npm run verify:summary && npm run verify:persistence && npm run lint && npm run check:templates && npm run verify:protected-cases && npm run check:storage-docs && npm test", + "verify:protected-cases": "node scripts/verify-protected-case-boundary.mjs" }, "dependencies": { "@neondatabase/serverless": "1.1.0" diff --git a/scripts/verify-protected-case-boundary.mjs b/scripts/verify-protected-case-boundary.mjs new file mode 100644 index 0000000..466dd5f --- /dev/null +++ b/scripts/verify-protected-case-boundary.mjs @@ -0,0 +1,98 @@ +#!/usr/bin/env node +/** + * @fileoverview Enforces the protected Case Study public/deployment boundary. + */ + +import { promises as fs } from 'node:fs'; + +import { PROTECTED_CASE_STUDY_MANIFEST } from '../api/protected-case-studies.manifest.js'; + +const root = new URL('../', import.meta.url); +const publicManifestUrl = new URL('../src/templates.manifest.js', import.meta.url); +const vercelIgnoreUrl = new URL('../.vercelignore', import.meta.url); +const vercelConfigUrl = new URL('../vercel.json', import.meta.url); + +async function read(url) { + return fs.readFile(url, 'utf8'); +} + +async function collectJavaScriptFiles(directoryUrl) { + const output = []; + const entries = await fs.readdir(directoryUrl, { withFileTypes: true }); + for (const entry of entries) { + const child = new URL(entry.name + (entry.isDirectory() ? '/' : ''), directoryUrl); + if (entry.isDirectory()) { + output.push(...await collectJavaScriptFiles(child)); + } else if (entry.name.endsWith('.js')) { + output.push(child); + } + } + return output; +} + +async function main() { + const [publicManifest, vercelIgnore, vercelConfigRaw] = await Promise.all([ + read(publicManifestUrl), + read(vercelIgnoreUrl), + read(vercelConfigUrl) + ]); + + const ignoredPatterns = vercelIgnore + .split(/\r?\n/u) + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + if (!ignoredPatterns.includes('templates/*.json')) { + throw new Error('Protected boundary requires .vercelignore to exclude templates/*.json from deployment.'); + } + + const vercelConfig = JSON.parse(vercelConfigRaw); + const buildCommand = typeof vercelConfig.buildCommand === 'string' ? vercelConfig.buildCommand : ''; + if (!buildCommand.includes('npm run verify:protected-cases')) { + throw new Error('Vercel buildCommand must run verify:protected-cases.'); + } + if (/build:templates|npm\s+run\s+build(?:\s|$)/u.test(buildCommand)) { + throw new Error('Vercel buildCommand must not regenerate manifests after authored template JSON is excluded.'); + } + + const browserFiles = [ + new URL('../main.js', import.meta.url), + new URL('../index.html', import.meta.url), + ...await collectJavaScriptFiles(new URL('../src/', import.meta.url)), + ...await collectJavaScriptFiles(new URL('../components/', import.meta.url)) + ]; + const browserSources = await Promise.all(browserFiles.map(async url => ({ + path: url.pathname, + content: await read(url) + }))); + + for (const caseStudy of PROTECTED_CASE_STUDY_MANIFEST) { + for (const protectedText of [caseStudy.id, caseStudy.name]) { + if (publicManifest.includes(protectedText)) { + throw new Error(`Protected Case Study text leaked into public manifest: ${protectedText}`); + } + const runtimeLeak = browserSources.find(file => file.content.includes(protectedText)); + if (runtimeLeak) { + throw new Error(`Protected Case Study text leaked into browser runtime ${runtimeLeak.path}: ${protectedText}`); + } + } + } + + for (const file of browserSources) { + if ( + file.content.includes('protected-case-studies.manifest') + || file.content.includes('PROTECTED_CASE_STUDY_MANIFEST') + ) { + throw new Error(`Browser runtime must not import the server-only protected manifest: ${file.path}`); + } + } + + console.log( + `[verify:protected-cases] Protected ${PROTECTED_CASE_STUDY_MANIFEST.length} Case Study payload(s); ` + + 'public manifest/runtime and Vercel deployment boundary verified.' + ); +} + +main().catch(error => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +}); diff --git a/vercel.json b/vercel.json index 8daf0a3..c1fb0f6 100644 --- a/vercel.json +++ b/vercel.json @@ -1,5 +1,5 @@ { - "buildCommand": "npm run build && npm test", + "buildCommand": "npm run verify:protected-cases", "outputDirectory": ".", "cleanUrls": true } From 6fa7d41f5cde0e187f85f419da0d6010ff7618bf Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:11:48 -0400 Subject: [PATCH 013/100] fix: complete protected Case Study client lifecycle --- src/classroomInstructor.js | 2 +- tests/templatesRoleAvailability.feature.test.mjs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/classroomInstructor.js b/src/classroomInstructor.js index 3cb6777..a504cbb 100644 --- a/src/classroomInstructor.js +++ b/src/classroomInstructor.js @@ -574,7 +574,6 @@ export function createInstructorClassroomController({ restoreLocal(); } saveSession(); - onClassConnected(token); renderDashboard(); renderRoster(); if (!selectedWorkspaceId && workspaces[0]) await selectWorkspace(workspaces[0].id); @@ -619,6 +618,7 @@ export function createInstructorClassroomController({ ? preferredWorkspaceId : workspaces[0]?.id || ''; saveSession(); + onClassConnected(token); renderDashboard(); renderRoster(); if (element('instructorClassCode')) element('instructorClassCode').value = ''; diff --git a/tests/templatesRoleAvailability.feature.test.mjs b/tests/templatesRoleAvailability.feature.test.mjs index e996b59..cd51eaf 100644 --- a/tests/templatesRoleAvailability.feature.test.mjs +++ b/tests/templatesRoleAvailability.feature.test.mjs @@ -13,7 +13,7 @@ import { } from '../src/experienceRoleController.js'; import { EXPERIENCE_ROLE_IDS } from '../src/experienceRoles.js'; import { initTemplatesDrawer } from '../src/templatesDrawer.js'; -import { collectAppState } from '../src/appState.js'; +import { getTemplatePayload, TEMPLATE_MODE_IDS } from '../src/templates.js'; import { installJsdomGlobals, restoreJsdomGlobals } from './helpers/jsdom-globals.js'; const INDEX_HTML = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); @@ -40,7 +40,7 @@ const protectedProvider = { }], getPayload: async caseStudyId => { if (caseStudyId !== 'authorized-case') return null; - const state = collectAppState(); + const state = getTemplatePayload('checkout-latency', TEMPLATE_MODE_IDS.FULL); state.pre.oneLine = 'Protected Case Study applied'; return { id: 'authorized-case', From 1d8da477560e96cb2927f311f190a9c7d466c286 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:13:22 -0400 Subject: [PATCH 014/100] test: initialize protected Case Study toast mock --- tests/templatesRoleAvailability.feature.test.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/templatesRoleAvailability.feature.test.mjs b/tests/templatesRoleAvailability.feature.test.mjs index cd51eaf..74eca55 100644 --- a/tests/templatesRoleAvailability.feature.test.mjs +++ b/tests/templatesRoleAvailability.feature.test.mjs @@ -19,6 +19,7 @@ import { installJsdomGlobals, restoreJsdomGlobals } from './helpers/jsdom-global const INDEX_HTML = readFileSync(new URL('../index.html', import.meta.url), 'utf8'); const dom = new JSDOM(INDEX_HTML, { url: 'https://intake.test/' }); const globals = installJsdomGlobals(dom.window); +globalThis.__toastMocks = { showToast: () => {} }; dom.window.requestAnimationFrame = callback => callback(); globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window); @@ -55,6 +56,7 @@ const protectedProvider = { initTemplatesDrawer({ protectedCaseStudies: protectedProvider }); after(() => { + delete globalThis.__toastMocks; restoreJsdomGlobals(globals); dom.window.close(); }); From ed6702a757737577a4f8446c4b0dd671b5cb78ff Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:15:26 -0400 Subject: [PATCH 015/100] docs: define protected Case Study security boundary --- SECURITY.md | 8 ++++++++ api/AGENTS.md | 12 ++++++++++++ src/AGENTS.md | 9 +++++++++ templates/AGENTS.md | 38 ++++++++++++++++++++++++++++++-------- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 9e90a8f..86cb49c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -29,6 +29,14 @@ A Student join capability alone must never enumerate class workspaces. Cross-cla Coaching authorization follows the same separation. Instructor GET/PUT/DELETE `/api/classes/coaching` is class-scoped by the Instructor capability and writes only `classroom_coaching_feedback`; it must never call the Student snapshot update path or increment a Student Intake revision. Student `GET /api/classes/coaching/student` resolves exactly one membership from the issued Student workspace capability, accepts no workspace selector, and has no write method. Coaching notes may contain instructional context about incident work and must not be logged or copied into analytics/error telemetry. Field fingerprints are change-detection evidence only, not credentials. +Protected Case Study delivery is a separate server boundary. The public browser manifest may contain **Standard Templates only**. Case Study metadata and payloads live in the server-only `api/protected-case-studies.manifest.js` and must never be imported by `main.js`, `src/`, `components/`, or any other browser runtime module. Instructor `GET/POST /api/classes/case-studies` is scoped by the active Instructor class capability. Student `GET/POST /api/classes/case-studies/student` resolves active classroom membership from the issued Student workspace capability; legacy Standalone collaboration capabilities do not authorize it. + +Protected Case Study catalog responses contain metadata only; payloads are selected in authenticated POST bodies, not query strings. All protected Case Study responses use `Cache-Control: no-store` and `Referrer-Policy: no-referrer`. The rotating mode password remains a pedagogical progression control and is **not** an authentication boundary. + +Authored JSON under `templates/*.json` is build-time source and is excluded from Vercel uploads with `.vercelignore`. Production must use committed generated manifests and run `npm run verify:protected-cases`; do not change the Vercel build back to a command that needs excluded authoring JSON. The verifier must remain in the canonical quality gate and must prove protected IDs/names are absent from public browser runtime assets. + +After an authorized Student applies a Case Study, its selected-mode content becomes that Student/team Intake and follows normal Intake/collaboration persistence. That is expected authorized use; the protected-delivery boundary prevents unauthenticated or Standalone retrieval of the original protected resource. + ## Supported code Security fixes target the current `main` branch. Dependency Review, CodeQL, Dependabot, repository quality checks, and GitHub branch rules are intended to prevent known regressions from entering `main`. diff --git a/api/AGENTS.md b/api/AGENTS.md index e9997ae..217b9e4 100644 --- a/api/AGENTS.md +++ b/api/AGENTS.md @@ -63,6 +63,17 @@ Instructor observation for #293 must **not** be inserted as a workspace alias. ` - Coaching PUT/DELETE must never route through collaboration snapshot mutation or advance `collaboration_workspaces.revision`. - Stable `targetId` values come from the client coaching registry; server validation must reject malformed IDs/status/note/fingerprint evidence. +## Protected Case Study delivery + +- `api/protected-case-studies.manifest.js` is server-only generated content. Never import it from browser runtime modules. +- Instructor `/api/classes/case-studies` access is scoped by `getClassByInstructor()`. +- Student `/api/classes/case-studies/student` access is scoped by `getStudentContext()` using the already-issued classroom workspace capability; do not accept a Student workspace selector or legacy collaboration token as a substitute. +- GET returns metadata catalog only. POST accepts `caseStudyId` in the JSON body and may return the full protected payload after authorization. +- Protected content and identifiers must not be added to URL query strings, logs, analytics, or error telemetry. +- Protected responses are always `no-store` / `no-referrer`. +- The Case Study mode password is pedagogical only; server authorization must never depend on it. +- The four authored Case Study JSON files remain repository source for maintainers but are excluded from Vercel deployment by `.vercelignore`. Production functions consume the committed server-only generated manifest. + ## Testing Any runtime change under `api/` is included in the repository test-change guard. @@ -78,3 +89,4 @@ Classroom API changes must preserve automated coverage for: - edit-capability kind restrictions; - legacy Standalone collaboration behavior. - coaching class scoping, independent feedback revisions, Student read-only access, and zero Student snapshot-revision changes. +- protected Case Study Instructor class scoping, Student membership-bound access, rejection of legacy collaboration capabilities, metadata-only catalogs, POST-only payload selection, and private response headers. diff --git a/src/AGENTS.md b/src/AGENTS.md index 2fcaf99..2505e94 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -13,3 +13,12 @@ Keep feature modules independently initialisable, own only their feature-specifi `coachableFields.js` owns stable coaching target identity and deterministic field fingerprints. Persistence identity must remain domain-based (`problem.one-line`, `kt.where-location`, etc.); DOM IDs and KT row bindings are rendering hooks only. Do not add ephemeral Possible Cause identifiers to the durable registry without first defining stable lifecycle identity. `classroomCoaching.js` owns Instructor coaching controls and Student read-only feedback presentation. Coaching is presentation plus a separate Classroom API channel: never add it to `collectAppState()`, `kt-intake-full-v2`, summaries, templates, or collaboration snapshot revisions. Student mode may only read feedback; Instructor controls must remain interactive while the observed Student Intake remains read-only. + + +## Protected Classroom resources + +`classroomCaseStudies.js` owns the authorized in-memory protected Case Study catalog/payload client. It receives active capabilities only through Student/Instructor lifecycle callbacks, keeps no localStorage/sessionStorage record, and clears catalog/context when the class role disconnects. Its public state must never expose a bearer capability. + +`templatesDrawer.js` may combine public Standard Template metadata with the current authorized in-memory Case Study catalog, but must fetch a protected payload only on demand and project it through `projectTemplateState()`. Standalone must work with no protected provider or classroom backend. + +Browser modules must never import `api/protected-case-studies.manifest.js` or refer to protected Case Study IDs/names. `npm run verify:protected-cases` enforces this boundary. The rotating Case Study password remains instructional UX, not authentication. diff --git a/templates/AGENTS.md b/templates/AGENTS.md index b1d71f6..c29a578 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -1,7 +1,7 @@ # Templates Directory Guidelines ## Scope -Applies to all files inside `templates/`. +Applies to all authored JSON files inside `templates/`. ## JSON Template Structure - Each file must export a single JSON object with the shape: @@ -15,13 +15,35 @@ Applies to all files inside `templates/`. "state": { /* SerializedAppState payload */ } } ``` -- `supportedModes` may omit entries that do not make sense for a given template, but it must never be empty. -- `templateKind` is a resource-kind axis, not an experience role. `standard` records are reusable Templates; `case-study` records are Case Studies. -- `src/templateAvailability.js` controls normal-UI visibility by experience role without duplicating this registry: Standalone = standard only, Student = both, Instructor = case-study teaching resources only. -- For Student application, `case-study` resources keep the rotating mode password while `standard` Templates skip password prompts and always load in `full` mode. -- **Security:** Case Study JSON is still compiled into the public static manifest until #295. Role hiding and the rotating password are not confidentiality or authentication controls. +- `supportedModes` may omit entries that do not make sense for a resource, but it must never be empty. +- `templateKind` is a resource-kind axis, not an experience role. `standard` records are reusable Templates; `case-study` records are protected teaching resources. - `state` must satisfy the `SerializedAppState` contract documented in `src/storage.js`. +## Security boundary + +Authored JSON is build-time source, not a production static asset. + +`npm run build:templates` validates **all** authored JSON and generates two explicit boundaries: + +- `src/templates.manifest.js` — public browser manifest containing Standard Templates only; +- `api/protected-case-studies.manifest.js` — server-only Case Study metadata + payloads. + +Never copy a Case Study into `src/templates.manifest.js`, `src/`, `main.js`, `components/`, or another browser-delivered file. Never import the server-only manifest from browser code. + +`.vercelignore` excludes `templates/*.json` from deployment, and production uses the committed generated manifests. `npm run verify:protected-cases` checks that authored JSON stays excluded and protected Case Study IDs/names do not leak into public browser runtime assets. + +Role policy remains separate from confidentiality: +- Standalone: public Standard Templates only; +- Student: public Standard Templates plus authorized protected Case Studies; +- Instructor: authorized protected Case Studies as teaching resources. + +The rotating Case Study mode password is pedagogy only. Authorization comes from the active classroom capability. + ## Editing Workflow -- After adding or updating JSON files, run `npm run build:templates` to regenerate `src/templates.manifest.js`. -- Keep metadata strings concise; the drawer truncates overly long labels. +1. Edit/add the authored JSON under `templates/`. +2. Run `npm run build:templates`. +3. Run `npm run verify:protected-cases`. +4. Run `npm run quality` before PR handoff. +5. Commit the authored JSON **and both generated manifests** together. + +Keep metadata strings concise; the drawer truncates overly long labels. From 0d4cf8f09f81fd3b7f4ce66ffc31b328515a6fdb Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:17:04 -0400 Subject: [PATCH 016/100] test: assert protected Case Study apply boundary --- tests/templatesRoleAvailability.feature.test.mjs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/templatesRoleAvailability.feature.test.mjs b/tests/templatesRoleAvailability.feature.test.mjs index 74eca55..00d8173 100644 --- a/tests/templatesRoleAvailability.feature.test.mjs +++ b/tests/templatesRoleAvailability.feature.test.mjs @@ -20,6 +20,15 @@ const INDEX_HTML = readFileSync(new URL('../index.html', import.meta.url), 'utf8 const dom = new JSDOM(INDEX_HTML, { url: 'https://intake.test/' }); const globals = installJsdomGlobals(dom.window); globalThis.__toastMocks = { showToast: () => {} }; +let lastAppliedState = null; +globalThis.__appStateMocks = { + collectAppState: () => ({}), + applyAppState: state => { lastAppliedState = state; }, + getSummaryState: () => ({}), + resetAnalysisId: () => {}, + getAnalysisId: () => '', + getLikelyCauseId: () => null +}; dom.window.requestAnimationFrame = callback => callback(); globalThis.requestAnimationFrame = dom.window.requestAnimationFrame.bind(dom.window); @@ -57,6 +66,7 @@ initTemplatesDrawer({ protectedCaseStudies: protectedProvider }); after(() => { delete globalThis.__toastMocks; + delete globalThis.__appStateMocks; restoreJsdomGlobals(globals); dom.window.close(); }); @@ -117,5 +127,5 @@ test('authorized Case Study payload is fetched only when Student applies it', as await new Promise(resolve => setImmediate(resolve)); await new Promise(resolve => setImmediate(resolve)); - assert.equal(dom.window.document.getElementById('oneLine').value, 'Protected Case Study applied'); + assert.equal(lastAppliedState?.pre?.oneLine, 'Protected Case Study applied'); }); From 43952b6453a1cf9a5f0636a52afd8a4422cf419f Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:18:42 -0400 Subject: [PATCH 017/100] docs: document protected Case Study architecture --- docs/architecture-overview.md | 16 +++++++------ docs/classroom-api.md | 44 ++++++++++++++++++++++++++++++++-- docs/classroom-architecture.md | 28 ++++++++++------------ 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 347027d..7e9ca71 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -18,7 +18,7 @@ This document explains how the intake app boots, which modules own which DOM reg 5. **Initialize feature modules with save hooks** – - `initPreface({ onSave: saveAppState })` wires `[section:preface]` / `[section:impact]` inputs. - `initializeCommunications({ onSave: saveAppState, showToast })` prepares the communications drawer DOM. - - `initCommsDrawer()` and `initTemplatesDrawer()` set up the slide-in panels triggered from the header anchors. + - `initCommsDrawer()` sets up communications; `initClassroomCaseStudies()` creates the in-memory authorized protected-resource client; `initTemplatesDrawer({ protectedCaseStudies })` combines public Standard Templates with the current authorized Classroom Case Study catalog. - `initStepsFeature({ onSave: saveAppState, onLog: logCommunication })` mounts the checklist UI and lets checkbox toggles emit communications log entries. 6. **Connect summary generation** – registers `getSummaryState` as the summary module’s state provider via `setSummaryStateProvider()`. 7. **Restore persisted state** – `restoreSavedIntake()` pulls any `kt-intake-full-v2` snapshot via `restoreFromStorage()` and rehydrates modules through `applyAppState()`. @@ -39,7 +39,7 @@ This document explains how the intake app boots, which modules own which DOM reg | `src/preface.js` | Manages `[section:preface]` + `[section:impact]` inputs, detection chips, mirror sync, and tokens such as `{OBJECT}` and `{DEVIATION}`. | Supplies `autoResize`, `updatePrefaceTitles`, `startMirrorSync`, `setBridgeOpenedNow`, `getPrefaceState`, `getObjectFull`, `getDeviationFull`. Receives `onSave` from `main.js`. | | `src/kt.js` | Owns `[section:table]`: builds the IS/IS NOT table, possible-cause cards, focus modes, and cause evidence previews. | Accepts callbacks from Preface & Toast via `configureKT()`. Provides `exportKTTableState`, `importKTTableState`, `getPossibleCauses`, and other helpers consumed by `appState` & summary modules. | | `src/comms.js` & `src/commsDrawer.js` | Handle the communications drawer DOM (`#commsDrawer` + backdrop), cadence inputs, log visibility, and logging buttons. | `initializeCommunications()` receives `onSave` + `showToast`. `initStepsFeature({ onLog: logCommunication })` lets checklist actions add log entries. `main.js` uses `getCommunicationElements()` to wire buttons. | -| `src/templatesDrawer.js`, `src/templateAvailability.js` & `src/templateExport.js` | Manage the shared Templates / Case Studies drawer, project resource kinds by experience role, and export the current state as reusable JSON. | `templateAvailability.js` is the policy bridge between independent experience-role and template-kind axes; the registry/projection remains in `templates.js`. Standalone gets Templates, Student gets Templates + password-gated Case Studies, and Instructor gets teaching-only Case Studies. | +| `src/templates.js`, `src/templatesDrawer.js`, `src/templateAvailability.js` & `src/templateExport.js` | Manage public Standard Templates, canonical mode projection, the shared resource drawer, role policy, and authoring export. | `src/templates.manifest.js` contains public Standards only. The drawer accepts an authorized `classroomCaseStudies` provider for Student/Instructor protected metadata/payloads; Standalone has no protected provider context. | | `src/steps.js` | Controls the checklist drawer anchored near the table, maintains completion metrics, and exposes import/export helpers. | Accepts `{ onSave, onLog }` callbacks; exposes `exportStepsState`, `importStepsState`, `getStepsItems`, and `getStepsCounts` for `appState` + summary. | | `components/actions/ActionListCard.js` & `src/actionsStore.js` | Render the Actions card mounted after `#possibleCausesCard`, persist action items under `kt-actions-by-analysis-v1`, and notify listeners via `refreshActionList()`. | `appState` imports/exports actions alongside the main intake snapshot and dispatches `intake:actions-updated` events when rehydrated. | | `src/appState.js` | Central collector/rehydrator that aggregates feature state, converts it into a serialized snapshot, and reapplies it later. | Supplies `collectAppState`, `applyAppState`, `getSummaryState`, `resetAnalysisId`. Imports helpers from Preface, KT, communications, steps, and actions modules to do so. | @@ -50,10 +50,11 @@ This document explains how the intake app boots, which modules own which DOM reg | `src/collaboration.js` | Owns `[feature:collaboration]` and `[feature:collaboration-presence]`, revision-safe snapshot synchronization, editable team and personal identity dialogs, participant presence heartbeats, the shared browser-tab title, and conflict recovery. | Receives `collectAppState`, `applyAppState`, local-save, and toast callbacks from `main.js`; presence and team metadata use a separate API and `kt-collaboration-profile-v1` so identity changes never alter intake revisions. | | `src/classroomStudent.js` | Owns Student class admission, local-only resume/recovery, Student context chrome, role pause/resume, and explicit Leave class. | Calls `/api/classes/join` once with class + assignment capabilities, retains only the issued workspace capability under `kt-classroom-student-session-v1`, then delegates all live editing/presence to `src/collaboration.js`. | | `src/classroomInstructor.js` | Owns Instructor class resume, class-scoped roster/search/filter, workspace selection, observer polling, read-only DOM projection, and local Intake restoration. | Persists only `kt-classroom-instructor-session-v1`; polls `/api/classes/workspaces` and GET-only `/api/classes/observe`; observed snapshots render through the existing Intake DOM inside a localStorage preservation boundary. | +| `src/classroomCaseStudies.js` | Owns authorized in-memory protected Case Study catalog/payload access for Student and Instructor roles. | Receives active Classroom capabilities only from lifecycle callbacks, uses bearer-authenticated GET/POST endpoints, never persists the capability/catalog/payload, and clears context on role/class disconnect. | | `src/coachableFields.js` | Owns the stable coaching target registry and versioned field fingerprints for 22 substantive static fields plus the 10 durable KT question rows. | Target IDs are domain identity; DOM IDs/KT bindings are only placement hooks. Formatting-only whitespace is normalized before fingerprinting. | | `src/classroomCoaching.js` | Owns Instructor per-target coaching controls, Student read-only feedback rendering, coaching polling, and changed-since-review presentation. | Uses `/api/classes/coaching` and `/api/classes/coaching/student`; never participates in `collectAppState()` or collaboration snapshot revisions. | | `api/_workspace.js` | Owns the existing Neon collaboration schema/handlers plus edit-capable alias resolution for classroom Student tokens and the internal-ID read helper used after Instructor authorization. | Legacy primary tokens remain valid. Alias resolution remains restricted to explicit edit kinds (`classroom-student`); Instructor observation never resolves through the editable alias path. | -| `api/_classroom.js` & `api/classes/*` | Own class metadata, hashed Instructor/Student/assignment capabilities, individual/group assignment, class-scoped listing/admission, rotation/revocation, Instructor read-only observation, and separate coaching feedback persistence. | Student join mints an editable alias for collaboration; Instructor observation and coaching use class-scoped APIs. Coaching writes only `classroom_coaching_feedback` and Student coaching access is GET-only for the membership behind its issued workspace capability. See `docs/classroom-api.md`. | +| `api/_classroom.js`, `api/_protectedCaseStudies.js` & `api/classes/*` | Own class metadata/capabilities, workspace membership, observation/coaching authorization, and protected Case Study delivery. | Protected Case Studies use the server-only generated manifest: Instructor access is class-scoped, Student access is membership-bound, GET is metadata-only, and full payload selection is authenticated POST with private caching/referrer headers. See `docs/classroom-api.md`. | | `src/toast.js`, `src/versionStamp.js` | Provide shared UI niceties (toasts, footer stamp). | `showToast` is passed to modules that need user feedback; `initVersionStamp` is called once from `boot()`. | ### Callback graph highlights @@ -69,10 +70,11 @@ This document explains how the intake app boots, which modules own which DOM reg 2. **Student classroom resume** – `src/classroomStudent.js` reads/writes `kt-classroom-student-session-v1` and the pre-class `kt-classroom-student-local-recovery-v1` separately. Neither key is part of SerializedAppState; join/assignment capabilities are never persisted. 3. **Instructor classroom resume** – `src/classroomInstructor.js` reads/writes `kt-classroom-instructor-session-v1` separately. The Instructor class capability, public class metadata, and last selected public workspace ID never enter SerializedAppState, exports, summaries, or templates. 4. **Coaching feedback** – `src/classroomCoaching.js` has no Intake persistence key. Feedback lives server-side in `classroom_coaching_feedback`, with independent per-target revisions and reviewed field fingerprints; it never enters SerializedAppState, Save/Load, templates, summaries, or Student workspace revisions. -5. **State capture** – `saveAppState()` → `collectAppState()` → `saveToStorage()` writes JSON to `localStorage` (`kt-intake-full-v2`) and mirrors the latest actions under `kt-actions-by-analysis-v1`. -6. **File export/import** – Save/Load buttons call `exportAppStateToFile()` / `importAppStateFromFile()`, which use the same serialized payload so cross-browser transfers reuse the storage contract. -7. **Restore on boot** – `restoreSavedIntake()` reads from storage and passes the payload to `applyAppState()`. That function pushes each slice of data back into its owner module (Preface, KT, comms, steps, actions) before re-rendering possible causes and refreshing the Actions card. -8. **Summary generation** – When `generateSummary()` runs it asks the registered provider (`getSummaryState`) for a read-only bundle of DOM nodes, helper callbacks (e.g., `buildHypothesisSentence`, `getStepsCounts`), and user-entered data. Summary builders never mutate the DOM—they format strings for `[section:summary]`, clipboard copies, or AI prompts. +5. **Protected Case Study context** – `src/classroomCaseStudies.js` keeps authorized catalog/capability context in memory only. Source Case Study payloads are not local storage. After a Student deliberately applies a projected Case Study, that projected exercise content becomes ordinary Intake state and follows the normal Student/team persistence path. +6. **State capture** – `saveAppState()` → `collectAppState()` → `saveToStorage()` writes JSON to `localStorage` (`kt-intake-full-v2`) and mirrors the latest actions under `kt-actions-by-analysis-v1`. +7. **File export/import** – Save/Load buttons call `exportAppStateToFile()` / `importAppStateFromFile()`, which use the same serialized payload so cross-browser transfers reuse the storage contract. +8. **Restore on boot** – `restoreSavedIntake()` reads from storage and passes the payload to `applyAppState()`. That function pushes each slice of data back into its owner module (Preface, KT, comms, steps, actions) before re-rendering possible causes and refreshing the Actions card. +9. **Summary generation** – When `generateSummary()` runs it asks the registered provider (`getSummaryState`) for a read-only bundle of DOM nodes, helper callbacks (e.g., `buildHypothesisSentence`, `getStepsCounts`), and user-entered data. Summary builders never mutate the DOM—they format strings for `[section:summary]`, clipboard copies, or AI prompts. ### Flow diagram diff --git a/docs/classroom-api.md b/docs/classroom-api.md index 45e01b3..e725723 100644 --- a/docs/classroom-api.md +++ b/docs/classroom-api.md @@ -1,6 +1,6 @@ # Classroom API and Capability Contract -This document is the canonical server contract for Classroom Experience slice #291. It complements `docs/classroom-architecture.md` so future human or AI sessions can extend Student, Instructor, coaching, and protected Case Study work without reconstructing authorization decisions from code. +This document is the canonical server contract for Classroom Experience slices #291–#295. It complements `docs/classroom-architecture.md` so future human or AI sessions can extend Student, Instructor, coaching, and protected Case Study work without reconstructing authorization decisions from code. ## Core model @@ -177,6 +177,43 @@ Requires the Student's issued classroom workspace capability. The server resolve The current product has no stable named Instructor account, so class Instructor authority is the reviewer context for #294. A rotating bearer-token hash is not stored as fake human identity. Each record stores both the Student workspace revision visible when reviewed and a field-specific versioned fingerprint. The fingerprint is the primary evidence for **changed since review**, so unrelated edits elsewhere in the Intake do not stale every feedback record. + +## Protected Case Study delivery (#295) + +Protected Case Studies do not use the public template manifest and require an active Classroom capability before metadata or payload is returned. + +### `GET /api/classes/case-studies` + +Requires the Instructor class capability. Returns the protected Case Study catalog for the represented class context. Catalog entries contain only `id`, `name`, `description`, `templateKind`, and `supportedModes`; they do **not** contain `state`. + +### `POST /api/classes/case-studies` + +Requires the Instructor class capability. Body: + +- `caseStudyId` + +Returns the full protected Case Study record only after class authorization. Case selection belongs in the JSON body, never a URL/query string. + +### `GET /api/classes/case-studies/student` + +Requires the Student's issued classroom workspace capability. The server resolves active membership through `getStudentContext()`. It accepts no workspace selector and does not accept a legacy Standalone collaboration capability as substitute authority. + +Returns the metadata-only protected catalog. + +### `POST /api/classes/case-studies/student` + +Uses the same Student membership authorization and accepts `caseStudyId` in the JSON body. On success it returns the full protected record for client-side projection through the canonical mode rules. + +All protected Case Study responses use `Cache-Control: no-store` and `Referrer-Policy: no-referrer`. + +The existing rotating Case Study mode password remains an instructional progression control in the Student drawer; it is not sent to or trusted by the server for authorization. An authorized client can request the raw Case Study payload directly, which is intentional: Classroom capability is the confidentiality boundary. + +The build produces two generated resources: + +- `src/templates.manifest.js` — public Standard Templates only; +- `api/protected-case-studies.manifest.js` — server-only Case Study metadata and payloads. + +Authored `templates/*.json` files are excluded from Vercel uploads. Production functions consume the committed server-only manifest, while GitHub CI continues to validate all authored JSON and generated-file freshness. ## Individual and group semantics ### Individual @@ -242,10 +279,13 @@ No classroom credential is stored in browser Intake state. - Student workspace capabilities may read coaching only for their resolved membership and never write coaching. - Existing Standalone collaboration remains unchanged. - Case Study unlock passwords are unrelated to classroom authorization. +- Protected Case Study catalogs/payloads require Instructor-class or Student-membership authorization and are never returned to Standalone. +- Protected Case Study IDs/names/payloads must be absent from public browser assets; authored `templates/*.json` files must remain excluded from Vercel deployment. +- Protected Case Study selection uses authenticated POST bodies rather than query strings. ## Tests and cold restart -`tests/classroom-api.unit.test.mjs` exercises the authorization matrix using deterministic in-memory repositories from `tests/helpers/classroom-test-repositories.mjs`. +`tests/classroom-api.unit.test.mjs` exercises the core classroom authorization matrix using deterministic in-memory repositories from `tests/helpers/classroom-test-repositories.mjs`. `tests/protectedCaseStudies.api.test.mjs` covers protected Case Study Instructor/Student authorization, metadata-only catalogs, legacy-token rejection, POST payload delivery, and private response headers. The repository test-change guard treats `api/` as runtime code, so future server changes require test changes. diff --git a/docs/classroom-architecture.md b/docs/classroom-architecture.md index 9e6fdd7..4b9a961 100644 --- a/docs/classroom-architecture.md +++ b/docs/classroom-architecture.md @@ -32,7 +32,7 @@ Do not collapse these concepts into one mode. 6. Coaching cannot share the Intake snapshot revision channel. 7. Student credentials cannot enumerate classmates or other workspaces. 8. Instructor access, student class admission, and Case Study unlock passwords are separate credential concepts. -9. Hiding Case Studies from Standalone is not a confidentiality boundary; protected Case Study payloads must ultimately be removed from the public bundle. +9. Protected Case Study metadata/payloads must remain outside public browser/static assets and require Classroom authorization before server delivery. 10. Existing ad-hoc collaboration remains supported for Standalone unless a later explicitly approved migration replaces it. ## Target domain model @@ -217,21 +217,17 @@ Changing a target ID is a data migration. The existing `templateKind` distinction remains useful. -Transitional state (#290): +Implemented state (#295): -- Standard Template content remains client-bundled and is available in Standalone and Student. -- Case Study content remains client-bundled, but `src/templateAvailability.js` makes normal UI availability role-aware: Standalone hides it, Student can apply it through the existing mode/password flow, and Instructor sees it as a teaching resource rather than starter content for an instructor Intake. -- Instructor teaching resources deliberately have no apply action until class assignment exists. -- The existing rotating Case Study password remains pedagogy only. -- **This is visibility, not confidentiality.** A technically capable user can still inspect the current public bundle until #295 removes protected Case Study payloads from static assets. - -Target state: - -- public manifest contains Standard Templates only; -- protected Case Study metadata/payloads are delivered by authorized server endpoints; -- Standalone production assets contain no protected case payloads; -- Student/Instructor authorization is evaluated before case content is returned; -- rotating Case Study passwords may remain as instructional progression controls if useful. +- `src/templates.manifest.js` is the public browser manifest and contains Standard Templates only. +- `api/protected-case-studies.manifest.js` is server-only generated content containing protected Case Study metadata + full payloads. +- All authored JSON remains under `templates/` for maintainers and is validated by `npm run build:templates`, but `.vercelignore` excludes `templates/*.json` from deployment so raw authoring files cannot be fetched as static assets. +- Student and Instructor clients receive protected catalog metadata only after Classroom authorization. Full Case Study state is fetched on demand with an authenticated POST and then projected through the same `MODE_RULES` path used by public Templates. +- `src/classroomCaseStudies.js` keeps authorized catalog/context in memory only and clears it when Classroom context disconnects. +- Standalone has no protected provider context and can operate with the Classroom backend unavailable. +- The rotating Case Study password remains pedagogy only; the Classroom capability is the confidentiality boundary. +- Instructor class capability currently authorizes the class teaching catalog. There is no separate case-assignment table yet; a future case-level assignment model can narrow teaching availability without changing the public/private bundle boundary. +- Once an authorized Student applies a Case Study, the projected content becomes that Student/team Intake and follows normal Intake/collaboration persistence. The boundary prevents unauthorized source retrieval; it is not DRM against an authorized learner. ## Server persistence @@ -244,7 +240,7 @@ Slice #291 adds these domain tables: - `classroom_workspaces` for individual/group assignment metadata and assignment capability hashes; - `classroom_memberships` for participant-to-workspace membership and per-participant workspace capability hashes. -Slice #294 adds `classroom_coaching_feedback` as a separate class/workspace/target channel. Protected Case Study assignment/access metadata remains future work. +Slice #294 adds `classroom_coaching_feedback` as a separate class/workspace/target channel. Slice #295 adds no new database table: protected Case Study access reuses the existing Instructor class capability and Student classroom membership capability. Case-level assignment metadata remains a possible future narrowing layer. Schema initialization/migration must be idempotent and documented. Avoid making browser boot depend on schema creation for Standalone. From b8324e362bbe3641d4791d72f4ec9d173899ca46 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:29:42 -0400 Subject: [PATCH 018/100] docs: update protected Case Study README --- README.md | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fb78cdf..7e9c08e 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ KT Intake is a browser-first Kepner–Tregoe (KT) incident workbook designed for - A genuinely new browser asks whether to **Work independently**, **Join a class**, or **Teach a class**. Existing saved Intakes and existing `?workspace=` collaboration links migrate silently to **Standalone** so the new chooser does not interrupt established workflows. - The selected experience resumes from the separate `kt-experience-role-v1` preference. Intake work itself still loads from `kt-intake-full-v2`, with action plans under `kt-actions-by-analysis-v1`. - Use the header controls to **Save to File** (exports a JSON snapshot) or **Load from File** (imports a previously saved snapshot) when you need to move an intake between browsers or machines. -- Open the **Templates** drawer and click **Save current notes as template** to download the in-progress intake as curated template JSON. The prompt lets you choose between a **Case Study** template (password protected, multi-mode) or a **Standard** template (no password, always loads Full mode). +- Open the shared resource drawer to work with curated material. **Standalone** receives public Standard Templates only. Connected **Students** receive Standard Templates plus Classroom-authorized Case Studies; connected **Instructors** receive authorized teaching Case Studies. The rotating Case Study mode password remains a learning/progression control, not authentication. ## Development Setup AI contributors should run the following commands (or manual preview) whenever the described workstream applies so linting, templates, and docs stay current. @@ -18,12 +18,14 @@ AI contributors should run the following commands (or manual preview) whenever t | `npm ci` / `npm install` | Run once after cloning or whenever `package.json` changes. | Installs the pinned toolchain for scripts, tests, and template validation. Node 24 is the repository baseline; `.nvmrc` is authoritative. See [`docs/AI-ONBOARDING.md`](docs/AI-ONBOARDING.md). | | `npm run dev` | During day-to-day feature work that touches `src/`, `components/`, or `scripts/`. | Starts the watcher so template manifests regenerate automatically; pair it with the guidance in [`docs/commenting-guide.md`](docs/commenting-guide.md) when wiring new anchors. | | Open `index.html` directly | For quick manual QA or smoke tests that do not require the watcher. | The static file reflects the latest bundle after any build step, so you can double-check flows without Node running. | -| `npm run build` | Before opening a pull request or testing deployment changes. | Rebuilds the static bundle and regenerates `src/templates.manifest.js`. Mirrors the Vercel command noted below. | -| `npm run build:templates` | Immediately after editing JSON under `templates/` or `templates.manifest` logic. | Validates curated snapshots and should accompany any template-focused feature (see "Template manifest workflow" below). | +| `npm run build` | After editing authored JSON under `templates/` and before opening a pull request. | Validates all authored resources and regenerates both the public Standard Template manifest and server-only protected Case Study manifest. This is a repository-authoring command, not the Vercel production command. | +| `npm run build:templates` | Immediately after editing JSON under `templates/` or generated-manifest logic. | Validates every authored resource, writes public Standards to `src/templates.manifest.js`, and writes protected Case Studies to `api/protected-case-studies.manifest.js`. | | `npm run verify:tests` | Any time you change runtime code under `src/`, `components/`, or server `api/`. | Enforces the coverage contract described in [`docs/testing-guidelines.md`](docs/testing-guidelines.md) and scaffolds missing suites. | | `npm run verify:summary` | Whenever you add or change form controls/options. | Ensures new inputs are wired into the Copy & Paste Summary, documented, and styled with the Apple-like rhythm. See [`docs/summary-style-checklist.md`](docs/summary-style-checklist.md). | | `npm run verify:persistence` | When adding or editing inputs/captions that should survive reloads. | Confirms new controls tie into `src/appState.js` and `src/storage.js`, prompting template/state updates so saves/loads remain lossless. | -| `npm test` | Before committing or when adding new suites. | Runs the full test matrix (DOM + unit) so CI sees the same state you validated locally. |\n| `npm run quality` | Before marking any pull request ready. | Canonical repository gate: lockfile, repo doctor, domain guards, lint, generated-file freshness, storage docs, and the full test suite. See [`docs/REPOSITORY-OPERATIONS.md`](docs/REPOSITORY-OPERATIONS.md). | +| `npm test` | Before committing or when adding new suites. | Runs the full test matrix (DOM + unit) so CI sees the same state you validated locally. | +| `npm run verify:protected-cases` | After any template/classroom/deployment boundary change. | Proves protected Case Study IDs/names are absent from public browser runtime assets and that authored `templates/*.json` remains excluded from Vercel deployment. | +| `npm run quality` | Before marking any pull request ready. | Canonical repository gate: lockfile, repo doctor, domain guards, lint, generated-file freshness, protected-case boundary, storage docs, and the full test suite. See [`docs/REPOSITORY-OPERATIONS.md`](docs/REPOSITORY-OPERATIONS.md). | | `npm run update:storage-docs` / `npm run check:storage-docs` | Run `update` whenever you alter persisted schema, then `check` before pushing. | Keeps [`docs/storage-schema.md`](docs/storage-schema.md) and [`docs/storage-schema.appendix.md`](docs/storage-schema.appendix.md) synced with new keys or shapes. | ## Entry Point & Boot Logic @@ -57,6 +59,7 @@ See [`docs/architecture-overview.md`](docs/architecture-overview.md) for the boo | `src/actionsStore.js` | Persists actions by analysis ID under `kt-actions-by-analysis-v1`, providing CRUD and sorting helpers for the card UI. | | `src/coachableFields.js` | Stable domain coaching-target registry and deterministic versioned field fingerprints; DOM IDs are placement hooks, not persistence identity. | | `src/classroomCoaching.js` | Instructor coaching controls and Student read-only feedback UI backed by the separate Classroom coaching API. | +| `src/classroomCaseStudies.js` | In-memory authorized Classroom Case Study catalog/payload client. It receives active Student/Instructor capabilities from their lifecycle controllers and never persists them. | | `main.js` | Entry point that imports every module, wires shared events, and runs `boot()`. | ### Storage keys @@ -74,8 +77,8 @@ Coaching feedback is server-side Classroom data, not a local Intake storage key. Experience role is a product-level choice, not an Intake workflow mode. General / IT / Pharma / Major Incident remain controlled by `meta.intakeMode`; Standalone / Student / Instructor are controlled separately by `src/experienceRoles.js` and `src/experienceRoleController.js`. - **Standalone** exposes the normal Intake and current collaboration behavior. Its resource drawer contains **Templates only**. -- **Student** first joins an instructor-preconfigured class workspace using a class code, assignment code, and display name. The two admission codes are used once and discarded; the returned per-participant workspace capability is retained locally for same-device resume and is attached to the existing collaboration engine without entering the URL. Once connected, the Student sees class/workspace/identity context and can use both **Templates** and **Case Studies**. Switching away from Student pauses the live classroom connection while preserving resume; **Leave class** clears resume and restores the local Intake captured before joining. Students also receive Instructor coaching beside the reviewed field, read-only, including optional notes and a **Changed since review** indicator when that field's current evidence no longer matches the reviewed fingerprint. -- **Instructor** opens an instructor-preconfigured class with the Instructor class capability, gets a searchable individual/team workspace rail, and can rapidly switch a **live read-only** view of each Student/team Intake. Observation uses the normal Intake renderer but the Student-owned controls are projected read-only; the Instructor credential is authorized only through the class-scoped GET observer path and never becomes a Student edit capability. Switching away pauses observation while preserving same-device class resume; **Leave class** clears the Instructor resume and restores the instructor's prior local Intake. In that read-only view, instructors can mark registered fields **Meets standard** or **Needs improvement**, add an optional note, revise/clear feedback, and see when the reviewed field has changed. +- **Student** first joins an instructor-preconfigured class workspace using a class code, assignment code, and display name. The two admission codes are used once and discarded; the returned per-participant workspace capability is retained locally for same-device resume and is attached to the existing collaboration engine without entering the URL. Once connected, the Student sees class/workspace/identity context, public **Templates**, and a protected **Case Study** catalog fetched with that issued Classroom capability. Full Case Study state is fetched only on demand when the Student applies a case after the existing pedagogical mode/password step. Switching away from Student pauses the live classroom connection and clears the in-memory protected catalog while preserving class resume; **Leave class** clears resume and restores the local Intake captured before joining. Students also receive Instructor coaching beside the reviewed field, read-only, including optional notes and a **Changed since review** indicator when that field's current evidence no longer matches the reviewed fingerprint. +- **Instructor** opens an instructor-preconfigured class with the Instructor class capability, gets a searchable individual/team workspace rail, and can rapidly switch a **live read-only** view of each Student/team Intake. Observation uses the normal Intake renderer but the Student-owned controls are projected read-only; the Instructor credential is authorized only through the class-scoped GET observer path and never becomes a Student edit capability. The same Instructor class capability authorizes the protected teaching Case Study catalog/payload API. Switching away pauses observation and clears the in-memory protected catalog while preserving same-device class resume; **Leave class** clears the Instructor resume and restores the instructor's prior local Intake. In that read-only view, instructors can mark registered fields **Meets standard** or **Needs improvement**, add an optional note, revise/clear feedback, and see when the reviewed field has changed. - Use **View → Experience** to switch roles without changing or deleting Intake data. ## Notes workspace @@ -105,11 +108,14 @@ The existing Vercel project is **`intake`**, with the **`neon-intake`** integrat Classroom slice #291 layers class organization and authorization over the existing collaboration engine; it does **not** create a second Intake synchronization system. -The server adds three class routes: +The core class routes are: - `/api/classes` — create/administer one class with an Instructor bearer capability; - `/api/classes/workspaces` — Instructor-only creation/listing and assignment lifecycle for individual/group workspaces; -- `/api/classes/join` — Student admission into exactly one assigned workspace. +- `/api/classes/join` — Student admission into exactly one assigned workspace; +- `/api/classes/observe` — Instructor-only read-only live workspace observation; +- `/api/classes/coaching` and `/api/classes/coaching/student` — separate Instructor-write / Student-read coaching channel; +- `/api/classes/case-studies` and `/api/classes/case-studies/student` — protected Case Study catalog and POST payload delivery after Classroom authorization. A Student join requires **two independent capabilities**: the class Student-join capability and an assignment capability for one individual/group workspace. The Student join capability by itself cannot list assignments or classmates. A successful join returns a newly minted per-participant workspace capability that works with the existing `/api/workspaces/session` and `/api/workspaces/presence` endpoints. @@ -166,16 +172,20 @@ Need to know which module owns a given storage field? Jump to the [Storage-to-Mo - Keep the UI accessible: reuse layout classes, maintain contrast, and follow the Apple-like spacing guidance in `AGENTS.md`. ### Template manifest workflow -- Curated resources live as JSON snapshots under `templates/` (one file per resource). Each file lists metadata (`id`, `name`, `description`, `templateKind`, `supportedModes`) plus a `SerializedAppState` payload. -- `templateKind: standard` means a reusable **Template**; `templateKind: case-study` means a **Case Study**. `src/templateAvailability.js` projects those existing records by experience role without duplicating the registry: Standalone = Templates, Student = Templates + Case Studies, Instructor = teaching-only Case Studies. -- **Transitional security boundary:** Case Study payloads are still compiled into the public client manifest in this slice. Hiding them from Standalone is normal-UI visibility, **not confidentiality**. #295 moves protected Case Study metadata/payloads behind authorized server delivery. -- Run `npm run build:templates` after editing or adding template JSON. The script validates each snapshot and regenerates `src/templates.manifest.js`. -- `npm run dev` and `npm run build` automatically invoke the generator, so the manifest always stays in sync during local development. +- Curated resources live as authored JSON snapshots under `templates/` (one file per resource). Each file lists metadata (`id`, `name`, `description`, `templateKind`, `supportedModes`) plus a `SerializedAppState` payload. +- `npm run build:templates` validates **all** authored resources and generates two explicit boundaries: + - `src/templates.manifest.js` — public Standard Templates only; + - `api/protected-case-studies.manifest.js` — server-only Case Study metadata and full payloads. +- `templateKind: standard` means a reusable public **Template**. `templateKind: case-study` means a protected Classroom **Case Study**. `src/templateAvailability.js` still owns role semantics; `src/classroomCaseStudies.js` supplies only the currently authorized protected catalog/payloads. +- Authored `templates/*.json` files are build-time source and are excluded from Vercel deployment by `.vercelignore`. Never import the server-only protected manifest from browser modules. +- Run `npm run verify:protected-cases` after changing this boundary and `npm run quality` before handoff. The rotating Case Study mode password remains pedagogy only; Classroom capability authorization is the confidentiality boundary. +- `npm run dev` and local `npm run build` still regenerate both manifests for maintainers. Commit the authored JSON and both generated manifests together. ### Vercel deployment -- Production deploys on Vercel now execute `npm run build && npm test` (see `vercel.json`). The build step regenerates `src/templates.manifest.js` so templates remain in sync, and the test pass acts as a guardrail for regressions before traffic hits the static bundle. -- When modifying the manifest workflow or required quality gates, update both the README and `vercel.json` so the documented steps mirror the actual build command. -- Use `vercel build` (or run `npm run build && npm test`) locally to mirror the hosted environment whenever you change deployment requirements. +- Production Vercel builds execute `npm run verify:protected-cases` (see `vercel.json`). They **do not regenerate manifests**, because authored `templates/*.json` source files are deliberately excluded from the deployment upload. +- Generated manifests are therefore committed artifacts. GitHub CI remains responsible for running `npm run build:templates` / `check:templates` and proving they match their authored JSON sources. +- `.vercelignore` prevents raw authored Case Study JSON from becoming directly fetchable static files. Keep that exclusion and the protected-case verifier aligned with any future output-directory/deployment changes. +- When modifying the manifest workflow or required quality gates, update both this README and `vercel.json` so the documented steps mirror the actual hosted build. - Dependency changes must be installed through the normal npm registry so npm generates the complete lockfile. Run `npm run verify:lockfile` before committing and `npm ci` from a clean dependency tree; the offline guard catches missing resolved root-package entries before CI reaches its clean install. ## Documentation & anchor hygiene From 1f4343b5052520881812da60bf2e250148b7e28e Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:30:50 -0400 Subject: [PATCH 019/100] docs: teach protected Case Study cold starts --- docs/AI-ONBOARDING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/AI-ONBOARDING.md b/docs/AI-ONBOARDING.md index ea6e5ad..86b869d 100644 --- a/docs/AI-ONBOARDING.md +++ b/docs/AI-ONBOARDING.md @@ -25,6 +25,7 @@ For any work in the Standalone / Student / Instructor program (#288), also read | `src/classroomInstructor.js` | Instructor class resume, roster, read-only observation, and coaching-observation lifecycle hooks. | | `src/coachableFields.js` | Stable coaching target IDs and versioned field fingerprints; DOM placement is deliberately separate from persistence identity. | | `src/classroomCoaching.js` | Instructor coaching controls and Student read-only feedback rendering through the separate coaching API. | +| `src/classroomCaseStudies.js` | Authorized in-memory protected Case Study catalog/payload client. Receives active Classroom capabilities from Student/Instructor lifecycle controllers, never persists them, and clears context on disconnect. | | `src/comms.js` | `initializeCommunications()`, `logCommunication()`, `toggleLogVisibility()`, `setCadence()`, `setManualNextUpdate()`, `getCommunicationElements()`. | | `src/constants.js` | `ROWS`, `STEP_DEFINITIONS`, `CAUSE_FINDING_MODES`, and other deep-frozen config. Never mutate these directly. | | `src/kt.js` | `configureKT()`, `initTable()`, `ensurePossibleCausesUI()`, `renderCauses()` for the IS/IS NOT workflow. | @@ -36,7 +37,8 @@ For any work in the Standalone / Student / Instructor program (#288), also read | `src/summary.js` | `generateSummary()`, `setSummaryStateProvider()`, helpers that compose both clipboard output and AI prompts. | | `src/toast.js` | `showToast()` for lightweight notifications reused by comms and bootstrapping. | `api/_workspace.js` | Existing collaboration persistence/handlers plus explicitly edit-capable workspace aliases. Only `classroom-student` aliases may resolve through legacy read/write collaboration endpoints. | -| `api/_classroom.js` | Class schema/capabilities, class-scoped observation, Student admission, rotation/revocation, and separate Instructor-write/Student-read coaching feedback APIs. | +| `api/_classroom.js` | Class schema/capabilities, class-scoped observation, Student admission, rotation/revocation, Student membership lookup, and separate Instructor-write/Student-read coaching feedback APIs. | +| `api/_protectedCaseStudies.js` | Server-only protected Case Study catalog/payload delivery. Instructor access is class-scoped; Student access resolves active membership from the issued Student workspace capability. | ## Working Agreement for AI Agents - **Stay modular:** Add new behaviour by creating a file under `src/` and exporting named helpers. Only touch `main.js` to import and wire these helpers. @@ -45,6 +47,7 @@ For any work in the Standalone / Student / Instructor program (#288), also read - **Document as you go:** Follow the patterns in [`docs/commenting-guide.md`](./commenting-guide.md) to add module docblocks, update the anchor catalogue, and refresh README/`AGENTS.md` anchors before merging. - **Reuse constants:** Extend `src/constants.js` if new enumerations or immutable lists are required. Deep-freeze ensures downstream modules receive read-only copies. - **Preserve storage compatibility:** When saving extra Intake data, extend the shape emitted by `collectAppState()` and persisted by `saveToStorage()`. Experience/class/session context is different: experience role is explicitly stored outside SerializedAppState under `kt-experience-role-v1` and must remain excluded from Intake exports/templates/summaries. Always update `applyAppState()` so round-trip tests pass. +- **Protect Classroom resources:** Standard Templates may live in the public browser manifest. Protected Case Studies may not. Keep full Case Study metadata/payloads in `api/protected-case-studies.manifest.js`, keep authored `templates/*.json` excluded from Vercel deployment, and never import the server-only manifest from browser code. Run `npm run verify:protected-cases` after touching this boundary. - Review [`docs/storage-schema.md`](./storage-schema.md) whenever you change persisted shapes, run `npm run update:storage-docs` afterward, and run `npm run check:storage-docs` (or rely on the CI workflow) before submitting. Update any tests under `tests/` that assert on persisted state so they reflect the new schema. @@ -136,6 +139,7 @@ function boot() { - Always run a manual smoke test by opening `index.html` in a modern browser, entering sample data, generating a summary, and refreshing to confirm persistence. - Automated suites should rely on `collectAppState()` / `applyAppState()` for deterministic state setup and on `generateSummary()` for output verification. - Core local intake workflows can still run browser-first, but collaboration uses Vercel Functions under `api/` and Neon persistence. Use a deployed/HTTP environment with the required database variable when testing shared sessions; do not infer collaboration health from a `file://` smoke test. +- Protected Case Study production checks require HTTP/Vercel verification as well: raw `/templates/*.json` authoring files must not be fetchable, Standalone must not receive the protected catalog, and Student/Instructor catalog/payload requests must require their active Classroom capability. ## Safe Extension Checklist 1. Identify the owning module for the UI you are touching; update that module instead of `main.js`. From 98c655d4276e471813c2900b4b5b114dc485ac80 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:31:18 -0400 Subject: [PATCH 020/100] docs: update Classroom roadmap through protected cases --- docs/classroom-roadmap.md | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/classroom-roadmap.md b/docs/classroom-roadmap.md index 9766a86..bb29b1f 100644 --- a/docs/classroom-roadmap.md +++ b/docs/classroom-roadmap.md @@ -146,7 +146,7 @@ Primary deliverables: **Issue:** #294 -**Implementation:** draft PR #306 on `feature/classroom-coaching-feedback`; implementation/coverage complete pending final documented-head gates and merge. +**Implementation:** PR #306 merged; #294 complete. Stacks on instructor observer. @@ -164,18 +164,28 @@ Primary deliverables: **Issue:** #295 -Recommended branch: `feature/classroom-protected-cases` +**Implementation:** draft PR #307 on `feature/classroom-protected-cases`; runtime/security implementation complete, documentation/final gates in progress. -Requires #290 semantics and #291 authorization. +Requires #290 semantics and #291 authorization; both prerequisites are merged. -Primary deliverables: +Implemented deliverables: + +- public generated manifest contains Standard Templates only; +- server-only generated manifest contains protected Case Study metadata/payloads; +- Instructor class-scoped and Student membership-bound protected catalog/payload APIs; +- metadata-only GET catalog and authenticated POST payload delivery; +- in-memory Student/Instructor protected-resource client integrated with the shared drawer; +- Standalone remains protected-resource free and backend-optional; +- authored `templates/*.json` source excluded from Vercel deployment; +- protected Case Study public-runtime/deployment verifier in the canonical quality gate; +- authorization, bundle-boundary, and role/drawer regression tests; +- security/architecture/cold-start documentation migration. + +Remaining before merge: -- split public template build from protected cases; -- authenticated case catalog/payload API; -- static-bundle guard; -- authorized Student/Instructor flows; -- unauthorized request tests; -- deployment/docs migration. +- finish final documentation synchronization; +- verify the final documented head has CI, CodeQL, Dependency Review, and Template Manifest Guard green; +- review the complete PR diff and production-preview behavior, especially that raw `/templates/*.json` paths are no longer served. ## Slice 8 — Browser/E2E and required CI From 55c6fb70d2a57de2d29e3b94fa3101107e93c8e9 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:31:44 -0400 Subject: [PATCH 021/100] docs: register protected resource drawer ownership --- docs/commenting-guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/commenting-guide.md b/docs/commenting-guide.md index 9340094..0b11968 100644 --- a/docs/commenting-guide.md +++ b/docs/commenting-guide.md @@ -43,7 +43,7 @@ Anchors keep HTML and documentation in sync. Preserve existing tokens and regist | `[header]` | `index.html` | Encapsulates the hero title plus toolbar controls for collaboration, comms, steps, file transfer, templates, and summary triggers; Collaboration remains available in every intake mode while comms and steps are progressively disclosed. | `src/commsDrawer.js`, `src/steps.js`, `src/fileTransfer.js`, `src/templatesDrawer.js`, `src/summary.js`, `src/intakeModeController.js` | `docTitle`, `intakeModeSelect`, `commsBtn`, `stepsBtn`, `stepsDrawer`, `stepsCompletedLabel`, `startFreshBtn`, `saveToFileBtn`, `loadFromFileBtn`, `genSummaryBtn`; persisted via `kt-intake-full-v2`. | | `[feature:collaboration]` | `index.html`, Collaboration menu | Provides secret-link session actions, synchronization status, and conflict recovery choices in every intake mode. | `src/collaboration.js`, `api/workspaces/*` | `collaborationStatus`, session action buttons; recovery uses `kt-collaboration-recovery-v1` while intake snapshots remain under `kt-intake-full-v2`. | | `[feature:collaboration-presence]` | `index.html`, in the workspace dock immediately before Notes workspace | Shows the editable shared team name and all recently active participants, and provides the accessible start/join/edit identity dialog. | `src/collaboration.js`, `api/workspaces/presence.js` | `collaborationWorkspace`, `collaborationParticipants`, `collaborationDialog`; local identity preference uses `kt-collaboration-profile-v1`, team metadata stays server-side, and both are excluded from intake snapshots. | -| `[feature:templates-drawer]` | `index.html` header toolbar + Instructor shell launcher | Shared role-aware Templates / Case Studies drawer. Standalone renders Templates only; Student renders Templates + Case Studies; Instructor renders teaching-only Case Studies. | `src/templatesDrawer.js`, `src/templates.js`, `src/templateAvailability.js` | `templatesBtn`, `instructorTeachingResourcesBtn`, `templatesDrawer`, `templatesList`, `templatesModeSection`, `templatesAuthSection`, `templatesTeachingNotice`, `templatesDrawerFooter`; only applicable Student/Standalone resources can write Intake state. | +| `[feature:templates-drawer]` | `index.html` header toolbar + Instructor shell launcher | Shared role-aware Templates / Case Studies drawer. Standalone renders public Standard Templates only; connected Student/Instructor roles receive the current authorized protected Case Study catalog in memory. Student fetches a protected payload only when applying it after the pedagogical unlock. | `src/templatesDrawer.js`, `src/templates.js`, `src/templateAvailability.js`, `src/classroomCaseStudies.js` | `templatesBtn`, `instructorTeachingResourcesBtn`, `templatesDrawer`, `templatesList`, `templatesModeSection`, `templatesAuthSection`, `templatesTeachingNotice`, `templatesDrawerFooter`; Classroom capability/catalog state is local-only and excluded from Intake persistence. | | `[feature:experience-role]` | `index.html`, immediately after the header | Accessible first-run/switching dialog for Standalone, Student, and Instructor. | `src/experienceRoles.js`, `src/experienceRoleController.js` | `experienceRoleGate`, `experienceRoleDialog`, `experienceRoleCancelBtn`; preference uses `kt-experience-role-v1` and is excluded from Intake state. | | `[feature:experience-role-switch]` | `index.html`, View menu | Reopens the experience chooser without resetting Intake data. | `src/experienceRoleController.js`, `src/menuBar.js` | `experienceRoleMenuBtn`, `data-experience-role-label`; local-only, summary-excluded. | | `[feature:student-class-entry]` | `index.html`, between role chooser and Instructor shell | Student class join/reconnect gate shown before the assigned Intake is available. | `src/classroomStudent.js`, `src/experienceRoleController.js` | `studentClassEntryShell`, `studentClassJoinForm`, class/assignment/name inputs, retry controls; all local-only/summary-excluded. | From 99ac5723cc046c90aa12918daec991e774d060af Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:32:13 -0400 Subject: [PATCH 022/100] docs: update protected resource storage guidance --- docs/storage-schema.appendix.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/storage-schema.appendix.md b/docs/storage-schema.appendix.md index 87137aa..936a133 100644 --- a/docs/storage-schema.appendix.md +++ b/docs/storage-schema.appendix.md @@ -1,22 +1,32 @@ ## Template payloads and drawer workflow -The storage schema documentation doubles as the contract for curated template payloads. Templates are authored as JSON files under `templates/`, then compiled into `src/templates.manifest.js` via `npm run build:templates`. The runtime drawer (`src/templates.js`) hydrates that manifest, normalises each template's stored `SerializedAppState`, and projects a payload per mode so the UI can safely prefill new intakes. +The storage schema documentation doubles as the contract for curated resource payloads. Resources are authored as JSON files under `templates/`, then `npm run build:templates` validates all of them and generates two separate delivery boundaries: + +- `src/templates.manifest.js` — public browser manifest containing Standard Templates only; +- `api/protected-case-studies.manifest.js` — server-only Case Study metadata and full payloads. + +Authored `templates/*.json` files are build-time source and are excluded from Vercel deployment. The public drawer runtime never imports the server-only Case Study manifest. ### Authoring checklist -1. Start from an existing template JSON file so required keys (`id`, `name`, `description`, `templateKind`, `supportedModes`, and `state`) stay in place. +1. Start from an existing resource JSON file so required keys (`id`, `name`, `description`, `templateKind`, `supportedModes`, and `state`) stay in place. 2. Ensure `state` mirrors the fields listed above, including nested objects for `pre`, `impact`, `ops`, `steps`, and `actions`. -3. Keep drawer labels short—the template list truncates anything overly long. -4. Run `npm run build:templates` after editing JSON so `src/templates.manifest.js` refreshes. -5. Run `npm run update:storage-docs` to verify new fields are documented. +3. Keep drawer labels short—the resource list truncates anything overly long. +4. Run `npm run build:templates` after editing JSON so both generated manifests refresh. +5. Run `npm run verify:protected-cases` to prove protected Case Study identifiers/content remain outside browser runtime assets and authored JSON remains excluded from Vercel deployment. +6. Run `npm run update:storage-docs` to verify new persisted Intake fields are documented when the payload schema changes. ### Drawer workflow -- `src/templates.js` normalises template payloads, guaranteeing arrays are cloned and booleans such as `steps.drawerOpen` default to `false`. -- Mode projections enforce the `MODE_RULES` contract—fields hidden in a mode are reset to safe defaults before the drawer sends state to the app. The KT table honours the same per-column `tableFields` map so IS / IS NOT only prefills IS/IS NOT, D&C adds distinctions/changes while keeping causes empty, and actions remain available for Intake, IS / IS NOT, D&C, and Full while Full exposes all rows plus causes. +- `src/templates.js` owns canonical state normalization/mode projection for public Templates and authorized external Case Study payloads. +- `src/templatesDrawer.js` combines public Standard Template metadata with the current in-memory authorized Classroom Case Study catalog supplied by `src/classroomCaseStudies.js`. +- Standalone receives no protected provider context. Student/Instructor protected catalog requests require their active Classroom capability. +- Student Case Study payloads are fetched on demand only when applying a case, then projected through the same `MODE_RULES` contract before `applyAppState()`. +- Mode projections reset fields hidden by the selected learning mode to safe defaults. The KT table honours the same per-column `tableFields` map so IS / IS NOT only prefills IS/IS NOT, D&C adds distinctions/changes while keeping causes empty, and actions remain available for Intake, IS / IS NOT, D&C, and Full while Full exposes all rows plus causes. - Actions and steps collections are scrubbed before exposure so tests and runtime code can assume they conform to the schema defined above. +- Once an authorized Student applies a projected Case Study, that exercise content becomes normal Student/team Intake state and follows the usual storage/collaboration contract. The protected boundary prevents unauthorized source retrieval; it does not attempt DRM after authorized application. -Keeping these guardrails in the appendix allows future guidance for template authors to live beside the generated schema without being overwritten each time the docs regenerate. +Keeping these guardrails in the appendix allows future guidance for resource authors to live beside the generated schema without being overwritten each time the docs regenerate. ## Storage-to-Module Responsibility Map From 771b13d3c21674c1edc50f3769d76a906bf6f3e0 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:32:29 -0400 Subject: [PATCH 023/100] docs: synchronize generated storage appendix --- docs/storage-schema.md | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/storage-schema.md b/docs/storage-schema.md index 7af5101..0eb3b4a 100644 --- a/docs/storage-schema.md +++ b/docs/storage-schema.md @@ -102,23 +102,33 @@ _Auto-generated by `scripts/update-storage-docs.js`. Do not edit this file manua ## Template payloads and drawer workflow -The storage schema documentation doubles as the contract for curated template payloads. Templates are authored as JSON files under `templates/`, then compiled into `src/templates.manifest.js` via `npm run build:templates`. The runtime drawer (`src/templates.js`) hydrates that manifest, normalises each template's stored `SerializedAppState`, and projects a payload per mode so the UI can safely prefill new intakes. +The storage schema documentation doubles as the contract for curated resource payloads. Resources are authored as JSON files under `templates/`, then `npm run build:templates` validates all of them and generates two separate delivery boundaries: + +- `src/templates.manifest.js` — public browser manifest containing Standard Templates only; +- `api/protected-case-studies.manifest.js` — server-only Case Study metadata and full payloads. + +Authored `templates/*.json` files are build-time source and are excluded from Vercel deployment. The public drawer runtime never imports the server-only Case Study manifest. ### Authoring checklist -1. Start from an existing template JSON file so required keys (`id`, `name`, `description`, `templateKind`, `supportedModes`, and `state`) stay in place. +1. Start from an existing resource JSON file so required keys (`id`, `name`, `description`, `templateKind`, `supportedModes`, and `state`) stay in place. 2. Ensure `state` mirrors the fields listed above, including nested objects for `pre`, `impact`, `ops`, `steps`, and `actions`. -3. Keep drawer labels short—the template list truncates anything overly long. -4. Run `npm run build:templates` after editing JSON so `src/templates.manifest.js` refreshes. -5. Run `npm run update:storage-docs` to verify new fields are documented. +3. Keep drawer labels short—the resource list truncates anything overly long. +4. Run `npm run build:templates` after editing JSON so both generated manifests refresh. +5. Run `npm run verify:protected-cases` to prove protected Case Study identifiers/content remain outside browser runtime assets and authored JSON remains excluded from Vercel deployment. +6. Run `npm run update:storage-docs` to verify new persisted Intake fields are documented when the payload schema changes. ### Drawer workflow -- `src/templates.js` normalises template payloads, guaranteeing arrays are cloned and booleans such as `steps.drawerOpen` default to `false`. -- Mode projections enforce the `MODE_RULES` contract—fields hidden in a mode are reset to safe defaults before the drawer sends state to the app. The KT table honours the same per-column `tableFields` map so IS / IS NOT only prefills IS/IS NOT, D&C adds distinctions/changes while keeping causes empty, and actions remain available for Intake, IS / IS NOT, D&C, and Full while Full exposes all rows plus causes. +- `src/templates.js` owns canonical state normalization/mode projection for public Templates and authorized external Case Study payloads. +- `src/templatesDrawer.js` combines public Standard Template metadata with the current in-memory authorized Classroom Case Study catalog supplied by `src/classroomCaseStudies.js`. +- Standalone receives no protected provider context. Student/Instructor protected catalog requests require their active Classroom capability. +- Student Case Study payloads are fetched on demand only when applying a case, then projected through the same `MODE_RULES` contract before `applyAppState()`. +- Mode projections reset fields hidden by the selected learning mode to safe defaults. The KT table honours the same per-column `tableFields` map so IS / IS NOT only prefills IS/IS NOT, D&C adds distinctions/changes while keeping causes empty, and actions remain available for Intake, IS / IS NOT, D&C, and Full while Full exposes all rows plus causes. - Actions and steps collections are scrubbed before exposure so tests and runtime code can assume they conform to the schema defined above. +- Once an authorized Student applies a projected Case Study, that exercise content becomes normal Student/team Intake state and follows the usual storage/collaboration contract. The protected boundary prevents unauthorized source retrieval; it does not attempt DRM after authorized application. -Keeping these guardrails in the appendix allows future guidance for template authors to live beside the generated schema without being overwritten each time the docs regenerate. +Keeping these guardrails in the appendix allows future guidance for resource authors to live beside the generated schema without being overwritten each time the docs regenerate. ## Storage-to-Module Responsibility Map From 35e8526592b9fd256616505ab11c196ac8e1d7f9 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:33:08 -0400 Subject: [PATCH 024/100] docs: add protected Case Study repository operations --- docs/REPOSITORY-OPERATIONS.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/docs/REPOSITORY-OPERATIONS.md b/docs/REPOSITORY-OPERATIONS.md index 6b64a96..cc1f723 100644 --- a/docs/REPOSITORY-OPERATIONS.md +++ b/docs/REPOSITORY-OPERATIONS.md @@ -18,12 +18,26 @@ After changing the runtime, update `.nvmrc` and `package.json#engines` together - summary integration guard; - persistence integration guard; - ESLint/JSDoc; -- generated template-manifest freshness; +- generated public/server template-manifest freshness; +- protected Case Study browser/deployment boundary verification; - generated storage-documentation freshness; - the full Node/jsdom test suite. Individual commands remain useful while developing, but a PR is not complete until the aggregate gate passes. +## Protected Case Study deployment boundary + +Case Study authoring source is intentionally present in Git but intentionally absent from the public Vercel deployment surface. + +- `templates/*.json` is repository/build-time source and is excluded by `.vercelignore`. +- `src/templates.manifest.js` is public and may contain Standard Templates only. +- `api/protected-case-studies.manifest.js` is server-only generated content and may contain protected Case Study metadata/payloads. +- Browser runtime files must never import or duplicate the server-only manifest. +- Production Vercel builds run `npm run verify:protected-cases` rather than regenerating manifests, because the authored JSON files are intentionally excluded from the upload. +- GitHub CI/local authoring remains responsible for `npm run build:templates` / `npm run check:templates` freshness. + +Any change to `vercel.json`, `.vercelignore`, template generation, static output layout, or protected-resource routing must re-run `npm run verify:protected-cases` and include a deployed HTTP check that raw `/templates/*.json` Case Study paths are not served. + ## GitHub branch controls The intended `main` policy is: @@ -58,6 +72,12 @@ A new AI coding session should not assume prior conversational state. Before edi At handoff, record what changed, what was tested, any manual checks still required, and any environment/settings changes that cannot live in Git. +For protected Case Study work, the handoff must also record whether the final deployed preview was checked for: +- blocked/unavailable raw `/templates/*.json` authoring files; +- Standalone absence of protected resources; +- authorized Student/Instructor catalog access; +- unauthorized protected API rejection. + ## Delivery resilience for AI-assisted work From 47ffd1bf11c813e69f8d7956659902c041c8e908 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:33:25 -0400 Subject: [PATCH 025/100] docs: update global protected Case Study invariant --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index f43fe9a..6c2d73e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,7 +47,7 @@ Classroom invariants: - coaching uses separate persistence/revision semantics; - student credentials never provide class/workspace enumeration; - Case Study unlock passwords are pedagogy, not authentication; -- protected Case Study payloads must ultimately be server-gated and absent from public Standalone assets. +- protected Case Study metadata/payloads are server-gated, absent from public browser/static assets, and authored `templates/*.json` remains excluded from Vercel deployment; `src/templates.manifest.js` is Standard-only and browser code must never import `api/protected-case-studies.manifest.js`. Every classroom PR must update `docs/classroom-workstream.md` before handoff with its issue, PR/base dependency, current HEAD, completed/incomplete work, validation, risks, and exact next action. For stacked PRs, state the merge order and never describe an unmerged dependent PR as independently mergeable. From 4f4e95ab02526a728a107325d83bfcb2a27fd851 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:34:11 -0400 Subject: [PATCH 026/100] docs: finalize protected Case Study restart ledger --- docs/classroom-workstream.md | 71 +++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/docs/classroom-workstream.md b/docs/classroom-workstream.md index c36415e..6888e1a 100644 --- a/docs/classroom-workstream.md +++ b/docs/classroom-workstream.md @@ -19,7 +19,7 @@ This is the live restart document for the Classroom Experience program (#288). | Student join/resume | #292 | Complete | PR #302 / `feature/classroom-student-experience` | 194 tests: 193 pass, 0 fail, 1 skip; CI/CodeQL/dependency/template guard green | | Instructor observer | #293 | Complete | PR #305 merged | 208 tests: 207 pass, 0 fail, 1 skip; repository gates green | | Coaching | #294 | Complete | PR #306 merged | 224 tests: 223 pass, 0 fail, 1 skip; all repository gates green; production deployment READY | -| Protected cases | #295 | In progress | `feature/classroom-protected-cases` | Public manifest leak confirmed; split contract being implemented | +| Protected cases | #295 | In progress | draft PR #307 / `feature/classroom-protected-cases` | Runtime/security implementation complete; final docs/gates and deployment verification remain | | Browser E2E/CI | #296 | Not started | — | Uses #279 infrastructure | | Browser test foundation | #279 | Existing open issue | — | Shared Playwright/accessibility foundation | @@ -32,7 +32,7 @@ This is the live restart document for the Classroom Experience program (#288). - Coaching is a separate persistence/revision channel. - Classroom credentials never belong in exported Intake state. - Case Study password is instructional gating, not authentication. -- Protected Case Study payloads ultimately move out of the public client bundle. +- Protected Case Study metadata/payloads are server-gated, absent from public browser assets, and authored `templates/*.json` is excluded from Vercel deployment. See `docs/classroom-architecture.md` for the full contract. @@ -60,11 +60,17 @@ See `docs/classroom-architecture.md` for the full contract. ## Last completed action -Merged PR #306 / completed #294 as `4d0ab953fbdde32dde3441fbe2e415f0bf886634`. Production Vercel deployment reached READY and a post-deploy runtime-error scan found no errors. +Completed the #295 implementation/security/documentation slices through root cold-start and repository-operations guidance. Latest durable docs checkpoint before this update is `47ffd1bf11c813e69f8d7956659902c041c8e908`, followed by README/onboarding/roadmap/commenting/storage/operations synchronization. The public/server manifest split, authorized API/client flow, raw authored-JSON Vercel exclusion, and protected-resource verifier are implemented. ## Next recommended action -Continue #295 on `feature/classroom-protected-cases`. The confirmed leak is the generated `src/templates.manifest.js`: `scripts/build-templates-manifest.mjs` currently compiles Standard Templates and Case Studies, including full Case Study state, into one public static module. Split the public generated manifest to Standard Templates only; keep validating all authored JSON; then add server-authorized Case Study catalog/payload delivery for Student and Instructor contexts. Do not treat the rotating Case Study password as authentication. +Continue #295 on draft PR #307 from the current branch head. Runtime/security implementation is complete. Finish only the finalization sequence: + +1. verify the current documented head has CI, CodeQL, Dependency Review, and Template Manifest Guard green; +2. inspect the complete #307 diff for accidental browser imports/identifiers and confirm `npm run verify:protected-cases` remains in `npm run quality`; +3. perform deployed HTTP verification that protected `/templates/*.json` authoring paths are no longer served and that authorized/unauthorized protected API behavior matches the contract; +4. update PR #307 and issue #295 with the final SHA/evidence; +5. mark ready for review only after those checks. Do not merge unless explicitly authorized. ## Completed #300 implementation @@ -149,32 +155,55 @@ Continue #295 on `feature/classroom-protected-cases`. The confirmed leak is the ## Active #295 implementation - Branch: `feature/classroom-protected-cases`. +- Draft PR: #307. - Issue: #295. -- Confirmed current leak: `scripts/build-templates-manifest.mjs` reads every JSON file under `templates/` and writes metadata + full state into public `src/templates.manifest.js`. -- `src/templates.js` statically imports that manifest and assumes every listed resource has an in-bundle payload; role filtering therefore controls visibility only, not confidentiality. -- Target architecture: - - authoring JSON remains in-repo and all resources stay build-validated; - - public generated manifest contains Standard Templates only; - - protected Case Study catalog/payload is loaded server-side after Classroom authorization; - - existing mode projection/pedagogical unlock behavior is preserved after authorization; - - static-bundle regression tests prove protected Case Study content is absent from public generated assets. -- Delivery-safe parts: generator/runtime split → server authorization/API → Student/Instructor drawer integration → bundle guard/docs/final gates. -- Public/server manifest split checkpoint: `5d8ecb756de179276164d575cfc4ea1a03ef7115`; Template Manifest Guard green. -- Authorized server API checkpoint: `5dfd2718a2fefc2357afb22a5a906cd3f045e970`. -- Authorized client/lifecycle checkpoint: `83e278b87392c7be5eb8ba8ec4f19d84a0b94def`. -- Drawer integration checkpoint: `35acd62d35a0826e8e350507a16c187ffceafbc1`. -- Student/Instructor protected catalog is in-memory only; Case Study payload fetch uses authenticated POST after the pedagogical unlock; Standalone has no protected provider context. -- Exact next action: inspect canonical gates on `35acd62...`, fix findings, then add final import/static-bundle guard + docs/security review. +- Public/server manifest split: + - `src/templates.manifest.js` contains Standard Templates only; + - `api/protected-case-studies.manifest.js` contains the four protected Case Studies server-side; + - all authored JSON remains validated by `npm run build:templates`. +- Authorized server delivery: + - Instructor `GET/POST /api/classes/case-studies` is class-scoped; + - Student `GET/POST /api/classes/case-studies/student` is membership-bound to the issued Classroom workspace capability; + - catalog GET omits `state`; + - full payload selection is authenticated POST-body data; + - protected responses are `no-store` / `no-referrer`; + - legacy Standalone collaboration capability does not authorize Student Case Studies. +- Client delivery: + - `src/classroomCaseStudies.js` owns authorized catalog/payload access in memory only; + - Student/Instructor lifecycle controllers connect/disconnect that context; + - Standalone has no protected provider context; + - Student fetches the full Case Study only when applying it after the existing pedagogical mode/password step; + - remote payloads reuse canonical `projectTemplateState()` mode projection. +- Deployment boundary: + - current production was verified to expose raw `/templates/Microcomputer%20Cabinets.json` before this PR, confirming a second real leak beyond the old public manifest; + - `.vercelignore` now excludes `templates/*.json`; + - Vercel production build no longer regenerates manifests from excluded authoring source and instead runs `npm run verify:protected-cases`; + - GitHub/local authoring remains responsible for generated-manifest freshness. +- Guardrails/tests: + - public manifest contains only Standard Templates; + - every authored resource is emitted to exactly one generated boundary; + - protected IDs/names must be absent from public manifest/browser runtime; + - browser runtime must not import the server-only protected manifest; + - authorization/API/client/drawer regression coverage is included; + - `verify:protected-cases` is part of the canonical `npm run quality` gate. +- Durable implementation checkpoints: + - `5d8ecb756de179276164d575cfc4ea1a03ef7115` — public/server manifest split + regression boundary; + - `5dfd2718a2fefc2357afb22a5a906cd3f045e970` — authorized protected API; + - `83e278b87392c7be5eb8ba8ec4f19d84a0b94def` — authorized client/lifecycle; + - `35acd62d35a0826e8e350507a16c187ffceafbc1` — drawer integration; + - `b3160264adfac5d500711c22515bf21f27dff75d` — authored-JSON Vercel exclusion + boundary verifier; + - later commits fix only test harness and documentation synchronization. +- Final docs now cover security, API/architecture, README/deployment, scoped/global AGENTS, AI onboarding, roadmap, commenting guide, storage guidance, and repository operations. +- Exact next action: run/inspect final documented-head gates, then deployed HTTP verification, then update PR #307 / #295. Do not merge without explicit authorization. ## Known risks / watch items -- Existing Case Study payloads are client-bundled; role-based hiding is not a confidentiality control. - Existing collaboration secret links grant equal edit access and have no administrator role; classroom authorization must layer over rather than silently reinterpret those links. - There is deliberately no current workspace-list endpoint; student non-enumeration must remain a security property when instructor listing is added. - Classroom role/session state must not leak into `kt-intake-full-v2`. - Instructor read-only behavior must be server-enforced, not just disabled controls. - Class creation remains capability-first and does not yet require account/SSO identity; rate limiting/abuse controls are an operational follow-on if public exposure warrants them. -- Feature-branch Vercel deployments are skipped, so the additive Neon migration cannot be smoke-tested against a preview database before merge. +- Feature-branch Vercel deployments are skipped in the normal Git flow. Final #295 handoff therefore needs an explicit deployed HTTP verification path before merge/production promotion, especially for raw `/templates/*.json` exclusion. ## Handoff template From 471dcec23d63210ed718324493018e6eb6d1bd22 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 14:39:03 -0400 Subject: [PATCH 027/100] docs: record protected Case Study validation state --- docs/classroom-workstream.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/classroom-workstream.md b/docs/classroom-workstream.md index 6888e1a..25dd6eb 100644 --- a/docs/classroom-workstream.md +++ b/docs/classroom-workstream.md @@ -66,11 +66,11 @@ Completed the #295 implementation/security/documentation slices through root col Continue #295 on draft PR #307 from the current branch head. Runtime/security implementation is complete. Finish only the finalization sequence: -1. verify the current documented head has CI, CodeQL, Dependency Review, and Template Manifest Guard green; -2. inspect the complete #307 diff for accidental browser imports/identifiers and confirm `npm run verify:protected-cases` remains in `npm run quality`; -3. perform deployed HTTP verification that protected `/templates/*.json` authoring paths are no longer served and that authorized/unauthorized protected API behavior matches the contract; -4. update PR #307 and issue #295 with the final SHA/evidence; -5. mark ready for review only after those checks. Do not merge unless explicitly authorized. +1. GitHub-side validation is complete on `4f4e95ab02526a728a107325d83bfcb2a27fd851`: CI, CodeQL, Dependency Review, and Template Manifest Guard are all green. +2. PR #307 and issue #295 are synchronized with the implemented architecture and remaining verification. +3. Deployed HTTP verification remains blocked because Vercel cancels this feature branch through the project's **Ignored Build Step** policy. Deployment `dpl_CHjpJ4b8SfBxvyJSawvsWkEfsxpi` for security checkpoint `b316026...` is `CANCELED` with Vercel's ignored-build-step reason. +4. Exact next action: obtain a preview deployment through an explicitly approved Vercel configuration/deployment path, then verify raw protected `/templates/*.json` paths are unavailable and authorized/unauthorized protected API behavior matches the contract. +5. Keep PR #307 draft and do not merge until that deployed verification is complete and merge is explicitly authorized. ## Completed #300 implementation @@ -194,7 +194,9 @@ Continue #295 on draft PR #307 from the current branch head. Runtime/security im - `b3160264adfac5d500711c22515bf21f27dff75d` — authored-JSON Vercel exclusion + boundary verifier; - later commits fix only test harness and documentation synchronization. - Final docs now cover security, API/architecture, README/deployment, scoped/global AGENTS, AI onboarding, roadmap, commenting guide, storage guidance, and repository operations. -- Exact next action: run/inspect final documented-head gates, then deployed HTTP verification, then update PR #307 / #295. Do not merge without explicit authorization. +- GitHub validation on `4f4e95ab02526a728a107325d83bfcb2a27fd851`: CI green; CodeQL green; Dependency Review green; Template Manifest Guard green. +- Preview deployment status: blocked by Vercel Ignored Build Step; feature-branch deployment is canceled before a testable artifact exists. +- Exact next action: obtain an explicitly approved preview deployment, run the deployed HTTP security checks, then mark #307 ready for review. Do not merge without explicit authorization. ## Known risks / watch items @@ -203,7 +205,7 @@ Continue #295 on draft PR #307 from the current branch head. Runtime/security im - Classroom role/session state must not leak into `kt-intake-full-v2`. - Instructor read-only behavior must be server-enforced, not just disabled controls. - Class creation remains capability-first and does not yet require account/SSO identity; rate limiting/abuse controls are an operational follow-on if public exposure warrants them. -- Feature-branch Vercel deployments are skipped in the normal Git flow. Final #295 handoff therefore needs an explicit deployed HTTP verification path before merge/production promotion, especially for raw `/templates/*.json` exclusion. +- Feature-branch Vercel deployments are skipped by the project's Vercel Ignored Build Step. This is confirmed by canceled deployment `dpl_CHjpJ4b8SfBxvyJSawvsWkEfsxpi`. Final #295 handoff therefore needs an explicitly approved preview/deployment path before merge, especially for raw `/templates/*.json` exclusion. ## Handoff template From a376d7751209eff02a616110a59e52fd45cc1b70 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:10:36 -0400 Subject: [PATCH 028/100] security: verify Vercel upload excludes authored cases --- scripts/verify-protected-case-boundary.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/scripts/verify-protected-case-boundary.mjs b/scripts/verify-protected-case-boundary.mjs index 466dd5f..f3e2555 100644 --- a/scripts/verify-protected-case-boundary.mjs +++ b/scripts/verify-protected-case-boundary.mjs @@ -11,6 +11,7 @@ const root = new URL('../', import.meta.url); const publicManifestUrl = new URL('../src/templates.manifest.js', import.meta.url); const vercelIgnoreUrl = new URL('../.vercelignore', import.meta.url); const vercelConfigUrl = new URL('../vercel.json', import.meta.url); +const templatesDirUrl = new URL('../templates/', import.meta.url); async function read(url) { return fs.readFile(url, 'utf8'); @@ -54,6 +55,20 @@ async function main() { throw new Error('Vercel buildCommand must not regenerate manifests after authored template JSON is excluded.'); } + if (process.env.VERCEL === '1') { + const deployedTemplateEntries = await fs.readdir(templatesDirUrl).catch(error => { + if (error?.code === 'ENOENT') return []; + throw error; + }); + const deployedAuthoredJson = deployedTemplateEntries.filter(name => name.endsWith('.json')); + if (deployedAuthoredJson.length) { + throw new Error( + `Vercel deployment source still contains authored template JSON: ${deployedAuthoredJson.join(', ')}` + ); + } + console.log('[verify:protected-cases] Vercel upload contains no authored templates/*.json files.'); + } + const browserFiles = [ new URL('../main.js', import.meta.url), new URL('../index.html', import.meta.url), From 3307edf1e9f1f3124d65c2cc30e0877c67d3782a Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:16:11 -0400 Subject: [PATCH 029/100] docs: record protected-case preview evidence --- docs/classroom-workstream.md | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/docs/classroom-workstream.md b/docs/classroom-workstream.md index 25dd6eb..81fac61 100644 --- a/docs/classroom-workstream.md +++ b/docs/classroom-workstream.md @@ -66,11 +66,13 @@ Completed the #295 implementation/security/documentation slices through root col Continue #295 on draft PR #307 from the current branch head. Runtime/security implementation is complete. Finish only the finalization sequence: -1. GitHub-side validation is complete on `4f4e95ab02526a728a107325d83bfcb2a27fd851`: CI, CodeQL, Dependency Review, and Template Manifest Guard are all green. -2. PR #307 and issue #295 are synchronized with the implemented architecture and remaining verification. -3. Deployed HTTP verification remains blocked because Vercel cancels this feature branch through the project's **Ignored Build Step** policy. Deployment `dpl_CHjpJ4b8SfBxvyJSawvsWkEfsxpi` for security checkpoint `b316026...` is `CANCELED` with Vercel's ignored-build-step reason. -4. Exact next action: obtain a preview deployment through an explicitly approved Vercel configuration/deployment path, then verify raw protected `/templates/*.json` paths are unavailable and authorized/unauthorized protected API behavior matches the contract. -5. Keep PR #307 draft and do not merge until that deployed verification is complete and merge is explicitly authorized. +1. Current #307 head: `a376d7751209eff02a616110a59e52fd45cc1b70`. +2. GitHub validation on that exact head is complete: **236 tests (235 pass, 0 fail, 1 intentional skip)**; CI, CodeQL, Dependency Review, Template Manifest Guard, and Vercel Preview Comments are green. +3. A temporary verification branch `verify/protected-cases-preview` over the prior fully documented runtime head added only a branch-scoped Vercel `ignoreCommand`. Deployment `dpl_A3H2rKdJAEGZpBM26iEFSyMicK6f` reached **READY** and the previously exposed protected path `/templates/Microcomputer%20Cabinets.json` returned a genuine HTTP **404**. +4. #307 was then strengthened at `a376d775...`: when `VERCEL=1`, `verify:protected-cases` now fails if any authored `templates/*.json` file is actually present in the Vercel upload, not merely if the ignore rule is missing. +5. A second verification branch `verify/protected-cases-preview-v2` attempted to deploy the strengthened head, but Vercel rejected it with `upgradeToPro=build-rate-limit` before creating a deployment. This is tracked in #304. +6. The connected Vercel fetch tool cannot traverse the READY preview's Vercel Authentication layer consistently, and the local runtime has no Vercel CLI/outbound DNS. Therefore deployed authenticated Student/Instructor API calls cannot be executed from this session without weakening/persisting a preview bypass secret. +7. Exact next action: once Vercel preview build capacity is available, deploy `a376d775...` (or a descendant with no runtime changes) and run the remaining authenticated/unauthenticated protected API HTTP checks. Keep PR #307 draft and do not merge until that verification is accepted and merge is explicitly authorized. ## Completed #300 implementation @@ -194,9 +196,11 @@ Continue #295 on draft PR #307 from the current branch head. Runtime/security im - `b3160264adfac5d500711c22515bf21f27dff75d` — authored-JSON Vercel exclusion + boundary verifier; - later commits fix only test harness and documentation synchronization. - Final docs now cover security, API/architecture, README/deployment, scoped/global AGENTS, AI onboarding, roadmap, commenting guide, storage guidance, and repository operations. -- GitHub validation on `4f4e95ab02526a728a107325d83bfcb2a27fd851`: CI green; CodeQL green; Dependency Review green; Template Manifest Guard green. -- Preview deployment status: blocked by Vercel Ignored Build Step; feature-branch deployment is canceled before a testable artifact exists. -- Exact next action: obtain an explicitly approved preview deployment, run the deployed HTTP security checks, then mark #307 ready for review. Do not merge without explicit authorization. +- Final runtime/security head: `a376d7751209eff02a616110a59e52fd45cc1b70`. +- Validation: 236 tests (235 pass, 0 fail, 1 intentional skip); CI, CodeQL, Dependency Review, Template Manifest Guard, and Vercel Preview Comments green. +- Deployment evidence: temporary preview `dpl_A3H2rKdJAEGZpBM26iEFSyMicK6f` reached READY and returned 404 for the previously exposed Microcomputer Cabinets authoring JSON path. +- Stronger Vercel-upload assertion is now part of #307; its follow-up preview was blocked by the Vercel build-rate limit, tracked in #304. +- Exact next action: re-run a preview of the current head when Vercel capacity permits, then execute the remaining protected API HTTP authorization checks. Do not merge without explicit authorization. ## Known risks / watch items @@ -205,7 +209,7 @@ Continue #295 on draft PR #307 from the current branch head. Runtime/security im - Classroom role/session state must not leak into `kt-intake-full-v2`. - Instructor read-only behavior must be server-enforced, not just disabled controls. - Class creation remains capability-first and does not yet require account/SSO identity; rate limiting/abuse controls are an operational follow-on if public exposure warrants them. -- Feature-branch Vercel deployments are skipped by the project's Vercel Ignored Build Step. This is confirmed by canceled deployment `dpl_CHjpJ4b8SfBxvyJSawvsWkEfsxpi`. Final #295 handoff therefore needs an explicitly approved preview/deployment path before merge, especially for raw `/templates/*.json` exclusion. +- Normal feature-branch Vercel deployments are skipped by the project's Ignored Build Step. A temporary branch override can produce a preview, but the second #295 verification attempt hit the team build-rate limit. Preview-authenticated API verification therefore depends on available Vercel build capacity / an automation bypass path; see #304. ## Handoff template From 40794816a6cfe620b8691a4359d056434e86825b Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:19:57 -0400 Subject: [PATCH 030/100] infra: gate Vercel Git deployments by branch --- vercel.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index c1fb0f6..fcf358e 100644 --- a/vercel.json +++ b/vercel.json @@ -1,5 +1,12 @@ { "buildCommand": "npm run verify:protected-cases", "outputDirectory": ".", - "cleanUrls": true + "cleanUrls": true, + "git": { + "deploymentEnabled": { + "**": false, + "main": true, + "verify/**": true + } + } } From 2749743be45ef6f603a76caf36f819f34e82ea18 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:20:29 -0400 Subject: [PATCH 031/100] test: guard Vercel branch deployment policy --- scripts/verify-protected-case-boundary.mjs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/scripts/verify-protected-case-boundary.mjs b/scripts/verify-protected-case-boundary.mjs index f3e2555..2d528db 100644 --- a/scripts/verify-protected-case-boundary.mjs +++ b/scripts/verify-protected-case-boundary.mjs @@ -55,6 +55,18 @@ async function main() { throw new Error('Vercel buildCommand must not regenerate manifests after authored template JSON is excluded.'); } + const deploymentEnabled = vercelConfig.git?.deploymentEnabled; + if ( + !deploymentEnabled + || deploymentEnabled['**'] !== false + || deploymentEnabled.main !== true + || deploymentEnabled['verify/**'] !== true + ) { + throw new Error( + 'Vercel Git deployments must be disabled by default, enabled for main, and enabled explicitly for verify/** branches.' + ); + } + if (process.env.VERCEL === '1') { const deployedTemplateEntries = await fs.readdir(templatesDirUrl).catch(error => { if (error?.code === 'ENOENT') return []; From b88f619f17442ca8eba24ffdb949d56e0ca858a9 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:21:20 -0400 Subject: [PATCH 032/100] docs: define Vercel branch deployment policy --- docs/REPOSITORY-OPERATIONS.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/REPOSITORY-OPERATIONS.md b/docs/REPOSITORY-OPERATIONS.md index cc1f723..d1399b0 100644 --- a/docs/REPOSITORY-OPERATIONS.md +++ b/docs/REPOSITORY-OPERATIONS.md @@ -38,6 +38,21 @@ Case Study authoring source is intentionally present in Git but intentionally ab Any change to `vercel.json`, `.vercelignore`, template generation, static output layout, or protected-resource routing must re-run `npm run verify:protected-cases` and include a deployed HTTP check that raw `/templates/*.json` Case Study paths are not served. + +### Vercel Git deployment policy + +Repository configuration deliberately separates ordinary development branches from intentional deployment verification: + +- `main` is allowed to deploy automatically to Vercel; +- `verify/**` branches are allowed to create deliberate preview deployments for security/E2E verification; +- all other Git branches are denied Vercel Git deployment by default. + +This is enforced in `vercel.json` with minimatch-based `git.deploymentEnabled` rules. The default deny is important for AI-assisted work: small checkpoint commits must continue to trigger GitHub quality/security checks **without** consuming Vercel build capacity. + +Do not replace this with the older project-level Ignored Build Step as the primary feature-branch control. Ignored builds still created canceled deployment records during the Classroom program and contributed to build-rate exhaustion tracked in #304. + +When a preview is genuinely needed, create a short-lived `verify/**` branch from the exact PR head. Do not add a temporary allow rule to a normal `feature/**` branch. + ## GitHub branch controls The intended `main` policy is: From 0cc4cda5c1aab1d96c1e746dbcb088b49999d94a Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:21:50 -0400 Subject: [PATCH 033/100] docs: explain deliberate Vercel preview branches --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7e9c08e..ca89ce3 100644 --- a/README.md +++ b/README.md @@ -185,6 +185,7 @@ Need to know which module owns a given storage field? Jump to the [Storage-to-Mo - Production Vercel builds execute `npm run verify:protected-cases` (see `vercel.json`). They **do not regenerate manifests**, because authored `templates/*.json` source files are deliberately excluded from the deployment upload. - Generated manifests are therefore committed artifacts. GitHub CI remains responsible for running `npm run build:templates` / `check:templates` and proving they match their authored JSON sources. - `.vercelignore` prevents raw authored Case Study JSON from becoming directly fetchable static files. Keep that exclusion and the protected-case verifier aligned with any future output-directory/deployment changes. +- Vercel Git deployments are deny-by-default for ordinary branches. `main` deploys automatically; short-lived `verify/**` branches are the explicit preview path for security/E2E checks. This prevents AI checkpoint commits from exhausting Vercel build capacity while preserving GitHub CI on every PR commit. - When modifying the manifest workflow or required quality gates, update both this README and `vercel.json` so the documented steps mirror the actual hosted build. - Dependency changes must be installed through the normal npm registry so npm generates the complete lockfile. Run `npm run verify:lockfile` before committing and `npm ci` from a clean dependency tree; the offline guard catches missing resolved root-package entries before CI reaches its clean install. From 5d8ec5370f04a642732a2c0c5f977bf7d7396295 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:22:41 -0400 Subject: [PATCH 034/100] docs: record Vercel branch gating --- docs/classroom-workstream.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/classroom-workstream.md b/docs/classroom-workstream.md index 81fac61..c9c48a3 100644 --- a/docs/classroom-workstream.md +++ b/docs/classroom-workstream.md @@ -66,13 +66,14 @@ Completed the #295 implementation/security/documentation slices through root col Continue #295 on draft PR #307 from the current branch head. Runtime/security implementation is complete. Finish only the finalization sequence: -1. Current #307 head: `a376d7751209eff02a616110a59e52fd45cc1b70`. +1. Current #307 head after deployment-policy/docs hardening: `0cc4cda5c1aab1d96c1e746dbcb088b49999d94a`. Final runtime/security implementation remains rooted at `a376d7751209eff02a616110a59e52fd45cc1b70`. 2. GitHub validation on that exact head is complete: **236 tests (235 pass, 0 fail, 1 intentional skip)**; CI, CodeQL, Dependency Review, Template Manifest Guard, and Vercel Preview Comments are green. 3. A temporary verification branch `verify/protected-cases-preview` over the prior fully documented runtime head added only a branch-scoped Vercel `ignoreCommand`. Deployment `dpl_A3H2rKdJAEGZpBM26iEFSyMicK6f` reached **READY** and the previously exposed protected path `/templates/Microcomputer%20Cabinets.json` returned a genuine HTTP **404**. 4. #307 was then strengthened at `a376d775...`: when `VERCEL=1`, `verify:protected-cases` now fails if any authored `templates/*.json` file is actually present in the Vercel upload, not merely if the ignore rule is missing. 5. A second verification branch `verify/protected-cases-preview-v2` attempted to deploy the strengthened head, but Vercel rejected it with `upgradeToPro=build-rate-limit` before creating a deployment. This is tracked in #304. 6. The connected Vercel fetch tool cannot traverse the READY preview's Vercel Authentication layer consistently, and the local runtime has no Vercel CLI/outbound DNS. Therefore deployed authenticated Student/Instructor API calls cannot be executed from this session without weakening/persisting a preview bypass secret. -7. Exact next action: once Vercel preview build capacity is available, deploy `a376d775...` (or a descendant with no runtime changes) and run the remaining authenticated/unauthenticated protected API HTTP checks. Keep PR #307 draft and do not merge until that verification is accepted and merge is explicitly authorized. +7. Vercel Git branch gating is now repository-enforced: ordinary branches are deny-by-default, `main` and `verify/**` are explicitly enabled. Feature commits `4079481...`, `2749743...`, and `0cc4cda...` each triggered GitHub checks but produced no Vercel status/deployment record; #304 tracks post-merge production confirmation. +8. Exact next action: once Vercel preview build capacity is available, create a fresh `verify/**` branch from the current #307 head and run the remaining authenticated/unauthenticated protected API HTTP checks. Keep PR #307 draft and do not merge until that verification is accepted and merge is explicitly authorized. ## Completed #300 implementation @@ -196,7 +197,7 @@ Continue #295 on draft PR #307 from the current branch head. Runtime/security im - `b3160264adfac5d500711c22515bf21f27dff75d` — authored-JSON Vercel exclusion + boundary verifier; - later commits fix only test harness and documentation synchronization. - Final docs now cover security, API/architecture, README/deployment, scoped/global AGENTS, AI onboarding, roadmap, commenting guide, storage guidance, and repository operations. -- Final runtime/security head: `a376d7751209eff02a616110a59e52fd45cc1b70`. +- Final runtime/security head: `a376d7751209eff02a616110a59e52fd45cc1b70`; current docs/deployment-policy head: `0cc4cda5c1aab1d96c1e746dbcb088b49999d94a`. - Validation: 236 tests (235 pass, 0 fail, 1 intentional skip); CI, CodeQL, Dependency Review, Template Manifest Guard, and Vercel Preview Comments green. - Deployment evidence: temporary preview `dpl_A3H2rKdJAEGZpBM26iEFSyMicK6f` reached READY and returned 404 for the previously exposed Microcomputer Cabinets authoring JSON path. - Stronger Vercel-upload assertion is now part of #307; its follow-up preview was blocked by the Vercel build-rate limit, tracked in #304. @@ -209,7 +210,7 @@ Continue #295 on draft PR #307 from the current branch head. Runtime/security im - Classroom role/session state must not leak into `kt-intake-full-v2`. - Instructor read-only behavior must be server-enforced, not just disabled controls. - Class creation remains capability-first and does not yet require account/SSO identity; rate limiting/abuse controls are an operational follow-on if public exposure warrants them. -- Normal feature-branch Vercel deployments are skipped by the project's Ignored Build Step. A temporary branch override can produce a preview, but the second #295 verification attempt hit the team build-rate limit. Preview-authenticated API verification therefore depends on available Vercel build capacity / an automation bypass path; see #304. +- Normal development branches are now denied at the repository `git.deploymentEnabled` layer before Vercel creates deployment records. `main` and `verify/**` are explicit allow paths. A deliberate verification preview still depends on available Vercel build capacity / preview-auth access; see #304. ## Handoff template From eef6ee4b0294bd454ba478655196a3353c675a38 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:50:19 -0400 Subject: [PATCH 035/100] infra: allow deliberate Vercel verification builds --- vercel.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vercel.json b/vercel.json index fcf358e..94246f4 100644 --- a/vercel.json +++ b/vercel.json @@ -8,5 +8,6 @@ "main": true, "verify/**": true } - } + }, + "ignoreCommand": "case \"$VERCEL_GIT_COMMIT_REF\" in main|verify/*) exit 1 ;; *) exit 0 ;; esac" } From f9f8fc8f22cd71029bb492ad278c4470264153b6 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:50:51 -0400 Subject: [PATCH 036/100] test: guard deliberate Vercel preview builds --- scripts/verify-protected-case-boundary.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/verify-protected-case-boundary.mjs b/scripts/verify-protected-case-boundary.mjs index 2d528db..0b4609f 100644 --- a/scripts/verify-protected-case-boundary.mjs +++ b/scripts/verify-protected-case-boundary.mjs @@ -67,6 +67,13 @@ async function main() { ); } + const expectedIgnoreCommand = 'case "$VERCEL_GIT_COMMIT_REF" in main|verify/*) exit 1 ;; *) exit 0 ;; esac'; + if (vercelConfig.ignoreCommand !== expectedIgnoreCommand) { + throw new Error( + 'Vercel ignoreCommand must allow only main and verify/** branches to continue when the build step is evaluated.' + ); + } + if (process.env.VERCEL === '1') { const deployedTemplateEntries = await fs.readdir(templatesDirUrl).catch(error => { if (error?.code === 'ENOENT') return []; From 5eced3949e69834c1ca608b848b885960f36deab Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:53:39 -0400 Subject: [PATCH 037/100] docs: finalize Vercel deployment controls --- docs/REPOSITORY-OPERATIONS.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/REPOSITORY-OPERATIONS.md b/docs/REPOSITORY-OPERATIONS.md index d1399b0..6bc98a0 100644 --- a/docs/REPOSITORY-OPERATIONS.md +++ b/docs/REPOSITORY-OPERATIONS.md @@ -47,11 +47,18 @@ Repository configuration deliberately separates ordinary development branches fr - `verify/**` branches are allowed to create deliberate preview deployments for security/E2E verification; - all other Git branches are denied Vercel Git deployment by default. -This is enforced in `vercel.json` with minimatch-based `git.deploymentEnabled` rules. The default deny is important for AI-assisted work: small checkpoint commits must continue to trigger GitHub quality/security checks **without** consuming Vercel build capacity. +Two repository-owned controls enforce this in `vercel.json`: -Do not replace this with the older project-level Ignored Build Step as the primary feature-branch control. Ignored builds still created canceled deployment records during the Classroom program and contributed to build-rate exhaustion tracked in #304. +1. `git.deploymentEnabled` denies all branches by default, then explicitly allows `main` and `verify/**`. This prevents ordinary feature/checkpoint commits from creating Vercel deployment records at all. +2. `ignoreCommand` returns “continue” only for `main` and `verify/**`. This overrides the older project-level Ignored Build Step for the branches we intentionally allow to reach Vercel. -When a preview is genuinely needed, create a short-lived `verify/**` branch from the exact PR head. Do not add a temporary allow rule to a normal `feature/**` branch. +Both rules are checked by `npm run verify:protected-cases`. + +The default deny is important for AI-assisted work: small checkpoint commits continue to trigger GitHub quality/security checks **without** consuming Vercel build capacity. The Classroom program confirmed this behavior repeatedly: ordinary `feature/classroom-protected-cases` commits produced no Vercel deployment record, while `verify/protected-cases-preview` produced a READY preview from the exact PR head. + +Do not rely on the project-level Ignored Build Step as the primary feature-branch control. Before this repo-owned policy, ignored builds still created canceled deployment records and contributed to build-rate exhaustion tracked in #304. + +When a preview is genuinely needed, move or create a short-lived `verify/**` branch at the exact PR head. Do not add a temporary allow rule to a normal `feature/**` branch. ## GitHub branch controls From bbf967c44255431a463740cd5382b4459a9eedc9 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:53:53 -0400 Subject: [PATCH 038/100] docs: clarify Vercel verification branch behavior --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ca89ce3..31ebbdf 100644 --- a/README.md +++ b/README.md @@ -185,7 +185,7 @@ Need to know which module owns a given storage field? Jump to the [Storage-to-Mo - Production Vercel builds execute `npm run verify:protected-cases` (see `vercel.json`). They **do not regenerate manifests**, because authored `templates/*.json` source files are deliberately excluded from the deployment upload. - Generated manifests are therefore committed artifacts. GitHub CI remains responsible for running `npm run build:templates` / `check:templates` and proving they match their authored JSON sources. - `.vercelignore` prevents raw authored Case Study JSON from becoming directly fetchable static files. Keep that exclusion and the protected-case verifier aligned with any future output-directory/deployment changes. -- Vercel Git deployments are deny-by-default for ordinary branches. `main` deploys automatically; short-lived `verify/**` branches are the explicit preview path for security/E2E checks. This prevents AI checkpoint commits from exhausting Vercel build capacity while preserving GitHub CI on every PR commit. +- Vercel Git deployments are deny-by-default for ordinary branches. `main` deploys automatically; short-lived `verify/**` branches are the explicit preview path for security/E2E checks. `vercel.json` also owns the ignored-build decision so those two allowed branch classes proceed even if the project dashboard has an older Ignored Build Step configured. This prevents AI checkpoint commits from exhausting Vercel build capacity while preserving GitHub CI on every PR commit. - When modifying the manifest workflow or required quality gates, update both this README and `vercel.json` so the documented steps mirror the actual hosted build. - Dependency changes must be installed through the normal npm registry so npm generates the complete lockfile. Run `npm run verify:lockfile` before committing and `npm ci` from a clean dependency tree; the offline guard catches missing resolved root-package entries before CI reaches its clean install. From 265932a0970d2d57f37e7640d296bf477324f8e2 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:54:47 -0400 Subject: [PATCH 039/100] docs: record final protected-case preview evidence --- docs/classroom-workstream.md | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/docs/classroom-workstream.md b/docs/classroom-workstream.md index c9c48a3..3bcc8ae 100644 --- a/docs/classroom-workstream.md +++ b/docs/classroom-workstream.md @@ -64,16 +64,19 @@ Completed the #295 implementation/security/documentation slices through root col ## Next recommended action -Continue #295 on draft PR #307 from the current branch head. Runtime/security implementation is complete. Finish only the finalization sequence: - -1. Current #307 head after deployment-policy/docs hardening: `0cc4cda5c1aab1d96c1e746dbcb088b49999d94a`. Final runtime/security implementation remains rooted at `a376d7751209eff02a616110a59e52fd45cc1b70`. -2. GitHub validation on that exact head is complete: **236 tests (235 pass, 0 fail, 1 intentional skip)**; CI, CodeQL, Dependency Review, Template Manifest Guard, and Vercel Preview Comments are green. -3. A temporary verification branch `verify/protected-cases-preview` over the prior fully documented runtime head added only a branch-scoped Vercel `ignoreCommand`. Deployment `dpl_A3H2rKdJAEGZpBM26iEFSyMicK6f` reached **READY** and the previously exposed protected path `/templates/Microcomputer%20Cabinets.json` returned a genuine HTTP **404**. -4. #307 was then strengthened at `a376d775...`: when `VERCEL=1`, `verify:protected-cases` now fails if any authored `templates/*.json` file is actually present in the Vercel upload, not merely if the ignore rule is missing. -5. A second verification branch `verify/protected-cases-preview-v2` attempted to deploy the strengthened head, but Vercel rejected it with `upgradeToPro=build-rate-limit` before creating a deployment. This is tracked in #304. -6. The connected Vercel fetch tool cannot traverse the READY preview's Vercel Authentication layer consistently, and the local runtime has no Vercel CLI/outbound DNS. Therefore deployed authenticated Student/Instructor API calls cannot be executed from this session without weakening/persisting a preview bypass secret. -7. Vercel Git branch gating is now repository-enforced: ordinary branches are deny-by-default, `main` and `verify/**` are explicitly enabled. Feature commits `4079481...`, `2749743...`, and `0cc4cda...` each triggered GitHub checks but produced no Vercel status/deployment record; #304 tracks post-merge production confirmation. -8. Exact next action: once Vercel preview build capacity is available, create a fresh `verify/**` branch from the current #307 head and run the remaining authenticated/unauthenticated protected API HTTP checks. Keep PR #307 draft and do not merge until that verification is accepted and merge is explicitly authorized. +Continue #295 on draft PR #307 from the current branch head. + +1. Runtime/security implementation is complete, including public/server manifest split, Classroom-authorized delivery, browser/runtime leak guards, Vercel upload verification, and role-aware client integration. +2. Vercel Git deployment policy is now repository-owned: + - ordinary branches are denied deployment by default; + - `main` remains allowed for production; + - `verify/**` branches are allowed for deliberate preview verification; + - repo `ignoreCommand` explicitly continues only `main` and `verify/**`. +3. Ordinary feature commits repeatedly triggered GitHub CI/security checks while producing **no Vercel deployment record**. +4. Verification branch `verify/protected-cases-preview` was moved to guarded PR head `f9f8fc8f22cd71029bb492ad278c4470264153b6`; Vercel deployment `dpl_Ep2q8Y3BMUWGEV7fzZQHnvdwPumA` reached **READY**. +5. On that exact READY preview, the previously exposed protected authoring path `/templates/Microcomputer%20Cabinets.json` returned a genuine HTTP **404**. +6. The connected Vercel fetch tool still stops at this project's Vercel Authentication layer for protected API requests after the initial static request. The normal web fetch cannot access the preview, the local runtime has no outbound DNS, and no Vercel CLI/automation bypass secret is available. Do not weaken preview protection or persist a bypass secret simply to satisfy this check. +7. Exact next action: finalize docs/PR/issue evidence on the latest head, confirm all repository gates are green, then decide whether the deployed API authorization matrix can be accepted based on deterministic server tests + READY preview/static boundary evidence or must wait for an approved Vercel automation-bypass path. Keep PR #307 draft and do not merge without explicit authorization. ## Completed #300 implementation @@ -197,11 +200,14 @@ Continue #295 on draft PR #307 from the current branch head. Runtime/security im - `b3160264adfac5d500711c22515bf21f27dff75d` — authored-JSON Vercel exclusion + boundary verifier; - later commits fix only test harness and documentation synchronization. - Final docs now cover security, API/architecture, README/deployment, scoped/global AGENTS, AI onboarding, roadmap, commenting guide, storage guidance, and repository operations. -- Final runtime/security head: `a376d7751209eff02a616110a59e52fd45cc1b70`; current docs/deployment-policy head: `0cc4cda5c1aab1d96c1e746dbcb088b49999d94a`. -- Validation: 236 tests (235 pass, 0 fail, 1 intentional skip); CI, CodeQL, Dependency Review, Template Manifest Guard, and Vercel Preview Comments green. -- Deployment evidence: temporary preview `dpl_A3H2rKdJAEGZpBM26iEFSyMicK6f` reached READY and returned 404 for the previously exposed Microcomputer Cabinets authoring JSON path. -- Stronger Vercel-upload assertion is now part of #307; its follow-up preview was blocked by the Vercel build-rate limit, tracked in #304. -- Exact next action: re-run a preview of the current head when Vercel capacity permits, then execute the remaining protected API HTTP authorization checks. Do not merge without explicit authorization. +- Final runtime/security implementation head before deployment-policy hardening: `a376d7751209eff02a616110a59e52fd45cc1b70`. +- Repository-owned Vercel deployment controls are now part of #307 and guarded by `verify:protected-cases`. +- Ordinary feature commits create no Vercel deployment records; `verify/**` is the explicit preview path. +- Deliberate verification head `f9f8fc8f22cd71029bb492ad278c4470264153b6` deployed successfully as `dpl_Ep2q8Y3BMUWGEV7fzZQHnvdwPumA` and reached **READY**. +- That READY preview returned HTTP **404** for the formerly public Microcomputer Cabinets authored JSON path. +- Deterministic API tests already cover Instructor authorization, Student membership-bound authorization, metadata-only catalogs, authenticated payload POST, legacy-token rejection, and no-store/no-referrer response headers. +- Remaining deployed API requests are blocked by the preview-auth tooling boundary, not by application/test failure. +- Exact next action: finish final metadata/diff review and keep #307 draft until the remaining verification requirement is explicitly accepted or an approved preview automation-bypass path becomes available. Do not merge without explicit authorization. ## Known risks / watch items From 03cc512eeac655224356c8f968790d53996299c5 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:59:24 -0400 Subject: [PATCH 040/100] build: create minimal Vercel public bundle --- scripts/build-vercel-public.mjs | 136 ++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 scripts/build-vercel-public.mjs diff --git a/scripts/build-vercel-public.mjs b/scripts/build-vercel-public.mjs new file mode 100644 index 0000000..f0c0efb --- /dev/null +++ b/scripts/build-vercel-public.mjs @@ -0,0 +1,136 @@ +#!/usr/bin/env node +/** + * @fileoverview Builds the minimal public static bundle used by Vercel. + * + * The repository contains internal documentation, tests, authoring data, and + * build tooling that must not become public static assets. This script copies + * only the browser runtime into dist/ and verifies the resulting boundary. + */ + +import { promises as fs } from 'node:fs'; + +import { PROTECTED_CASE_STUDY_MANIFEST } from '../api/protected-case-studies.manifest.js'; + +const root = new URL('../', import.meta.url); +const output = new URL('../dist/', import.meta.url); + +const ROOT_RUNTIME_FILES = Object.freeze([ + 'index.html', + 'main.js', + 'styles.css' +]); + +const RUNTIME_DIRECTORIES = Object.freeze([ + 'components', + 'src' +]); + +async function copyFile(relativePath) { + const source = new URL(relativePath, root); + const destination = new URL(relativePath, output); + await fs.mkdir(new URL('./', destination), { recursive: true }); + await fs.copyFile(source, destination); +} + +async function copyJavaScriptTree(directoryName) { + const sourceDirectory = new URL(`../${directoryName}/`, import.meta.url); + const destinationDirectory = new URL(`../dist/${directoryName}/`, import.meta.url); + await fs.mkdir(destinationDirectory, { recursive: true }); + + const entries = await fs.readdir(sourceDirectory, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + throw new Error( + `Unexpected nested directory in public runtime tree ${directoryName}/${entry.name}; update build-vercel-public.mjs explicitly.` + ); + } + if (!entry.isFile() || !entry.name.endsWith('.js')) continue; + await fs.copyFile( + new URL(entry.name, sourceDirectory), + new URL(entry.name, destinationDirectory) + ); + } +} + +async function collectOutputFiles(directoryUrl, prefix = '') { + const files = []; + const entries = await fs.readdir(directoryUrl, { withFileTypes: true }); + for (const entry of entries) { + const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name; + const child = new URL(entry.name + (entry.isDirectory() ? '/' : ''), directoryUrl); + if (entry.isDirectory()) { + files.push(...await collectOutputFiles(child, relativePath)); + } else { + files.push(relativePath); + } + } + return files; +} + +async function verifyOutput() { + const files = (await collectOutputFiles(output)).sort(); + + for (const file of files) { + if (!( + ROOT_RUNTIME_FILES.includes(file) + || file.startsWith('src/') + || file.startsWith('components/') + )) { + throw new Error(`Unexpected public deployment file: ${file}`); + } + + if ( + file.endsWith('.md') + || file.endsWith('.json') + || file.endsWith('.mjs') + || file.includes('AGENTS') + ) { + throw new Error(`Internal repository file leaked into public deployment output: ${file}`); + } + } + + for (const required of ROOT_RUNTIME_FILES) { + if (!files.includes(required)) { + throw new Error(`Required browser runtime file missing from public deployment output: ${required}`); + } + } + + const textFiles = await Promise.all(files.map(async file => ({ + file, + content: await fs.readFile(new URL(file, output), 'utf8') + }))); + + for (const caseStudy of PROTECTED_CASE_STUDY_MANIFEST) { + for (const protectedText of [caseStudy.id, caseStudy.name]) { + const leaked = textFiles.find(item => item.content.includes(protectedText)); + if (leaked) { + throw new Error( + `Protected Case Study text leaked into public deployment output ${leaked.file}: ${protectedText}` + ); + } + } + } + + console.log( + `[build:vercel-public] Built ${files.length} public file(s) in dist/ with no internal docs or protected Case Study identifiers.` + ); +} + +async function main() { + await fs.rm(output, { recursive: true, force: true }); + await fs.mkdir(output, { recursive: true }); + + for (const file of ROOT_RUNTIME_FILES) { + await copyFile(file); + } + for (const directory of RUNTIME_DIRECTORIES) { + await copyJavaScriptTree(directory); + } + + await verifyOutput(); +} + +main().catch(error => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; +}); From e358fd9ac054821e160680eeb01e248a9e47321b Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 15:59:43 -0400 Subject: [PATCH 041/100] fix: recurse through public component modules --- scripts/build-vercel-public.mjs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/scripts/build-vercel-public.mjs b/scripts/build-vercel-public.mjs index f0c0efb..e6e65f4 100644 --- a/scripts/build-vercel-public.mjs +++ b/scripts/build-vercel-public.mjs @@ -32,17 +32,23 @@ async function copyFile(relativePath) { await fs.copyFile(source, destination); } -async function copyJavaScriptTree(directoryName) { - const sourceDirectory = new URL(`../${directoryName}/`, import.meta.url); - const destinationDirectory = new URL(`../dist/${directoryName}/`, import.meta.url); +async function copyJavaScriptTree(directoryName, relativeDirectory = '') { + const sourceDirectory = new URL( + `../${directoryName}/${relativeDirectory ? `${relativeDirectory}/` : ''}`, + import.meta.url + ); + const destinationDirectory = new URL( + `../dist/${directoryName}/${relativeDirectory ? `${relativeDirectory}/` : ''}`, + import.meta.url + ); await fs.mkdir(destinationDirectory, { recursive: true }); const entries = await fs.readdir(sourceDirectory, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { - throw new Error( - `Unexpected nested directory in public runtime tree ${directoryName}/${entry.name}; update build-vercel-public.mjs explicitly.` - ); + const child = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name; + await copyJavaScriptTree(directoryName, child); + continue; } if (!entry.isFile() || !entry.name.endsWith('.js')) continue; await fs.copyFile( From 3d371729c7bc583cf27f7b52ec7ada5e254a4b71 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:00:05 -0400 Subject: [PATCH 042/100] build: validate minimal Vercel bundle in quality --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b8d91b3..e037232 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,9 @@ "verify:lockfile": "node scripts/verify-lockfile.js", "check:templates": "npm run build:templates && git diff --exit-code -- src/templates.manifest.js api/protected-case-studies.manifest.js", "repo:doctor": "node scripts/repo-doctor.js", - "quality": "npm run verify:lockfile && npm run repo:doctor && npm run verify:tests && npm run verify:summary && npm run verify:persistence && npm run lint && npm run check:templates && npm run verify:protected-cases && npm run check:storage-docs && npm test", - "verify:protected-cases": "node scripts/verify-protected-case-boundary.mjs" + "quality": "npm run verify:lockfile && npm run repo:doctor && npm run verify:tests && npm run verify:summary && npm run verify:persistence && npm run lint && npm run check:templates && npm run verify:protected-cases && npm run build:vercel-public && npm run check:storage-docs && npm test", + "verify:protected-cases": "node scripts/verify-protected-case-boundary.mjs", + "build:vercel-public": "node scripts/build-vercel-public.mjs" }, "dependencies": { "@neondatabase/serverless": "1.1.0" From b06116f4b160a5ea400eac6159151edfdef5d2b6 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:00:29 -0400 Subject: [PATCH 043/100] security: serve only generated Vercel public bundle --- vercel.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vercel.json b/vercel.json index 94246f4..5d8f4e4 100644 --- a/vercel.json +++ b/vercel.json @@ -1,6 +1,6 @@ { - "buildCommand": "npm run verify:protected-cases", - "outputDirectory": ".", + "buildCommand": "npm run verify:protected-cases && npm run build:vercel-public", + "outputDirectory": "dist", "cleanUrls": true, "git": { "deploymentEnabled": { From 7f998f96b92a6760d5437a8fd8ffc36ea0a97f4c Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:01:00 -0400 Subject: [PATCH 044/100] test: guard minimal Vercel static output --- scripts/verify-protected-case-boundary.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/scripts/verify-protected-case-boundary.mjs b/scripts/verify-protected-case-boundary.mjs index 0b4609f..6c1e579 100644 --- a/scripts/verify-protected-case-boundary.mjs +++ b/scripts/verify-protected-case-boundary.mjs @@ -47,12 +47,14 @@ async function main() { } const vercelConfig = JSON.parse(vercelConfigRaw); - const buildCommand = typeof vercelConfig.buildCommand === 'string' ? vercelConfig.buildCommand : ''; - if (!buildCommand.includes('npm run verify:protected-cases')) { - throw new Error('Vercel buildCommand must run verify:protected-cases.'); + const expectedBuildCommand = 'npm run verify:protected-cases && npm run build:vercel-public'; + if (vercelConfig.buildCommand !== expectedBuildCommand) { + throw new Error( + 'Vercel buildCommand must verify protected cases and then build the minimal public dist/ bundle.' + ); } - if (/build:templates|npm\s+run\s+build(?:\s|$)/u.test(buildCommand)) { - throw new Error('Vercel buildCommand must not regenerate manifests after authored template JSON is excluded.'); + if (vercelConfig.outputDirectory !== 'dist') { + throw new Error('Vercel outputDirectory must be dist; serving the repository root exposes internal files.'); } const deploymentEnabled = vercelConfig.git?.deploymentEnabled; From 770c92835489257dfb58754a1148b4f8808b435b Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:06:29 -0400 Subject: [PATCH 045/100] docs: define minimal public Vercel output --- docs/REPOSITORY-OPERATIONS.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/REPOSITORY-OPERATIONS.md b/docs/REPOSITORY-OPERATIONS.md index 6bc98a0..824a895 100644 --- a/docs/REPOSITORY-OPERATIONS.md +++ b/docs/REPOSITORY-OPERATIONS.md @@ -33,10 +33,13 @@ Case Study authoring source is intentionally present in Git but intentionally ab - `src/templates.manifest.js` is public and may contain Standard Templates only. - `api/protected-case-studies.manifest.js` is server-only generated content and may contain protected Case Study metadata/payloads. - Browser runtime files must never import or duplicate the server-only manifest. -- Production Vercel builds run `npm run verify:protected-cases` rather than regenerating manifests, because the authored JSON files are intentionally excluded from the upload. -- GitHub CI/local authoring remains responsible for `npm run build:templates` / `npm run check:templates` freshness. +- `scripts/build-vercel-public.mjs` generates the **only** public static surface under `dist/`: `index.html`, `main.js`, `styles.css`, and browser JavaScript under `src/` and `components/`. +- Vercel `outputDirectory` must remain `dist`. Serving `.` is prohibited because production previously exposed internal files such as `docs/classroom-workstream.md` and `scripts/build-templates-manifest.mjs`. +- The public-bundle builder rejects internal Markdown/JSON/MJS/AGENTS files and scans all emitted text for protected Case Study IDs/names. +- Production Vercel builds run `npm run verify:protected-cases && npm run build:vercel-public`; they do not regenerate manifests because authored template JSON is intentionally excluded from the upload. +- GitHub CI/local authoring remains responsible for `npm run build:templates` / `npm run check:templates` freshness, and `npm run quality` also builds/verifies the same minimal `dist/` surface. -Any change to `vercel.json`, `.vercelignore`, template generation, static output layout, or protected-resource routing must re-run `npm run verify:protected-cases` and include a deployed HTTP check that raw `/templates/*.json` Case Study paths are not served. +Any change to `vercel.json`, `.vercelignore`, template generation, static output layout, or protected-resource routing must re-run `npm run quality` and include a deployed HTTP check that raw `/templates/*.json` Case Study paths and internal `/docs/*` / `/scripts/*` paths are not served. ### Vercel Git deployment policy From 482acf7d6ca0947aa925ea4d074ec519474b0b7a Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:06:55 -0400 Subject: [PATCH 046/100] docs: document minimal Vercel static bundle --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 31ebbdf..dc08177 100644 --- a/README.md +++ b/README.md @@ -182,11 +182,13 @@ Need to know which module owns a given storage field? Jump to the [Storage-to-Mo - `npm run dev` and local `npm run build` still regenerate both manifests for maintainers. Commit the authored JSON and both generated manifests together. ### Vercel deployment -- Production Vercel builds execute `npm run verify:protected-cases` (see `vercel.json`). They **do not regenerate manifests**, because authored `templates/*.json` source files are deliberately excluded from the deployment upload. -- Generated manifests are therefore committed artifacts. GitHub CI remains responsible for running `npm run build:templates` / `check:templates` and proving they match their authored JSON sources. -- `.vercelignore` prevents raw authored Case Study JSON from becoming directly fetchable static files. Keep that exclusion and the protected-case verifier aligned with any future output-directory/deployment changes. +- Production Vercel builds execute `npm run verify:protected-cases && npm run build:vercel-public` (see `vercel.json`). They **do not regenerate manifests**, because authored `templates/*.json` source files are deliberately excluded from the deployment upload. +- `scripts/build-vercel-public.mjs` creates the only public static surface under `dist/`: `index.html`, `main.js`, `styles.css`, and browser JavaScript under `src/` and `components/`. Internal docs, tests, build scripts, authoring JSON, AGENTS files, and repository metadata are not copied. +- `vercel.json` must keep `outputDirectory: "dist"`. The former repository-root output exposed internal files such as `/docs/classroom-workstream.md` and `/scripts/build-templates-manifest.mjs`. +- Generated manifests are committed artifacts. GitHub CI remains responsible for running `npm run build:templates` / `check:templates` and proving they match authored JSON; `npm run quality` also builds/verifies the same minimal `dist/` bundle. +- `.vercelignore` prevents raw authored Case Study JSON from entering the Vercel source upload, while the `dist/` build prevents all non-runtime repository files from becoming public static assets. - Vercel Git deployments are deny-by-default for ordinary branches. `main` deploys automatically; short-lived `verify/**` branches are the explicit preview path for security/E2E checks. `vercel.json` also owns the ignored-build decision so those two allowed branch classes proceed even if the project dashboard has an older Ignored Build Step configured. This prevents AI checkpoint commits from exhausting Vercel build capacity while preserving GitHub CI on every PR commit. -- When modifying the manifest workflow or required quality gates, update both this README and `vercel.json` so the documented steps mirror the actual hosted build. +- When modifying the manifest workflow, public bundle, or required quality gates, update both this README and `vercel.json` so the documented steps mirror the actual hosted build. - Dependency changes must be installed through the normal npm registry so npm generates the complete lockfile. Run `npm run verify:lockfile` before committing and `npm ci` from a clean dependency tree; the offline guard catches missing resolved root-package entries before CI reaches its clean install. ## Documentation & anchor hygiene From d47a999203202fd1675913b0da9e80c0a6691837 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:07:22 -0400 Subject: [PATCH 047/100] docs: harden public static security boundary --- SECURITY.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index 86cb49c..728d046 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -35,6 +35,8 @@ Protected Case Study catalog responses contain metadata only; payloads are selec Authored JSON under `templates/*.json` is build-time source and is excluded from Vercel uploads with `.vercelignore`. Production must use committed generated manifests and run `npm run verify:protected-cases`; do not change the Vercel build back to a command that needs excluded authoring JSON. The verifier must remain in the canonical quality gate and must prove protected IDs/names are absent from public browser runtime assets. +Public static delivery is also an explicit allowlist boundary. `scripts/build-vercel-public.mjs` emits only browser runtime files to `dist/`, and Vercel must keep `outputDirectory: "dist"`. Never return static output to the repository root: production previously exposed internal Markdown and build scripts when `outputDirectory` was `.`. Internal `docs/`, `tests/`, `scripts/`, AGENTS files, authoring material, and repository metadata must not be public assets. + After an authorized Student applies a Case Study, its selected-mode content becomes that Student/team Intake and follows normal Intake/collaboration persistence. That is expected authorized use; the protected-delivery boundary prevents unauthenticated or Standalone retrieval of the original protected resource. ## Supported code From 59cf423fd4eb5796b28f53338b1fba0bf06aa5c1 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:07:52 -0400 Subject: [PATCH 048/100] docs: add public bundle cold-start invariant --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index 6c2d73e..da599de 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,7 @@ Classroom invariants: - student credentials never provide class/workspace enumeration; - Case Study unlock passwords are pedagogy, not authentication; - protected Case Study metadata/payloads are server-gated, absent from public browser/static assets, and authored `templates/*.json` remains excluded from Vercel deployment; `src/templates.manifest.js` is Standard-only and browser code must never import `api/protected-case-studies.manifest.js`. +- public Vercel static output comes only from generated `dist/`; never set `outputDirectory` back to `.` or otherwise expose repository docs/tests/scripts/AGENTS/authoring files. `scripts/build-vercel-public.mjs` owns the allowlisted browser bundle and is part of the canonical quality gate. Every classroom PR must update `docs/classroom-workstream.md` before handoff with its issue, PR/base dependency, current HEAD, completed/incomplete work, validation, risks, and exact next action. For stacked PRs, state the merge order and never describe an unmerged dependent PR as independently mergeable. From 0641d8d477bd10f7b995c439acee6d38e1300656 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:08:19 -0400 Subject: [PATCH 049/100] docs: checkpoint minimal Vercel public bundle --- docs/classroom-workstream.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/classroom-workstream.md b/docs/classroom-workstream.md index 3bcc8ae..1981a40 100644 --- a/docs/classroom-workstream.md +++ b/docs/classroom-workstream.md @@ -66,17 +66,18 @@ Completed the #295 implementation/security/documentation slices through root col Continue #295 on draft PR #307 from the current branch head. -1. Runtime/security implementation is complete, including public/server manifest split, Classroom-authorized delivery, browser/runtime leak guards, Vercel upload verification, and role-aware client integration. -2. Vercel Git deployment policy is now repository-owned: - - ordinary branches are denied deployment by default; - - `main` remains allowed for production; - - `verify/**` branches are allowed for deliberate preview verification; - - repo `ignoreCommand` explicitly continues only `main` and `verify/**`. -3. Ordinary feature commits repeatedly triggered GitHub CI/security checks while producing **no Vercel deployment record**. -4. Verification branch `verify/protected-cases-preview` was moved to guarded PR head `f9f8fc8f22cd71029bb492ad278c4470264153b6`; Vercel deployment `dpl_Ep2q8Y3BMUWGEV7fzZQHnvdwPumA` reached **READY**. -5. On that exact READY preview, the previously exposed protected authoring path `/templates/Microcomputer%20Cabinets.json` returned a genuine HTTP **404**. -6. The connected Vercel fetch tool still stops at this project's Vercel Authentication layer for protected API requests after the initial static request. The normal web fetch cannot access the preview, the local runtime has no outbound DNS, and no Vercel CLI/automation bypass secret is available. Do not weaken preview protection or persist a bypass secret simply to satisfy this check. -7. Exact next action: finalize docs/PR/issue evidence on the latest head, confirm all repository gates are green, then decide whether the deployed API authorization matrix can be accepted based on deterministic server tests + READY preview/static boundary evidence or must wait for an approved Vercel automation-bypass path. Keep PR #307 draft and do not merge without explicit authorization. +1. Protected Case Study runtime/security implementation is complete: public/server manifest split, Classroom authorization, role-aware client delivery, static/runtime leak guards, and deterministic API/client coverage. +2. A second-order production exposure was discovered during deployed verification: with the old `outputDirectory: "."`, production returned internal repository files such as `/docs/classroom-workstream.md` and `/scripts/build-templates-manifest.mjs` with HTTP 200. +3. #307 now builds a minimal public `dist/` surface via `scripts/build-vercel-public.mjs`. It copies only `index.html`, `main.js`, `styles.css`, and browser JavaScript under `src/` + `components/`; it rejects internal file types and protected Case Study identifiers. +4. `vercel.json` now uses `outputDirectory: "dist"` and `npm run verify:protected-cases && npm run build:vercel-public`. The verifier rejects any return to repository-root static output. +5. Exact GitHub CI evidence on `7f998f96b92a6760d5437a8fd8ffc36ea0a97f4c`: + - `[verify:protected-cases] Protected 4 Case Study payload(s); public manifest/runtime and Vercel deployment boundary verified.` + - `[build:vercel-public] Built 45 public file(s) in dist/ with no internal docs or protected Case Study identifiers.` + - CI, CodeQL, Dependency Review, and Template Manifest Guard all green. +6. Vercel Git deployment policy remains repository-owned: ordinary branches create no Vercel deployment; `main` and `verify/**` are the only allowed classes, and repo `ignoreCommand` explicitly continues those branches. +7. A deliberate preview of the new `dist/` head was requested through `verify/protected-cases-preview`, but Vercel returned `upgradeToPro=build-rate-limit` before creating the deployment. This remains tracked in #304. +8. The earlier guarded preview `dpl_Ep2q8Y3BMUWGEV7fzZQHnvdwPumA` reached READY and returned HTTP 404 for the formerly public Microcomputer Cabinets authored JSON path, proving the protected authoring-source boundary before the `dist/` broadening. +9. Exact next action: finish PR/issue/diff synchronization on the latest documented head, then obtain one deliberate `verify/**` preview when Vercel capacity permits and confirm `/docs/classroom-workstream.md`, `/scripts/build-templates-manifest.mjs`, and protected `/templates/*.json` paths return 404. The deployed protected API matrix still requires an approved preview-auth bypass path; deterministic server tests are already green. Keep #307 draft and do not merge without explicit authorization. ## Completed #300 implementation @@ -207,7 +208,9 @@ Continue #295 on draft PR #307 from the current branch head. - That READY preview returned HTTP **404** for the formerly public Microcomputer Cabinets authored JSON path. - Deterministic API tests already cover Instructor authorization, Student membership-bound authorization, metadata-only catalogs, authenticated payload POST, legacy-token rejection, and no-store/no-referrer response headers. - Remaining deployed API requests are blocked by the preview-auth tooling boundary, not by application/test failure. -- Exact next action: finish final metadata/diff review and keep #307 draft until the remaining verification requirement is explicitly accepted or an approved preview automation-bypass path becomes available. Do not merge without explicit authorization. +- Additional static-output hardening: production's old repository-root output was confirmed to expose internal `docs/*` and `scripts/*` files. #307 now generates/serves only `dist/`; canonical CI built 45 public files with no internal docs or protected identifiers. +- Current deployed `dist/` verification is blocked only by Vercel's build-rate limit; #304 contains the infrastructure evidence. +- Exact next action: synchronize final PR/issue metadata, review the complete diff, and run one deliberate `verify/**` preview of the latest head when Vercel capacity permits. Do not merge without explicit authorization. ## Known risks / watch items From e7d6439d828d29cdd9db9398038b5546340ccfe9 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:11:49 -0400 Subject: [PATCH 050/100] docs: finalize protected-case roadmap status --- docs/classroom-roadmap.md | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/classroom-roadmap.md b/docs/classroom-roadmap.md index bb29b1f..d019bd6 100644 --- a/docs/classroom-roadmap.md +++ b/docs/classroom-roadmap.md @@ -164,7 +164,7 @@ Primary deliverables: **Issue:** #295 -**Implementation:** draft PR #307 on `feature/classroom-protected-cases`; runtime/security implementation complete, documentation/final gates in progress. +**Implementation:** draft PR #307 on `feature/classroom-protected-cases`; implementation, repository validation, and documentation are complete. Final deployed `dist/` verification remains blocked only by Vercel build capacity / preview-auth tooling. Requires #290 semantics and #291 authorization; both prerequisites are merged. @@ -176,16 +176,26 @@ Implemented deliverables: - metadata-only GET catalog and authenticated POST payload delivery; - in-memory Student/Instructor protected-resource client integrated with the shared drawer; - Standalone remains protected-resource free and backend-optional; -- authored `templates/*.json` source excluded from Vercel deployment; +- authored `templates/*.json` source excluded from Vercel source upload; - protected Case Study public-runtime/deployment verifier in the canonical quality gate; -- authorization, bundle-boundary, and role/drawer regression tests; -- security/architecture/cold-start documentation migration. +- minimal generated `dist/` public bundle replaces repository-root static output so internal docs/tests/scripts/authoring files are not public assets; +- Vercel Git deployments are deny-by-default, with `main` and deliberate `verify/**` branches explicitly allowed; +- authorization, bundle-boundary, role/drawer, and public-output regression coverage; +- security/architecture/cold-start/deployment documentation migration. + +Validation evidence: + +- full suite: 236 tests, 235 pass, 0 fail, 1 intentional skip; +- exact minimal-output implementation head `7f998f96...`: CI, CodeQL, Dependency Review, and Template Manifest Guard green; +- canonical CI built 45 public `dist/` files with no internal docs or protected Case Study identifiers; +- prior deliberate READY preview returned HTTP 404 for the formerly public Microcomputer Cabinets authored JSON path. Remaining before merge: -- finish final documentation synchronization; -- verify the final documented head has CI, CodeQL, Dependency Review, and Template Manifest Guard green; -- review the complete PR diff and production-preview behavior, especially that raw `/templates/*.json` paths are no longer served. +- obtain one deliberate `verify/**` preview of the latest `dist/` head when Vercel build capacity permits; +- confirm app root loads and internal `/docs/*`, `/scripts/*`, and protected `/templates/*.json` paths return 404; +- execute the deployed protected-API matrix only if an approved Preview Authentication bypass path is available; do not weaken preview protection to manufacture this evidence; +- explicit merge authorization. ## Slice 8 — Browser/E2E and required CI From 300c5f7b6fd451da61993b57f952b15e34e339cb Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:19:10 -0400 Subject: [PATCH 051/100] fix: keep public EULA in minimal Vercel bundle --- scripts/build-vercel-public.mjs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/scripts/build-vercel-public.mjs b/scripts/build-vercel-public.mjs index e6e65f4..ba1ab52 100644 --- a/scripts/build-vercel-public.mjs +++ b/scripts/build-vercel-public.mjs @@ -20,6 +20,10 @@ const ROOT_RUNTIME_FILES = Object.freeze([ 'styles.css' ]); +const PUBLIC_DOCUMENT_FILES = Object.freeze([ + 'docs/eula.md' +]); + const RUNTIME_DIRECTORIES = Object.freeze([ 'components', 'src' @@ -79,6 +83,7 @@ async function verifyOutput() { for (const file of files) { if (!( ROOT_RUNTIME_FILES.includes(file) + || PUBLIC_DOCUMENT_FILES.includes(file) || file.startsWith('src/') || file.startsWith('components/') )) { @@ -86,7 +91,7 @@ async function verifyOutput() { } if ( - file.endsWith('.md') + (file.endsWith('.md') && !PUBLIC_DOCUMENT_FILES.includes(file)) || file.endsWith('.json') || file.endsWith('.mjs') || file.includes('AGENTS') @@ -95,9 +100,9 @@ async function verifyOutput() { } } - for (const required of ROOT_RUNTIME_FILES) { + for (const required of [...ROOT_RUNTIME_FILES, ...PUBLIC_DOCUMENT_FILES]) { if (!files.includes(required)) { - throw new Error(`Required browser runtime file missing from public deployment output: ${required}`); + throw new Error(`Required public runtime file missing from deployment output: ${required}`); } } @@ -126,7 +131,7 @@ async function main() { await fs.rm(output, { recursive: true, force: true }); await fs.mkdir(output, { recursive: true }); - for (const file of ROOT_RUNTIME_FILES) { + for (const file of [...ROOT_RUNTIME_FILES, ...PUBLIC_DOCUMENT_FILES]) { await copyFile(file); } for (const directory of RUNTIME_DIRECTORIES) { From 6d1b638311bfa561a658d48b5fa485adc8fe52e2 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:22:15 -0400 Subject: [PATCH 052/100] docs: record public EULA bundle exception --- docs/REPOSITORY-OPERATIONS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/REPOSITORY-OPERATIONS.md b/docs/REPOSITORY-OPERATIONS.md index 824a895..fa82373 100644 --- a/docs/REPOSITORY-OPERATIONS.md +++ b/docs/REPOSITORY-OPERATIONS.md @@ -33,9 +33,9 @@ Case Study authoring source is intentionally present in Git but intentionally ab - `src/templates.manifest.js` is public and may contain Standard Templates only. - `api/protected-case-studies.manifest.js` is server-only generated content and may contain protected Case Study metadata/payloads. - Browser runtime files must never import or duplicate the server-only manifest. -- `scripts/build-vercel-public.mjs` generates the **only** public static surface under `dist/`: `index.html`, `main.js`, `styles.css`, and browser JavaScript under `src/` and `components/`. +- `scripts/build-vercel-public.mjs` generates the **only** public static surface under `dist/`: `index.html`, `main.js`, `styles.css`, browser JavaScript under `src/` and `components/`, plus the explicitly public `docs/eula.md` linked from the app footer. - Vercel `outputDirectory` must remain `dist`. Serving `.` is prohibited because production previously exposed internal files such as `docs/classroom-workstream.md` and `scripts/build-templates-manifest.mjs`. -- The public-bundle builder rejects internal Markdown/JSON/MJS/AGENTS files and scans all emitted text for protected Case Study IDs/names. +- The public-bundle builder rejects all other Markdown plus JSON/MJS/AGENTS/internal files and scans all emitted text—including the EULA—for protected Case Study IDs/names. - Production Vercel builds run `npm run verify:protected-cases && npm run build:vercel-public`; they do not regenerate manifests because authored template JSON is intentionally excluded from the upload. - GitHub CI/local authoring remains responsible for `npm run build:templates` / `npm run check:templates` freshness, and `npm run quality` also builds/verifies the same minimal `dist/` surface. From 0d869b973ff93c949d6e4560bc0f8e7e10793b42 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:22:49 -0400 Subject: [PATCH 053/100] docs: document public EULA bundle exception --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dc08177..ee2e8ff 100644 --- a/README.md +++ b/README.md @@ -183,7 +183,7 @@ Need to know which module owns a given storage field? Jump to the [Storage-to-Mo ### Vercel deployment - Production Vercel builds execute `npm run verify:protected-cases && npm run build:vercel-public` (see `vercel.json`). They **do not regenerate manifests**, because authored `templates/*.json` source files are deliberately excluded from the deployment upload. -- `scripts/build-vercel-public.mjs` creates the only public static surface under `dist/`: `index.html`, `main.js`, `styles.css`, and browser JavaScript under `src/` and `components/`. Internal docs, tests, build scripts, authoring JSON, AGENTS files, and repository metadata are not copied. +- `scripts/build-vercel-public.mjs` creates the only public static surface under `dist/`: `index.html`, `main.js`, `styles.css`, browser JavaScript under `src/` and `components/`, plus the explicitly public `docs/eula.md` linked from the footer. All other docs, tests, build scripts, authoring JSON, AGENTS files, and repository metadata are not copied. - `vercel.json` must keep `outputDirectory: "dist"`. The former repository-root output exposed internal files such as `/docs/classroom-workstream.md` and `/scripts/build-templates-manifest.mjs`. - Generated manifests are committed artifacts. GitHub CI remains responsible for running `npm run build:templates` / `check:templates` and proving they match authored JSON; `npm run quality` also builds/verifies the same minimal `dist/` bundle. - `.vercelignore` prevents raw authored Case Study JSON from entering the Vercel source upload, while the `dist/` build prevents all non-runtime repository files from becoming public static assets. From 1238b82b0d6f880a49e83bedd31d8ebe5f0d967d Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 16:23:18 -0400 Subject: [PATCH 054/100] docs: scope public-document security exception --- SECURITY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SECURITY.md b/SECURITY.md index 728d046..ae91b49 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -35,7 +35,7 @@ Protected Case Study catalog responses contain metadata only; payloads are selec Authored JSON under `templates/*.json` is build-time source and is excluded from Vercel uploads with `.vercelignore`. Production must use committed generated manifests and run `npm run verify:protected-cases`; do not change the Vercel build back to a command that needs excluded authoring JSON. The verifier must remain in the canonical quality gate and must prove protected IDs/names are absent from public browser runtime assets. -Public static delivery is also an explicit allowlist boundary. `scripts/build-vercel-public.mjs` emits only browser runtime files to `dist/`, and Vercel must keep `outputDirectory: "dist"`. Never return static output to the repository root: production previously exposed internal Markdown and build scripts when `outputDirectory` was `.`. Internal `docs/`, `tests/`, `scripts/`, AGENTS files, authoring material, and repository metadata must not be public assets. +Public static delivery is also an explicit allowlist boundary. `scripts/build-vercel-public.mjs` emits only browser runtime files plus the intentionally public `docs/eula.md` to `dist/`, and Vercel must keep `outputDirectory: "dist"`. Never return static output to the repository root: production previously exposed internal Markdown and build scripts when `outputDirectory` was `.`. Other `docs/`, `tests/`, `scripts/`, AGENTS files, authoring material, and repository metadata must not be public assets. After an authorized Student applies a Case Study, its selected-mode content becomes that Student/team Intake and follows normal Intake/collaboration persistence. That is expected authorized use; the protected-delivery boundary prevents unauthenticated or Standalone retrieval of the original protected resource. From b0ee1803e752a8ab9d54047aa01427d68c74a7db Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:31:42 -0400 Subject: [PATCH 055/100] docs: record final protected-case preview --- docs/classroom-workstream.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/classroom-workstream.md b/docs/classroom-workstream.md index 1981a40..d595d9c 100644 --- a/docs/classroom-workstream.md +++ b/docs/classroom-workstream.md @@ -210,7 +210,13 @@ Continue #295 on draft PR #307 from the current branch head. - Remaining deployed API requests are blocked by the preview-auth tooling boundary, not by application/test failure. - Additional static-output hardening: production's old repository-root output was confirmed to expose internal `docs/*` and `scripts/*` files. #307 now generates/serves only `dist/`; canonical CI built 45 public files with no internal docs or protected identifiers. - Current deployed `dist/` verification is blocked only by Vercel's build-rate limit; #304 contains the infrastructure evidence. -- Exact next action: synchronize final PR/issue metadata, review the complete diff, and run one deliberate `verify/**` preview of the latest head when Vercel capacity permits. Do not merge without explicit authorization. +- Final documented head before this update: `1238b82b0d6f880a49e83bedd31d8ebe5f0d967d`; all required GitHub checks green and PR #307 has no unresolved review threads. +- Complete `main → #307` diff review: 54 commits ahead, 0 behind; changed files remain confined to protected Case Study delivery, Vercel/public-output hardening, tests, and documentation. +- Deliberate final-head preview: `verify/protected-cases-preview` was moved to `1238b82...`; Vercel deployment `dpl_GqbgkkwKLJ7Z1Ekiw4oZ1eMYwgXz` reached **READY**. +- On that READY preview, `/docs/classroom-workstream.md` returned a genuine HTTP **404**. +- Vercel Preview Authentication then intercepted subsequent HTTP probes. A deliberately nonexistent path also returned the same HTTP 302 authentication response, proving those 302s are auth-layer behavior and cannot be interpreted as file-existence results. +- Therefore the deployed evidence is: exact final-head preview READY + build-time protected/public-boundary checks passed + one formerly exposed internal document returned 404. Remaining protected API HTTP matrix cannot be executed from the current connector without an approved preview-auth bypass path. +- Exact next action: synchronize PR #307 / issue #295 with this final preview evidence, then keep the PR draft until the user explicitly accepts the remaining preview-auth limitation or provides/approves a bypass path. Do not merge without explicit authorization. ## Known risks / watch items From 593b9766d3a993c4834f475a90a362cfaf1aa6f0 Mon Sep 17 00:00:00 2001 From: Shane Chagpar <42649692+MajorIncident@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:40:45 -0400 Subject: [PATCH 056/100] chore: refresh browser E2E stack on protected-case head --- .github/workflows/browser-e2e.yml | 52 ++++++++++++ .gitignore | 2 + index.html | 2 +- package-lock.json | 70 ++++++++++++++++ package.json | 9 +- playwright.config.mjs | 45 ++++++++++ scripts/serve-browser-tests.mjs | 124 ++++++++++++++++++++++++++++ src/kt.js | 9 +- styles.css | 8 +- tests/e2e/intake.smoke.spec.mjs | 53 ++++++++++++ tests/e2e/standalone.spec.mjs | 83 +++++++++++++++++++ tests/kt-table.persistence.test.mjs | 27 ++++++ 12 files changed, 477 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/browser-e2e.yml create mode 100644 playwright.config.mjs create mode 100644 scripts/serve-browser-tests.mjs create mode 100644 tests/e2e/intake.smoke.spec.mjs create mode 100644 tests/e2e/standalone.spec.mjs diff --git a/.github/workflows/browser-e2e.yml b/.github/workflows/browser-e2e.yml new file mode 100644 index 0000000..a19d677 --- /dev/null +++ b/.github/workflows/browser-e2e.yml @@ -0,0 +1,52 @@ +name: Browser E2E + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: browser-e2e-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + browser: + timeout-minutes: 20 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: '.nvmrc' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Install Chromium + run: npx playwright install --with-deps chromium + + - name: Run browser tests + run: npm run test:browser + + - name: Upload browser failure artifacts + if: failure() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: browser-e2e-artifacts + path: | + playwright-report/ + test-results/ + if-no-files-found: ignore + retention-days: 7 diff --git a/.gitignore b/.gitignore index 6e8f6a2..121b9cd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules/ dist/ data/ .DS_Store +playwright-report/ +test-results/ diff --git a/index.html b/index.html index 586a74c..461f5e4 100644 --- a/index.html +++ b/index.html @@ -49,7 +49,7 @@

KT Intake

Choose experience -