From 65367dccbb5787935bd118921ed6237f94a365b0 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 1 Oct 2026 03:54:01 -0400 Subject: [PATCH] TASK-console-attention-actions: add guarded decision shortcuts --- ...sk-console-attention-actions-2026-10-01.md | 37 +++++++++++ site/attention-actions.js | 25 ++++++++ site/attention-projection.js | 1 + site/console.html | 1 + site/console.js | 36 ++++++++++- site/styles.css | 7 +++ test/fleet/console-attention.test.mjs | 63 +++++++++++++++++++ test/fleet/product-surface.test.mjs | 5 +- 8 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 reports/test-audit/test-integrity-task-console-attention-actions-2026-10-01.md create mode 100644 site/attention-actions.js diff --git a/reports/test-audit/test-integrity-task-console-attention-actions-2026-10-01.md b/reports/test-audit/test-integrity-task-console-attention-actions-2026-10-01.md new file mode 100644 index 0000000..f87842e --- /dev/null +++ b/reports/test-audit/test-integrity-task-console-attention-actions-2026-10-01.md @@ -0,0 +1,37 @@ +# Test integrity note — TASK-console-attention-actions + +```yaml +test_integrity_note: + change_type: [new_tests] + scenarios: + - id: SCN-console-attention-owner-shortcut + given: "A pending approval names the project owner and has an available decision form." + when: "The owner chooses an Approve or Reject shortcut from its attention card." + then: "Only the matching decision field is selected and the existing preview path starts; no decision is submitted directly." + - id: SCN-console-attention-shortcut-drafts + given: "The form is unavailable, has an active preview, or contains the opposite decision draft." + when: "An attention shortcut is selected." + then: "The action is declined with an explicit status and existing draft or preview values remain unchanged." + - id: SCN-console-attention-ownership + given: "An approval is pending for the owner or another named approver." + when: "Attention actions are projected." + then: "Only the owner-addressed item carries an owner decision shortcut identifier." + observable_outcomes: + - "Owner attention cards expose Approve and Reject controls only for pending requests addressed to owner." + - "A shortcut enters the existing revision/evidence preview and confirmation flow." + - "Missing forms, active previews, and conflicting drafts remain visible and unchanged." + - "Long evidence is available through a compact expandable disclosure." + determinism_controls: + - "Projection and shortcut scenarios use fixed in-memory fixtures without clock, network, or randomness." + anti_cheat_rationale: + prevents: + - "direct decision submission without preview and confirmation" + - "peer approval authority exposed as an owner control" + - "overwriting a pending decision draft or preview" + - "long evidence silently omitted from compact cards" + relaxation: + did_relax_any_assertion: false + if_true_explain_spec_basis: "" +``` + +Existing approval-console scenarios continue to cover stale revisions, named-approver enforcement, preview confirmation, and failed decisions. The attention tests add coverage at the UI projection and shortcut boundary; actual desktop/mobile visual acceptance remains separate and incomplete until a real browser capture is available. diff --git a/site/attention-actions.js b/site/attention-actions.js new file mode 100644 index 0000000..f89e8b7 --- /dev/null +++ b/site/attention-actions.js @@ -0,0 +1,25 @@ +(function (global) { + function prepareOwnerDecisionShortcut(form, decision, beginPreview) { + if (!['approved', 'rejected'].includes(decision)) { + return { started: false, reason: 'Choose approve or reject from an owner-only shortcut.' }; + } + const choice = form?.elements?.namedItem('decision'); + if (!form || !choice) { + return { started: false, reason: 'This owner decision form is no longer available. Refresh the current evidence before acting.' }; + } + if (form.dataset.previewToken || choice.disabled) { + return { started: false, reason: 'An existing decision preview is preserved. Review or edit it in Flow watch before using another shortcut.' }; + } + if (choice.value && choice.value !== decision) { + return { started: false, reason: 'Your existing decision draft is preserved. Review or change it in Flow watch before choosing another shortcut.' }; + } + if (typeof beginPreview !== 'function') { + return { started: false, reason: 'The guarded decision preview is unavailable. Refresh before acting.' }; + } + choice.value = decision; + beginPreview?.(form); + return { started: true }; + } + + global.TorchAttentionActions = Object.freeze({ prepareOwnerDecisionShortcut }); +})(globalThis); diff --git a/site/attention-projection.js b/site/attention-projection.js index 54c27e1..ba08c2a 100644 --- a/site/attention-projection.js +++ b/site/attention-projection.js @@ -27,6 +27,7 @@ function attentionGroups(snapshot) { waitSince: approval.createdAt ?? null, href: `#approval-${encodeURIComponent(approval.id)}`, action: isOwner ? 'Review request and evidence' : 'Open approval details', + decisionApprovalId: isOwner ? approval.id : null, }); } diff --git a/site/console.html b/site/console.html index 1b6921a..662156a 100644 --- a/site/console.html +++ b/site/console.html @@ -15,6 +15,7 @@ + diff --git a/site/console.js b/site/console.js index d502b4c..cba404d 100644 --- a/site/console.js +++ b/site/console.js @@ -743,8 +743,16 @@ function renderAttention(groups) { ${items.length ? items.map((item) => `
${escapeHtml(item.title)}

