import { STATES, evaluateAssessment } from './act-crosswalk-engine.mjs'; // This module fetches only the public prompt catalogue. Optional usage events are consent-gated by the shared module; assessment entries are never sent. const clouds = { aws: 'AWS', azure: 'Azure', gcp: 'Google Cloud' }; const labels = { unknown: 'Unknown', missing: 'Missing', configured: 'Configured only', evidence_recorded: 'Evidence recorded (self-reported)', not_applicable: 'Not applicable (recorded claim)', incomplete: 'Incomplete record', invalid: 'Invalid response' }; const assets = { '01': 'AI initiative intake and risk ownership', '02': 'Agent authority and tool boundaries', '03A': 'MCP and skill sandbox gate', '03B': 'MCP sandbox review and decision', '04A': 'AI vendor and subprocessor review', '04B': 'AI supplier contract rider and negotiation record', '05': 'Post-deployment drift and human review', '06': 'Agent incident containment and recovery runbook', '07': 'Executive risk appetite and exception record', '08A': 'Board AI oversight deck', '08B': 'Board and audit committee AI briefing' }; const fields = [['evidence', 'Evidence reference', 'text'], ['reviewer', 'Reviewer reference', 'text'], ['reviewDate', 'Review date', 'date'], ['owner', 'Action owner reference', 'text'], ['nextAction', 'Next action', 'textarea'], ['dueDate', 'Action due date', 'date'], ['rationale', 'Reason for not applicable', 'textarea']]; const $ = id => document.getElementById(id); const el = (tag, text, cls) => { const node = document.createElement(tag); if (text !== undefined) node.textContent = text; if (cls) node.className = cls; return node; }; const blank = () => ({ controls: {}, suppliers: {} }); let data; let cloud = 'aws'; let scope = ''; let state = { aws: blank(), azure: blank(), gcp: blank() }; let result; function notice(message) { $('mapper-notice').textContent = message; } function selectedControls() { const filter = $('workflow-filter').value; return data.controls.filter(control => filter === 'all' || control.assets.includes(filter)); } function assetLink(id) { const link = el('a', 'Asset ' + id + ': ' + assets[id]); link.href = 'ai-controls-toolkit.html#asset-' + id.toLowerCase(); return link; } function assetList(ids) { const list = el('ul', undefined, 'mapper-asset-links'); [...new Set(ids)].forEach(id => { const item = el('li'); item.append(assetLink(id)); list.append(item); }); return list; } function captureRecord(group, record, card) { window.M78Analytics && window.M78Analytics.toolStart('aws-azure-gcp-ai-governance-control-evidence-crosswalk', cloud); const answer = { status: card.querySelector('select').value }; fields.forEach(([key]) => { answer[key] = card.querySelector('[data-field="' + key + '"]').value; }); state[cloud][group][record.id] = answer; const rationale = card.querySelector('[data-rationale]'); rationale.hidden = answer.status !== 'not_applicable'; if (answer.status === 'evidence_recorded' || answer.status === 'not_applicable') card.querySelector('details').open = true; updateSummary(); if(result.controls.unresolved.length===0 && result.suppliers.unresolved.length===0) {window.M78Analytics && window.M78Analytics.toolComplete('aws-azure-gcp-ai-governance-control-evidence-crosswalk', cloud);} } function recordCard(record, group) { const answer = state[cloud][group][record.id] || { status: 'unknown' }; const card = el('article', undefined, 'mapper-record'); card.id = record.id; const heading = el('h3', record.title); heading.id = record.id + '-heading'; card.setAttribute('aria-labelledby', heading.id); card.append(el('p', record.id + (record.critical ? ' · Critical evidence topic' : ' · Evidence topic'), 'mapper-record-id'), heading); card.append(el('p', record.evidence_needed)); if (group === 'controls') card.append(el('p', clouds[cloud] + ' evidence prompt: ' + record[cloud], 'mapper-cloud-prompt')); card.append(el('p', 'Suggested responsible role: ' + record.suggested_owner, 'mapper-help')); const statusLabel = el('label', 'Status'); const select = el('select'); select.id = record.id + '-status'; statusLabel.htmlFor = select.id; STATES.forEach(value => { const option = el('option', labels[value]); option.value = value; select.append(option); }); select.value = answer.status; select.setAttribute('aria-describedby', 'status-help'); card.append(statusLabel, select); const detail = el('details'); detail.open = ['evidence_recorded', 'not_applicable'].includes(answer.status); detail.append(el('summary', 'Evidence, owner and next action')); const fieldGrid = el('div', undefined, 'mapper-fields'); fields.forEach(([key, title, type]) => { const holder = el('div', undefined, type === 'textarea' ? 'mapper-field mapper-field-wide' : 'mapper-field'); const label = el('label', title); const input = el(type === 'textarea' ? 'textarea' : 'input'); if (type !== 'textarea') input.type = type; else input.rows = 2; input.id = record.id + '-' + key; input.dataset.field = key; label.htmlFor = input.id; input.value = answer[key] || ''; input.autocomplete = 'off'; if (type !== 'date') input.maxLength = type === 'textarea' ? 1000 : 240; if (key === 'owner') input.placeholder = record.suggested_owner; if (key === 'nextAction') input.placeholder = record.next_action; if (key === 'evidence') input.placeholder = 'Use an internal reference, not evidence contents'; if (key === 'rationale') { holder.dataset.rationale = ''; holder.hidden = answer.status !== 'not_applicable'; } holder.append(label, input); fieldGrid.append(holder); input.addEventListener('input', () => captureRecord(group, record, card)); }); detail.append(el('p', 'Evidence-recorded and not-applicable entries need every field, including valid review and due dates. An exclusion also needs a reason.', 'mapper-help'), fieldGrid); card.append(detail, el('p', '', 'mapper-record-state')); const related = el('div', undefined, 'mapper-related'); related.append(el('p', 'Related ACT records'), assetList(record.assets || ['04A', '04B'])); card.append(related); select.addEventListener('change', () => captureRecord(group, record, card)); return card; } function renderRecords() { const controls = $('control-records'); controls.replaceChildren(); data.controls.forEach(record => controls.append(recordCard(record, 'controls'))); const suppliers = $('supplier-records'); suppliers.replaceChildren(); data.supplier_checks.forEach(record => suppliers.append(recordCard(record, 'suppliers'))); applyFilter(); } function applyFilter() { const visible = new Set(selectedControls().map(record => record.id)); data.controls.forEach(record => { $(record.id).hidden = !visible.has(record.id); }); $('visible-controls').textContent = visible.size + ' of 10 control topics displayed. All 10 remain in the assessment. All six supplier checks remain in scope.'; updateSummary(); } function revealRecord(id, group) { if (group === 'controls') { $('workflow-filter').value = 'all'; applyFilter(); } const card = $(id); card.querySelector('details').open = true; card.scrollIntoView({ block: 'center', behavior: 'auto' }); card.querySelector('select').focus(); } function resultPanel(value, group) { const panel = el('section', undefined, 'mapper-result-panel'); panel.dataset.disposition = value.disposition; panel.append(el('h3', group === 'controls' ? 'All 10 control topics' : 'All 6 supplier evidence checks')); panel.append(el('p', value.disposition.replaceAll('_', ' '), 'mapper-disposition')); const descriptions = { NOT_ASSESSED: 'No responses recorded for this cloud. Unknown evidence remains unresolved.', CRITICAL_GAP: 'Critical evidence is unknown, missing or incomplete. Route the gap to a responsible person for a scoped decision.', EVIDENCE_GAPS: 'Unresolved evidence remains. Assign the missing records and next actions before human review.', REVIEW_REQUIRED: 'Record fields are complete. These self-reported entries still require human review and a separate decision.' }; panel.append(el('p', descriptions[value.disposition])); if (value.allExcluded) panel.append(el('p', 'All records are excluded by self-report. No applicable controls have been evidenced. Review every exclusion and the assessment scope.', 'mapper-warning')); const counts = el('dl', undefined, 'mapper-counts'); [...STATES, 'incomplete', 'invalid'].forEach(key => { const pair = el('div'); pair.append(el('dt', labels[key]), el('dd', String(value.counts[key] || 0))); counts.append(pair); }); panel.append(counts, el('p', value.unresolved.length + ' unresolved of ' + value.total + '; ' + value.criticalGaps.length + ' critical gaps.', 'mapper-total')); const list = el('ul', undefined, 'mapper-actions'); value.rows.filter(row => row.unresolved).forEach(row => { const item = el('li'); const button = el('button', row.id, 'mapper-text-button'); button.type = 'button'; button.addEventListener('click', () => revealRecord(row.id, group)); item.append(button, el('p', 'State: ' + labels[row.state] + '. Responsible role / recorded owner: ' + row.owner), el('p', 'Next action: ' + row.nextAction)); if (row.dueDate) item.append(el('p', 'Due: ' + row.dueDate)); list.append(item); }); if (value.unresolved.length) { panel.append(el('p', 'Unresolved IDs: ' + value.unresolved.join(', '), 'mapper-unresolved-ids')); const detail = el('details'); detail.append(el('summary', 'Review ' + value.unresolved.length + ' owners and next actions'), list); panel.append(detail); } else panel.append(el('p', 'No unresolved record fields. Refer the records and exclusions to the authorised reviewer.')); return panel; } function updateSummary() { result = evaluateAssessment(data, cloud, state); $('assessed-cloud').textContent = clouds[cloud]; $('assessed-system').textContent = scope || 'Not started'; $('summary-panels').replaceChildren(resultPanel(result.controls, 'controls'), resultPanel(result.suppliers, 'suppliers')); const visible = new Set(selectedControls().map(record => record.id)); const hidden = result.controls.rows.filter(row => row.unresolved && !visible.has(row.id)); $('hidden-gaps').textContent = hidden.length + ' hidden unresolved controls' + (hidden.length ? ': ' + hidden.map(row => row.id).join(', ') : '') + '. Filtering never changes the scope or the result.'; for (const group of ['controls', 'suppliers']) for (const row of result[group].rows) { const card = $(row.id); if (card) card.querySelector('.mapper-record-state').textContent = 'Record state: ' + labels[row.state] + (row.unresolved ? '. Follow-up required.' : '. Human review required.'); } const relevant = new Set(); data.controls.filter(record => result.controls.unresolved.includes(record.id)).forEach(record => record.assets.forEach(id => relevant.add(id))); if (result.suppliers.unresolved.length) { relevant.add('04A'); relevant.add('04B'); } if (!relevant.size) ['07', '08A', '08B'].forEach(id => relevant.add(id)); $('result-assets').replaceChildren(assetList([...relevant].sort())); $('print-snapshot').textContent = textReport(exportRecord()); } function exportRecord() { return { tool: 'Cloud AI Control-to-Evidence Mapper', schema: data.schema_version, editorialReviewDate: data.editorial_review_date, exportedAt: new Date().toISOString(), systemReference: scope, cloud, cloudName: clouds[cloud], scope: { controls: data.controls.map(record => record.id), supplierChecks: data.supplier_checks.map(record => record.id) }, boundary: 'Self-reported evidence and exclusions, not independently verified. Human review and a separate decision are required. This is not deployment approval or proof of an enforceable contract.', selfReportedInputs: structuredClone(state[cloud]), assessment: result }; } function textReport(record) { const lines = [record.tool, 'System reference: ' + (record.systemReference || 'Not started'), 'Assessed cloud: ' + record.cloudName, 'Export / print date: ' + record.exportedAt, record.boundary, '']; for (const group of ['controls', 'suppliers']) { const summary = record.assessment[group]; const catalogue = group === 'controls' ? data.controls : data.supplier_checks; lines.push(group === 'controls' ? 'CONTROL TOPICS (10)' : 'SUPPLIER CHECKS (6)', 'Result: ' + summary.disposition, 'Counts: ' + JSON.stringify(summary.counts), 'All excluded: ' + summary.allExcluded, 'Unresolved IDs: ' + (summary.unresolved.join(', ') || 'None'), ''); for (const row of summary.rows) { const definition = catalogue.find(item => item.id === row.id); const answer = record.selfReportedInputs[group][row.id] || {}; lines.push(row.id + ': ' + definition.title, 'State: ' + labels[row.state], 'Critical: ' + row.critical, 'Owner / suggested role: ' + row.owner, 'Next action: ' + row.nextAction); fields.forEach(([key, label]) => lines.push(label + ': ' + (answer[key] || 'Not recorded'))); lines.push(''); } } return lines.join('\n'); } function download(format) { const record = exportRecord(); const json = format === 'json'; const blob = new Blob([json ? JSON.stringify(record, null, 2) : textReport(record)], { type: json ? 'application/json' : 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const anchor = el('a'); anchor.href = url; anchor.download = 'act-cloud-evidence-' + cloud + '-' + new Date().toISOString().slice(0, 10) + (json ? '.json' : '.txt'); document.body.append(anchor); anchor.click(); window.M78Analytics && window.M78Analytics.reportExport('aws-azure-gcp-ai-governance-control-evidence-crosswalk', json ? 'json' : 'txt'); anchor.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000); notice('Export prepared for ' + clouds[cloud] + '. Review the downloaded file before sharing; it includes your self-reported entries.'); } function setStarted(started) { $('system-reference').disabled = started; $('start-assessment').hidden = started; $('mapper-response-fields').disabled = !started; $('new-assessment').disabled = !started; $('reset-cloud').disabled = !started; } async function initialise() { const response = await fetch(new URL('./cloud-control-crosswalk.json', import.meta.url), { credentials: 'omit', referrerPolicy: 'no-referrer' }); if (!response.ok) throw new Error('Prompt catalogue unavailable'); data = await response.json(); if (data.controls.length !== 10 || data.supplier_checks.length !== 6) throw new Error('Unexpected assessment scope'); $('mapper-loading').hidden = true; $('mapper-app').hidden = false; renderRecords(); setStarted(false); $('start-assessment').addEventListener('click', () => { const reference = $('system-reference').value.trim(); if (!reference) { notice('Enter a non-sensitive system reference to begin.'); $('system-reference').focus(); return; } scope = reference; setStarted(true); updateSummary(); notice('Started ' + clouds[cloud] + ' assessment. Other clouds have separate response sets.'); $('workflow-filter').focus(); }); $('cloud-select').addEventListener('change', () => { cloud = $('cloud-select').value; renderRecords(); notice('Showing ' + clouds[cloud] + '. This cloud has a separate response set for the same system. Other clouds retain their answers in this open page.'); }); $('workflow-filter').addEventListener('change', () => { applyFilter(); notice($('hidden-gaps').textContent); }); $('show-all-controls').addEventListener('click', () => { $('workflow-filter').value = 'all'; applyFilter(); $('workflow-filter').focus(); }); $('reset-cloud').addEventListener('click', () => { window.M78Analytics && window.M78Analytics.toolReset('aws-azure-gcp-ai-governance-control-evidence-crosswalk', cloud);state[cloud] = blank(); $('workflow-filter').value = 'all'; renderRecords(); notice(clouds[cloud] + ' responses cleared. Both panels are NOT ASSESSED. The system reference and other clouds are retained.'); }); $('new-assessment').addEventListener('click', () => { if (!window.confirm('Start a different system? This clears all three clouds and the system reference. Export first if you need the current records.')) return; ['aws','azure','gcp'].forEach(c=>{window.M78Analytics && window.M78Analytics.toolReset('aws-azure-gcp-ai-governance-control-evidence-crosswalk', c);}); state = { aws: blank(), azure: blank(), gcp: blank() }; scope = ''; cloud = 'aws'; $('cloud-select').value = 'aws'; $('system-reference').value = ''; $('workflow-filter').value = 'all'; renderRecords(); setStarted(false); notice('New assessment. All clouds and the previous system reference are cleared.'); $('system-reference').focus(); }); $('export-json').addEventListener('click', () => download('json')); $('export-text').addEventListener('click', () => download('text')); $('print-summary').addEventListener('click', () => { updateSummary(); window.print(); }); window.addEventListener('beforeprint', () => { $('print-snapshot').textContent = textReport(exportRecord()); }); } initialise().catch(() => { $('mapper-loading').textContent = 'The mapper could not load its evidence prompts. Refresh this page to try again. No assessment is available until the prompts load.'; });