${escapeHtml(item.detail)}

- Severity: ${escapeHtml(({ urgent: 'Urgent', review: 'Review', decision: 'Decision required', info: 'Progress' })[item.tone] ?? 'Unknown')} · Owner: ${escapeHtml(item.owner)}${item.waitSince ? ` · Waiting ${escapeHtml(ageLabel(item.waitSince))}` : ''} · ${escapeHtml(item.evidence)} + Severity: ${escapeHtml(({ urgent: 'Urgent', review: 'Review', decision: 'Decision required', info: 'Progress' })[item.tone] ?? 'Unknown')} · Owner: ${escapeHtml(item.owner)}${item.waitSince ? ` · Waiting ${escapeHtml(ageLabel(item.waitSince))}` : ''} + ${item.evidence.length > 120 + ? `
Evidence and references

${escapeHtml(item.evidence)}

` + : `Evidence: ${escapeHtml(item.evidence)}`} ${escapeHtml(item.action)} + ${item.decisionApprovalId ? `
+ + + +
` : ''} ${item.requestOwner ? `Request review from ${escapeHtml(item.owner)}` : ''}
`).join('') : `

${escapeHtml(description)}

`} `; @@ -1829,6 +1837,32 @@ $('#change-list')?.addEventListener('click', async (event) => { }); $('#attention-list')?.addEventListener('click', (event) => { + const decisionButton = event.target.closest('[data-attention-decision]'); + if (decisionButton) { + const card = decisionButton.closest('.attention-item'); + const status = card?.querySelector('.attention-action-status'); + const approvalId = decisionButton.dataset.attentionApproval; + const decision = decisionButton.dataset.attentionDecision; + const form = [...document.querySelectorAll('#approval-request-list .approval-decision-form')] + .find((candidate) => candidate.dataset.approvalId === approvalId); + const previewButton = form?.querySelector('[data-approval-preview]'); + const shortcut = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(form, decision, previewButton ? (approvalForm) => { + approvalForm.scrollIntoView({ behavior: 'smooth', block: 'center' }); + previewButton.click(); + } : null); + if (!shortcut.started) { + if (status) { + status.textContent = shortcut.reason; + status.hidden = false; + } + return; + } + if (status) { + status.textContent = `Opening the guarded ${decision} preview for this owner-addressed request. Confirm only after reviewing its current evidence.`; + status.hidden = false; + } + return; + } const link = event.target.closest('a[href^="#"]'); const target = link && document.getElementById(link.getAttribute('href').slice(1)); const recipient = link?.dataset.attentionRecipient; diff --git a/site/styles.css b/site/styles.css index dbceea6..7ca7687 100644 --- a/site/styles.css +++ b/site/styles.css @@ -190,6 +190,13 @@ footer div { display: flex; gap: 1.5rem; } .attention-item p { margin: .18rem 0 .35rem; color: var(--muted); font-size: .78rem; line-height: 1.4; } .attention-item small { display: block; color: var(--muted); font-size: .69rem; line-height: 1.35; overflow-wrap: anywhere; } .attention-item a { display: inline-block; margin: .35rem .8rem 0 0; color: var(--route); font-size: .76rem; font-weight: 650; } +.attention-evidence { margin-top: .28rem; color: var(--muted); font-size: .69rem; } +.attention-evidence summary { width: fit-content; cursor: pointer; color: var(--route); font-weight: 650; } +.attention-evidence p { margin: .25rem 0 0; white-space: pre-wrap; overflow-wrap: anywhere; } +.attention-quick-actions { display: flex; align-items: center; gap: .4rem; flex-wrap: wrap; margin-top: .4rem; } +.attention-quick-actions button { min-height: 2rem; padding: .3rem .55rem; } +.attention-action-status { flex-basis: 100%; margin: .1rem 0 0; color: var(--muted); font-size: .7rem; line-height: 1.4; } +.attention-action-status[hidden] { display: none; } .board-section, .console-section { padding-block: 1.65rem; border-bottom: 1px solid var(--line); scroll-margin-top: 1rem; } .section-heading { margin-bottom: 1rem; } .section-heading h2 { margin: 0; } diff --git a/test/fleet/console-attention.test.mjs b/test/fleet/console-attention.test.mjs index 0355af4..35b0377 100644 --- a/test/fleet/console-attention.test.mjs +++ b/test/fleet/console-attention.test.mjs @@ -1,6 +1,8 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import '../../site/attention-projection.js'; +import '../../site/attention-actions.js'; +import '../../site/live-refresh.js'; const project = (snapshot) => globalThis.TorchAttentionProjection.groups(snapshot); @@ -20,13 +22,74 @@ test('SCN-console-attention-ownership: only owner-addressed pending decisions en assert.deepEqual(groups.owner.map((item) => item.title), ['Review executor boundary']); assert.equal(groups.owner[0].href, '#approval-owner-review'); + assert.equal(groups.owner[0].decisionApprovalId, 'owner-review'); assert.match(groups.owner[0].detail, /bounded scope/); assert.equal(groups.owner[0].waitSince, '2026-10-01T06:00:00Z'); assert.equal(groups.fleet[0].owner, 'Independent QA'); + assert.equal(groups.fleet[0].decisionApprovalId, null); assert.equal(groups.fleet[0].action, 'Open approval details'); assert.doesNotMatch(JSON.stringify(groups.fleet), /Approve|Reject/); }); +test('SCN-console-attention-owner-shortcut: a shortcut selects only an owner decision and retains the preview-confirm boundary', () => { + const choice = { value: '', disabled: false }; + const form = { dataset: {}, elements: { namedItem: () => choice } }; + let previewCalls = 0; + const result = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(form, 'approved', () => { previewCalls += 1; }); + + assert.equal(result.started, true); + assert.equal(choice.value, 'approved'); + assert.equal(previewCalls, 1, 'the caller can only start the established preview handler'); + + const nonOwnerForm = { dataset: {}, elements: { namedItem: () => ({ value: '', disabled: false }) } }; + const noOwnerShortcut = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(nonOwnerForm, 'published', () => { previewCalls += 1; }); + assert.equal(noOwnerShortcut.started, false); + assert.equal(nonOwnerForm.elements.namedItem().value, ''); +}); + +test('SCN-console-attention-shortcut-drafts: missing forms, active previews, and opposite drafts remain unchanged', () => { + const missing = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(null, 'rejected'); + assert.equal(missing.started, false); + assert.match(missing.reason, /no longer available/); + + const previewForm = { dataset: { previewToken: 'current-preview' }, + elements: { namedItem: () => ({ value: 'approved', disabled: true }) } }; + const preview = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(previewForm, 'rejected'); + assert.equal(preview.started, false); + assert.equal(previewForm.elements.namedItem().value, 'approved'); + + const draftChoice = { value: 'approved', disabled: false }; + const draftForm = { dataset: {}, elements: { namedItem: () => draftChoice } }; + const draft = globalThis.TorchAttentionActions.prepareOwnerDecisionShortcut(draftForm, 'rejected'); + assert.equal(draft.started, false); + assert.equal(draftChoice.value, 'approved'); + assert.match(draft.reason, /draft is preserved/); +}); + +test('SCN-console-attention-refresh-draft: an edited owner decision remains protected while newer evidence renders', () => { + const choice = { value: '', checked: false, tagName: 'SELECT', selectedOptions: [] }; + const note = { value: '', checked: false, tagName: 'TEXTAREA' }; + const fields = [choice, note]; + const form = { + contains: () => false, + matches: () => false, + querySelector: () => null, + querySelectorAll: () => fields, + }; + const document = { activeElement: null, querySelectorAll: () => fields }; + const protection = globalThis.TorchLiveRefresh.createProtection(document); + protection.remember(); + choice.value = 'rejected'; + note.value = 'Need the exact impact first.'; + + assert.equal(protection.protects(form), true); + const dirty = protection.dirty(); + protection.remember({ preserve: dirty }); + assert.equal(protection.protects(form), true); + assert.equal(choice.value, 'rejected'); + assert.equal(note.value, 'Need the exact impact first.'); +}); + test('SCN-console-attention-dedup: doctor and snapshot message counts become one honest queue item', () => { const groups = project({ messages: { unacknowledged: 4 }, diff --git a/test/fleet/product-surface.test.mjs b/test/fleet/product-surface.test.mjs index 78cb14a..eb37b42 100644 --- a/test/fleet/product-surface.test.mjs +++ b/test/fleet/product-surface.test.mjs @@ -30,6 +30,7 @@ test('SCN-product-site / SCN-dashboard-demo / SCN-torch-brand-surface: public si const html = readFileSync(new URL('../../site/index.html', import.meta.url), 'utf8'); const demoHtml = readFileSync(new URL('../../site/demo.html', import.meta.url), 'utf8'); const consoleHtml = readFileSync(new URL('../../site/console.html', import.meta.url), 'utf8'); + const consoleScript = readFileSync(new URL('../../site/console.js', import.meta.url), 'utf8'); const styles = readFileSync(new URL('../../site/styles.css', import.meta.url), 'utf8'); const torchMark = readFileSync(new URL('../../site/torch-mark.svg', import.meta.url), 'utf8'); assert.match(html, /TORCH — an AI fleet shaped by your project<\/title>/); @@ -70,6 +71,9 @@ test('SCN-product-site / SCN-dashboard-demo / SCN-torch-brand-surface: public si assert.match(consoleHtml, /console-rail/); assert.match(consoleHtml, /id="backlog-board"/); assert.match(consoleHtml, /id="attention-list"/); + assert.match(consoleHtml, /src="\/attention-actions\.js" defer/); + assert.match(consoleScript, /data-attention-decision/); + assert.match(consoleScript, /data-approval-preview/); assert.match(consoleHtml, /id="artifact-list"/); assert.match(consoleHtml, /id="organization-map"/); assert.match(consoleHtml, /id="organization-lifecycle"/); @@ -93,7 +97,6 @@ test('SCN-product-site / SCN-dashboard-demo / SCN-torch-brand-surface: public si assert.match(consoleHtml, /Create a task proposal/); assert.match(consoleHtml, /Session Manager still owns triage and assignment/); assert.match(consoleHtml, /gates remain separate/); - const consoleScript = readFileSync(new URL('../../site/console.js', import.meta.url), 'utf8'); assert.match(consoleScript, /artifact-feedback/); assert.match(consoleScript, /feedback\/preview/); assert.match(consoleScript, /data-feedback-confirm/);