agents/openai.yaml
interface: display_name: "Codebase Map" short_description: "Evidence-linked current and proposed HTML flowcharts"
gazohhh/workflow-axa · GitHub
Create an evidence-linked local flowchart or codebase map when the user requests one, or workflow requests its visual approval report. Use the bundled flowchart design guide for the approved editorial diagrams; inspect current code and preserve tasks/evidence. Visualization only: never implement, approve work or launch an architecture-refactoring workflow.
프로젝트 폴더에서 아래 명령어를 실행하고, 설치할 에이전트를 선택하세요.
npx skills add gazohhh/workflow-axa --skill codebase-map설치 명령을 직접 실행해야 적용됩니다. 지원 에이전트와 필요한 권한·라이선스는 제작자의 안내를 확인하세요.
agents/openai.yamlinterface: display_name: "Codebase Map" short_description: "Evidence-linked current and proposed HTML flowcharts"
assets/report-extra.js
// Highlight the contents entry for the section being read.
(function () {
const links = [...document.querySelectorAll('.toc__link')];
const targets = links.map(a => document.getElementById(a.getAttribute('href').slice(1))).filter(Boolean);
if (!targets.length) return;
function update() {
const line = Math.min(window.innerHeight * 0.3, 240);
let current = targets[0];
for (const t of targets) { if (t.getBoundingClientRect().top - line <= 0) current = t; }
if (window.innerHeight + window.scrollY >= document.documentElement.scrollHeight - 2) current = targets[targets.length - 1];
for (const a of links) {
const on = a.getAttribute('href') === '#' + current.id;
a.setAttribute('aria-current', on ? 'true' : 'false');
if (on && a.closest('.toc').scrollWidth > a.closest('.toc').clientWidth) {
const nav = a.closest('.toc');
const r = a.getBoundingClientRect(), n = nav.getBoundingClientRect();
if (r.left < n.left || r.right > n.right) nav.scrollLeft += r.left - n.left - 24;
}
}
}
let queued = false;
window.addEventListener('scroll', () => { if (!queued) { queued = true; requestAnimationFrame(() => { queued = false; update(); }); } }, {passive:true});
window.addEventListener('resize', update);
update();
})();
// Start diagrams on "Fit" when the whole board fits at a readable size.
(function () {
const canvas = document.querySelector('.panel .canvas');
const fit = document.getElementById('zoom-fit');
if (!canvas || !fit || location.hash) return;
const visible = [...document.querySelectorAll('.canvas')].find(c => c.clientWidth);
if (!visible) return;
const board = parseFloat(getComputedStyle(visible).getPropertyValue('--board-width')) || 0;
if (board && visible.clientWidth < board && visible.clientWidth / board >= 0.6) {
fit.checked = true;
fit.dispatchEvent(new Event('change'));
}
})();
assets/report-style-compat.css/* Older token names used by ui-review.css, mapped onto report-style.css tokens. */
:root {
--paper: var(--color-bg);
--ink: var(--color-ink);
--muted: var(--color-muted);
--rule: var(--color-line);
--accent: var(--color-accent);
--accent-tint: var(--color-accent-soft);
--sans: var(--font-sans);
--serif: var(--font-serif);
--mono: var(--font-mono);
}
assets/report-style.css/* ==========================================================================
Review report — editorial, white, lean
========================================================================== */
/* Tokens
========================================================================== */
:root {
--color-bg: #fff;
--color-ink: #1f2330;
--color-muted: #545d6e; /* 6.6:1 on white */
--color-faint: #6b7280; /* 4.8:1 on white, minimum for small text */
--color-line: #e4e6ea;
--color-line-strong: #1f2330;
--color-wash: #f7f8f9;
--color-accent: #b5401c; /* 5.4:1 on white */
--color-accent-soft: #fbf1ec;
--color-accent-line: #efd2c6;
--color-ok: #2b6535;
--color-ok-soft: #e8f2ea;
--color-ok-line: #cfe0d2;
--font-sans: 'Geist', 'Inter', 'Segoe UI', system-ui, sans-serif;
--font-serif: 'Instrument Serif', Georgia, 'Times New Roman', serif;
--font-mono: 'Geist Mono', ui-monospace, 'Cascadia Mono', 'SF Mono', Menlo, monospace;
--measure: 70ch;
--radius: 6px;
--toc-width: 200px;
--gap: 48px;
--sticky-offset: 20px;
color-scheme: light;
}
/* Base
========================================================================== */
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
scroll-padding-top: var(--sticky-offset);
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background: var(--color-bg);
color: var(--color-ink);
font: 15px/1.6 var(--font-sans);
-webkit-font-smoothing: antialiased;
}
a {
color: inherit;
text-decoration-color: var(--color-line);
text-underline-offset: 3px;
}
a:hover {
color: var(--color-accent);
text-decoration-color: currentColor;
}
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
border-radius: 2px;
}
strong {
font-weight: 600;
}
code,
.code {
font: 12.5px/1.6 var(--font-mono);
overflow-wrap: anywhere;
}
.muted,
small {
color: var(--color-muted);
}
.visually-hidden,
.sr-radio {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
.skip-link {
position: absolute;
top: 8px;
left: 8px;
z-index: 20;
padding: 10px 14px;
background: var(--color-ink);
color: #fff;
border-radius: var(--radius);
transform: translateY(-200%);
}
.skip-link:focus {
transform: none;
color: #fff;
}
/* Page frame
========================================================================== */
.page {
max-width: 1180px;
margin: 0 auto;
padding: 0 40px 48px;
}
.masthead {
display: flex;
justify-content: space-between;
gap: 20px;
padding: 24px 0 12px;
border-bottom: 1px solid var(--color-line);
font: 11px/1.5 var(--font-mono);
letter-spacing: .04em;
color: var(--color-faint);
}
.masthead strong {
font-weight: 500;
color: var(--color-muted);
}
/* Report header
========================================================================== */
.report-header {
padding: 28px 0 32px;
}
.report-title {
max-width: 26ch;
margin: 0 0 10px;
font: 400 clamp(32px, 3.6vw, 46px)/1.08 var(--font-serif);
letter-spacing: -.02em;
text-wrap: balance;
}
.report-lead {
max-width: var(--measure);
margin: 0;
font-size: 16px;
color: var(--color-muted);
}
.status {
display: flex;
align-items: baseline;
gap: 10px;
max-width: var(--measure);
margin: 18px 0 0;
padding-left: 12px;
border-left: 2px solid var(--color-accent);
font-size: 14px;
}
.status--approved {
border-left-color: var(--color-ok);
}
.status__tag {
flex: none;
font: 500 11px/1.6 var(--font-mono);
letter-spacing: .04em;
color: var(--color-accent);
}
.status--approved .status__tag {
color: var(--color-ok);
}
/* At-a-glance facts */
.facts {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
margin: 28px 0 0;
border-top: 1px solid var(--color-line);
border-bottom: 1px solid var(--color-line);
}
.facts__item {
min-width: 0;
padding: 14px 18px 14px 0;
}
.facts__item + .facts__item {
padding-left: 18px;
border-left: 1px solid var(--color-line);
}
.facts dt {
font: 10.5px/1.5 var(--font-mono);
letter-spacing: .06em;
text-transform: uppercase;
color: var(--color-faint);
}
.facts dd {
margin: 4px 0 0;
font-size: 15px;
font-weight: 500;
line-height: 1.35;
}
.facts dd a {
text-decoration: none;
}
.facts__value--attention {
color: var(--color-accent);
}
/* Layout: sticky contents beside the report
========================================================================== */
.layout {
display: grid;
grid-template-columns: var(--toc-width) minmax(0, 1fr);
gap: var(--gap);
align-items: start;
}
.content {
min-width: 0;
overflow-wrap: anywhere;
}
.toc {
position: sticky;
top: 0;
max-height: 100vh;
overflow: auto;
padding: 28px 0;
}
.toc__title {
margin: 0 0 8px;
font: 10.5px/1.5 var(--font-mono);
letter-spacing: .06em;
text-transform: uppercase;
color: var(--color-faint);
}
.toc__list {
margin: 0;
padding: 0;
list-style: none;
counter-reset: toc;
font-size: 13px;
}
.toc__list > li {
counter-increment: toc;
}
.toc__link {
display: grid;
grid-template-columns: 24px 1fr;
padding: 5px 0;
color: var(--color-muted);
line-height: 1.4;
text-decoration: none;
}
.toc__link::before {
content: counter(toc, decimal-leading-zero);
font: 11px/1.75 var(--font-mono);
color: var(--color-faint);
}
.toc__link:hover {
color: var(--color-ink);
}
.toc__link[aria-current='true'] {
color: var(--color-ink);
font-weight: 600;
}
.toc__link[aria-current='true']::before {
color: var(--color-accent);
}
.toc__flag {
display: block;
grid-column: 2;
font-size: 11.5px;
font-weight: 400;
color: var(--color-accent);
}
.toc__extra {
display: block;
margin-top: 14px;
padding: 5px 0 5px 20px;
font-size: 13px;
color: var(--color-faint);
}
/* Sections
========================================================================== */
.content {
counter-reset: section;
}
.section {
padding-top: 32px;
counter-increment: section;
}
.section + .section {
margin-top: 40px;
border-top: 1px solid var(--color-line);
}
.section__title {
display: flex;
align-items: baseline;
gap: 12px;
margin: 0 0 8px;
font: 400 30px/1.15 var(--font-serif);
letter-spacing: -.01em;
}
.section__title::before {
content: counter(section, decimal-leading-zero);
font: 11px var(--font-mono);
color: var(--color-faint);
}
.section__summary {
max-width: var(--measure);
margin: 0 0 20px;
font-size: 16px;
color: var(--color-muted);
}
.section h3,
.subhead {
margin: 0 0 6px;
font-size: 14px;
font-weight: 600;
}
.section p {
max-width: var(--measure);
margin: 0 0 10px;
}
.block + .block {
margin-top: 24px;
}
.columns {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 32px;
margin-top: 24px;
}
.columns > * {
min-width: 0;
padding-top: 14px;
border-top: 1px solid var(--color-line-strong);
}
.columns p:last-child {
margin-bottom: 0;
}
.list {
margin: 0;
padding-left: 18px;
}
.list > li {
margin: 3px 0;
padding-left: 2px;
}
.list > li::marker {
color: var(--color-faint);
}
.list--check,
.list--keep {
padding-left: 0;
list-style: none;
}
.list--check > li,
.list--keep > li {
position: relative;
padding: 7px 0 7px 26px;
border-bottom: 1px solid var(--color-line);
}
.list--check > li:last-child,
.list--keep > li:last-child {
border-bottom: 0;
}
.list--check > li::before,
.list--keep > li::before {
position: absolute;
top: 10px;
left: 0;
display: grid;
place-items: center;
width: 16px;
height: 16px;
border-radius: 50%;
font: 600 10px/1 var(--font-sans);
}
.list--check > li::before {
content: '✓';
background: var(--color-ok-soft);
color: var(--color-ok);
}
.list--keep > li::before {
content: '=';
background: var(--color-wash);
color: var(--color-muted);
box-shadow: inset 0 0 0 1px var(--color-line);
}
.note {
max-width: var(--measure);
margin: 20px 0 0;
padding: 10px 14px;
background: var(--color-wash);
border-radius: var(--radius);
font-size: 14px;
color: var(--color-muted);
}
.note a {
color: var(--color-accent);
}
/* Diagrams
========================================================================== */
.review {
position: relative;
}
.viewbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
flex-wrap: wrap;
border-bottom: 1px solid var(--color-line);
}
.view-tabs,
.zoom-tabs {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.zoom-tabs {
gap: 14px;
}
.control-name {
display: none;
}
.view-tabs label,
.zoom-tabs label {
margin-bottom: -1px;
border-bottom: 2px solid transparent;
color: var(--color-muted);
font-size: 13px;
line-height: 44px;
cursor: pointer;
user-select: none;
}
.view-tabs label {
font-weight: 500;
}
.zoom-tabs label {
font: 11px/44px var(--font-mono);
}
.view-tabs label:hover,
.zoom-tabs label:hover {
color: var(--color-ink);
}
.figures {
--scale: 1;
}
#zoom-125:checked ~ .figures {
--scale: 1.25;
}
#zoom-150:checked ~ .figures {
--scale: 1.5;
}
#zoom-fit:checked ~ .figures .map {
width: 100%;
min-width: 0;
}
.panel {
display: none;
margin: 0;
}
.figure-head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 20px;
padding: 18px 0 2px;
}
.figure-head h3 {
margin: 0;
}
.figure-head span {
font: 11px var(--font-mono);
color: var(--color-faint);
white-space: nowrap;
}
.figure-description {
margin: 2px 0 10px;
font-size: 13px;
color: var(--color-muted);
}
.hint {
margin: 0 0 8px;
font-size: 12px;
color: var(--color-faint);
}
.canvas {
max-width: 100%;
overflow-x: auto;
overflow-y: hidden;
background: var(--color-wash);
border-radius: var(--radius);
scrollbar-width: thin;
scrollbar-color: var(--color-line) transparent;
overscroll-behavior-x: contain;
cursor: grab;
}
.canvas.dragging {
cursor: grabbing;
user-select: none;
}
.map {
display: block;
width: calc(var(--board-width) * var(--scale));
min-width: calc(var(--board-width) * var(--scale));
height: auto;
}
.node-shape {
fill: #fff;
stroke: var(--color-ink);
stroke-width: 1.1;
}
.start .node-shape,
.result .node-shape {
fill: #eef0f3;
stroke: var(--color-muted);
}
.focal .node-shape {
fill: var(--color-accent-soft);
stroke: var(--color-accent);
stroke-width: 1.3;
}
.node.inferred .node-shape,
.node.unknown .node-shape,
.connector.inferred,
.connector.unknown {
stroke-dasharray: 5 4;
}
.connector {
fill: none;
stroke: var(--color-faint);
stroke-width: 1.3;
}
.map marker path {
fill: var(--color-faint);
}
.node-title {
font: 600 18px var(--font-sans);
fill: var(--color-ink);
}
.node-note {
font: 13px var(--font-sans);
fill: var(--color-muted);
}
.node-item {
font: 15px var(--font-sans);
fill: var(--color-ink);
}
.arrow-label rect {
fill: var(--color-wash);
}
.edge-label {
font: 10px var(--font-mono);
letter-spacing: 1px;
fill: var(--color-muted);
}
figcaption {
margin: 0 0 4px;
font-size: 12px;
color: var(--color-faint);
}
.legend {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 22px;
padding: 10px 0 4px;
font-size: 12px;
color: var(--color-muted);
}
.legend span {
display: flex;
align-items: center;
gap: 8px;
}
.legend i {
display: inline-block;
width: 20px;
height: 12px;
border: 1px solid var(--color-muted);
background: #fff;
}
.legend .legend-start {
border-radius: 8px;
background: #eef0f3;
}
.legend .legend-action {
border-radius: 2px;
}
.legend .legend-decision {
width: 11px;
height: 11px;
margin: 0 4px;
transform: rotate(45deg);
}
.legend .legend-focus {
border-color: var(--color-accent);
border-radius: 2px;
background: var(--color-accent-soft);
}
/* Tables
========================================================================== */
.table-scroll {
max-width: 100%;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
text-align: left;
}
caption {
padding: 0 0 8px;
font-size: 13px;
color: var(--color-muted);
text-align: left;
}
th {
padding: 8px 16px 8px 0;
border-bottom: 1px solid var(--color-line-strong);
font: 500 10.5px/1.5 var(--font-mono);
letter-spacing: .06em;
text-transform: uppercase;
color: var(--color-faint);
vertical-align: bottom;
}
td {
padding: 11px 16px 11px 0;
border-bottom: 1px solid var(--color-line);
vertical-align: top;
}
td:last-child,
th:last-child {
padding-right: 0;
}
.compare th[scope='row'] {
width: 32%;
font: 500 14px/1.5 var(--font-sans);
letter-spacing: 0;
text-transform: none;
color: var(--color-ink);
border-bottom-color: var(--color-line);
vertical-align: top;
}
.compare td:nth-child(2) {
color: var(--color-muted);
}
.compare .after {
font-weight: 500;
}
.tag {
display: inline-block;
margin-left: 6px;
padding: 1px 7px;
border: 1px solid var(--color-line);
border-radius: 999px;
font-size: 11.5px;
font-weight: 500;
color: var(--color-muted);
white-space: nowrap;
}
.tag--changed {
border-color: var(--color-accent-line);
background: var(--color-accent-soft);
color: var(--color-accent);
}
.tag--recommended {
border-color: var(--color-ok-line);
background: var(--color-ok-soft);
color: var(--color-ok);
}
/* Work order
========================================================================== */
.steps {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 8px 0 0;
padding: 0;
list-style: none;
}
.steps > li {
display: flex;
align-items: center;
gap: 8px;
}
.steps > li + li::before {
content: '';
width: 18px;
height: 1px;
background: var(--color-faint);
}
.steps a {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 36px;
padding: 6px 12px;
border: 1px solid var(--color-line);
border-radius: var(--radius);
font-size: 13px;
text-decoration: none;
}
.steps a:hover {
border-color: var(--color-ink);
color: var(--color-ink);
}
.steps .key {
font: 500 12px var(--font-mono);
}
/* Tickets
========================================================================== */
.tickets {
display: grid;
gap: 14px;
margin-top: 20px;
}
.ticket {
--stub: 124px;
position: relative;
display: grid;
grid-template-columns: var(--stub) minmax(0, 1fr);
max-width: 880px;
margin: 0;
border: 1px solid var(--color-line);
border-radius: var(--radius);
background: #fff;
box-shadow: 0 1px 2px rgb(31 35 48 / 4%);
transition: border-color .15s, box-shadow .15s;
}
.ticket:hover {
border-color: #d3d6dc;
box-shadow: 0 2px 8px rgb(31 35 48 / 6%);
}
.ticket:target {
border-color: var(--color-accent);
}
.ticket::before,
.ticket::after {
content: '';
position: absolute;
left: calc(var(--stub) - 7px);
width: 14px;
height: 14px;
border: 1px solid var(--color-line);
border-radius: 50%;
background: var(--color-bg);
}
.ticket::before {
top: -8px;
clip-path: inset(8px 0 0 0);
}
.ticket::after {
bottom: -8px;
clip-path: inset(0 0 8px 0);
}
.ticket__stub {
grid-row: 1 / span 2;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 10px;
padding: 18px 16px;
border-right: 1.5px dashed #d3d6dc;
border-radius: var(--radius) 0 0 var(--radius);
background: var(--color-wash);
font: 10.5px/1.4 var(--font-mono);
letter-spacing: .08em;
text-transform: uppercase;
color: var(--color-faint);
}
.ticket__key {
display: block;
margin-top: 4px;
font: 500 24px/1 var(--font-mono);
letter-spacing: -.02em;
text-transform: none;
color: var(--color-ink);
}
.ticket__status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border: 1px solid var(--color-line);
border-radius: 999px;
background: #fff;
font: 500 11px/1 var(--font-sans);
letter-spacing: 0;
text-transform: none;
color: var(--color-muted);
white-space: nowrap;
}
.ticket__status::before {
content: '';
width: 6px;
height: 6px;
border: 1.5px solid currentColor;
border-radius: 50%;
}
.ticket__status--in-progress,
.ticket__status--blocked {
border-color: var(--color-accent-line);
color: var(--color-accent);
}
.ticket__status--in-progress::before {
border-right-color: transparent;
}
.ticket__status--blocked::before {
border-radius: 1px;
background: currentColor;
}
.ticket__status--done {
border-color: var(--color-ok-line);
color: var(--color-ok);
}
.ticket__status--done::before {
background: currentColor;
}
.ticket__main {
padding: 16px 20px 0;
}
.ticket__title {
margin: 0;
font-size: 15px;
font-weight: 600;
line-height: 1.4;
}
.ticket__main p {
margin: 4px 0 0;
font-size: 14px;
color: var(--color-muted);
}
.ticket__facts {
display: grid;
grid-template-columns: 120px minmax(0, 1fr);
gap: 8px 16px;
margin: 14px 0 0;
padding: 12px 20px 16px;
border-top: 1px solid var(--color-line);
font-size: 13px;
}
.ticket__facts dt {
font: 10.5px/2 var(--font-mono);
letter-spacing: .06em;
text-transform: uppercase;
color: var(--color-faint);
}
.ticket__facts dd {
margin: 0;
}
.ticket__facts .list > li {
margin: 0 0 2px;
}
.ticket-ref {
display: inline-block;
margin-right: 4px;
padding: 1px 7px;
border: 1px solid var(--color-line);
border-radius: 4px;
background: var(--color-wash);
font: 500 12px/1.6 var(--font-mono);
text-decoration: none;
}
.ticket-ref:hover {
border-color: var(--color-accent);
}
/* Checks
========================================================================== */
.progress {
display: flex;
align-items: center;
gap: 14px;
max-width: 880px;
margin: 18px 0 0;
font-size: 13px;
color: var(--color-muted);
}
.progress__bar {
display: flex;
flex: 1;
gap: 3px;
height: 6px;
}
.progress__bar span {
flex: 1;
border-radius: 2px;
background: var(--color-line);
}
.progress__bar .is-verified {
background: var(--color-ok);
}
.progress__bar .is-failed {
background: var(--color-accent);
}
.checks {
max-width: 880px;
margin: 10px 0 0;
padding: 0;
list-style: none;
border-top: 1px solid var(--color-line-strong);
}
.check {
display: grid;
grid-template-columns: 40px minmax(0, 1fr) auto;
column-gap: 12px;
padding: 12px 0;
border-bottom: 1px solid var(--color-line);
font-size: 14px;
}
.check__key {
grid-row: 1 / span 2;
font: 11px/1.9 var(--font-mono);
color: var(--color-faint);
}
.check__expect {
margin: 0;
font-weight: 500;
}
.check__how {
grid-column: 2;
margin: 2px 0 0;
font-size: 13px;
color: var(--color-muted);
}
.check .state {
grid-column: 3;
grid-row: 1;
}
.state {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--color-faint);
white-space: nowrap;
}
.state::before {
content: '';
width: 6px;
height: 6px;
border: 1.5px solid currentColor;
border-radius: 50%;
}
.state--verified {
color: var(--color-ok);
}
.state--verified::before,
.state--failed::before {
background: currentColor;
}
.state--failed,
.state--partial {
color: var(--color-accent);
}
/* Risks and open questions
========================================================================== */
.risks {
max-width: var(--measure);
margin: 0;
padding: 0;
list-style: none;
}
.risks > li {
position: relative;
padding: 8px 0 8px 22px;
border-bottom: 1px solid var(--color-line);
}
.risks > li:last-child {
border-bottom: 0;
}
.risks > li::before {
content: '';
position: absolute;
top: 15px;
left: 2px;
width: 9px;
height: 9px;
border: 1.5px solid var(--color-accent);
transform: rotate(45deg);
}
.questions {
max-width: 880px;
margin: 28px 0 0;
padding: 18px 20px 6px;
border: 1px solid var(--color-accent-line);
border-radius: var(--radius);
background: #fff;
}
.questions__head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
flex-wrap: wrap;
}
.questions__head h3 {
margin: 0;
color: var(--color-accent);
}
.questions__head p {
margin: 0;
font-size: 13px;
color: var(--color-muted);
}
.question-list {
margin: 12px 0 0;
padding: 0;
list-style: none;
}
.question {
padding: 14px 0;
border-top: 1px solid var(--color-line);
}
.question__title {
display: flex;
gap: 10px;
margin: 0 0 8px;
font-weight: 600;
}
.question__key {
font: 500 12px/1.9 var(--font-mono);
color: var(--color-accent);
}
.options {
margin: 0;
padding: 0 0 0 30px;
list-style: none;
counter-reset: option;
}
.options > li {
position: relative;
padding: 4px 0 4px 24px;
font-size: 14px;
color: var(--color-muted);
counter-increment: option;
}
.options > li::before {
content: counter(option, lower-alpha);
position: absolute;
top: 6px;
left: 0;
display: grid;
place-items: center;
width: 16px;
height: 16px;
border: 1px solid var(--color-line);
border-radius: 50%;
font: 500 10px/1 var(--font-mono);
}
.options > .is-recommended {
color: var(--color-ink);
}
.options > .is-recommended::before {
border-color: var(--color-ok);
background: var(--color-ok);
color: #fff;
}
/* Technical details (appendix)
========================================================================== */
details {
padding: 12px 0;
border-top: 1px solid var(--color-line);
}
summary {
display: flex;
align-items: center;
gap: 10px;
min-height: 32px;
font-size: 14px;
font-weight: 500;
list-style: none;
cursor: pointer;
}
summary::-webkit-details-marker {
display: none;
}
summary::before {
content: '';
flex: none;
width: 6px;
height: 6px;
border-right: 1.5px solid var(--color-faint);
border-bottom: 1.5px solid var(--color-faint);
transform: rotate(-45deg);
transition: transform .15s;
}
details[open] > summary::before {
transform: rotate(45deg);
}
details.appendix {
margin-top: 48px;
padding-top: 18px;
border-top: 1px solid var(--color-line-strong);
}
details.appendix > summary {
font: 400 24px/1.2 var(--font-serif);
}
.appendix-body {
padding-top: 14px;
font-size: 14px;
}
.appendix-body section {
margin-bottom: 28px;
}
.appendix-body h2 {
margin: 0 0 6px;
font-size: 14px;
font-weight: 600;
}
.appendix-body > p.muted {
max-width: var(--measure);
font-size: 13px;
}
.appendix .table-scroll table {
min-width: 820px;
font-size: 12px;
}
.appendix ol {
padding-left: 20px;
font-size: 12px;
}
.appendix ol li {
margin: 8px 0;
}
.appendix blockquote {
margin: 10px 0;
padding-left: 14px;
border-left: 2px solid var(--color-line);
color: var(--color-muted);
}
.appendix .two-col {
display: grid;
grid-template-columns: 1fr 1.2fr;
gap: 40px;
margin: 24px 0;
}
.appendix .plain {
margin: 0;
padding: 0;
list-style: none;
}
.appendix .plain li {
padding: 8px 0;
border-bottom: 1px solid var(--color-line);
}
.evidence {
padding: 14px 0;
border-top: 1px solid var(--color-line);
}
.evidence p,
.appendix code {
font: 12px/1.7 var(--font-mono);
overflow-wrap: anywhere;
}
/* Footer
========================================================================== */
.report-footer {
margin-top: 32px;
padding-top: 12px;
border-top: 1px solid var(--color-line);
font: 11px/1.7 var(--font-mono);
color: var(--color-faint);
}
.report-footer p {
margin: 0;
}
/* Responsive
========================================================================== */
@media (max-width: 1100px) {
.facts {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.facts__item:nth-child(4) {
padding-left: 0;
border-left: 0;
}
.facts__item:nth-child(n + 4) {
border-top: 1px solid var(--color-line);
}
}
@media (max-width: 960px) {
:root {
--sticky-offset: 64px;
}
.page {
padding: 0 24px 40px;
}
.layout {
display: block;
}
.toc {
z-index: 5;
max-height: none;
margin: 0 -24px;
padding: 0;
border-bottom: 1px solid var(--color-line);
background: var(--color-bg);
overflow-x: auto;
scrollbar-width: none;
}
.toc__title,
.toc__flag,
.toc__extra {
display: none;
}
.toc__list {
display: flex;
gap: 20px;
padding: 0 24px;
white-space: nowrap;
}
.toc__link {
display: block;
padding: 14px 0 12px; /* 48px touch target */
border-bottom: 2px solid transparent;
}
.toc__link::before {
margin-right: 6px;
}
.toc__link[aria-current='true'] {
border-bottom-color: var(--color-ink);
}
}
@media (max-width: 640px) {
.page {
padding: 0 16px 32px;
}
.toc {
margin: 0 -16px;
}
.toc__list {
padding: 0 16px;
}
.report-title {
font-size: 32px;
}
.facts {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.facts__item,
.facts__item + .facts__item {
padding: 12px 12px 12px 0;
border-left: 0;
}
.facts__item:nth-child(n + 3) {
border-top: 1px solid var(--color-line);
}
.section__title {
font-size: 25px;
}
.columns {
grid-template-columns: 1fr;
gap: 20px;
}
.figure-head,
.viewbar {
display: block;
}
.zoom-tabs {
border-top: 1px solid var(--color-line);
}
.compare {
min-width: 560px;
}
.ticket {
grid-template-columns: 1fr;
}
.ticket::before,
.ticket::after {
display: none;
}
.ticket__stub {
grid-row: auto;
flex-direction: row;
align-items: center;
flex-wrap: wrap;
padding: 12px 16px;
border-right: 0;
border-bottom: 1.5px dashed #d3d6dc;
border-radius: var(--radius) var(--radius) 0 0;
}
.ticket__key {
margin: 0;
font-size: 18px;
}
.ticket__status {
margin-left: auto;
}
.ticket__main {
padding: 14px 16px 0;
}
.ticket__facts {
grid-template-columns: 1fr;
gap: 0;
padding: 10px 16px 14px;
}
.ticket__facts dd {
margin-bottom: 8px;
}
.check {
grid-template-columns: 32px minmax(0, 1fr);
}
.check .state {
grid-column: 2;
grid-row: auto;
margin-top: 6px;
}
.questions {
padding: 16px 16px 4px;
}
.options {
padding-left: 0;
}
.appendix .two-col {
grid-template-columns: 1fr;
gap: 16px;
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
*,
*::before,
*::after {
transition: none !important;
}
}
@media print {
.page {
max-width: none;
padding: 0;
}
.layout {
display: block;
}
.toc,
.skip-link,
.viewbar,
.hint {
display: none !important;
}
.panel {
display: block !important;
break-inside: avoid;
}
.canvas {
overflow: visible;
}
.map {
width: 100% !important;
min-width: 0 !important;
}
.ticket,
.check,
.question,
.facts {
break-inside: avoid;
}
a {
color: inherit;
text-decoration: none;
}
}
/* Overrides for paragraphs inside components
========================================================================== */
.section .check__expect,
.section .question__title {
margin: 0;
}
.section .question__title {
margin-bottom: 8px;
}
.section .check__how {
margin: 2px 0 0;
}
.compare th[scope='row'],
.compare td {
padding-top: 12px;
padding-bottom: 12px;
}
.compare small {
display: block;
margin-top: 2px;
font-size: 12px;
}
.section h3 + .risks,
.section h3 + .steps {
margin-top: 6px;
}
.section .risks + .questions,
.section p + h3,
.section .section__summary + h3 {
margin-top: 24px;
}
.section .ticket__stub {
margin: 0;
}
@media (max-width: 640px) {
.steps {
flex-direction: column;
gap: 6px;
}
.steps > li + li::before {
display: none;
}
}
/* Diagram view and zoom state (generated per report)
========================================================================== */
#view-after:checked ~ .figures #panel-after {
display: block;
}
#view-after:checked ~ .viewbar label[for='view-after'] {
border-bottom-color: var(--color-accent);
color: var(--color-ink);
}
#view-after:focus-visible ~ .viewbar label[for='view-after'] {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
}
#view-today:checked ~ .figures #panel-today {
display: block;
}
#view-today:checked ~ .viewbar label[for='view-today'] {
border-bottom-color: var(--color-accent);
color: var(--color-ink);
}
#view-today:focus-visible ~ .viewbar label[for='view-today'] {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
}
#view-order:checked ~ .figures #panel-order {
display: block;
}
#view-order:checked ~ .viewbar label[for='view-order'] {
border-bottom-color: var(--color-accent);
color: var(--color-ink);
}
#view-order:focus-visible ~ .viewbar label[for='view-order'] {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
}
#zoom-100:checked ~ .viewbar label[for='zoom-100'] {
border-bottom-color: var(--color-ink);
color: var(--color-ink);
}
#zoom-100:focus-visible ~ .viewbar label[for='zoom-100'] {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
}
#zoom-125:checked ~ .viewbar label[for='zoom-125'] {
border-bottom-color: var(--color-ink);
color: var(--color-ink);
}
#zoom-125:focus-visible ~ .viewbar label[for='zoom-125'] {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
}
#zoom-150:checked ~ .viewbar label[for='zoom-150'] {
border-bottom-color: var(--color-ink);
color: var(--color-ink);
}
#zoom-150:focus-visible ~ .viewbar label[for='zoom-150'] {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
}
#zoom-fit:checked ~ .viewbar label[for='zoom-fit'] {
border-bottom-color: var(--color-ink);
color: var(--color-ink);
}
#zoom-fit:focus-visible ~ .viewbar label[for='zoom-fit'] {
outline: 2px solid var(--color-accent);
outline-offset: 3px;
}
assets/report.css:root {
--paper:#f5f5f5; --ink:#2d3142; --muted:#4f5d75; --rule:#cdd0d5;
--accent:#bf4520; --accent-tint:#f5e9e4;
--sans:'Geist','Inter','Segoe UI',system-ui,sans-serif;
--serif:'Instrument Serif',Georgia,'Times New Roman',serif;
--mono:'Geist Mono',ui-monospace,'Cascadia Mono',monospace;
color-scheme:light;
}
* { box-sizing:border-box; }
body { margin:0; background:var(--paper); color:var(--ink); font:15px/1.6 var(--sans); }
main { max-width:1392px; margin:auto; padding:36px 56px 48px; }
a { color:var(--ink); text-underline-offset:4px; }
a:hover { color:var(--accent); }
a:focus-visible,summary:focus-visible,.canvas:focus-visible { outline:2px solid var(--accent); outline-offset:5px; }
header { padding-bottom:24px; }
.mast { display:flex; justify-content:space-between; gap:20px; border-bottom:1px solid var(--rule); padding-bottom:14px; font:11px/1.5 var(--mono); letter-spacing:.1em; color:var(--muted); }
.mast strong { font-weight:500; color:var(--ink); }
h1 { font:400 clamp(38px,4vw,58px)/1.08 var(--serif); letter-spacing:-.035em; margin:25px 0 14px; max-width:1080px; }
h2 { font-size:20px; line-height:1.4; font-weight:600; margin:0 0 12px; }
.status { font:11px/1.6 var(--mono); letter-spacing:.04em; color:var(--accent); margin:0 0 14px; }
.lead { font-size:16px; color:var(--muted); margin:0; max-width:1040px; }
.muted,small { color:var(--muted); }
.review { position:relative; border-top:1px solid var(--ink); }
.sr-radio { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); clip-path:inset(50%); white-space:nowrap; }
.viewbar { display:flex; justify-content:space-between; align-items:center; gap:16px; border-bottom:1px solid var(--rule); min-height:60px; }
.view-tabs,.zoom-tabs { display:flex; align-items:center; gap:24px; flex-wrap:wrap; }
.view-tabs label,.zoom-tabs label { cursor:pointer; user-select:none; line-height:58px; color:var(--muted); border-bottom:2px solid transparent; font-size:14px; }
.view-tabs label { font-weight:550; }
.zoom-tabs { gap:14px; flex-shrink:0; }
.zoom-tabs label { font:11px/58px var(--mono); }
.figures { --scale:1; }
#zoom-125:checked~.figures { --scale:1.25; }
#zoom-150:checked~.figures { --scale:1.5; }
#zoom-fit:checked~.figures .map { width:100%; min-width:0; }
.panel { display:none; margin:0; }
.figure-head { display:flex; justify-content:space-between; align-items:baseline; gap:24px; padding:22px 0 4px; }
.figure-head h2 { font-size:18px; margin:0; }
.figure-head span { font:10px var(--mono); color:var(--muted); text-transform:uppercase; white-space:nowrap; }
.figure-description { margin:6px 0 12px; font-size:13px; color:var(--muted); max-width:1100px; }
.hint { margin:0 0 4px; font-size:11px; color:var(--muted); }
.canvas { max-width:100%; overflow-x:auto; overflow-y:hidden; scrollbar-width:thin; scrollbar-color:#9198a5 transparent; overscroll-behavior-x:contain; cursor:grab; }
.canvas.dragging { cursor:grabbing; user-select:none; }
.map { display:block; width:calc(var(--board-width) * var(--scale)); min-width:calc(var(--board-width) * var(--scale)); height:auto; }
.node-shape { fill:#fff; stroke:var(--ink); stroke-width:1.2; }
.start .node-shape,.result .node-shape { fill:#edf0f2; stroke:var(--muted); }
.focal .node-shape { fill:var(--accent-tint); stroke:var(--accent); stroke-width:1.4; }
.node.inferred .node-shape,.node.unknown .node-shape { stroke-dasharray:5 4; }
.connector { fill:none; stroke:var(--muted); stroke-width:1.4; }
.connector.inferred,.connector.unknown { stroke-dasharray:5 4; }
.map marker path { fill:var(--muted); }
.node-title { font:600 18px var(--sans); fill:var(--ink); }
.node-note { font:13px var(--sans); fill:var(--muted); }
.node-item { font:15px var(--sans); fill:var(--ink); }
.arrow-label rect { fill:var(--paper); }
.edge-label { font:10px var(--mono); letter-spacing:1px; fill:var(--muted); }
figcaption { font-size:12px; color:var(--muted); margin:12px 0 24px; }
.legend { display:flex; flex-wrap:wrap; align-items:center; gap:16px 32px; border-top:1px solid var(--rule); padding:16px 0 10px; }
.legend span { display:flex; align-items:center; gap:10px; }
.legend i { display:inline-block; width:26px; height:16px; border:1px solid var(--muted); background:#fff; }
.legend-start { border-radius:10px; }
.legend-action,.legend-group { border-radius:3px; }
.legend .legend-decision { width:16px; height:16px; transform:rotate(45deg); margin:0 5px; }
.legend .legend-focus { background:var(--accent-tint); border-color:var(--accent); border-radius:3px; }
.comparison { border-top:1px solid var(--ink); padding-top:24px; margin-top:8px; }
.comparison h2,.boundaries h2 { font:400 30px var(--serif); }
.table-scroll { max-width:100%; overflow-x:auto; }
table { border-collapse:collapse; width:100%; font-size:14px; text-align:left; }
th { font:10px var(--mono); letter-spacing:.08em; text-transform:uppercase; color:var(--muted); padding:14px 18px 12px 0; border-bottom:1px solid var(--rule); vertical-align:bottom; }
td { padding:16px 24px 16px 0; border-bottom:1px solid var(--rule); vertical-align:top; }
td:first-child { width:20%; }
td:last-child { padding-right:0; }
.comparison td:nth-child(2) { width:36%; color:var(--muted); }
small { display:block; font-size:12px; line-height:1.6; margin-top:5px; }
.boundaries { padding-top:24px; }
.boundaries ul { padding-left:20px; font-size:14px; }
.two-col { display:grid; grid-template-columns:1fr 1.2fr; gap:48px; margin:32px 0; }
.plain { list-style:none; padding:0; margin:0; }
.plain li { padding:13px 0; border-bottom:1px solid var(--rule); font-size:14px; }
.approval { border-top:1px solid var(--ink); padding:24px 0; display:grid; grid-template-columns:230px 1fr; gap:32px; }
.approval h2 { font:400 30px var(--serif); }
.approval p { margin:0 0 10px; font-size:14px; }
.warning { padding:18px 0; margin:0 0 20px; border-top:1px solid var(--accent); border-bottom:1px solid var(--rule); }
.warning h2 { color:var(--accent); font-size:17px; }
details { border-top:1px solid var(--rule); padding:17px 0; }
summary { font-size:14px; font-weight:550; cursor:pointer; list-style-position:inside; }
.appendix-body { padding-top:24px; }
.appendix-body section { margin-bottom:30px; }
.appendix table { min-width:840px; font-size:12px; }
.appendix td:first-child { width:auto; }
.appendix ol { font-size:12px; padding-left:22px; }
.appendix ol li { margin:10px 0; }
.evidence { padding:18px 0; border-top:1px solid var(--rule); }
.evidence strong { font-size:14px; }
.evidence p,code { font:12px/1.8 var(--mono); overflow-wrap:anywhere; }
.snapshot p { font-size:12px; }
footer { border-top:1px solid var(--rule); padding-top:14px; margin-top:28px; color:var(--muted); font:10px/1.7 var(--mono); }
li,tr,article { scroll-margin-top:24px; }
@media(max-width:800px) {
main { padding:22px 20px 36px; }
h1 { font-size:39px; }
.mast { font-size:9px; }
.viewbar { display:block; }
.view-tabs { gap:18px; }
.view-tabs label { font-size:12px; line-height:46px; }
.zoom-tabs { justify-content:flex-end; border-top:1px solid var(--rule); }
.zoom-tabs label { line-height:34px; }
.figure-head { display:block; }
.figure-head span { display:block; margin-top:8px; }
.comparison table { min-width:740px; }
.two-col,.approval { grid-template-columns:1fr; gap:20px; }
}
@media print {
main { max-width:none; padding:0; }
.viewbar,.hint { display:none!important; }
.panel { display:block!important; break-inside:avoid; }
.canvas { overflow:visible; }
.map { width:100%!important; min-width:0!important; }
.figure-head { margin-top:25px; }
.approval { break-inside:avoid; }
a { color:inherit; }
}
.task-checkpoint { border-top:1px solid var(--ink); padding:20px 0 24px; }
.task-checkpoint h2 { font:400 28px var(--serif); margin:0 0 6px; }
.checkpoint-asof,.checkpoint-limit,.flow-context { font-size:12px; color:var(--muted); overflow-wrap:anywhere; }
.checkpoint-asof { margin:0 0 18px; }
.checkpoint-axes { display:grid; grid-template-columns:1fr 1fr 1fr; gap:28px; margin:0 0 16px; }
.checkpoint-axes > div { min-width:0; }
.checkpoint-axes dt { font:10px/1.6 var(--mono); text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
.checkpoint-axes dd { margin:5px 0 0; font-size:14px; overflow-wrap:anywhere; }
.checkpoint-axes dd p { margin:5px 0 0; font-size:13px; color:var(--muted); }
.next-action { font-size:14px; border-top:1px solid var(--rule); padding-top:14px; margin:0; overflow-wrap:anywhere; }
.next-action strong { margin-right:12px; }
.checkpoint-limit { margin:12px 0 0; }
.flow-context { margin:0 0 14px; }
.revision-log { margin-bottom:12px; }
.revision-log > p { font-size:12px; color:var(--muted); }
.revision-log table { min-width:740px; }
.revision-log td { overflow-wrap:anywhere; }
@media(max-width:800px) { .checkpoint-axes { grid-template-columns:1fr; gap:16px; } }
@media print { .task-checkpoint { break-inside:avoid; } }
assets/report.js'use strict';
// Native radio inputs and scrolling work without this progressive enhancement.
function revealAnchor() {
const id = location.hash.slice(1);
const target = document.getElementById(id);
if (!target) return;
let parent = target.parentElement;
while (parent) {
if (parent.tagName === 'DETAILS') parent.open = true;
parent = parent.parentElement;
}
target.scrollIntoView({block:target.closest('.canvas') ? 'nearest' : 'start', inline:'center'});
}
for (const anchor of document.querySelectorAll('a[href^="#"]')) {
anchor.addEventListener('click', () => {
const target = document.getElementById(anchor.getAttribute('href').slice(1));
let parent = target;
while (parent) {
if (parent.tagName === 'DETAILS') parent.open = true;
parent = parent.parentElement;
}
});
}
window.addEventListener('hashchange', revealAnchor);
if (location.hash) revealAnchor();
function centerEntry(canvas) {
if (!canvas.clientWidth) return;
const node = document.getElementById(canvas.dataset.startId);
if (!node) return;
const bounds = node.getBoundingClientRect();
const viewport = canvas.getBoundingClientRect();
canvas.scrollLeft += bounds.x + bounds.width/2 - viewport.x - viewport.width/2;
}
function centerVisibleEntries() {
for (const canvas of document.querySelectorAll('.canvas')) centerEntry(canvas);
}
for (const input of document.querySelectorAll('.review input[type="radio"]')) {
input.addEventListener('change', centerVisibleEntries);
}
const resizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(entries => {
for (const entry of entries) centerEntry(entry.target);
}) : null;
for (const canvas of document.querySelectorAll('.canvas')) {
// On narrow screens show the entry node, not the diagram's empty left margin.
resizeObserver?.observe(canvas);
let drag = null;
canvas.addEventListener('keydown', event => {
const choices = ['100', '125', '150'];
const selected = document.querySelector('input[name="diagram-zoom"]:checked');
let index = choices.indexOf(selected.id.slice(5));
let next = null;
if (event.key === '+' || event.key === '=') next = choices[Math.min(2, index+1)];
else if (event.key === '-') next = choices[Math.max(0, index-1)];
else if (event.key.toLowerCase() === 'f') next = 'fit';
else if (event.key === '0') next = '100';
if (next) {
event.preventDefault();
const input = document.getElementById('zoom-'+next);
input.checked = true;
input.dispatchEvent(new Event('change'));
}
});
canvas.addEventListener('pointerdown', event => {
if (event.pointerType !== 'mouse' || event.button !== 0) return;
drag = {x:event.clientX, scroll:canvas.scrollLeft};
canvas.setPointerCapture(event.pointerId);
canvas.classList.add('dragging');
});
canvas.addEventListener('pointermove', event => {
if (drag) canvas.scrollLeft = drag.scroll + drag.x-event.clientX;
});
const stop = () => { drag = null; canvas.classList.remove('dragging'); };
canvas.addEventListener('pointerup', stop);
canvas.addEventListener('pointercancel', stop);
canvas.addEventListener('lostpointercapture', stop);
}
centerVisibleEntries();
assets/task-report.css.status-line { font-size:17px; line-height:1.5; font-weight:550; color:var(--ink); margin:16px 0 0; padding:12px 16px; border-left:3px solid var(--accent); background:var(--accent-tint); max-width:1040px; }
.status-line.approved,.status-line.inspection { border-left-color:var(--muted); background:#edf0f2; }
.report-toc { border-top:1px solid var(--rule); padding:14px 0; }
.report-toc ol { display:flex; flex-wrap:wrap; gap:6px 28px; margin:0; padding:0; list-style:none; counter-reset:toc; font-size:14px; }
.report-toc li { counter-increment:toc; }
.report-toc li::before { content:counter(toc) "."; font:12px var(--mono); color:var(--muted); margin-right:6px; }
.task-content { overflow-wrap:anywhere; }
.tr-section { margin-top:40px; }
.tr-section > h3 { font-size:16px; margin:24px 0 6px; }
.tr-section > p,.tr-section > .tr-list { max-width:880px; }
.tr-summary { font-size:16px; color:var(--muted); }
.tr-list { padding-left:20px; margin:6px 0 0; font-size:14px; }
.tr-list li { margin:4px 0; }
.tr-section .review { border-top:0; }
.figure-head h3 { font-size:18px; margin:0; }
.control-name { font:10px/58px var(--mono); letter-spacing:.08em; text-transform:uppercase; color:var(--muted); }
.zoom-tabs.labelled { border-left:1px solid var(--rule); padding-left:20px; }
.ticket,.tr-check { border:1px solid var(--rule); background:#fff; padding:18px 22px; margin:16px 0; max-width:960px; }
.ticket h3 { font-size:18px; margin:2px 0 8px; }
.tr-check h4 { font-size:15px; margin:0 0 4px; }
.ticket-meta { font:11px var(--mono); letter-spacing:.04em; color:var(--muted); margin:0; }
.ticket-facts { display:grid; grid-template-columns:170px 1fr; gap:10px 20px; margin:14px 0 0; font-size:14px; }
.ticket-facts > div { display:contents; }
.ticket-facts dt { font-weight:600; }
.ticket-facts dd { margin:0; }
.ticket-facts dd p { margin:0 0 6px; }
.ticket-facts .tr-list { margin:0; }
.tr-acceptance { padding-left:24px; max-width:960px; font-size:14px; }
.tr-acceptance > li { padding:12px 0; border-bottom:1px solid var(--rule); }
.tr-acceptance p { margin:4px 0 0; }
.tr-how { color:var(--muted); }
.tr-result.verified strong { color:#2f6b3a; }
.tr-result.failed strong,.tr-result.partial strong,.tr-result.unverified strong { color:var(--accent); }
.task-content blockquote,.appendix blockquote { margin:12px 0; border-left:2px solid var(--rule); padding-left:16px; }
li,tr,article,h2 { scroll-margin-top:24px; }
@media(max-width:800px) {
.ticket-facts { grid-template-columns:1fr; gap:2px; }
.ticket-facts dd { margin-bottom:10px; }
.ticket,.tr-check { padding:14px 16px; }
.control-name { line-height:46px; }
.zoom-tabs.labelled { border-left:0; padding-left:0; justify-content:flex-start; }
.zoom-tabs.labelled .control-name { line-height:34px; }
.tr-section table { min-width:0; }
.tr-section td,.tr-section th { overflow-wrap:break-word; padding-right:12px; }
.tr-section td:first-child { width:34%; }
.tr-section td:nth-child(2) { width:28%; }
}
@media print { .report-toc { display:none; } .ticket,.tr-check { break-inside:avoid; } }
assets/ui-review.css
/* Optional screen-flow view. Existing editorial shell and application styles are not changed. */
.ui-review .ui-notice { border-left:2px solid var(--accent); padding:10px 16px; font-size:13px; }
.ui-review .ui-authority { border-top:1px solid var(--rule); border-bottom:1px solid var(--rule); padding:12px 0; margin:16px 0; font-size:13px; }
.ui-review .ui-authority p { margin:4px 0; }
.ui-scaled { width:calc(var(--ui-width) * var(--scale)); height:calc(var(--ui-height) * var(--scale)); position:relative; }
.ui-board { width:var(--ui-width); height:var(--ui-height); position:relative; transform:scale(var(--scale)); transform-origin:top left; }
.ui-screen-node { position:absolute; margin:0; }
.ui-routes { position:absolute; inset:0; pointer-events:none; }
.ui-routes marker path { fill:var(--muted); }
.ui-screen { display:block; max-width:100%; height:auto; }
.ui-screen-paper { fill:#fff; stroke:var(--rule); stroke-width:1.25; }
.ui-screen-caption { font:600 14px var(--sans); fill:var(--ink); }
.ui-divider { stroke:var(--rule); stroke-width:1; }
.ui-control-heading { font:600 17px var(--sans); fill:var(--ink); }
.ui-control-text { font:14px var(--sans); fill:var(--ink); }
.ui-control-small { font:12px var(--sans); fill:var(--muted); }
.ui-checkbox,.ui-field { fill:#fff; stroke:var(--muted); }
.ui-button { fill:#f5f5f5; stroke:var(--muted); }
.ui-button.ui-disabled { fill:#eee; stroke:#a4a8b0; }
.ui-check { fill:none; stroke:var(--ink); stroke-width:1.7; }
.ui-message-rule { stroke:var(--muted); stroke-width:2; }
.ui-marker { fill:var(--accent-tint); stroke:var(--accent); }
.ui-marker-text { fill:var(--accent); font:10px var(--mono); }
.ui-footnote { fill:var(--muted); font:10px var(--mono); }
.ui-inspect { position:absolute; right:12px; top:10px; font:10px/28px var(--mono); background:#fff; padding:0 4px; }
.ui-decisions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:0 32px; margin:24px 0; }
.ui-decisions > h2 { grid-column:1/-1; }
.ui-decision { border-top:1px solid var(--rule); padding:16px 0; font-size:13px; }
.ui-decision h3 { margin:0 0 8px; font-size:15px; }
.ui-decision p { margin:6px 0; }
.ui-screen-detail .ui-screen { width:400px; margin:20px 0; }
.ui-screen-detail p,.ui-evidence { font-size:13px; }
.ui-review .ui-inspect:focus-visible,.ui-screen a:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
@media(max-width:800px) { .ui-decisions { grid-template-columns:1fr; } }
@media print { .ui-scaled { width:100%; height:auto; } .ui-board { transform:none; width:100%; height:auto; display:grid; grid-template-columns:1fr 1fr; gap:20px; }
.ui-screen-node { position:static; width:100%!important; height:auto!important; } .ui-routes,.ui-inspect { display:none; } .ui-authority { break-inside:avoid; } }
.ui-capture-detail { display:block; max-width:100%; height:auto; margin:20px 0; }
/* Optional source-backed snippets: preview-only padding never touches app CSS. */
.ui-quick-gallery { margin:24px 0; border-top:1px solid var(--rule); padding-top:20px; }
.ui-quick-gallery > .hint { max-width:70ch; }
.ui-quick-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,330px),1fr)); gap:20px; }
.ui-quick-card { min-width:0; border:1px solid var(--rule); border-radius:10px; padding:16px; }
.ui-quick-card:has(details[open]) { grid-column:1/-1; }
.ui-quick-card h3 { margin:0 0 6px; font-size:16px; }
.ui-quick-card > p { margin:6px 0 12px; font-size:13px; }
.ui-quick-card iframe { display:block; width:100%; border:0; background:white; }
.ui-quick-small { border:1px solid var(--rule); border-radius:6px; overflow:hidden; }
.ui-quick-small iframe { height:220px; pointer-events:none; }
.ui-quick-card details { margin:12px 0; }
.ui-quick-card details summary { cursor:pointer; font-size:13px; }
.ui-quick-full { margin:12px 0; border:1px solid var(--rule); border-radius:6px; }
.ui-quick-full iframe { height:min(75vh,850px); min-height:420px; }
@media print { .ui-quick-small { display:none; } .ui-quick-full iframe { height:520px; } }
assets/ui-review.js
// Screen previews are inert; this only scales the report viewer, never an application.
function fitUiFlow() {
const panel=document.getElementById('panel-ui-preview');
if(!panel || !panel.offsetWidth) return;
const canvas=panel.querySelector('.ui-canvas');
const scaled=panel.querySelector('.ui-scaled');
const width=parseFloat(scaled.style.getPropertyValue('--ui-width'));
const selected=document.querySelector('input[name="diagram-zoom"]:checked');
const value=selected?.id==='zoom-fit' ? Math.min(1,canvas.clientWidth/width) : ({'zoom-100':1,'zoom-125':1.25,'zoom-150':1.5}[selected?.id]||1);
scaled.style.setProperty('--scale',value);
}
for(const input of document.querySelectorAll('.review input[type="radio"]')) input.addEventListener('change',fitUiFlow);
if(typeof ResizeObserver==='function') {
const uiCanvas=document.querySelector('.ui-canvas');
if(uiCanvas) new ResizeObserver(fitUiFlow).observe(uiCanvas);
}
fitUiFlow();
// Keep source/decision links clickable; report panning must not capture their pointer.
for(const link of document.querySelectorAll('.ui-canvas a')) {
link.addEventListener('pointerdown',event=>event.stopPropagation());
}
DIAGRAM-DESIGN.md# Compatibility pointer — local flowchart design This legacy filename is retained for existing references. Read [FLOWCHART-DESIGN.md](FLOWCHART-DESIGN.md) for the authoritative, bundled design guidance and [REPORT.md](REPORT.md) for the local renderer. The earlier external-provider setup instructions at this path are superseded. Do not install, discover or invoke a separate diagram-design skill. The approved appearance, local renderer and Matt's other specialist integrations are unchanged. This pointer creates no new approval or implementation authority.
examples/FINDINGS.md# Synthetic example evidence E1: src/scoreboard.py, format_score and web_score. Static inspection: web calls the shared formatter; it interpolates integers without negative validation. No runtime test is claimed. E2: src/scoreboard.py, _legacy_score and player_score. Static inspection: player uses a separate helper, also without negative validation. E3: The supplied small fixture has these two display entrypoints. Static inspection identifies duplicated formatting; this does not establish all callers in a real repository. The proposed validation/error path does not exist yet. This report is not implementation evidence.
examples/PLAN.md# Synthetic example — reject negative scores Revision P1. Awaiting user approval. No implementation has started. Example request: Reject negative home or away scores in both display paths. Keep all nonnegative formatting unchanged. Do not introduce dependencies, features or configuration. R1: Reject negatives in web and player paths. R2: Preserve nonnegative output. A1: Negative home/away values each raise ValueError through shared/web/player entrypoints. A2: Zero/positive values retain the existing `home - away` output in both paths. Current evidence E1/E2/E3 is in FINDINGS. Shared formatter is the existing owner; the player bypasses it. Proposed: add validation there, route both paths through it, remove directly superseded helper, verify both behaviors and update the existing README usage note. Boundaries: no UI redesign, score history, bulk operations, new packages or Git actions. Approval: pending. This is illustrative fixture content, not an actual user's approval.
examples/README.md# Synthetic report example `review.json` and `review.html` show the 2.2 flowchart-first renderer using the small bundled scoreboard fixture. No actual application change is approved or implemented. The example contains current behavior, the proposed branching feature flow, and a separate delivery-dependency view. A settings tree is not added when the request does not need it. The styling follows the owner-approved second diagram-design sample; this fixture does not claim a live specialist invocation. Source documents and code are illustrative. Render commands and schema migration are in [REPORT.md](../REPORT.md). Use a new output path, not the bundled review.html. Runtime/generated hashes identify only the supplied fixture bytes. ## Current state without rewriting a saved review `task-overview.json` uses the same flow geometry and baseline sources as `review.json`. Its additional STATUS.md is a synthetic legacy record with later fixture approval, separate implementation/verification, and an inline revision log. This is an example only; the workflow does not add STATUS.md to new real tasks just for a report. The original review.html intentionally remains a prior presentation snapshot, including its original pending state and renderer version. Render the overview from this directory using the same renderer, with a new output path. It has no approval action. Do not refresh it on every task update; ordinary status belongs in existing records and chat.
examples/review.html<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-Ct/4BgYIGLXq783UVb4B4liFvGHnUs1jSYOGFzy3QsY='; style-src 'unsafe-inline'; img-src 'none'; connect-src 'none'; font-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">
<title>Reject invalid scores. Keep valid output.</title><style>:root {
--paper:#f5f5f5; --ink:#2d3142; --muted:#4f5d75; --rule:#cdd0d5;
--accent:#bf4520; --accent-tint:#f5e9e4;
--sans:'Geist','Inter','Segoe UI',system-ui,sans-serif;
--serif:'Instrument Serif',Georgia,'Times New Roman',serif;
--mono:'Geist Mono',ui-monospace,'Cascadia Mono',monospace;
color-scheme:light;
}
* { box-sizing:border-box; }
body { margin:0; background:var(--paper); color:var(--ink); font:15px/1.6 var(--sans); }
main { max-width:1392px; margin:auto; padding:36px 56px 48px; }
a { color:var(--ink); text-underline-offset:4px; }
a:hover { color:var(--accent); }
a:focus-visible,summary:focus-visible,.canvas:focus-visible { outline:2px solid var(--accent); outline-offset:5px; }
header { padding-bottom:24px; }
.mast { display:flex; justify-content:space-between; gap:20px; border-bottom:1px solid var(--rule); padding-bottom:14px; font:11px/1.5 var(--mono); letter-spacing:.1em; color:var(--muted); }
.mast strong { font-weight:500; color:var(--ink); }
h1 { font:400 clamp(38px,4vw,58px)/1.08 var(--serif); letter-spacing:-.035em; margin:25px 0 14px; max-width:1080px; }
h2 { font-size:20px; line-height:1.4; font-weight:600; margin:0 0 12px; }
.status { font:11px/1.6 var(--mono); letter-spacing:.04em; color:var(--accent); margin:0 0 14px; }
.lead { font-size:16px; color:var(--muted); margin:0; max-width:1040px; }
.muted,small { color:var(--muted); }
.review { position:relative; border-top:1px solid var(--ink); }
.sr-radio { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); clip-path:inset(50%); white-space:nowrap; }
.viewbar { display:flex; justify-content:space-between; align-items:center; gap:16px; border-bottom:1px solid var(--rule); min-height:60px; }
.view-tabs,.zoom-tabs { display:flex; align-items:center; gap:24px; flex-wrap:wrap; }
.view-tabs label,.zoom-tabs label { cursor:pointer; user-select:none; line-height:58px; color:var(--muted); border-bottom:2px solid transparent; font-size:14px; }
.view-tabs label { font-weight:550; }
.zoom-tabs { gap:14px; flex-shrink:0; }
.zoom-tabs label { font:11px/58px var(--mono); }
.figures { --scale:1; }
#zoom-125:checked~.figures { --scale:1.25; }
#zoom-150:checked~.figures { --scale:1.5; }
#zoom-fit:checked~.figures .map { width:100%; min-width:0; }
.panel { display:none; margin:0; }
.figure-head { display:flex; justify-content:space-between; align-items:baseline; gap:24px; padding:22px 0 4px; }
.figure-head h2 { font-size:18px; margin:0; }
.figure-head span { font:10px var(--mono); color:var(--muted); text-transform:uppercase; white-space:nowrap; }
.figure-description { margin:6px 0 12px; font-size:13px; color:var(--muted); max-width:1100px; }
.hint { margin:0 0 4px; font-size:11px; color:var(--muted); }
.canvas { max-width:100%; overflow-x:auto; overflow-y:hidden; scrollbar-width:thin; scrollbar-color:#9198a5 transparent; overscroll-behavior-x:contain; cursor:grab; }
.canvas.dragging { cursor:grabbing; user-select:none; }
.map { display:block; width:calc(var(--board-width) * var(--scale)); min-width:calc(var(--board-width) * var(--scale)); height:auto; }
.node-shape { fill:#fff; stroke:var(--ink); stroke-width:1.2; }
.start .node-shape,.result .node-shape { fill:#edf0f2; stroke:var(--muted); }
.focal .node-shape { fill:var(--accent-tint); stroke:var(--accent); stroke-width:1.4; }
.node.inferred .node-shape,.node.unknown .node-shape { stroke-dasharray:5 4; }
.connector { fill:none; stroke:var(--muted); stroke-width:1.4; }
.connector.inferred,.connector.unknown { stroke-dasharray:5 4; }
.map marker path { fill:var(--muted); }
.node-title { font:600 18px var(--sans); fill:var(--ink); }
.node-note { font:13px var(--sans); fill:var(--muted); }
.node-item { font:15px var(--sans); fill:var(--ink); }
.arrow-label rect { fill:var(--paper); }
.edge-label { font:10px var(--mono); letter-spacing:1px; fill:var(--muted); }
figcaption { font-size:12px; color:var(--muted); margin:12px 0 24px; }
.legend { display:flex; flex-wrap:wrap; align-items:center; gap:16px 32px; border-top:1px solid var(--rule); padding:16px 0 10px; }
.legend span { display:flex; align-items:center; gap:10px; }
.legend i { display:inline-block; width:26px; height:16px; border:1px solid var(--muted); background:#fff; }
.legend-start { border-radius:10px; }
.legend-action,.legend-group { border-radius:3px; }
.legend .legend-decision { width:16px; height:16px; transform:rotate(45deg); margin:0 5px; }
.legend .legend-focus { background:var(--accent-tint); border-color:var(--accent); border-radius:3px; }
.comparison { border-top:1px solid var(--ink); padding-top:24px; margin-top:8px; }
.comparison h2,.boundaries h2 { font:400 30px var(--serif); }
.table-scroll { max-width:100%; overflow-x:auto; }
table { border-collapse:collapse; width:100%; font-size:14px; text-align:left; }
th { font:10px var(--mono); letter-spacing:.08em; text-transform:uppercase; color:var(--muted); padding:14px 18px 12px 0; border-bottom:1px solid var(--rule); vertical-align:bottom; }
td { padding:16px 24px 16px 0; border-bottom:1px solid var(--rule); vertical-align:top; }
td:first-child { width:20%; }
td:last-child { padding-right:0; }
.comparison td:nth-child(2) { width:36%; color:var(--muted); }
small { display:block; font-size:12px; line-height:1.6; margin-top:5px; }
.boundaries { padding-top:24px; }
.boundaries ul { padding-left:20px; font-size:14px; }
.two-col { display:grid; grid-template-columns:1fr 1.2fr; gap:48px; margin:32px 0; }
.plain { list-style:none; padding:0; margin:0; }
.plain li { padding:13px 0; border-bottom:1px solid var(--rule); font-size:14px; }
.approval { border-top:1px solid var(--ink); padding:24px 0; display:grid; grid-template-columns:230px 1fr; gap:32px; }
.approval h2 { font:400 30px var(--serif); }
.approval p { margin:0 0 10px; font-size:14px; }
.warning { padding:18px 0; margin:0 0 20px; border-top:1px solid var(--accent); border-bottom:1px solid var(--rule); }
.warning h2 { color:var(--accent); font-size:17px; }
details { border-top:1px solid var(--rule); padding:17px 0; }
summary { font-size:14px; font-weight:550; cursor:pointer; list-style-position:inside; }
.appendix-body { padding-top:24px; }
.appendix-body section { margin-bottom:30px; }
.appendix table { min-width:840px; font-size:12px; }
.appendix td:first-child { width:auto; }
.appendix ol { font-size:12px; padding-left:22px; }
.appendix ol li { margin:10px 0; }
.evidence { padding:18px 0; border-top:1px solid var(--rule); }
.evidence strong { font-size:14px; }
.evidence p,code { font:12px/1.8 var(--mono); overflow-wrap:anywhere; }
.snapshot p { font-size:12px; }
footer { border-top:1px solid var(--rule); padding-top:14px; margin-top:28px; color:var(--muted); font:10px/1.7 var(--mono); }
li,tr,article { scroll-margin-top:24px; }
@media(max-width:800px) {
main { padding:22px 20px 36px; }
h1 { font-size:39px; }
.mast { font-size:9px; }
.viewbar { display:block; }
.view-tabs { gap:18px; }
.view-tabs label { font-size:12px; line-height:46px; }
.zoom-tabs { justify-content:flex-end; border-top:1px solid var(--rule); }
.zoom-tabs label { line-height:34px; }
.figure-head { display:block; }
.figure-head span { display:block; margin-top:8px; }
.comparison table { min-width:740px; }
.two-col,.approval { grid-template-columns:1fr; gap:20px; }
}
@media print {
main { max-width:none; padding:0; }
.viewbar,.hint { display:none!important; }
.panel { display:block!important; break-inside:avoid; }
.canvas { overflow:visible; }
.map { width:100%!important; min-width:0!important; }
.figure-head { margin-top:25px; }
.approval { break-inside:avoid; }
a { color:inherit; }
}
.task-checkpoint { border-top:1px solid var(--ink); padding:20px 0 24px; }
.task-checkpoint h2 { font:400 28px var(--serif); margin:0 0 6px; }
.checkpoint-asof,.checkpoint-limit,.flow-context { font-size:12px; color:var(--muted); overflow-wrap:anywhere; }
.checkpoint-asof { margin:0 0 18px; }
.checkpoint-axes { display:grid; grid-template-columns:1fr 1fr 1fr; gap:28px; margin:0 0 16px; }
.checkpoint-axes > div { min-width:0; }
.checkpoint-axes dt { font:10px/1.6 var(--mono); text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
.checkpoint-axes dd { margin:5px 0 0; font-size:14px; overflow-wrap:anywhere; }
.checkpoint-axes dd p { margin:5px 0 0; font-size:13px; color:var(--muted); }
.next-action { font-size:14px; border-top:1px solid var(--rule); padding-top:14px; margin:0; overflow-wrap:anywhere; }
.next-action strong { margin-right:12px; }
.checkpoint-limit { margin:12px 0 0; }
.flow-context { margin:0 0 14px; }
.revision-log { margin-bottom:12px; }
.revision-log > p { font-size:12px; color:var(--muted); }
.revision-log table { min-width:740px; }
.revision-log td { overflow-wrap:anywhere; }
@media(max-width:800px) { .checkpoint-axes { grid-template-columns:1fr; gap:16px; } }
@media print { .task-checkpoint { break-inside:avoid; } }
#view-proposed:checked~.figures #panel-proposed{display:block}
#view-proposed:checked~.viewbar label[for="view-proposed"]{border-color:var(--accent);color:var(--ink)}
#view-proposed:focus-visible~.viewbar label[for="view-proposed"]{outline:2px solid var(--accent);outline-offset:4px}
#view-current:checked~.figures #panel-current{display:block}
#view-current:checked~.viewbar label[for="view-current"]{border-color:var(--accent);color:var(--ink)}
#view-current:focus-visible~.viewbar label[for="view-current"]{outline:2px solid var(--accent);outline-offset:4px}
#view-build:checked~.figures #panel-build{display:block}
#view-build:checked~.viewbar label[for="view-build"]{border-color:var(--accent);color:var(--ink)}
#view-build:focus-visible~.viewbar label[for="view-build"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-100:checked~.viewbar label[for="zoom-100"]{border-color:var(--ink);color:var(--ink)}
#zoom-100:focus-visible~.viewbar label[for="zoom-100"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-125:checked~.viewbar label[for="zoom-125"]{border-color:var(--ink);color:var(--ink)}
#zoom-125:focus-visible~.viewbar label[for="zoom-125"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-150:checked~.viewbar label[for="zoom-150"]{border-color:var(--ink);color:var(--ink)}
#zoom-150:focus-visible~.viewbar label[for="zoom-150"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-fit:checked~.viewbar label[for="zoom-fit"]{border-color:var(--ink);color:var(--ink)}
#zoom-fit:focus-visible~.viewbar label[for="zoom-fit"]{outline:2px solid var(--accent);outline-offset:4px}</style></head><body><main>
<header><div class="mast"><strong>Synthetic scoreboard fixture — example only</strong><span>Plan P1</span></div>
<h1>Reject invalid scores. Keep valid output.</h1><p class="status awaiting-approval">WAITING FOR YOUR APPROVAL. No code is written yet</p><p class="lead">You want both display paths to reject negative scores, while keeping every nonnegative score formatted exactly as before. Reuse the existing formatter rather than adding another feature or validation layer.</p></header>
<section class="review" aria-label="Flowchart review"><input class="sr-radio" type="radio" name="diagram-view" id="view-proposed" aria-label="Feature flow" aria-controls="panel-proposed" checked><input class="sr-radio" type="radio" name="diagram-view" id="view-current" aria-label="Current flow" aria-controls="panel-current" ><input class="sr-radio" type="radio" name="diagram-view" id="view-build" aria-label="Build plan" aria-controls="panel-build" ><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-100" aria-label="100% diagram size" checked><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-125" aria-label="125% diagram size" ><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-150" aria-label="150% diagram size" ><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-fit" aria-label="Fit diagram size" ><div class="viewbar"><div class="view-tabs" role="group" aria-label="Diagram views"><label for="view-proposed">Feature flow</label><label for="view-current">Current flow</label><label for="view-build">Build plan</label></div><div class="zoom-tabs" role="group" aria-label="Diagram size"><label for="zoom-100">100%</label><label for="zoom-125">125%</label><label for="zoom-150">150%</label><label for="zoom-fit">Fit</label></div></div><div class="figures"><figure class="panel" id="panel-proposed"><div class="figure-head"><h2>Proposed: one check, two outcomes</h2><span>proposed · flow</span></div><p class="figure-description">Both display paths use the existing formatter. Reject either negative input. Otherwise return exactly the existing score text. Proposed behavior, not a runtime observation.</p><p class="hint">Scroll sideways to explore. <a href="#node-8-proposed-N1">Go to start</a>. Fit is an optional overview.</p><div class="canvas" tabindex="0" role="region" aria-label="Feature flow diagram, scroll to explore" data-start-id="node-8-proposed-N1" style="--board-width:1280px"><svg class="map" xmlns="http://www.w3.org/2000/svg" width="1280" height="736" viewBox="0 0 1280 736" role="img" aria-labelledby="proposed-title proposed-desc"><title id="proposed-title">Proposed: one check, two outcomes</title><desc id="proposed-desc">Both display paths use the existing formatter. Reject either negative input. Otherwise return exactly the existing score text. Proposed behavior, not a runtime observation.</desc><defs><marker id="arrow-proposed" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs><path class="connector proposed" data-from="N1" data-to="N2" data-points="[[640, 112], [640, 160]]" d="M 640 112 L 640 160" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="N2" data-to="N3" data-points="[[640, 248], [640, 320]]" d="M 640 248 L 640 320" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="N3" data-to="N4" data-points="[[768, 400], [920, 400]]" d="M 768 400 L 920 400" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="N3" data-to="N5" data-points="[[640, 480], [640, 576]]" d="M 640 480 L 640 576" marker-end="url(#arrow-proposed)"/><g class="arrow-label"><rect x="825.5" y="357" width="37" height="22"/><text class="edge-label" text-anchor="middle" x="844" y="372">YES</text></g><g class="arrow-label"><rect x="673" y="509" width="30" height="22"/><text class="edge-label" text-anchor="middle" x="688" y="524">NO</text></g><g id="node-8-proposed-N1" class="node start proposed " data-id="N1" data-box="464,40,352,72"><title>Both display paths. proposed / modified. </title><rect class="node-shape" x="464" y="40" width="352" height="72" rx="20"/><text class="node-title" x="640" y="72" text-anchor="middle">Both display paths</text><text class="node-note" x="640" y="96" text-anchor="middle">Web and player</text></g><g id="node-8-proposed-N2" class="node action proposed " data-id="N2" data-box="464,160,352,88"><title>Shared formatter. proposed / modified. </title><rect class="node-shape" x="464" y="160" width="352" height="88" rx="6"/><text class="node-title" x="640" y="200" text-anchor="middle">Shared formatter</text><text class="node-note" x="640" y="224" text-anchor="middle">One owner for both paths</text></g><g id="node-8-proposed-N3" class="node decision proposed focal" data-id="N3" data-box="512,320,256,160"><title>Either score
negative?. proposed / modified. </title><polygon class="node-shape" points="640,320 768,400 640,480 512,400"/><text class="node-title" x="640" y="394" text-anchor="middle">Either score</text><text class="node-title" x="640" y="418" text-anchor="middle">negative?</text></g><g id="node-8-proposed-N4" class="node result proposed focal" data-id="N4" data-box="920,352,304,96"><title>Reject invalid input. proposed / added. </title><rect class="node-shape" x="920" y="352" width="304" height="96" rx="20"/><text class="node-title" x="1072" y="396" text-anchor="middle">Reject invalid input</text><text class="node-note" x="1072" y="420" text-anchor="middle">Raise ValueError</text></g><g id="node-8-proposed-N5" class="node result observed " data-id="N5" data-box="460,576,360,88"><title>Keep score formatting. observed / unchanged. </title><rect class="node-shape" x="460" y="576" width="360" height="88" rx="20"/><text class="node-title" x="640" y="616" text-anchor="middle">Keep score formatting</text><text class="node-note" x="640" y="640" text-anchor="middle">Zero and positive scores unchanged</text></g></svg></div><figcaption><div class="legend"><span><i class="legend-start" aria-hidden="true"></i>Start / result</span><span><i class="legend-action" aria-hidden="true"></i>Action</span><span><i class="legend-decision" aria-hidden="true"></i>Decision</span><span><i class="legend-focus" aria-hidden="true"></i>Focus of the change</span></div><span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. Proposed is not implemented.</span></figcaption></figure><figure class="panel" id="panel-current"><div class="figure-head"><h2>Current: two separate paths</h2><span>current · flow</span></div><p class="figure-description">Static inspection of the bundled fixture. Each path returns score text. Neither path rejects negative values.</p><p class="hint">Scroll sideways to explore. <a href="#node-7-current-N1">Go to start</a>. Fit is an optional overview.</p><div class="canvas" tabindex="0" role="region" aria-label="Current flow diagram, scroll to explore" data-start-id="node-7-current-N1" style="--board-width:1280px"><svg class="map" xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-labelledby="current-title current-desc"><title id="current-title">Current: two separate paths</title><desc id="current-desc">Static inspection of the bundled fixture. Each path returns score text. Neither path rejects negative values.</desc><defs><marker id="arrow-current" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs><path class="connector observed" data-from="N1" data-to="N2" data-points="[[336, 200], [456, 200]]" d="M 336 200 L 456 200" marker-end="url(#arrow-current)"/><path class="connector observed" data-from="N2" data-to="N3" data-points="[[800, 200], [920, 200]]" d="M 800 200 L 920 200" marker-end="url(#arrow-current)"/><path class="connector observed" data-from="N4" data-to="N5" data-points="[[336, 472], [456, 472]]" d="M 336 472 L 456 472" marker-end="url(#arrow-current)"/><path class="connector observed" data-from="N5" data-to="N6" data-points="[[800, 472], [920, 472]]" d="M 800 472 L 920 472" marker-end="url(#arrow-current)"/><g class="arrow-label"><rect x="370.5" y="157" width="51" height="22"/><text class="edge-label" text-anchor="middle" x="396" y="172">CALLS</text></g><g class="arrow-label"><rect x="838" y="157" width="44" height="22"/><text class="edge-label" text-anchor="middle" x="860" y="172">TEXT</text></g><g class="arrow-label"><rect x="370.5" y="429" width="51" height="22"/><text class="edge-label" text-anchor="middle" x="396" y="444">CALLS</text></g><g class="arrow-label"><rect x="838" y="429" width="44" height="22"/><text class="edge-label" text-anchor="middle" x="860" y="444">TEXT</text></g><g id="node-7-current-N1" class="node start observed " data-id="N1" data-box="48,152,288,96"><title>Web display. observed / unchanged. </title><rect class="node-shape" x="48" y="152" width="288" height="96" rx="20"/><text class="node-title" x="192" y="196" text-anchor="middle">Web display</text><text class="node-note" x="192" y="220" text-anchor="middle">Shared formatting path</text></g><g id="node-7-current-N2" class="node action observed " data-id="N2" data-box="456,152,344,96"><title>Shared formatter. observed / unchanged. </title><rect class="node-shape" x="456" y="152" width="344" height="96" rx="6"/><text class="node-title" x="628" y="196" text-anchor="middle">Shared formatter</text><text class="node-note" x="628" y="220" text-anchor="middle">No negative-score check</text></g><g id="node-7-current-N3" class="node result observed " data-id="N3" data-box="920,152,304,96"><title>Score text. observed / unchanged. </title><rect class="node-shape" x="920" y="152" width="304" height="96" rx="20"/><text class="node-title" x="1072" y="196" text-anchor="middle">Score text</text><text class="node-note" x="1072" y="220" text-anchor="middle">Negatives are not rejected</text></g><g id="node-7-current-N4" class="node start observed " data-id="N4" data-box="48,424,288,96"><title>Player display. observed / unchanged. </title><rect class="node-shape" x="48" y="424" width="288" height="96" rx="20"/><text class="node-title" x="192" y="468" text-anchor="middle">Player display</text><text class="node-note" x="192" y="492" text-anchor="middle">Separate formatting path</text></g><g id="node-7-current-N5" class="node action observed " data-id="N5" data-box="456,424,344,96"><title>Legacy formatter. observed / unchanged. </title><rect class="node-shape" x="456" y="424" width="344" height="96" rx="6"/><text class="node-title" x="628" y="468" text-anchor="middle">Legacy formatter</text><text class="node-note" x="628" y="492" text-anchor="middle">Duplicates score formatting</text></g><g id="node-7-current-N6" class="node result observed " data-id="N6" data-box="920,424,304,96"><title>Score text. observed / unchanged. </title><rect class="node-shape" x="920" y="424" width="304" height="96" rx="20"/><text class="node-title" x="1072" y="468" text-anchor="middle">Score text</text><text class="node-note" x="1072" y="492" text-anchor="middle">Shared owner is bypassed</text></g></svg></div><figcaption><div class="legend"><span><i class="legend-start" aria-hidden="true"></i>Start / result</span><span><i class="legend-action" aria-hidden="true"></i>Action</span></div><span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. Proposed is not implemented.</span></figcaption></figure><figure class="panel" id="panel-build"><div class="figure-head"><h2>Build plan: after approval only</h2><span>proposed · delivery</span></div><p class="figure-description">These are delivery dependencies, not the feature runtime. Approval must be recorded before T1 begins. No diagram action grants it.</p><p class="hint">Scroll sideways to explore. <a href="#node-5-build-approval">Go to start</a>. Fit is an optional overview.</p><div class="canvas" tabindex="0" role="region" aria-label="Build plan diagram, scroll to explore" data-start-id="node-5-build-approval" style="--board-width:1280px"><svg class="map" xmlns="http://www.w3.org/2000/svg" width="1280" height="848" viewBox="0 0 1280 848" role="img" aria-labelledby="build-title build-desc"><title id="build-title">Build plan: after approval only</title><desc id="build-desc">These are delivery dependencies, not the feature runtime. Approval must be recorded before T1 begins. No diagram action grants it.</desc><defs><marker id="arrow-build" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs><path class="connector proposed" data-from="approval" data-to="B1" data-points="[[640, 104], [640, 176]]" d="M 640 104 L 640 176" marker-end="url(#arrow-build)"/><path class="connector proposed" data-from="B1" data-to="B2" data-points="[[640, 264], [640, 328]]" d="M 640 264 L 640 328" marker-end="url(#arrow-build)"/><path class="connector proposed" data-from="B2" data-to="B3" data-points="[[640, 416], [640, 480]]" d="M 640 416 L 640 480" marker-end="url(#arrow-build)"/><path class="connector proposed" data-from="B3" data-to="B4" data-points="[[640, 576], [640, 640]]" d="M 640 576 L 640 640" marker-end="url(#arrow-build)"/><g id="node-5-build-approval" class="node start proposed focal" data-id="approval" data-box="472,40,336,64"><title>User approves this plan. proposed / unchanged. </title><rect class="node-shape" x="472" y="40" width="336" height="64" rx="20"/><text class="node-title" x="640" y="78" text-anchor="middle">User approves this plan</text></g><g id="node-5-build-B1" class="node action proposed " data-id="B1" data-box="432,176,416,88"><title>Add regression tests. proposed / modified. </title><rect class="node-shape" x="432" y="176" width="416" height="88" rx="6"/><text class="node-title" x="640" y="216" text-anchor="middle">Add regression tests</text><text class="node-note" x="640" y="240" text-anchor="middle">Invalid inputs and valid output</text></g><g id="node-5-build-B2" class="node action proposed " data-id="B2" data-box="432,328,416,88"><title>Validate at the owner. proposed / modified. </title><rect class="node-shape" x="432" y="328" width="416" height="88" rx="6"/><text class="node-title" x="640" y="368" text-anchor="middle">Validate at the owner</text><text class="node-note" x="640" y="392" text-anchor="middle">Use the existing formatter</text></g><g id="node-5-build-B3" class="node action proposed " data-id="B3" data-box="432,480,416,96"><title>Route player; remove duplicate. proposed / modified. </title><rect class="node-shape" x="432" y="480" width="416" height="96" rx="6"/><text class="node-title" x="640" y="524" text-anchor="middle">Route player; remove duplicate</text><text class="node-note" x="640" y="548" text-anchor="middle">Replace the bypass, not the feature</text></g><g id="node-5-build-B4" class="node result proposed " data-id="B4" data-box="432,640,416,96"><title>Verify and update docs. proposed / modified. </title><rect class="node-shape" x="432" y="640" width="416" height="96" rx="20"/><text class="node-title" x="640" y="684" text-anchor="middle">Verify and update docs</text><text class="node-note" x="640" y="708" text-anchor="middle">Check both paths and stale references</text></g></svg></div><figcaption><div class="legend"><span><i class="legend-start" aria-hidden="true"></i>Start / result</span><span><i class="legend-action" aria-hidden="true"></i>Action</span><span><i class="legend-focus" aria-hidden="true"></i>Focus of the change</span></div><span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. Proposed is not implemented.</span></figcaption></figure></div></section>
<section class="comparison"><h2>What changes</h2><div class="table-scroll"><table><thead><tr><th>Area</th><th>Today, from the recorded evidence</th><th>Proposed scope</th></tr></thead><tbody><tr><td><strong>Negative scores</strong><small><a href="#r-R1">R1</a></small></td><td>Both display paths format negative scores without rejecting them.<small><a href="#e-E3">E3</a></small></td><td>Reject a negative home or away score through the shared formatter.<small><a href="#t-T1">T1</a> <a href="#t-T2">T2</a> <a href="#t-T3">T3</a> <a href="#t-T4">T4</a></small></td></tr><tr><td><strong>Valid scores</strong><small><a href="#r-R2">R2</a></small></td><td>Zero and positive scores are formatted as home - away.<small><a href="#e-E1">E1</a> <a href="#e-E2">E2</a></small></td><td>Keep the same text for every nonnegative score.<small><a href="#t-T1">T1</a> <a href="#t-T2">T2</a> <a href="#t-T3">T3</a> <a href="#t-T4">T4</a></small></td></tr><tr><td><strong>Ownership</strong><small><a href="#r-R1">R1</a> <a href="#r-R2">R2</a></small></td><td>Web and player use separate formatting functions.<small><a href="#e-E1">E1</a> <a href="#e-E2">E2</a></small></td><td>Use the existing shared owner and remove the redundant helper.<small><a href="#t-T3">T3</a> <a href="#t-T4">T4</a></small></td></tr></tbody></table></div></section><section class="boundaries"><h2>What will not change</h2><ul><li>No new features, UI redesign or score history.</li><li>No dependencies, extra configuration or automatic Git actions.</li><li>Only the shared score behavior, required caller, tests, scoped cleanup and existing documentation change.</li></ul></section>
<div class="two-col"><section><h2>Changes and cleanup</h2><ul class="plain"><li><a href="#t-T1">T1</a> Add behavior-level regression tests<small>Depends on: No prior task</small></li><li><a href="#t-T2">T2</a> Validate at the existing shared formatter<small>Depends on: T1</small></li><li><a href="#t-T3">T3</a> Route the player through shared validation and remove the old helper<small>Depends on: T2</small></li><li><a href="#t-T4">T4</a> Verify both paths and update existing usage documentation<small>Depends on: T3</small></li></ul></section>
<section><h2>How completion will be checked</h2><ul class="plain"><li id="a-A1"><strong>A1: Negative scores are rejected through all affected entrypoints.</strong><small>Verify: Add/run tests for negative home and negative away at shared, web and player entrypoints. Expect ValueError. · <a href="#r-R1">R1</a></small></li><li id="a-A2"><strong>A2: Nonnegative scores still render exactly as before.</strong><small>Verify: Run zero/positive examples through both display paths and compare exact strings. Make sure that no reference to the old helper remains. · <a href="#r-R2">R2</a></small></li></ul></section></div>
<section class="approval"><h2>Review before code.</h2><div><p>Does this match what you want built? Approve plan P1 in the chat, or say what to change.</p><p>Opening this report does not approve anything. There is no approve button, no connection to the agent and no work that starts in the background.</p></div></section><details class="appendix" id="technical-details"><summary>Technical evidence and task details</summary><div class="appendix-body">
<section><h2>Requested outcome</h2><ul class="plain"><li id="r-R1"><strong>R1.</strong> Reject a negative home or away score in both display paths.<small>Source: Synthetic request in PLAN.md; not an actual product change.</small></li><li id="r-R2"><strong>R2.</strong> Keep zero and positive score formatting unchanged.<small>Source: Explicit compatibility constraint in the synthetic request.</small></li></ul></section>
<section><h2>Changes and mapped tasks</h2><div class="table-scroll"><table><thead><tr><th>Task</th><th>Work and affected areas</th><th>Traceability</th><th>Depends on</th></tr></thead><tbody><tr id="t-T1"><td><strong>T1</strong></td><td><strong>Add behavior-level regression tests</strong><small>prerequisite: Prove the requested rejection and preserve existing output.<br>Areas: tests/test_scoreboard.py</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td>—</td></tr><tr id="t-T2"><td><strong>T2</strong></td><td><strong>Validate at the existing shared formatter</strong><small>requested: The shared owner already formats the same score values.<br>Areas: src/scoreboard.py: format_score</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E1">E1</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td><a href="#t-T1">T1</a></td></tr><tr id="t-T3"><td><strong>T3</strong></td><td><strong>Route the player through shared validation and remove the old helper</strong><small>prerequisite: If only the shared formatter changes, the player still bypasses it.<br>Areas: src/scoreboard.py: player_score, _legacy_score</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E2">E2</a> <a href="#e-E3">E3</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td><a href="#t-T2">T2</a></td></tr><tr id="t-T4"><td><strong>T4</strong></td><td><strong>Verify both paths and update existing usage documentation</strong><small>prerequisite: Complete acceptance evidence and remove stale behavior descriptions.<br>Areas: tests/test_scoreboard.py, README.md</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td><a href="#t-T3">T3</a></td></tr></tbody></table></div></section>
<section><h2>Evidence and limits</h2><article id="e-E1" class="evidence"><strong>E1 · static: The web path uses an existing shared formatter without validation.</strong><p>src/scoreboard.py: format_score, web_score</p><small>Sources: <a href="#s-S4">S4</a> <a href="#s-S3">S3</a><br>Limits: Static inspection only; no runtime result claimed.</small></article><article id="e-E2" class="evidence"><strong>E2 · static: The player calls a separate formatter and bypasses the shared owner.</strong><p>src/scoreboard.py: _legacy_score, player_score</p><small>Sources: <a href="#s-S4">S4</a> <a href="#s-S3">S3</a><br>Limits: Static fixture only; not a complete application call graph.</small></article><article id="e-E3" class="evidence"><strong>E3 · static: Both supplied paths format scores but neither rejects negatives.</strong><p>Both entrypoints and their directly called formatters</p><small>Sources: <a href="#s-S4">S4</a> <a href="#s-S3">S3</a><br>Limits: Only the tiny supplied fixture was inspected.</small></article></section><details><summary>Current flow: steps and links</summary><ol><li><strong>N1: Web display</strong>. Shared formatting path (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N2: Shared formatter</strong>. No negative-score check (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N3: Score text</strong>. Negatives are not rejected (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N4: Player display</strong>. Separate formatting path (observed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li><li><strong>N5: Legacy formatter</strong>. Duplicates score formatting (observed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li><li><strong>N6: Score text</strong>. Shared owner is bypassed (observed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li><li><strong>N1 → N2</strong>: calls (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N2 → N3</strong>: text (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N4 → N5</strong>: calls (observed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li><li><strong>N5 → N6</strong>: text (observed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li></ol></details><details><summary>Feature flow: steps and links</summary><ol><li><strong>N1: Both display paths</strong>. Web and player (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a>; tasks <a href="#t-T3">T3</a>.</li><li><strong>N2: Shared formatter</strong>. One owner for both paths (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N3: Either score
negative?</strong>. (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N4: Reject invalid input</strong>. Raise ValueError (proposed, added) Evidence <a href="#e-E1">E1</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N5: Keep score formatting</strong>. Zero and positive scores unchanged (observed, unchanged) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a>; tasks —.</li><li><strong>N1 → N2</strong>: continues (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a>; tasks <a href="#t-T3">T3</a>.</li><li><strong>N2 → N3</strong>: continues (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N3 → N4</strong>: Yes (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N3 → N5</strong>: No (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li></ol></details><details><summary>Build plan: steps and links</summary><ol><li><strong>approval: User approves this plan</strong>. (proposed, unchanged) Evidence —; tasks —.</li><li><strong>B1: Add regression tests</strong>. Invalid inputs and valid output (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T1">T1</a>.</li><li><strong>B2: Validate at the owner</strong>. Use the existing formatter (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>B3: Route player; remove duplicate</strong>. Replace the bypass, not the feature (proposed, modified) Evidence <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T3">T3</a>.</li><li><strong>B4: Verify and update docs</strong>. Check both paths and stale references (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T4">T4</a>.</li><li><strong>approval → B1</strong>: continues (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T1">T1</a>.</li><li><strong>B1 → B2</strong>: continues (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>B2 → B3</strong>: continues (proposed, modified) Evidence <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T3">T3</a>.</li><li><strong>B3 → B4</strong>: continues (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T4">T4</a>.</li></ol></details>
<details class="snapshot"><summary>Inspected snapshot and supplied-source fingerprints</summary><p>Synthetic scoreboard fixture — example only · Local illustrative files; no application branch</p>
<p>Based on the bundled fixture and named source files. Current flow is statically inspected; proposed behavior is not implemented.</p><p>Generated 2026-11-03T12:20:57.814851+00:00. Hashes cover only these supplied files, including local changes. They do not prove complete code coverage or correctness.</p><div class="table-scroll"><table><thead><tr><th>ID</th><th>Source</th><th>Role</th><th>SHA-256 prefix</th></tr></thead><tbody><tr id="s-S1"><td>S1</td><td><code>PLAN.md</code></td><td>plan</td><td><code>afeb79818ad8cae6…</code></td></tr><tr id="s-S2"><td>S2</td><td><code>TODO.md</code></td><td>todo</td><td><code>34453899a983f495…</code></td></tr><tr id="s-S3"><td>S3</td><td><code>FINDINGS.md</code></td><td>findings</td><td><code>6aca6ff40deb18d1…</code></td></tr><tr id="s-S4"><td>S4</td><td><code>src/scoreboard.py</code></td><td>code</td><td><code>7e1df39d2884f0b3…</code></td></tr><tr id="s-report-input"><td>report-input</td><td><code>review.json</code></td><td>projection</td><td><code>d654a392f5411990…</code></td></tr></tbody></table></div></details>
</div></details><p class="muted">System-font mode. No network assets. A saved projection, not a live repository map.</p><footer>Local HTML + SVG · no external assets, uploads or approval actions · saved review snapshot; progress belongs in the existing task records</footer>
</main><script type="application/json" id="report-manifest">{"schema_version": 1, "tool_version": "2.8.0-rc.1", "layout_version": 2, "style": "workflow-editorial-v1", "revision": "P1", "report_kind": "review", "generated_utc": "2026-11-03T12:20:57.814851+00:00", "sources": [{"id": "S1", "role": "plan", "path": "PLAN.md", "sha256": "afeb79818ad8cae67f80ea9aef77754391f84eefc29f32bccc500c0ac06217ef"}, {"id": "S2", "role": "todo", "path": "TODO.md", "sha256": "34453899a983f495c86dc398a7f0218439aeb5c6de0c129cb0f48f0f69c0e1cd"}, {"id": "S3", "role": "findings", "path": "FINDINGS.md", "sha256": "6aca6ff40deb18d14d610448bc4fab4aa94442b94b8e53f464a9212d32d6262e"}, {"id": "S4", "role": "code", "path": "src/scoreboard.py", "sha256": "7e1df39d2884f0b337a0939370cacce6f6c462c6bfa3c10c42c81f339889aed9"}, {"id": "report-input", "role": "projection", "path": "review.json", "sha256": "d654a392f5411990318c0cd3119e2981794c82d8c9754db145c0661279b00bd6"}]}</script><script>'use strict';
// Native radio inputs and scrolling work without this progressive enhancement.
function revealAnchor() {
const id = location.hash.slice(1);
const target = document.getElementById(id);
if (!target) return;
let parent = target.parentElement;
while (parent) {
if (parent.tagName === 'DETAILS') parent.open = true;
parent = parent.parentElement;
}
target.scrollIntoView({block:target.closest('.canvas') ? 'nearest' : 'start', inline:'center'});
}
for (const anchor of document.querySelectorAll('a[href^="#"]')) {
anchor.addEventListener('click', () => {
const target = document.getElementById(anchor.getAttribute('href').slice(1));
let parent = target;
while (parent) {
if (parent.tagName === 'DETAILS') parent.open = true;
parent = parent.parentElement;
}
});
}
window.addEventListener('hashchange', revealAnchor);
if (location.hash) revealAnchor();
function centerEntry(canvas) {
if (!canvas.clientWidth) return;
const node = document.getElementById(canvas.dataset.startId);
if (!node) return;
const bounds = node.getBoundingClientRect();
const viewport = canvas.getBoundingClientRect();
canvas.scrollLeft += bounds.x + bounds.width/2 - viewport.x - viewport.width/2;
}
function centerVisibleEntries() {
for (const canvas of document.querySelectorAll('.canvas')) centerEntry(canvas);
}
for (const input of document.querySelectorAll('.review input[type="radio"]')) {
input.addEventListener('change', centerVisibleEntries);
}
const resizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(entries => {
for (const entry of entries) centerEntry(entry.target);
}) : null;
for (const canvas of document.querySelectorAll('.canvas')) {
// On narrow screens show the entry node, not the diagram's empty left margin.
resizeObserver?.observe(canvas);
let drag = null;
canvas.addEventListener('keydown', event => {
const choices = ['100', '125', '150'];
const selected = document.querySelector('input[name="diagram-zoom"]:checked');
let index = choices.indexOf(selected.id.slice(5));
let next = null;
if (event.key === '+' || event.key === '=') next = choices[Math.min(2, index+1)];
else if (event.key === '-') next = choices[Math.max(0, index-1)];
else if (event.key.toLowerCase() === 'f') next = 'fit';
else if (event.key === '0') next = '100';
if (next) {
event.preventDefault();
const input = document.getElementById('zoom-'+next);
input.checked = true;
input.dispatchEvent(new Event('change'));
}
});
canvas.addEventListener('pointerdown', event => {
if (event.pointerType !== 'mouse' || event.button !== 0) return;
drag = {x:event.clientX, scroll:canvas.scrollLeft};
canvas.setPointerCapture(event.pointerId);
canvas.classList.add('dragging');
});
canvas.addEventListener('pointermove', event => {
if (drag) canvas.scrollLeft = drag.scroll + drag.x-event.clientX;
});
const stop = () => { drag = null; canvas.classList.remove('dragging'); };
canvas.addEventListener('pointerup', stop);
canvas.addEventListener('pointercancel', stop);
canvas.addEventListener('lostpointercapture', stop);
}
centerVisibleEntries();
</script></body></html>examples/review.json{
"schema_version": 2,
"title": "Reject invalid scores. Keep valid output.",
"revision": "P1",
"mode": "compare",
"state": "awaiting-approval",
"snapshot": {
"repository": "Synthetic scoreboard fixture — example only",
"branch": "Local illustrative files; no application branch",
"basis": "Based on the bundled fixture and named source files. Current flow is statically inspected; proposed behavior is not implemented."
},
"understanding": "You want both display paths to reject negative scores, while keeping every nonnegative score formatted exactly as before. Reuse the existing formatter rather than adding another feature or validation layer.",
"requirements": [
{
"id": "R1",
"text": "Reject a negative home or away score in both display paths.",
"origin": "Synthetic request in PLAN.md; not an actual product change."
},
{
"id": "R2",
"text": "Keep zero and positive score formatting unchanged.",
"origin": "Explicit compatibility constraint in the synthetic request."
}
],
"boundaries": [
"No new features, UI redesign or score history.",
"No dependencies, extra configuration or automatic Git actions.",
"Only the shared score behavior, required caller, tests, scoped cleanup and existing documentation change."
],
"open_decisions": [],
"sources": [
{
"id": "S1",
"path": "PLAN.md",
"role": "plan"
},
{
"id": "S2",
"path": "TODO.md",
"role": "todo"
},
{
"id": "S3",
"path": "FINDINGS.md",
"role": "findings"
},
{
"id": "S4",
"path": "src/scoreboard.py",
"role": "code"
}
],
"evidence": [
{
"id": "E1",
"claim": "The web path uses an existing shared formatter without validation.",
"kind": "static",
"sources": [
"S4",
"S3"
],
"locator": "src/scoreboard.py: format_score, web_score",
"limits": "Static inspection only; no runtime result claimed."
},
{
"id": "E2",
"claim": "The player calls a separate formatter and bypasses the shared owner.",
"kind": "static",
"sources": [
"S4",
"S3"
],
"locator": "src/scoreboard.py: _legacy_score, player_score",
"limits": "Static fixture only; not a complete application call graph."
},
{
"id": "E3",
"claim": "Both supplied paths format scores but neither rejects negatives.",
"kind": "static",
"sources": [
"S4",
"S3"
],
"locator": "Both entrypoints and their directly called formatters",
"limits": "Only the tiny supplied fixture was inspected."
}
],
"tasks": [
{
"id": "T1",
"title": "Add behavior-level regression tests",
"kind": "prerequisite",
"reason": "Prove the requested rejection and preserve existing output.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1",
"E2",
"E3"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"tests/test_scoreboard.py"
],
"depends_on": []
},
{
"id": "T2",
"title": "Validate at the existing shared formatter",
"kind": "requested",
"reason": "The shared owner already formats the same score values.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"src/scoreboard.py: format_score"
],
"depends_on": [
"T1"
]
},
{
"id": "T3",
"title": "Route the player through shared validation and remove the old helper",
"kind": "prerequisite",
"reason": "If only the shared formatter changes, the player still bypasses it.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E2",
"E3"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"src/scoreboard.py: player_score, _legacy_score"
],
"depends_on": [
"T2"
]
},
{
"id": "T4",
"title": "Verify both paths and update existing usage documentation",
"kind": "prerequisite",
"reason": "Complete acceptance evidence and remove stale behavior descriptions.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1",
"E2",
"E3"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"tests/test_scoreboard.py",
"README.md"
],
"depends_on": [
"T3"
]
}
],
"acceptance": [
{
"id": "A1",
"text": "Negative scores are rejected through all affected entrypoints.",
"requirements": [
"R1"
],
"verification": "Add/run tests for negative home and negative away at shared, web and player entrypoints. Expect ValueError."
},
{
"id": "A2",
"text": "Nonnegative scores still render exactly as before.",
"requirements": [
"R2"
],
"verification": "Run zero/positive examples through both display paths and compare exact strings. Make sure that no reference to the old helper remains."
}
],
"graphs": [
{
"kind": "current",
"title": "Current: two separate paths",
"nodes": [
{
"id": "N1",
"label": "Web display",
"note": "Shared formatting path",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"x": 48,
"y": 152,
"width": 288,
"height": 96,
"shape": "start"
},
{
"id": "N2",
"label": "Shared formatter",
"note": "No negative-score check",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"x": 456,
"y": 152,
"width": 344,
"height": 96,
"shape": "action"
},
{
"id": "N3",
"label": "Score text",
"note": "Negatives are not rejected",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"x": 920,
"y": 152,
"width": 304,
"height": 96,
"shape": "result"
},
{
"id": "N4",
"label": "Player display",
"note": "Separate formatting path",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E2"
],
"tasks": [],
"x": 48,
"y": 424,
"width": 288,
"height": 96,
"shape": "start"
},
{
"id": "N5",
"label": "Legacy formatter",
"note": "Duplicates score formatting",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E2"
],
"tasks": [],
"x": 456,
"y": 424,
"width": 344,
"height": 96,
"shape": "action"
},
{
"id": "N6",
"label": "Score text",
"note": "Shared owner is bypassed",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E2"
],
"tasks": [],
"x": 920,
"y": 424,
"width": 304,
"height": 96,
"shape": "result"
}
],
"edges": [
{
"from": "N1",
"to": "N2",
"label": "calls",
"evidence": [
"E1"
],
"change": "unchanged",
"basis": "observed",
"tasks": [],
"points": [
[
336,
200
],
[
456,
200
]
],
"label_position": [
396,
172
]
},
{
"from": "N2",
"to": "N3",
"label": "text",
"evidence": [
"E1"
],
"change": "unchanged",
"basis": "observed",
"tasks": [],
"points": [
[
800,
200
],
[
920,
200
]
],
"label_position": [
860,
172
]
},
{
"from": "N4",
"to": "N5",
"label": "calls",
"evidence": [
"E2"
],
"change": "unchanged",
"basis": "observed",
"tasks": [],
"points": [
[
336,
472
],
[
456,
472
]
],
"label_position": [
396,
444
]
},
{
"from": "N5",
"to": "N6",
"label": "text",
"evidence": [
"E2"
],
"change": "unchanged",
"basis": "observed",
"tasks": [],
"points": [
[
800,
472
],
[
920,
472
]
],
"label_position": [
860,
444
]
}
],
"id": "current",
"purpose": "flow",
"tab": "Current flow",
"description": "Static inspection of the bundled fixture. Each path returns score text. Neither path rejects negative values.",
"width": 1280,
"height": 640
},
{
"id": "proposed",
"kind": "proposed",
"purpose": "flow",
"tab": "Feature flow",
"title": "Proposed: one check, two outcomes",
"description": "Both display paths use the existing formatter. Reject either negative input. Otherwise return exactly the existing score text. Proposed behavior, not a runtime observation.",
"width": 1280,
"height": 736,
"nodes": [
{
"id": "N1",
"label": "Both display paths",
"note": "Web and player",
"x": 464,
"y": 40,
"width": 352,
"height": 72,
"shape": "start",
"tasks": [
"T3"
],
"evidence": [
"E1",
"E2"
],
"basis": "proposed",
"change": "modified",
"highlight": false
},
{
"id": "N2",
"label": "Shared formatter",
"note": "One owner for both paths",
"x": 464,
"y": 160,
"width": 352,
"height": 88,
"shape": "action",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified",
"highlight": false
},
{
"id": "N3",
"label": "Either score\nnegative?",
"note": "",
"x": 512,
"y": 320,
"width": 256,
"height": 160,
"shape": "decision",
"tasks": [
"T2"
],
"evidence": [
"E1",
"E3"
],
"basis": "proposed",
"change": "modified",
"highlight": true
},
{
"id": "N4",
"label": "Reject invalid input",
"note": "Raise ValueError",
"x": 920,
"y": 352,
"width": 304,
"height": 96,
"shape": "result",
"tasks": [
"T2"
],
"evidence": [
"E1",
"E3"
],
"basis": "proposed",
"change": "added",
"highlight": true
},
{
"id": "N5",
"label": "Keep score formatting",
"note": "Zero and positive scores unchanged",
"x": 460,
"y": 576,
"width": 360,
"height": 88,
"shape": "result",
"tasks": [],
"evidence": [
"E1",
"E2"
],
"basis": "observed",
"change": "unchanged",
"highlight": false
}
],
"edges": [
{
"from": "N1",
"to": "N2",
"points": [
[
640,
112
],
[
640,
160
]
],
"label": "",
"tasks": [
"T3"
],
"evidence": [
"E1",
"E2"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "N2",
"to": "N3",
"points": [
[
640,
248
],
[
640,
320
]
],
"label": "",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "N3",
"to": "N4",
"points": [
[
768,
400
],
[
920,
400
]
],
"label": "Yes",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified",
"label_position": [
844,
372
]
},
{
"from": "N3",
"to": "N5",
"points": [
[
640,
480
],
[
640,
576
]
],
"label": "No",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified",
"label_position": [
688,
524
]
}
]
},
{
"id": "build",
"kind": "proposed",
"purpose": "delivery",
"tab": "Build plan",
"title": "Build plan: after approval only",
"description": "These are delivery dependencies, not the feature runtime. Approval must be recorded before T1 begins. No diagram action grants it.",
"width": 1280,
"height": 848,
"nodes": [
{
"id": "approval",
"label": "User approves this plan",
"note": "",
"x": 472,
"y": 40,
"width": 336,
"height": 64,
"shape": "start",
"tasks": [],
"evidence": [],
"basis": "proposed",
"change": "unchanged",
"highlight": true,
"precondition": "approval"
},
{
"id": "B1",
"label": "Add regression tests",
"note": "Invalid inputs and valid output",
"x": 432,
"y": 176,
"width": 416,
"height": 88,
"shape": "action",
"tasks": [
"T1"
],
"evidence": [
"E1",
"E2",
"E3"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T1"
},
{
"id": "B2",
"label": "Validate at the owner",
"note": "Use the existing formatter",
"x": 432,
"y": 328,
"width": 416,
"height": 88,
"shape": "action",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T2"
},
{
"id": "B3",
"label": "Route player; remove duplicate",
"note": "Replace the bypass, not the feature",
"x": 432,
"y": 480,
"width": 416,
"height": 96,
"shape": "action",
"tasks": [
"T3"
],
"evidence": [
"E2",
"E3"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T3"
},
{
"id": "B4",
"label": "Verify and update docs",
"note": "Check both paths and stale references",
"x": 432,
"y": 640,
"width": 416,
"height": 96,
"shape": "result",
"tasks": [
"T4"
],
"evidence": [
"E1",
"E2",
"E3"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T4"
}
],
"edges": [
{
"from": "approval",
"to": "B1",
"points": [
[
640,
104
],
[
640,
176
]
],
"label": "",
"tasks": [
"T1"
],
"evidence": [
"E1",
"E2",
"E3"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "B1",
"to": "B2",
"points": [
[
640,
264
],
[
640,
328
]
],
"label": "",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "B2",
"to": "B3",
"points": [
[
640,
416
],
[
640,
480
]
],
"label": "",
"tasks": [
"T3"
],
"evidence": [
"E2",
"E3"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "B3",
"to": "B4",
"points": [
[
640,
576
],
[
640,
640
]
],
"label": "",
"tasks": [
"T4"
],
"evidence": [
"E1",
"E2",
"E3"
],
"basis": "proposed",
"change": "modified"
}
]
}
],
"default_view": "proposed",
"comparison": [
{
"label": "Negative scores",
"current": "Both display paths format negative scores without rejecting them.",
"proposed": "Reject a negative home or away score through the shared formatter.",
"requirements": [
"R1"
],
"evidence": [
"E3"
],
"tasks": [
"T1",
"T2",
"T3",
"T4"
]
},
{
"label": "Valid scores",
"current": "Zero and positive scores are formatted as home - away.",
"proposed": "Keep the same text for every nonnegative score.",
"requirements": [
"R2"
],
"evidence": [
"E1",
"E2"
],
"tasks": [
"T1",
"T2",
"T3",
"T4"
]
},
{
"label": "Ownership",
"current": "Web and player use separate formatting functions.",
"proposed": "Use the existing shared owner and remove the redundant helper.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1",
"E2"
],
"tasks": [
"T3",
"T4"
]
}
]
}
examples/src/scoreboard.py"""Synthetic scoreboard used only to demonstrate a code review report."""
def format_score(home: int, away: int) -> str:
return f"{home} - {away}"
def web_score(home: int, away: int) -> str:
return format_score(home, away)
def _legacy_score(home: int, away: int) -> str:
return f"{home} - {away}"
def player_score(home: int, away: int) -> str:
return _legacy_score(home, away)
examples/STATUS.md# Synthetic legacy task — current record This is a fixture, not a real project's approval. It illustrates a legacy STATUS.md checkpoint and inline decisions. The earlier PLAN, TODO, FINDINGS, review.json and review.html remain historical fixture inputs. No application work has actually been run by this example. ## Current checkpoint - As recorded: 2026-10-29T12:00:00+00:00 (fixed synthetic example time). - Approval: P1 approved in fixture decision D2 below; both entrypoints and existing output format are covered. - Implementation: not started. The supplied scoreboard.py still has its original behavior. - Verification/acceptance: not run for the proposed change. Static inspection of the baseline is not runtime verification. - Next safe action: add regression coverage for either negative score, then perform the approved change. No additional interview is needed for unchanged fixture scope. - Reviewed flow: P1 in review.html/review.json; its pending badge records presentation, not this later synthetic checkpoint. ## Inline decisions and revision log - P0 / old-draft: proposed changing the web path alone. D1 superseded that incomplete proposal; no P0 HTML exists in this fixture and none should be invented. - P1 / review.html: both web and player callers must reject either negative score. D2 synthetic approval: "Proceed with P1: reject either negative score through both entrypoints; preserve nonnegative output. No dependencies or extra features." - Reason for the change: the player bypassed the shared formatting owner, so a web-only fix would leave the other caller unchanged. - No implementation or verification is claimed. A rerun or handoff would update this checkpoint, not create P2.
examples/task-overview.html<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-Ct/4BgYIGLXq783UVb4B4liFvGHnUs1jSYOGFzy3QsY='; style-src 'unsafe-inline'; img-src 'none'; connect-src 'none'; font-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">
<title>Reject invalid scores. Keep valid output.</title><style>:root {
--paper:#f5f5f5; --ink:#2d3142; --muted:#4f5d75; --rule:#cdd0d5;
--accent:#bf4520; --accent-tint:#f5e9e4;
--sans:'Geist','Inter','Segoe UI',system-ui,sans-serif;
--serif:'Instrument Serif',Georgia,'Times New Roman',serif;
--mono:'Geist Mono',ui-monospace,'Cascadia Mono',monospace;
color-scheme:light;
}
* { box-sizing:border-box; }
body { margin:0; background:var(--paper); color:var(--ink); font:15px/1.6 var(--sans); }
main { max-width:1392px; margin:auto; padding:36px 56px 48px; }
a { color:var(--ink); text-underline-offset:4px; }
a:hover { color:var(--accent); }
a:focus-visible,summary:focus-visible,.canvas:focus-visible { outline:2px solid var(--accent); outline-offset:5px; }
header { padding-bottom:24px; }
.mast { display:flex; justify-content:space-between; gap:20px; border-bottom:1px solid var(--rule); padding-bottom:14px; font:11px/1.5 var(--mono); letter-spacing:.1em; color:var(--muted); }
.mast strong { font-weight:500; color:var(--ink); }
h1 { font:400 clamp(38px,4vw,58px)/1.08 var(--serif); letter-spacing:-.035em; margin:25px 0 14px; max-width:1080px; }
h2 { font-size:20px; line-height:1.4; font-weight:600; margin:0 0 12px; }
.status { font:11px/1.6 var(--mono); letter-spacing:.04em; color:var(--accent); margin:0 0 14px; }
.lead { font-size:16px; color:var(--muted); margin:0; max-width:1040px; }
.muted,small { color:var(--muted); }
.review { position:relative; border-top:1px solid var(--ink); }
.sr-radio { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); clip-path:inset(50%); white-space:nowrap; }
.viewbar { display:flex; justify-content:space-between; align-items:center; gap:16px; border-bottom:1px solid var(--rule); min-height:60px; }
.view-tabs,.zoom-tabs { display:flex; align-items:center; gap:24px; flex-wrap:wrap; }
.view-tabs label,.zoom-tabs label { cursor:pointer; user-select:none; line-height:58px; color:var(--muted); border-bottom:2px solid transparent; font-size:14px; }
.view-tabs label { font-weight:550; }
.zoom-tabs { gap:14px; flex-shrink:0; }
.zoom-tabs label { font:11px/58px var(--mono); }
.figures { --scale:1; }
#zoom-125:checked~.figures { --scale:1.25; }
#zoom-150:checked~.figures { --scale:1.5; }
#zoom-fit:checked~.figures .map { width:100%; min-width:0; }
.panel { display:none; margin:0; }
.figure-head { display:flex; justify-content:space-between; align-items:baseline; gap:24px; padding:22px 0 4px; }
.figure-head h2 { font-size:18px; margin:0; }
.figure-head span { font:10px var(--mono); color:var(--muted); text-transform:uppercase; white-space:nowrap; }
.figure-description { margin:6px 0 12px; font-size:13px; color:var(--muted); max-width:1100px; }
.hint { margin:0 0 4px; font-size:11px; color:var(--muted); }
.canvas { max-width:100%; overflow-x:auto; overflow-y:hidden; scrollbar-width:thin; scrollbar-color:#9198a5 transparent; overscroll-behavior-x:contain; cursor:grab; }
.canvas.dragging { cursor:grabbing; user-select:none; }
.map { display:block; width:calc(var(--board-width) * var(--scale)); min-width:calc(var(--board-width) * var(--scale)); height:auto; }
.node-shape { fill:#fff; stroke:var(--ink); stroke-width:1.2; }
.start .node-shape,.result .node-shape { fill:#edf0f2; stroke:var(--muted); }
.focal .node-shape { fill:var(--accent-tint); stroke:var(--accent); stroke-width:1.4; }
.node.inferred .node-shape,.node.unknown .node-shape { stroke-dasharray:5 4; }
.connector { fill:none; stroke:var(--muted); stroke-width:1.4; }
.connector.inferred,.connector.unknown { stroke-dasharray:5 4; }
.map marker path { fill:var(--muted); }
.node-title { font:600 18px var(--sans); fill:var(--ink); }
.node-note { font:13px var(--sans); fill:var(--muted); }
.node-item { font:15px var(--sans); fill:var(--ink); }
.arrow-label rect { fill:var(--paper); }
.edge-label { font:10px var(--mono); letter-spacing:1px; fill:var(--muted); }
figcaption { font-size:12px; color:var(--muted); margin:12px 0 24px; }
.legend { display:flex; flex-wrap:wrap; align-items:center; gap:16px 32px; border-top:1px solid var(--rule); padding:16px 0 10px; }
.legend span { display:flex; align-items:center; gap:10px; }
.legend i { display:inline-block; width:26px; height:16px; border:1px solid var(--muted); background:#fff; }
.legend-start { border-radius:10px; }
.legend-action,.legend-group { border-radius:3px; }
.legend .legend-decision { width:16px; height:16px; transform:rotate(45deg); margin:0 5px; }
.legend .legend-focus { background:var(--accent-tint); border-color:var(--accent); border-radius:3px; }
.comparison { border-top:1px solid var(--ink); padding-top:24px; margin-top:8px; }
.comparison h2,.boundaries h2 { font:400 30px var(--serif); }
.table-scroll { max-width:100%; overflow-x:auto; }
table { border-collapse:collapse; width:100%; font-size:14px; text-align:left; }
th { font:10px var(--mono); letter-spacing:.08em; text-transform:uppercase; color:var(--muted); padding:14px 18px 12px 0; border-bottom:1px solid var(--rule); vertical-align:bottom; }
td { padding:16px 24px 16px 0; border-bottom:1px solid var(--rule); vertical-align:top; }
td:first-child { width:20%; }
td:last-child { padding-right:0; }
.comparison td:nth-child(2) { width:36%; color:var(--muted); }
small { display:block; font-size:12px; line-height:1.6; margin-top:5px; }
.boundaries { padding-top:24px; }
.boundaries ul { padding-left:20px; font-size:14px; }
.two-col { display:grid; grid-template-columns:1fr 1.2fr; gap:48px; margin:32px 0; }
.plain { list-style:none; padding:0; margin:0; }
.plain li { padding:13px 0; border-bottom:1px solid var(--rule); font-size:14px; }
.approval { border-top:1px solid var(--ink); padding:24px 0; display:grid; grid-template-columns:230px 1fr; gap:32px; }
.approval h2 { font:400 30px var(--serif); }
.approval p { margin:0 0 10px; font-size:14px; }
.warning { padding:18px 0; margin:0 0 20px; border-top:1px solid var(--accent); border-bottom:1px solid var(--rule); }
.warning h2 { color:var(--accent); font-size:17px; }
details { border-top:1px solid var(--rule); padding:17px 0; }
summary { font-size:14px; font-weight:550; cursor:pointer; list-style-position:inside; }
.appendix-body { padding-top:24px; }
.appendix-body section { margin-bottom:30px; }
.appendix table { min-width:840px; font-size:12px; }
.appendix td:first-child { width:auto; }
.appendix ol { font-size:12px; padding-left:22px; }
.appendix ol li { margin:10px 0; }
.evidence { padding:18px 0; border-top:1px solid var(--rule); }
.evidence strong { font-size:14px; }
.evidence p,code { font:12px/1.8 var(--mono); overflow-wrap:anywhere; }
.snapshot p { font-size:12px; }
footer { border-top:1px solid var(--rule); padding-top:14px; margin-top:28px; color:var(--muted); font:10px/1.7 var(--mono); }
li,tr,article { scroll-margin-top:24px; }
@media(max-width:800px) {
main { padding:22px 20px 36px; }
h1 { font-size:39px; }
.mast { font-size:9px; }
.viewbar { display:block; }
.view-tabs { gap:18px; }
.view-tabs label { font-size:12px; line-height:46px; }
.zoom-tabs { justify-content:flex-end; border-top:1px solid var(--rule); }
.zoom-tabs label { line-height:34px; }
.figure-head { display:block; }
.figure-head span { display:block; margin-top:8px; }
.comparison table { min-width:740px; }
.two-col,.approval { grid-template-columns:1fr; gap:20px; }
}
@media print {
main { max-width:none; padding:0; }
.viewbar,.hint { display:none!important; }
.panel { display:block!important; break-inside:avoid; }
.canvas { overflow:visible; }
.map { width:100%!important; min-width:0!important; }
.figure-head { margin-top:25px; }
.approval { break-inside:avoid; }
a { color:inherit; }
}
.task-checkpoint { border-top:1px solid var(--ink); padding:20px 0 24px; }
.task-checkpoint h2 { font:400 28px var(--serif); margin:0 0 6px; }
.checkpoint-asof,.checkpoint-limit,.flow-context { font-size:12px; color:var(--muted); overflow-wrap:anywhere; }
.checkpoint-asof { margin:0 0 18px; }
.checkpoint-axes { display:grid; grid-template-columns:1fr 1fr 1fr; gap:28px; margin:0 0 16px; }
.checkpoint-axes > div { min-width:0; }
.checkpoint-axes dt { font:10px/1.6 var(--mono); text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
.checkpoint-axes dd { margin:5px 0 0; font-size:14px; overflow-wrap:anywhere; }
.checkpoint-axes dd p { margin:5px 0 0; font-size:13px; color:var(--muted); }
.next-action { font-size:14px; border-top:1px solid var(--rule); padding-top:14px; margin:0; overflow-wrap:anywhere; }
.next-action strong { margin-right:12px; }
.checkpoint-limit { margin:12px 0 0; }
.flow-context { margin:0 0 14px; }
.revision-log { margin-bottom:12px; }
.revision-log > p { font-size:12px; color:var(--muted); }
.revision-log table { min-width:740px; }
.revision-log td { overflow-wrap:anywhere; }
@media(max-width:800px) { .checkpoint-axes { grid-template-columns:1fr; gap:16px; } }
@media print { .task-checkpoint { break-inside:avoid; } }
#view-proposed:checked~.figures #panel-proposed{display:block}
#view-proposed:checked~.viewbar label[for="view-proposed"]{border-color:var(--accent);color:var(--ink)}
#view-proposed:focus-visible~.viewbar label[for="view-proposed"]{outline:2px solid var(--accent);outline-offset:4px}
#view-current:checked~.figures #panel-current{display:block}
#view-current:checked~.viewbar label[for="view-current"]{border-color:var(--accent);color:var(--ink)}
#view-current:focus-visible~.viewbar label[for="view-current"]{outline:2px solid var(--accent);outline-offset:4px}
#view-build:checked~.figures #panel-build{display:block}
#view-build:checked~.viewbar label[for="view-build"]{border-color:var(--accent);color:var(--ink)}
#view-build:focus-visible~.viewbar label[for="view-build"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-100:checked~.viewbar label[for="zoom-100"]{border-color:var(--ink);color:var(--ink)}
#zoom-100:focus-visible~.viewbar label[for="zoom-100"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-125:checked~.viewbar label[for="zoom-125"]{border-color:var(--ink);color:var(--ink)}
#zoom-125:focus-visible~.viewbar label[for="zoom-125"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-150:checked~.viewbar label[for="zoom-150"]{border-color:var(--ink);color:var(--ink)}
#zoom-150:focus-visible~.viewbar label[for="zoom-150"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-fit:checked~.viewbar label[for="zoom-fit"]{border-color:var(--ink);color:var(--ink)}
#zoom-fit:focus-visible~.viewbar label[for="zoom-fit"]{outline:2px solid var(--accent);outline-offset:4px}</style></head><body><main>
<header><div class="mast"><strong>Synthetic scoreboard fixture — example only</strong><span>Plan P1</span></div>
<h1>Reject invalid scores. Keep valid output.</h1><p class="status awaiting-approval">SAVED TASK OVERVIEW. Read the dated checkpoint. This is not a new request for approval</p><p class="lead">Synthetic task overview: reject negative scores in both display paths. The later fixture approval is recorded, but implementation and verification have not started. The retained flow is the reviewed design, not proof of completed code.</p></header>
<section class="task-checkpoint" aria-labelledby="checkpoint-heading"><h2 id="checkpoint-heading">Current task checkpoint</h2><p class="checkpoint-asof">As recorded: 2026-10-29T12:00:00+00:00 — synthetic fixture · <a href="#s-S5">S5</a> , Current checkpoint; inline decision D2</p><dl class="checkpoint-axes"><div><dt>Approval</dt><dd><strong>Approved · P1</strong><p>D2 records the fixture approval for both callers; no extra features or dependencies.</p></dd></div><div><dt>Implementation</dt><dd><strong>Not started</strong><p>The supplied code still has the original behavior. Approval is not implementation.</p></dd></div><div><dt>Verification / acceptance</dt><dd><strong>Not run</strong><p>The proposed change has not been tested. Baseline inspection is not a passing result.</p></dd></div></dl><p class="next-action"><strong>Next safe action</strong> Add the agreed regression coverage, then implement the covered scope. Reuse P1; an agent handoff is not another plan revision.</p><p class="checkpoint-limit">Saved from the task records, not a live status check or proof of approval. Approval, completed code and acceptance are different states.</p></section><p class="flow-context">Flow reviewed for P1. The "Today" view shows the code before the change. The proposed view shows the reviewed scope. It does not prove that the work was built.</p><section class="review" aria-label="Flowchart review"><input class="sr-radio" type="radio" name="diagram-view" id="view-proposed" aria-label="Feature flow" aria-controls="panel-proposed" checked><input class="sr-radio" type="radio" name="diagram-view" id="view-current" aria-label="Current flow" aria-controls="panel-current" ><input class="sr-radio" type="radio" name="diagram-view" id="view-build" aria-label="Build plan" aria-controls="panel-build" ><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-100" aria-label="100% diagram size" checked><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-125" aria-label="125% diagram size" ><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-150" aria-label="150% diagram size" ><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-fit" aria-label="Fit diagram size" ><div class="viewbar"><div class="view-tabs" role="group" aria-label="Diagram views"><label for="view-proposed">Feature flow</label><label for="view-current">Current flow</label><label for="view-build">Build plan</label></div><div class="zoom-tabs" role="group" aria-label="Diagram size"><label for="zoom-100">100%</label><label for="zoom-125">125%</label><label for="zoom-150">150%</label><label for="zoom-fit">Fit</label></div></div><div class="figures"><figure class="panel" id="panel-proposed"><div class="figure-head"><h2>Proposed: one check, two outcomes</h2><span>proposed · flow</span></div><p class="figure-description">Both display paths use the existing formatter. Reject either negative input. Otherwise return exactly the existing score text. Proposed behavior, not a runtime observation.</p><p class="hint">Scroll sideways to explore. <a href="#node-8-proposed-N1">Go to start</a>. Fit is an optional overview.</p><div class="canvas" tabindex="0" role="region" aria-label="Feature flow diagram, scroll to explore" data-start-id="node-8-proposed-N1" style="--board-width:1280px"><svg class="map" xmlns="http://www.w3.org/2000/svg" width="1280" height="736" viewBox="0 0 1280 736" role="img" aria-labelledby="proposed-title proposed-desc"><title id="proposed-title">Proposed: one check, two outcomes</title><desc id="proposed-desc">Both display paths use the existing formatter. Reject either negative input. Otherwise return exactly the existing score text. Proposed behavior, not a runtime observation.</desc><defs><marker id="arrow-proposed" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs><path class="connector proposed" data-from="N1" data-to="N2" data-points="[[640, 112], [640, 160]]" d="M 640 112 L 640 160" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="N2" data-to="N3" data-points="[[640, 248], [640, 320]]" d="M 640 248 L 640 320" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="N3" data-to="N4" data-points="[[768, 400], [920, 400]]" d="M 768 400 L 920 400" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="N3" data-to="N5" data-points="[[640, 480], [640, 576]]" d="M 640 480 L 640 576" marker-end="url(#arrow-proposed)"/><g class="arrow-label"><rect x="825.5" y="357" width="37" height="22"/><text class="edge-label" text-anchor="middle" x="844" y="372">YES</text></g><g class="arrow-label"><rect x="673" y="509" width="30" height="22"/><text class="edge-label" text-anchor="middle" x="688" y="524">NO</text></g><g id="node-8-proposed-N1" class="node start proposed " data-id="N1" data-box="464,40,352,72"><title>Both display paths. proposed / modified. </title><rect class="node-shape" x="464" y="40" width="352" height="72" rx="20"/><text class="node-title" x="640" y="72" text-anchor="middle">Both display paths</text><text class="node-note" x="640" y="96" text-anchor="middle">Web and player</text></g><g id="node-8-proposed-N2" class="node action proposed " data-id="N2" data-box="464,160,352,88"><title>Shared formatter. proposed / modified. </title><rect class="node-shape" x="464" y="160" width="352" height="88" rx="6"/><text class="node-title" x="640" y="200" text-anchor="middle">Shared formatter</text><text class="node-note" x="640" y="224" text-anchor="middle">One owner for both paths</text></g><g id="node-8-proposed-N3" class="node decision proposed focal" data-id="N3" data-box="512,320,256,160"><title>Either score
negative?. proposed / modified. </title><polygon class="node-shape" points="640,320 768,400 640,480 512,400"/><text class="node-title" x="640" y="394" text-anchor="middle">Either score</text><text class="node-title" x="640" y="418" text-anchor="middle">negative?</text></g><g id="node-8-proposed-N4" class="node result proposed focal" data-id="N4" data-box="920,352,304,96"><title>Reject invalid input. proposed / added. </title><rect class="node-shape" x="920" y="352" width="304" height="96" rx="20"/><text class="node-title" x="1072" y="396" text-anchor="middle">Reject invalid input</text><text class="node-note" x="1072" y="420" text-anchor="middle">Raise ValueError</text></g><g id="node-8-proposed-N5" class="node result observed " data-id="N5" data-box="460,576,360,88"><title>Keep score formatting. observed / unchanged. </title><rect class="node-shape" x="460" y="576" width="360" height="88" rx="20"/><text class="node-title" x="640" y="616" text-anchor="middle">Keep score formatting</text><text class="node-note" x="640" y="640" text-anchor="middle">Zero and positive scores unchanged</text></g></svg></div><figcaption><div class="legend"><span><i class="legend-start" aria-hidden="true"></i>Start / result</span><span><i class="legend-action" aria-hidden="true"></i>Action</span><span><i class="legend-decision" aria-hidden="true"></i>Decision</span><span><i class="legend-focus" aria-hidden="true"></i>Focus of the change</span></div><span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. Proposed is not implemented.</span></figcaption></figure><figure class="panel" id="panel-current"><div class="figure-head"><h2>Current: two separate paths</h2><span>current · flow</span></div><p class="figure-description">Static inspection of the bundled fixture. Each path returns score text. Neither path rejects negative values.</p><p class="hint">Scroll sideways to explore. <a href="#node-7-current-N1">Go to start</a>. Fit is an optional overview.</p><div class="canvas" tabindex="0" role="region" aria-label="Current flow diagram, scroll to explore" data-start-id="node-7-current-N1" style="--board-width:1280px"><svg class="map" xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-labelledby="current-title current-desc"><title id="current-title">Current: two separate paths</title><desc id="current-desc">Static inspection of the bundled fixture. Each path returns score text. Neither path rejects negative values.</desc><defs><marker id="arrow-current" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs><path class="connector observed" data-from="N1" data-to="N2" data-points="[[336, 200], [456, 200]]" d="M 336 200 L 456 200" marker-end="url(#arrow-current)"/><path class="connector observed" data-from="N2" data-to="N3" data-points="[[800, 200], [920, 200]]" d="M 800 200 L 920 200" marker-end="url(#arrow-current)"/><path class="connector observed" data-from="N4" data-to="N5" data-points="[[336, 472], [456, 472]]" d="M 336 472 L 456 472" marker-end="url(#arrow-current)"/><path class="connector observed" data-from="N5" data-to="N6" data-points="[[800, 472], [920, 472]]" d="M 800 472 L 920 472" marker-end="url(#arrow-current)"/><g class="arrow-label"><rect x="370.5" y="157" width="51" height="22"/><text class="edge-label" text-anchor="middle" x="396" y="172">CALLS</text></g><g class="arrow-label"><rect x="838" y="157" width="44" height="22"/><text class="edge-label" text-anchor="middle" x="860" y="172">TEXT</text></g><g class="arrow-label"><rect x="370.5" y="429" width="51" height="22"/><text class="edge-label" text-anchor="middle" x="396" y="444">CALLS</text></g><g class="arrow-label"><rect x="838" y="429" width="44" height="22"/><text class="edge-label" text-anchor="middle" x="860" y="444">TEXT</text></g><g id="node-7-current-N1" class="node start observed " data-id="N1" data-box="48,152,288,96"><title>Web display. observed / unchanged. </title><rect class="node-shape" x="48" y="152" width="288" height="96" rx="20"/><text class="node-title" x="192" y="196" text-anchor="middle">Web display</text><text class="node-note" x="192" y="220" text-anchor="middle">Shared formatting path</text></g><g id="node-7-current-N2" class="node action observed " data-id="N2" data-box="456,152,344,96"><title>Shared formatter. observed / unchanged. </title><rect class="node-shape" x="456" y="152" width="344" height="96" rx="6"/><text class="node-title" x="628" y="196" text-anchor="middle">Shared formatter</text><text class="node-note" x="628" y="220" text-anchor="middle">No negative-score check</text></g><g id="node-7-current-N3" class="node result observed " data-id="N3" data-box="920,152,304,96"><title>Score text. observed / unchanged. </title><rect class="node-shape" x="920" y="152" width="304" height="96" rx="20"/><text class="node-title" x="1072" y="196" text-anchor="middle">Score text</text><text class="node-note" x="1072" y="220" text-anchor="middle">Negatives are not rejected</text></g><g id="node-7-current-N4" class="node start observed " data-id="N4" data-box="48,424,288,96"><title>Player display. observed / unchanged. </title><rect class="node-shape" x="48" y="424" width="288" height="96" rx="20"/><text class="node-title" x="192" y="468" text-anchor="middle">Player display</text><text class="node-note" x="192" y="492" text-anchor="middle">Separate formatting path</text></g><g id="node-7-current-N5" class="node action observed " data-id="N5" data-box="456,424,344,96"><title>Legacy formatter. observed / unchanged. </title><rect class="node-shape" x="456" y="424" width="344" height="96" rx="6"/><text class="node-title" x="628" y="468" text-anchor="middle">Legacy formatter</text><text class="node-note" x="628" y="492" text-anchor="middle">Duplicates score formatting</text></g><g id="node-7-current-N6" class="node result observed " data-id="N6" data-box="920,424,304,96"><title>Score text. observed / unchanged. </title><rect class="node-shape" x="920" y="424" width="304" height="96" rx="20"/><text class="node-title" x="1072" y="468" text-anchor="middle">Score text</text><text class="node-note" x="1072" y="492" text-anchor="middle">Shared owner is bypassed</text></g></svg></div><figcaption><div class="legend"><span><i class="legend-start" aria-hidden="true"></i>Start / result</span><span><i class="legend-action" aria-hidden="true"></i>Action</span></div><span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. Proposed is not implemented.</span></figcaption></figure><figure class="panel" id="panel-build"><div class="figure-head"><h2>Build plan: after approval only</h2><span>proposed · delivery</span></div><p class="figure-description">These are delivery dependencies, not the feature runtime. Approval must be recorded before T1 begins. No diagram action grants it.</p><p class="hint">Scroll sideways to explore. <a href="#node-5-build-approval">Go to start</a>. Fit is an optional overview.</p><div class="canvas" tabindex="0" role="region" aria-label="Build plan diagram, scroll to explore" data-start-id="node-5-build-approval" style="--board-width:1280px"><svg class="map" xmlns="http://www.w3.org/2000/svg" width="1280" height="848" viewBox="0 0 1280 848" role="img" aria-labelledby="build-title build-desc"><title id="build-title">Build plan: after approval only</title><desc id="build-desc">These are delivery dependencies, not the feature runtime. Approval must be recorded before T1 begins. No diagram action grants it.</desc><defs><marker id="arrow-build" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs><path class="connector proposed" data-from="approval" data-to="B1" data-points="[[640, 104], [640, 176]]" d="M 640 104 L 640 176" marker-end="url(#arrow-build)"/><path class="connector proposed" data-from="B1" data-to="B2" data-points="[[640, 264], [640, 328]]" d="M 640 264 L 640 328" marker-end="url(#arrow-build)"/><path class="connector proposed" data-from="B2" data-to="B3" data-points="[[640, 416], [640, 480]]" d="M 640 416 L 640 480" marker-end="url(#arrow-build)"/><path class="connector proposed" data-from="B3" data-to="B4" data-points="[[640, 576], [640, 640]]" d="M 640 576 L 640 640" marker-end="url(#arrow-build)"/><g id="node-5-build-approval" class="node start proposed focal" data-id="approval" data-box="472,40,336,64"><title>User approves this plan. proposed / unchanged. </title><rect class="node-shape" x="472" y="40" width="336" height="64" rx="20"/><text class="node-title" x="640" y="78" text-anchor="middle">User approves this plan</text></g><g id="node-5-build-B1" class="node action proposed " data-id="B1" data-box="432,176,416,88"><title>Add regression tests. proposed / modified. </title><rect class="node-shape" x="432" y="176" width="416" height="88" rx="6"/><text class="node-title" x="640" y="216" text-anchor="middle">Add regression tests</text><text class="node-note" x="640" y="240" text-anchor="middle">Invalid inputs and valid output</text></g><g id="node-5-build-B2" class="node action proposed " data-id="B2" data-box="432,328,416,88"><title>Validate at the owner. proposed / modified. </title><rect class="node-shape" x="432" y="328" width="416" height="88" rx="6"/><text class="node-title" x="640" y="368" text-anchor="middle">Validate at the owner</text><text class="node-note" x="640" y="392" text-anchor="middle">Use the existing formatter</text></g><g id="node-5-build-B3" class="node action proposed " data-id="B3" data-box="432,480,416,96"><title>Route player; remove duplicate. proposed / modified. </title><rect class="node-shape" x="432" y="480" width="416" height="96" rx="6"/><text class="node-title" x="640" y="524" text-anchor="middle">Route player; remove duplicate</text><text class="node-note" x="640" y="548" text-anchor="middle">Replace the bypass, not the feature</text></g><g id="node-5-build-B4" class="node result proposed " data-id="B4" data-box="432,640,416,96"><title>Verify and update docs. proposed / modified. </title><rect class="node-shape" x="432" y="640" width="416" height="96" rx="20"/><text class="node-title" x="640" y="684" text-anchor="middle">Verify and update docs</text><text class="node-note" x="640" y="708" text-anchor="middle">Check both paths and stale references</text></g></svg></div><figcaption><div class="legend"><span><i class="legend-start" aria-hidden="true"></i>Start / result</span><span><i class="legend-action" aria-hidden="true"></i>Action</span><span><i class="legend-focus" aria-hidden="true"></i>Focus of the change</span></div><span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. Proposed is not implemented.</span></figcaption></figure></div></section>
<section class="comparison"><h2>What changes</h2><div class="table-scroll"><table><thead><tr><th>Area</th><th>Baseline before the selected change</th><th>Reviewed scope (the checkpoint shows progress)</th></tr></thead><tbody><tr><td><strong>Negative scores</strong><small><a href="#r-R1">R1</a></small></td><td>Both display paths format negative scores without rejecting them.<small><a href="#e-E3">E3</a></small></td><td>Reject a negative home or away score through the shared formatter.<small><a href="#t-T1">T1</a> <a href="#t-T2">T2</a> <a href="#t-T3">T3</a> <a href="#t-T4">T4</a></small></td></tr><tr><td><strong>Valid scores</strong><small><a href="#r-R2">R2</a></small></td><td>Zero and positive scores are formatted as home - away.<small><a href="#e-E1">E1</a> <a href="#e-E2">E2</a></small></td><td>Keep the same text for every nonnegative score.<small><a href="#t-T1">T1</a> <a href="#t-T2">T2</a> <a href="#t-T3">T3</a> <a href="#t-T4">T4</a></small></td></tr><tr><td><strong>Ownership</strong><small><a href="#r-R1">R1</a> <a href="#r-R2">R2</a></small></td><td>Web and player use separate formatting functions.<small><a href="#e-E1">E1</a> <a href="#e-E2">E2</a></small></td><td>Use the existing shared owner and remove the redundant helper.<small><a href="#t-T3">T3</a> <a href="#t-T4">T4</a></small></td></tr></tbody></table></div></section><section class="boundaries"><h2>What will not change</h2><ul><li>No new features, UI redesign or score history.</li><li>No dependencies, extra configuration or automatic Git actions.</li><li>Only the shared score behavior, required caller, tests, scoped cleanup and existing documentation change.</li></ul></section>
<div class="two-col"><section><h2>Tasks in the reviewed scope</h2><ul class="plain"><li><a href="#t-T1">T1</a> Add behavior-level regression tests<small class="task-outcome">Not started: STATUS.md checkpoint: implementation has not started.</small><small>Depends on: No prior task</small></li><li><a href="#t-T2">T2</a> Validate at the existing shared formatter<small class="task-outcome">Not started: STATUS.md checkpoint: implementation has not started.</small><small>Depends on: T1</small></li><li><a href="#t-T3">T3</a> Route the player through shared validation and remove the old helper<small class="task-outcome">Not started: STATUS.md checkpoint: implementation has not started.</small><small>Depends on: T2</small></li><li><a href="#t-T4">T4</a> Verify both paths and update existing usage documentation<small class="task-outcome">Not started: STATUS.md checkpoint: implementation has not started.</small><small>Depends on: T3</small></li></ul></section>
<section><h2>How completion will be checked</h2><ul class="plain"><li id="a-A1"><strong>A1: Negative scores are rejected through all affected entrypoints.</strong><small>Verify: Add/run tests for negative home and negative away at shared, web and player entrypoints. Expect ValueError. · <a href="#r-R1">R1</a></small></li><li id="a-A2"><strong>A2: Nonnegative scores still render exactly as before.</strong><small>Verify: Run zero/positive examples through both display paths and compare exact strings. Make sure that no reference to the old helper remains. · <a href="#r-R2">R2</a></small></li></ul></section></div>
<section class="approval"><h2>The task record decides.</h2><div><p>This saved overview does not approve work, reopen an approval or confirm that work is finished. An older review can still say "waiting for approval". Use the dated checkpoint and the real approval record.</p><p>There is no approve button and no connection to the agent.</p></div></section><details class="revision-log" id="revision-history"><summary>Revision history: what changed and why</summary><p>Changed agreements, not a log of every retry. Review aliases are preserved as recorded; a missing original is not reconstructed.</p><div class="table-scroll"><table><thead><tr><th>Revision / review</th><th>Change</th><th>Reason</th><th>Recorded outcome / source</th></tr></thead><tbody><tr><td><strong>P0</strong><small>old-draft — no retained HTML in this fixture</small></td><td>Initially considered the web path only.</td><td>The first proposal did not cover the separate player path.</td><td>Superseded<small><a href="#s-S5">S5</a>, D1; superseded by the P1 scope</small></td></tr><tr><td><strong>P1</strong><small>review.html / review.json</small></td><td>Cover both display paths; preserve existing nonnegative output.</td><td>Both callers must apply the same requested rule.</td><td>Approved<small><a href="#s-S5">S5</a>, D2 — synthetic approval, not actual authorization</small></td></tr></tbody></table></div></details><details class="appendix" id="technical-details"><summary>Technical evidence and task details</summary><div class="appendix-body">
<section><h2>Requested outcome</h2><ul class="plain"><li id="r-R1"><strong>R1.</strong> Reject a negative home or away score in both display paths.<small>Source: Synthetic request in PLAN.md; not an actual product change.</small></li><li id="r-R2"><strong>R2.</strong> Keep zero and positive score formatting unchanged.<small>Source: Explicit compatibility constraint in the synthetic request.</small></li></ul></section>
<section><h2>Changes and mapped tasks</h2><div class="table-scroll"><table><thead><tr><th>Task</th><th>Work and affected areas</th><th>Traceability</th><th>Depends on</th></tr></thead><tbody><tr id="t-T1"><td><strong>T1</strong></td><td><strong>Add behavior-level regression tests</strong><small>prerequisite: Prove the requested rejection and preserve existing output.<br>Areas: tests/test_scoreboard.py</small><small class="task-outcome">Not started: STATUS.md checkpoint: implementation has not started.</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td>—</td></tr><tr id="t-T2"><td><strong>T2</strong></td><td><strong>Validate at the existing shared formatter</strong><small>requested: The shared owner already formats the same score values.<br>Areas: src/scoreboard.py: format_score</small><small class="task-outcome">Not started: STATUS.md checkpoint: implementation has not started.</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E1">E1</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td><a href="#t-T1">T1</a></td></tr><tr id="t-T3"><td><strong>T3</strong></td><td><strong>Route the player through shared validation and remove the old helper</strong><small>prerequisite: If only the shared formatter changes, the player still bypasses it.<br>Areas: src/scoreboard.py: player_score, _legacy_score</small><small class="task-outcome">Not started: STATUS.md checkpoint: implementation has not started.</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E2">E2</a> <a href="#e-E3">E3</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td><a href="#t-T2">T2</a></td></tr><tr id="t-T4"><td><strong>T4</strong></td><td><strong>Verify both paths and update existing usage documentation</strong><small>prerequisite: Complete acceptance evidence and remove stale behavior descriptions.<br>Areas: tests/test_scoreboard.py, README.md</small><small class="task-outcome">Not started: STATUS.md checkpoint: implementation has not started.</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td><a href="#t-T3">T3</a></td></tr></tbody></table></div></section>
<section><h2>Evidence and limits</h2><article id="e-E1" class="evidence"><strong>E1 · static: The web path uses an existing shared formatter without validation.</strong><p>src/scoreboard.py: format_score, web_score</p><small>Sources: <a href="#s-S4">S4</a> <a href="#s-S3">S3</a><br>Limits: Static inspection only; no runtime result claimed.</small></article><article id="e-E2" class="evidence"><strong>E2 · static: The player calls a separate formatter and bypasses the shared owner.</strong><p>src/scoreboard.py: _legacy_score, player_score</p><small>Sources: <a href="#s-S4">S4</a> <a href="#s-S3">S3</a><br>Limits: Static fixture only; not a complete application call graph.</small></article><article id="e-E3" class="evidence"><strong>E3 · static: Both supplied paths format scores but neither rejects negatives.</strong><p>Both entrypoints and their directly called formatters</p><small>Sources: <a href="#s-S4">S4</a> <a href="#s-S3">S3</a><br>Limits: Only the tiny supplied fixture was inspected.</small></article></section><details><summary>Current flow: steps and links</summary><ol><li><strong>N1: Web display</strong>. Shared formatting path (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N2: Shared formatter</strong>. No negative-score check (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N3: Score text</strong>. Negatives are not rejected (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N4: Player display</strong>. Separate formatting path (observed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li><li><strong>N5: Legacy formatter</strong>. Duplicates score formatting (observed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li><li><strong>N6: Score text</strong>. Shared owner is bypassed (observed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li><li><strong>N1 → N2</strong>: calls (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N2 → N3</strong>: text (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N4 → N5</strong>: calls (observed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li><li><strong>N5 → N6</strong>: text (observed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li></ol></details><details><summary>Feature flow: steps and links</summary><ol><li><strong>N1: Both display paths</strong>. Web and player (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a>; tasks <a href="#t-T3">T3</a>.</li><li><strong>N2: Shared formatter</strong>. One owner for both paths (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N3: Either score
negative?</strong>. (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N4: Reject invalid input</strong>. Raise ValueError (proposed, added) Evidence <a href="#e-E1">E1</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N5: Keep score formatting</strong>. Zero and positive scores unchanged (observed, unchanged) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a>; tasks —.</li><li><strong>N1 → N2</strong>: continues (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a>; tasks <a href="#t-T3">T3</a>.</li><li><strong>N2 → N3</strong>: continues (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N3 → N4</strong>: Yes (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N3 → N5</strong>: No (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li></ol></details><details><summary>Build plan: steps and links</summary><ol><li><strong>approval: User approves this plan</strong>. (proposed, unchanged) Evidence —; tasks —.</li><li><strong>B1: Add regression tests</strong>. Invalid inputs and valid output (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T1">T1</a>.</li><li><strong>B2: Validate at the owner</strong>. Use the existing formatter (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>B3: Route player; remove duplicate</strong>. Replace the bypass, not the feature (proposed, modified) Evidence <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T3">T3</a>.</li><li><strong>B4: Verify and update docs</strong>. Check both paths and stale references (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T4">T4</a>.</li><li><strong>approval → B1</strong>: continues (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T1">T1</a>.</li><li><strong>B1 → B2</strong>: continues (proposed, modified) Evidence <a href="#e-E1">E1</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>B2 → B3</strong>: continues (proposed, modified) Evidence <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T3">T3</a>.</li><li><strong>B3 → B4</strong>: continues (proposed, modified) Evidence <a href="#e-E1">E1</a> <a href="#e-E2">E2</a> <a href="#e-E3">E3</a>; tasks <a href="#t-T4">T4</a>.</li></ol></details>
<details class="snapshot"><summary>Inspected snapshot and supplied-source fingerprints</summary><p>Synthetic scoreboard fixture — example only · Local illustrative files; no application branch</p>
<p>Based on the bundled fixture and named source files. Current flow is statically inspected; proposed behavior is not implemented.</p><p>Generated 2026-11-03T12:20:57.976177+00:00. Hashes cover only these supplied files, including local changes. They do not prove complete code coverage or correctness.</p><div class="table-scroll"><table><thead><tr><th>ID</th><th>Source</th><th>Role</th><th>SHA-256 prefix</th></tr></thead><tbody><tr id="s-S1"><td>S1</td><td><code>PLAN.md</code></td><td>plan</td><td><code>afeb79818ad8cae6…</code></td></tr><tr id="s-S2"><td>S2</td><td><code>TODO.md</code></td><td>todo</td><td><code>34453899a983f495…</code></td></tr><tr id="s-S3"><td>S3</td><td><code>FINDINGS.md</code></td><td>findings</td><td><code>6aca6ff40deb18d1…</code></td></tr><tr id="s-S4"><td>S4</td><td><code>src/scoreboard.py</code></td><td>code</td><td><code>7e1df39d2884f0b3…</code></td></tr><tr id="s-S5"><td>S5</td><td><code>STATUS.md</code></td><td>checkpoint</td><td><code>b1aad19a80868c9a…</code></td></tr><tr id="s-report-input"><td>report-input</td><td><code>task-overview.json</code></td><td>projection</td><td><code>3fa6858126ff7f0f…</code></td></tr></tbody></table></div></details>
</div></details><p class="muted">System-font mode. No network assets. A saved projection, not a live repository map.</p><footer>Local HTML + SVG · no external assets, uploads or approval actions · saved review snapshot; progress belongs in the existing task records</footer>
</main><script type="application/json" id="report-manifest">{"schema_version": 1, "tool_version": "2.8.0-rc.1", "layout_version": 2, "style": "workflow-editorial-v1", "revision": "P1", "report_kind": "task-overview", "generated_utc": "2026-11-03T12:20:57.976177+00:00", "sources": [{"id": "S1", "role": "plan", "path": "PLAN.md", "sha256": "afeb79818ad8cae67f80ea9aef77754391f84eefc29f32bccc500c0ac06217ef"}, {"id": "S2", "role": "todo", "path": "TODO.md", "sha256": "34453899a983f495c86dc398a7f0218439aeb5c6de0c129cb0f48f0f69c0e1cd"}, {"id": "S3", "role": "findings", "path": "FINDINGS.md", "sha256": "6aca6ff40deb18d14d610448bc4fab4aa94442b94b8e53f464a9212d32d6262e"}, {"id": "S4", "role": "code", "path": "src/scoreboard.py", "sha256": "7e1df39d2884f0b337a0939370cacce6f6c462c6bfa3c10c42c81f339889aed9"}, {"id": "S5", "role": "checkpoint", "path": "STATUS.md", "sha256": "b1aad19a80868c9ad71b4caf2e2a5da31b8f5fb8d04f8e5cec42c8f3eaf91183"}, {"id": "report-input", "role": "projection", "path": "task-overview.json", "sha256": "3fa6858126ff7f0f64f0a1d6013cfb59ebee8e459f566181f0c4dcaffd9cb0a5"}]}</script><script>'use strict';
// Native radio inputs and scrolling work without this progressive enhancement.
function revealAnchor() {
const id = location.hash.slice(1);
const target = document.getElementById(id);
if (!target) return;
let parent = target.parentElement;
while (parent) {
if (parent.tagName === 'DETAILS') parent.open = true;
parent = parent.parentElement;
}
target.scrollIntoView({block:target.closest('.canvas') ? 'nearest' : 'start', inline:'center'});
}
for (const anchor of document.querySelectorAll('a[href^="#"]')) {
anchor.addEventListener('click', () => {
const target = document.getElementById(anchor.getAttribute('href').slice(1));
let parent = target;
while (parent) {
if (parent.tagName === 'DETAILS') parent.open = true;
parent = parent.parentElement;
}
});
}
window.addEventListener('hashchange', revealAnchor);
if (location.hash) revealAnchor();
function centerEntry(canvas) {
if (!canvas.clientWidth) return;
const node = document.getElementById(canvas.dataset.startId);
if (!node) return;
const bounds = node.getBoundingClientRect();
const viewport = canvas.getBoundingClientRect();
canvas.scrollLeft += bounds.x + bounds.width/2 - viewport.x - viewport.width/2;
}
function centerVisibleEntries() {
for (const canvas of document.querySelectorAll('.canvas')) centerEntry(canvas);
}
for (const input of document.querySelectorAll('.review input[type="radio"]')) {
input.addEventListener('change', centerVisibleEntries);
}
const resizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(entries => {
for (const entry of entries) centerEntry(entry.target);
}) : null;
for (const canvas of document.querySelectorAll('.canvas')) {
// On narrow screens show the entry node, not the diagram's empty left margin.
resizeObserver?.observe(canvas);
let drag = null;
canvas.addEventListener('keydown', event => {
const choices = ['100', '125', '150'];
const selected = document.querySelector('input[name="diagram-zoom"]:checked');
let index = choices.indexOf(selected.id.slice(5));
let next = null;
if (event.key === '+' || event.key === '=') next = choices[Math.min(2, index+1)];
else if (event.key === '-') next = choices[Math.max(0, index-1)];
else if (event.key.toLowerCase() === 'f') next = 'fit';
else if (event.key === '0') next = '100';
if (next) {
event.preventDefault();
const input = document.getElementById('zoom-'+next);
input.checked = true;
input.dispatchEvent(new Event('change'));
}
});
canvas.addEventListener('pointerdown', event => {
if (event.pointerType !== 'mouse' || event.button !== 0) return;
drag = {x:event.clientX, scroll:canvas.scrollLeft};
canvas.setPointerCapture(event.pointerId);
canvas.classList.add('dragging');
});
canvas.addEventListener('pointermove', event => {
if (drag) canvas.scrollLeft = drag.scroll + drag.x-event.clientX;
});
const stop = () => { drag = null; canvas.classList.remove('dragging'); };
canvas.addEventListener('pointerup', stop);
canvas.addEventListener('pointercancel', stop);
canvas.addEventListener('lostpointercapture', stop);
}
centerVisibleEntries();
</script></body></html>examples/task-overview.json{
"schema_version": 2,
"title": "Reject invalid scores. Keep valid output.",
"revision": "P1",
"mode": "compare",
"state": "awaiting-approval",
"snapshot": {
"repository": "Synthetic scoreboard fixture — example only",
"branch": "Local illustrative files; no application branch",
"basis": "Based on the bundled fixture and named source files. Current flow is statically inspected; proposed behavior is not implemented."
},
"understanding": "Synthetic task overview: reject negative scores in both display paths. The later fixture approval is recorded, but implementation and verification have not started. The retained flow is the reviewed design, not proof of completed code.",
"requirements": [
{
"id": "R1",
"text": "Reject a negative home or away score in both display paths.",
"origin": "Synthetic request in PLAN.md; not an actual product change."
},
{
"id": "R2",
"text": "Keep zero and positive score formatting unchanged.",
"origin": "Explicit compatibility constraint in the synthetic request."
}
],
"boundaries": [
"No new features, UI redesign or score history.",
"No dependencies, extra configuration or automatic Git actions.",
"Only the shared score behavior, required caller, tests, scoped cleanup and existing documentation change."
],
"open_decisions": [],
"sources": [
{
"id": "S1",
"path": "PLAN.md",
"role": "plan"
},
{
"id": "S2",
"path": "TODO.md",
"role": "todo"
},
{
"id": "S3",
"path": "FINDINGS.md",
"role": "findings"
},
{
"id": "S4",
"path": "src/scoreboard.py",
"role": "code"
},
{
"id": "S5",
"path": "STATUS.md",
"role": "checkpoint"
}
],
"evidence": [
{
"id": "E1",
"claim": "The web path uses an existing shared formatter without validation.",
"kind": "static",
"sources": [
"S4",
"S3"
],
"locator": "src/scoreboard.py: format_score, web_score",
"limits": "Static inspection only; no runtime result claimed."
},
{
"id": "E2",
"claim": "The player calls a separate formatter and bypasses the shared owner.",
"kind": "static",
"sources": [
"S4",
"S3"
],
"locator": "src/scoreboard.py: _legacy_score, player_score",
"limits": "Static fixture only; not a complete application call graph."
},
{
"id": "E3",
"claim": "Both supplied paths format scores but neither rejects negatives.",
"kind": "static",
"sources": [
"S4",
"S3"
],
"locator": "Both entrypoints and their directly called formatters",
"limits": "Only the tiny supplied fixture was inspected."
}
],
"tasks": [
{
"id": "T1",
"title": "Add behavior-level regression tests",
"kind": "prerequisite",
"reason": "Prove the requested rejection and preserve existing output.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1",
"E2",
"E3"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"tests/test_scoreboard.py"
],
"depends_on": [],
"outcome": "planned",
"outcome_note": "STATUS.md checkpoint: implementation has not started."
},
{
"id": "T2",
"title": "Validate at the existing shared formatter",
"kind": "requested",
"reason": "The shared owner already formats the same score values.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"src/scoreboard.py: format_score"
],
"depends_on": [
"T1"
],
"outcome": "planned",
"outcome_note": "STATUS.md checkpoint: implementation has not started."
},
{
"id": "T3",
"title": "Route the player through shared validation and remove the old helper",
"kind": "prerequisite",
"reason": "If only the shared formatter changes, the player still bypasses it.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E2",
"E3"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"src/scoreboard.py: player_score, _legacy_score"
],
"depends_on": [
"T2"
],
"outcome": "planned",
"outcome_note": "STATUS.md checkpoint: implementation has not started."
},
{
"id": "T4",
"title": "Verify both paths and update existing usage documentation",
"kind": "prerequisite",
"reason": "Complete acceptance evidence and remove stale behavior descriptions.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1",
"E2",
"E3"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"tests/test_scoreboard.py",
"README.md"
],
"depends_on": [
"T3"
],
"outcome": "planned",
"outcome_note": "STATUS.md checkpoint: implementation has not started."
}
],
"acceptance": [
{
"id": "A1",
"text": "Negative scores are rejected through all affected entrypoints.",
"requirements": [
"R1"
],
"verification": "Add/run tests for negative home and negative away at shared, web and player entrypoints. Expect ValueError."
},
{
"id": "A2",
"text": "Nonnegative scores still render exactly as before.",
"requirements": [
"R2"
],
"verification": "Run zero/positive examples through both display paths and compare exact strings. Make sure that no reference to the old helper remains."
}
],
"graphs": [
{
"kind": "current",
"title": "Current: two separate paths",
"nodes": [
{
"id": "N1",
"label": "Web display",
"note": "Shared formatting path",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"x": 48,
"y": 152,
"width": 288,
"height": 96,
"shape": "start"
},
{
"id": "N2",
"label": "Shared formatter",
"note": "No negative-score check",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"x": 456,
"y": 152,
"width": 344,
"height": 96,
"shape": "action"
},
{
"id": "N3",
"label": "Score text",
"note": "Negatives are not rejected",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"x": 920,
"y": 152,
"width": 304,
"height": 96,
"shape": "result"
},
{
"id": "N4",
"label": "Player display",
"note": "Separate formatting path",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E2"
],
"tasks": [],
"x": 48,
"y": 424,
"width": 288,
"height": 96,
"shape": "start"
},
{
"id": "N5",
"label": "Legacy formatter",
"note": "Duplicates score formatting",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E2"
],
"tasks": [],
"x": 456,
"y": 424,
"width": 344,
"height": 96,
"shape": "action"
},
{
"id": "N6",
"label": "Score text",
"note": "Shared owner is bypassed",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E2"
],
"tasks": [],
"x": 920,
"y": 424,
"width": 304,
"height": 96,
"shape": "result"
}
],
"edges": [
{
"from": "N1",
"to": "N2",
"label": "calls",
"evidence": [
"E1"
],
"change": "unchanged",
"basis": "observed",
"tasks": [],
"points": [
[
336,
200
],
[
456,
200
]
],
"label_position": [
396,
172
]
},
{
"from": "N2",
"to": "N3",
"label": "text",
"evidence": [
"E1"
],
"change": "unchanged",
"basis": "observed",
"tasks": [],
"points": [
[
800,
200
],
[
920,
200
]
],
"label_position": [
860,
172
]
},
{
"from": "N4",
"to": "N5",
"label": "calls",
"evidence": [
"E2"
],
"change": "unchanged",
"basis": "observed",
"tasks": [],
"points": [
[
336,
472
],
[
456,
472
]
],
"label_position": [
396,
444
]
},
{
"from": "N5",
"to": "N6",
"label": "text",
"evidence": [
"E2"
],
"change": "unchanged",
"basis": "observed",
"tasks": [],
"points": [
[
800,
472
],
[
920,
472
]
],
"label_position": [
860,
444
]
}
],
"id": "current",
"purpose": "flow",
"tab": "Current flow",
"description": "Static inspection of the bundled fixture. Each path returns score text. Neither path rejects negative values.",
"width": 1280,
"height": 640
},
{
"id": "proposed",
"kind": "proposed",
"purpose": "flow",
"tab": "Feature flow",
"title": "Proposed: one check, two outcomes",
"description": "Both display paths use the existing formatter. Reject either negative input. Otherwise return exactly the existing score text. Proposed behavior, not a runtime observation.",
"width": 1280,
"height": 736,
"nodes": [
{
"id": "N1",
"label": "Both display paths",
"note": "Web and player",
"x": 464,
"y": 40,
"width": 352,
"height": 72,
"shape": "start",
"tasks": [
"T3"
],
"evidence": [
"E1",
"E2"
],
"basis": "proposed",
"change": "modified",
"highlight": false
},
{
"id": "N2",
"label": "Shared formatter",
"note": "One owner for both paths",
"x": 464,
"y": 160,
"width": 352,
"height": 88,
"shape": "action",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified",
"highlight": false
},
{
"id": "N3",
"label": "Either score\nnegative?",
"note": "",
"x": 512,
"y": 320,
"width": 256,
"height": 160,
"shape": "decision",
"tasks": [
"T2"
],
"evidence": [
"E1",
"E3"
],
"basis": "proposed",
"change": "modified",
"highlight": true
},
{
"id": "N4",
"label": "Reject invalid input",
"note": "Raise ValueError",
"x": 920,
"y": 352,
"width": 304,
"height": 96,
"shape": "result",
"tasks": [
"T2"
],
"evidence": [
"E1",
"E3"
],
"basis": "proposed",
"change": "added",
"highlight": true
},
{
"id": "N5",
"label": "Keep score formatting",
"note": "Zero and positive scores unchanged",
"x": 460,
"y": 576,
"width": 360,
"height": 88,
"shape": "result",
"tasks": [],
"evidence": [
"E1",
"E2"
],
"basis": "observed",
"change": "unchanged",
"highlight": false
}
],
"edges": [
{
"from": "N1",
"to": "N2",
"points": [
[
640,
112
],
[
640,
160
]
],
"label": "",
"tasks": [
"T3"
],
"evidence": [
"E1",
"E2"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "N2",
"to": "N3",
"points": [
[
640,
248
],
[
640,
320
]
],
"label": "",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "N3",
"to": "N4",
"points": [
[
768,
400
],
[
920,
400
]
],
"label": "Yes",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified",
"label_position": [
844,
372
]
},
{
"from": "N3",
"to": "N5",
"points": [
[
640,
480
],
[
640,
576
]
],
"label": "No",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified",
"label_position": [
688,
524
]
}
]
},
{
"id": "build",
"kind": "proposed",
"purpose": "delivery",
"tab": "Build plan",
"title": "Build plan: after approval only",
"description": "These are delivery dependencies, not the feature runtime. Approval must be recorded before T1 begins. No diagram action grants it.",
"width": 1280,
"height": 848,
"nodes": [
{
"id": "approval",
"label": "User approves this plan",
"note": "",
"x": 472,
"y": 40,
"width": 336,
"height": 64,
"shape": "start",
"tasks": [],
"evidence": [],
"basis": "proposed",
"change": "unchanged",
"highlight": true,
"precondition": "approval"
},
{
"id": "B1",
"label": "Add regression tests",
"note": "Invalid inputs and valid output",
"x": 432,
"y": 176,
"width": 416,
"height": 88,
"shape": "action",
"tasks": [
"T1"
],
"evidence": [
"E1",
"E2",
"E3"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T1"
},
{
"id": "B2",
"label": "Validate at the owner",
"note": "Use the existing formatter",
"x": 432,
"y": 328,
"width": 416,
"height": 88,
"shape": "action",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T2"
},
{
"id": "B3",
"label": "Route player; remove duplicate",
"note": "Replace the bypass, not the feature",
"x": 432,
"y": 480,
"width": 416,
"height": 96,
"shape": "action",
"tasks": [
"T3"
],
"evidence": [
"E2",
"E3"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T3"
},
{
"id": "B4",
"label": "Verify and update docs",
"note": "Check both paths and stale references",
"x": 432,
"y": 640,
"width": 416,
"height": 96,
"shape": "result",
"tasks": [
"T4"
],
"evidence": [
"E1",
"E2",
"E3"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T4"
}
],
"edges": [
{
"from": "approval",
"to": "B1",
"points": [
[
640,
104
],
[
640,
176
]
],
"label": "",
"tasks": [
"T1"
],
"evidence": [
"E1",
"E2",
"E3"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "B1",
"to": "B2",
"points": [
[
640,
264
],
[
640,
328
]
],
"label": "",
"tasks": [
"T2"
],
"evidence": [
"E1"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "B2",
"to": "B3",
"points": [
[
640,
416
],
[
640,
480
]
],
"label": "",
"tasks": [
"T3"
],
"evidence": [
"E2",
"E3"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "B3",
"to": "B4",
"points": [
[
640,
576
],
[
640,
640
]
],
"label": "",
"tasks": [
"T4"
],
"evidence": [
"E1",
"E2",
"E3"
],
"basis": "proposed",
"change": "modified"
}
]
}
],
"default_view": "proposed",
"comparison": [
{
"label": "Negative scores",
"current": "Both display paths format negative scores without rejecting them.",
"proposed": "Reject a negative home or away score through the shared formatter.",
"requirements": [
"R1"
],
"evidence": [
"E3"
],
"tasks": [
"T1",
"T2",
"T3",
"T4"
]
},
{
"label": "Valid scores",
"current": "Zero and positive scores are formatted as home - away.",
"proposed": "Keep the same text for every nonnegative score.",
"requirements": [
"R2"
],
"evidence": [
"E1",
"E2"
],
"tasks": [
"T1",
"T2",
"T3",
"T4"
]
},
{
"label": "Ownership",
"current": "Web and player use separate formatting functions.",
"proposed": "Use the existing shared owner and remove the redundant helper.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1",
"E2"
],
"tasks": [
"T3",
"T4"
]
}
],
"report_kind": "task-overview",
"checkpoint": {
"as_of": "2026-10-29T12:00:00+00:00 — synthetic fixture",
"source": "S5",
"locator": "Current checkpoint; inline decision D2",
"approval": {
"status": "approved",
"revision": "P1",
"summary": "D2 records the fixture approval for both callers; no extra features or dependencies."
},
"implementation": {
"status": "not-started",
"summary": "The supplied code still has the original behavior. Approval is not implementation."
},
"verification": {
"status": "not-run",
"summary": "The proposed change has not been tested. Baseline inspection is not a passing result."
},
"next_action": "Add the agreed regression coverage, then implement the covered scope. Reuse P1; an agent handoff is not another plan revision."
},
"revision_log": [
{
"revision": "P0",
"review": "old-draft — no retained HTML in this fixture",
"change": "Initially considered the web path only.",
"reason": "The first proposal did not cover the separate player path.",
"status": "superseded",
"source": "S5",
"locator": "D1; superseded by the P1 scope"
},
{
"revision": "P1",
"review": "review.html / review.json",
"change": "Cover both display paths; preserve existing nonnegative output.",
"reason": "Both callers must apply the same requested rule.",
"status": "approved",
"source": "S5",
"locator": "D2 — synthetic approval, not actual authorization"
}
]
}
examples/TODO.md# Synthetic example tasks — prepared, not executed - [ ] T1: Add behavioral regression coverage for R1/R2; evidence E1/E2/E3; acceptance A1/A2; no dependencies. - [ ] T2: Validate scores at format_score for R1/R2; E1; A1/A2; depends T1. - [ ] T3: Route player through existing owner and remove _legacy_score for R1/R2; E2/E3; A1/A2; depends T2. - [ ] T4: Update the README usage note and run final checks for R1/R2; E1/E2/E3; A1/A2; depends T3.
examples/ui-review/README.md# Full report example — original shell, optional screen flow `review-r2.html` is generated by the bundled renderer from `review-r2.json`. It retains the supplied rules-audit R2 draft's substantive report data and layout. The original immutable HTML/JSON remain under docs/plans/2026-10-30-rules-audit-design-kit. The **UI screens** tab is a fictional notification-settings illustration, explicitly outside the R2 rules-audit application scope. The rule-audit task has no new application screen. These screen sketches demonstrate the newly requested report format; they are not Altinstar or Cheflabels components and are not approved appearance. Existing historical claims/status in the R2 source report are not a fresh inspection of either project. Its recorded installation/activation work was pending in the uploaded task. Do not infer current host state or implementation consent from this sample. Preview authority: the owner requested a full report sample with UI appearance shown inside the existing report. This authorises this illustrative report only, not a production theme, component, route or product feature. Design alternatives remain unselected. The original editorial header, Today/Proposed flow, comparison, tasks, acceptance and evidence are not replaced. Screens show Edit → Saving → Saved, plus Failure → Retry. The same renderer can show actual approved-kit PNG captures when supplied. From the package root: ```bash python -X utf8 skills/codebase-map/scripts/render_report.py --root . --input skills/codebase-map/examples/ui-review/review-r2.json --output /path/to/new-report.html --open ``` Read UI-FLOW.md for the complete optional contract. The sample has no application network calls, mutable approval or executable app UI. ## rc.7 compatibility note The included rc.6 HTML files are retained historical demonstrations. Their schematic/plain-preview and design-authoring discussion does not grant current application permission. The D14 rule in rule-maker/rules/design.md (existing styles only) supersedes those modes for new work. New approval-ready application reports use actual kit references, captured states and capture input fingerprints. Do not update the old HTML to imply approval; use UI-FLOW.md for current input fields.
examples/ui-review/review-r2.html<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-YndMZv6yWmKjZPrLFg9fwPIzifD6yGUbESh4tYyfEeU='; style-src 'unsafe-inline'; img-src 'none'; connect-src 'none'; font-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">
<title>Shared rules load in every ordinary session, both projects</title><style>:root {
--paper:#f5f5f5; --ink:#2d3142; --muted:#4f5d75; --rule:#cdd0d5;
--accent:#bf4520; --accent-tint:#f5e9e4;
--sans:'Geist','Inter','Segoe UI',system-ui,sans-serif;
--serif:'Instrument Serif',Georgia,'Times New Roman',serif;
--mono:'Geist Mono',ui-monospace,'Cascadia Mono',monospace;
color-scheme:light;
}
* { box-sizing:border-box; }
body { margin:0; background:var(--paper); color:var(--ink); font:15px/1.6 var(--sans); }
main { max-width:1392px; margin:auto; padding:36px 56px 48px; }
a { color:var(--ink); text-underline-offset:4px; }
a:hover { color:var(--accent); }
a:focus-visible,summary:focus-visible,.canvas:focus-visible { outline:2px solid var(--accent); outline-offset:5px; }
header { padding-bottom:24px; }
.mast { display:flex; justify-content:space-between; gap:20px; border-bottom:1px solid var(--rule); padding-bottom:14px; font:11px/1.5 var(--mono); letter-spacing:.1em; color:var(--muted); }
.mast strong { font-weight:500; color:var(--ink); }
h1 { font:400 clamp(38px,4vw,58px)/1.08 var(--serif); letter-spacing:-.035em; margin:25px 0 14px; max-width:1080px; }
h2 { font-size:20px; line-height:1.4; font-weight:600; margin:0 0 12px; }
.status { font:11px/1.6 var(--mono); letter-spacing:.04em; color:var(--accent); margin:0 0 14px; }
.lead { font-size:16px; color:var(--muted); margin:0; max-width:1040px; }
.muted,small { color:var(--muted); }
.review { position:relative; border-top:1px solid var(--ink); }
.sr-radio { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); clip-path:inset(50%); white-space:nowrap; }
.viewbar { display:flex; justify-content:space-between; align-items:center; gap:16px; border-bottom:1px solid var(--rule); min-height:60px; }
.view-tabs,.zoom-tabs { display:flex; align-items:center; gap:24px; flex-wrap:wrap; }
.view-tabs label,.zoom-tabs label { cursor:pointer; user-select:none; line-height:58px; color:var(--muted); border-bottom:2px solid transparent; font-size:14px; }
.view-tabs label { font-weight:550; }
.zoom-tabs { gap:14px; flex-shrink:0; }
.zoom-tabs label { font:11px/58px var(--mono); }
.figures { --scale:1; }
#zoom-125:checked~.figures { --scale:1.25; }
#zoom-150:checked~.figures { --scale:1.5; }
#zoom-fit:checked~.figures .map { width:100%; min-width:0; }
.panel { display:none; margin:0; }
.figure-head { display:flex; justify-content:space-between; align-items:baseline; gap:24px; padding:22px 0 4px; }
.figure-head h2 { font-size:18px; margin:0; }
.figure-head span { font:10px var(--mono); color:var(--muted); text-transform:uppercase; white-space:nowrap; }
.figure-description { margin:6px 0 12px; font-size:13px; color:var(--muted); max-width:1100px; }
.hint { margin:0 0 4px; font-size:11px; color:var(--muted); }
.canvas { max-width:100%; overflow-x:auto; overflow-y:hidden; scrollbar-width:thin; scrollbar-color:#9198a5 transparent; overscroll-behavior-x:contain; cursor:grab; }
.canvas.dragging { cursor:grabbing; user-select:none; }
.map { display:block; width:calc(var(--board-width) * var(--scale)); min-width:calc(var(--board-width) * var(--scale)); height:auto; }
.node-shape { fill:#fff; stroke:var(--ink); stroke-width:1.2; }
.start .node-shape,.result .node-shape { fill:#edf0f2; stroke:var(--muted); }
.focal .node-shape { fill:var(--accent-tint); stroke:var(--accent); stroke-width:1.4; }
.node.inferred .node-shape,.node.unknown .node-shape { stroke-dasharray:5 4; }
.connector { fill:none; stroke:var(--muted); stroke-width:1.4; }
.connector.inferred,.connector.unknown { stroke-dasharray:5 4; }
.map marker path { fill:var(--muted); }
.node-title { font:600 18px var(--sans); fill:var(--ink); }
.node-note { font:13px var(--sans); fill:var(--muted); }
.node-item { font:15px var(--sans); fill:var(--ink); }
.arrow-label rect { fill:var(--paper); }
.edge-label { font:10px var(--mono); letter-spacing:1px; fill:var(--muted); }
figcaption { font-size:12px; color:var(--muted); margin:12px 0 24px; }
.legend { display:flex; flex-wrap:wrap; align-items:center; gap:16px 32px; border-top:1px solid var(--rule); padding:16px 0 10px; }
.legend span { display:flex; align-items:center; gap:10px; }
.legend i { display:inline-block; width:26px; height:16px; border:1px solid var(--muted); background:#fff; }
.legend-start { border-radius:10px; }
.legend-action,.legend-group { border-radius:3px; }
.legend .legend-decision { width:16px; height:16px; transform:rotate(45deg); margin:0 5px; }
.legend .legend-focus { background:var(--accent-tint); border-color:var(--accent); border-radius:3px; }
.comparison { border-top:1px solid var(--ink); padding-top:24px; margin-top:8px; }
.comparison h2,.boundaries h2 { font:400 30px var(--serif); }
.table-scroll { max-width:100%; overflow-x:auto; }
table { border-collapse:collapse; width:100%; font-size:14px; text-align:left; }
th { font:10px var(--mono); letter-spacing:.08em; text-transform:uppercase; color:var(--muted); padding:14px 18px 12px 0; border-bottom:1px solid var(--rule); vertical-align:bottom; }
td { padding:16px 24px 16px 0; border-bottom:1px solid var(--rule); vertical-align:top; }
td:first-child { width:20%; }
td:last-child { padding-right:0; }
.comparison td:nth-child(2) { width:36%; color:var(--muted); }
small { display:block; font-size:12px; line-height:1.6; margin-top:5px; }
.boundaries { padding-top:24px; }
.boundaries ul { padding-left:20px; font-size:14px; }
.two-col { display:grid; grid-template-columns:1fr 1.2fr; gap:48px; margin:32px 0; }
.plain { list-style:none; padding:0; margin:0; }
.plain li { padding:13px 0; border-bottom:1px solid var(--rule); font-size:14px; }
.approval { border-top:1px solid var(--ink); padding:24px 0; display:grid; grid-template-columns:230px 1fr; gap:32px; }
.approval h2 { font:400 30px var(--serif); }
.approval p { margin:0 0 10px; font-size:14px; }
.warning { padding:18px 0; margin:0 0 20px; border-top:1px solid var(--accent); border-bottom:1px solid var(--rule); }
.warning h2 { color:var(--accent); font-size:17px; }
details { border-top:1px solid var(--rule); padding:17px 0; }
summary { font-size:14px; font-weight:550; cursor:pointer; list-style-position:inside; }
.appendix-body { padding-top:24px; }
.appendix-body section { margin-bottom:30px; }
.appendix table { min-width:840px; font-size:12px; }
.appendix td:first-child { width:auto; }
.appendix ol { font-size:12px; padding-left:22px; }
.appendix ol li { margin:10px 0; }
.evidence { padding:18px 0; border-top:1px solid var(--rule); }
.evidence strong { font-size:14px; }
.evidence p,code { font:12px/1.8 var(--mono); overflow-wrap:anywhere; }
.snapshot p { font-size:12px; }
footer { border-top:1px solid var(--rule); padding-top:14px; margin-top:28px; color:var(--muted); font:10px/1.7 var(--mono); }
li,tr,article { scroll-margin-top:24px; }
@media(max-width:800px) {
main { padding:22px 20px 36px; }
h1 { font-size:39px; }
.mast { font-size:9px; }
.viewbar { display:block; }
.view-tabs { gap:18px; }
.view-tabs label { font-size:12px; line-height:46px; }
.zoom-tabs { justify-content:flex-end; border-top:1px solid var(--rule); }
.zoom-tabs label { line-height:34px; }
.figure-head { display:block; }
.figure-head span { display:block; margin-top:8px; }
.comparison table { min-width:740px; }
.two-col,.approval { grid-template-columns:1fr; gap:20px; }
}
@media print {
main { max-width:none; padding:0; }
.viewbar,.hint { display:none!important; }
.panel { display:block!important; break-inside:avoid; }
.canvas { overflow:visible; }
.map { width:100%!important; min-width:0!important; }
.figure-head { margin-top:25px; }
.approval { break-inside:avoid; }
a { color:inherit; }
}
.task-checkpoint { border-top:1px solid var(--ink); padding:20px 0 24px; }
.task-checkpoint h2 { font:400 28px var(--serif); margin:0 0 6px; }
.checkpoint-asof,.checkpoint-limit,.flow-context { font-size:12px; color:var(--muted); overflow-wrap:anywhere; }
.checkpoint-asof { margin:0 0 18px; }
.checkpoint-axes { display:grid; grid-template-columns:1fr 1fr 1fr; gap:28px; margin:0 0 16px; }
.checkpoint-axes > div { min-width:0; }
.checkpoint-axes dt { font:10px/1.6 var(--mono); text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
.checkpoint-axes dd { margin:5px 0 0; font-size:14px; overflow-wrap:anywhere; }
.checkpoint-axes dd p { margin:5px 0 0; font-size:13px; color:var(--muted); }
.next-action { font-size:14px; border-top:1px solid var(--rule); padding-top:14px; margin:0; overflow-wrap:anywhere; }
.next-action strong { margin-right:12px; }
.checkpoint-limit { margin:12px 0 0; }
.flow-context { margin:0 0 14px; }
.revision-log { margin-bottom:12px; }
.revision-log > p { font-size:12px; color:var(--muted); }
.revision-log table { min-width:740px; }
.revision-log td { overflow-wrap:anywhere; }
@media(max-width:800px) { .checkpoint-axes { grid-template-columns:1fr; gap:16px; } }
@media print { .task-checkpoint { break-inside:avoid; } }
#view-proposed:checked~.figures #panel-proposed{display:block}
#view-proposed:checked~.viewbar label[for="view-proposed"]{border-color:var(--accent);color:var(--ink)}
#view-proposed:focus-visible~.viewbar label[for="view-proposed"]{outline:2px solid var(--accent);outline-offset:4px}
#view-current:checked~.figures #panel-current{display:block}
#view-current:checked~.viewbar label[for="view-current"]{border-color:var(--accent);color:var(--ink)}
#view-current:focus-visible~.viewbar label[for="view-current"]{outline:2px solid var(--accent);outline-offset:4px}
#view-ui-preview:checked~.figures #panel-ui-preview{display:block}
#view-ui-preview:checked~.viewbar label[for="view-ui-preview"]{border-color:var(--accent);color:var(--ink)}
#view-ui-preview:focus-visible~.viewbar label[for="view-ui-preview"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-100:checked~.viewbar label[for="zoom-100"]{border-color:var(--ink);color:var(--ink)}
#zoom-100:focus-visible~.viewbar label[for="zoom-100"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-125:checked~.viewbar label[for="zoom-125"]{border-color:var(--ink);color:var(--ink)}
#zoom-125:focus-visible~.viewbar label[for="zoom-125"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-150:checked~.viewbar label[for="zoom-150"]{border-color:var(--ink);color:var(--ink)}
#zoom-150:focus-visible~.viewbar label[for="zoom-150"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-fit:checked~.viewbar label[for="zoom-fit"]{border-color:var(--ink);color:var(--ink)}
#zoom-fit:focus-visible~.viewbar label[for="zoom-fit"]{outline:2px solid var(--accent);outline-offset:4px}
/* Optional screen-flow view. Existing editorial shell and application styles are not changed. */
.ui-review .ui-notice { border-left:2px solid var(--accent); padding:10px 16px; font-size:13px; }
.ui-review .ui-authority { border-top:1px solid var(--rule); border-bottom:1px solid var(--rule); padding:12px 0; margin:16px 0; font-size:13px; }
.ui-review .ui-authority p { margin:4px 0; }
.ui-scaled { width:calc(var(--ui-width) * var(--scale)); height:calc(var(--ui-height) * var(--scale)); position:relative; }
.ui-board { width:var(--ui-width); height:var(--ui-height); position:relative; transform:scale(var(--scale)); transform-origin:top left; }
.ui-screen-node { position:absolute; margin:0; }
.ui-routes { position:absolute; inset:0; pointer-events:none; }
.ui-routes marker path { fill:var(--muted); }
.ui-screen { display:block; max-width:100%; height:auto; }
.ui-screen-paper { fill:#fff; stroke:var(--rule); stroke-width:1.25; }
.ui-screen-caption { font:600 14px var(--sans); fill:var(--ink); }
.ui-divider { stroke:var(--rule); stroke-width:1; }
.ui-control-heading { font:600 17px var(--sans); fill:var(--ink); }
.ui-control-text { font:14px var(--sans); fill:var(--ink); }
.ui-control-small { font:12px var(--sans); fill:var(--muted); }
.ui-checkbox,.ui-field { fill:#fff; stroke:var(--muted); }
.ui-button { fill:#f5f5f5; stroke:var(--muted); }
.ui-button.ui-disabled { fill:#eee; stroke:#a4a8b0; }
.ui-check { fill:none; stroke:var(--ink); stroke-width:1.7; }
.ui-message-rule { stroke:var(--muted); stroke-width:2; }
.ui-marker { fill:var(--accent-tint); stroke:var(--accent); }
.ui-marker-text { fill:var(--accent); font:10px var(--mono); }
.ui-footnote { fill:var(--muted); font:10px var(--mono); }
.ui-inspect { position:absolute; right:12px; top:10px; font:10px/28px var(--mono); background:#fff; padding:0 4px; }
.ui-decisions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:0 32px; margin:24px 0; }
.ui-decisions > h2 { grid-column:1/-1; }
.ui-decision { border-top:1px solid var(--rule); padding:16px 0; font-size:13px; }
.ui-decision h3 { margin:0 0 8px; font-size:15px; }
.ui-decision p { margin:6px 0; }
.ui-screen-detail .ui-screen { width:400px; margin:20px 0; }
.ui-screen-detail p,.ui-evidence { font-size:13px; }
.ui-review .ui-inspect:focus-visible,.ui-screen a:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
@media(max-width:800px) { .ui-decisions { grid-template-columns:1fr; } }
@media print { .ui-scaled { width:100%; height:auto; } .ui-board { transform:none; width:100%; height:auto; display:grid; grid-template-columns:1fr 1fr; gap:20px; }
.ui-screen-node { position:static; width:100%!important; height:auto!important; } .ui-routes,.ui-inspect { display:none; } .ui-authority { break-inside:avoid; } }
.ui-capture-detail { display:block; max-width:100%; height:auto; margin:20px 0; }
</style></head><body><main>
<header><div class="mast"><strong>workflow-axa package + installed runtime + Altinstar and Cheflabels rule files</strong><span>Plan R2-draft</span></div>
<h1>Shared rules load in every ordinary session, both projects</h1><p class="status draft">DRAFT. Decisions or review are still open</p><p class="lead">You want one central set of shared rules that ordinary Claude and Codex sessions in both projects pick up without slash commands, while each project keeps only its own knowledge. Today only the artifact policy is activated globally, and the installed copy fails its own integrity check because of line endings. The draft fixes the copy, activates the shared core for both hosts, adds one shared TypeScript rule and a few project lines, and changes no application code.</p></header>
<section class="warning"><h2>Open decisions. The plan is not ready to approve</h2><ul><li>Historical R2 draft reproduced for report-format review. This is not a fresh audit or installation status. The separate UI screen example adds no application scope.</li><li>F1 method: keep raw-byte integrity and reinstall LF bytes (drafted) or a text-normalised manifest; decided after independent review.</li><li>Altinstar AGENTS.md:105 one-line comment rule versus the owner brief.</li><li>Global ~/.claude/CLAUDE.md mandatory design vocabulary versus the owner brief.</li><li>Number and credit budget of live fresh-session acceptance runs.</li><li>Independent Astra review has not run yet.</li></ul></section><section class="warning"><h2>Evidence limits</h2><ul><li>Depends on the model matching a skill description; not traced.</li><li>Inferred from missing global block; not traced.</li><li>Not traced.</li></ul></section><section class="review" aria-label="Flowchart review"><input class="sr-radio" type="radio" name="diagram-view" id="view-proposed" aria-label="Proposed" aria-controls="panel-proposed" checked><input class="sr-radio" type="radio" name="diagram-view" id="view-current" aria-label="Today" aria-controls="panel-current" ><input class="sr-radio" type="radio" name="diagram-view" id="view-ui-preview" aria-label="UI screens" aria-controls="panel-ui-preview" ><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-100" aria-label="100% diagram size" checked><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-125" aria-label="125% diagram size" ><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-150" aria-label="150% diagram size" ><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-fit" aria-label="Fit diagram size" ><div class="viewbar"><div class="view-tabs" role="group" aria-label="Diagram views"><label for="view-proposed">Proposed</label><label for="view-current">Today</label><label for="view-ui-preview">UI screens</label></div><div class="zoom-tabs" role="group" aria-label="Diagram size"><label for="zoom-100">100%</label><label for="zoom-125">125%</label><label for="zoom-150">150%</label><label for="zoom-fit">Fit</label></div></div><div class="figures"><figure class="panel" id="panel-proposed"><div class="figure-head"><h2>Proposed: shared core first, modules on demand</h2><span>proposed · flow</span></div><p class="figure-description">Arrows show what loads as an ordinary session starts. Claude imports START and CORE; Codex is instructed to read them (guidance, not guaranteed). Draft awaiting review.</p><p class="hint">Scroll sideways to explore. <a href="#node-8-proposed-P1">Go to start</a>. Fit is an optional overview.</p><div class="canvas" tabindex="0" role="region" aria-label="Proposed diagram, scroll to explore" data-start-id="node-8-proposed-P1" style="--board-width:1440px"><svg class="map" xmlns="http://www.w3.org/2000/svg" width="1440" height="464" viewBox="0 0 1440 464" role="img" aria-labelledby="proposed-title proposed-desc"><title id="proposed-title">Proposed: shared core first, modules on demand</title><desc id="proposed-desc">Arrows show what loads as an ordinary session starts. Claude imports START and CORE; Codex is instructed to read them (guidance, not guaranteed). Draft awaiting review.</desc><defs><marker id="arrow-proposed" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs><path class="connector proposed" data-from="P1" data-to="P2" data-points="[[272, 144], [320, 144]]" d="M 272 144 L 320 144" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="P2" data-to="P3" data-points="[[544, 144], [592, 144]]" d="M 544 144 L 592 144" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="P3" data-to="P4" data-points="[[816, 144], [864, 144]]" d="M 816 144 L 864 144" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="P4" data-to="P5" data-points="[[992, 224], [992, 320]]" d="M 992 224 L 992 320" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="P4" data-to="P6" data-points="[[1120, 144], [1200, 144]]" d="M 1120 144 L 1200 144" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="P5" data-to="P6" data-points="[[1120, 368], [1304, 368], [1304, 192]]" d="M 1120 368 L 1296 368 Q 1304 368 1304 360 L 1304 192" marker-end="url(#arrow-proposed)"/><g class="arrow-label"><rect x="1009.5" y="261" width="37" height="22"/><text class="edge-label" text-anchor="middle" x="1028" y="276">YES</text></g><g class="arrow-label"><rect x="1145" y="113" width="30" height="22"/><text class="edge-label" text-anchor="middle" x="1160" y="128">NO</text></g><g id="node-8-proposed-P1" class="node start observed " data-id="P1" data-box="48,96,224,96"><title>Ordinary session. observed / unchanged. </title><rect class="node-shape" x="48" y="96" width="224" height="96" rx="20"/><text class="node-title" x="160" y="140" text-anchor="middle">Ordinary session</text><text class="node-note" x="160" y="164" text-anchor="middle">No slash command</text></g><g id="node-8-proposed-P2" class="node action proposed focal" data-id="P2" data-box="320,96,224,96"><title>Shared core block. proposed / added. </title><rect class="node-shape" x="320" y="96" width="224" height="96" rx="6"/><text class="node-title" x="432" y="140" text-anchor="middle">Shared core block</text><text class="node-note" x="432" y="164" text-anchor="middle">START + CORE</text></g><g id="node-8-proposed-P3" class="node action proposed " data-id="P3" data-box="592,96,224,96"><title>Project rules. proposed / modified. </title><rect class="node-shape" x="592" y="96" width="224" height="96" rx="6"/><text class="node-title" x="704" y="140" text-anchor="middle">Project rules</text><text class="node-note" x="704" y="164" text-anchor="middle">+ pointer, UI mode</text></g><g id="node-8-proposed-P4" class="node decision proposed " data-id="P4" data-box="864,64,256,160"><title>Framework
or UI work?. proposed / unchanged. </title><polygon class="node-shape" points="992,64 1120,144 992,224 864,144"/><text class="node-title" x="992" y="138" text-anchor="middle">Framework</text><text class="node-title" x="992" y="162" text-anchor="middle">or UI work?</text></g><g id="node-8-proposed-P5" class="node action proposed focal" data-id="P5" data-box="864,320,256,96"><title>Read matching modules. proposed / modified. </title><rect class="node-shape" x="864" y="320" width="256" height="96" rx="6"/><text class="node-title" x="992" y="364" text-anchor="middle">Read matching modules</text><text class="node-note" x="992" y="388" text-anchor="middle">UI: functionality-only</text></g><g id="node-8-proposed-P6" class="node result proposed " data-id="P6" data-box="1200,96,208,96"><title>Work with shared
+ project rules. proposed / unchanged. </title><rect class="node-shape" x="1200" y="96" width="208" height="96" rx="20"/><text class="node-title" x="1304" y="138" text-anchor="middle">Work with shared</text><text class="node-title" x="1304" y="162" text-anchor="middle">+ project rules</text></g></svg></div><figcaption><div class="legend"><span><i class="legend-start" aria-hidden="true"></i>Start / result</span><span><i class="legend-action" aria-hidden="true"></i>Action</span><span><i class="legend-decision" aria-hidden="true"></i>Decision</span><span><i class="legend-focus" aria-hidden="true"></i>Focus of the change</span></div><span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. Proposed is not implemented.</span></figcaption></figure><figure class="panel" id="panel-current"><div class="figure-head"><h2>Today: shared rules depend on skill choice</h2><span>current · flow</span></div><p class="figure-description">Arrows show what loads as an ordinary session starts in either project. Based on disk inspection and host docs, not a live trace.</p><p class="hint">Scroll sideways to explore. <a href="#node-7-current-C1">Go to start</a>. Fit is an optional overview.</p><div class="canvas" tabindex="0" role="region" aria-label="Today diagram, scroll to explore" data-start-id="node-7-current-C1" style="--board-width:1440px"><svg class="map" xmlns="http://www.w3.org/2000/svg" width="1440" height="464" viewBox="0 0 1440 464" role="img" aria-labelledby="current-title current-desc"><title id="current-title">Today: shared rules depend on skill choice</title><desc id="current-desc">Arrows show what loads as an ordinary session starts in either project. Based on disk inspection and host docs, not a live trace.</desc><defs><marker id="arrow-current" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs><path class="connector observed" data-from="C1" data-to="C2" data-points="[[272, 144], [320, 144]]" d="M 272 144 L 320 144" marker-end="url(#arrow-current)"/><path class="connector observed" data-from="C2" data-to="C3" data-points="[[544, 144], [592, 144]]" d="M 544 144 L 592 144" marker-end="url(#arrow-current)"/><path class="connector observed" data-from="C3" data-to="C4" data-points="[[816, 144], [864, 144]]" d="M 816 144 L 864 144" marker-end="url(#arrow-current)"/><path class="connector inferred" data-from="C4" data-to="C5" data-points="[[992, 224], [992, 320]]" d="M 992 224 L 992 320" marker-end="url(#arrow-current)"/><path class="connector inferred" data-from="C4" data-to="C6" data-points="[[1120, 144], [1200, 144]]" d="M 1120 144 L 1200 144" marker-end="url(#arrow-current)"/><g class="arrow-label"><rect x="1009.5" y="261" width="37" height="22"/><text class="edge-label" text-anchor="middle" x="1028" y="276">YES</text></g><g class="arrow-label"><rect x="1145" y="113" width="30" height="22"/><text class="edge-label" text-anchor="middle" x="1160" y="128">NO</text></g><g id="node-7-current-C1" class="node start observed " data-id="C1" data-box="48,96,224,96"><title>Ordinary session. observed / unchanged. </title><rect class="node-shape" x="48" y="96" width="224" height="96" rx="20"/><text class="node-title" x="160" y="140" text-anchor="middle">Ordinary session</text><text class="node-note" x="160" y="164" text-anchor="middle">No slash command</text></g><g id="node-7-current-C2" class="node action observed " data-id="C2" data-box="320,96,224,96"><title>Global rules. observed / unchanged. </title><rect class="node-shape" x="320" y="96" width="224" height="96" rx="6"/><text class="node-title" x="432" y="140" text-anchor="middle">Global rules</text><text class="node-note" x="432" y="164" text-anchor="middle">Artifact policy only</text></g><g id="node-7-current-C3" class="node action observed " data-id="C3" data-box="592,96,224,96"><title>Project rules. observed / unchanged. </title><rect class="node-shape" x="592" y="96" width="224" height="96" rx="6"/><text class="node-title" x="704" y="140" text-anchor="middle">Project rules</text><text class="node-note" x="704" y="164" text-anchor="middle">Root + area files</text></g><g id="node-7-current-C4" class="node decision inferred focal" data-id="C4" data-box="864,64,256,160"><title>Model picks
a skill?. inferred / unchanged. Depends on the model matching a skill description; not traced.</title><polygon class="node-shape" points="992,64 1120,144 992,224 864,144"/><text class="node-title" x="992" y="138" text-anchor="middle">Model picks</text><text class="node-title" x="992" y="162" text-anchor="middle">a skill?</text></g><g id="node-7-current-C5" class="node result observed focal" data-id="C5" data-box="864,320,256,96"><title>Shared rules read. observed / unchanged. </title><rect class="node-shape" x="864" y="320" width="256" height="96" rx="20"/><text class="node-title" x="992" y="364" text-anchor="middle">Shared rules read</text><text class="node-note" x="992" y="388" text-anchor="middle">Helpers blocked: CRLF</text></g><g id="node-7-current-C6" class="node result inferred " data-id="C6" data-box="1200,96,208,96"><title>No shared core. inferred / unchanged. Inferred from missing global block; not traced.</title><rect class="node-shape" x="1200" y="96" width="208" height="96" rx="20"/><text class="node-title" x="1304" y="140" text-anchor="middle">No shared core</text><text class="node-note" x="1304" y="164" text-anchor="middle">Project rules only</text></g></svg></div><figcaption><div class="legend"><span><i class="legend-start" aria-hidden="true"></i>Start / result</span><span><i class="legend-action" aria-hidden="true"></i>Action</span><span><i class="legend-decision" aria-hidden="true"></i>Decision</span><span><i class="legend-focus" aria-hidden="true"></i>Focus of the change</span></div><span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. Proposed is not implemented.</span></figcaption></figure><figure class="panel ui-review" id="panel-ui-preview"><div class="figure-head"><h2>UI screen flow — example inside the same report</h2><span>screens · interaction · design</span></div><p class="figure-description">See the screens and feedback states, not just boxes naming them. Arrows show actions or outcomes; D1 and D2 point to design choices on the controls.</p><p class="ui-notice"><strong>Example UI only. It is outside the scope of this report.</strong> These are fictional screens to demonstrate the report format, not an approved project kit.</p><div class="ui-authority"><p><strong>Design mode:</strong> unselected · unconfirmed.</p><p>Fictional illustration only. No real project kit or styling decision has been selected. Reference: <a href="#s-UIREF">UIREF</a>, Preview authority and limits.</p><p><strong>Preview:</strong> This offline report, not either application, authorised. This isolated local report example only; no new application route or production UI.</p><p><strong>Design acceptance:</strong> pending. No design alternative has been accepted for either project.</p></div><p class="hint">Screen frames are connected by actions. Markers refer to design decisions below. <a href="#ui-screen-UI1">Go to first screen</a>. Scroll at readable size; Fit is an overview.</p><noscript><p class="hint">Screen Fit needs JavaScript; use normal-size scrolling and the screen details below.</p></noscript><div class="canvas ui-canvas" tabindex="0" role="region" aria-label="UI screen flow, scroll to explore" data-start-id="ui-screen-UI1" style="--board-width:1152px"><div class="ui-scaled" style="--ui-width:1152px;--ui-height:1024px"><div class="ui-board"><svg class="ui-routes" width="1152" height="1024" viewBox="0 0 1152 1024" aria-hidden="true"><defs><marker id="ui-flow-arrow" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M0 0 L10 4 L0 8 Z"/></marker></defs><path class="connector" d="M 336 240 L 416 240" marker-end="url(#ui-flow-arrow)"/><g class="arrow-label"><rect x="354" y="201" width="44" height="22"/><text class="edge-label" x="376" y="216" text-anchor="middle">SAVE</text></g><path class="connector" d="M 736 240 L 816 240" marker-end="url(#ui-flow-arrow)"/><g class="arrow-label"><rect x="750.5" y="201" width="51" height="22"/><text class="edge-label" x="776" y="216" text-anchor="middle">SAVED</text></g><path class="connector" d="M 576 432 L 576 592" marker-end="url(#ui-flow-arrow)"/><g class="arrow-label"><rect x="595" y="497" width="58" height="22"/><text class="edge-label" x="624" y="512" text-anchor="middle">FAILED</text></g><path class="connector" d="M 736 784 L 776 784 Q 784 784 784 776 L 784 504 Q 784 496 776 496 L 680 496 Q 672 496 672 488 L 672 432" marker-end="url(#ui-flow-arrow)"/><g class="arrow-label"><rect x="702.5" y="461" width="51" height="22"/><text class="edge-label" x="728" y="476" text-anchor="middle">RETRY</text></g></svg><article class="ui-screen-node" id="ui-screen-UI1" style="left:16px;top:48px;width:320px;height:384px"><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-board-UI1-title ui-board-UI1-desc"><title id="ui-board-UI1-title">01 · Edit preference</title><desc id="ui-board-UI1-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">01 · Edit preference</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Choose what you receive.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><rect class="ui-button" x="24" y="210" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="233">Save</text><text class="ui-control-small" x="24" y="278">Changes are not yet saved.</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><a class="ui-inspect" href="#ui-detail-UI1">Inspect screen ↗</a></article><article class="ui-screen-node" id="ui-screen-UI2" style="left:416px;top:48px;width:320px;height:384px"><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-board-UI2-title ui-board-UI2-desc"><title id="ui-board-UI2-title">02 · Saving</title><desc id="ui-board-UI2-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">02 · Saving</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><rect class="ui-button ui-disabled" x="24" y="210" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="233">Saving…</text><text class="ui-control-small" x="24" y="278">Wait for the save result.</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><a class="ui-inspect" href="#ui-detail-UI2">Inspect screen ↗</a></article><article class="ui-screen-node" id="ui-screen-UI3" style="left:816px;top:48px;width:320px;height:384px"><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-board-UI3-title ui-board-UI3-desc"><title id="ui-board-UI3-title">03 · Saved</title><desc id="ui-board-UI3-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">03 · Saved</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><path class="ui-message-rule" d="M 24 214 V 238"/><text class="ui-control-text" x="36" y="230">Preference saved.</text><a href="#ui-decision-D2" aria-label="Review decision D2"><circle class="ui-marker" cx="292" cy="225" r="13"/><text class="ui-marker-text" x="292" y="229" text-anchor="middle">D2</text></a><rect class="ui-button" x="24" y="258" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="281">Save</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><a class="ui-inspect" href="#ui-detail-UI3">Inspect screen ↗</a></article><article class="ui-screen-node" id="ui-screen-UI4" style="left:416px;top:592px;width:320px;height:384px"><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-board-UI4-title ui-board-UI4-desc"><title id="ui-board-UI4-title">04 · Save failed</title><desc id="ui-board-UI4-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. A failed save keeps the selection so the user can retry.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">04 · Save failed</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><path class="ui-message-rule" d="M 24 214 V 238"/><text class="ui-control-text" x="36" y="230">Could not save. Try again.</text><a href="#ui-decision-D2" aria-label="Review decision D2"><circle class="ui-marker" cx="292" cy="225" r="13"/><text class="ui-marker-text" x="292" y="229" text-anchor="middle">D2</text></a><rect class="ui-button" x="24" y="258" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="281">Try again</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><a class="ui-inspect" href="#ui-detail-UI4">Inspect screen ↗</a></article></div></div></div><figcaption>Schematic previews, not pixel-matched kit captures. This example is separate from the shared-rules task above. No styling or app implementation is approved. Selecting or inspecting a screen does not record approval or change the application.</figcaption><section class="ui-decisions"><h2>Design decisions on these screens</h2><article id="ui-decision-D1" class="ui-decision"><h3>D1 · Preference control</h3><p>Should this be the approved checkbox or the approved switch? The drawing shows a checkbox as an example, not a selection.</p><p>Alternatives: Approved checkbox / Approved switch.</p><p><strong>Open: the owner must decide</strong></p></article><article id="ui-decision-D2" class="ui-decision"><h3>D2 · Feedback position</h3><p>Where should the approved success and error feedback appear? The sample shows inline text only.</p><p>Alternatives: Inside the form / Approved floating message.</p><p><strong>Open: the owner must decide</strong></p></article></section><details class="ui-screen-index"><summary>Inspect screens and their sources</summary><details id="ui-detail-UI1" class="ui-screen-detail"><summary>01 · Edit preference: screen and sources</summary><p>Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</p><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-detail-UI1-title ui-detail-UI1-desc"><title id="ui-detail-UI1-title">01 · Edit preference</title><desc id="ui-detail-UI1-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">01 · Edit preference</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Choose what you receive.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><rect class="ui-button" x="24" y="210" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="233">Save</text><text class="ui-control-small" x="24" y="278">Changes are not yet saved.</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><p class="muted"><strong>Component provenance not established.</strong> This view shows structure, not proof of the final look.</p><p class="ui-evidence">State: proposed. Sources: <a href="#s-UIREF">UIREF</a>. Requirements: —. Tasks: —.</p></details><details id="ui-detail-UI2" class="ui-screen-detail"><summary>02 · Saving: screen and sources</summary><p>Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</p><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-detail-UI2-title ui-detail-UI2-desc"><title id="ui-detail-UI2-title">02 · Saving</title><desc id="ui-detail-UI2-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">02 · Saving</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><rect class="ui-button ui-disabled" x="24" y="210" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="233">Saving…</text><text class="ui-control-small" x="24" y="278">Wait for the save result.</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><p class="muted"><strong>Component provenance not established.</strong> This view shows structure, not proof of the final look.</p><p class="ui-evidence">State: proposed. Sources: <a href="#s-UIREF">UIREF</a>. Requirements: —. Tasks: —.</p></details><details id="ui-detail-UI3" class="ui-screen-detail"><summary>03 · Saved: screen and sources</summary><p>Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</p><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-detail-UI3-title ui-detail-UI3-desc"><title id="ui-detail-UI3-title">03 · Saved</title><desc id="ui-detail-UI3-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">03 · Saved</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><path class="ui-message-rule" d="M 24 214 V 238"/><text class="ui-control-text" x="36" y="230">Preference saved.</text><a href="#ui-decision-D2" aria-label="Review decision D2"><circle class="ui-marker" cx="292" cy="225" r="13"/><text class="ui-marker-text" x="292" y="229" text-anchor="middle">D2</text></a><rect class="ui-button" x="24" y="258" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="281">Save</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><p class="muted"><strong>Component provenance not established.</strong> This view shows structure, not proof of the final look.</p><p class="ui-evidence">State: proposed. Sources: <a href="#s-UIREF">UIREF</a>. Requirements: —. Tasks: —.</p></details><details id="ui-detail-UI4" class="ui-screen-detail"><summary>04 · Save failed: screen and sources</summary><p>Illustrative notification setting; neither current application was inspected or styled for this preview. A failed save keeps the selection so the user can retry.</p><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-detail-UI4-title ui-detail-UI4-desc"><title id="ui-detail-UI4-title">04 · Save failed</title><desc id="ui-detail-UI4-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. A failed save keeps the selection so the user can retry.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">04 · Save failed</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><path class="ui-message-rule" d="M 24 214 V 238"/><text class="ui-control-text" x="36" y="230">Could not save. Try again.</text><a href="#ui-decision-D2" aria-label="Review decision D2"><circle class="ui-marker" cx="292" cy="225" r="13"/><text class="ui-marker-text" x="292" y="229" text-anchor="middle">D2</text></a><rect class="ui-button" x="24" y="258" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="281">Try again</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><p class="muted"><strong>Component provenance not established.</strong> This view shows structure, not proof of the final look.</p><p class="ui-evidence">State: proposed. Sources: <a href="#s-UIREF">UIREF</a>. Requirements: —. Tasks: —.</p></details></details></figure></div></section>
<section class="comparison"><h2>What changes</h2><div class="table-scroll"><table><thead><tr><th>Area</th><th>Today, from the recorded evidence</th><th>Proposed scope</th></tr></thead><tbody><tr><td><strong>Shared core in ordinary sessions</strong><small><a href="#r-R1">R1</a></small></td><td>Loaded only if the model happens to pick a workflow skill.<small><a href="#e-E1">E1</a> <a href="#e-E3">E3</a></small></td><td>Global block: Claude imports START and CORE; Codex is told to read them.<small><a href="#t-T4">T4</a></small></td></tr><tr><td><strong>Installed copy</strong><small><a href="#r-R2">R2</a></small></td><td>CRLF copy fails its own manifest; helpers blocked.<small><a href="#e-E2">E2</a></small></td><td>Byte-identical copy, checked by the source audit.<small><a href="#t-T1">T1</a> <a href="#t-T3">T3</a></small></td></tr><tr><td><strong>Project files</strong><small><a href="#r-R3">R3</a> <a href="#r-R4">R4</a></small></td><td>No pointer to shared rules; no UI mode line.<small><a href="#e-E5">E5</a></small></td><td>One portable pointer and one UI-mode line per project; no copied shared prose.<small><a href="#t-T2">T2</a> <a href="#t-T5">T5</a></small></td></tr></tbody></table></div></section><section class="boundaries"><h2>What will not change</h2><ul><li>No application code, styles, dependencies or build configuration change.</li><li>No hooks, permissions or status line changes.</li><li>No framework upgrades and no retroactive cleanup.</li><li>Nothing is deleted; history and approved records are preserved.</li><li>Teammates' machines are not updated by this local install.</li></ul></section>
<div class="two-col"><section><h2>Changes and cleanup</h2><ul class="plain"><li><a href="#t-T1">T1</a> Make the package line-ending safe and diagnosable<small>Depends on: No prior task</small></li><li><a href="#t-T2">T2</a> Add the shared TypeScript discriminant rule<small>Depends on: T1</small></li><li><a href="#t-T3">T3</a> Reinstall the runtime copy as package bytes<small>Depends on: T1, T2</small></li><li><a href="#t-T4">T4</a> Activate the shared core for Claude and Codex<small>Depends on: T3</small></li><li><a href="#t-T5">T5</a> Project lines and memory status<small>Depends on: No prior task</small></li></ul></section>
<section><h2>How completion will be checked</h2><ul class="plain"><li id="a-A1"><strong>A1: Fresh sessions in both projects show START and CORE in context or reads, plus the matching modules.</strong><small>Verify: Owner cases 1-3: neutral prompts in new T3 Claude and Codex sessions; inspect context and tool-read traces. · <a href="#r-R1">R1</a> <a href="#r-R3">R3</a></small></li><li id="a-A2"><strong>A2: The runtime copy matches the package byte for byte.</strong><small>Verify: Source audit_installation.py --candidate ~/.agents/skills reports MATCH. · <a href="#r-R2">R2</a></small></li><li id="a-A3"><strong>A3: Project diffs contain only the listed lines, with CRLF and the owner's edits kept.</strong><small>Verify: Review the diff; the file command shows CRLF. · <a href="#r-R3">R3</a></small></li><li id="a-A4"><strong>A4: A UI task without authority adds no styling and claims no kit.</strong><small>Verify: Owner case 9 in a disposable fixture. · <a href="#r-R4">R4</a></small></li></ul></section></div>
<section class="approval"><h2>No permission to build</h2><div><p>This page does not allow code changes. Settle the open decisions first, then approve the plan in the chat.</p></div></section><details class="appendix" id="technical-details"><summary>Technical evidence and task details</summary><div class="appendix-body">
<section><h2>Requested outcome</h2><ul class="plain"><li id="r-R1"><strong>R1.</strong> Ordinary sessions in both projects load the shared core without /workflow or /rule-maker.<small>Source: Owner brief section 3C</small></li><li id="r-R2"><strong>R2.</strong> The installed copy verifiably matches the reviewed package bytes.<small>Source: Owner brief sections 1 and 4</small></li><li id="r-R3"><strong>R3.</strong> Projects keep their own knowledge; shared rules are not duplicated into them.<small>Source: Owner brief section 3A/3B; DECISIONS D2</small></li><li id="r-R4"><strong>R4.</strong> UI work stays functionality-only where no approved kit is established.<small>Source: Owner brief section 7; DECISIONS D3</small></li></ul></section>
<section><h2>Changes and mapped tasks</h2><div class="table-scroll"><table><thead><tr><th>Task</th><th>Work and affected areas</th><th>Traceability</th><th>Depends on</th></tr></thead><tbody><tr id="t-T1"><td><strong>T1</strong></td><td><strong>Make the package line-ending safe and diagnosable</strong><small>requested: Fixes the cause of F1 without weakening integrity checks.<br>Areas: .gitattributes, skills/rule-maker/scripts, tools/update_rules_manifest.py, INSTALLATION.md, UPGRADE.md, tests</small></td><td><a href="#r-R2">R2</a><br><a href="#e-E2">E2</a><br><a href="#a-A2">A2</a></td><td>—</td></tr><tr id="t-T2"><td><strong>T2</strong></td><td><strong>Add the shared TypeScript discriminant rule</strong><small>requested: Owner decision D2.<br>Areas: skills/rule-maker/rules/typescript.md, rules/version.json</small></td><td><a href="#r-R3">R3</a><br><a href="#e-E5">E5</a><br><a href="#a-A1">A1</a></td><td><a href="#t-T1">T1</a></td></tr><tr id="t-T3"><td><strong>T3</strong></td><td><strong>Reinstall the runtime copy as package bytes</strong><small>prerequisite: Helpers refuse to run on the CRLF copy.<br>Areas: ~/.agents/skills</small></td><td><a href="#r-R2">R2</a><br><a href="#e-E2">E2</a><br><a href="#a-A2">A2</a></td><td><a href="#t-T1">T1</a> <a href="#t-T2">T2</a></td></tr><tr id="t-T4"><td><strong>T4</strong></td><td><strong>Activate the shared core for Claude and Codex</strong><small>requested: Goal requires loading without slash commands in both hosts.<br>Areas: ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md</small></td><td><a href="#r-R1">R1</a><br><a href="#e-E1">E1</a> <a href="#e-E3">E3</a> <a href="#e-E4">E4</a><br><a href="#a-A1">A1</a></td><td><a href="#t-T3">T3</a></td></tr><tr id="t-T5"><td><strong>T5</strong></td><td><strong>Project lines and memory status</strong><small>requested: Shared-library pointer, UI mode, Node range, GREEN wording, superseded memories.<br>Areas: Altinstar AGENTS.md, src/AGENTS.md, memories, Cheflabels AGENTS.md, memory</small></td><td><a href="#r-R3">R3</a> <a href="#r-R4">R4</a><br><a href="#e-E5">E5</a><br><a href="#a-A3">A3</a> <a href="#a-A4">A4</a></td><td>—</td></tr></tbody></table></div></section>
<section><h2>Evidence and limits</h2><article id="e-E1" class="evidence"><strong>E1 · static: Global Claude and Codex instructions activate only the artifact policy; the shared-rules block is missing for both.</strong><p>FINDINGS R2 F2; audit_installation.py --check-global reports UPDATE</p><small>Sources: <a href="#s-S2">S2</a><br>Limits: Disk state only; loading in a live session not traced.</small></article><article id="e-E2" class="evidence"><strong>E2 · static: The runtime copy differs from the package only by CRLF line endings, so all integrity-checked helpers refuse or report false drift.</strong><p>FINDINGS R2 F1</p><small>Sources: <a href="#s-S2">S2</a><br>Limits: The install route that produced CRLF is unknown.</small></article><article id="e-E3" class="evidence"><strong>E3 · static: The activation helper adds @START.md and @CORE.md for Claude and a read instruction for Codex, preserving unrelated text.</strong><p>activate_rules.py block() and merge()</p><small>Sources: <a href="#s-S4">S4</a><br>Limits: Claude user-scope imports load eagerly per the docs; Codex has no import syntax, so its loading is guidance.</small></article><article id="e-E4" class="evidence"><strong>E4 · static: START routes to version-relevant modules and to ui-design for UI work.</strong><p>START.md module table</p><small>Sources: <a href="#s-S5">S5</a><br>Limits: Selection by the model is not traced yet.</small></article><article id="e-E5" class="evidence"><strong>E5 · design: Owner chose a shared TypeScript rule, superseded concurrency memories and functionality-only UI.</strong><p>DECISIONS D1-D3</p><small>Sources: <a href="#s-S3">S3</a><br>Limits: Direction only, not implementation approval.</small></article></section><details><summary>Today: steps and links</summary><ol><li><strong>C1: Ordinary session</strong>. No slash command (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>C2: Global rules</strong>. Artifact policy only (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>C3: Project rules</strong>. Root + area files (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>C4: Model picks
a skill?</strong>. (inferred, unchanged) Depends on the model matching a skill description; not traced. Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>C5: Shared rules read</strong>. Helpers blocked: CRLF (observed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li><li><strong>C6: No shared core</strong>. Project rules only (inferred, unchanged) Inferred from missing global block; not traced. Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>C1 → C2</strong>: continues (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>C2 → C3</strong>: continues (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>C3 → C4</strong>: continues (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>C4 → C5</strong>: Yes (inferred, unchanged) Not traced. Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>C4 → C6</strong>: No (inferred, unchanged) Not traced. Evidence <a href="#e-E1">E1</a>; tasks —.</li></ol></details><details><summary>Proposed: steps and links</summary><ol><li><strong>P1: Ordinary session</strong>. No slash command (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>P2: Shared core block</strong>. START + CORE (proposed, added) Evidence <a href="#e-E3">E3</a>; tasks <a href="#t-T3">T3</a> <a href="#t-T4">T4</a>.</li><li><strong>P3: Project rules</strong>. + pointer, UI mode (proposed, modified) Evidence <a href="#e-E5">E5</a>; tasks <a href="#t-T5">T5</a>.</li><li><strong>P4: Framework
or UI work?</strong>. (proposed, unchanged) Evidence <a href="#e-E4">E4</a>; tasks —.</li><li><strong>P5: Read matching modules</strong>. UI: functionality-only (proposed, modified) Evidence <a href="#e-E4">E4</a> <a href="#e-E5">E5</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>P6: Work with shared
+ project rules</strong>. (proposed, unchanged) Evidence <a href="#e-E3">E3</a>; tasks —.</li><li><strong>P1 → P2</strong>: continues (proposed, added) Evidence <a href="#e-E3">E3</a>; tasks <a href="#t-T4">T4</a>.</li><li><strong>P2 → P3</strong>: continues (proposed, added) Evidence <a href="#e-E3">E3</a>; tasks <a href="#t-T4">T4</a>.</li><li><strong>P3 → P4</strong>: continues (proposed, unchanged) Evidence <a href="#e-E4">E4</a>; tasks —.</li><li><strong>P4 → P5</strong>: Yes (proposed, unchanged) Evidence <a href="#e-E4">E4</a>; tasks —.</li><li><strong>P4 → P6</strong>: No (proposed, unchanged) Evidence <a href="#e-E4">E4</a>; tasks —.</li><li><strong>P5 → P6</strong>: continues (proposed, unchanged) Evidence <a href="#e-E4">E4</a>; tasks —.</li></ol></details>
<details class="snapshot"><summary>Inspected snapshot and supplied-source fingerprints</summary><p>workflow-axa package + installed runtime + Altinstar and Cheflabels rule files · workflow-axa main (task records uncommitted); projects inspected read-only</p>
<p>Static inspection and read-only helper runs on 2026-10-30, recorded in FINDINGS.md. Global profile and project files lie outside this report root, so they are cited through FINDINGS.</p><p>Generated 2026-11-03T12:20:58.136870+00:00. Hashes cover only these supplied files, including local changes. They do not prove complete code coverage or correctness.</p><div class="table-scroll"><table><thead><tr><th>ID</th><th>Source</th><th>Role</th><th>SHA-256 prefix</th></tr></thead><tbody><tr id="s-S1"><td>S1</td><td><code>docs/plans/2026-10-30-rules-audit-design-kit/PLAN.md</code></td><td>plan</td><td><code>95d296d16f367a72…</code></td></tr><tr id="s-S2"><td>S2</td><td><code>docs/plans/2026-10-30-rules-audit-design-kit/FINDINGS.md</code></td><td>findings</td><td><code>3da422db49061fce…</code></td></tr><tr id="s-S3"><td>S3</td><td><code>docs/plans/2026-10-30-rules-audit-design-kit/DECISIONS.md</code></td><td>decisions</td><td><code>1012c0326c16ea73…</code></td></tr><tr id="s-S4"><td>S4</td><td><code>skills/rule-maker/scripts/activate_rules.py</code></td><td>code</td><td><code>8dcf9642b58a85fa…</code></td></tr><tr id="s-S5"><td>S5</td><td><code>skills/rule-maker/rules/START.md</code></td><td>code</td><td><code>46420b13446bad1a…</code></td></tr><tr id="s-UIREF"><td>UIREF</td><td><code>skills/codebase-map/examples/ui-review/README.md</code></td><td>design-example</td><td><code>863c4b8bd4ebc241…</code></td></tr><tr id="s-report-input"><td>report-input</td><td><code>skills/codebase-map/examples/ui-review/review-r2.json</code></td><td>projection</td><td><code>da0b6a3e0d01ccc4…</code></td></tr></tbody></table></div></details>
</div></details><p class="muted">System-font mode. No network assets. A saved projection, not a live repository map.</p><footer>Local HTML + SVG · no external assets, uploads or approval actions · saved review snapshot; progress belongs in the existing task records</footer>
</main><script type="application/json" id="report-manifest">{"schema_version": 1, "tool_version": "2.8.0-rc.1", "layout_version": 2, "style": "workflow-editorial-v1", "revision": "R2-draft", "report_kind": "review", "generated_utc": "2026-11-03T12:20:58.136870+00:00", "sources": [{"id": "S1", "role": "plan", "path": "docs/plans/2026-10-30-rules-audit-design-kit/PLAN.md", "sha256": "95d296d16f367a724fbb604f5aded87bf59914a1feb606bf418c1c446a9eacae"}, {"id": "S2", "role": "findings", "path": "docs/plans/2026-10-30-rules-audit-design-kit/FINDINGS.md", "sha256": "3da422db49061fcea216e4d8b86f85a865b7cfd58d77b69132e7bc4f08f6404a"}, {"id": "S3", "role": "decisions", "path": "docs/plans/2026-10-30-rules-audit-design-kit/DECISIONS.md", "sha256": "1012c0326c16ea73d48ae8a33e379efcadcea7322d1ed1d1a996d152d1e30aab"}, {"id": "S4", "role": "code", "path": "skills/rule-maker/scripts/activate_rules.py", "sha256": "8dcf9642b58a85fa09e6740d75164c7b21b2fe683e2625289a4467cb02aaffcf"}, {"id": "S5", "role": "code", "path": "skills/rule-maker/rules/START.md", "sha256": "46420b13446bad1ada89220f024ef75213809903d579085f64bd22fb13623cfe"}, {"id": "UIREF", "role": "design-example", "path": "skills/codebase-map/examples/ui-review/README.md", "sha256": "863c4b8bd4ebc241167afc8d7feab08cf3a99857e719589c19c28bb1b625737e"}, {"id": "report-input", "role": "projection", "path": "skills/codebase-map/examples/ui-review/review-r2.json", "sha256": "da0b6a3e0d01ccc4df783f1ba020decc9ad8ed5232b6e965ccab51e1992842db"}]}</script><script>'use strict';
// Native radio inputs and scrolling work without this progressive enhancement.
function revealAnchor() {
const id = location.hash.slice(1);
const target = document.getElementById(id);
if (!target) return;
let parent = target.parentElement;
while (parent) {
if (parent.tagName === 'DETAILS') parent.open = true;
parent = parent.parentElement;
}
target.scrollIntoView({block:target.closest('.canvas') ? 'nearest' : 'start', inline:'center'});
}
for (const anchor of document.querySelectorAll('a[href^="#"]')) {
anchor.addEventListener('click', () => {
const target = document.getElementById(anchor.getAttribute('href').slice(1));
let parent = target;
while (parent) {
if (parent.tagName === 'DETAILS') parent.open = true;
parent = parent.parentElement;
}
});
}
window.addEventListener('hashchange', revealAnchor);
if (location.hash) revealAnchor();
function centerEntry(canvas) {
if (!canvas.clientWidth) return;
const node = document.getElementById(canvas.dataset.startId);
if (!node) return;
const bounds = node.getBoundingClientRect();
const viewport = canvas.getBoundingClientRect();
canvas.scrollLeft += bounds.x + bounds.width/2 - viewport.x - viewport.width/2;
}
function centerVisibleEntries() {
for (const canvas of document.querySelectorAll('.canvas')) centerEntry(canvas);
}
for (const input of document.querySelectorAll('.review input[type="radio"]')) {
input.addEventListener('change', centerVisibleEntries);
}
const resizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(entries => {
for (const entry of entries) centerEntry(entry.target);
}) : null;
for (const canvas of document.querySelectorAll('.canvas')) {
// On narrow screens show the entry node, not the diagram's empty left margin.
resizeObserver?.observe(canvas);
let drag = null;
canvas.addEventListener('keydown', event => {
const choices = ['100', '125', '150'];
const selected = document.querySelector('input[name="diagram-zoom"]:checked');
let index = choices.indexOf(selected.id.slice(5));
let next = null;
if (event.key === '+' || event.key === '=') next = choices[Math.min(2, index+1)];
else if (event.key === '-') next = choices[Math.max(0, index-1)];
else if (event.key.toLowerCase() === 'f') next = 'fit';
else if (event.key === '0') next = '100';
if (next) {
event.preventDefault();
const input = document.getElementById('zoom-'+next);
input.checked = true;
input.dispatchEvent(new Event('change'));
}
});
canvas.addEventListener('pointerdown', event => {
if (event.pointerType !== 'mouse' || event.button !== 0) return;
drag = {x:event.clientX, scroll:canvas.scrollLeft};
canvas.setPointerCapture(event.pointerId);
canvas.classList.add('dragging');
});
canvas.addEventListener('pointermove', event => {
if (drag) canvas.scrollLeft = drag.scroll + drag.x-event.clientX;
});
const stop = () => { drag = null; canvas.classList.remove('dragging'); };
canvas.addEventListener('pointerup', stop);
canvas.addEventListener('pointercancel', stop);
canvas.addEventListener('lostpointercapture', stop);
}
centerVisibleEntries();
// Screen previews are inert; this only scales the report viewer, never an application.
function fitUiFlow() {
const panel=document.getElementById('panel-ui-preview');
if(!panel || !panel.offsetWidth) return;
const canvas=panel.querySelector('.ui-canvas');
const scaled=panel.querySelector('.ui-scaled');
const width=parseFloat(scaled.style.getPropertyValue('--ui-width'));
const selected=document.querySelector('input[name="diagram-zoom"]:checked');
const value=selected?.id==='zoom-fit' ? Math.min(1,canvas.clientWidth/width) : ({'zoom-100':1,'zoom-125':1.25,'zoom-150':1.5}[selected?.id]||1);
scaled.style.setProperty('--scale',value);
}
for(const input of document.querySelectorAll('.review input[type="radio"]')) input.addEventListener('change',fitUiFlow);
if(typeof ResizeObserver==='function') {
const uiCanvas=document.querySelector('.ui-canvas');
if(uiCanvas) new ResizeObserver(fitUiFlow).observe(uiCanvas);
}
fitUiFlow();
// Keep source/decision links clickable; report panning must not capture their pointer.
for(const link of document.querySelectorAll('.ui-canvas a')) {
link.addEventListener('pointerdown',event=>event.stopPropagation());
}
</script></body></html>examples/ui-review/review-r2.json{
"schema_version": 2,
"title": "Shared rules load in every ordinary session, both projects",
"revision": "R2-draft",
"mode": "compare",
"state": "draft",
"snapshot": {
"repository": "workflow-axa package + installed runtime + Altinstar and Cheflabels rule files",
"branch": "workflow-axa main (task records uncommitted); projects inspected read-only",
"basis": "Static inspection and read-only helper runs on 2026-10-30, recorded in FINDINGS.md. Global profile and project files lie outside this report root, so they are cited through FINDINGS.",
"limits": "No fresh-session trace has been run, so loading is inferred from host documentation. No independent review yet. Draft: not an approval request."
},
"understanding": "You want one central set of shared rules that ordinary Claude and Codex sessions in both projects pick up without slash commands, while each project keeps only its own knowledge. Today only the artifact policy is activated globally, and the installed copy fails its own integrity check because of line endings. The draft fixes the copy, activates the shared core for both hosts, adds one shared TypeScript rule and a few project lines, and changes no application code.",
"requirements": [
{
"id": "R1",
"text": "Ordinary sessions in both projects load the shared core without /workflow or /rule-maker.",
"origin": "Owner brief section 3C"
},
{
"id": "R2",
"text": "The installed copy verifiably matches the reviewed package bytes.",
"origin": "Owner brief sections 1 and 4"
},
{
"id": "R3",
"text": "Projects keep their own knowledge; shared rules are not duplicated into them.",
"origin": "Owner brief section 3A/3B; DECISIONS D2"
},
{
"id": "R4",
"text": "UI work stays functionality-only where no approved kit is established.",
"origin": "Owner brief section 7; DECISIONS D3"
}
],
"boundaries": [
"No application code, styles, dependencies or build configuration change.",
"No hooks, permissions or status line changes.",
"No framework upgrades and no retroactive cleanup.",
"Nothing is deleted; history and approved records are preserved.",
"Teammates' machines are not updated by this local install."
],
"open_decisions": [
"Historical R2 draft reproduced for report-format review. This is not a fresh audit or installation status. The separate UI screen example adds no application scope.",
"F1 method: keep raw-byte integrity and reinstall LF bytes (drafted) or a text-normalised manifest; decided after independent review.",
"Altinstar AGENTS.md:105 one-line comment rule versus the owner brief.",
"Global ~/.claude/CLAUDE.md mandatory design vocabulary versus the owner brief.",
"Number and credit budget of live fresh-session acceptance runs.",
"Independent Astra review has not run yet."
],
"sources": [
{
"id": "S1",
"path": "docs/plans/2026-10-30-rules-audit-design-kit/PLAN.md",
"role": "plan"
},
{
"id": "S2",
"path": "docs/plans/2026-10-30-rules-audit-design-kit/FINDINGS.md",
"role": "findings"
},
{
"id": "S3",
"path": "docs/plans/2026-10-30-rules-audit-design-kit/DECISIONS.md",
"role": "decisions"
},
{
"id": "S4",
"path": "skills/rule-maker/scripts/activate_rules.py",
"role": "code"
},
{
"id": "S5",
"path": "skills/rule-maker/rules/START.md",
"role": "code"
},
{
"id": "UIREF",
"path": "skills/codebase-map/examples/ui-review/README.md",
"role": "design-example"
}
],
"evidence": [
{
"id": "E1",
"claim": "Global Claude and Codex instructions activate only the artifact policy; the shared-rules block is missing for both.",
"kind": "static",
"sources": [
"S2"
],
"locator": "FINDINGS R2 F2; audit_installation.py --check-global reports UPDATE",
"limits": "Disk state only; loading in a live session not traced."
},
{
"id": "E2",
"claim": "The runtime copy differs from the package only by CRLF line endings, so all integrity-checked helpers refuse or report false drift.",
"kind": "static",
"sources": [
"S2"
],
"locator": "FINDINGS R2 F1",
"limits": "The install route that produced CRLF is unknown."
},
{
"id": "E3",
"claim": "The activation helper adds @START.md and @CORE.md for Claude and a read instruction for Codex, preserving unrelated text.",
"kind": "static",
"sources": [
"S4"
],
"locator": "activate_rules.py block() and merge()",
"limits": "Claude user-scope imports load eagerly per the docs; Codex has no import syntax, so its loading is guidance."
},
{
"id": "E4",
"claim": "START routes to version-relevant modules and to ui-design for UI work.",
"kind": "static",
"sources": [
"S5"
],
"locator": "START.md module table",
"limits": "Selection by the model is not traced yet."
},
{
"id": "E5",
"claim": "Owner chose a shared TypeScript rule, superseded concurrency memories and functionality-only UI.",
"kind": "design",
"sources": [
"S3"
],
"locator": "DECISIONS D1-D3",
"limits": "Direction only, not implementation approval."
}
],
"acceptance": [
{
"id": "A1",
"text": "Fresh sessions in both projects show START and CORE in context or reads, plus the matching modules.",
"requirements": [
"R1",
"R3"
],
"verification": "Owner cases 1-3: neutral prompts in new T3 Claude and Codex sessions; inspect context and tool-read traces."
},
{
"id": "A2",
"text": "The runtime copy matches the package byte for byte.",
"requirements": [
"R2"
],
"verification": "Source audit_installation.py --candidate ~/.agents/skills reports MATCH."
},
{
"id": "A3",
"text": "Project diffs contain only the listed lines, with CRLF and the owner's edits kept.",
"requirements": [
"R3"
],
"verification": "Review the diff; the file command shows CRLF."
},
{
"id": "A4",
"text": "A UI task without authority adds no styling and claims no kit.",
"requirements": [
"R4"
],
"verification": "Owner case 9 in a disposable fixture."
}
],
"tasks": [
{
"id": "T1",
"title": "Make the package line-ending safe and diagnosable",
"kind": "requested",
"reason": "Fixes the cause of F1 without weakening integrity checks.",
"requirements": [
"R2"
],
"evidence": [
"E2"
],
"acceptance": [
"A2"
],
"areas": [
".gitattributes",
"skills/rule-maker/scripts",
"tools/update_rules_manifest.py",
"INSTALLATION.md",
"UPGRADE.md",
"tests"
],
"depends_on": []
},
{
"id": "T2",
"title": "Add the shared TypeScript discriminant rule",
"kind": "requested",
"reason": "Owner decision D2.",
"requirements": [
"R3"
],
"evidence": [
"E5"
],
"acceptance": [
"A1"
],
"areas": [
"skills/rule-maker/rules/typescript.md",
"rules/version.json"
],
"depends_on": [
"T1"
]
},
{
"id": "T3",
"title": "Reinstall the runtime copy as package bytes",
"kind": "prerequisite",
"reason": "Helpers refuse to run on the CRLF copy.",
"requirements": [
"R2"
],
"evidence": [
"E2"
],
"acceptance": [
"A2"
],
"areas": [
"~/.agents/skills"
],
"depends_on": [
"T1",
"T2"
]
},
{
"id": "T4",
"title": "Activate the shared core for Claude and Codex",
"kind": "requested",
"reason": "Goal requires loading without slash commands in both hosts.",
"requirements": [
"R1"
],
"evidence": [
"E1",
"E3",
"E4"
],
"acceptance": [
"A1"
],
"areas": [
"~/.claude/CLAUDE.md",
"~/.codex/AGENTS.md"
],
"depends_on": [
"T3"
]
},
{
"id": "T5",
"title": "Project lines and memory status",
"kind": "requested",
"reason": "Shared-library pointer, UI mode, Node range, GREEN wording, superseded memories.",
"requirements": [
"R3",
"R4"
],
"evidence": [
"E5"
],
"acceptance": [
"A3",
"A4"
],
"areas": [
"Altinstar AGENTS.md, src/AGENTS.md, memories",
"Cheflabels AGENTS.md, memory"
],
"depends_on": []
}
],
"comparison": [
{
"label": "Shared core in ordinary sessions",
"current": "Loaded only if the model happens to pick a workflow skill.",
"proposed": "Global block: Claude imports START and CORE; Codex is told to read them.",
"requirements": [
"R1"
],
"evidence": [
"E1",
"E3"
],
"tasks": [
"T4"
]
},
{
"label": "Installed copy",
"current": "CRLF copy fails its own manifest; helpers blocked.",
"proposed": "Byte-identical copy, checked by the source audit.",
"requirements": [
"R2"
],
"evidence": [
"E2"
],
"tasks": [
"T1",
"T3"
]
},
{
"label": "Project files",
"current": "No pointer to shared rules; no UI mode line.",
"proposed": "One portable pointer and one UI-mode line per project; no copied shared prose.",
"requirements": [
"R3",
"R4"
],
"evidence": [
"E5"
],
"tasks": [
"T2",
"T5"
]
}
],
"default_view": "proposed",
"graphs": [
{
"id": "current",
"kind": "current",
"purpose": "flow",
"tab": "Today",
"title": "Today: shared rules depend on skill choice",
"description": "Arrows show what loads as an ordinary session starts in either project. Based on disk inspection and host docs, not a live trace.",
"width": 1440,
"height": 464,
"nodes": [
{
"id": "C1",
"label": "Ordinary session",
"note": "No slash command",
"shape": "start",
"x": 48,
"y": 96,
"width": 224,
"height": 96,
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": []
},
{
"id": "C2",
"label": "Global rules",
"note": "Artifact policy only",
"shape": "action",
"x": 320,
"y": 96,
"width": 224,
"height": 96,
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": []
},
{
"id": "C3",
"label": "Project rules",
"note": "Root + area files",
"shape": "action",
"x": 592,
"y": 96,
"width": 224,
"height": 96,
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": []
},
{
"id": "C4",
"label": "Model picks\na skill?",
"note": "",
"shape": "decision",
"x": 864,
"y": 64,
"width": 256,
"height": 160,
"highlight": true,
"basis": "inferred",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"uncertainty": "Depends on the model matching a skill description; not traced."
},
{
"id": "C5",
"label": "Shared rules read",
"note": "Helpers blocked: CRLF",
"shape": "result",
"x": 864,
"y": 320,
"width": 256,
"height": 96,
"highlight": true,
"basis": "observed",
"change": "unchanged",
"evidence": [
"E2"
],
"tasks": []
},
{
"id": "C6",
"label": "No shared core",
"note": "Project rules only",
"shape": "result",
"x": 1200,
"y": 96,
"width": 208,
"height": 96,
"basis": "inferred",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"uncertainty": "Inferred from missing global block; not traced."
}
],
"edges": [
{
"from": "C1",
"to": "C2",
"label": "",
"points": [
[
272,
144
],
[
320,
144
]
],
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": []
},
{
"from": "C2",
"to": "C3",
"label": "",
"points": [
[
544,
144
],
[
592,
144
]
],
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": []
},
{
"from": "C3",
"to": "C4",
"label": "",
"points": [
[
816,
144
],
[
864,
144
]
],
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": []
},
{
"from": "C4",
"to": "C5",
"label": "Yes",
"label_position": [
1028,
276
],
"points": [
[
992,
224
],
[
992,
320
]
],
"basis": "inferred",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"uncertainty": "Not traced."
},
{
"from": "C4",
"to": "C6",
"label": "No",
"label_position": [
1160,
128
],
"points": [
[
1120,
144
],
[
1200,
144
]
],
"basis": "inferred",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"uncertainty": "Not traced."
}
]
},
{
"id": "proposed",
"kind": "proposed",
"purpose": "flow",
"tab": "Proposed",
"title": "Proposed: shared core first, modules on demand",
"description": "Arrows show what loads as an ordinary session starts. Claude imports START and CORE; Codex is instructed to read them (guidance, not guaranteed). Draft awaiting review.",
"width": 1440,
"height": 464,
"nodes": [
{
"id": "P1",
"label": "Ordinary session",
"note": "No slash command",
"shape": "start",
"x": 48,
"y": 96,
"width": 224,
"height": 96,
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": []
},
{
"id": "P2",
"label": "Shared core block",
"note": "START + CORE",
"shape": "action",
"x": 320,
"y": 96,
"width": 224,
"height": 96,
"highlight": true,
"basis": "proposed",
"change": "added",
"evidence": [
"E3"
],
"tasks": [
"T3",
"T4"
]
},
{
"id": "P3",
"label": "Project rules",
"note": "+ pointer, UI mode",
"shape": "action",
"x": 592,
"y": 96,
"width": 224,
"height": 96,
"basis": "proposed",
"change": "modified",
"evidence": [
"E5"
],
"tasks": [
"T5"
]
},
{
"id": "P4",
"label": "Framework\nor UI work?",
"note": "",
"shape": "decision",
"x": 864,
"y": 64,
"width": 256,
"height": 160,
"basis": "proposed",
"change": "unchanged",
"evidence": [
"E4"
],
"tasks": []
},
{
"id": "P5",
"label": "Read matching modules",
"note": "UI: functionality-only",
"shape": "action",
"x": 864,
"y": 320,
"width": 256,
"height": 96,
"highlight": true,
"basis": "proposed",
"change": "modified",
"evidence": [
"E4",
"E5"
],
"tasks": [
"T2"
]
},
{
"id": "P6",
"label": "Work with shared\n+ project rules",
"note": "",
"shape": "result",
"x": 1200,
"y": 96,
"width": 208,
"height": 96,
"basis": "proposed",
"change": "unchanged",
"evidence": [
"E3"
],
"tasks": []
}
],
"edges": [
{
"from": "P1",
"to": "P2",
"label": "",
"points": [
[
272,
144
],
[
320,
144
]
],
"basis": "proposed",
"change": "added",
"evidence": [
"E3"
],
"tasks": [
"T4"
]
},
{
"from": "P2",
"to": "P3",
"label": "",
"points": [
[
544,
144
],
[
592,
144
]
],
"basis": "proposed",
"change": "added",
"evidence": [
"E3"
],
"tasks": [
"T4"
]
},
{
"from": "P3",
"to": "P4",
"label": "",
"points": [
[
816,
144
],
[
864,
144
]
],
"basis": "proposed",
"change": "unchanged",
"evidence": [
"E4"
],
"tasks": []
},
{
"from": "P4",
"to": "P5",
"label": "Yes",
"label_position": [
1028,
276
],
"points": [
[
992,
224
],
[
992,
320
]
],
"basis": "proposed",
"change": "unchanged",
"evidence": [
"E4"
],
"tasks": []
},
{
"from": "P4",
"to": "P6",
"label": "No",
"label_position": [
1160,
128
],
"points": [
[
1120,
144
],
[
1200,
144
]
],
"basis": "proposed",
"change": "unchanged",
"evidence": [
"E4"
],
"tasks": []
},
{
"from": "P5",
"to": "P6",
"label": "",
"points": [
[
1120,
368
],
[
1304,
368
],
[
1304,
192
]
],
"basis": "proposed",
"change": "unchanged",
"evidence": [
"E4"
],
"tasks": []
}
]
}
],
"ui_review": {
"kind": "illustration",
"mode": "unselected",
"title": "UI screen flow — example inside the same report",
"summary": "See the screens and feedback states, not just boxes naming them. Arrows show actions or outcomes; D1 and D2 point to design choices on the controls.",
"limit": "Schematic previews, not pixel-matched kit captures. This example is separate from the shared-rules task above. No styling or app implementation is approved.",
"authority": {
"status": "unconfirmed",
"source": "UIREF",
"locator": "Preview authority and limits",
"summary": "Fictional illustration only. No real project kit or styling decision has been selected."
},
"preview": {
"status": "authorised",
"source": "UIREF",
"locator": "Preview authority",
"summary": "This isolated local report example only; no new application route or production UI.",
"destination": "This offline report, not either application"
},
"acceptance": {
"status": "pending",
"summary": "No design alternative has been accepted for either project."
},
"width": 1152,
"height": 1024,
"open_first": false,
"screens": [
{
"id": "UI1",
"title": "01 · Edit preference",
"description": "Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.",
"state": "proposed",
"x": 16,
"y": 48,
"width": 320,
"height": 384,
"representation": "schematic",
"content": [
{
"kind": "heading",
"label": "Notifications"
},
{
"kind": "text",
"label": "Choose what you receive."
},
{
"kind": "checkbox",
"label": "Email notifications",
"checked": true,
"decision": "D1"
},
{
"kind": "button",
"label": "Save"
},
{
"kind": "text",
"label": "Changes are not yet saved."
}
],
"sources": [
"UIREF"
],
"requirements": [],
"evidence": [],
"tasks": [],
"decisions": [
"D1"
]
},
{
"id": "UI2",
"title": "02 · Saving",
"description": "Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.",
"state": "proposed",
"x": 416,
"y": 48,
"width": 320,
"height": 384,
"representation": "schematic",
"content": [
{
"kind": "heading",
"label": "Notifications"
},
{
"kind": "text",
"label": "Your choice stays visible."
},
{
"kind": "checkbox",
"label": "Email notifications",
"checked": true,
"decision": "D1"
},
{
"kind": "button",
"label": "Saving…",
"disabled": true
},
{
"kind": "text",
"label": "Wait for the save result."
}
],
"sources": [
"UIREF"
],
"requirements": [],
"evidence": [],
"tasks": [],
"decisions": [
"D1"
]
},
{
"id": "UI3",
"title": "03 · Saved",
"description": "Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.",
"state": "proposed",
"x": 816,
"y": 48,
"width": 320,
"height": 384,
"representation": "schematic",
"content": [
{
"kind": "heading",
"label": "Notifications"
},
{
"kind": "text",
"label": "Your choice stays visible."
},
{
"kind": "checkbox",
"label": "Email notifications",
"checked": true,
"decision": "D1"
},
{
"kind": "message",
"label": "Preference saved.",
"decision": "D2"
},
{
"kind": "button",
"label": "Save"
}
],
"sources": [
"UIREF"
],
"requirements": [],
"evidence": [],
"tasks": [],
"decisions": [
"D1",
"D2"
]
},
{
"id": "UI4",
"title": "04 · Save failed",
"description": "Illustrative notification setting; neither current application was inspected or styled for this preview. A failed save keeps the selection so the user can retry.",
"state": "proposed",
"x": 416,
"y": 592,
"width": 320,
"height": 384,
"representation": "schematic",
"content": [
{
"kind": "heading",
"label": "Notifications"
},
{
"kind": "text",
"label": "Your choice stays visible."
},
{
"kind": "checkbox",
"label": "Email notifications",
"checked": true,
"decision": "D1"
},
{
"kind": "message",
"label": "Could not save. Try again.",
"decision": "D2"
},
{
"kind": "button",
"label": "Try again"
}
],
"sources": [
"UIREF"
],
"requirements": [],
"evidence": [],
"tasks": [],
"decisions": [
"D1",
"D2"
]
}
],
"transitions": [
{
"from": "UI1",
"to": "UI2",
"label": "Save",
"points": [
[
336,
240
],
[
416,
240
]
],
"label_position": [
376,
216
]
},
{
"from": "UI2",
"to": "UI3",
"label": "Saved",
"points": [
[
736,
240
],
[
816,
240
]
],
"label_position": [
776,
216
]
},
{
"from": "UI2",
"to": "UI4",
"label": "Failed",
"points": [
[
576,
432
],
[
576,
592
]
],
"label_position": [
624,
512
]
},
{
"from": "UI4",
"to": "UI2",
"label": "Retry",
"points": [
[
736,
784
],
[
784,
784
],
[
784,
496
],
[
672,
496
],
[
672,
432
]
],
"label_position": [
728,
476
]
}
],
"decisions": [
{
"id": "D1",
"title": "Preference control",
"question": "Should this be the approved checkbox or the approved switch? The drawing shows a checkbox as an example, not a selection.",
"options": [
"Approved checkbox",
"Approved switch"
],
"status": "open",
"screens": [
"UI1",
"UI2",
"UI3",
"UI4"
]
},
{
"id": "D2",
"title": "Feedback position",
"question": "Where should the approved success and error feedback appear? The sample shows inline text only.",
"options": [
"Inside the form",
"Approved floating message"
],
"status": "open",
"screens": [
"UI3",
"UI4"
]
}
]
}
}
examples/ui-review/ui-baseline.json{
"fixture": true,
"screen": "Notification preferences",
"fields": [
{
"name": "Email notifications",
"value": true,
"editable": false
}
],
"save_action": false
}
examples/ui-review/UI-FINDINGS.md# Synthetic fixture evidence ui-baseline.json defines this example's current view: a read-only email preference with no editable control or save action. This is known only within the fixture; it does not describe Altinstar or Cheflabels. No real screen, API, test, kit or device has been inspected for this example. The proposed four screens show edit, saving, success and failure. D1 chooses an approved control; D2 chooses feedback placement. The initial drawn checkbox and inline text are illustrative, not selections. No kit is invented, no browser control saves anything, and no implementation/design approval is recorded.
examples/ui-review/UI-PLAN.md# Synthetic UI task — notification preferences This is a reusable report example, not an approved feature for a real application. ## Current checkpoint Plan R2 is a draft. Implementation has not started. Design/source selection is unresolved; no app publication is authorised. The owner requested a visual report sample, so isolated schematics in this report are allowed. Next action in a real task: select an approved kit or agree a bounded functional preview and resolve the marked choices. ## Requested example behaviour Allow someone to change the email-notification preference and explicitly save it. Keep the selected value visible while saving and on failure. Prevent duplicate submissions; show a successful result or let the user retry a failed save. No automatic retry, additional settings, new dependencies or invented application theme. ## Scope Use the existing settings owner and persistence mechanism after inspecting them in the real project. This fixture contains only a baseline description, not live application code. Tests and design acceptance are not claimed. ## Revision history R1: general idea, behaviour still ambiguous. R2: explicit save, retained selection and manual retry proposed; control and feedback presentation remain undecided. Neither synthetic revision is real application authority.
examples/ui-review/ui-task-review-r2.html<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-YndMZv6yWmKjZPrLFg9fwPIzifD6yGUbESh4tYyfEeU='; style-src 'unsafe-inline'; img-src 'none'; connect-src 'none'; font-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">
<title>Notification preferences: review the screens before building</title><style>:root {
--paper:#f5f5f5; --ink:#2d3142; --muted:#4f5d75; --rule:#cdd0d5;
--accent:#bf4520; --accent-tint:#f5e9e4;
--sans:'Geist','Inter','Segoe UI',system-ui,sans-serif;
--serif:'Instrument Serif',Georgia,'Times New Roman',serif;
--mono:'Geist Mono',ui-monospace,'Cascadia Mono',monospace;
color-scheme:light;
}
* { box-sizing:border-box; }
body { margin:0; background:var(--paper); color:var(--ink); font:15px/1.6 var(--sans); }
main { max-width:1392px; margin:auto; padding:36px 56px 48px; }
a { color:var(--ink); text-underline-offset:4px; }
a:hover { color:var(--accent); }
a:focus-visible,summary:focus-visible,.canvas:focus-visible { outline:2px solid var(--accent); outline-offset:5px; }
header { padding-bottom:24px; }
.mast { display:flex; justify-content:space-between; gap:20px; border-bottom:1px solid var(--rule); padding-bottom:14px; font:11px/1.5 var(--mono); letter-spacing:.1em; color:var(--muted); }
.mast strong { font-weight:500; color:var(--ink); }
h1 { font:400 clamp(38px,4vw,58px)/1.08 var(--serif); letter-spacing:-.035em; margin:25px 0 14px; max-width:1080px; }
h2 { font-size:20px; line-height:1.4; font-weight:600; margin:0 0 12px; }
.status { font:11px/1.6 var(--mono); letter-spacing:.04em; color:var(--accent); margin:0 0 14px; }
.lead { font-size:16px; color:var(--muted); margin:0; max-width:1040px; }
.muted,small { color:var(--muted); }
.review { position:relative; border-top:1px solid var(--ink); }
.sr-radio { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0,0,0,0); clip-path:inset(50%); white-space:nowrap; }
.viewbar { display:flex; justify-content:space-between; align-items:center; gap:16px; border-bottom:1px solid var(--rule); min-height:60px; }
.view-tabs,.zoom-tabs { display:flex; align-items:center; gap:24px; flex-wrap:wrap; }
.view-tabs label,.zoom-tabs label { cursor:pointer; user-select:none; line-height:58px; color:var(--muted); border-bottom:2px solid transparent; font-size:14px; }
.view-tabs label { font-weight:550; }
.zoom-tabs { gap:14px; flex-shrink:0; }
.zoom-tabs label { font:11px/58px var(--mono); }
.figures { --scale:1; }
#zoom-125:checked~.figures { --scale:1.25; }
#zoom-150:checked~.figures { --scale:1.5; }
#zoom-fit:checked~.figures .map { width:100%; min-width:0; }
.panel { display:none; margin:0; }
.figure-head { display:flex; justify-content:space-between; align-items:baseline; gap:24px; padding:22px 0 4px; }
.figure-head h2 { font-size:18px; margin:0; }
.figure-head span { font:10px var(--mono); color:var(--muted); text-transform:uppercase; white-space:nowrap; }
.figure-description { margin:6px 0 12px; font-size:13px; color:var(--muted); max-width:1100px; }
.hint { margin:0 0 4px; font-size:11px; color:var(--muted); }
.canvas { max-width:100%; overflow-x:auto; overflow-y:hidden; scrollbar-width:thin; scrollbar-color:#9198a5 transparent; overscroll-behavior-x:contain; cursor:grab; }
.canvas.dragging { cursor:grabbing; user-select:none; }
.map { display:block; width:calc(var(--board-width) * var(--scale)); min-width:calc(var(--board-width) * var(--scale)); height:auto; }
.node-shape { fill:#fff; stroke:var(--ink); stroke-width:1.2; }
.start .node-shape,.result .node-shape { fill:#edf0f2; stroke:var(--muted); }
.focal .node-shape { fill:var(--accent-tint); stroke:var(--accent); stroke-width:1.4; }
.node.inferred .node-shape,.node.unknown .node-shape { stroke-dasharray:5 4; }
.connector { fill:none; stroke:var(--muted); stroke-width:1.4; }
.connector.inferred,.connector.unknown { stroke-dasharray:5 4; }
.map marker path { fill:var(--muted); }
.node-title { font:600 18px var(--sans); fill:var(--ink); }
.node-note { font:13px var(--sans); fill:var(--muted); }
.node-item { font:15px var(--sans); fill:var(--ink); }
.arrow-label rect { fill:var(--paper); }
.edge-label { font:10px var(--mono); letter-spacing:1px; fill:var(--muted); }
figcaption { font-size:12px; color:var(--muted); margin:12px 0 24px; }
.legend { display:flex; flex-wrap:wrap; align-items:center; gap:16px 32px; border-top:1px solid var(--rule); padding:16px 0 10px; }
.legend span { display:flex; align-items:center; gap:10px; }
.legend i { display:inline-block; width:26px; height:16px; border:1px solid var(--muted); background:#fff; }
.legend-start { border-radius:10px; }
.legend-action,.legend-group { border-radius:3px; }
.legend .legend-decision { width:16px; height:16px; transform:rotate(45deg); margin:0 5px; }
.legend .legend-focus { background:var(--accent-tint); border-color:var(--accent); border-radius:3px; }
.comparison { border-top:1px solid var(--ink); padding-top:24px; margin-top:8px; }
.comparison h2,.boundaries h2 { font:400 30px var(--serif); }
.table-scroll { max-width:100%; overflow-x:auto; }
table { border-collapse:collapse; width:100%; font-size:14px; text-align:left; }
th { font:10px var(--mono); letter-spacing:.08em; text-transform:uppercase; color:var(--muted); padding:14px 18px 12px 0; border-bottom:1px solid var(--rule); vertical-align:bottom; }
td { padding:16px 24px 16px 0; border-bottom:1px solid var(--rule); vertical-align:top; }
td:first-child { width:20%; }
td:last-child { padding-right:0; }
.comparison td:nth-child(2) { width:36%; color:var(--muted); }
small { display:block; font-size:12px; line-height:1.6; margin-top:5px; }
.boundaries { padding-top:24px; }
.boundaries ul { padding-left:20px; font-size:14px; }
.two-col { display:grid; grid-template-columns:1fr 1.2fr; gap:48px; margin:32px 0; }
.plain { list-style:none; padding:0; margin:0; }
.plain li { padding:13px 0; border-bottom:1px solid var(--rule); font-size:14px; }
.approval { border-top:1px solid var(--ink); padding:24px 0; display:grid; grid-template-columns:230px 1fr; gap:32px; }
.approval h2 { font:400 30px var(--serif); }
.approval p { margin:0 0 10px; font-size:14px; }
.warning { padding:18px 0; margin:0 0 20px; border-top:1px solid var(--accent); border-bottom:1px solid var(--rule); }
.warning h2 { color:var(--accent); font-size:17px; }
details { border-top:1px solid var(--rule); padding:17px 0; }
summary { font-size:14px; font-weight:550; cursor:pointer; list-style-position:inside; }
.appendix-body { padding-top:24px; }
.appendix-body section { margin-bottom:30px; }
.appendix table { min-width:840px; font-size:12px; }
.appendix td:first-child { width:auto; }
.appendix ol { font-size:12px; padding-left:22px; }
.appendix ol li { margin:10px 0; }
.evidence { padding:18px 0; border-top:1px solid var(--rule); }
.evidence strong { font-size:14px; }
.evidence p,code { font:12px/1.8 var(--mono); overflow-wrap:anywhere; }
.snapshot p { font-size:12px; }
footer { border-top:1px solid var(--rule); padding-top:14px; margin-top:28px; color:var(--muted); font:10px/1.7 var(--mono); }
li,tr,article { scroll-margin-top:24px; }
@media(max-width:800px) {
main { padding:22px 20px 36px; }
h1 { font-size:39px; }
.mast { font-size:9px; }
.viewbar { display:block; }
.view-tabs { gap:18px; }
.view-tabs label { font-size:12px; line-height:46px; }
.zoom-tabs { justify-content:flex-end; border-top:1px solid var(--rule); }
.zoom-tabs label { line-height:34px; }
.figure-head { display:block; }
.figure-head span { display:block; margin-top:8px; }
.comparison table { min-width:740px; }
.two-col,.approval { grid-template-columns:1fr; gap:20px; }
}
@media print {
main { max-width:none; padding:0; }
.viewbar,.hint { display:none!important; }
.panel { display:block!important; break-inside:avoid; }
.canvas { overflow:visible; }
.map { width:100%!important; min-width:0!important; }
.figure-head { margin-top:25px; }
.approval { break-inside:avoid; }
a { color:inherit; }
}
.task-checkpoint { border-top:1px solid var(--ink); padding:20px 0 24px; }
.task-checkpoint h2 { font:400 28px var(--serif); margin:0 0 6px; }
.checkpoint-asof,.checkpoint-limit,.flow-context { font-size:12px; color:var(--muted); overflow-wrap:anywhere; }
.checkpoint-asof { margin:0 0 18px; }
.checkpoint-axes { display:grid; grid-template-columns:1fr 1fr 1fr; gap:28px; margin:0 0 16px; }
.checkpoint-axes > div { min-width:0; }
.checkpoint-axes dt { font:10px/1.6 var(--mono); text-transform:uppercase; letter-spacing:.05em; color:var(--muted); }
.checkpoint-axes dd { margin:5px 0 0; font-size:14px; overflow-wrap:anywhere; }
.checkpoint-axes dd p { margin:5px 0 0; font-size:13px; color:var(--muted); }
.next-action { font-size:14px; border-top:1px solid var(--rule); padding-top:14px; margin:0; overflow-wrap:anywhere; }
.next-action strong { margin-right:12px; }
.checkpoint-limit { margin:12px 0 0; }
.flow-context { margin:0 0 14px; }
.revision-log { margin-bottom:12px; }
.revision-log > p { font-size:12px; color:var(--muted); }
.revision-log table { min-width:740px; }
.revision-log td { overflow-wrap:anywhere; }
@media(max-width:800px) { .checkpoint-axes { grid-template-columns:1fr; gap:16px; } }
@media print { .task-checkpoint { break-inside:avoid; } }
#view-proposed:checked~.figures #panel-proposed{display:block}
#view-proposed:checked~.viewbar label[for="view-proposed"]{border-color:var(--accent);color:var(--ink)}
#view-proposed:focus-visible~.viewbar label[for="view-proposed"]{outline:2px solid var(--accent);outline-offset:4px}
#view-current:checked~.figures #panel-current{display:block}
#view-current:checked~.viewbar label[for="view-current"]{border-color:var(--accent);color:var(--ink)}
#view-current:focus-visible~.viewbar label[for="view-current"]{outline:2px solid var(--accent);outline-offset:4px}
#view-build:checked~.figures #panel-build{display:block}
#view-build:checked~.viewbar label[for="view-build"]{border-color:var(--accent);color:var(--ink)}
#view-build:focus-visible~.viewbar label[for="view-build"]{outline:2px solid var(--accent);outline-offset:4px}
#view-ui-preview:checked~.figures #panel-ui-preview{display:block}
#view-ui-preview:checked~.viewbar label[for="view-ui-preview"]{border-color:var(--accent);color:var(--ink)}
#view-ui-preview:focus-visible~.viewbar label[for="view-ui-preview"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-100:checked~.viewbar label[for="zoom-100"]{border-color:var(--ink);color:var(--ink)}
#zoom-100:focus-visible~.viewbar label[for="zoom-100"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-125:checked~.viewbar label[for="zoom-125"]{border-color:var(--ink);color:var(--ink)}
#zoom-125:focus-visible~.viewbar label[for="zoom-125"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-150:checked~.viewbar label[for="zoom-150"]{border-color:var(--ink);color:var(--ink)}
#zoom-150:focus-visible~.viewbar label[for="zoom-150"]{outline:2px solid var(--accent);outline-offset:4px}
#zoom-fit:checked~.viewbar label[for="zoom-fit"]{border-color:var(--ink);color:var(--ink)}
#zoom-fit:focus-visible~.viewbar label[for="zoom-fit"]{outline:2px solid var(--accent);outline-offset:4px}
/* Optional screen-flow view. Existing editorial shell and application styles are not changed. */
.ui-review .ui-notice { border-left:2px solid var(--accent); padding:10px 16px; font-size:13px; }
.ui-review .ui-authority { border-top:1px solid var(--rule); border-bottom:1px solid var(--rule); padding:12px 0; margin:16px 0; font-size:13px; }
.ui-review .ui-authority p { margin:4px 0; }
.ui-scaled { width:calc(var(--ui-width) * var(--scale)); height:calc(var(--ui-height) * var(--scale)); position:relative; }
.ui-board { width:var(--ui-width); height:var(--ui-height); position:relative; transform:scale(var(--scale)); transform-origin:top left; }
.ui-screen-node { position:absolute; margin:0; }
.ui-routes { position:absolute; inset:0; pointer-events:none; }
.ui-routes marker path { fill:var(--muted); }
.ui-screen { display:block; max-width:100%; height:auto; }
.ui-screen-paper { fill:#fff; stroke:var(--rule); stroke-width:1.25; }
.ui-screen-caption { font:600 14px var(--sans); fill:var(--ink); }
.ui-divider { stroke:var(--rule); stroke-width:1; }
.ui-control-heading { font:600 17px var(--sans); fill:var(--ink); }
.ui-control-text { font:14px var(--sans); fill:var(--ink); }
.ui-control-small { font:12px var(--sans); fill:var(--muted); }
.ui-checkbox,.ui-field { fill:#fff; stroke:var(--muted); }
.ui-button { fill:#f5f5f5; stroke:var(--muted); }
.ui-button.ui-disabled { fill:#eee; stroke:#a4a8b0; }
.ui-check { fill:none; stroke:var(--ink); stroke-width:1.7; }
.ui-message-rule { stroke:var(--muted); stroke-width:2; }
.ui-marker { fill:var(--accent-tint); stroke:var(--accent); }
.ui-marker-text { fill:var(--accent); font:10px var(--mono); }
.ui-footnote { fill:var(--muted); font:10px var(--mono); }
.ui-inspect { position:absolute; right:12px; top:10px; font:10px/28px var(--mono); background:#fff; padding:0 4px; }
.ui-decisions { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:0 32px; margin:24px 0; }
.ui-decisions > h2 { grid-column:1/-1; }
.ui-decision { border-top:1px solid var(--rule); padding:16px 0; font-size:13px; }
.ui-decision h3 { margin:0 0 8px; font-size:15px; }
.ui-decision p { margin:6px 0; }
.ui-screen-detail .ui-screen { width:400px; margin:20px 0; }
.ui-screen-detail p,.ui-evidence { font-size:13px; }
.ui-review .ui-inspect:focus-visible,.ui-screen a:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
@media(max-width:800px) { .ui-decisions { grid-template-columns:1fr; } }
@media print { .ui-scaled { width:100%; height:auto; } .ui-board { transform:none; width:100%; height:auto; display:grid; grid-template-columns:1fr 1fr; gap:20px; }
.ui-screen-node { position:static; width:100%!important; height:auto!important; } .ui-routes,.ui-inspect { display:none; } .ui-authority { break-inside:avoid; } }
.ui-capture-detail { display:block; max-width:100%; height:auto; margin:20px 0; }
</style></head><body><main>
<header><div class="mast"><strong>Illustrative UI workflow · not a real application change</strong><span>Plan R2</span></div>
<h1>Notification preferences: review the screens before building</h1><p class="status draft">DRAFT. Decisions or review are still open</p><p class="lead">In this fictional example, you want people to change their email preference, save it, see whether it worked, and retry without losing their choice. The existing report stays intact; the UI screens view adds the proposed screen states and marked design questions. Nothing here is an approved design for Altinstar or Cheflabels.</p></header>
<section class="warning"><h2>Open decisions. The plan is not ready to approve</h2><ul><li>Name the existing element each control copies (kit or existing code: same tag, classes and wrapper), or ask which element to mimic; never new CSS.</li><li>D1: checkbox or switch? D2: feedback inside the form or an approved floating message?</li><li>Agree the actual preview destination, verification commands and required designer acceptance before implementation.</li></ul></section><section class="review" aria-label="Flowchart review"><input class="sr-radio" type="radio" name="diagram-view" id="view-proposed" aria-label="Feature flow" aria-controls="panel-proposed" ><input class="sr-radio" type="radio" name="diagram-view" id="view-current" aria-label="Current flow" aria-controls="panel-current" ><input class="sr-radio" type="radio" name="diagram-view" id="view-build" aria-label="Build plan" aria-controls="panel-build" ><input class="sr-radio" type="radio" name="diagram-view" id="view-ui-preview" aria-label="UI screens" aria-controls="panel-ui-preview" checked><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-100" aria-label="100% diagram size" checked><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-125" aria-label="125% diagram size" ><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-150" aria-label="150% diagram size" ><input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-fit" aria-label="Fit diagram size" ><div class="viewbar"><div class="view-tabs" role="group" aria-label="Diagram views"><label for="view-proposed">Feature flow</label><label for="view-current">Current flow</label><label for="view-build">Build plan</label><label for="view-ui-preview">UI screens</label></div><div class="zoom-tabs" role="group" aria-label="Diagram size"><label for="zoom-100">100%</label><label for="zoom-125">125%</label><label for="zoom-150">150%</label><label for="zoom-fit">Fit</label></div></div><div class="figures"><figure class="panel" id="panel-proposed"><div class="figure-head"><h2>Proposed: save and show the result</h2><span>proposed · flow</span></div><p class="figure-description">Proposed example behaviour. Failure retains the choice; retry is user-initiated. The UI screens tab shows the visible states and design questions.</p><p class="hint">Scroll sideways to explore. <a href="#node-8-proposed-N1">Go to start</a>. Fit is an optional overview.</p><div class="canvas" tabindex="0" role="region" aria-label="Feature flow diagram, scroll to explore" data-start-id="node-8-proposed-N1" style="--board-width:1280px"><svg class="map" xmlns="http://www.w3.org/2000/svg" width="1280" height="736" viewBox="0 0 1280 736" role="img" aria-labelledby="proposed-title proposed-desc"><title id="proposed-title">Proposed: save and show the result</title><desc id="proposed-desc">Proposed example behaviour. Failure retains the choice; retry is user-initiated. The UI screens tab shows the visible states and design questions.</desc><defs><marker id="arrow-proposed" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs><path class="connector proposed" data-from="N1" data-to="N2" data-points="[[640, 112], [640, 160]]" d="M 640 112 L 640 160" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="N2" data-to="N3" data-points="[[640, 248], [640, 320]]" d="M 640 248 L 640 320" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="N3" data-to="N4" data-points="[[768, 400], [920, 400]]" d="M 768 400 L 920 400" marker-end="url(#arrow-proposed)"/><path class="connector proposed" data-from="N3" data-to="N5" data-points="[[640, 480], [640, 576]]" d="M 640 480 L 640 576" marker-end="url(#arrow-proposed)"/><g class="arrow-label"><rect x="825.5" y="357" width="37" height="22"/><text class="edge-label" text-anchor="middle" x="844" y="372">YES</text></g><g class="arrow-label"><rect x="673" y="509" width="30" height="22"/><text class="edge-label" text-anchor="middle" x="688" y="524">NO</text></g><g id="node-8-proposed-N1" class="node start proposed " data-id="N1" data-box="464,40,352,72"><title>Edit preference. proposed / modified. </title><rect class="node-shape" x="464" y="40" width="352" height="72" rx="20"/><text class="node-title" x="640" y="82" text-anchor="middle">Edit preference</text></g><g id="node-8-proposed-N2" class="node action proposed " data-id="N2" data-box="464,160,352,88"><title>Save selected value. proposed / modified. </title><rect class="node-shape" x="464" y="160" width="352" height="88" rx="6"/><text class="node-title" x="640" y="210" text-anchor="middle">Save selected value</text></g><g id="node-8-proposed-N3" class="node decision proposed focal" data-id="N3" data-box="512,320,256,160"><title>Save
succeeded?. proposed / modified. </title><polygon class="node-shape" points="640,320 768,400 640,480 512,400"/><text class="node-title" x="640" y="394" text-anchor="middle">Save</text><text class="node-title" x="640" y="418" text-anchor="middle">succeeded?</text></g><g id="node-8-proposed-N4" class="node result proposed focal" data-id="N4" data-box="920,352,304,96"><title>Show confirmation. proposed / modified. </title><rect class="node-shape" x="920" y="352" width="304" height="96" rx="20"/><text class="node-title" x="1072" y="406" text-anchor="middle">Show confirmation</text></g><g id="node-8-proposed-N5" class="node result proposed " data-id="N5" data-box="460,576,360,88"><title>Keep choice; allow retry. proposed / modified. </title><rect class="node-shape" x="460" y="576" width="360" height="88" rx="20"/><text class="node-title" x="640" y="626" text-anchor="middle">Keep choice; allow retry</text></g></svg></div><figcaption><div class="legend"><span><i class="legend-start" aria-hidden="true"></i>Start / result</span><span><i class="legend-action" aria-hidden="true"></i>Action</span><span><i class="legend-decision" aria-hidden="true"></i>Decision</span><span><i class="legend-focus" aria-hidden="true"></i>Focus of the change</span></div><span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. Proposed is not implemented.</span></figcaption></figure><figure class="panel" id="panel-current"><div class="figure-head"><h2>Current fixture: the preference is read-only</h2><span>current · flow</span></div><p class="figure-description">The current state belongs only to ui-baseline.json. Arrows show the simple opening/display path, not a real app audit.</p><p class="hint">Scroll sideways to explore. <a href="#node-7-current-N1">Go to start</a>. Fit is an optional overview.</p><div class="canvas" tabindex="0" role="region" aria-label="Current flow diagram, scroll to explore" data-start-id="node-7-current-N1" style="--board-width:1280px"><svg class="map" xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640" role="img" aria-labelledby="current-title current-desc"><title id="current-title">Current fixture: the preference is read-only</title><desc id="current-desc">The current state belongs only to ui-baseline.json. Arrows show the simple opening/display path, not a real app audit.</desc><defs><marker id="arrow-current" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs><path class="connector observed" data-from="N1" data-to="N2" data-points="[[336, 200], [456, 200]]" d="M 336 200 L 456 200" marker-end="url(#arrow-current)"/><path class="connector observed" data-from="N2" data-to="N3" data-points="[[800, 200], [920, 200]]" d="M 800 200 L 920 200" marker-end="url(#arrow-current)"/><g id="node-7-current-N1" class="node start observed " data-id="N1" data-box="48,152,288,96"><title>Open settings. observed / unchanged. </title><rect class="node-shape" x="48" y="152" width="288" height="96" rx="20"/><text class="node-title" x="192" y="206" text-anchor="middle">Open settings</text></g><g id="node-7-current-N2" class="node action observed " data-id="N2" data-box="456,152,344,96"><title>Read-only preference. observed / unchanged. </title><rect class="node-shape" x="456" y="152" width="344" height="96" rx="6"/><text class="node-title" x="628" y="206" text-anchor="middle">Read-only preference</text></g><g id="node-7-current-N3" class="node result observed " data-id="N3" data-box="920,152,304,96"><title>View current value. observed / unchanged. </title><rect class="node-shape" x="920" y="152" width="304" height="96" rx="20"/><text class="node-title" x="1072" y="206" text-anchor="middle">View current value</text></g></svg></div><figcaption><div class="legend"><span><i class="legend-start" aria-hidden="true"></i>Start / result</span><span><i class="legend-action" aria-hidden="true"></i>Action</span></div><span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. Proposed is not implemented.</span></figcaption></figure><figure class="panel" id="panel-build"><div class="figure-head"><h2>Build plan — after approval only</h2><span>proposed · delivery</span></div><p class="figure-description">These are delivery dependencies, not the feature runtime. Approval must be recorded before T1 begins; no diagram action grants it.</p><p class="hint">Scroll sideways to explore. <a href="#node-5-build-approval">Go to start</a>. Fit is an optional overview.</p><div class="canvas" tabindex="0" role="region" aria-label="Build plan diagram, scroll to explore" data-start-id="node-5-build-approval" style="--board-width:1280px"><svg class="map" xmlns="http://www.w3.org/2000/svg" width="1280" height="848" viewBox="0 0 1280 848" role="img" aria-labelledby="build-title build-desc"><title id="build-title">Build plan — after approval only</title><desc id="build-desc">These are delivery dependencies, not the feature runtime. Approval must be recorded before T1 begins; no diagram action grants it.</desc><defs><marker id="arrow-build" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs><path class="connector proposed" data-from="approval" data-to="B1" data-points="[[640, 104], [640, 176]]" d="M 640 104 L 640 176" marker-end="url(#arrow-build)"/><path class="connector proposed" data-from="B1" data-to="B2" data-points="[[640, 264], [640, 328]]" d="M 640 264 L 640 328" marker-end="url(#arrow-build)"/><path class="connector proposed" data-from="B2" data-to="B3" data-points="[[640, 416], [640, 480]]" d="M 640 416 L 640 480" marker-end="url(#arrow-build)"/><path class="connector proposed" data-from="B3" data-to="B4" data-points="[[640, 576], [640, 640]]" d="M 640 576 L 640 640" marker-end="url(#arrow-build)"/><g id="node-5-build-approval" class="node start proposed focal" data-id="approval" data-box="472,40,336,64"><title>User approves this plan. proposed / unchanged. </title><rect class="node-shape" x="472" y="40" width="336" height="64" rx="20"/><text class="node-title" x="640" y="78" text-anchor="middle">User approves this plan</text></g><g id="node-5-build-B1" class="node action proposed " data-id="B1" data-box="432,176,416,88"><title>Add behaviour tests. proposed / modified. </title><rect class="node-shape" x="432" y="176" width="416" height="88" rx="6"/><text class="node-title" x="640" y="226" text-anchor="middle">Add behaviour tests</text></g><g id="node-5-build-B2" class="node action proposed " data-id="B2" data-box="432,328,416,88"><title>Implement saving. proposed / modified. </title><rect class="node-shape" x="432" y="328" width="416" height="88" rx="6"/><text class="node-title" x="640" y="378" text-anchor="middle">Implement saving</text></g><g id="node-5-build-B3" class="node action proposed " data-id="B3" data-box="432,480,416,96"><title>Use approved presentation. proposed / modified. </title><rect class="node-shape" x="432" y="480" width="416" height="96" rx="6"/><text class="node-title" x="640" y="534" text-anchor="middle">Use approved presentation</text></g><g id="node-5-build-B4" class="node result proposed " data-id="B4" data-box="432,640,416,96"><title>Verify and hand off. proposed / modified. </title><rect class="node-shape" x="432" y="640" width="416" height="96" rx="20"/><text class="node-title" x="640" y="694" text-anchor="middle">Verify and hand off</text></g></svg></div><figcaption><div class="legend"><span><i class="legend-start" aria-hidden="true"></i>Start / result</span><span><i class="legend-action" aria-hidden="true"></i>Action</span><span><i class="legend-focus" aria-hidden="true"></i>Focus of the change</span></div><span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. Proposed is not implemented.</span></figcaption></figure><figure class="panel ui-review" id="panel-ui-preview"><div class="figure-head"><h2>The proposed interface, state by state</h2><span>screens · interaction · design</span></div><p class="figure-description">See the screens and feedback states, not just boxes naming them. Arrows show actions or outcomes; D1 and D2 point to design choices on the controls.</p><div class="ui-authority"><p><strong>Design mode:</strong> unselected · unconfirmed.</p><p>Fictional illustration only. No real project kit or styling decision has been selected. Reference: <a href="#s-UIREF">UIREF</a>, Preview authority and limits.</p><p><strong>Preview:</strong> This offline report, not either application, not-needed. This isolated local report example only; no new application route or production UI.</p><p><strong>Design acceptance:</strong> pending. No design alternative has been accepted for either project.</p></div><p class="hint">Screen frames are connected by actions. Markers refer to design decisions below. <a href="#ui-screen-UI1">Go to first screen</a>. Scroll at readable size; Fit is an overview.</p><noscript><p class="hint">Screen Fit needs JavaScript; use normal-size scrolling and the screen details below.</p></noscript><div class="canvas ui-canvas" tabindex="0" role="region" aria-label="UI screen flow, scroll to explore" data-start-id="ui-screen-UI1" style="--board-width:1152px"><div class="ui-scaled" style="--ui-width:1152px;--ui-height:1024px"><div class="ui-board"><svg class="ui-routes" width="1152" height="1024" viewBox="0 0 1152 1024" aria-hidden="true"><defs><marker id="ui-flow-arrow" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto"><path d="M0 0 L10 4 L0 8 Z"/></marker></defs><path class="connector" d="M 336 240 L 416 240" marker-end="url(#ui-flow-arrow)"/><g class="arrow-label"><rect x="354" y="201" width="44" height="22"/><text class="edge-label" x="376" y="216" text-anchor="middle">SAVE</text></g><path class="connector" d="M 736 240 L 816 240" marker-end="url(#ui-flow-arrow)"/><g class="arrow-label"><rect x="750.5" y="201" width="51" height="22"/><text class="edge-label" x="776" y="216" text-anchor="middle">SAVED</text></g><path class="connector" d="M 576 432 L 576 592" marker-end="url(#ui-flow-arrow)"/><g class="arrow-label"><rect x="595" y="497" width="58" height="22"/><text class="edge-label" x="624" y="512" text-anchor="middle">FAILED</text></g><path class="connector" d="M 736 784 L 776 784 Q 784 784 784 776 L 784 504 Q 784 496 776 496 L 680 496 Q 672 496 672 488 L 672 432" marker-end="url(#ui-flow-arrow)"/><g class="arrow-label"><rect x="702.5" y="461" width="51" height="22"/><text class="edge-label" x="728" y="476" text-anchor="middle">RETRY</text></g></svg><article class="ui-screen-node" id="ui-screen-UI1" style="left:16px;top:48px;width:320px;height:384px"><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-board-UI1-title ui-board-UI1-desc"><title id="ui-board-UI1-title">01 · Edit preference</title><desc id="ui-board-UI1-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">01 · Edit preference</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Choose what you receive.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><rect class="ui-button" x="24" y="210" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="233">Save</text><text class="ui-control-small" x="24" y="278">Changes are not yet saved.</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><a class="ui-inspect" href="#ui-detail-UI1">Inspect screen ↗</a></article><article class="ui-screen-node" id="ui-screen-UI2" style="left:416px;top:48px;width:320px;height:384px"><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-board-UI2-title ui-board-UI2-desc"><title id="ui-board-UI2-title">02 · Saving</title><desc id="ui-board-UI2-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">02 · Saving</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><rect class="ui-button ui-disabled" x="24" y="210" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="233">Saving…</text><text class="ui-control-small" x="24" y="278">Wait for the save result.</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><a class="ui-inspect" href="#ui-detail-UI2">Inspect screen ↗</a></article><article class="ui-screen-node" id="ui-screen-UI3" style="left:816px;top:48px;width:320px;height:384px"><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-board-UI3-title ui-board-UI3-desc"><title id="ui-board-UI3-title">03 · Saved</title><desc id="ui-board-UI3-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">03 · Saved</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><path class="ui-message-rule" d="M 24 214 V 238"/><text class="ui-control-text" x="36" y="230">Preference saved.</text><a href="#ui-decision-D2" aria-label="Review decision D2"><circle class="ui-marker" cx="292" cy="225" r="13"/><text class="ui-marker-text" x="292" y="229" text-anchor="middle">D2</text></a><rect class="ui-button" x="24" y="258" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="281">Save</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><a class="ui-inspect" href="#ui-detail-UI3">Inspect screen ↗</a></article><article class="ui-screen-node" id="ui-screen-UI4" style="left:416px;top:592px;width:320px;height:384px"><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-board-UI4-title ui-board-UI4-desc"><title id="ui-board-UI4-title">04 · Save failed</title><desc id="ui-board-UI4-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. A failed save keeps the selection so the user can retry.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">04 · Save failed</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><path class="ui-message-rule" d="M 24 214 V 238"/><text class="ui-control-text" x="36" y="230">Could not save. Try again.</text><a href="#ui-decision-D2" aria-label="Review decision D2"><circle class="ui-marker" cx="292" cy="225" r="13"/><text class="ui-marker-text" x="292" y="229" text-anchor="middle">D2</text></a><rect class="ui-button" x="24" y="258" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="281">Try again</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><a class="ui-inspect" href="#ui-detail-UI4">Inspect screen ↗</a></article></div></div></div><figcaption>A synthetic UI task. These are authorised report schematics, not a real project kit, working application or approved design. Selecting or inspecting a screen does not record approval or change the application.</figcaption><section class="ui-decisions"><h2>Design decisions on these screens</h2><article id="ui-decision-D1" class="ui-decision"><h3>D1 · Preference control</h3><p>Should this be the approved checkbox or the approved switch? The drawing shows a checkbox as an example, not a selection.</p><p>Alternatives: Approved checkbox / Approved switch.</p><p><strong>Open: the owner must decide</strong></p></article><article id="ui-decision-D2" class="ui-decision"><h3>D2 · Feedback position</h3><p>Where should the approved success and error feedback appear? The sample shows inline text only.</p><p>Alternatives: Inside the form / Approved floating message.</p><p><strong>Open: the owner must decide</strong></p></article></section><details class="ui-screen-index"><summary>Inspect screens and their sources</summary><details id="ui-detail-UI1" class="ui-screen-detail"><summary>01 · Edit preference: screen and sources</summary><p>Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</p><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-detail-UI1-title ui-detail-UI1-desc"><title id="ui-detail-UI1-title">01 · Edit preference</title><desc id="ui-detail-UI1-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">01 · Edit preference</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Choose what you receive.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><rect class="ui-button" x="24" y="210" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="233">Save</text><text class="ui-control-small" x="24" y="278">Changes are not yet saved.</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><p class="muted"><strong>Component provenance not established.</strong> This view shows structure, not proof of the final look.</p><p class="ui-evidence">State: proposed. Sources: <a href="#s-UIREF">UIREF</a>. Requirements: <a href="#r-R1">R1</a> <a href="#r-R2">R2</a>. Tasks: <a href="#t-T2">T2</a> <a href="#t-T3">T3</a>.</p></details><details id="ui-detail-UI2" class="ui-screen-detail"><summary>02 · Saving: screen and sources</summary><p>Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</p><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-detail-UI2-title ui-detail-UI2-desc"><title id="ui-detail-UI2-title">02 · Saving</title><desc id="ui-detail-UI2-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">02 · Saving</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><rect class="ui-button ui-disabled" x="24" y="210" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="233">Saving…</text><text class="ui-control-small" x="24" y="278">Wait for the save result.</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><p class="muted"><strong>Component provenance not established.</strong> This view shows structure, not proof of the final look.</p><p class="ui-evidence">State: proposed. Sources: <a href="#s-UIREF">UIREF</a>. Requirements: <a href="#r-R1">R1</a> <a href="#r-R2">R2</a>. Tasks: <a href="#t-T2">T2</a> <a href="#t-T3">T3</a>.</p></details><details id="ui-detail-UI3" class="ui-screen-detail"><summary>03 · Saved: screen and sources</summary><p>Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</p><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-detail-UI3-title ui-detail-UI3-desc"><title id="ui-detail-UI3-title">03 · Saved</title><desc id="ui-detail-UI3-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">03 · Saved</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><path class="ui-message-rule" d="M 24 214 V 238"/><text class="ui-control-text" x="36" y="230">Preference saved.</text><a href="#ui-decision-D2" aria-label="Review decision D2"><circle class="ui-marker" cx="292" cy="225" r="13"/><text class="ui-marker-text" x="292" y="229" text-anchor="middle">D2</text></a><rect class="ui-button" x="24" y="258" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="281">Save</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><p class="muted"><strong>Component provenance not established.</strong> This view shows structure, not proof of the final look.</p><p class="ui-evidence">State: proposed. Sources: <a href="#s-UIREF">UIREF</a>. Requirements: <a href="#r-R1">R1</a> <a href="#r-R2">R2</a>. Tasks: <a href="#t-T2">T2</a> <a href="#t-T3">T3</a>.</p></details><details id="ui-detail-UI4" class="ui-screen-detail"><summary>04 · Save failed: screen and sources</summary><p>Illustrative notification setting; neither current application was inspected or styled for this preview. A failed save keeps the selection so the user can retry.</p><svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="320" height="384" viewBox="0 0 320 384" role="img" aria-labelledby="ui-detail-UI4-title ui-detail-UI4-desc"><title id="ui-detail-UI4-title">04 · Save failed</title><desc id="ui-detail-UI4-desc">Illustrative notification setting; neither current application was inspected or styled for this preview. A failed save keeps the selection so the user can retry.</desc><rect class="ui-screen-paper" x="1" y="1" width="318" height="382" rx="6"/><path class="ui-divider" d="M 1 48 H 319"/><text class="ui-screen-caption" x="20" y="30">04 · Save failed</text><text class="ui-control-heading" x="24" y="86">Notifications</text><text class="ui-control-small" x="24" y="134">Your choice stays visible.</text><rect class="ui-checkbox" x="24" y="168" width="16" height="16" rx="1"/><path class="ui-check" d="M 27 176 l 4 4 l 6 -9"/><text class="ui-control-text" x="50" y="182">Email notifications</text><a href="#ui-decision-D1" aria-label="Review decision D1"><circle class="ui-marker" cx="292" cy="177" r="13"/><text class="ui-marker-text" x="292" y="181" text-anchor="middle">D1</text></a><path class="ui-message-rule" d="M 24 214 V 238"/><text class="ui-control-text" x="36" y="230">Could not save. Try again.</text><a href="#ui-decision-D2" aria-label="Review decision D2"><circle class="ui-marker" cx="292" cy="225" r="13"/><text class="ui-marker-text" x="292" y="229" text-anchor="middle">D2</text></a><rect class="ui-button" x="24" y="258" width="120" height="36" rx="3"/><text class="ui-control-text" text-anchor="middle" x="84.0" y="281">Try again</text><text class="ui-footnote" x="20" y="368">Sketch · not a screenshot</text></svg><p class="muted"><strong>Component provenance not established.</strong> This view shows structure, not proof of the final look.</p><p class="ui-evidence">State: proposed. Sources: <a href="#s-UIREF">UIREF</a>. Requirements: <a href="#r-R1">R1</a> <a href="#r-R2">R2</a>. Tasks: <a href="#t-T2">T2</a> <a href="#t-T3">T3</a>.</p></details></details></figure></div></section>
<section class="comparison"><h2>What changes</h2><div class="table-scroll"><table><thead><tr><th>Area</th><th>Today, from the recorded evidence</th><th>Proposed scope</th></tr></thead><tbody><tr><td><strong>Changing a preference</strong><small><a href="#r-R1">R1</a></small></td><td>Synthetic baseline is read-only.<small><a href="#e-E1">E1</a></small></td><td>An agreed control allows editing and explicit save.<small><a href="#t-T1">T1</a> <a href="#t-T2">T2</a> <a href="#t-T3">T3</a></small></td></tr><tr><td><strong>Saving and failures</strong><small><a href="#r-R2">R2</a></small></td><td>No save action exists in the fixture.<small><a href="#e-E1">E1</a> <a href="#e-E2">E2</a></small></td><td>Retain the choice, show the result and allow a manual retry.<small><a href="#t-T1">T1</a> <a href="#t-T2">T2</a> <a href="#t-T4">T4</a></small></td></tr><tr><td><strong>Design</strong><small><a href="#r-R1">R1</a> <a href="#r-R2">R2</a></small></td><td>No actual project kit is included in this sample.<small><a href="#e-E2">E2</a></small></td><td>Confirm the source and appearance before integration; do not invent styling.<small><a href="#t-T3">T3</a> <a href="#t-T4">T4</a></small></td></tr></tbody></table></div></section><section class="boundaries"><h2>What will not change</h2><ul><li>Do not change either actual application, its styles, dependencies or backend.</li><li>No automatic retry, extra notification features or new component system.</li><li>Report sketches are not shipped UI or captured approved components.</li><li>Preview generation does not approve application implementation, visual design or release.</li></ul></section>
<div class="two-col"><section><h2>Changes and cleanup</h2><ul class="plain"><li><a href="#t-T1">T1</a> Add behaviour tests<small>Depends on: No prior task</small></li><li><a href="#t-T2">T2</a> Implement preference saving<small>Depends on: T1</small></li><li><a href="#t-T3">T3</a> Apply the selected presentation<small>Depends on: T2</small></li><li><a href="#t-T4">T4</a> Verify and record remaining design work<small>Depends on: T3</small></li></ul></section>
<section><h2>How completion will be checked</h2><ul class="plain"><li id="a-A1"><strong>A1: An agreed change is saved once and success is shown only after confirmation.</strong><small>Verify: In a real project, test the existing persistence owner with controlled responses; do not run production requests. · <a href="#r-R1">R1</a></small></li><li id="a-A2"><strong>A2: A pending or failed save retains the choice, prevents duplicate submits, and exposes an accessible manual retry.</strong><small>Verify: Test pending/failure/retry states, keyboard and focus behaviour; inspect the approved source-backed UI and preview isolation. · <a href="#r-R2">R2</a></small></li></ul></section></div>
<section class="approval"><h2>No permission to build</h2><div><p>This page does not allow code changes. Settle the open decisions first, then approve the plan in the chat.</p></div></section><details class="appendix" id="technical-details"><summary>Technical evidence and task details</summary><div class="appendix-body">
<section><h2>Requested outcome</h2><ul class="plain"><li id="r-R1"><strong>R1.</strong> Change and explicitly save the email-notification preference.<small>Source: Synthetic request in UI-PLAN.md.</small></li><li id="r-R2"><strong>R2.</strong> Keep the choice during saving/failure; prevent duplicate saves and support manual retry.<small>Source: Synthetic requirement, not existing app behaviour.</small></li></ul></section>
<section><h2>Changes and mapped tasks</h2><div class="table-scroll"><table><thead><tr><th>Task</th><th>Work and affected areas</th><th>Traceability</th><th>Depends on</th></tr></thead><tbody><tr id="t-T1"><td><strong>T1</strong></td><td><strong>Add behaviour tests</strong><small>prerequisite: Prove the requirement before changing its implementation.<br>Areas: Existing settings owner / approved preview (resolve in actual project)</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E1">E1</a> <a href="#e-E2">E2</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td>—</td></tr><tr id="t-T2"><td><strong>T2</strong></td><td><strong>Implement preference saving</strong><small>requested: Use the inspected settings owner, not a parallel settings system.<br>Areas: Existing settings owner / approved preview (resolve in actual project)</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E1">E1</a> <a href="#e-E2">E2</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td><a href="#t-T1">T1</a></td></tr><tr id="t-T3"><td><strong>T3</strong></td><td><strong>Apply the selected presentation</strong><small>prerequisite: Use only the existing elements chosen to copy; plain HTML only on the owner's explicit instruction.<br>Areas: Existing settings owner / approved preview (resolve in actual project)</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E1">E1</a> <a href="#e-E2">E2</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td><a href="#t-T2">T2</a></td></tr><tr id="t-T4"><td><strong>T4</strong></td><td><strong>Verify and record remaining design work</strong><small>prerequisite: Distinguish tested behaviour, design acceptance and release readiness.<br>Areas: Existing settings owner / approved preview (resolve in actual project)</small></td><td><a href="#r-R1">R1</a> <a href="#r-R2">R2</a><br><a href="#e-E1">E1</a> <a href="#e-E2">E2</a><br><a href="#a-A1">A1</a> <a href="#a-A2">A2</a></td><td><a href="#t-T3">T3</a></td></tr></tbody></table></div></section>
<section><h2>Evidence and limits</h2><article id="e-E1" class="evidence"><strong>E1 · static: The synthetic current view shows a read-only email preference without a Save action.</strong><p>ui-baseline.json: editable=false, save_action=false</p><small>Sources: <a href="#s-S4">S4</a> <a href="#s-S3">S3</a><br>Limits: Fixture evidence only; no real application inspection.</small></article><article id="e-E2" class="evidence"><strong>E2 · design: The intended edit/saving/success/failure states and remaining design questions are recorded for this sample.</strong><p>Requested example behaviour and scope</p><small>Sources: <a href="#s-S1">S1</a> <a href="#s-S3">S3</a><br>Limits: Proposed behaviour and schematic preview, not implementation or design acceptance.</small></article></section><details><summary>Current flow: steps and links</summary><ol><li><strong>N1: Open settings</strong>. (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N2: Read-only preference</strong>. (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N3: View current value</strong>. (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N1 → N2</strong>: continues (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li><li><strong>N2 → N3</strong>: continues (observed, unchanged) Evidence <a href="#e-E1">E1</a>; tasks —.</li></ol></details><details><summary>Feature flow: steps and links</summary><ol><li><strong>N1: Edit preference</strong>. (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N2: Save selected value</strong>. (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N3: Save
succeeded?</strong>. (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N4: Show confirmation</strong>. (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N5: Keep choice; allow retry</strong>. (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N1 → N2</strong>: continues (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N2 → N3</strong>: continues (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N3 → N4</strong>: Yes (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>N3 → N5</strong>: No (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T2">T2</a>.</li></ol></details><details><summary>Build plan: steps and links</summary><ol><li><strong>approval: User approves this plan</strong>. (proposed, unchanged) Evidence <a href="#e-E2">E2</a>; tasks —.</li><li><strong>B1: Add behaviour tests</strong>. (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T1">T1</a>.</li><li><strong>B2: Implement saving</strong>. (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>B3: Use approved presentation</strong>. (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T3">T3</a>.</li><li><strong>B4: Verify and hand off</strong>. (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T4">T4</a>.</li><li><strong>approval → B1</strong>: continues (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T1">T1</a>.</li><li><strong>B1 → B2</strong>: continues (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T2">T2</a>.</li><li><strong>B2 → B3</strong>: continues (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T3">T3</a>.</li><li><strong>B3 → B4</strong>: continues (proposed, modified) Evidence <a href="#e-E2">E2</a>; tasks <a href="#t-T4">T4</a>.</li></ol></details>
<details class="snapshot"><summary>Inspected snapshot and supplied-source fingerprints</summary><p>Illustrative UI workflow · not a real application change · Example only</p>
<p>A local synthetic baseline defines the current state. No actual application, kit, server or device was inspected.</p><p>Generated 2026-11-03T12:20:58.301849+00:00. Hashes cover only these supplied files, including local changes. They do not prove complete code coverage or correctness.</p><div class="table-scroll"><table><thead><tr><th>ID</th><th>Source</th><th>Role</th><th>SHA-256 prefix</th></tr></thead><tbody><tr id="s-S1"><td>S1</td><td><code>skills/codebase-map/examples/ui-review/UI-PLAN.md</code></td><td>plan</td><td><code>f0074a9e847c20a9…</code></td></tr><tr id="s-S2"><td>S2</td><td><code>skills/codebase-map/examples/ui-review/UI-TODO.md</code></td><td>todo</td><td><code>fb6af32ee7e8bb64…</code></td></tr><tr id="s-S3"><td>S3</td><td><code>skills/codebase-map/examples/ui-review/UI-FINDINGS.md</code></td><td>findings</td><td><code>4eff723645d01afb…</code></td></tr><tr id="s-S4"><td>S4</td><td><code>skills/codebase-map/examples/ui-review/ui-baseline.json</code></td><td>fixture</td><td><code>b885d5de4706318b…</code></td></tr><tr id="s-UIREF"><td>UIREF</td><td><code>skills/codebase-map/examples/ui-review/README.md</code></td><td>design-example</td><td><code>863c4b8bd4ebc241…</code></td></tr><tr id="s-report-input"><td>report-input</td><td><code>skills/codebase-map/examples/ui-review/ui-task-review-r2.json</code></td><td>projection</td><td><code>3f292ff23ad9b1e4…</code></td></tr></tbody></table></div></details>
</div></details><p class="muted">System-font mode. No network assets. A saved projection, not a live repository map.</p><footer>Local HTML + SVG · no external assets, uploads or approval actions · saved review snapshot; progress belongs in the existing task records</footer>
</main><script type="application/json" id="report-manifest">{"schema_version": 1, "tool_version": "2.8.0-rc.1", "layout_version": 2, "style": "workflow-editorial-v1", "revision": "R2", "report_kind": "review", "generated_utc": "2026-11-03T12:20:58.301849+00:00", "sources": [{"id": "S1", "role": "plan", "path": "skills/codebase-map/examples/ui-review/UI-PLAN.md", "sha256": "f0074a9e847c20a979cbe330ac970b6117183af04a6ea656277f5e6dc73627d2"}, {"id": "S2", "role": "todo", "path": "skills/codebase-map/examples/ui-review/UI-TODO.md", "sha256": "fb6af32ee7e8bb6411621a2ae48dd6575ba7277fdd26e4be252057accdfa1904"}, {"id": "S3", "role": "findings", "path": "skills/codebase-map/examples/ui-review/UI-FINDINGS.md", "sha256": "4eff723645d01afba5c29fbaf3b90557bf87cf194e819f701a4b0317473a7cfc"}, {"id": "S4", "role": "fixture", "path": "skills/codebase-map/examples/ui-review/ui-baseline.json", "sha256": "b885d5de4706318bbe05865a0259df08a327500b9ebbfaa79063037bf1de8390"}, {"id": "UIREF", "role": "design-example", "path": "skills/codebase-map/examples/ui-review/README.md", "sha256": "863c4b8bd4ebc241167afc8d7feab08cf3a99857e719589c19c28bb1b625737e"}, {"id": "report-input", "role": "projection", "path": "skills/codebase-map/examples/ui-review/ui-task-review-r2.json", "sha256": "3f292ff23ad9b1e4a99429b1025f57533c311b96653d24f8174ddc6dbc772ad2"}]}</script><script>'use strict';
// Native radio inputs and scrolling work without this progressive enhancement.
function revealAnchor() {
const id = location.hash.slice(1);
const target = document.getElementById(id);
if (!target) return;
let parent = target.parentElement;
while (parent) {
if (parent.tagName === 'DETAILS') parent.open = true;
parent = parent.parentElement;
}
target.scrollIntoView({block:target.closest('.canvas') ? 'nearest' : 'start', inline:'center'});
}
for (const anchor of document.querySelectorAll('a[href^="#"]')) {
anchor.addEventListener('click', () => {
const target = document.getElementById(anchor.getAttribute('href').slice(1));
let parent = target;
while (parent) {
if (parent.tagName === 'DETAILS') parent.open = true;
parent = parent.parentElement;
}
});
}
window.addEventListener('hashchange', revealAnchor);
if (location.hash) revealAnchor();
function centerEntry(canvas) {
if (!canvas.clientWidth) return;
const node = document.getElementById(canvas.dataset.startId);
if (!node) return;
const bounds = node.getBoundingClientRect();
const viewport = canvas.getBoundingClientRect();
canvas.scrollLeft += bounds.x + bounds.width/2 - viewport.x - viewport.width/2;
}
function centerVisibleEntries() {
for (const canvas of document.querySelectorAll('.canvas')) centerEntry(canvas);
}
for (const input of document.querySelectorAll('.review input[type="radio"]')) {
input.addEventListener('change', centerVisibleEntries);
}
const resizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(entries => {
for (const entry of entries) centerEntry(entry.target);
}) : null;
for (const canvas of document.querySelectorAll('.canvas')) {
// On narrow screens show the entry node, not the diagram's empty left margin.
resizeObserver?.observe(canvas);
let drag = null;
canvas.addEventListener('keydown', event => {
const choices = ['100', '125', '150'];
const selected = document.querySelector('input[name="diagram-zoom"]:checked');
let index = choices.indexOf(selected.id.slice(5));
let next = null;
if (event.key === '+' || event.key === '=') next = choices[Math.min(2, index+1)];
else if (event.key === '-') next = choices[Math.max(0, index-1)];
else if (event.key.toLowerCase() === 'f') next = 'fit';
else if (event.key === '0') next = '100';
if (next) {
event.preventDefault();
const input = document.getElementById('zoom-'+next);
input.checked = true;
input.dispatchEvent(new Event('change'));
}
});
canvas.addEventListener('pointerdown', event => {
if (event.pointerType !== 'mouse' || event.button !== 0) return;
drag = {x:event.clientX, scroll:canvas.scrollLeft};
canvas.setPointerCapture(event.pointerId);
canvas.classList.add('dragging');
});
canvas.addEventListener('pointermove', event => {
if (drag) canvas.scrollLeft = drag.scroll + drag.x-event.clientX;
});
const stop = () => { drag = null; canvas.classList.remove('dragging'); };
canvas.addEventListener('pointerup', stop);
canvas.addEventListener('pointercancel', stop);
canvas.addEventListener('lostpointercapture', stop);
}
centerVisibleEntries();
// Screen previews are inert; this only scales the report viewer, never an application.
function fitUiFlow() {
const panel=document.getElementById('panel-ui-preview');
if(!panel || !panel.offsetWidth) return;
const canvas=panel.querySelector('.ui-canvas');
const scaled=panel.querySelector('.ui-scaled');
const width=parseFloat(scaled.style.getPropertyValue('--ui-width'));
const selected=document.querySelector('input[name="diagram-zoom"]:checked');
const value=selected?.id==='zoom-fit' ? Math.min(1,canvas.clientWidth/width) : ({'zoom-100':1,'zoom-125':1.25,'zoom-150':1.5}[selected?.id]||1);
scaled.style.setProperty('--scale',value);
}
for(const input of document.querySelectorAll('.review input[type="radio"]')) input.addEventListener('change',fitUiFlow);
if(typeof ResizeObserver==='function') {
const uiCanvas=document.querySelector('.ui-canvas');
if(uiCanvas) new ResizeObserver(fitUiFlow).observe(uiCanvas);
}
fitUiFlow();
// Keep source/decision links clickable; report panning must not capture their pointer.
for(const link of document.querySelectorAll('.ui-canvas a')) {
link.addEventListener('pointerdown',event=>event.stopPropagation());
}
</script></body></html>examples/ui-review/ui-task-review-r2.json{
"schema_version": 2,
"title": "Notification preferences: review the screens before building",
"revision": "R2",
"mode": "compare",
"state": "draft",
"snapshot": {
"repository": "Illustrative UI workflow · not a real application change",
"basis": "A local synthetic baseline defines the current state. No actual application, kit, server or device was inspected.",
"branch": "Example only"
},
"understanding": "In this fictional example, you want people to change their email preference, save it, see whether it worked, and retry without losing their choice. The existing report stays intact; the UI screens view adds the proposed screen states and marked design questions. Nothing here is an approved design for Altinstar or Cheflabels.",
"requirements": [
{
"id": "R1",
"text": "Change and explicitly save the email-notification preference.",
"origin": "Synthetic request in UI-PLAN.md."
},
{
"id": "R2",
"text": "Keep the choice during saving/failure; prevent duplicate saves and support manual retry.",
"origin": "Synthetic requirement, not existing app behaviour."
}
],
"boundaries": [
"Do not change either actual application, its styles, dependencies or backend.",
"No automatic retry, extra notification features or new component system.",
"Report sketches are not shipped UI or captured approved components.",
"Preview generation does not approve application implementation, visual design or release."
],
"open_decisions": [
"Name the existing element each control copies (kit or existing code: same tag, classes and wrapper), or ask which element to mimic; never new CSS.",
"D1: checkbox or switch? D2: feedback inside the form or an approved floating message?",
"Agree the actual preview destination, verification commands and required designer acceptance before implementation."
],
"sources": [
{
"id": "S1",
"role": "plan",
"path": "skills/codebase-map/examples/ui-review/UI-PLAN.md"
},
{
"id": "S2",
"role": "todo",
"path": "skills/codebase-map/examples/ui-review/UI-TODO.md"
},
{
"id": "S3",
"role": "findings",
"path": "skills/codebase-map/examples/ui-review/UI-FINDINGS.md"
},
{
"id": "S4",
"role": "fixture",
"path": "skills/codebase-map/examples/ui-review/ui-baseline.json"
},
{
"id": "UIREF",
"role": "design-example",
"path": "skills/codebase-map/examples/ui-review/README.md"
}
],
"evidence": [
{
"id": "E1",
"claim": "The synthetic current view shows a read-only email preference without a Save action.",
"kind": "static",
"sources": [
"S4",
"S3"
],
"locator": "ui-baseline.json: editable=false, save_action=false",
"limits": "Fixture evidence only; no real application inspection."
},
{
"id": "E2",
"claim": "The intended edit/saving/success/failure states and remaining design questions are recorded for this sample.",
"kind": "design",
"sources": [
"S1",
"S3"
],
"locator": "Requested example behaviour and scope",
"limits": "Proposed behaviour and schematic preview, not implementation or design acceptance."
}
],
"tasks": [
{
"id": "T1",
"title": "Add behaviour tests",
"kind": "prerequisite",
"reason": "Prove the requirement before changing its implementation.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1",
"E2"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"Existing settings owner / approved preview (resolve in actual project)"
],
"depends_on": []
},
{
"id": "T2",
"title": "Implement preference saving",
"kind": "requested",
"reason": "Use the inspected settings owner, not a parallel settings system.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1",
"E2"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"Existing settings owner / approved preview (resolve in actual project)"
],
"depends_on": [
"T1"
]
},
{
"id": "T3",
"title": "Apply the selected presentation",
"kind": "prerequisite",
"reason": "Use only the existing elements chosen to copy; plain HTML only on the owner's explicit instruction.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1",
"E2"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"Existing settings owner / approved preview (resolve in actual project)"
],
"depends_on": [
"T2"
]
},
{
"id": "T4",
"title": "Verify and record remaining design work",
"kind": "prerequisite",
"reason": "Distinguish tested behaviour, design acceptance and release readiness.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E1",
"E2"
],
"acceptance": [
"A1",
"A2"
],
"areas": [
"Existing settings owner / approved preview (resolve in actual project)"
],
"depends_on": [
"T3"
]
}
],
"acceptance": [
{
"id": "A1",
"text": "An agreed change is saved once and success is shown only after confirmation.",
"requirements": [
"R1"
],
"verification": "In a real project, test the existing persistence owner with controlled responses; do not run production requests."
},
{
"id": "A2",
"text": "A pending or failed save retains the choice, prevents duplicate submits, and exposes an accessible manual retry.",
"requirements": [
"R2"
],
"verification": "Test pending/failure/retry states, keyboard and focus behaviour; inspect the approved source-backed UI and preview isolation."
}
],
"graphs": [
{
"kind": "current",
"title": "Current fixture: the preference is read-only",
"nodes": [
{
"id": "N1",
"label": "Open settings",
"note": "",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"x": 48,
"y": 152,
"width": 288,
"height": 96,
"shape": "start"
},
{
"id": "N2",
"label": "Read-only preference",
"note": "",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"x": 456,
"y": 152,
"width": 344,
"height": 96,
"shape": "action"
},
{
"id": "N3",
"label": "View current value",
"note": "",
"basis": "observed",
"change": "unchanged",
"evidence": [
"E1"
],
"tasks": [],
"x": 920,
"y": 152,
"width": 304,
"height": 96,
"shape": "result"
}
],
"edges": [
{
"from": "N1",
"to": "N2",
"label": "",
"evidence": [
"E1"
],
"change": "unchanged",
"basis": "observed",
"tasks": [],
"points": [
[
336,
200
],
[
456,
200
]
],
"label_position": [
396,
172
]
},
{
"from": "N2",
"to": "N3",
"label": "",
"evidence": [
"E1"
],
"change": "unchanged",
"basis": "observed",
"tasks": [],
"points": [
[
800,
200
],
[
920,
200
]
],
"label_position": [
860,
172
]
}
],
"id": "current",
"purpose": "flow",
"tab": "Current flow",
"description": "The current state belongs only to ui-baseline.json. Arrows show the simple opening/display path, not a real app audit.",
"width": 1280,
"height": 640
},
{
"id": "proposed",
"kind": "proposed",
"purpose": "flow",
"tab": "Feature flow",
"title": "Proposed: save and show the result",
"description": "Proposed example behaviour. Failure retains the choice; retry is user-initiated. The UI screens tab shows the visible states and design questions.",
"width": 1280,
"height": 736,
"nodes": [
{
"id": "N1",
"label": "Edit preference",
"note": "",
"x": 464,
"y": 40,
"width": 352,
"height": 72,
"shape": "start",
"tasks": [
"T2"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified",
"highlight": false
},
{
"id": "N2",
"label": "Save selected value",
"note": "",
"x": 464,
"y": 160,
"width": 352,
"height": 88,
"shape": "action",
"tasks": [
"T2"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified",
"highlight": false
},
{
"id": "N3",
"label": "Save\nsucceeded?",
"note": "",
"x": 512,
"y": 320,
"width": 256,
"height": 160,
"shape": "decision",
"tasks": [
"T2"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified",
"highlight": true
},
{
"id": "N4",
"label": "Show confirmation",
"note": "",
"x": 920,
"y": 352,
"width": 304,
"height": 96,
"shape": "result",
"tasks": [
"T2"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified",
"highlight": true
},
{
"id": "N5",
"label": "Keep choice; allow retry",
"note": "",
"x": 460,
"y": 576,
"width": 360,
"height": 88,
"shape": "result",
"tasks": [
"T2"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified",
"highlight": false
}
],
"edges": [
{
"from": "N1",
"to": "N2",
"points": [
[
640,
112
],
[
640,
160
]
],
"label": "",
"tasks": [
"T2"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "N2",
"to": "N3",
"points": [
[
640,
248
],
[
640,
320
]
],
"label": "",
"tasks": [
"T2"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "N3",
"to": "N4",
"points": [
[
768,
400
],
[
920,
400
]
],
"label": "Yes",
"tasks": [
"T2"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified",
"label_position": [
844,
372
]
},
{
"from": "N3",
"to": "N5",
"points": [
[
640,
480
],
[
640,
576
]
],
"label": "No",
"tasks": [
"T2"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified",
"label_position": [
688,
524
]
}
]
},
{
"id": "build",
"kind": "proposed",
"purpose": "delivery",
"tab": "Build plan",
"title": "Build plan — after approval only",
"description": "These are delivery dependencies, not the feature runtime. Approval must be recorded before T1 begins; no diagram action grants it.",
"width": 1280,
"height": 848,
"nodes": [
{
"id": "approval",
"label": "User approves this plan",
"note": "",
"x": 472,
"y": 40,
"width": 336,
"height": 64,
"shape": "start",
"tasks": [],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "unchanged",
"highlight": true,
"precondition": "approval"
},
{
"id": "B1",
"label": "Add behaviour tests",
"note": "",
"x": 432,
"y": 176,
"width": 416,
"height": 88,
"shape": "action",
"tasks": [
"T1"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T1"
},
{
"id": "B2",
"label": "Implement saving",
"note": "",
"x": 432,
"y": 328,
"width": 416,
"height": 88,
"shape": "action",
"tasks": [
"T2"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T2"
},
{
"id": "B3",
"label": "Use approved presentation",
"note": "",
"x": 432,
"y": 480,
"width": 416,
"height": 96,
"shape": "action",
"tasks": [
"T3"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T3"
},
{
"id": "B4",
"label": "Verify and hand off",
"note": "",
"x": 432,
"y": 640,
"width": 416,
"height": 96,
"shape": "result",
"tasks": [
"T4"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified",
"highlight": false,
"task_id": "T4"
}
],
"edges": [
{
"from": "approval",
"to": "B1",
"points": [
[
640,
104
],
[
640,
176
]
],
"label": "",
"tasks": [
"T1"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "B1",
"to": "B2",
"points": [
[
640,
264
],
[
640,
328
]
],
"label": "",
"tasks": [
"T2"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "B2",
"to": "B3",
"points": [
[
640,
416
],
[
640,
480
]
],
"label": "",
"tasks": [
"T3"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified"
},
{
"from": "B3",
"to": "B4",
"points": [
[
640,
576
],
[
640,
640
]
],
"label": "",
"tasks": [
"T4"
],
"evidence": [
"E2"
],
"basis": "proposed",
"change": "modified"
}
]
}
],
"default_view": "proposed",
"comparison": [
{
"label": "Changing a preference",
"current": "Synthetic baseline is read-only.",
"proposed": "An agreed control allows editing and explicit save.",
"requirements": [
"R1"
],
"evidence": [
"E1"
],
"tasks": [
"T1",
"T2",
"T3"
]
},
{
"label": "Saving and failures",
"current": "No save action exists in the fixture.",
"proposed": "Retain the choice, show the result and allow a manual retry.",
"requirements": [
"R2"
],
"evidence": [
"E1",
"E2"
],
"tasks": [
"T1",
"T2",
"T4"
]
},
{
"label": "Design",
"current": "No actual project kit is included in this sample.",
"proposed": "Confirm the source and appearance before integration; do not invent styling.",
"requirements": [
"R1",
"R2"
],
"evidence": [
"E2"
],
"tasks": [
"T3",
"T4"
]
}
],
"ui_review": {
"kind": "application",
"mode": "unselected",
"title": "The proposed interface, state by state",
"summary": "See the screens and feedback states, not just boxes naming them. Arrows show actions or outcomes; D1 and D2 point to design choices on the controls.",
"limit": "A synthetic UI task. These are authorised report schematics, not a real project kit, working application or approved design.",
"authority": {
"status": "unconfirmed",
"source": "UIREF",
"locator": "Preview authority and limits",
"summary": "Fictional illustration only. No real project kit or styling decision has been selected."
},
"preview": {
"status": "not-needed",
"source": "UIREF",
"locator": "Preview authority",
"summary": "This isolated local report example only; no new application route or production UI.",
"destination": "This offline report, not either application"
},
"acceptance": {
"status": "pending",
"summary": "No design alternative has been accepted for either project."
},
"width": 1152,
"height": 1024,
"open_first": true,
"screens": [
{
"id": "UI1",
"title": "01 · Edit preference",
"description": "Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.",
"state": "proposed",
"x": 16,
"y": 48,
"width": 320,
"height": 384,
"representation": "schematic",
"content": [
{
"kind": "heading",
"label": "Notifications"
},
{
"kind": "text",
"label": "Choose what you receive."
},
{
"kind": "checkbox",
"label": "Email notifications",
"checked": true,
"decision": "D1"
},
{
"kind": "button",
"label": "Save"
},
{
"kind": "text",
"label": "Changes are not yet saved."
}
],
"sources": [
"UIREF"
],
"requirements": [
"R1",
"R2"
],
"evidence": [
"E2"
],
"tasks": [
"T2",
"T3"
],
"decisions": [
"D1"
]
},
{
"id": "UI2",
"title": "02 · Saving",
"description": "Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.",
"state": "proposed",
"x": 416,
"y": 48,
"width": 320,
"height": 384,
"representation": "schematic",
"content": [
{
"kind": "heading",
"label": "Notifications"
},
{
"kind": "text",
"label": "Your choice stays visible."
},
{
"kind": "checkbox",
"label": "Email notifications",
"checked": true,
"decision": "D1"
},
{
"kind": "button",
"label": "Saving…",
"disabled": true
},
{
"kind": "text",
"label": "Wait for the save result."
}
],
"sources": [
"UIREF"
],
"requirements": [
"R1",
"R2"
],
"evidence": [
"E2"
],
"tasks": [
"T2",
"T3"
],
"decisions": [
"D1"
]
},
{
"id": "UI3",
"title": "03 · Saved",
"description": "Illustrative notification setting; neither current application was inspected or styled for this preview. Screen state is proposed for the example only.",
"state": "proposed",
"x": 816,
"y": 48,
"width": 320,
"height": 384,
"representation": "schematic",
"content": [
{
"kind": "heading",
"label": "Notifications"
},
{
"kind": "text",
"label": "Your choice stays visible."
},
{
"kind": "checkbox",
"label": "Email notifications",
"checked": true,
"decision": "D1"
},
{
"kind": "message",
"label": "Preference saved.",
"decision": "D2"
},
{
"kind": "button",
"label": "Save"
}
],
"sources": [
"UIREF"
],
"requirements": [
"R1",
"R2"
],
"evidence": [
"E2"
],
"tasks": [
"T2",
"T3"
],
"decisions": [
"D1",
"D2"
]
},
{
"id": "UI4",
"title": "04 · Save failed",
"description": "Illustrative notification setting; neither current application was inspected or styled for this preview. A failed save keeps the selection so the user can retry.",
"state": "proposed",
"x": 416,
"y": 592,
"width": 320,
"height": 384,
"representation": "schematic",
"content": [
{
"kind": "heading",
"label": "Notifications"
},
{
"kind": "text",
"label": "Your choice stays visible."
},
{
"kind": "checkbox",
"label": "Email notifications",
"checked": true,
"decision": "D1"
},
{
"kind": "message",
"label": "Could not save. Try again.",
"decision": "D2"
},
{
"kind": "button",
"label": "Try again"
}
],
"sources": [
"UIREF"
],
"requirements": [
"R1",
"R2"
],
"evidence": [
"E2"
],
"tasks": [
"T2",
"T3"
],
"decisions": [
"D1",
"D2"
]
}
],
"transitions": [
{
"from": "UI1",
"to": "UI2",
"label": "Save",
"points": [
[
336,
240
],
[
416,
240
]
],
"label_position": [
376,
216
]
},
{
"from": "UI2",
"to": "UI3",
"label": "Saved",
"points": [
[
736,
240
],
[
816,
240
]
],
"label_position": [
776,
216
]
},
{
"from": "UI2",
"to": "UI4",
"label": "Failed",
"points": [
[
576,
432
],
[
576,
592
]
],
"label_position": [
624,
512
]
},
{
"from": "UI4",
"to": "UI2",
"label": "Retry",
"points": [
[
736,
784
],
[
784,
784
],
[
784,
496
],
[
672,
496
],
[
672,
432
]
],
"label_position": [
728,
476
]
}
],
"decisions": [
{
"id": "D1",
"title": "Preference control",
"question": "Should this be the approved checkbox or the approved switch? The drawing shows a checkbox as an example, not a selection.",
"options": [
"Approved checkbox",
"Approved switch"
],
"status": "open",
"screens": [
"UI1",
"UI2",
"UI3",
"UI4"
]
},
{
"id": "D2",
"title": "Feedback position",
"question": "Where should the approved success and error feedback appear? The sample shows inline text only.",
"options": [
"Inside the form",
"Approved floating message"
],
"status": "open",
"screens": [
"UI3",
"UI4"
]
}
]
}
}
examples/ui-review/UI-TODO.md# Synthetic work items — none executed - T1 Add relevant preference-save, duplicate-submit and failure tests. - T2 Implement the agreed behaviour using the inspected owner. - T3 Apply only the selected approved kit or authorised functional preview; design gaps remain pending. - T4 Verify required states, accessibility, preview isolation and the final agreed appearance. Every item depends on approval of its actual scope. T2 follows T1; T3 follows T2; T4 follows T3. These are demonstration tasks, not work performed in either application.
FLOWCHART-DESIGN.md# Local flowchart design ## Scope and local files This is codebase-map's own design procedure. Read it directly; do not resolve, invoke, install, fetch or require an external diagram-design skill. Its absence is expected, not a blocker and not a fallback exception. Matt's research, debugging, TDD, review, grilling and other workflow routes remain unchanged. The complete codebase-map folder contains this guide, [REPORT.md](REPORT.md), [scripts/render_report.py](scripts/render_report.py), [assets/report.css](assets/report.css), [assets/report.js](assets/report.js), and [the synthetic example](examples/review.json). No upstream references, scripts, font files or network calls are needed to design or render a report. Preserve the full folder when installing it. Python 3.10+ is the renderer runtime; browser QA uses existing local tools when available, never an automatic dependency install. Missing local files or an unavailable renderer: report the incomplete installation or runtime, preserve task evidence and pause only the dependent visual phase. Repair needs setup authority. Do not install another person's skill, silently replace the design, or waive the implementation-approval gate. A missing optional browser check is reported as untested; it is not a reason to invent a third-party dependency. ## Approved design brief Establish once per visualization phase: - The requested outcome, audience, approved answers, current code evidence, task/acceptance IDs and unresolved limits. - Primary actual feature/process flow. Additional build-dependency and structural views only when useful. A diagram of the agent’s workflow does not substitute for the feature. - **Approved style: `workflow-editorial-v1`.** Minimal light, paper `#f5f5f5`, ink `#2d3142`, muted `#4f5d75`, accent `#bf4520`, accent tint `#f5e9e4`, rule `#cdd0d5`. Serif report heading; sans node names; mono for small technical labels. System-font mode; no font files or external requests. This is the owner-approved second sample, not an unapproved branded default. - Default 1280px-wide drawing, natural height, 18px node names and 13px notes at 100%. Reuse this choice; adjust a bounded view to keep it readable rather than reopening a style interview. No animations, glow, shadows, oversized rounded cards or decorative grid. - Allowed output: explicit schema-2 graph geometry and a small design rationale in the existing FINDINGS. The bundled renderer creates the final report. No application edits, global profile/style-guide mutations, installation, uploads, approval actions or new requirements. The style is already approved; no automatic brand discovery, website fetching or repeated style interview. Reuse existing answers and the selected content. A truly new user-owned design/scope decision still goes to the owner. Alternative formats must not erase the requested flowchart. An attribute comparison can accompany the flow as a table, not replace it. ## Visual contract Shapes carry meaning: oval/rounded terminator for start/result, modestly rounded rectangle for action, diamond for a genuine decision with 2–3 labelled exits. Groups collect members rather than implying runtime order. Keep one arrow meaning per view; the caption explains it. Independent controls are neither exclusive nor necessarily sequential. Do not add error/default branches without source support. Use at most 9 nodes, 12 connectors and 2 focal nodes per view (the selected style’s budget, not a universal scientific limit). Split larger maps by the question being answered. No repeated file paths, status badges or evidence IDs inside every node. Keep full traceability below and link it. Place nodes on a 4px structural grid. Route through explicit orthogonal corridors, rounded elbows, separate attachment ports and off-stroke label masks. The local renderer rejects crossing/shared routes rather than supporting automatic bridges; reroute or split instead. Do not shrink labels to force a fit. Compare current/proposed meaning in aligned rows, and use stable positions when a direct topology comparison benefits from them. The approved sample’s three views are an example, not mandatory tabs for every task. Real bug logic may need a flow and a build plan; settings work may also need containment. The default report must explain the substantive behavior, not just the implementation file graph. ## Evidence and approval boundaries Necessary questions still use `grilling`; research and plan/task preparation precede a ready report. Evidence, exclusions, important unknowns and acceptance stay visible. The HTML is a projection, not authority. The workflow stops for real user approval of the identified revision before code. Approving the report style does not approve a represented application change. ## Quality checks Validate schema, task/evidence references, current/proposed basis, explicit geometry, decision labels and source fingerprints. In a browser inspect actual font fit (including diamond interiors), branch fidelity, native size/Fit, alternate views, keyboard controls, evidence links, offline/no-script display and narrow-window scrolling. Technical geometry checks do not prove user understanding or semantic completeness. List unavailable checks in the task record. ## Provenance, not a dependency The owner approved the flowchart sample inspired by Cathryn Lavery's diagram-design repository. This guide retains and localizes the workflow's own 2.2.0 design contract; it is not a vendored copy or complete reimplementation of that upstream skill. The approved renderer/style remain local and versioned with workflow-axa. No upstream scripts, font files or complete skill tree are redistributed, and no installed upstream invocation is claimed. Historical design references, reviewed in the 2.2.0 work on 2026-10-14: [skill](https://github.com/cathrynlavery/diagram-design/blob/main/skills/diagram-design/SKILL.md), [flowchart grammar](https://github.com/cathrynlavery/diagram-design/blob/main/skills/diagram-design/references/type-flowchart.md), [style](https://github.com/cathrynlavery/diagram-design/blob/main/skills/diagram-design/references/style-guide.md), [connector primitives](https://github.com/cathrynlavery/diagram-design/blob/main/skills/diagram-design/references/primitives-core.md), [output sizing](https://github.com/cathrynlavery/diagram-design/blob/main/skills/diagram-design/references/output-spec.md). These are attribution only, not required reading during a task or an instruction to follow upstream changes. Review changes to this local contract through the team's normal versioned workflow. ## Explain and preserve the approved view Use everyday action/result labels and explain necessary technical terms in visible context. Do not assume the reader knows the project, but retain precise evidence and original requirement terms in the technical section. A reader should be able to describe the proposed behavior and approval decision without the prior conversation. Report an unresolved meaning rather than guessing it to simplify the diagram. For renderer/style maintenance, compare deterministic screenshots against a reviewed prior example with the same browser, fonts, viewport and data. Missing or mismatched references are not verification. Reference replacement requires deliberate review; do not make a test pass by automatically accepting its output. Pixel equality only addresses that fixture's appearance, not correctness, accessibility or human comprehension. Keep semantic checks and the real user approval boundary. ## Optional UI screens [UI-FLOW.md](UI-FLOW.md) extends the same report with source-backed screen previews joined by actions and control-linked decisions. This does not change the editorial shell or permit invented application styling. Non-UI flows keep this exact visual grammar. Use kit captures for actual appearance and label authorised schematics as approximate.
REPORT.md# Offline flowchart report contract
For a complete engineering task, use the optional sourced task content described in [TASK-REPORT.md](TASK-REPORT.md). It adds agreement, specifications, checks and delivery to this same renderer. Ordinary diagrams and historical inputs retain their existing contract.
Design the flow locally using [FLOWCHART-DESIGN.md](FLOWCHART-DESIGN.md); no separately installed diagram skill is required. Python 3.10+ standard library packages that explicit layout into local HTML/SVG; it does not install or invoke skills. Copy this complete skill folder for a standalone installation. The output has no CDN, fonts, telemetry, uploads or approval action. Edit the JSON and render a new revision; this is not a collaborative whiteboard.
## Run
From the plugin root, validate input before recording the final report filename. Fix schema or wording errors in the same JSON until validation passes. Then record the destination, validate again against the final source records, and render a **new** output filename:
```powershell
python -X utf8 skills/codebase-map/scripts/render_report.py --root skills/codebase-map/examples --input skills/codebase-map/examples/review.json --validate-input
python -X utf8 skills/codebase-map/scripts/render_report.py --root skills/codebase-map/examples --input skills/codebase-map/examples/review.json --output skills/codebase-map/examples/local-preview.html --open
python -X utf8 skills/codebase-map/scripts/render_report.py --root skills/codebase-map/examples --check skills/codebase-map/examples/local-preview.html
```
For real work substitute the actual source root, task JSON and authorized artifact path. Quote paths with spaces. CLI paths resolve from the current directory; source paths inside JSON are root-relative POSIX paths. Input must be inside root; output parents must already exist, output must be .html/.htm, and existing files are never overwritten. Source references outside root, symlink escapes and oversized inputs are rejected. Do not claim a listed source path was inspected unless it actually was.
`--validate-input` needs `--root` and `--input`. It uses existing schema, source, geometry, size and wording checks without creating files, directories or Python helper caches, and without opening a browser. It cannot combine with `--output`, `--open`, `--check` or `--replace-unpresented`. Exit 0 means valid, 1 means blocking wording, and 2 means invalid input or arguments. Advisory notes remain successful. This covers renderer-controlled writes, not unrelated host instrumentation, and proves neither meaning nor approval.
Ordinary render/check behavior is unchanged. Exit 0: rendered/check matched; 1: freshness mismatch/missing sources; 2: invalid input or filesystem error. `--open` requests browser launch, not proof of display. An opener failure leaves the artifact intact and grants no implementation permission.
Default zoom is 100% native size inside a locally scrollable panel. Native radio inputs select view/100/125/150/Fit, including without JavaScript. Fit may reduce text and is optional, never the mandatory reading size. Progressive script adds mouse drag, centered entry on narrow screens, focused-canvas +/−/F/0 and evidence-detail opening. A native “Go to start” link works without JavaScript. Keyboard arrows and touch use native scrolling. No executable application controls.
## Schema 2
[examples/review.json](examples/review.json) is a complete synthetic example with current, proposed and build-dependency views. The example is not an actual feature authorization.
| Field | Contract |
|---|---|
| `schema_version` | `2`. New renders deliberately reject schema 1 with migration instructions. |
| `title`, `revision`, `understanding` | Plain text identifying the task, exact plan revision and understood outcome. |
| `mode` / `state` | current/proposed/compare; inspection/draft/awaiting-approval/approved/superseded. Current-only uses inspection. |
| `snapshot` | Repository label, optional branch/worktree, inspection basis and limits. Do not call uncommitted work a clean HEAD snapshot. |
| `boundaries`, `open_decisions` | Text arrays for exclusions and unresolved material decisions. Open decisions prevent approval-ready state. |
| `write_paths` | Optional array of repository-relative files, or folders ending in `/`, that the plan may change. Shown as the first "What will not change" item and stored in the report manifest; after approval the phase gate blocks other file writes (task records stay writable). No wildcards, `..` or absolute paths. |
| `sources` | `id`, existing root-relative `path`, `role`. Approval-ready needs plan/todo/findings roles. Only hashes/paths, not source contents, are copied automatically. |
| `requirements` | `id`, `text`, `origin` identifying an actual request/answer/policy. |
| `evidence` | `id`, `claim`, `kind` static/runtime/design, source IDs, `locator`, `limits`. Design intent is not observed runtime/code. |
| `acceptance` | `id`, observable `text`, requirement IDs and concrete `verification`. |
| `tasks` | `id`, `title`, requested/prerequisite `kind`, justified `reason`, affected `areas`, requirement/evidence/acceptance IDs, `depends_on` task IDs. Cycles and unmapped work fail validation. |
| `comparison` | Array of `label`, `current`, `proposed`, requirement/evidence/task ID arrays. Required for compare. This aligned summary supports, never replaces, the flowchart. |
| `default_view` | Graph ID. Compare opens a **proposed flow**, not a build checklist, table or settings hierarchy. |
| `graphs` | Up to six individually scoped views. Compare includes current and proposed kinds; multiple graphs of a kind are allowed. |
| `approval` | Approved state only: matching revision, real record reference and response. Structure validation is not authentication. |
### Qualified source tickets
Keep source identities unchanged. JSON IDs use `[A-Za-z][A-Za-z0-9_-]{0,47}`; a source ticket `D4/T8` therefore uses a **report-only** alias such as `D4-T8`, and a title beginning `D4/T8 — <action>`. Use that alias consistently in graph, dependency, comparison and task references. Retain the original ID in source locators. Check uniqueness; do not blindly replace characters when two IDs could collapse to the same alias. This maps a projection, not a new task, revision or source-file migration.
### Graph and geometry
Each graph has `id`, `kind` current/proposed, `purpose` flow/delivery/structure/technical, short `tab`, `title`, `description`, integer `width`/`height`, `nodes`, `edges`. State the arrow meaning and any inference/proposal limits in the description. Default width 1280, appropriate natural height; allowed board bounds are 400–1600 wide, 240–1600 high. These bounds keep one view readable, not an invitation to fill it.
Each node has `id`, short `label`, `note` (may be empty), `shape` start/action/decision/result/group, `x`, `y`, `width`, `height`, optional boolean `highlight`, `basis`, `change`, `evidence`, `tasks`. Group nodes may have an `items` array. Start/result are terminators, actions modest rectangles, decisions diamonds. Use explicit `\n` for at most two short label/note lines. The renderer does not auto-shrink text. Diamond labels must fit their interior, not merely their bounding rectangle. Details belong in evidence records.
Each edge has `from`, `to`, `label` (empty only for an unambiguous continuation), explicit `points: [[x,y], ...]`, basis/change/evidence/tasks. Nonempty labels require `label_position: [center_x, baseline_y]`. A decision has 2–3 distinct labelled exits. Each path segment is horizontal or vertical; rounded elbows are generated from the points. Do not substitute two diagonal endpoints and expect automatic layout.
Structural coordinates are integers divisible by four. Nodes stay within the board with padding and do not overlap. Each connector attaches to a distinct permitted boundary port, at least 12px from another port. Diamond ports are its cardinal vertices. Paths leave outward, do not pass through any node or cross/share another route, and labels sit on opaque masks clear of nodes and routes. Crossings/automatic bridge glyphs are intentionally unsupported: reroute or split a view. At most 9 nodes, 12 edges and 2 highlights per view. These are the adopted design budget, not scientific universals.
Example decision fragment (all referenced IDs must exist in the full report):
```json
{
"id": "check", "label": "Either score\nnegative?", "note": "",
"shape": "decision", "x": 512, "y": 320, "width": 256, "height": 160,
"highlight": true, "basis": "proposed", "change": "modified",
"evidence": ["E1"], "tasks": ["T2"]
}
```
Basis: observed/inferred/unknown/proposed. Change: unchanged/added/modified/removed. Existing relationships require static/runtime evidence, unknowns require `uncertainty`; that text is surfaced outside collapsed details. Current graphs cannot contain future modifications. Changed future behavior is proposed and task-mapped. Edges need evidence too. A quoted intended behavior does not prove runtime correctness.
Delivery views use `task_id` on task nodes, matching the single task reference, and `precondition: "approval"` on a prerequisite start node. Delivery edges are checked against dependencies among the shown tasks. For a focused subset, declare omitted prerequisite IDs in graph `external_dependencies`; these are displayed below the diagram. Preserve the task dependency set; an approval node describes a required gate, not a completed approval or additional TODO. Do not infer concurrency from two tasks without a dependency. Shared-file ownership remains governed by the workflow.
## Migration from 2.1 / schema 1
Existing HTML remains unchanged and its schema-1 source manifest is still accepted by `--check`. New rendering uses schema 2 only; it does not auto-convert the old component grid into assumed user behavior.
1. Preserve the old reviewed HTML/JSON and authoritative plan revision. Do not overwrite a reviewed artifact.
2. Reuse requirements, evidence, tasks, acceptance, limits and source paths after reconciling freshness. Correct material source ambiguities only through the owner’s decision process.
3. Use the local FLOWCHART-DESIGN.md guide to produce focused views with ids, purpose, explicit positions/shapes/routes, `default_view`, and aligned `comparison`. Remove old `row`/`col`/`group` layout fields; no stale renderer fallback.
4. Save a new presentation revision/path, render and inspect. A presentation-only restyle does not create application approval or require reapproving unchanged application scope already approved. A semantic change does.
## Source of truth, safety and limits
PLAN/TODO/FINDINGS/DECISIONS remain authoritative. Hashes include saved input JSON and all supplied sources, including uncommitted bytes. `--check` detects changes/missing supplied files, not omitted/new files, fabricated evidence, semantic correctness, trustworthy approval or malicious manifest modification. Source paths and report text may be sensitive; sanitize evidence before sharing.
The input is escaped text, not raw HTML/SVG/CSS. No input script/URL is executed. Geometry checks catch common defects; actual browser label measurements and review remain required. For that review, `scripts/screenshot_report.py <report.html> <output-folder>` takes desktop and narrow screenshots with the installed Edge or Chrome (nothing is installed; files are never overwritten); look at the images. Print displays all diagrams; expand required technical details before printing. No PDF export or browser installation is implied. A successful structural check is not a complete accessibility/usability audit or proof of agent obedience.
## Maintaining the approved presentation
The package's tools/visual_regression.py captures the stable bundled fixture and compares screenshots read-only. See package tests/VISUAL-REGRESSION.md for the controlled-environment protocol. These maintenance tools are not required by a standalone map installation; retain existing browser/manual checks when unavailable. Never update an approved screenshot automatically, substitute image equality for semantic review, or claim screenshots establish user comprehension. Keep generated timestamps outside the compared view.
## Durable reviews versus disposable checks
Review HTML/JSON are requested deliverables, not generic test output. Use the project's review-page location or the existing task folder. Preserve every presented/approved revision according to project rules. The renderer refuses existing outputs intentionally: do not bypass that protection or invent another task root. Revise an unpresented draft only when its ownership and the writer's rules allow it.
For optional browser/visual verification, apply the active workflow ARTIFACTS.md/global policy. These tools may require a fresh child directory to avoid overwriting evidence; place it under the stable task output parent. After comparing, preserve approved references and necessary failure/proof images, then remove only the current operation's eligible temporary captures. Do not retain every identical successful comparison or create an extra manifest outside what the test tool actually needs.
## Additive record context (2.6; schema 2 stays valid)
Existing schema-2 input requires no migration or new fields. Existing HTML is never rewritten; schema-1 HTML manifests remain checkable. A new render of schema-1 layout still requires the deliberate layout migration documented above. None of these checks interprets arbitrary Markdown, authenticates decisions or proves agent compliance.
Ordinary approval reviews remain the default. For a specifically requested task-status illustration, set `report_kind: "task-overview"` and supply `checkpoint`. This is a saved projection of the existing task record, not a dashboard to regenerate per checkpoint. It does not ask for new approval or grant implementation authority. Preserve the selected `revision` exactly, even when a legacy review uses another filename/phase alias. The task overview labels the comparison as a historical baseline versus reviewed scope; a flow is not current-implementation evidence just because it is approved. In current-only mode, the inspected flow still needs fresh code evidence.
| Optional field | Meaning and requirements |
|---|---|
| `report_kind` | `review` (default) or `task-overview`. The latter requires a sourced checkpoint; it is not an approval-mode bypass. |
| `checkpoint` | `as_of` (actual known time or explicit unknown), `source` (supplied source ID), `locator` (current record section), `next_action`, and the three state objects below. |
| `checkpoint.approval` | `status`: approved/waived/not-approved/unknown; `revision`: exact covered revision for approved/waived, otherwise null/omitted; `summary`: authority and limits in plain language. An approved report must match this revision and still needs its existing `approval` record/response. |
| `checkpoint.implementation` | `status`: not-started/in-progress/implemented/partial/unknown; `summary`: supported work state and evidence/limitations. |
| `checkpoint.verification` | `status`: not-run/in-progress/blocked/passed/failed/limited/unknown; `summary`: checks and any owner acceptance still outstanding. Passed automated checks alone must not conceal pending owner acceptance. |
| `revision_log` | Ordered array of `revision`, optional `review` alias, `change`, `reason`, `status` (proposed/presented/approved/superseded/rejected/withdrawn/unknown), `source` and `locator`. One row per agreement, presentation aliases in that row. Keep exact identifiers; no automatic sorting by filename or invented missing history. Review aliases are escaped text, not executable URLs. |
| Task `outcome` / `outcome_note` | Optional sourced status: planned/in-progress/implemented/verified/blocked/removed/superseded/not-applicable/unknown. A note explaining the evidence or owner decision is required. Removed/superseded work is labelled as not a passed test. Without a status, the task is only a scope description, not a completion claim. |
Use the existing PLAN checkpoint, or established STATUS.md in a legacy task. Keep the revision log in existing PLAN/DECISIONS (or existing legacy equivalent). Do not create a JSON state file or new Markdown file to feed this feature. Generate the requested report input from those records; no competing task database or duplicate handwritten history.
[The task-overview example](examples/task-overview.json) adds context to the unchanged synthetic flowchart. Its legacy [STATUS.md](examples/STATUS.md) is deliberately separate from the older pending review. It demonstrates a later recorded fixture approval without altering the old HTML. It is not real user approval or runtime evidence.
The renderer continues exclusive creation. Do not bypass an existing-output refusal to edit a presented/approved snapshot, or create retry-named reports automatically. An unpresented draft may be replaced only through explicitly authorised ownership-safe operations; a saved overview remains an as-of snapshot. Normal progress belongs in the task record/chat, not more HTML files.
A source hash change after a checkpoint update does not by itself invalidate unchanged approved scope. `--check` only reports byte differences; the agent must distinguish expected progress, material changes and missing authority. Keep presented history intact and ask only about genuinely unresolved authority.
## Optional UI screen flow (rc.6)
[UI-FLOW.md](UI-FLOW.md) adds `ui_review` to existing schema 2. It renders connected screen/state previews as another tab inside this full report. Existing JSON, style and sections do not need rewriting. No raw app markup or code is executed. Local PNG captures must be listed as sources and are included in freshness checks. Explicit schematics remain visibly approximate. Every rendering uses the same bundled renderer; do not hand-replace the HTML shell with an independent design page.
## Optional fast UI style references
The `ui_review` screen flow may include `quick_previews` selecting **existing HTML elements** and **existing compiled CSS** by supplied source IDs. These render in sandboxed, expandable frames inside the existing HTML report. This is a fast, static style reference—not a replacement for Angular/React runtime, not a styled mockup, and not a way to bypass the implementation gate. See [UI-FLOW.md](UI-FLOW.md). No quick preview is required when code-backed sources are unavailable.
scripts/kit_bindings.py"""Resolve UI selections against a local catalogue; no app execution or approval inference."""
from __future__ import annotations
import hashlib
import html
import importlib.util
import json
import re
from pathlib import Path
def kit_api():
path = Path(__file__).resolve().parents[2] / 'ui-design/scripts/check_design_kit.py'
if not path.is_file():
raise ValueError('Install the complete ui-design sibling skill to resolve kit references.')
spec = importlib.util.spec_from_file_location('workflow_kit_references', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def resolve(ui: dict, root: Path, sources: dict) -> dict | None:
binding = ui.get('kit_binding')
if binding is None:
if any('capture_context' in s for s in ui['screens']):
raise ValueError('Component selections need an explicit kit_binding; no guessed catalogue.')
for screen in ui['screens']:
if 'elements' in screen:
check_reused(screen['elements'], sources)
return None
k = kit_api()
if not isinstance(binding, dict) or set(binding) not in ({'source', 'scope'}, {'source', 'scope', 'projection'}):
raise ValueError('kit_binding requires one supplied catalogue source and a repository-relative scope.')
k.text(binding['source'], 'kit_binding.source')
if binding['source'] not in sources:
raise ValueError('kit_binding requires an actual supplied source.')
scope = k.path_value(binding['scope']).as_posix()
reader = k.Reader(root)
catalogue_path = sources[binding['source']]['path']
raw = reader.read(catalogue_path)
data = binding['projection'] if 'projection' in binding else json.loads(raw.decode('utf-8-sig'), object_pairs_hook=k.unique_object)
entries = k.validate(data)
requested = []
screen_rows = {}
for screen in ui['screens']:
elements = screen.get('elements')
if not isinstance(elements, list) or not 1 <= len(elements) <= 20:
raise ValueError('Each kit-backed screen needs one to twenty exact element/pattern selections.')
seen = set()
for element in elements:
fields = {'id', 'item', 'role', 'variant', 'state', 'example_source', 'example_locator', 'changes'}
if not isinstance(element, dict) or set(element) != fields:
raise ValueError('Unknown/incomplete element reference; raw markup, styles and arbitrary links are prohibited.')
for field in fields:
k.text(element[field], 'element.' + field)
if not k.ID.fullmatch(element['id']) or element['id'] in seen:
raise ValueError('Element IDs must be unique within the screen.')
seen.add(element['id'])
row = entries.get(element['item'])
if row is None:
raise ValueError('Selected component/pattern was removed or is unknown; no replacement inferred.')
if element['variant'] not in (row['variants'] or ['not-applicable']):
raise ValueError('Selected variant is not declared by the kit entry.')
if element['state'] not in (row['states'] or ['not-applicable']):
raise ValueError('Selected state is not declared by the kit entry.')
example = sources.get(element['example_source'])
if example is None or example['path'] not in {ref['path'] for ref in row['sources']}:
raise ValueError('The exact example source must be supplied and covered by its kit entry.')
requested.append(element['item'])
if not any(entries[e['item']]['kind'] == 'pattern' for e in elements):
raise ValueError('A kit screen needs a selected composition pattern, not just approved individual controls.')
screen_rows[screen['id']] = elements
try:
check = k.inspect_data(reader, data, raw, requested, [scope])
except OSError as exc:
raise ValueError('Kit selection blocked: a declared source is missing or unreadable: ' + str(exc)) from exc
if check['state'] != 'REVIEW_REQUIRED':
raise ValueError('Kit selection blocked: ' + '; '.join(check['blockers'] + check['pending']))
checked_ids = {row['id'] for row in check['checked_items']}
references = {catalogue_path: hashlib.sha256(raw).hexdigest()}
for ref in [data['approval'], data['preview']] + [ref for key in checked_ids for ref in entries[key]['sources'] + [entries[key]['approval']]]:
if ref is not None:
references[ref['path']] = ref['sha256']
# Capture inputs exclude approval prose but include every selected source dependency.
for screen in ui['screens']:
context = screen.get('capture_context')
needed = set()
def add(key):
if key not in needed:
needed.add(key)
for child in entries[key]['uses']:
add(child)
for e in screen_rows[screen['id']]:
add(e['item'])
expected = {ref['path']: ref['sha256'] for key in needed for ref in entries[key]['sources']}
if screen['representation'] != 'capture':
raise ValueError('Kit-backed screen requires an actual capture, never an invented schematic.')
if not isinstance(context, dict) or set(context) != {'source', 'locator', 'summary', 'capture_sha256', 'inputs'}:
raise ValueError('Each selected screen needs a capture_context tied to existing evidence and captured source hashes.')
k.text(context['source'], 'capture_context.source')
if context['source'] not in sources or context['source'] not in screen['sources']:
raise ValueError('Capture context must name a listed evidence source.')
for field in ('locator', 'summary'):
k.text(context[field], 'capture_context.' + field)
capture_path = sources[screen['capture']]['path']
if context['capture_sha256'] != hashlib.sha256(reader.read(capture_path)).hexdigest():
raise ValueError('Capture image differs from its recorded fingerprint.')
refs = context['inputs']
if not isinstance(refs, list) or not 1 <= len(refs) <= 256:
raise ValueError('Capture inputs must be a bounded list of inspected source fingerprints.')
observed = {}
for ref in refs:
k.reference(ref)
if ref['path'] in observed:
raise ValueError('Duplicate capture input path.')
if not reader.match(ref):
raise ValueError('Capture input changed or disappeared: ' + ref['path'])
observed[ref['path']] = ref['sha256']
references[ref['path']] = ref['sha256']
if any(observed.get(path) != digest for path, digest in expected.items()):
raise ValueError('Capture context omits a selected component/example/theme dependency or records older bytes.')
return {'kit': data, 'entries': entries, 'screens': screen_rows, 'references': references,
'source': binding['source'], 'scope': scope}
REUSED_FIELDS = {'id', 'source', 'locator', 'tag', 'classes', 'context', 'changes'}
TAG = re.compile(r'(?:[a-z][a-z0-9-]*|[A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_$][A-Za-z0-9_$]*)*)')
CLASS = re.compile(r'-?[A-Za-z_][A-Za-z0-9_-]{0,80}')
ELEMENT_ID = re.compile(r'[A-Za-z][A-Za-z0-9_-]{0,15}')
def check_reused(elements, sources: dict) -> None:
"""Without a kit catalogue, `elements` names the existing markup a screen copies (D14 reuse)."""
if not isinstance(elements, list) or not 1 <= len(elements) <= 20:
raise ValueError('List one to twenty reused elements per screen.')
seen = set()
for element in elements:
if not isinstance(element, dict) or set(element) != REUSED_FIELDS:
raise ValueError('A reused element needs exactly id, source, locator, tag, classes, context and changes '
'(the existing element it copies); raw markup and styles are prohibited.')
if not isinstance(element['id'], str) or not ELEMENT_ID.fullmatch(element['id']) or element['id'] in seen:
raise ValueError('Reused element IDs must be short and unique within the screen.')
seen.add(element['id'])
if element['source'] not in sources:
raise ValueError(f"Reused element {element['id']} must cite a supplied source (the file it copies).")
for field in ('locator', 'context', 'changes'):
value = element[field]
if not isinstance(value, str) or not value.strip() or len(value) > 300 or any(c in value for c in '<>{}'):
raise ValueError(f'Reused element {field} must be plain text (no markup or styles).')
if not isinstance(element['tag'], str) or len(element['tag']) > 41 or not TAG.fullmatch(element['tag']):
raise ValueError('Reused element tag must be a plain tag or component name, for example "div", "ion-chip" or "Form.Group".')
classes = element['classes']
if not isinstance(classes, list) or len(classes) > 20 or not all(isinstance(c, str) and CLASS.fullmatch(c) for c in classes):
raise ValueError('Reused element classes must be a list of existing class names (empty when it has none).')
def reused_html(screen: dict, api) -> str:
e = html.escape
rows = []
for item in screen['elements']:
classes = f' class="{" ".join(item["classes"])}"' if item['classes'] else ''
rows.append(f'<tr id="ui-element-{e(screen["id"])}-{e(item["id"])}"><td><strong>{e(item["id"])}</strong></td>'
f'<td>{api.links([item["source"]],"s")} · {e(item["locator"])}</td>'
f'<td><code>{e("<" + item["tag"] + classes + ">")}</code><small>Context: {e(item["context"])}</small></td>'
f'<td>{e(item["changes"])}</td></tr>')
return ('<section class="ui-elements"><h3>Existing elements this screen copies</h3>'
'<p class="muted">Same tag, classes and wrapper as the cited markup; no new CSS. '
'The capture or sketch still needs review against the built screen.</p>'
'<div class="table-scroll appendix"><table><thead><tr><th>On screen</th><th>Copied from</th>'
'<th>Tag, classes and context</th><th>Intended changes</th></tr></thead><tbody>' + ''.join(rows) +
'</tbody></table></div></section>')
def snapshot_records(resolved: dict | None, sources: dict) -> list[dict]:
if resolved is None:
return []
supplied = {row['path'] for row in sources.values()}
result = []
for index, (path, digest) in enumerate(sorted(resolved['references'].items()), 1):
if path not in supplied:
key = f'kit-input-{index}'
if key in sources:
raise ValueError('Reserved kit-input source identity collision.')
result.append({'id': key, 'role': 'ui-kit-input', 'path': path, 'sha256': digest})
return result
def selection_html(screen: dict, resolved: dict | None, sources: dict, api) -> str:
if resolved is None:
if screen.get('elements'):
return reused_html(screen, api)
return ('<p class="muted"><strong>Component provenance not established.</strong> '
'This view shows structure, not proof of the final look.</p>')
e = html.escape
paths = {row['path']: key for key, row in sources.items()}
paths.update({row['path']: row['id'] for row in snapshot_records(resolved, sources)})
rows = []
for selection in resolved['screens'][screen['id']]:
entry = resolved['entries'][selection['item']]
links = ' '.join(f'<a href="#s-{e(paths[ref["path"]])}"><code>{e(ref["path"])}</code></a>' for ref in entry['sources'])
rows.append(f'<tr id="ui-element-{e(screen["id"])}-{e(selection["id"])}">'
f'<td><strong>{e(selection["id"])} · {e(selection["role"])}</strong></td>'
f'<td>{e(entry["name"])}<small>{e(entry["kind"])} · {e(entry["id"])}</small></td>'
f'<td>{e(selection["variant"])} / {e(selection["state"])}</td>'
f'<td>{api.links([selection["example_source"]],"s")} · {e(selection["example_locator"])}<small>{links}</small></td>'
f'<td>{e(selection["changes"])}</td></tr>')
return ('<section class="ui-elements"><h3>Exactly which kit elements are used</h3>'
f'<p>Catalogue: {api.links([resolved["source"]],"s")} · {e(resolved["kit"]["kit_id"])} '
f'· {e(resolved["kit"]["revision"])} · scope <code>{e(resolved["scope"])}</code>.</p>'
'<p class="muted">Declared approved entries; listed source fingerprints match. '
'The capture and its evidence still require review: matching hashes do not authenticate consent '
'or prove these components produced the image.</p>'
'<p class="hint">Scroll the reference table horizontally on small screens.</p>'
'<div class="table-scroll appendix"><table><thead><tr><th>On screen</th><th>Kit entry</th>'
'<th>Variant / state</th><th>Example and actual sources</th><th>Intended changes</th></tr></thead>'
'<tbody>'+''.join(rows)+'</tbody></table></div></section>')
scripts/quick_preview.py"""Source-backed, inert HTML/CSS fragment previews for the existing UI review.
Only an existing DOM subtree and existing compiled CSS are displayed. No framework
runtime, application network calls, generated UI classes or designer approvals.
"""
from __future__ import annotations
import html
import re
from html.parser import HTMLParser
from pathlib import Path
MAX_HTML = 250_000
MAX_CSS = 500_000
MAX_PREVIEWS = 6
VOID = {'br', 'hr', 'input', 'meta', 'source', 'wbr', 'img', 'link', 'base', 'embed'}
ALLOWED = {'div','span','p','strong','em','b','i','small','label','button','input',
'section','article','header','footer','main','nav','aside','ul','ol','li',
'h1','h2','h3','h4','h5','h6','table','thead','tbody','tr','td','th','a'}
SELECTOR = re.compile(r'^(?:[a-z][a-z0-9-]*)?(?:\.[a-zA-Z_][\w-]*|#[a-zA-Z_][\w-]*)$')
SAFE_ATTR = {'class', 'id', 'role', 'type', 'value', 'placeholder', 'title',
'disabled', 'checked', 'selected', 'readonly', 'name', 'for', 'tabindex'}
def tag_allowed(tag: str) -> bool:
return tag in ALLOWED or bool(re.fullmatch(r'(?:ion|app)-[a-z][a-z0-9-]*', tag))
class Fragment(HTMLParser):
"""Select one element by tag/class/id; copy only inert elements and attributes."""
def __init__(self, selector: str):
super().__init__(convert_charrefs=True)
self.selector = selector
self.depth = 0
self.started = False
self.done = False
self.parts: list[str] = []
self.stack: list[bool] = []
self.blocked = 0
def matches(self, tag: str, attrs: dict[str, str]) -> bool:
selector = self.selector
name = selector.split('.', 1)[0].split('#', 1)[0]
if name and tag != name:
return False
if '#' in selector:
return attrs.get('id') == selector.split('#', 1)[1]
return selector.rsplit('.', 1)[1] in attrs.get('class', '').split()
def handle_starttag(self, tag, attrs):
if self.done:
return
values = dict(attrs)
if not self.started:
if not self.matches(tag, values):
return
self.started = True
if self.blocked or tag in {'script','style','iframe','object','embed','form','template',
'svg','math','video','audio','img','link','base'}:
self.blocked += 1 if tag not in VOID else 0
self.stack.append(False)
self.depth += 1
if tag in VOID:
self.handle_endtag(tag)
return
permitted = tag_allowed(tag)
self.stack.append(permitted)
self.depth += 1
if permitted:
safe = []
for key, value in attrs:
if value is None and key not in {'disabled', 'checked', 'selected', 'readonly'}:
continue
if key in SAFE_ATTR or key.startswith('aria-'):
if key == 'type' and value not in {'text','number','checkbox','radio','button','submit','email','password'}:
continue
safe.append(f' {key}="{html.escape(value if value is not None else key, quote=True)}"')
self.parts.append(f'<{tag}{"".join(safe)}>')
if tag in VOID:
self.handle_endtag(tag)
def handle_startendtag(self, tag, attrs):
self.handle_starttag(tag, attrs)
if tag not in VOID:
self.handle_endtag(tag)
def handle_endtag(self, tag):
if not self.started or self.done or not self.stack:
return
allowed = self.stack.pop()
self.depth -= 1
if self.blocked and tag not in VOID:
self.blocked -= 1
elif allowed and tag not in VOID:
self.parts.append(f'</{tag}>')
if self.depth <= 0:
self.done = True
def handle_data(self, data):
if self.started and not self.done and not self.blocked:
self.parts.append(html.escape(data))
def extract_fragment(source: str, selector: str) -> str:
parser = Fragment(selector)
parser.feed(source)
parser.close()
if not parser.done or not parser.parts:
raise ValueError(f'Quick preview cannot find a complete element at {selector!r}')
result = ''.join(parser.parts)
if len(result.encode('utf-8')) > 100_000:
raise ValueError('Quick preview fragment is too large; select a smaller element')
return result
def collect(ui: dict, root: Path, sources: dict, api) -> list[dict]:
previews = ui.get('quick_previews', [])
if not isinstance(previews, list) or len(previews) > MAX_PREVIEWS:
raise ValueError('quick_previews must be a list of at most six existing elements')
seen = set()
prepared = []
for item in previews:
if not isinstance(item, dict) or set(item) != {'id','title','html_source','selector','css_sources','note'}:
raise ValueError('Quick preview needs id, title, html_source, selector, css_sources and note')
name = item['id']
if not isinstance(name, str) or not re.fullmatch(r'[A-Za-z][\w-]{0,35}', name) or name in seen:
raise ValueError('Quick preview IDs must be unique simple identifiers')
seen.add(name)
api.text(item['title'], 'quick_preview.title', 80)
api.text(item['note'], 'quick_preview.note', 300)
selector = item['selector']
if not isinstance(selector, str) or not SELECTOR.fullmatch(selector):
raise ValueError('Quick preview selector must be a single tag.class, .class or #id')
hs = item['html_source']
if hs not in sources or not sources[hs]['path'].lower().endswith(('.html','.htm')):
raise ValueError('Quick preview needs an existing supplied HTML source')
html_path = api.safe_source(root, sources[hs]['path'])
if html_path.stat().st_size > MAX_HTML:
raise ValueError('Quick preview HTML source too large; use a smaller saved example')
fragment = extract_fragment(html_path.read_text(encoding='utf-8'), selector)
css_sources = item['css_sources']
if not isinstance(css_sources,list) or not 1 <= len(css_sources) <= 4 or len(set(css_sources)) != len(css_sources):
raise ValueError('Quick preview needs one to four unique compiled CSS sources')
css = []
for sid in css_sources:
if sid not in sources or not sources[sid]['path'].lower().endswith('.css'):
raise ValueError('Quick preview requires supplied compiled .css files (SCSS must be compiled first)')
path = api.safe_source(root, sources[sid]['path'])
if path.stat().st_size > MAX_CSS:
raise ValueError('Quick preview stylesheet too large; select focused compiled CSS')
contents = path.read_text(encoding='utf-8')
if '</style' in contents.lower():
raise ValueError('Quick preview stylesheet contains an unsafe style-tag terminator')
css.append(contents)
if sum(len(c.encode('utf-8')) for c in css) > MAX_CSS:
raise ValueError('Quick preview CSS total exceeds 500 KB')
prepared.append({**item, 'markup': fragment, 'css': '\n'.join(css)})
return prepared
def frame_document(markup: str, css: str) -> str:
# An opaque sandbox origin, no script/forms/navigation, and no remote assets.
csp = ("default-src 'none'; script-src 'none'; style-src 'unsafe-inline'; "
"img-src data:; font-src data:; connect-src 'none'; form-action 'none'; "
"base-uri 'none'; frame-src 'none'")
return (f'<!doctype html><html><head><meta charset="utf-8">'
f'<meta http-equiv="Content-Security-Policy" content="{csp}">'
f'<style>{css}\nhtml,body{{margin:0;}}body{{padding:20px;box-sizing:border-box;}}</style>'
f'</head><body>{markup}</body></html>')
def render(previews: list[dict], api) -> str:
if not previews:
return ''
rows = []
for p in previews:
doc = html.escape(frame_document(p['markup'], p['css']), quote=True)
title = html.escape(p['title'])
note = html.escape(p['note'])
src = api.links([p['html_source'],*p['css_sources']], 's')
frame = (f'<iframe sandbox="" loading="lazy" referrerpolicy="no-referrer" '
f'title="Static source-backed style sample: {title}" srcdoc="{doc}"></iframe>')
rows.append(f'<article class="ui-quick-card"><h3>{title}</h3><p>{note}</p>'
f'<div class="ui-quick-small">{frame}</div>'
f'<details><summary>Expand full-size sample</summary><div class="ui-quick-full">{frame}</div></details>'
f'<p class="ui-evidence">Existing source and compiled CSS: {src}</p></article>')
return ('<section class="ui-quick-gallery" aria-label="Existing styled UI samples">'
'<h2>Real-style component samples</h2>'
'<p class="hint">Static snippets of existing markup with existing compiled CSS. '
'No Angular/React runtime, interactions or network. Not a rendered proposed page or design approval.</p>'
f'<div class="ui-quick-grid">{"".join(rows)}</div></section>')
scripts/render_report.py#!/usr/bin/env python3
"""Render an offline evidence-linked HTML/SVG review; check supplied-source freshness.
This validates a report projection, not code semantics or user authorization.
Python 3.10+, standard library only. No repository writes except the requested HTML.
"""
from __future__ import annotations
import sys
# The CLI must not create helper or standard-library caches while checking input,
# including when argparse rejects a combination or a malformed flag value.
if any(arg.split('=', 1)[0] == '--validate-input' for arg in sys.argv[1:]):
sys.dont_write_bytecode = True
import argparse
import base64
import hashlib
import html
import importlib.util
import json
import re
import webbrowser
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
ASSETS = Path(__file__).resolve().parents[1] / 'assets'
ID = re.compile(r'[A-Za-z][A-Za-z0-9_-]{0,47}\Z')
MAX_BYTES = 8 * 1024 * 1024
STATES = {'draft', 'awaiting-approval', 'approved', 'inspection', 'superseded'}
CHANGE = {'unchanged', 'added', 'modified', 'removed'}
BASIS = {'observed', 'inferred', 'unknown', 'proposed'}
RENDERER_VERSION = '2.8.0-rc.25'
def ui_module():
path = Path(__file__).with_name('screen_flow.py')
spec = importlib.util.spec_from_file_location('workflow_screen_flow', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def task_module():
path = Path(__file__).with_name('task_report.py')
spec = importlib.util.spec_from_file_location('workflow_task_report', path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def renderer_api():
# Works when this script was loaded through importlib without registration in sys.modules.
from types import SimpleNamespace
return SimpleNamespace(text=text, rows=rows, indexed=indexed, references=references,
coordinate=coordinate, validate_geometry=validate_geometry,
safe_source=safe_source, links=links, elbow_path=elbow_path, edge_label_box=edge_label_box)
def text(value: Any, name: str, limit: int = 2000) -> str:
if not isinstance(value, str) or not value.strip() or len(value) > limit:
raise ValueError(f'{name}: expected nonempty text, at most {limit} characters')
if any(ord(c) < 32 and c not in '\n\t\r' for c in value):
raise ValueError(f'{name}: unsupported control character')
return value
def rows(value: Any, name: str, *, empty: bool = True) -> list[dict]:
if not isinstance(value, list) or any(not isinstance(v, dict) for v in value):
raise ValueError(f'{name}: expected an array of objects')
if not empty and not value:
raise ValueError(f'{name}: must not be empty')
return value
def indexed(value: Any, name: str, *, empty: bool = True) -> dict[str, dict]:
result: dict[str, dict] = {}
for row in rows(value, name, empty=empty):
key = text(row.get('id'), f'{name}.id', 48)
if not ID.fullmatch(key) or key in result:
raise ValueError(f'{name}: invalid or duplicate id {key!r}')
result[key] = row
return result
def references(value: Any, allowed: dict, name: str, *, required: bool = False) -> list[str]:
if not isinstance(value, list) or any(not isinstance(v, str) for v in value):
raise ValueError(f'{name}: expected an array of identifiers')
if required and not value:
raise ValueError(f'{name}: at least one reference is required')
if len(value) != len(set(value)) or any(v not in allowed for v in value):
raise ValueError(f'{name}: unknown or duplicate reference')
return value
def safe_source(root: Path, relative: Any) -> Path:
relative = text(relative, 'source path', 500)
# Root-relative POSIX paths keep the projection portable across Windows and Unix.
if '\\' in relative or ':' in relative or '?' in relative or '#' in relative:
raise ValueError(f'Unsupported source path: {relative}')
part = Path(relative)
if part.is_absolute() or '..' in part.parts or '.git' in part.parts:
raise ValueError(f'Source path must stay inside root: {relative}')
path = (root / part).resolve()
if not path.is_relative_to(root.resolve()) or not path.is_file():
raise ValueError(f'Missing or outside-root source: {relative}')
if path.stat().st_size > MAX_BYTES:
raise ValueError(f'Source exceeds 8 MiB limit: {relative}')
return path
def digest(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def load_json(path: Path) -> dict:
if path.stat().st_size > MAX_BYTES:
raise ValueError('Input exceeds 8 MiB limit')
value = json.loads(path.read_text(encoding='utf-8-sig'))
if not isinstance(value, dict):
raise ValueError('Input must be a JSON object')
return value
TASK_OUTCOMES = {
'planned': 'Not started', 'in-progress': 'In progress',
'implemented': 'Implemented, not yet checked', 'verified': 'Verified',
'blocked': 'Blocked', 'removed': 'Removed by the owner, not a passed test',
'superseded': 'Replaced, not a passed test', 'not-applicable': 'Not applicable',
'unknown': 'Unknown',
}
def validate_record_context(data: dict, sources: dict) -> None:
"""Check optional projection structure; never interpret Markdown as authorization."""
kind = data.get('report_kind', 'review')
if kind not in ('review', 'task-overview'):
raise ValueError('report_kind must be review or task-overview')
checkpoint = data.get('checkpoint')
if kind == 'task-overview' and checkpoint is None:
raise ValueError('A task-overview needs a sourced checkpoint')
if checkpoint is not None:
if not isinstance(checkpoint, dict):
raise ValueError('checkpoint must be an object')
for key in ('as_of', 'locator', 'next_action'):
text(checkpoint.get(key), f'checkpoint.{key}')
if checkpoint.get('source') not in sources:
raise ValueError('checkpoint.source must name a supplied source')
approval = checkpoint.get('approval')
if not isinstance(approval, dict) or approval.get('status') not in ('approved', 'waived', 'not-approved', 'unknown'):
raise ValueError('checkpoint.approval needs an explicit status')
text(approval.get('summary'), 'checkpoint.approval.summary')
revision = approval.get('revision')
if revision is not None:
text(revision, 'checkpoint.approval.revision')
if approval['status'] in ('approved', 'waived') and revision is None:
raise ValueError('Approved/waived checkpoint needs the exact covered revision')
if approval['status'] in ('not-approved', 'unknown') and revision is not None:
raise ValueError('An unknown/unapproved checkpoint cannot name an approved revision')
statuses = {
'implementation': {'not-started', 'in-progress', 'implemented', 'partial', 'unknown'},
'verification': {'not-run', 'in-progress', 'blocked', 'passed', 'failed', 'limited', 'unknown'},
}
for field, allowed in statuses.items():
value = checkpoint.get(field)
if not isinstance(value, dict) or value.get('status') not in allowed:
raise ValueError(f'checkpoint.{field} needs an explicit status')
text(value.get('summary'), f'checkpoint.{field}.summary')
if kind == 'review' and data['state'] == 'awaiting-approval' and approval['status'] == 'approved' and revision == data['revision']:
raise ValueError('Already approved scope needs a task-overview, not another approval request')
if data['state'] == 'approved' and (approval['status'] != 'approved' or revision != data['revision']):
raise ValueError('Approved report and checkpoint must identify the same approved revision')
if 'revision_log' in data:
history = rows(data['revision_log'], 'revision_log')
if len(history) > 100:
raise ValueError('Use at most 100 revision rows; link older history from the source')
seen = set()
for row in history:
revision = text(row.get('revision'), 'revision_log.revision')
if revision in seen:
raise ValueError('Duplicate revision_log revision; record presentation aliases in one row')
seen.add(revision)
for field in ('change', 'reason', 'locator'):
text(row.get(field), f'revision_log.{field}')
if row.get('source') not in sources:
raise ValueError('revision_log.source must name a supplied source')
if row.get('status') not in ('proposed', 'presented', 'approved', 'superseded', 'rejected', 'withdrawn', 'unknown'):
raise ValueError('revision_log.status is invalid')
if 'review' in row:
text(row['review'], 'revision_log.review')
if checkpoint and row['revision'] == checkpoint['approval'].get('revision'):
if row['status'] in ('rejected', 'withdrawn', 'superseded'):
raise ValueError('Current approval conflicts with the revision_log outcome')
def validate(data: dict, root: Path) -> dict[str, dict]:
if data.get('schema_version') != 2:
raise ValueError('New renders require schema_version 2; migrate the layout using REPORT.md. Existing HTML remains checkable with --check.')
for key in ('title', 'revision', 'understanding'):
text(data.get(key), key)
mode, state = data.get('mode'), data.get('state')
if mode not in ('current', 'proposed', 'compare') or state not in STATES:
raise ValueError('Invalid mode or state')
if mode == 'current' and state != 'inspection':
raise ValueError('Current-only maps must use inspection state')
if mode != 'current' and state == 'inspection':
raise ValueError('Inspection state is current-only')
snapshot = data.get('snapshot')
if not isinstance(snapshot, dict):
raise ValueError('snapshot must be an object')
for key in ('repository', 'basis'):
text(snapshot.get(key), f'snapshot.{key}')
if 'branch' in snapshot:
text(snapshot['branch'], 'snapshot.branch')
for field in ('boundaries', 'open_decisions'):
if not isinstance(data.get(field), list):
raise ValueError(f'{field} must be an array')
for item in data[field]:
text(item, field)
if 'write_paths' in data: # the files an approved plan may change; the phase gate enforces them after approval
paths = data['write_paths']
if not isinstance(paths, list) or not paths:
raise ValueError('write_paths must be a non-empty array of repository-relative paths')
for path in paths:
text(path, 'write_paths', 300)
if path.startswith(('/', '~')) or re.match(r'^[A-Za-z]:', path) or '\\' in path or '..' in path.split('/') \
or any(c in path for c in '*?[]'):
raise ValueError(f'write_paths entries must be plain repository-relative files or folders ending in / (got {path!r})')
sources = indexed(data.get('sources'), 'sources', empty=False)
for row in sources.values():
text(row.get('role'), 'source.role', 80)
safe_source(root, row.get('path'))
validate_record_context(data, sources)
requirements = indexed(data.get('requirements'), 'requirements')
for row in requirements.values():
text(row.get('text'), 'requirement.text')
text(row.get('origin'), 'requirement.origin')
evidence = indexed(data.get('evidence'), 'evidence')
for row in evidence.values():
text(row.get('claim'), 'evidence.claim')
if row.get('kind') not in ('static', 'runtime', 'design'):
raise ValueError(f"Evidence kind must be static, runtime or design (evidence {row.get('id')}: got {row.get('kind')!r})")
references(row.get('sources'), sources, 'evidence.sources', required=True)
text(row.get('locator'), 'evidence.locator')
text(row.get('limits'), 'evidence.limits')
acceptance = indexed(data.get('acceptance'), 'acceptance')
for row in acceptance.values():
text(row.get('text'), 'acceptance.text')
text(row.get('verification'), 'acceptance.verification')
references(row.get('requirements'), requirements, 'acceptance.requirements', required=True)
tasks = indexed(data.get('tasks'), 'tasks')
for row in tasks.values():
text(row.get('title'), 'task.title')
text(row.get('reason'), 'task.reason')
if 'outcome' in row:
if row['outcome'] not in TASK_OUTCOMES:
raise ValueError('task.outcome is invalid')
text(row.get('outcome_note'), 'task.outcome_note')
elif 'outcome_note' in row:
raise ValueError('task.outcome_note requires an explicit outcome')
if row.get('kind') not in ('requested', 'prerequisite'):
raise ValueError('Task kind must be requested or prerequisite')
references(row.get('requirements'), requirements, 'task.requirements', required=True)
references(row.get('evidence'), evidence, 'task.evidence', required=True)
references(row.get('acceptance'), acceptance, 'task.acceptance', required=True)
references(row.get('depends_on'), tasks, 'task.depends_on')
if row['id'] in row['depends_on']:
raise ValueError('Task cannot depend on itself')
if not isinstance(row.get('areas'), list) or not row['areas']:
raise ValueError('task.areas must name at least one area')
for area in row['areas']:
text(area, 'task.area')
pending = {k: set(v['depends_on']) for k, v in tasks.items()}
while pending:
ready = {k for k, v in pending.items() if not v}
if not ready:
raise ValueError('Task dependency cycle')
pending = {k: v - ready for k, v in pending.items() if k not in ready}
graphs = indexed(data.get('graphs'), 'graphs', empty=False)
if len(graphs) > 6:
raise ValueError('Use at most six focused views per report')
kinds = {g.get('kind') for g in graphs.values()}
expected = {'current', 'proposed'} if mode == 'compare' else {mode}
if kinds != expected:
raise ValueError('Graphs must match mode; compare requires current and proposed')
for graph in graphs.values():
text(graph.get('title'), 'graph.title')
nodes = indexed(graph.get('nodes'), 'nodes', empty=False)
for node in nodes.values():
text(node.get('label'), 'node.label', 80)
if node.get('note') != '':
text(node.get('note'), 'node.note', 112)
for edge in rows(graph.get('edges'), 'edges'):
if edge.get('from') not in nodes or edge.get('to') not in nodes:
raise ValueError('Edge endpoint does not exist')
if edge.get('label') != '':
text(edge.get('label'), 'edge.label', 48)
for item in list(nodes.values()) + graph['edges']:
if item.get('basis') not in BASIS or item.get('change') not in CHANGE:
raise ValueError('Each node/edge needs valid basis and change')
basis = item['basis']
references(item.get('evidence'), evidence, 'graph.evidence', required=basis in ('observed', 'inferred'))
references(item.get('tasks'), tasks, 'graph.tasks', required=item['change'] != 'unchanged')
if basis in ('observed', 'inferred'):
if not any(evidence[e]['kind'] in ('static', 'runtime') for e in item['evidence']):
raise ValueError('Observed/inferred code needs static or runtime evidence')
if basis == 'unknown' and not item.get('uncertainty'):
raise ValueError('Unknown relationships must explain uncertainty')
if 'uncertainty' in item:
text(item['uncertainty'], 'graph.uncertainty')
if graph['kind'] == 'current' and (basis == 'proposed' or item['change'] != 'unchanged'):
raise ValueError('Current flow cannot contain future behavior/change labels')
if graph['kind'] == 'proposed' and item['change'] in ('added', 'modified') and basis != 'proposed':
raise ValueError('Added/modified future behavior must be marked proposed')
validate_geometry(graph, nodes)
validate_delivery(graph, tasks)
if data.get('default_view') not in graphs:
raise ValueError('default_view must identify a graph in this report')
if mode == 'compare' and (graphs[data['default_view']]['kind'] != 'proposed' or graphs[data['default_view']]['purpose'] != 'flow'):
raise ValueError('Approval comparison opens with the proposed feature flow, not a build plan or structural map')
comparison = rows(data.get('comparison'), 'comparison')
for row in comparison:
for key in ('label', 'current', 'proposed'):
text(row.get(key), f'comparison.{key}')
references(row.get('requirements'), requirements, 'comparison.requirements', required=True)
references(row.get('evidence'), evidence, 'comparison.evidence', required=True)
references(row.get('tasks'), tasks, 'comparison.tasks', required=True)
if mode == 'compare' and not comparison:
raise ValueError('Comparison needs an evidence-linked current/proposed change summary')
if state in ('awaiting-approval', 'approved'):
if mode != 'compare' or data['open_decisions']:
raise ValueError('Approval-ready reviews need compare mode and no blocking decisions')
roles = {s['role'] for s in sources.values()}
if not {'plan', 'todo', 'findings'}.issubset(roles):
raise ValueError('Approval-ready reviews need plan, todo and findings sources')
if not requirements or not tasks or not acceptance or not evidence:
raise ValueError('Approval-ready reviews need requirements, tasks, acceptance and evidence')
covered = {r for a in acceptance.values() for r in a['requirements']}
planned = {r for task in tasks.values() for r in task['requirements']}
if set(requirements) - covered or set(requirements) - planned:
raise ValueError('Every requirement needs acceptance and planned work')
if state == 'approved':
approval = data.get('approval')
if not isinstance(approval, dict) or approval.get('revision') != data['revision']:
raise ValueError('Approved state needs matching recorded approval revision')
for field in ('record', 'response'):
text(approval.get(field), f'approval.{field}')
if 'ui_review' in data:
ui_module().validate(data, root, sources, renderer_api())
if 'task_report' in data:
task_module().validate(data, sources, renderer_api())
return sources
def esc(value: Any) -> str:
return html.escape(str(value), quote=True)
def links(ids: list[str], prefix: str) -> str:
return ' '.join(f'<a href="#{prefix}-{esc(v)}">{esc(v)}</a>' for v in ids) or '—'
def coordinate(value: Any, name: str, lower: int = 0, upper: int = 2400) -> int:
if type(value) is not int or not lower <= value <= upper or value % 4:
raise ValueError(f'{name}: use a 4px-grid integer from {lower} to {upper}')
return value
def boxes_overlap(a: tuple, b: tuple, gap: float = 0) -> bool:
x, y, w, h = a
xx, yy, ww, hh = b
return x < xx + ww + gap and xx < x + w + gap and y < yy + hh + gap and yy < y + h + gap
def segment_hits_box(a: list, b: list, box: tuple, inset: float = 0) -> bool:
x, y, w, h = box
if a[0] == b[0]:
return x + inset < a[0] < x + w - inset and max(min(a[1], b[1]), y + inset) < min(max(a[1], b[1]), y + h - inset)
return y + inset < a[1] < y + h - inset and max(min(a[0], b[0]), x + inset) < min(max(a[0], b[0]), x + w - inset)
def segments_meet(a: list, b: list, c: list, d: list) -> bool:
if a[0] == b[0] and c[0] == d[0]:
return a[0] == c[0] and max(min(a[1], b[1]), min(c[1], d[1])) <= min(max(a[1], b[1]), max(c[1], d[1]))
if a[1] == b[1] and c[1] == d[1]:
return a[1] == c[1] and max(min(a[0], b[0]), min(c[0], d[0])) <= min(max(a[0], b[0]), max(c[0], d[0]))
if a[0] != b[0]:
a, b, c, d = c, d, a, b
return min(c[0], d[0]) <= a[0] <= max(c[0], d[0]) and min(a[1], b[1]) <= c[1] <= max(a[1], b[1])
def port_side(node: dict, point: list) -> str:
x, y, w, h = (node[k] for k in ('x', 'y', 'width', 'height'))
px, py = point
if node['shape'] == 'decision':
for side, port in (('top', [x+w/2, y]), ('right', [x+w, y+h/2]), ('bottom', [x+w/2, y+h]), ('left', [x, y+h/2])):
if point == port:
return side
else:
if py == y and x+16 <= px <= x+w-16: return 'top'
if py == y+h and x+16 <= px <= x+w-16: return 'bottom'
if px == x and y+16 <= py <= y+h-16: return 'left'
if px == x+w and y+16 <= py <= y+h-16: return 'right'
raise ValueError('Connector endpoint must attach to an allowed node boundary port')
def outward(side: str, point: list, adjacent: list) -> bool:
return {'top': adjacent[1] < point[1] and adjacent[0] == point[0],
'bottom': adjacent[1] > point[1] and adjacent[0] == point[0],
'left': adjacent[0] < point[0] and adjacent[1] == point[1],
'right': adjacent[0] > point[0] and adjacent[1] == point[1]}[side]
def edge_label_box(edge: dict) -> tuple:
x, y = edge['label_position']
width = 16 + len(edge['label']) * 7
return x - width/2, y - 15, width, 22
def validate_geometry(graph: dict, nodes: dict) -> None:
width = coordinate(graph.get('width'), 'graph.width', 400, 1600)
height = coordinate(graph.get('height'), 'graph.height', 240, 1600)
if graph.get('purpose') not in ('flow', 'delivery', 'structure', 'technical'):
raise ValueError('Graph purpose must be flow, delivery, structure or technical')
for field in ('tab', 'description'):
text(graph.get(field), f'graph.{field}', 700 if field == 'description' else 36)
if len(nodes) > 9 or len(graph['edges']) > 12:
raise ValueError('Split the diagram: at most 9 nodes and 12 connectors per view')
if sum(n.get('highlight', False) is True for n in nodes.values()) > 2:
raise ValueError('Use at most two focal nodes per view')
boxes = {}
for key, node in nodes.items():
for field, low, high in (('x', 0, width), ('y', 0, height), ('width', 128, width), ('height', 48, height)):
coordinate(node.get(field), f'node.{field}', low, high)
if node.get('shape') not in ('start', 'action', 'decision', 'result', 'group'):
raise ValueError(f"Node needs a start/action/decision/result/group shape (node {key}: got {node.get('shape')!r})")
if 'highlight' in node and type(node['highlight']) is not bool:
raise ValueError('node.highlight must be boolean')
box = tuple(node[k] for k in ('x', 'y', 'width', 'height'))
x, y, w, h = box
if x+w > width-16 or y+h > height-16 or x < 16 or y < 16:
raise ValueError(f'Node exceeds the diagram bounds/padding (node {key}: box {box}, view {width}x{height}, 16px padding)')
if any(boxes_overlap(box, other, gap=12) for other in boxes.values()):
raise ValueError(f'Overlapping nodes or less than 12px separation (node {key} vs ' + ', '.join(k for k, o in boxes.items() if boxes_overlap(box, o, gap=12)) + ')')
boxes[key] = box
lines = node['label'].split('\n')
notes = node['note'].split('\n') if node['note'] else []
available = w * .62 if node['shape'] == 'decision' else w-40
# Conservative character budgets catch obvious clipping; browser measurements remain required.
if len(lines) > 2 or any(len(line) * 10 > available for line in lines):
raise ValueError(f'Shorten node label or widen its shape; never shrink type (node {key}: longest line needs ~{max(len(l) for l in lines) * 10}px, {available:.0f}px available, at most 2 lines)')
if len(notes) > 2 or any(len(line) * 7 > w-40 for line in notes):
raise ValueError(f'Shorten node note or move detail below the diagram (node {key}: at most 2 lines of ~{(w-40)//7} characters)')
if node['shape'] == 'decision' and (notes or h < 128):
raise ValueError(f'Decision diamond needs >=128px height and no note; detail belongs below (node {key}: height {h})')
items = node.get('items', [])
if not isinstance(items, list) or len(items) > 9 or (items and node['shape'] != 'group'):
raise ValueError('Only group nodes have items, at most nine')
for item in items:
text(item, 'node.items', 60)
if len(item) * 8 > w-48:
raise ValueError('Shorten group item or widen the group')
needed = 36 + 24*len(lines) + 20*len(notes) + (36*len(items)+12 if items else 0)
if node['shape'] != 'decision' and h < needed-12:
raise ValueError(f'Node height is too small for its text (node {key}: height {h}, needs {needed-12})')
all_segments = []
ports: dict[str, list[list]] = {}
masks = []
for edge in graph['edges']:
points = edge.get('points')
if not isinstance(points, list) or not 2 <= len(points) <= 16:
raise ValueError('Connector needs 2–16 explicit orthogonal points')
for point in points:
if not isinstance(point, list) or len(point) != 2:
raise ValueError('Connector point must be [x, y]')
coordinate(point[0], 'point.x', 4, width-4)
coordinate(point[1], 'point.y', 4, height-4)
segments = list(zip(points, points[1:]))
for a, b in segments:
if a == b or (a[0] != b[0] and a[1] != b[1]):
raise ValueError('Connectors must be orthogonal with distinct consecutive points')
if abs(a[0]-b[0]) + abs(a[1]-b[1]) < 12:
raise ValueError('Connector segments need at least 12px for clear elbows')
for box in boxes.values():
if segment_hits_box(a, b, box):
raise ValueError(f"Connector crosses a node; route outside boxes (edge {edge['from']}->{edge['to']} segment {a}->{b} crosses " + next(k for k, bx in boxes.items() if bx == box) + ')')
for key, point, neighbor in ((edge['from'], points[0], points[1]), (edge['to'], points[-1], points[-2])):
side = port_side(nodes[key], point)
if not outward(side, point, neighbor):
raise ValueError(f"Connector must leave/enter the node outward, not along its boundary (edge {edge['from']}->{edge['to']} at node {key}, point {point})")
if any(abs(point[0]-p[0]) + abs(point[1]-p[1]) < 12 for p in ports.get(key, [])):
raise ValueError(f"Give connectors distinct ports at least 12px apart (node {key}, point {point})")
ports.setdefault(key, []).append(point)
for i, (a, b) in enumerate(segments):
if any(segments_meet(a, b, c, d) for c, d in segments[i+2:]):
raise ValueError(f"Connector doubles back or crosses itself (edge {edge['from']}->{edge['to']})")
if any(segments_meet(a, b, c, d) for c, d in all_segments):
raise ValueError(f"Connectors cross or share a stroke; reroute or split the view (edge {edge['from']}->{edge['to']} segment {a}->{b})")
all_segments.extend(segments)
if edge['label']:
if len(edge['label']) > 14 or '\n' in edge['label']:
raise ValueError(f"Keep connector labels within 14 characters, one line (edge {edge['from']}->{edge['to']}: {edge['label']!r} has {len(edge['label'])})")
position = edge.get('label_position')
if not isinstance(position, list) or len(position) != 2:
raise ValueError('Labelled connectors need label_position [x, baseline_y]')
for i, value in enumerate(position):
coordinate(value, 'label_position', 0, width if i == 0 else height)
mask = edge_label_box(edge)
mx, my, mw, mh = mask
if mx < 0 or my < 0 or mx+mw > width or my+mh > height:
raise ValueError(f"Connector label falls outside the view (edge {edge['from']}->{edge['to']}: label box {mask}, view {width}x{height})")
if any(boxes_overlap(mask, box, gap=4) for box in boxes.values()):
raise ValueError(f"Connector label overlaps a node (edge {edge['from']}->{edge['to']}: label box {mask})")
if any(boxes_overlap(mask, other, gap=4) for other in masks):
raise ValueError(f"Connector labels overlap (edge {edge['from']}->{edge['to']}: label box {mask})")
masks.append(mask)
elif nodes[edge['from']]['shape'] == 'decision':
raise ValueError(f"Every decision branch needs an explicit label (edge {edge['from']}->{edge['to']})")
for mask in masks:
x, y, w, h = mask
padded = (x-6, y-6, w+12, h+12)
if any(segment_hits_box(a, b, padded) for a, b in all_segments):
raise ValueError(f'Connector label needs at least a 6px gap from routes (label box {mask})')
for key, node in nodes.items():
outgoing = [e for e in graph['edges'] if e['from'] == key]
if node['shape'] == 'decision':
if not 2 <= len(outgoing) <= 3 or len({e['label'].casefold() for e in outgoing}) != len(outgoing):
raise ValueError(f'A decision needs two or three distinctly labelled exits (node {key}: ' + ', '.join(repr(e["label"]) for e in outgoing) + ')')
def validate_delivery(graph: dict, tasks: dict) -> None:
if graph['purpose'] != 'delivery':
return
if graph['kind'] != 'proposed':
raise ValueError('Delivery view describes proposed work, not current application behavior')
nodes = {n['id']: n for n in graph['nodes']}
task_nodes, approvals = {}, []
for node in nodes.values():
if node.get('precondition') == 'approval':
if node['shape'] != 'start' or node['tasks']:
raise ValueError('Approval precondition is a start node, not a task or recorded approval')
approvals.append(node['id'])
else:
task = node.get('task_id')
if task not in tasks or node['tasks'] != [task] or task in task_nodes:
raise ValueError('Delivery nodes need a unique task_id matching their task reference')
task_nodes[task] = node['id']
if len(approvals) != 1:
raise ValueError('Delivery view needs exactly one explicit approval precondition')
selected = set(task_nodes)
external = {dep for key in selected for dep in tasks[key]['depends_on'] if dep not in selected}
declared = graph.get('external_dependencies', [])
references(declared, tasks, 'external_dependencies')
if set(declared) != external:
raise ValueError('Partial delivery view must declare external_dependencies; do not hide prerequisites')
expected = {(task_nodes[dep], task_nodes[key]) for key in selected for dep in tasks[key]['depends_on'] if dep in selected}
roots = {task_nodes[key] for key in selected if not set(tasks[key]['depends_on']) & selected}
expected |= {(approvals[0], key) for key in roots}
actual = [(edge['from'], edge['to']) for edge in graph['edges']]
if len(actual) != len(set(actual)) or set(actual) != expected:
raise ValueError('Delivery edges must match the task dependencies and approval precondition')
def graph_records(graph: dict) -> str:
details = []
for node in graph['nodes']:
details.append(f'<li><strong>{esc(node["id"])}: {esc(node["label"])}</strong>. {esc(node["note"])} '
f'({esc(node["basis"])}, {esc(node["change"])}) {esc(node.get("uncertainty", ""))} '
f'Evidence {links(node["evidence"], "e")}; tasks {links(node["tasks"], "t")}.</li>')
for edge in graph['edges']:
details.append(f'<li><strong>{esc(edge["from"])} → {esc(edge["to"])}</strong>: {esc(edge["label"] or "continues")} '
f'({esc(edge["basis"])}, {esc(edge["change"])}) {esc(edge.get("uncertainty", ""))} '
f'Evidence {links(edge["evidence"], "e")}; tasks {links(edge["tasks"], "t")}.</li>')
return f'<details><summary>{esc(graph["tab"])}: steps and links</summary><ol>{"".join(details)}</ol></details>'
def elbow_path(points: list) -> str:
result = f'M {points[0][0]} {points[0][1]}'
for i in range(1, len(points)-1):
previous, corner, following = points[i-1:i+2]
length_in = abs(corner[0]-previous[0]) + abs(corner[1]-previous[1])
length_out = abs(following[0]-corner[0]) + abs(following[1]-corner[1])
radius = min(8, length_in/2, length_out/2)
before = [corner[j] + (previous[j]-corner[j])/length_in*radius for j in (0, 1)]
after = [corner[j] + (following[j]-corner[j])/length_out*radius for j in (0, 1)]
result += f' L {before[0]:g} {before[1]:g} Q {corner[0]} {corner[1]} {after[0]:g} {after[1]:g}'
return result + f' L {points[-1][0]} {points[-1][1]}'
FRIENDLY_CAPTIONS = {'proposed': 'Proposed, not built yet', 'current': 'As the code works today', 'delivery': 'Order of the work'}
def friendly_tab(graph: dict) -> str:
"""Plain tab names for central task reports; other views keep their recorded tab."""
if graph.get('purpose') == 'delivery':
return 'Work order'
if graph.get('purpose') == 'flow':
return {'proposed': 'After the change', 'current': 'Today'}.get(graph.get('kind'), graph['tab'])
return graph['tab']
def graph_html(graph: dict, friendly: bool = False, delivered: bool = False) -> str:
key = graph['id']
start = min(graph['nodes'], key=lambda node: (node['shape'] != 'start', node['y'], node['x']))
start_id = f'node-{len(key)}-{key}-{start["id"]}'
parts = [f'<svg class="map" xmlns="http://www.w3.org/2000/svg" width="{graph["width"]}" height="{graph["height"]}" '
f'viewBox="0 0 {graph["width"]} {graph["height"]}" role="img" aria-labelledby="{key}-title {key}-desc">'
f'<title id="{key}-title">{esc(graph["title"])}</title><desc id="{key}-desc">{esc(graph["description"])}</desc>'
f'<defs><marker id="arrow-{key}" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto">'
'<path d="M 0 0 L 10 4 L 0 8 Z"/></marker></defs>']
for edge in graph['edges']:
parts.append(f'<path class="connector {edge["basis"]}" data-from="{esc(edge["from"])}" data-to="{esc(edge["to"])}" '
f'data-points="{esc(json.dumps(edge["points"]))}" d="{elbow_path(edge["points"])}" marker-end="url(#arrow-{key})"/>' )
for edge in graph['edges']:
if edge['label']:
x, y, w, h = edge_label_box(edge)
cx, baseline = edge['label_position']
parts.append(f'<g class="arrow-label"><rect x="{x:g}" y="{y:g}" width="{w:g}" height="{h:g}"/>'
f'<text class="edge-label" text-anchor="middle" x="{cx}" y="{baseline}">{esc(edge["label"].upper())}</text></g>')
for node in graph['nodes']:
x, y, w, h = (node[v] for v in ('x', 'y', 'width', 'height'))
cx, cy = x+w/2, y+h/2
shape = node['shape']
classes = f'node {shape} {node["basis"]} {"focal" if node.get("highlight") else ""}'
parts.append(f'<g id="node-{len(key)}-{key}-{esc(node["id"])}" class="{classes}" data-id="{esc(node["id"])}" data-box="{x},{y},{w},{h}">'
f'<title>{esc(node["label"])}. {esc(node["basis"])} / {esc(node["change"])}. '
f'{esc(node.get("uncertainty", ""))}</title>')
if shape == 'decision':
parts.append(f'<polygon class="node-shape" points="{cx:g},{y} {x+w},{cy:g} {cx:g},{y+h} {x},{cy:g}"/>')
else:
radius = 20 if shape in ('start', 'result') else 6
parts.append(f'<rect class="node-shape" x="{x}" y="{y}" width="{w}" height="{h}" rx="{radius}"/>')
lines = node['label'].split('\n')
notes = node['note'].split('\n') if node['note'] else []
items = node.get('items', [])
tx = x+24 if shape == 'group' else cx
anchor = 'start' if shape == 'group' else 'middle'
baseline = y+36 if shape == 'group' else cy-(len(lines)-1)*12-len(notes)*10+6
for i, line in enumerate(lines):
parts.append(f'<text class="node-title" x="{tx:g}" y="{baseline+i*24:g}" text-anchor="{anchor}">{esc(line)}</text>')
for i, line in enumerate(notes):
parts.append(f'<text class="node-note" x="{tx:g}" y="{baseline+len(lines)*24+i*20:g}" text-anchor="{anchor}">{esc(line)}</text>')
for i, line in enumerate(items):
iy = baseline + len(lines)*24 + len(notes)*20 + 20+i*36
parts.append(f'<text class="node-item" x="{x+24}" y="{iy:g}">{esc(line)}</text>')
parts.append('</g>')
parts.append('</svg>')
shapes = {n['shape'] for n in graph['nodes']}
legend = []
for shape, label in (('start', 'Start / result'), ('action', 'Action'), ('decision', 'Decision'), ('group', 'Group')):
if shape in shapes or (shape == 'start' and 'result' in shapes):
legend.append(f'<span><i class="legend-{shape}" aria-hidden="true"></i>{label}</span>')
if any(n.get('highlight') for n in graph['nodes']):
legend.append('<span><i class="legend-focus" aria-hidden="true"></i>Focus of the change</span>')
caption = f'{graph["kind"]} · {graph["purpose"]}'
if friendly:
caption = FRIENDLY_CAPTIONS['delivery' if graph['purpose'] == 'delivery' else graph['kind']]
if delivered and graph['kind'] == 'proposed':
caption = 'As built. The results are below.' # round-5 retro: "not built yet" under "Delivered"
built_note = 'The proposed flow is what was built. Its checks are below.' if delivered else 'Proposed is not implemented.'
external_note = f'<p>Other required tasks outside this view: {links(graph["external_dependencies"], "t")}</p>' if graph.get('external_dependencies') else ''
tag = 'h3' if friendly else 'h2'
return (f'<figure class="panel" id="panel-{key}"><div class="figure-head"><{tag}>{esc(graph["title"])}</{tag}>'
f'<span>{esc(caption)}</span></div><p class="figure-description">{esc(graph["description"])}</p>'
f'<p class="hint">Scroll sideways to explore. <a href="#{start_id}">Go to start</a>. Fit is an optional overview.</p>'
f'<div class="canvas" tabindex="0" role="region" aria-label="{esc(graph["tab"])} diagram, scroll to explore" '
f'data-start-id="{start_id}" style="--board-width:{graph["width"]}px">{"".join(parts)}</div>'
f'<figcaption><div class="legend">{"".join(legend)}</div>'
f'{external_note}<span class="muted">Dashed outlines show a guess or an unknown. Details and evidence are below. {built_note}</span></figcaption></figure>')
def view_controls(graphs: list, default: str, friendly: bool = False) -> tuple[str, str]:
inputs, labels, rules = [], [], []
names = {graph['id']: friendly_tab(graph) if friendly and 'kind' in graph else graph['tab'] for graph in graphs}
if len(set(names.values())) != len(names):
names = {graph['id']: graph['tab'] for graph in graphs} # never show two identical tab names
for graph in graphs:
key = graph['id']
inputs.append(f'<input class="sr-radio" type="radio" name="diagram-view" id="view-{key}" aria-label="{esc(names[key])}" '
f'aria-controls="panel-{key}" {"checked" if key == default else ""}>')
labels.append(f'<label for="view-{key}">{esc(names[key])}</label>')
rules.extend([f'#view-{key}:checked~.figures #panel-{key}{{display:block}}',
f'#view-{key}:checked~.viewbar label[for="view-{key}"]{{border-color:var(--accent);color:var(--ink)}}',
f'#view-{key}:focus-visible~.viewbar label[for="view-{key}"]{{outline:2px solid var(--accent);outline-offset:4px}}'])
zooms = []
for value, label in (('100', '100%'), ('125', '125%'), ('150', '150%'), ('fit', 'Fit')):
inputs.append(f'<input class="sr-radio" type="radio" name="diagram-zoom" id="zoom-{value}" '
f'aria-label="{label} diagram size" {"checked" if value == "100" else ""}>')
zooms.append(f'<label for="zoom-{value}">{label}</label>')
rules.extend([f'#zoom-{value}:checked~.viewbar label[for="zoom-{value}"]{{border-color:var(--ink);color:var(--ink)}}',
f'#zoom-{value}:focus-visible~.viewbar label[for="zoom-{value}"]{{outline:2px solid var(--accent);outline-offset:4px}}'])
if friendly:
controls = (''.join(inputs) + '<div class="viewbar"><div class="view-tabs" role="group" aria-labelledby="view-tabs-name">'
'<span class="control-name" id="view-tabs-name">Show</span>' + ''.join(labels) +
'</div><div class="zoom-tabs labelled" role="group" aria-labelledby="zoom-tabs-name">'
'<span class="control-name" id="zoom-tabs-name">Zoom</span>' + ''.join(zooms) + '</div></div>')
return controls, '\n'.join(rules)
controls = (''.join(inputs) + '<div class="viewbar"><div class="view-tabs" role="group" aria-label="Diagram views">' +
''.join(labels) + '</div><div class="zoom-tabs" role="group" aria-label="Diagram size">' + ''.join(zooms) + '</div></div>')
return controls, '\n'.join(rules)
def task_outcome_html(task: dict) -> str:
if 'outcome' not in task:
return ''
return f'<small class="task-outcome">{esc(TASK_OUTCOMES[task["outcome"]])}: {esc(task["outcome_note"])}</small>'
def record_context_html(data: dict) -> tuple[str, str]:
checkpoint = data.get('checkpoint')
current = ''
if checkpoint:
approval = checkpoint['approval']
labels = {'approved': 'Approved', 'waived': 'Explicit waiver', 'not-approved': 'Not approved', 'unknown': 'Unknown'}
revision = f' · {approval["revision"]}' if approval.get('revision') else ''
axes = [('Approval', labels[approval['status']] + revision, approval['summary'])]
for label, key in [('Implementation', 'implementation'), ('Verification / acceptance', 'verification')]:
item = checkpoint[key]
axes.append((label, item['status'].replace('-', ' ').capitalize(), item['summary']))
columns = ''.join(f'<div><dt>{esc(label)}</dt><dd><strong>{esc(status)}</strong><p>{esc(summary)}</p></dd></div>'
for label, status, summary in axes)
current = (f'<section class="task-checkpoint" aria-labelledby="checkpoint-heading"><h2 id="checkpoint-heading">Current task checkpoint</h2>'
f'<p class="checkpoint-asof">As recorded: {esc(checkpoint["as_of"])} · {links([checkpoint["source"]], "s")} '
f', {esc(checkpoint["locator"])}</p><dl class="checkpoint-axes">{columns}</dl>'
f'<p class="next-action"><strong>Next safe action</strong> {esc(checkpoint["next_action"])}</p>'
'<p class="checkpoint-limit">Saved from the task records, not a live status check or proof of approval. '
'Approval, completed code and acceptance are different states.</p></section>')
history = ''
if data.get('revision_log'):
entries = ''.join(
f'<tr><td><strong>{esc(row["revision"])}</strong><small>{esc(row.get("review", "No review alias recorded"))}</small></td>'
f'<td>{esc(row["change"])}</td><td>{esc(row["reason"])}</td>'
f'<td>{esc(row["status"].capitalize())}<small>{links([row["source"]], "s")}, {esc(row["locator"])}</small></td></tr>'
for row in data['revision_log'])
history = ('<details class="revision-log" id="revision-history"><summary>Revision history: what changed and why</summary>'
'<p>Changed agreements, not a log of every retry. Review aliases are preserved as recorded; '
'a missing original is not reconstructed.</p><div class="table-scroll"><table><thead><tr>'
'<th>Revision / review</th><th>Change</th><th>Reason</th><th>Recorded outcome / source</th>'
f'</tr></thead><tbody>{entries}</tbody></table></div></details>')
return current, history
def plan_label(revision: str) -> str:
return revision if re.match(r'(?i)^plan\b', revision) else f'Plan {revision}'
def render(data: dict, root: Path, input_path: Path) -> str:
sources = validate(data, root)
input_path = input_path.resolve()
if not input_path.is_relative_to(root.resolve()):
raise ValueError('Input JSON must be inside the supplied source root')
if data != load_json(input_path):
raise ValueError('Projection data must match saved input JSON before rendering')
records = [{'id': key, 'role': value['role'], 'path': value['path'], 'sha256': digest(safe_source(root, value['path']))}
for key, value in sources.items()]
if 'ui_review' in data:
records.extend(ui_module().kit_records(data['ui_review'], root, sources))
records.append({'id': 'report-input', 'role': 'projection', 'path': input_path.relative_to(root.resolve()).as_posix(), 'sha256': digest(input_path)})
manifest = {'schema_version': 1, 'tool_version': RENDERER_VERSION, 'layout_version': 2, 'style': 'workflow-editorial-v1',
'revision': data['revision'], 'report_kind': data.get('report_kind', 'review'), 'generated_utc': datetime.now(timezone.utc).isoformat(), 'sources': records,
**({'write_paths': data['write_paths']} if 'write_paths' in data else {})}
raw_manifest = json.dumps(manifest, ensure_ascii=True).replace('<', '\\u003c').replace('>', '\\u003e').replace('&', '\\u0026')
ordered = sorted(data['graphs'], key=lambda graph: graph['id'] != data['default_view'])
views = ordered + ([{'id': 'ui-preview', 'tab': 'UI screens'}] if 'ui_review' in data else [])
ui = data.get('ui_review') or {}
# Application UI changes open on their screen diagram unless the input says otherwise (D14 goal).
default = 'ui-preview' if ui.get('kind') == 'application' or ui.get('open_first') else data['default_view'] # owner: the diagram must be seen
friendly = 'task_report' in data
controls, view_css = view_controls(views, default, friendly)
if friendly:
# Owner's report design (2026-11-03): report-style.css and report-extra.js; older reports keep report.css.
css = '\n'.join((ASSETS/name).read_text(encoding='utf-8') for name in ('report-style.css', 'report-style-compat.css')) + '\n' + view_css
js = (ASSETS/'report.js').read_text(encoding='utf-8') + '\n' + (ASSETS/'report-extra.js').read_text(encoding='utf-8')
else:
css = (ASSETS/'report.css').read_text(encoding='utf-8') + '\n' + view_css
js = (ASSETS/'report.js').read_text(encoding='utf-8')
ui_html = ''
image_policy = "'none'"
frame_policy = "'none'"
if 'ui_review' in data:
css += '\n' + (ASSETS/'ui-review.css').read_text(encoding='utf-8')
js += '\n' + (ASSETS/'ui-review.js').read_text(encoding='utf-8')
ui_html = ui_module().render(data['ui_review'], root, sources, renderer_api())
if data['ui_review'].get('quick_previews'):
frame_policy = "'self' about:"
if any(s['representation'] == 'capture' for s in data['ui_review']['screens']):
image_policy = 'data:'
script_hash = base64.b64encode(hashlib.sha256(js.encode('utf-8')).digest()).decode('ascii')
state = data['state']
overview = data.get('report_kind') == 'task-overview'
checkpoint_html, history_html = record_context_html(data)
status = {'draft': 'DRAFT. Decisions or review are still open',
'awaiting-approval': 'WAITING FOR YOUR APPROVAL. No code is written yet',
'approved': 'APPROVAL RECORDED. Read the task record before you continue',
'inspection': 'CURRENT STATE. Inspection only, not a plan to build',
'superseded': 'REPLACED. Do not build from this plan'}[state]
if overview:
status = 'SAVED TASK OVERVIEW. Read the dated checkpoint. This is not a new request for approval'
req = ''.join(f'<li id="r-{esc(r["id"])}"><strong>{esc(r["id"])}.</strong> {esc(r["text"])}<small>Source: {esc(r["origin"])}</small></li>' for r in data['requirements'])
written = [f'Only these files may change: {", ".join(data["write_paths"])}.'] if data.get('write_paths') else []
bounds = ''.join(f'<li>{esc(b)}</li>' for b in written + data['boundaries']) or '<li>No additional boundaries recorded.</li>'
task_rows = ''.join(f'<tr id="t-{esc(t["id"])}"><td><strong>{esc(t["id"])}</strong></td><td><strong>{esc(t["title"])}</strong>'
f'<small>{esc(t["kind"])}: {esc(t["reason"])}<br>Areas: {esc(", ".join(t["areas"]))}</small>{task_outcome_html(t)}</td>'
f'<td>{links(t["requirements"], "r")}<br>{links(t["evidence"], "e")}<br>{links(t["acceptance"], "a")}</td>'
f'<td>{links(t["depends_on"], "t")}</td></tr>' for t in data['tasks'])
task_summary = ''.join(f'<li><a href="#t-{esc(t["id"])}">{esc(t["id"])}</a> {esc(t["title"])}'
f'{task_outcome_html(t)}<small>Depends on: {esc(", ".join(t["depends_on"]) or "No prior task")}</small></li>' for t in data['tasks'])
acceptance = ''.join(f'<li id="a-{esc(a["id"])}"><strong>{esc(a["id"])}: {esc(a["text"])}</strong>'
f'<small>Verify: {esc(a["verification"])} · {links(a["requirements"], "r")}</small></li>' for a in data['acceptance'])
ev = ''.join(f'<article id="e-{esc(e["id"])}" class="evidence"><strong>{esc(e["id"])} · {esc(e["kind"])}: {esc(e["claim"])}</strong>'
f'<p>{esc(e["locator"])}</p><small>Sources: {links(e["sources"], "s")}<br>Limits: {esc(e["limits"])}</small></article>' for e in data['evidence'])
source_rows = ''.join(f'<tr id="s-{esc(s["id"])}"><td>{esc(s["id"])}</td><td><code>{esc(s["path"])}</code></td>'
f'<td>{esc(s["role"])}</td><td><code>{s["sha256"][:16]}…</code></td></tr>' for s in records)
issues = ''.join(f'<li>{esc(v)}</li>' for v in data['open_decisions'])
issues = f'<section class="warning"><h2>Open decisions. The plan is not ready to approve</h2><ul>{issues}</ul></section>' if issues else ''
uncertainty = sorted({item.get('uncertainty', '') for g in data['graphs'] for item in g['nodes'] + g['edges'] if item.get('uncertainty')})
if uncertainty:
issues += '<section class="warning"><h2>Evidence limits</h2><ul>' + ''.join(f'<li>{esc(v)}</li>' for v in uncertainty) + '</ul></section>'
comparison_rows = ''.join(f'<tr><td><strong>{esc(row["label"])}</strong><small>{links(row["requirements"], "r")}</small></td>'
f'<td>{esc(row["current"])}<small>{links(row["evidence"], "e")}</small></td>'
f'<td>{esc(row["proposed"])}<small>{links(row["tasks"], "t")}</small></td></tr>' for row in data['comparison'])
current_heading = 'Baseline before the selected change' if overview else 'Today, from the recorded evidence'
proposed_heading = 'Reviewed scope (the checkpoint shows progress)' if overview else 'Reviewed scope' if state in ('approved', 'superseded') else 'Proposed scope'
comparison = (f'<section class="comparison"><h2>What changes</h2><div class="table-scroll"><table><thead><tr>'
f'<th>Area</th><th>{esc(current_heading)}</th><th>{esc(proposed_heading)}</th></tr></thead>'
f'<tbody>{comparison_rows}</tbody></table></div></section>') if comparison_rows else ''
if overview:
gate = ('<section class="approval"><h2>The task record decides.</h2><div><p>This saved overview does not approve '
'work, reopen an approval or confirm that work is finished. An older review can still say "waiting for approval". '
'Use the dated checkpoint and the real approval record.</p>'
'<p>There is no approve button and no connection to the agent.</p></div></section>')
elif state == 'awaiting-approval':
gate = (f'<section class="approval"><h2>Review before code.</h2><div><p>Does this match what you want built? '
f'Approve plan {esc(data["revision"])} in the chat, or say what to change.</p>'
'<p>Opening this report does not approve anything. There is no approve button, no connection to the agent '
'and no work that starts in the background.</p></div></section>')
elif state == 'approved':
gate = (f'<section class="approval"><h2>Recorded approval reference</h2><div><p>{esc(data["approval"]["record"])}</p>'
f'<blockquote>{esc(data["approval"]["response"])}</blockquote><p>This page cannot prove who approved, and it gives '
'no permission. Compare it with the real task record.</p></div></section>')
else:
gate = ('<section class="approval"><h2>No permission to build</h2><div><p>This page does not allow code changes. '
'Settle the open decisions first, then approve the plan in the chat.</p></div></section>')
delivered = (data.get('task_report') or {}).get('phase') == 'delivery'
flow_context = (f'<p class="flow-context">Flow reviewed for {esc(data["revision"])}. The "Today" view shows the code '
'before the change. ' + ('The proposed view is what was built. The checks below show what was verified.</p>'
if delivered else 'The proposed view shows the reviewed scope. It does not prove that the work was built.</p>')) if overview else ''
if overview and data['mode'] == 'current':
flow_context = '<p class="flow-context">Current flow as inspected for this saved snapshot; not a live repository map.</p>'
task_heading = 'Tasks in the reviewed scope' if overview else 'Changes and cleanup'
viewer_note = ('<p class="muted">System-font mode. No network assets. A saved projection, not a live repository map.</p>')
head = f'''<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-{script_hash}'; style-src 'unsafe-inline'; img-src {image_policy}; frame-src {frame_policy}; connect-src 'none'; font-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">
<title>{esc(data['title'])}</title><style>{css}</style></head><body><main>
<header><div class="mast"><strong>{esc(data['snapshot']['repository'])}</strong><span>{esc('Delivery ' + data['revision'] if data.get('task_report', {}).get('phase') == 'delivery' else plan_label(data['revision']))}</span></div>
<h1>{esc(data['title'])}</h1>'''
snapshot_html = f'''<details class="snapshot"><summary>Inspected snapshot and supplied-source fingerprints</summary><p>{esc(data['snapshot']['repository'])} · {esc(data['snapshot'].get('branch', 'No branch recorded'))}</p>
<p>{esc(data['snapshot']['basis'])}</p><p>Generated {esc(manifest['generated_utc'])}. Hashes cover only these supplied files, including local changes. They do not prove complete code coverage or correctness.</p><div class="table-scroll"><table><thead><tr><th>ID</th><th>Source</th><th>Role</th><th>SHA-256 prefix</th></tr></thead><tbody>{source_rows}</tbody></table></div></details>'''
tail = f'''{viewer_note}<footer>Local HTML + SVG · no external assets, uploads or approval actions · saved review snapshot; progress belongs in the existing task records</footer>
</main><script type="application/json" id="report-manifest">{raw_manifest}</script><script>{js}</script></body></html>'''
if 'task_report' in data:
from types import SimpleNamespace
parts = SimpleNamespace(
controls=controls, figures=''.join(graph_html(g, friendly=True, delivered=(data.get('task_report') or {}).get('phase') == 'delivery') for g in ordered) + ui_html,
flow_context=flow_context, checkpoint=checkpoint_html, history=history_html, requirements=req,
task_rows=task_rows, evidence=ev, graph_records=''.join(graph_records(g) for g in data['graphs']),
snapshot=snapshot_html, uncertainty=uncertainty, outcome=task_outcome_html)
status_line, facts_html, body = task_module().render(data, renderer_api(), parts)
tag, good = task_module().status_tag(data)
mast = esc('Delivery ' + data['revision'] if data['task_report']['phase'] == 'delivery' else plan_label(data['revision']))
return f'''<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-{script_hash}'; style-src 'unsafe-inline'; img-src {image_policy}; frame-src {frame_policy}; connect-src 'none'; font-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'">
<title>{esc(data['title'])}</title><style>{css}</style></head><body><a class="skip-link" href="#report">Skip to the report</a>
<div class="page"><header class="masthead"><strong>{esc(data['snapshot']['repository'])}</strong><span>{mast}</span></header>
<main id="report"><header class="report-header"><h1 class="report-title">{esc(data['title'])}</h1>
<p class="report-lead">{esc(data["understanding"])}</p>
<p class="status status--{'approved' if good else esc(state)}"><span class="status__tag">{esc(tag)}</span><span>{esc(status_line)}</span></p>
{facts_html}</header>
{body}</main><footer class="report-footer"><p>Local HTML and SVG. No external files, uploads or approval actions. A saved snapshot: progress belongs in the task records.</p></footer></div>
<script type="application/json" id="report-manifest">{raw_manifest}</script><script>{js}</script></body></html>'''
return f'''{head}<p class="status {state}">{esc(status)}</p><p class="lead">{esc(data['understanding'])}</p></header>
{checkpoint_html}{issues}{flow_context}<section class="review" aria-label="Flowchart review">{controls}<div class="figures">{''.join(graph_html(g) for g in ordered)}{ui_html}</div></section>
{comparison}<section class="boundaries"><h2>What will not change</h2><ul>{bounds}</ul></section>
<div class="two-col"><section><h2>{task_heading}</h2><ul class="plain">{task_summary or '<li>Inspection only; no implementation tasks.</li>'}</ul></section>
<section><h2>How completion will be checked</h2><ul class="plain">{acceptance or '<li>No implementation acceptance criteria for this inspection.</li>'}</ul></section></div>
{gate}{history_html}<details class="appendix" id="technical-details"><summary>Technical evidence and task details</summary><div class="appendix-body">
<section><h2>Requested outcome</h2><ul class="plain">{req or '<li>No implementation requirements in this inspection.</li>'}</ul></section>
<section><h2>Changes and mapped tasks</h2><div class="table-scroll"><table><thead><tr><th>Task</th><th>Work and affected areas</th><th>Traceability</th><th>Depends on</th></tr></thead><tbody>{task_rows or '<tr><td colspan="4">Inspection only; no implementation tasks.</td></tr>'}</tbody></table></div></section>
<section><h2>Evidence and limits</h2>{ev}</section>{''.join(graph_records(g) for g in data['graphs'])}
{snapshot_html}
</div></details>{tail}'''
def check_report(path: Path, root: Path) -> list[str]:
if path.stat().st_size > MAX_BYTES:
raise ValueError('Report exceeds 8 MiB limit')
content = path.read_text(encoding='utf-8')
match = re.search(r'<script type="application/json" id="report-manifest">(.*?)</script>',content,re.S)
if not match:
raise ValueError('No supported report manifest found')
manifest = json.loads(match[1])
if not isinstance(manifest, dict) or manifest.get('schema_version') != 1:
raise ValueError('Unsupported report manifest')
changes=[]
for source in rows(manifest.get('sources'),'manifest.sources',empty=False):
value=source.get('sha256')
if not isinstance(value,str) or not re.fullmatch(r'[0-9a-f]{64}',value):
raise ValueError('Invalid manifest source hash')
relative=text(source.get('path'),'manifest.path',500)
try:
if digest(safe_source(root,relative)) != value:
changes.append(f'CHANGED: {relative}')
except ValueError:
changes.append(f'MISSING OR UNSAFE: {relative}')
return changes
def unpresented(path: Path, root: Path, data: dict) -> None:
"""Refuse to replace a report that is not an unpresented draft this renderer made (audit E18)."""
try:
check_report(path, root)
content = path.read_text(encoding='utf-8')
except (ValueError, OSError) as exc:
raise ValueError(f'--replace-unpresented replaces only a report this renderer made ({exc})') from exc
# The manifest carries no state (older reports must stay byte-identical), so read the approved markers that
# only an approved state renders: the earlier layout's status class, the task layout's plan approval record and tag.
if any(marker in content for marker in ('<p class="status approved">', '<h2>Plan approval record</h2>',
'<span class="status__tag">Approved</span>')):
raise ValueError(f'--replace-unpresented refuses {path.name}: the existing report is approved. Render a new file.')
# The task records name a report once it was shown or approved.
for source in (data.get('sources') or []):
try:
named = path.name in safe_source(root, source['path']).read_text(encoding='utf-8', errors='replace')
except (ValueError, OSError, KeyError, TypeError):
continue
if named:
raise ValueError(f'--replace-unpresented refuses {path.name}: the supplied source {source["path"]} names it, '
'so it counts as shown. Render a new file.')
def main() -> int:
parser=argparse.ArgumentParser(description=__doc__)
parser.add_argument('--root',type=Path,required=True,help='Repository/source root; read-only.')
parser.add_argument('--input',type=Path,help='JSON projection inside root.')
parser.add_argument('--output',type=Path,help='HTML file to create. An existing file is never overwritten, '
'except an unpresented draft with --replace-unpresented.')
parser.add_argument('--replace-unpresented',action='store_true',
help='Replace an existing report this renderer made. Refused when the report is approved or '
'a supplied source file names it (the task records name a report once it was shown).')
parser.add_argument('--open',action='store_true',help='Request default-browser opening; not proof of display. '
'Skipped while wording problems remain.')
parser.add_argument('--check',type=Path,help='Check an existing HTML against its supplied sources; no writes.')
parser.add_argument('--validate-input',action='store_true',
help='Validate input without files or browser opening; exit 1 for wording problems.')
args=parser.parse_args()
previous_bytecode = sys.dont_write_bytecode
if args.validate_input:
sys.dont_write_bytecode = True
try:
root=args.root.resolve(strict=True)
if not root.is_dir():
raise ValueError('--root must be a directory')
if args.validate_input:
if not args.input or args.output or args.open or args.check or args.replace_unpresented:
raise ValueError('--validate-input requires --input and cannot combine with output, open, check or replace')
if not args.input.resolve().is_relative_to(root):
raise ValueError('Input JSON must be inside the supplied source root')
data = load_json(args.input)
result = render(data, root, args.input)
if len(result.encode('utf-8')) > MAX_BYTES:
raise ValueError('Rendered report exceeds 8 MiB; reduce captures or split the view')
issues = task_module().prose_issues(data)
for path, problem in issues:
print(f'PLAIN ENGLISH: {path}: {problem}')
for path, note in task_module().prose_notes(data):
print(f'PLAIN ENGLISH NOTE: {path}: {note}')
if issues:
print('INVALID WORDING: fix these fields before recording the final report path.')
return 1
print('VALID: input checks passed; no files written or browser opened. Not semantic or approval verification.')
return 0
if args.check:
if args.input or args.output or args.open:
raise ValueError('--check cannot be combined with render arguments')
changes=check_report(args.check,root)
print('\n'.join(changes) if changes else 'MATCH: supplied source bytes match; this is not semantic or approval verification.')
return 1 if changes else 0
if not args.input or not args.output:
raise ValueError('Rendering requires --input and --output')
if not args.input.resolve().is_relative_to(root):
raise ValueError('Input JSON must be inside the supplied source root')
if args.output.suffix.lower() not in ('.html', '.htm'):
raise ValueError('Output must use .html or .htm extension')
result=render(load_json(args.input),root,args.input)
if len(result.encode('utf-8')) > MAX_BYTES:
raise ValueError('Rendered report exceeds 8 MiB; reduce captures or split the view')
# Wording advice is computed before the write (audit E17). It never blocks a valid report.
data = load_json(args.input)
issues = task_module().prose_issues(data)
notes = task_module().prose_notes(data)
# Exclusive creation protects earlier reviewed revisions and unrelated files. A draft not yet shown to the
# user may be replaced on request, but only a file this renderer made (round v5: -v2/-v3 copies).
replacing = args.replace_unpresented and args.output.exists()
if replacing:
unpresented(args.output, root, data)
with args.output.open('w' if replacing else 'x',encoding='utf-8',newline='\n') as out:
out.write(result)
print(f'{"REPLACED" if replacing else "RENDERED"}: {args.output.resolve()}')
print('Artifact creation does not approve or start implementation.')
for path, problem in issues: # simple-english advice; fix the JSON, then render again (--replace-unpresented if not yet shown)
print(f'PLAIN ENGLISH: {path}: {problem}')
for path, note in notes:
print(f'PLAIN ENGLISH NOTE: {path}: {note}')
if issues:
print(f'PLAIN ENGLISH: {len(issues)} problem(s). Rewrite those fields with the simple-english skill, then render into the same file with --replace-unpresented.')
if args.open and issues:
print('NOT OPENED: wording problems remain. Opening would show the report to the user. Fix them first, then render again with --open.')
elif args.open:
try:
accepted=webbrowser.open(args.output.resolve().as_uri(),new=2)
print('OPEN REQUEST ACCEPTED; display not verified.' if accepted else 'OPEN REQUEST NOT ACCEPTED; use the file path manually.')
except (OSError,webbrowser.Error) as exc:
print(f'OPEN REQUEST FAILED: {exc}; report is still available at the path above.')
return 0
except (OSError,ValueError,TypeError,KeyError,UnicodeError) as exc:
print(f'ERROR: {exc}',file=sys.stderr)
return 2
finally:
sys.dont_write_bytecode = previous_bytecode
if __name__=='__main__':
raise SystemExit(main())
scripts/screen_flow.py"""Optional, inert UI screen flow inside the existing review. No application code execution.
Input is a bounded screen description or local PNG; optional quick previews extract
existing inert markup and compiled CSS from supplied project sources.
The main renderer supplies its validation and geometry functions to avoid a second graph engine.
"""
from __future__ import annotations
import base64
import html
import importlib.util
import struct
import zlib
from pathlib import Path
MAX_CAPTURE = 2_000_000
MAX_CAPTURE_TOTAL = 4_000_000
MODES = {'unselected', 'behaviour-only', 'reuse', 'plain-by-instruction', 'blocked', 'functionality-only', 'approved-kit', 'design-authoring'}
CURRENT_MODES = {'behaviour-only', 'reuse', 'plain-by-instruction', 'approved-kit'} # D14: existing styles only
ELEMENTS = {'heading', 'text', 'checkbox', 'input', 'button', 'message'}
def bindings_module():
spec = importlib.util.spec_from_file_location('workflow_kit_bindings', Path(__file__).with_name('kit_bindings.py'))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def quick_module():
spec = importlib.util.spec_from_file_location('workflow_quick_preview', Path(__file__).with_name('quick_preview.py'))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def kit_records(ui, root, sources):
module = bindings_module()
return module.snapshot_records(module.resolve(ui, root, sources), sources)
def png_bytes(path: Path) -> tuple[bytes, int, int]:
if path.is_symlink() or path.stat().st_size > MAX_CAPTURE:
raise ValueError('UI capture must be an ordinary PNG of at most 2 MB')
raw = path.read_bytes()
if len(raw) > MAX_CAPTURE or not raw.startswith(b'\x89PNG\r\n\x1a\n'):
raise ValueError('UI capture is not a supported PNG')
offset, size, ended, has_pixels = 8, None, False, False
while offset + 12 <= len(raw):
length = struct.unpack('>I', raw[offset:offset+4])[0]
kind = raw[offset+4:offset+8]
end = offset + 12 + length
if end > len(raw):
raise ValueError('Truncated UI capture')
body = raw[offset+8:offset+8+length]
crc = struct.unpack('>I', raw[offset+8+length:end])[0]
if zlib.crc32(kind + body) & 0xffffffff != crc:
raise ValueError('Invalid UI capture checksum')
if size is None:
if kind != b'IHDR' or length != 13:
raise ValueError('UI capture needs a PNG header')
size = struct.unpack('>II', body[:8])
if not all(0 < n <= 8192 for n in size) or size[0]*size[1] > 16_777_216:
raise ValueError('UI capture dimensions exceed the supported limit')
elif kind == b'IHDR':
raise ValueError('Duplicate PNG header')
if kind == b'IDAT' and length:
has_pixels = True
if kind == b'acTL':
raise ValueError('Use a static UI capture, not an animated image')
offset = end
if kind == b'IEND':
if length or offset != len(raw):
raise ValueError('Unexpected trailing capture data')
ended = True
break
if not ended or size is None or not has_pixels:
raise ValueError('Incomplete UI capture')
return raw, *size
def record(value: object, name: str, sources: dict, api) -> None:
if not isinstance(value, dict) or value.get('source') not in sources:
raise ValueError(f'{name} needs an actual supplied source reference')
for field in ('locator', 'summary'):
api.text(value.get(field), name + '.' + field)
def validate(data: dict, root: Path, sources: dict, api) -> None:
ui = data.get('ui_review')
if ui is None:
return
if not isinstance(ui, dict):
raise ValueError('ui_review must be an object')
allowed = {'title','summary','kind','mode','authority','preview','acceptance','width','height',
'screens','transitions','decisions','open_first','limit','kit_binding','quick_previews'}
if set(ui) - allowed:
raise ValueError('Unknown UI review fields; raw HTML/CSS/URLs are not supported')
if any(g['id'] == 'ui-preview' for g in data['graphs']):
raise ValueError('ui-preview is reserved for the optional screen view')
for field in ('title','summary','limit'):
api.text(ui.get(field), 'ui_review.' + field)
if ui.get('kind') not in ('application','illustration') or ui.get('mode') not in MODES:
raise ValueError('UI review needs an explicit kind and design mode')
if type(ui.get('open_first', False)) is not bool:
raise ValueError('ui_review.open_first must be boolean')
record(ui.get('authority'), 'UI authority', sources, api)
if ui['authority'].get('status') not in ('unconfirmed','confirmed'):
raise ValueError('UI authority needs confirmed/unconfirmed status')
record(ui.get('preview'), 'Preview authority', sources, api)
if ui['preview'].get('status') not in ('proposed','authorised','not-needed'):
raise ValueError('Preview status is not-needed (sketches only), proposed or authorised')
api.text(ui['preview'].get('destination'), 'preview.destination')
acceptance = ui.get('acceptance')
if not isinstance(acceptance, dict) or acceptance.get('status') not in ('pending','accepted'):
raise ValueError('UI acceptance needs pending/accepted status')
api.text(acceptance.get('summary'), 'UI acceptance summary')
if acceptance['status'] == 'accepted':
record(acceptance, 'UI acceptance', sources, api)
if ui['kind'] == 'illustration' and acceptance['status'] != 'pending':
raise ValueError('An illustrative sample cannot claim application design acceptance')
if ui['mode'] == 'unselected' and ui['authority']['status'] == 'confirmed':
raise ValueError('An unselected UI mode cannot be confirmed')
quick_module().collect(ui, root, sources, api) # also validates source-backed static previews
decisions = api.indexed(ui.get('decisions'), 'UI decisions')
if len(decisions) > 8:
raise ValueError('Keep at most eight relevant UI decisions in this view')
screens = api.indexed(ui.get('screens'), 'UI screens', empty=False)
if len(screens) > 6:
raise ValueError('Split UI flows with more than six screens')
for d in decisions.values():
for key in ('title','question'):
api.text(d.get(key), 'UI decision.'+key, 500)
api.references(d.get('screens'), screens, 'decision.screens', required=True)
options = d.get('options')
if not isinstance(options,list) or not 2 <= len(options) <= 4 or len(set(options)) != len(options):
raise ValueError('UI decisions need two to four distinct text options')
for option in options:
api.text(option,'decision.option',180)
if d.get('status') not in ('open','selected','deferred'):
raise ValueError('UI decision status is invalid')
if d['status'] == 'selected':
if d.get('selection') not in options:
raise ValueError('Selected decision must name one of its options')
record(d, 'Selected UI decision', sources, api)
elif 'selection' in d:
raise ValueError('Unsettled decisions cannot imply a selected option')
if d['status'] == 'deferred':
api.text(d.get('reason'), 'Deferred UI decision reason')
requirements = {r['id']:r for r in data['requirements']}
evidence = {r['id']:r for r in data['evidence']}
tasks = {r['id']:r for r in data['tasks']}
total = 0
geometry = []
problems = []
for key,s in screens.items():
try:
allowed_screen = {'id','title','description','state','x','y','width','height','representation',
'content','capture','sources','requirements','evidence','tasks','decisions','markers','elements','capture_context'}
if set(s)-allowed_screen:
raise ValueError('Unknown screen fields; executable markup/styles are prohibited')
api.text(s.get('title'), 'screen.title', 48)
api.text(s.get('description'), 'screen.description', 700)
if s.get('state') not in ('current','proposed'):
raise ValueError('UI screen needs current/proposed state')
if data['mode'] == 'current' and s['state'] != 'current':
raise ValueError('Current inspection cannot include proposed UI screens')
api.references(s.get('sources'), sources, 'screen.sources', required=True)
api.references(s.get('decisions'), decisions, 'screen.decisions')
for field,lookup in [('requirements',requirements),('evidence',evidence),('tasks',tasks)]:
api.references(s.get(field),lookup,'screen.'+field,
required=ui['kind']=='application' and (field!='tasks' or s['state']=='proposed'))
for decision in s['decisions']:
if key not in decisions[decision]['screens']:
raise ValueError('Screen decision mapping disagrees with decision.screens')
for field,low,high in [('width',280,560),('height',280,720)]:
api.coordinate(s.get(field),'screen.'+field,low,high)
if len(s['title']) * 8 > s['width'] - 145 or any(c in s['title'] for c in '\n\r'):
raise ValueError('Screen title does not fit beside Inspect; shorten it or widen the screen')
if s.get('representation') == 'capture':
if 'content' in s or s.get('capture') not in s['sources']:
raise ValueError('Captured screen needs a listed source and no invented controls')
src = sources[s['capture']]
if src['role'] != 'ui-capture' or not src['path'].lower().endswith('.png'):
raise ValueError('Screen capture must reference a local ui-capture PNG source')
raw,_,_ = png_bytes(api.safe_source(root,src['path']))
total += len(raw)
markers = api.rows(s.get('markers', []), 'capture.markers')
if len(markers) > 8:
raise ValueError('Use at most eight capture decision markers')
for marker in markers:
if not isinstance(marker, dict):
raise ValueError('Capture marker must be an object')
if set(marker) == {'decision', 'position'}:
if marker['decision'] not in s['decisions']:
raise ValueError('Capture marker must name a mapped decision')
elif set(marker) == {'element', 'position'}:
elements = s.get('elements', [])
if not isinstance(elements, list) or marker['element'] not in {e.get('id') for e in elements if isinstance(e, dict)}:
raise ValueError('Capture marker must name a selected element')
else:
raise ValueError('Capture marker needs a decision or element and position')
position = marker['position']
if (not isinstance(position, list) or len(position) != 2 or
any(type(n) not in (int, float) or not 0 <= n <= 1 for n in position)):
raise ValueError('Capture marker position uses two fractions from 0 to 1')
elif s.get('representation') == 'schematic':
if 'capture' in s or 'markers' in s:
raise ValueError('Schematic screen cannot claim a captured source')
content = api.rows(s.get('content'),'screen.content',empty=False)
if len(content)>6:
raise ValueError('Use at most six controls/text blocks in one schematic screen')
if s['height'] < 120 + 48*len(content):
raise ValueError('Screen height is too small; expand it, do not shrink text')
for c in content:
if set(c)-{'kind','label','value','checked','disabled','decision'} or c.get('kind') not in ELEMENTS:
raise ValueError('Only structured inert schematic controls are supported')
api.text(c.get('label'),'control.label',42)
if any(ch in c['label'] for ch in '\n\r'):
raise ValueError('Use a single-line schematic label')
available = s['width'] - (86 if c.get('decision') else 48) - (26 if c['kind']=='checkbox' else 0)
estimate = 9 if c['kind']=='heading' else 7
if len(c['label']) * estimate > available:
raise ValueError('Schematic label will not fit; widen or shorten it')
if c.get('value') is not None:
api.text(c['value'],'control.value',42)
if any(ch in c['value'] for ch in '\n\r') or len(c['value'])*7 > s['width']-64:
raise ValueError('Schematic field value does not fit')
for flag in ('checked','disabled'):
if flag in c and type(c[flag]) is not bool:
raise ValueError('Control flags must be boolean')
if 'decision' in c and c['decision'] not in s['decisions']:
raise ValueError('Control decision is not mapped to this screen')
else:
raise ValueError('Use a captured PNG or an explicitly labelled schematic')
except ValueError as exc: # report every screen problem in one run, not one per attempt
problems.append(f"screen {s.get('id', key) if isinstance(s, dict) else key}: {exc}")
continue
geometry.append({**{f:s[f] for f in ('id','x','y','width','height')},
'label':s['title'], 'note':'','shape':'group'})
if problems:
raise ValueError('; '.join(problems))
if total > MAX_CAPTURE_TOTAL:
raise ValueError('UI captures exceed the 4 MB total report budget')
for d in decisions.values():
if any(d['id'] not in screens[s]['decisions'] for s in d['screens']):
raise ValueError('Decision must be mapped on all referenced screens')
transitions = api.rows(ui.get('transitions'), 'UI transitions')
for e in transitions:
if set(e)-{'from','to','label','points','label_position'}:
raise ValueError('Unknown UI transition fields')
if e.get('from') not in screens or e.get('to') not in screens:
raise ValueError('Unknown UI transition endpoint')
api.text(e.get('label'),'UI transition label',14)
graph={'width':ui.get('width'),'height':ui.get('height'),'purpose':'flow',
'tab':'UI screens','description':ui['summary'], 'nodes':geometry,'edges':transitions}
api.validate_geometry(graph,{n['id']:n for n in geometry})
# A sketch inside this offline report needs no permission (round 5: an extra question per plan);
# capturing a proposed screen means running a preview of the app, which does.
proposed_capture = any(s['state']=='proposed' and s['representation']=='capture' for s in screens.values())
if proposed_capture and ui['preview']['status']=='not-needed':
raise ValueError('preview.status not-needed is only for sketch-only reports; a captured proposed screen ran a preview')
if proposed_capture and ui['preview']['status']!='authorised':
raise ValueError('A captured proposed screen needs recorded preview authority (it ran a preview of the app); '
'a labelled sketch in this report needs none')
if ui['mode']=='approved-kit' and any(screen['representation']!='capture' for screen in screens.values()):
raise ValueError('Approved-kit review needs actual component captures; schematics do not prove its appearance')
if acceptance['status']=='accepted' and (ui['authority']['status']!='confirmed' or ui['mode']=='unselected'
or any(d['status']!='selected' for d in decisions.values())):
raise ValueError('An accepted design cannot retain an unselected source or unresolved design decisions')
if ui['kind']=='application' and data['state'] in ('awaiting-approval','approved'):
if ui['mode']=='unselected' or ui['authority']['status']!='confirmed' or (proposed_capture and ui['preview']['status']!='authorised'):
raise ValueError('Approval-ready UI work needs confirmed mode/source, and preview authority for captured proposed screens')
if any(d['status']=='open' for d in decisions.values()):
raise ValueError('Resolve blocking UI decisions before approval-ready output')
resolved = bindings_module().resolve(ui, root, sources)
# Drafts too: the combined-gates route always renders a draft (round-6 regression run).
if ui['kind']=='application' and ui['mode'] == 'reuse' and any(s['state'] == 'proposed' and not s.get('elements') for s in screens.values()):
raise ValueError('Reuse mode: every proposed screen lists the existing elements it copies '
'(elements: id, source, locator, tag, classes, context, changes)')
if ui['kind']=='application' and (data['state'] in ('awaiting-approval','approved') or acceptance['status']=='accepted'):
if ui['mode'] not in CURRENT_MODES:
raise ValueError(f"Approval-ready UI needs a current mode (behaviour-only, reuse or plain-by-instruction); got {ui['mode']!r}. "
'Unmatched elements stay blocked until the user names an element to mimic.')
if ui['mode'] == 'approved-kit' and resolved is None:
raise ValueError('Legacy approved-kit review needs its kit binding and real selected captures')
def screen_svg(screen: dict, root: Path, sources: dict, api, suffix: str) -> str:
"""A static screen view, reused verbatim in overview and enlarged details."""
e=html.escape
width,height=screen['width'],screen['height']
sid='ui-'+suffix+'-'+screen['id']
parts=[f'<svg class="ui-screen" xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" '
f'viewBox="0 0 {width} {height}" role="img" aria-labelledby="{sid}-title {sid}-desc">',
f'<title id="{sid}-title">{e(screen["title"])}</title><desc id="{sid}-desc">{e(screen["description"])}</desc>',
f'<rect class="ui-screen-paper" x="1" y="1" width="{width-2}" height="{height-2}" rx="6"/>',
f'<path class="ui-divider" d="M 1 48 H {width-1}"/>',
f'<text class="ui-screen-caption" x="20" y="30">{e(screen["title"])}</text>']
if screen['representation']=='capture':
raw,iw,ih=png_bytes(api.safe_source(root,sources[screen['capture']]['path']))
value=base64.b64encode(raw).decode('ascii')
parts.append(f'<image x="8" y="56" width="{width-16}" height="{height-86}" preserveAspectRatio="xMidYMin meet" '
f'href="data:image/png;base64,{value}"/>')
scale = min((width-16)/iw, (height-86)/ih)
left, top = 8 + (width-16-iw*scale)/2, 56
for marker in screen.get('markers', []):
dx,dy=marker['position'];mx=left+dx*iw*scale;my=top+dy*ih*scale
d=e(marker.get('decision', marker.get('element')))
target='ui-decision-'+d if 'decision' in marker else 'ui-element-'+e(screen['id'])+'-'+d
label = "Review decision " if "decision" in marker else "Inspect kit element "
parts.append(f'<a href="#{target}" aria-label="{label}{d}"><circle class="ui-marker" cx="{mx:g}" cy="{my:g}" r="13"/>'
f'<text class="ui-marker-text" x="{mx:g}" y="{my+4:g}" text-anchor="middle">{d}</text></a>')
else:
y=86
for c in screen['content']:
label=e(c['label']);kind=c['kind'];x=24
if kind=='heading':
parts.append(f'<text class="ui-control-heading" x="{x}" y="{y}">{label}</text>')
elif kind=='checkbox':
parts.append(f'<rect class="ui-checkbox" x="{x}" y="{y-14}" width="16" height="16" rx="1"/>')
if c.get('checked'):
parts.append(f'<path class="ui-check" d="M {x+3} {y-6} l 4 4 l 6 -9"/>')
parts.append(f'<text class="ui-control-text" x="{x+26}" y="{y}">{label}</text>')
elif kind=='input':
parts.append(f'<text class="ui-control-small" x="{x}" y="{y-8}">{label}</text>'
f'<rect class="ui-field" x="{x}" y="{y}" width="{width-48}" height="28" rx="2"/>'
f'<text class="ui-control-text" x="{x+8}" y="{y+19}">{e(c.get("value",""))}</text>')
elif kind=='button':
button_width=min(width-48,max(120,len(c['label'])*8+32))
disabled=' ui-disabled' if c.get('disabled') else ''
parts.append(f'<rect class="ui-button{disabled}" x="{x}" y="{y-20}" width="{button_width}" height="36" rx="3"/>'
f'<text class="ui-control-text" text-anchor="middle" x="{x+button_width/2}" y="{y+3}">{label}</text>')
elif kind=='message':
parts.append(f'<path class="ui-message-rule" d="M {x} {y-16} V {y+8}"/>'
f'<text class="ui-control-text" x="{x+12}" y="{y}">{label}</text>')
else:
parts.append(f'<text class="ui-control-small" x="{x}" y="{y}">{label}</text>')
if c.get('decision'):
d=e(c['decision']);cx=width-28
parts.append(f'<a href="#ui-decision-{d}" aria-label="Review decision {d}"><circle class="ui-marker" cx="{cx}" cy="{y-5}" r="13"/>'
f'<text class="ui-marker-text" x="{cx}" y="{y-1}" text-anchor="middle">{d}</text></a>')
y+=48
caption=(('Screenshot · kit parts listed below' if 'item' in screen['elements'][0] else 'Screenshot · copied elements listed below') if screen.get('elements') else 'Screenshot of the page') if screen['representation']=='capture' else 'Sketch · not a screenshot'
parts.append(f'<text class="ui-footnote" x="20" y="{height-16}">{caption}</text></svg>')
return ''.join(parts)
def render(ui: dict, root: Path, sources: dict, api) -> str:
e=html.escape
bindings = bindings_module()
resolved = bindings.resolve(ui, root, sources)
w,h=ui['width'],ui['height']
routes=[f'<svg class="ui-routes" width="{w}" height="{h}" viewBox="0 0 {w} {h}" aria-hidden="true">'
'<defs><marker id="ui-flow-arrow" viewBox="0 0 10 8" refX="9" refY="4" markerWidth="9" markerHeight="7" orient="auto">'
'<path d="M0 0 L10 4 L0 8 Z"/></marker></defs>']
for edge in ui['transitions']:
routes.append(f'<path class="connector" d="{api.elbow_path(edge["points"])}" marker-end="url(#ui-flow-arrow)"/>')
x,y,bw,bh=api.edge_label_box(edge);cx,baseline=edge['label_position']
routes.append(f'<g class="arrow-label"><rect x="{x:g}" y="{y:g}" width="{bw:g}" height="{bh:g}"/>'
f'<text class="edge-label" x="{cx}" y="{baseline}" text-anchor="middle">{e(edge["label"].upper())}</text></g>')
routes.append('</svg>')
cards=[];details=[]
for s in ui['screens']:
detail='ui-detail-'+s['id']
cards.append(f'<article class="ui-screen-node" id="ui-screen-{e(s["id"])}" '
f'style="left:{s["x"]}px;top:{s["y"]}px;width:{s["width"]}px;height:{s["height"]}px">'
f'{screen_svg(s,root,sources,api,"board")}<a class="ui-inspect" href="#{detail}">Inspect screen ↗</a></article>')
refs=api.links(s['sources'],'s')
full_screen=screen_svg(s,root,sources,api,'detail')
if s['representation']=='capture':
capture,iw,ih=png_bytes(api.safe_source(root,sources[s['capture']]['path']))
full_screen=(f'<img class="ui-capture-detail" width="{iw}" height="{ih}" alt="{e(s["description"])}" '
f'src="data:image/png;base64,{base64.b64encode(capture).decode("ascii")}"/>')
details.append(f'<details id="{detail}" class="ui-screen-detail"><summary>{e(s["title"])}: screen and sources</summary>'
f'<p>{e(s["description"])}</p>{full_screen}'
f'{bindings.selection_html(s, resolved, sources, api)}'
f'<p class="ui-evidence">State: {e(s["state"])}. Sources: {refs}. '
f'Requirements: {api.links(s["requirements"],"r")}. Tasks: {api.links(s["tasks"],"t")}.</p></details>')
decisions=[]
for d in ui['decisions']:
options=' / '.join(e(o) for o in d['options'])
state='Open: the owner must decide' if d['status']=='open' else 'Deferred: the design is not finished' if d['status']=='deferred' else 'Selected: '+e(d['selection'])
record_text=('<p>'+e(d.get('reason',''))+'</p>') if d['status']=='deferred' else ''
if d['status']=='selected':
record_text=f'<p>Recorded at {api.links([d["source"]],"s")}: {e(d["locator"])}. {e(d["summary"])}</p>'
decisions.append(f'<article id="ui-decision-{e(d["id"])}" class="ui-decision"><h3>{e(d["id"])} · {e(d["title"])}</h3>'
f'<p>{e(d["question"])}</p><p>Alternatives: {options}.</p><p><strong>{state}</strong></p>{record_text}</article>')
illustration=('<p class="ui-notice"><strong>Example UI only. It is outside the scope of this report.</strong> '
'These are fictional screens to demonstrate the report format, not an approved project kit.</p>') if ui['kind']=='illustration' else ''
authority=ui['authority'];preview=ui['preview']
legacy_note = ('<p class="ui-notice"><strong>Old or draft UI mode. It does not allow implementation now.</strong> '
'The current rule: no new CSS; reuse classes and elements that already exist (design kit or existing code); '
'plain unstyled HTML only when the user explicitly allows it.</p>') if ui['mode'] in {'functionality-only','design-authoring'} else ''
source_note=f'{e(authority["summary"])} Reference: {api.links([authority["source"]],"s")}, {e(authority["locator"])}.'
first=ui['screens'][0]['id']
gallery=quick_module().render(quick_module().collect(ui, root, sources, api), api)
return (f'<figure class="panel ui-review" id="panel-ui-preview"><div class="figure-head"><h2>{e(ui["title"])}</h2>'
'<span>screens · interaction · design</span></div>'
f'<p class="figure-description">{e(ui["summary"])}</p>{illustration}{legacy_note}'
f'<div class="ui-authority"><p><strong>Design mode:</strong> {e(ui["mode"])} · {e(authority["status"])}.</p>'
f'<p>{source_note}</p><p><strong>Preview:</strong> {e(preview["destination"])}, {e(preview["status"])}. '
f'{e(preview["summary"])}</p><p><strong>Design acceptance:</strong> {e(ui["acceptance"]["status"])}. '
f'{e(ui["acceptance"]["summary"])}</p></div>'
'<p class="hint">Screen frames are connected by actions. Markers refer to design decisions below. '
f'<a href="#ui-screen-{e(first)}">Go to first screen</a>. Scroll at readable size; Fit is an overview.</p>'
'<noscript><p class="hint">Screen Fit needs JavaScript; use normal-size scrolling and the screen details below.</p></noscript>'
f'<div class="canvas ui-canvas" tabindex="0" role="region" aria-label="UI screen flow, scroll to explore" '
f'data-start-id="ui-screen-{e(first)}" style="--board-width:{w}px"><div class="ui-scaled" style="--ui-width:{w}px;--ui-height:{h}px">'
f'<div class="ui-board">{"".join(routes)}{"".join(cards)}</div></div></div>'
f'<figcaption>{e(ui["limit"])} Selecting or inspecting a screen does not record approval or change the application.</figcaption>'
+ gallery
+ ('<section class="ui-decisions"><h2>Design decisions on these screens</h2>'+''.join(decisions)+'</section>' if decisions else '') +
'<details class="ui-screen-index"><summary>Inspect screens and their sources</summary>'+''.join(details)+'</details>'
'</figure>')
scripts/screenshot_report.py"""Screenshot a rendered report with an already-installed Chromium browser (Edge or Chrome). No installs.
usage: python -X utf8 -I screenshot_report.py <report.html> <out-folder> [--browser PATH] [--height PX]
Writes <name>-desktop.png (1280 wide) and <name>-narrow.png (500 wide, the narrowest window headless
Chromium allows; exact phone widths need Playwright) into <out-folder> (created if its parent exists;
use the task's output folder under ARTIFACTS.md). Never overwrites an existing file. The image is
--height pixels tall (default 6000), enough for most reports; long reports are cut at that height, so look
at the bottom too. A screenshot shows layout; it does not prove the report is correct or that anyone saw it.
Exit 0: both images written; 1: browser failed; 2: bad input or no browser found.
"""
from __future__ import annotations
import pathlib
import shutil
import subprocess
import sys
import tempfile
import time
CANDIDATES = (
r'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
r'C:\Program Files\Microsoft\Edge\Application\msedge.exe',
r'C:\Program Files\Google\Chrome\Application\chrome.exe',
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
)
NAMES = ('msedge', 'microsoft-edge', 'google-chrome', 'chromium', 'chromium-browser', 'chrome')
def find_browser(explicit: str | None) -> str | None:
if explicit:
return explicit if pathlib.Path(explicit).is_file() else None
for path in CANDIDATES:
if pathlib.Path(path).is_file():
return path
for name in NAMES:
found = shutil.which(name)
if found:
return found
return None
def command(browser: str, url: str, out: pathlib.Path, width: int, height: int, profile: str) -> list[str]:
return [browser, '--headless=new', '--disable-gpu', '--hide-scrollbars', '--no-first-run',
f'--user-data-dir={profile}', f'--window-size={width},{height}', f'--screenshot={out}', url]
def main(argv: list[str]) -> int:
args = argv[1:]
browser_arg = height = None
if '--browser' in args:
i = args.index('--browser'); browser_arg = args[i + 1]; del args[i:i + 2]
if '--height' in args:
i = args.index('--height'); height = int(args[i + 1]); del args[i:i + 2]
if len(args) != 2:
print(__doc__)
return 2
report, folder = pathlib.Path(args[0]).resolve(), pathlib.Path(args[1]).resolve()
if not report.is_file() or report.suffix.lower() != '.html':
print(f'BLOCKED: {report} is not an HTML file')
return 2
fresh_artifacts = folder.parent.name == '.artifacts' and folder.parent.parent.is_dir() # first output in a repo
if not folder.parent.is_dir() and not fresh_artifacts:
print(f'BLOCKED: parent of the output folder {folder} does not exist')
return 2
folder.mkdir(parents=fresh_artifacts, exist_ok=True)
browser = find_browser(browser_arg)
if not browser:
print('BLOCKED: no installed Edge or Chrome found; pass --browser PATH. Nothing is installed by this tool.')
return 2
targets = [(folder / f'{report.stem}-desktop.png', 1280), (folder / f'{report.stem}-narrow.png', 500)]
for out, _ in targets:
if out.exists():
print(f'BLOCKED: {out} exists; never overwritten')
return 2
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as profile: # Edge holds files briefly on Windows
for out, width in targets:
try:
result = subprocess.run(command(browser, report.as_uri(), out, width, height or 6000, profile),
capture_output=True, text=True, timeout=120)
except subprocess.TimeoutExpired:
print(f'FAILED: browser timed out after 120 seconds while capturing {out.name}. '
'Any completed captures are preserved; no retry was started.')
return 1
# On Windows the launcher can return before its child process has written the file.
for _ in range(60):
if result.returncode != 0 or out.is_file():
break
time.sleep(0.5)
if result.returncode != 0 or not out.is_file():
print(f'FAILED: {browser} exit {result.returncode}: {result.stderr.strip()[:300]}')
return 1
print(f'WROTE: {out}')
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv))
scripts/task_report.py"""Sourced task content for schema-2 reports; no execution or approval authority."""
from __future__ import annotations
import html
import re
PHASES = {"planning", "progress", "delivery"}
CHECK_STATES = {"passed", "failed", "red", "stale", "limited", "blocked", "not-run"} # red: failed before the change, as planned
RESULT_STATES = {"verified", "partial", "failed", "unverified", "waived", "superseded"}
AGREEMENT_STATES = {"approved", "waived", "legacy", "not-required", "not-approved", "unknown"}
RECORDED = {"approved", "waived"} # an exact revision and the real response are required
AUTHORITY = RECORDED | {"legacy", "not-required"} # cite the record; no invented response
TERMINAL_TASKS = {"verified", "removed", "superseded", "not-applicable"}
SECTIONS = {"request", "agreement", "specification", "decisions", "risks", "testing", "milestones", "delivery"}
def fields(value, name, required, optional=()):
if not isinstance(value, dict):
raise ValueError(f"{name} must be an object")
unknown = set(value) - set(required) - set(optional)
if unknown:
raise ValueError(f"Unknown {name} fields: {', '.join(sorted(unknown))}")
missing = set(required) - set(value)
if missing:
raise ValueError(f"Missing {name} fields: {', '.join(sorted(missing))}")
def sourced(row, sources, api, name):
api.text(row.get("source"), f"{name}.source", 48)
if row.get("source") not in sources:
raise ValueError(f"{name}.source must name a supplied source")
api.text(row.get("locator"), f"{name}.locator")
def validate(data, sources, api):
report = data["task_report"]
fields(report, "task_report", ("phase", "agreement", "sections", "tickets", "checks", "acceptance_results"), ("completion", "screen_change"))
api.text(report["phase"], "task_report.phase", 30)
if report["phase"] not in PHASES:
raise ValueError("task_report.phase is invalid")
# Design retro: a plan copied a sibling report without ui_review and asked approval for a visible change.
screen = report.get("screen_change")
if screen is None and (data["state"] in {"awaiting-approval", "approved"} or report["phase"] == "planning"):
raise ValueError('task_report.screen_change is required: "visible" when users see any change on screen '
'(order, text, which rows show, layout), otherwise "none"')
if screen is not None and screen not in {"visible", "none"}:
raise ValueError('task_report.screen_change must be "visible" or "none"')
ui = data.get("ui_review")
if screen == "visible" and not (isinstance(ui, dict) and ui.get("kind") == "application"):
raise ValueError("A change users can see needs its screen diagram: add ui_review (kind application) "
"with current and proposed screens; see UI-FLOW.md and ui-design step 3")
agreement = report["agreement"]
fields(agreement, "agreement", ("status", "summary"), ("revision", "response", "source", "locator"))
api.text(agreement["summary"], "agreement.summary")
api.text(agreement["status"], "agreement.status", 30)
if agreement["status"] not in AGREEMENT_STATES:
raise ValueError("agreement.status is invalid")
if agreement["status"] in RECORDED:
for key in ("revision", "response"):
api.text(agreement.get(key), f"agreement.{key}")
sourced(agreement, sources, api, "agreement")
elif "response" in agreement or ("revision" in agreement and agreement["status"] not in AUTHORITY):
raise ValueError("Only an approved or waived agreement records a response; others cannot claim an approved revision or response")
elif agreement["status"] in AUTHORITY:
if "revision" in agreement:
api.text(agreement["revision"], "agreement.revision")
sourced(agreement, sources, api, "agreement")
elif "source" in agreement or "locator" in agreement:
sourced(agreement, sources, api, "agreement")
if data["state"] in {"awaiting-approval", "approved"} and agreement["status"] not in AUTHORITY:
raise ValueError("An approval-ready implementation plan needs recorded behavior agreement")
sections = api.indexed(report["sections"], "task_report.sections", empty=False)
if set(sections) - SECTIONS:
raise ValueError("Unknown task_report section id")
needed = {"request", "agreement", "specification", "decisions", "risks", "testing"}
if report["phase"] == "delivery":
needed.add("delivery")
if needed - set(sections):
raise ValueError("task_report needs request, agreement, specification, decisions, risks and testing sections; delivery also needs its summary")
for row in sections.values():
fields(row, "section", ("id", "title", "text", "source", "locator"))
api.text(row["title"], "section.title", 100)
api.text(row["text"], "section.text", 20000)
sourced(row, sources, api, "section")
tasks = {row["id"] for row in data["tasks"]}
ticket_ids = set()
for row in api.rows(report["tickets"], "task_report.tickets"):
fields(row, "ticket specification", ("task", "specification", "expected", "source", "locator"))
api.text(row["task"], "ticket.task", 48)
if row["task"] not in tasks or row["task"] in ticket_ids:
raise ValueError("Ticket specifications must reference unique known tasks")
ticket_ids.add(row["task"])
api.text(row["specification"], "ticket.specification", 12000)
api.text(row["expected"], "ticket.expected", 4000)
sourced(row, sources, api, "ticket")
if ticket_ids != tasks:
raise ValueError("A central report needs a specification for every task")
checks = api.indexed(report["checks"], "task_report.checks")
for row in checks.values():
fields(row, "check", ("id", "title", "kind", "status", "expected", "actual", "limits", "source", "locator"), ("command",))
api.text(row["kind"], "check.kind", 30)
api.text(row["status"], "check.status", 30)
if row["kind"] not in {"test", "review", "inspection"} or row["status"] not in CHECK_STATES:
raise ValueError("Invalid check kind or status")
for key in ("title", "expected", "actual", "limits"):
api.text(row[key], f"check.{key}")
if "command" in row:
api.text(row["command"], "check.command", 4000)
sourced(row, sources, api, "check")
acceptance = {row["id"] for row in data["acceptance"]}
result_ids = set()
for row in api.rows(report["acceptance_results"], "task_report.acceptance_results"):
fields(row, "acceptance result", ("acceptance", "status", "checks", "note", "source", "locator"))
key = row["acceptance"]
api.text(key, "acceptance result.acceptance", 48)
api.text(row["status"], "acceptance result.status", 30)
if key not in acceptance or key in result_ids:
raise ValueError("Acceptance results must reference unique known criteria")
result_ids.add(key)
if row["status"] not in RESULT_STATES:
raise ValueError("Invalid acceptance result status")
api.text(row["note"], "acceptance result.note")
sourced(row, sources, api, "acceptance result")
refs = api.references(row["checks"], checks, "acceptance result.checks", required=row["status"] == "verified")
if row["status"] == "verified" and any(checks[ref]["status"] != "passed" for ref in refs):
raise ValueError("Verified acceptance requires passed evidence")
if row["status"] == "failed" and not any(checks[ref]["status"] == "failed" for ref in refs):
raise ValueError("Failed acceptance needs a failed check")
if row["status"] == "superseded" and refs:
raise ValueError("A superseded criterion cites its decision in note/source, not checks")
if report["phase"] == "delivery":
if data.get("report_kind") != "task-overview":
raise ValueError("Delivery is a sourced task-overview, not a new approval request")
if result_ids != acceptance:
raise ValueError("Delivery must account for every acceptance criterion")
api.text(report.get("completion"), "task_report.completion", 30)
if report["completion"] not in {"complete", "partial", "blocked"}:
raise ValueError("Delivery needs explicit completion state")
if report["completion"] == "complete":
checkpoint = data["checkpoint"]
covered = {req for row in data["acceptance"] for req in row["requirements"]}
results = report["acceptance_results"]
failed = [message for broken, message in (
(not acceptance or any(row["status"] not in {"verified", "superseded"} for row in results)
or not any(row["status"] == "verified" for row in results), "every acceptance verified or superseded, at least one verified"),
(not checks or any(row["status"] not in ("passed", "red") for row in checks.values()), "every check passed (red only before the change)"),
(not any(row["kind"] == "review" for row in checks.values()), "a review among the checks"),
(any(row.get("outcome") not in TERMINAL_TASKS for row in data["tasks"]),
"every task outcome verified/removed/superseded/not-applicable"),
({row["id"] for row in data["requirements"]} - covered, "every requirement covered by an acceptance item"),
(data["open_decisions"], "no open decision"),
(checkpoint["verification"]["status"] != "passed", "checkpoint verification passed"),
(checkpoint["implementation"]["status"] != "implemented", "checkpoint implementation implemented"),
(checkpoint["approval"]["status"] not in AUTHORITY or agreement["status"] not in AUTHORITY,
"agreement and approval recorded")) if broken]
if failed: # name what failed (round 7: the agent read the renderer source to find out)
raise ValueError("A complete delivery needs: " + "; ".join(failed) + ". Otherwise use partial.")
elif "completion" in report:
raise ValueError("completion belongs only to delivery")
PLAIN_WORDS = re.compile(r"\b(ensure|leverage|utilize|robust|seamless(?:ly)?|comprehensive|crucial|simply)\b", re.I)
# Audit E16: these words carry uncertainty, permission or a proposal. Replacing or deleting them can turn
# "it could be the cache" into a false certainty, so they get a note that keeps the meaning, not a problem.
MEANING_WORDS = re.compile(r"\b(should|would|may|might|could)\b", re.I)
MEANING_NOTE = ("keep each word that states uncertainty, permission or a proposal. Rewrite only empty hedging, "
"and never just delete the word")
CONTRACTION = re.compile(r"\b\w+['’](?:t|ve|re|ll|d|m)\b", re.I)
# Quoted user words and code keep their exact wording, including punctuation such as semicolons.
QUOTED = re.compile(r'"[^"\n]*"|“[^”\n]*”|`[^`\n]*`')
def prose_fields(data):
"""(JSON path, text) for the prose a reader sees, in every report kind."""
report = data.get("task_report") if isinstance(data.get("task_report"), dict) else {}
fields = [("understanding", data.get("understanding", ""))]
if report:
fields.append(("task_report.agreement.summary", (report.get("agreement") or {}).get("summary", "")))
fields += [(f'task_report.sections[{row["id"]}].text', row["text"]) for row in report.get("sections", [])]
fields += [(f'task_report.tickets[{row["task"]}].{key}', row[key]) for row in report.get("tickets", []) for key in ("specification", "expected")]
fields += [(f'task_report.checks[{row["id"]}].{key}', row[key]) for row in report.get("checks", []) for key in ("expected", "actual", "limits")]
fields += [(f'task_report.acceptance_results[{row["acceptance"]}].note', row["note"]) for row in report.get("acceptance_results", [])]
# Round 15: other text the page shows kept semicolons.
fields += [(f'boundaries[{i}]', text) for i, text in enumerate(data.get("boundaries", []))]
fields += [(f'open_decisions[{i}]', text) for i, text in enumerate(data.get("open_decisions", []))]
fields += [(f'tasks[{row["id"]}].outcome_note', row.get("outcome_note", "")) for row in data.get("tasks", [])]
# Round 17: requirements, ticket reasons, comparison rows and acceptance text kept semicolons too.
fields += [(f'tasks[{row["id"]}].{key}', row.get(key, "")) for row in data.get("tasks", []) for key in ("title", "reason")]
fields += [(f'requirements[{row["id"]}].text', row.get("text", "")) for row in data.get("requirements", [])]
fields += [(f'acceptance[{row["id"]}].{key}', row.get(key, "")) for row in data.get("acceptance", []) for key in ("text", "verification")]
fields += [(f'comparison[{i}].{key}', row.get(key, "")) for i, row in enumerate(data.get("comparison", []))
for key in ("label", "current", "proposed")]
fields += [(f'graphs[{g["id"]}].{key}', g.get(key, "")) for g in data.get("graphs", []) for key in ("title", "description")]
if isinstance(data.get("checkpoint"), dict):
fields.append(("checkpoint.next_action", data["checkpoint"].get("next_action", "")))
ui = data.get("ui_review") or {}
fields += [(f'ui_review.{key}', ui.get(key, "")) for key in ("summary", "limit")]
fields += [(f'ui_review.screens[{s.get("id")}].description', s.get("description", "")) for s in ui.get("screens", [])]
return [(path, QUOTED.sub("quote", text)) for path, text in fields if isinstance(text, str) and not text.startswith("FILL: ")]
def prose_notes(data):
"""Advisory notes for words that may carry meaning: (JSON path, note). Never a problem to fix."""
notes = []
for path, text in prose_fields(data):
words = sorted({m.group(0).lower() for m in MEANING_WORDS.finditer(text)})
if words:
notes.append((path, f'{", ".join(words)}: {MEANING_NOTE}'))
return notes
def prose_issues(data):
"""Simple English checks on the report's prose fields: (JSON path, problem). Advice, not a validation error."""
issues = []
for path, text in prose_fields(data):
found = []
if ";" in text:
found.append("semicolon (write two sentences)")
if "—" in text:
found.append("em-dash (name the relation or write two sentences)")
if "**" in text:
found.append("bold markup")
words = sorted({m.group(0).lower() for m in PLAIN_WORDS.finditer(text)})
if words:
found.append("words to replace: " + ", ".join(words))
if CONTRACTION.search(text):
found.append("contraction")
for paragraph in text.split("\n\n"):
sentences = [s for s in re.split(r"(?<=[.!?])\s+", paragraph.strip()) if s]
long = [s for s in sentences if len(s.split()) > 25]
if long:
found.append(f"{len(long)} sentence(s) over 25 words")
if len(sentences) > 6:
found.append(f"paragraph of {len(sentences)} sentences (6 at most)")
issues += [(path, problem) for problem in found]
return issues
def paragraphs(value):
return "".join("<p>" + html.escape(part).replace("\n", "<br>") + "</p>" for part in value.split("\n\n"))
def attribution(row, api):
return f'<small>Source: {api.links([row["source"]], "s")}, {html.escape(row["locator"])}</small>'
AGREEMENT_WORDS = {
"approved": "Behavior agreed", "waived": "Behavior agreement waived by the owner",
"legacy": "Behavior approved under the older single approval",
"not-required": "No separate behavior agreement needed for this change",
"not-approved": "Behavior not agreed yet", "unknown": "Behavior agreement not recorded",
}
PLAN_WORDS = {
"awaiting-approval": "Waiting for your OK on this plan in the chat. The agent writes no code before you give it.",
"draft": "Draft. The plan is not finished, so there is nothing to approve yet.",
"superseded": "A newer plan replaced this one. Do not build from it.",
}
COMPLETION_WORDS = {
"complete": "everything agreed is built and checked. You still need to accept it.",
"partial": "some work or checks are still open. The sections below name them.",
"blocked": "the work stopped. The sections below say why.",
}
RESULT_WORDS = {
"verified": "Verified", "partial": "Partly verified", "failed": "Failed", "unverified": "Not verified",
"waived": "Waived by the owner, not a pass", "superseded": "Removed from scope, not a pass",
}
CHECK_WORDS = {
"passed": "Passed", "failed": "Failed", "red": "Failed before the change, as expected", "stale": "Stale: it passed on files that changed after the check",
"limited": "Limited", "blocked": "Blocked", "not-run": "Not run",
}
TOC = (("agreed", None), ("how-it-works", "How it works"), ("what-changes", "What changes"),
("the-work", "The work"), ("checks", "How the work will be checked"), ("risks", "Risks and open questions"))
def status_sentence(data):
"""One plain sentence pair: behaviour agreement and where the plan or delivery stands."""
report = data["task_report"]
agreement = report["agreement"]
words = AGREEMENT_WORDS[agreement["status"]]
if agreement.get("revision") and agreement["status"] != "not-required":
words += f' ({agreement["revision"]})'
words += "."
checkpoint = data.get("checkpoint")
if report["phase"] == "delivery":
return f'Delivered: {report["completion"]}. {COMPLETION_WORDS[report["completion"]][0].upper()}{COMPLETION_WORDS[report["completion"]][1:]} {words}'
if report["phase"] == "progress" or data.get("report_kind") == "task-overview":
if checkpoint:
impl = checkpoint["implementation"]["status"].replace("-", " ")
ver = checkpoint["verification"]["status"].replace("-", " ")
return f'{words} Work in progress: building {impl}, checks {ver}. Next: {checkpoint["next_action"]}'
return f"{words} Work in progress."
if data["state"] == "approved":
return f'{words} Plan approved ({data["revision"]}). Read the task record before you continue.'
if data["state"] == "draft" and agreement["status"] == "not-approved" and not data.get("open_decisions"):
# Both gates in one message (VISUAL-REVIEW): nothing open, behaviour and plan shown together.
return ("Waiting for your OK on the behavior and on this plan. Nothing is agreed, approved "
"or built until you say yes to both.")
return f'{words} {PLAN_WORDS.get(data["state"], PLAN_WORDS["draft"])}'
STATUS_TAGS = {"draft": "Draft", "awaiting-approval": "Waiting for you", "approved": "Approved",
"superseded": "Replaced", "inspection": "Inspection"}
TICKET_STATES = { # task outcome -> (label, ticket__status modifier)
None: ("Planned", "requested"), "planned": ("Not started", "requested"), "in-progress": ("In progress", "in-progress"),
"implemented": ("Built, not checked", "in-progress"), "verified": ("Verified", "done"), "blocked": ("Blocked", "blocked"),
"removed": ("Removed", "requested"), "superseded": ("Replaced", "requested"), "not-applicable": ("Not applicable", "requested"),
}
STATE_CLASS = {"verified": "verified", "passed": "verified", "failed": "failed", "partial": "partial", "stale": "partial",
"limited": "partial", "blocked": "failed"}
def status_tag(data):
"""The short tag before the status sentence, and whether it reads as a good outcome."""
report = data["task_report"]
if report["phase"] == "delivery":
return "Delivered", report["completion"] == "complete" # the sentence already says complete/partial/blocked
if report["phase"] == "progress" or data.get("report_kind") == "task-overview":
return "In progress", False
if data["state"] == "draft" and report["agreement"]["status"] == "not-approved" and not data.get("open_decisions"):
return "Waiting for you", False # both gates asked in one message (round 17: tag said Draft)
return STATUS_TAGS.get(data["state"], "Draft"), data["state"] == "approved"
def facts(data, api):
"""The at-a-glance row under the title."""
esc = html.escape
report = data["task_report"]
results = report["acceptance_results"]
verified = sum(1 for row in results if row["status"] == "verified")
open_count = len(data["open_decisions"])
as_of = (data.get("checkpoint") or {}).get("as_of", "")
items = [("Revision", esc(data["revision"])), ("Tickets", f'{len(report["tickets"])}'),
("Acceptance", f'{verified} of {len(data["acceptance"])} verified'),
("Open questions", f'<a href="#open-questions">{open_count} to answer</a>' if open_count else "None")]
if as_of:
items.append(("As of", esc(as_of)))
attention = ' class="facts__value--attention"'
cells = "".join(f'<div class="facts__item"><dt>{name}</dt><dd{attention if name == "Open questions" and open_count else ""}>{value}</dd></div>'
for name, value in items)
return f'<dl class="facts" aria-label="At a glance">{cells}</dl>'
def render(data, api, parts):
"""Return (status sentence, facts HTML, body HTML) for a central task report; every data string is escaped."""
esc = html.escape
report = data["task_report"]
sections = {row["id"]: row for row in report["sections"]}
tasks = {row["id"]: row for row in data["tasks"]}
acceptance = {row["id"]: row for row in data["acceptance"]}
results = {row["acceptance"]: row for row in report["acceptance_results"]}
checks = {row["id"]: row for row in report["checks"]}
delivered = report["phase"] == "delivery" # round 8: plan-time headings stayed in delivery reports
agreed_title = "What you approve" if report["phase"] == "planning" else "What was agreed"
checks_title = "How it was checked" if delivered else "How the work will be checked"
open_count = len(data["open_decisions"])
def section(key, title, summary=""):
lead = f'<p class="section__summary">{esc(summary)}</p>' if summary else ""
return f'<section class="section" aria-labelledby="{key}"><h2 class="section__title" id="{key}">{esc(title)}</h2>{lead}'
def block(key, label):
if key not in sections:
return ""
return f'<div><h3>{esc(label)}</h3>{paragraphs(sections[key]["text"])}</div>'
titles = {"agreed": agreed_title, "checks": checks_title}
flag = f'<span class="toc__flag">{open_count} open question{"s" if open_count != 1 else ""}</span>' if open_count else ""
toc = "".join(f'<li><a class="toc__link" href="#{key}">{esc(titles.get(key, title or ""))}{flag if key == "risks" else ""}</a></li>'
for key, title in TOC)
out = ['<div class="layout"><nav class="toc" aria-label="Contents"><p class="toc__title">Contents</p>'
f'<ol class="toc__list">{toc}</ol><a class="toc__extra" href="#technical-details">Technical details</a></nav>'
'<div class="content" id="task-report">']
# 1. Agreement, what stays the same, decisions.
out.append(section("agreed", agreed_title, report["agreement"]["summary"]))
out.append('<div class="columns">' + block("request", "What was asked")
+ block("agreement", "What will happen" if report["phase"] == "planning" else "Agreed behavior") + "</div>")
bounds = "".join(f"<li>{esc(item)}</li>" for item in data["boundaries"])
keep = f'<div><h3>What stays the same</h3><ul class="list--keep">{bounds}</ul></div>' if bounds else ""
out.append('<div class="columns">' + block("decisions", "Decisions made") + keep + "</div>")
if open_count:
out.append(f'<p class="note">{open_count} question{"s are" if open_count != 1 else " is"} still open. '
'<a href="#open-questions">See the open questions</a></p>')
out.append("</section>")
# 2. Flow diagrams.
out.append(section("how-it-works", "How it works") + parts.flow_context +
f'<section class="review" aria-label="Flow diagrams">{parts.controls}<div class="figures">{parts.figures}</div></section></section>')
# 3. Today versus after the change.
if data["comparison"]:
before, after = ("Before", "After") if data.get("report_kind") == "task-overview" else ("Today", "After the change")
rows = "".join(
f'<tr><th scope="row">{esc(row["label"])}</th><td>{esc(row["current"])}</td><td class="after">{esc(row["proposed"])} '
+ ('<span class="tag">Unchanged</span>' if row["current"].strip() == row["proposed"].strip() else '') # no guessed "Changed" tag
+ "</td></tr>" for row in data["comparison"])
out.append(section("what-changes", "What changes", "Side by side, before and after the change.")
+ f'<div class="table-scroll"><table class="compare"><thead><tr><th scope="col">Situation</th>'
f'<th scope="col">{before}</th><th scope="col">{after}</th></tr></thead><tbody>{rows}</tbody></table></div></section>')
else:
out.append(section("what-changes", "What changes") + "<p>No before and after comparison is recorded.</p></section>")
# 4. The work: approach, order, one card per ticket.
out.append(section("the-work", "The work"))
for key, label in (("delivery", "What was delivered"), ("specification", "Approach")):
if key in sections:
out.append(f'<div class="block"><h3>{label}</h3>{paragraphs(sections[key]["text"])}</div>')
steps = "".join(f'<li><a href="#spec-{esc(row["task"])}"><span class="key">{esc(row["task"])}</span> {esc(tasks[row["task"]]["title"])}</a></li>'
for row in report["tickets"])
out.append(f'<div class="block"><h3>Order of the work</h3>{paragraphs(sections["milestones"]["text"]) if "milestones" in sections else ""}'
f'<ol class="steps">{steps}<li><a href="#checks"><span class="key">Checks</span> {esc(checks_title)}</a></li></ol></div>')
cards = []
for row in report["tickets"]:
task = tasks[row["task"]]
label, modifier = TICKET_STATES.get(task.get("outcome"), TICKET_STATES[None])
deps = " ".join(f'<a class="ticket-ref" href="#spec-{esc(dep)}">{esc(dep)}</a> {esc(tasks[dep]["title"])}'
for dep in task["depends_on"]) or "Nothing. It can start first."
how = "".join(f"<li>{esc(item)}</li>" for item in dict.fromkeys(acceptance[key]["verification"] for key in task["acceptance"]))
outcome = f'<dt>Outcome</dt><dd>{parts.outcome(task)}</dd>' if "outcome" in task else ""
cards.append(f'<article class="ticket" id="spec-{esc(row["task"])}" aria-labelledby="spec-{esc(row["task"])}-title">'
f'<p class="ticket__stub">Ticket <span class="ticket__key">{esc(row["task"])}</span>'
f'<span class="ticket__status ticket__status--{modifier}">{esc(label)}</span></p>'
f'<div class="ticket__main"><h3 class="ticket__title" id="spec-{esc(row["task"])}-title">{esc(task["title"])}</h3>'
f'{paragraphs(row["specification"])}</div><dl class="ticket__facts"><dt>Expected result</dt><dd>{paragraphs(row["expected"])}</dd>'
f'<dt>Depends on</dt><dd>{deps}</dd><dt>How it is checked</dt><dd><ul class="list">{how}</ul></dd>{outcome}</dl></article>')
out.append(f'<div class="tickets">{"".join(cards)}</div></section>')
# 5. Each acceptance item once, with its result, then each check.
testing = sections.get("testing", {}).get("text", "")
out.append(section("checks", checks_title))
if testing:
out.append(paragraphs(testing))
verified = sum(1 for key in acceptance if results.get(key, {}).get("status") == "verified")
bar = "".join(f'<span class="is-{"verified" if results.get(k, {}).get("status") == "verified" else "failed" if results.get(k, {}).get("status") == "failed" else "open"}"></span>'
for k in acceptance)
out.append(f'<div class="progress"><span>{verified} of {len(acceptance)} verified</span><span class="progress__bar" aria-hidden="true">{bar}</span></div>')
if not results and not checks:
out.append('<p class="muted">No check has run yet. Results appear here when the work is checked.</p>')
items = []
for key, row in acceptance.items():
result = results.get(key)
state = (f'<span class="state state--{STATE_CLASS.get(result["status"], "open")}" id="result-{esc(key)}" data-result="{esc(result["status"])}">'
f'{esc(RESULT_WORDS[result["status"]])}</span>'
if result else '<span class="state">Not checked</span>')
note = ""
if result:
refs = ", ".join(f'<a href="#check-{esc(ref)}">{esc(checks[ref]["title"])}</a> ({esc(CHECK_WORDS[checks[ref]["status"]])})'
for ref in result["checks"]) or "no check linked"
note = f'<p class="check__how">{esc(result["note"])} Evidence: {refs}</p>'
items.append(f'<li class="check" id="a-{esc(key)}"><span class="check__key">{esc(key)}</span>'
f'<p class="check__expect">{esc(row["text"])}</p><p class="check__how">Checked by: {esc(row["verification"])}</p>{note}{state}</li>')
out.append(f'<ol class="checks">{"".join(items)}</ol>')
if checks:
rows = []
for row in checks.values():
command = f'<p class="check__how">Command: <code>{esc(row["command"])}</code></p>' if "command" in row else ""
rows.append(f'<li class="check" id="check-{esc(row["id"])}"><span class="check__key">{esc(row["id"])}</span>'
f'<p class="check__expect">{esc(row["title"])} <span class="muted">({esc(row["kind"])})</span></p>'
f'<p class="check__how">Expected: {esc(row["expected"])}</p><p class="check__how">Observed: {esc(row["actual"])}</p>'
f'<p class="check__how">Limits: {esc(row["limits"])}</p>{command}'
f'<span class="state state--{STATE_CLASS.get(row["status"], "open")}">{esc(CHECK_WORDS[row["status"]])}</span></li>')
out.append(f'<h3>Checks and reviews</h3><ol class="checks">{"".join(rows)}</ol>')
out.append("</section>")
# 6. Risks, open questions and evidence limits.
out.append(section("risks", "Risks and open questions"))
if "risks" in sections:
risks = "".join(f"<li>{esc(part).replace(chr(10), '<br>')}</li>" for part in sections["risks"]["text"].split("\n\n") if part.strip())
out.append(f'<h3>Risks and limits</h3><ul class="risks">{risks}</ul>')
if parts.uncertainty:
out.append('<h3>Evidence limits</h3><ul class="risks">' + "".join(f"<li>{esc(v)}</li>" for v in parts.uncertainty) + "</ul>")
if open_count:
questions = "".join(f'<li class="question" id="q-{i}"><p class="question__title"><span class="question__key">Q{i}</span> {esc(item)}</p></li>'
for i, item in enumerate(data["open_decisions"], 1))
out.append('<div class="questions" id="open-questions"><div class="questions__head"><h3>Open questions</h3>'
f'<p>Work starts after these are answered.</p></div><ol class="question-list">{questions}</ol></div>')
else:
out.append('<h3 id="open-questions">Open questions</h3><p>None recorded.</p>')
out.append("</section>")
# 7. Technical details: provenance, records and traceability.
agreement = report["agreement"]
record = [f'<p>Status: <strong>{esc(agreement["status"])}</strong>'
+ (f' · {esc(agreement["revision"])}' if agreement.get("revision") else "") + "</p>"]
if "response" in agreement:
record.append(f'<p>Recorded response:</p><blockquote>{esc(agreement["response"])}</blockquote>')
if "source" in agreement:
record.append(attribution(agreement, api))
approval = ""
if data["state"] == "approved" and isinstance(data.get("approval"), dict):
approval = (f'<section><h2>Plan approval record</h2><p>{esc(data["approval"]["record"])}</p>'
f'<blockquote>{esc(data["approval"]["response"])}</blockquote></section>')
change_rows = "".join(f'<tr><td>{esc(row["label"])}</td><td>{api.links(row["requirements"], "r")}</td>'
f'<td>{api.links(row["evidence"], "e")}</td><td>{api.links(row["tasks"], "t")}</td></tr>' for row in data["comparison"])
accept_rows = "".join(f'<li><strong>{esc(key)}</strong>: requirements {api.links(row["requirements"], "r")}</li>' for key, row in acceptance.items())
origins = [(f'Section “{row["title"]}”', row) for row in report["sections"]]
origins += [(f'Ticket {row["task"]}', row) for row in report["tickets"]]
origins += [(f'Check {row["id"]}', row) for row in report["checks"]]
origins += [(f'Result for {row["acceptance"]}', row) for row in report["acceptance_results"]]
origin_html = "".join(f"<li>{esc(label)} {attribution(row, api)}</li>" for label, row in origins)
task_table = parts.task_rows or '<tr><td colspan="4">No implementation tasks.</td></tr>'
out.append('<details class="appendix" id="technical-details"><summary>Technical details: sources, evidence and traceability</summary><div class="appendix-body">'
'<p class="muted">This page comes from the task records: the plan, the to-do list, the findings and the decisions. '
'It does not approve anything, it does not show who approved, and it does not prove that a check ran. '
'There is no approve button. Waived or unverified work is not a passed check.</p>'
f'<section><h2>Behavior agreement record</h2>{"".join(record)}</section>{approval}{parts.checkpoint}{parts.history}'
f'<section><h2>Requirements</h2><ul class="plain">{parts.requirements or "<li>No requirements recorded.</li>"}</ul></section>'
'<section><h2>Tickets: traceability</h2><div class="table-scroll"><table><thead><tr><th>Ticket</th><th>Work and affected areas</th>'
f'<th>Traceability</th><th>Depends on</th></tr></thead><tbody>{task_table}</tbody></table></div></section>'
+ ('<section><h2>What changes: traceability</h2><div class="table-scroll"><table><thead><tr><th>Situation</th><th>Requirements</th>'
f'<th>Evidence</th><th>Tickets</th></tr></thead><tbody>{change_rows}</tbody></table></div></section>' if change_rows else '')
+ f'<section><h2>Acceptance: traceability</h2><ul class="plain">{accept_rows or "<li>No acceptance criteria.</li>"}</ul></section>'
f'<section><h2>Where each part comes from</h2><ul class="plain">{origin_html}</ul></section>'
f'<section><h2>Evidence and limits</h2>{parts.evidence}</section>{parts.graph_records}\n{parts.snapshot}\n</div></details>')
out.append("</div></div>")
return status_sentence(data), facts(data, api), "".join(out)
FILL = 'FILL: '
STALE_WORDS = ('not yet', ' will ', 'at delivery', 'can reject', 'waits for')
def scaffold_delivery(review: dict) -> dict:
"""A valid delivery projection from an approved review JSON. Only the FILL texts need editing.
The scaffold never claims a result: every acceptance starts unverified, completion starts partial,
verification limited. Edit them from the records, adding checks with their real outcomes.
"""
import copy
data = copy.deepcopy(review)
source = data['sources'][0]['id']
where = {'source': source, 'locator': FILL + 'record heading'}
data['report_kind'] = 'task-overview'
data['state'] = 'draft'
# Always a fresh checkpoint: the plan's said "not implemented" under "Delivered" (round-5 retro).
data['checkpoint'] = ({
'as_of': FILL + 'date of this delivery', 'source': source, 'locator': 'Current checkpoint',
'approval': {'status': 'approved', 'revision': data['revision'], 'summary': FILL + 'who approved what, when'},
'implementation': {'status': 'implemented', 'summary': FILL + 'what was built'},
'verification': {'status': 'limited', 'summary': FILL + 'what was checked and what was not'},
'next_action': FILL + 'next safe action'})
# Round-6 retro: these were copied from the plan and said "Nothing was run" next to passed checks.
if isinstance(data.get('snapshot'), dict):
data['snapshot']['basis'] = FILL + 'what was run and read for this delivery'
ui = data.get('ui_review')
if isinstance(ui, dict) and isinstance(ui.get('preview'), dict):
sketches_only = all(s.get('representation') != 'capture' for s in ui.get('screens', []))
ui['preview'].update(status='not-needed' if sketches_only else ui['preview'].get('status'),
summary=FILL + 'how the screens were shown (sketch or capture of the built result)')
for task in data.get('tasks', []): # round 7: the scaffold left these out; never claim more than implemented
task.setdefault('outcome', 'implemented')
task.setdefault('outcome_note', FILL + 'result and V-ID; set verified only with passing evidence')
old = data.get('task_report', {})
agreement = old.get('agreement') or {'status': 'unknown', 'summary': FILL + 'agreement status in plain words'}
sections = {row['id']: dict(row) for row in old.get('sections', [])}
for row in sections.values(): # round 14: plan-time wording ("not yet", "will") stayed in deliveries
if any(word in row['text'].lower() for word in STALE_WORDS):
row['text'] = FILL + 'rewrite in the past tense from the records: ' + row['text']
for key in ('request', 'agreement', 'specification', 'decisions', 'risks', 'testing', 'delivery'):
sections.setdefault(key, {'id': key, 'title': key.title(), 'text': FILL + key, **where})
tickets = {row['task']: row for row in old.get('tickets', [])}
data['task_report'] = {
'phase': 'delivery', 'completion': 'partial', 'agreement': agreement,
'sections': list(sections.values()),
'tickets': [tickets.get(t['id']) or {'task': t['id'], 'specification': FILL + 'what this ticket built',
'expected': FILL + 'expected result', **where} for t in data['tasks']],
'checks': old.get('checks', []),
'acceptance_results': [{'acceptance': a['id'], 'status': 'unverified', 'checks': [],
'note': FILL + 'result and evidence', **where} for a in data['acceptance']],
}
return data
if __name__ == '__main__':
import json
import pathlib
import sys
if len(sys.argv) != 4 or sys.argv[1] != 'scaffold':
print('usage: python -X utf8 -I task_report.py scaffold <approved-review.json> <new-delivery.json>\n'
'Writes a valid delivery projection; edit every "FILL:" text from the records, then render it.')
sys.exit(2)
target = pathlib.Path(sys.argv[3])
if target.exists():
print(f'BLOCKED: {target} exists; never overwritten')
sys.exit(2)
review = json.loads(pathlib.Path(sys.argv[2]).read_text(encoding='utf-8'))
target.write_text(json.dumps(scaffold_delivery(review), indent=1, ensure_ascii=False), encoding='utf-8')
print(f'WROTE {target}; edit the FILL texts, then render with render_report.py')
SKILL.md--- name: codebase-map description: "Create an evidence-linked local flowchart or codebase map when the user requests one, or workflow requests its visual approval report. Use the bundled flowchart design guide for the approved editorial diagrams; inspect current code and preserve tasks/evidence. Visualization only: never implement, approve work or launch an architecture-refactoring workflow." --- # Codebase map Create offline HTML/SVG in the existing editorial report. Preserve the full report: flow views, change summary, boundaries, tasks, acceptance and evidence. For application UI, add the optional screen-flow view described in [UI-FLOW.md](UI-FLOW.md): connected screens with actions, decisions and the existing elements each proposed screen copies; captures where safe, else labelled sketches. Every visible change needs it; no invented markup/styles. Never call workflow recursively. Explain behaviour for a reader new to the project first, technical evidence underneath. Keep risks and unresolved decisions visible. ## 1. Establish the view and current evidence Apply workflow rules readiness: gaps permit read-only inspection or authorised setup drafts, not an approval-ready implementation report. Read project rules, worktree and local changes. Inspect the behavior’s owner, entrypoints, callers, alternate paths and tests/docs. Reuse task findings after checking freshness. Keep static inspection, runtime observation, inference and unknowns distinct. Missing access blocks current-code claims; label explicitly requested hypothetical designs. Choose **explain** when the user wants a no-assumed-context walkthrough of how a feature works today; **current** for a concise current-state map, **proposed** for a supplied/requested future design, **compare** for an implementation review. Explain mode shows the real flow, then exact evidence; do not create a plan or improvements unless asked. Compare opens the proposed flow; current behaviour and an aligned comparison remain available. For new features show the verified integration point, not a fictional baseline. Identify and reuse the current/named task and its requirements, answers, PLAN, TODO, FINDINGS and decisions. If a likely existing record is ambiguous, ask before creating another artifact location; a new command is not a new task. Read the selected plan/review identity exactly. Distinguish the recorded baseline, approved scope, implementation and acceptance; do not infer authority from a pending badge in a historical snapshot. No new refactor candidates or product features merely to enrich the map. Inspection needs no task scaffold. ## 2. Apply the local flowchart design Read [FLOWCHART-DESIGN.md](FLOWCHART-DESIGN.md): no external diagram-design installation, lookup or invocation is required. Use local assets/renderer, not Matt's architecture command. Missing files/runtime require repair, not waived approval. Use the inspected evidence, approved answers, type, audience, revision and allowed artifact paths. Choose the geometry, not new product behavior. Main flow: start/actions/real decisions/results, short domain labels and labelled branches. Structural containment, runtime steps and delivery dependencies belong in separate focused views when needed. Preserve independent controls as independent; do not invent an exclusive decision or sequence. Unknowns and conditional/error paths needed for approval stay visible. Reuse workflow-editorial-v1 styling and offline/system fonts; no branding interview or external dependency. Keep the flowchart, not summary cards. Sourced UI previews do not permit redesigning the report shell or application. ## 3. Render, inspect and open Read [REPORT.md](REPORT.md); workflow plan, progress and delivery reports also follow [TASK-REPORT.md](TASK-REPORT.md). Derive schema-2 JSON from authoritative task records. Historical/draft inputs remain readable; UI-FLOW.md tightens new application approvals. Use the optional checkpoint/revision log only for a requested task overview; show its as-of/source and treat the selected flow as historical reviewed scope, not live implementation proof. Explicit node geometry and connector routes are validated; each change retains requirement/task/evidence traceability. Approval-ready compare requires the resolved revision, PLAN/TODO/acceptance and no material open decisions; otherwise use draft. Follow REPORT.md: `--validate-input`, fix, record path, recheck, render in the authorized directory. The renderer packages the explicit design; it neither understands code nor authenticates approval. No implementation, installation, publication. Explicit no-file instructions take precedence. Check freshness and meaning. Inspect desktop/mobile output when available: default text readable, no node/path/mask collision, no document-wide overflow, correct branches and evidence navigation. Native-size scrolling is the default; Fit is optional. Disclose unavailable checks instead of claiming them. Before delivery, check that someone without the prior conversation can explain the change, exclusions and decision. For renderer/style edits, compare stable screenshots with the reviewed reference in a controlled environment; never replace a failed baseline automatically. Request local opening with `--open` and give its path. Report the actual accepted/failed/unverified result; launch acceptance is not proof of display. Headless hosts deliver the file; do not install or upload an opener workaround. ## 4. Return without implementing Return report/source paths, mode, revision, local design/renderer version, checks and limits. There is no approval button or agent connection. Workflow shows the report, stops, then records the user’s real approval before coding. A standalone map ends with artifact delivery. Visual approval never replaces necessary grilling, and settled questions are not reopened here. ## Output lifetime Follow the active workflow's ARTIFACTS.md or its installed global policy. Keep requested HTML/JSON at the explicit review destination (project convention, otherwise existing task folder); do not create a new task to store a map. Approved/presented reviews and visual baselines are protected, not test scratch. Re-render unseen drafts with `--replace-unpresented`; don't regenerate unchanged reviews on resume. Browser-check captures use the existing task output parent, with only necessary proof retained. No extra cleanup report.
TASK-REPORT.md# Central engineering task report
Use optional `task_report` in schema 2 when the user requests a full engineering overview, or workflow-specify/workflow-deliver prepares one. Keep the existing flowchart, screen flow, comparison, scope and traceability. The bundled task_report.py helper validates and renders the added content; its scoped CSS leaves old reports unchanged.
PLAN, TODO, FINDINGS and DECISIONS (or their established equivalents) remain authoritative. Generate the JSON projection from those records, cite its source IDs and exact sections, and compare the projection to those records before presentation. Update records first; never hand-maintain an independent specification in HTML/JSON. A source hash proves byte identity, not that the agent interpreted the source correctly. Never edit a presented/approved HTML or JSON snapshot; retention follows `workflow/VISUAL-REVIEW.md` "Superseded review cleanup".
## Fields
`task_report` has `phase` (planning/progress/delivery), `agreement`, `sections`, `tickets`, `checks` and `acceptance_results`. Delivery also requires `completion` (complete/partial/blocked). An awaiting-approval or approved plan requires `screen_change`: `visible` when users see any change on screen (order, text, which rows show, layout), which also requires an application `ui_review` screen diagram ([UI-FLOW.md](UI-FLOW.md)), otherwise `none`. Never copy a sibling task's report without checking this. Unknown keys inside these new objects fail validation; older top-level extension behavior is unchanged.
Every sourced object uses `source` (an existing supplied source ID) and `locator` (the specific record heading, line or result). Text is escaped; no Markdown, raw HTML, scripts or executable commands are evaluated. Paragraph breaks and line breaks remain readable. Write every prose field with the `simple-english` skill (`../simple-english/SKILL.md`, Plain mode and its workflow-axa adaptation) and run its self-check before rendering; keep exact commands, identifiers and contractual terms.
| Object | Fields and meaning |
|---|---|
| `agreement` | `status`: approved/waived/legacy/not-required/not-approved/unknown; `summary`. Approved/waived needs exact `revision`, actual `response`, `source`, `locator`. Legacy (a pre-2.8 single-gate approval) and not-required (a lightweight edit) cite their record with `source`/`locator`, may name a `revision`, and never carry an invented `response`. Unapproved/unknown may cite a source but must not claim an approved revision/response. This is behavior/scope authority, separate from checkpoint.approval, which continues to mean implementation-plan authority. |
| `sections[]` | `id`, `title`, `text`, `source`, `locator`. IDs: request, agreement, specification, decisions, risks, testing, optional milestones, and delivery when applicable. All except milestones/delivery are required for a central overview. Put the original request and objectives in request; exclusions/edge cases in agreement; assumptions/unresolved questions in risks; implementation order in milestones. Split paragraphs for readability. |
| `tickets[]` | `task`, `specification`, `expected`, `source`, `locator`. Exactly one specification for each existing tasks[].id; existing task relationships, acceptance links, affected files and outcome remain in tasks. Describe implementation responsibility and expected behavior here, not another task status. |
| `checks[]` | `id`, `title`, `kind` (test/review/inspection), `status` (passed/failed/red/stale/limited/blocked/not-run; red = failed before the change, as the test-first plan expects, shown as "Failed before the change, as expected" and never cited as acceptance evidence; stale = passed on files that changed since), `expected`, `actual`, `limits`, `source`, `locator`; optional exact `command`. Name the relevant source/runtime identity in locator/limits. Reviews keep Standards and Spec findings distinct, with links to their fixes/rechecks. |
| `acceptance_results[]` | `acceptance` (existing acceptance ID), `status` (verified/partial/failed/unverified/waived/superseded), `checks` (check IDs), `note`, `source`, `locator`. Verified needs at least one passed check and no failed/unrun evidence among its references. Failed needs a failed check. Waiver and superseded (descoped) must cite the actual decision in note/source and are not a pass; superseded cites no checks. These are the report values of workflow-deliver's verdicts. |
During planning, checks/results may be empty. An approval-ready implementation report requires recorded approved/waived behavior agreement; neither field authenticates the user or grants authority. A draft may honestly show an unresolved agreement. A new implementation plan still follows the normal real-response approval gate.
## Layout
A report with `task_report` uses the owner's report design (`assets/report-style.css` and `assets/report-extra.js`, used unchanged; `report-style-compat.css` maps the older token names the screen diagram uses) and states each fact once. The page has a masthead, a header with the title, the understanding line, a status tag with one status sentence and an at-a-glance row (revision, tickets, checks verified, open questions, as-of date), then a sticky numbered contents list beside the sections. The contents list highlights the section being read, and a diagram that fits at 60% size or more starts on Fit. Reports without `task_report` keep the earlier layout unchanged.
1. Header: title, the `understanding` summary and one plain status sentence built from the agreement status and the phase (planning: plan state such as "Waiting for your OK on this plan"; progress: checkpoint implementation/verification and next action; delivery: "Delivered: complete/partial/blocked").
2. What you're approving (planning) or What was agreed: agreement summary, request, agreement, boundaries ("What stays the same") and decisions.
3. How it works: the flow views. Tabs read "After the change", "Today" and "Work order" (proposed flow, current flow, delivery view); other views keep their `tab`. Zoom controls are labelled separately.
4. What changes: the comparison rows as situation, today and after the change, without traceability links.
5. The work: specification, milestones and delivery sections, then one card per ticket with specification, expected result, dependencies by title, the verification of its acceptance items and any outcome.
6. How we'll know it works: the testing section, each acceptance item once with its verification and result, then each check once (status, expected, observed, limits, command).
7. Risks and open questions: the risks section, open_decisions and evidence limits.
8. Technical details (collapsed): provenance note, agreement record and response, plan approval record, checkpoint, revision history, requirements, traceability tables, source attribution for every sourced object, evidence, node/edge records and source fingerprints.
A numbered contents list links sections 2 to 7.
## Progress and delivery
Use `report_kind: task-overview` with the existing sourced checkpoint for delivery. Include every acceptance result, the final delivery section and an explicit completion state. Complete is permitted only with implemented work, applicable agreement/plan authority (approved, waived, legacy or not-required), every acceptance verified or superseded with at least one verified, every recorded check passed including at least one review, every task outcome terminal (verified/removed/superseded/not-applicable), every requirement covered by an acceptance item, no open decision and checkpoint verification passed. Partial/blocked output is valid and must explain unfinished or unverified work. An owner waiver, unavailable environment or known limitation cannot be disguised as verified completion.
The delivery section explains requested/agreed/implemented work, completed and open tickets, changed files/components, check and review outcomes, known limitations and follow-ups. Link exact evidence through checks and acceptance_results. Actual UI design acceptance and release readiness remain separate when relevant; reuse the existing UI screen-flow and kit source contract.
Snapshot timing follows VISUAL-REVIEW.md "Saved reports are snapshots"; delivery reuse follows workflow-deliver step 8. Progress updates normally remain in the records. Plan revision changes only when the agreement changes; a new dated progress snapshot may project the same plan revision. Do not overwrite a historical review to make its status look current. Before naming the final report in task records, run `render_report.py --root <root> --input <json> --validate-input`. Fix errors in the same input until it passes. Record the final destination, run preflight again against the final source records, then render once. An older draft may use `--replace-unpresented` only while unpresented, unapproved and unnamed in source records (no `-v2`/`-v3` copies). A failed opening does not remove those protections. Once the user has seen it, a change gets a new file. The renderer refuses the replacement when a supplied source file names the report (a planned path in those records is also protected) or the report is approved. It runs the wording check before it writes. Wording problems never block a valid report, but `--open` waits until none remain.
## Verification
Validate through render_report.py and its freshness check. Inspect actual rendering at desktop and narrow widths, section navigation and long content. Check every displayed claim against its cited record and actual execution. Renderer tests establish structure, escaping and consistency; they do not prove test execution, completeness of requirements, reliable model behavior or human understanding.
UI-FLOW.md# Screen flows inside the existing report
This is an optional extension of schema 2, not a new template. Keep report.css, report.js, the header, comparison, boundaries, task list, acceptance, approval and technical evidence. Non-UI reports render as before. Use `ui_review` only when actual UI is in scope or the owner explicitly requests a labelled illustration. The latter never becomes a requirement of the original task.
## Before rendering
Use local ui-design to establish the task source, mode, any preview environment permission and meaningful interaction choices. Reuse an explicit task choice or valid same-task record; otherwise ask before choosing the design. A project DESIGN.md is evidence to inspect, not automatic selection. No kit is not automatic permission for plain application HTML. Preview authoring does not permit implementation.
Capture the current screen as an authorised PNG when it can be opened safely (a local static page: `scripts/screenshot_report.py`); use its narrow (500px) image for a screen card, because a full-width page shrinks to unreadable size there. Obtain them only through allowed tooling and safe sample data; no browser, app build, server, credentials or paid service starts merely because the renderer needs an image. Name source/kit revision, capture context and limits in the supplied records. A static capture shows appearance, not verified behaviour or responsive/device performance. Do not stretch or crop away important errors, states or controls.
When a real capture cannot be taken safely, use the structured **schematic** (layout sketch) built only from the reused elements; every UI-visible change needs one of the two. The report labels it “Sketch · not a screenshot”. It is not a styled prototype or proof of the kit's look. Legacy approved-kit work without real captures stays blocked; other authorised work can continue.
## Fast source-backed styled previews (optional)
When the owner wants to **see the real existing styling** without authoring a prototype, add `ui_review.quick_previews` to the **same schema-2 projection**. The report renders a compact gallery plus **expandable full-size frames**; larger/multiple changes do not need to fit in the flowchart's little screen cards. This is a separate visual reference underneath the screen flow, not another implementation or approval gate.
Each item is exactly:
```json
{"id":"save-card","title":"Existing save card","html_source":"UI_HTML","selector":".save-card","css_sources":["UI_CSS"],"note":"Current component style; future wiring described in T2."}
```
Declare `UI_HTML` and `UI_CSS` in the report's existing `sources`. `html_source` must be an **existing local .html example/template**. `selector` supports `#id`, `.class`, `tag.class` or `tag#id` and selects the complete subtree, including its wrapper when that wrapper is selected. `css_sources` are **existing compiled `.css` files**, not SCSS source files or invented styles. Choose only the needed CSS; no installation, new page, app runtime or screenshots are required. A report-only 20px body padding improves legibility; it never alters application styles.
The renderer sanitises source markup into inert elements (removes event handlers, links, media and executable tags), isolates the real CSS in a sandboxed `srcdoc` iframe with no network/script/form permission, fingerprints all supplied sources and keeps historical reports immutable. These are **static HTML/CSS samples**: Angular/Ionic/React components needing framework execution, runtime state, compiled SCSS, generated classes, external fonts/images or layout depending on absent ancestors can look incomplete. Label those limits; use a genuine captured screenshot or the project's existing preview when fidelity matters. The optional gallery does **not** prove that the proposed page exists or that its design is approved. Never invent markup or add a second style source to force a preview. If the required sources are unavailable, skip this gallery and retain the existing schematic/capture plus references; don't block the whole planning task merely for a sample.
Use up to six small examples; group by component or interaction rather than adding full page copies. The small frame shows context; "Expand full-size sample" provides a scrollable larger view. The screen flow and source/decision mapping remain unchanged. Report page sizes stay bounded by the existing 8 MiB check.
## Projection
See the complete [report example](examples/ui-review/review-r2.json) and [example notes](examples/ui-review/README.md). Keep existing schema-2 fields. The optional `ui_review` contains:
- `kind`: `application` for the actual task, or `illustration` for a separately labelled demonstration outside the original implementation scope.
- `title`, `summary`, `limit`, `mode`: `behaviour-only` / `reuse` / `plain-by-instruction` (current), `unselected` / `blocked` (not approval-ready), legacy `approved-kit`, `functionality-only`, `design-authoring` (historical; ui-design CONTRACT.md maps them). An `application` review always opens on the screen diagram; `open_first` only applies to illustrations. Non-UI default behaviour is unchanged.
- `authority`: `status` confirmed/unconfirmed, source ID, locator, summary identifying task selection.
- `preview`: proposed/authorised status, source ID, locator, summary and destination/access boundary.
- `acceptance`: pending/accepted and summary; accepted also needs an actual supplied source and locator. Structure checking never authenticates consent. Release readiness is not inferred.
- `quick_previews` (optional): source-backed static HTML/CSS fragments, as described above. Not the complete proposed page.
- `screens`: up to six with `id`, title, description, current/proposed state, x/y/width/height, `sources`, requirement/evidence/task IDs and decision IDs. They are source-backed projections, not independent specifications.
- `representation`: `capture` plus `capture` source ID (role `ui-capture`, local static PNG), or `schematic` plus structured `content` entries. Schematic entries allow only heading/text/checkbox/input/button/message, plain label, optional value/boolean flags and decision ID. No raw HTML, arbitrary styles, event handlers, CSS classes, URLs or scripts in schematic controls; only the optional source-backed quick-preview extractor can show existing classes. Native-looking drawings are explicitly approximate, not actual browser controls.
- Captured screens can add `markers` with a mapped decision ID and `position: [xFraction, yFraction]` on the original image (0–1). Their full-size details preserve the uncropped source.
- `transitions`: from/to IDs, short action/outcome label, explicit orthogonal points and label position. Reuse the existing validated connector conventions. Screen boundaries must not be crossed.
- `decisions`: ID/title/question, two to four options, screen IDs, status open/selected/deferred. Selected choices require selection and source/locator/summary. Deferred choices state the reason and remain pending; they do not permit styling.
- `elements` (no `kit_binding`): one to twenty existing elements the screen copies, each exactly `{id, source, locator, tag, classes, context, changes}`: a supplied source ID, its `file:line`, the tag, the class list (empty when it has none), the wrapper it needs to look the same (`"inside div.dashboard-info-line-chip"`, or `"none: no rule needs context"`; ui-design `scripts/class_rules.py` lists the rules) and the content or wiring change. Required on every proposed screen in `reuse` mode. With a legacy `kit_binding`, use the strict kit fields below instead.
Keep the source tag/component identifier, such as `ion-chip`, `SwatchTile` or `Form.Group`; do not replace a framework component with an inferred DOM tag to satisfy validation. This is an escaped reference, not executable markup or automatic source verification.
Coordinates use the same 4px grid and maximum 1600×1600 board as normal flows, but keep a screen board at most 1160px wide (two or three screens of 280–360px per row; wrap a longer flow to a second row) so a laptop window shows every screen without sideways scrolling. Screens are 280–560px wide and 280–720px high. Avoid miniaturising text to fit; split larger flows into a meaningful reviewed subset and explain omitted paths. Sketches have at most six rows; a sketch screen is at least 120 + 48 × rows px high; labels and values are single-line and at most 42 characters, and about 7px per character (9 for headings) must fit the screen width less 48px. The checker reports every screen's problems in one run; fix them all before rendering again. Every decision maps to its affected screens. The viewer provides enlarged source/details links, not application actions.
## Approval
A proposed screen drawn as a labelled sketch inside this offline report needs no permission: never ask for it, and set `preview.status: not-needed` (its source and locator name where the report is saved), not `authorised`. A proposed screen *captured* from a running preview needs recorded preview authority (`preview.status: authorised`). A UI-visible change without a screen diagram keeps the report a draft. Legacy approved-kit mode always needs actual captures, never a lookalike schematic. A recorded accepted design cannot retain an unselected source or unresolved design choices.
For actual application UI, awaiting-approval requires a current mode, confirmed authority, an authorised preview, the copied elements for `reuse` screens and no open material UI decisions. Older mode names map as ui-design CONTRACT.md says. A behaviour-only repair preserving existing UI can use the normal report without adding a proposed screen view; do not manufacture a kit or redesign merely to report that repair. See the strict reference fields below.
A standalone visual request stops after delivery. The owning workflow records real approval in the existing task documents. Viewer links, zoom, option exploration and opening a page do not approve work. No approve button, model callback, telemetry, network fetch or application endpoint is added.
## Safety, output and compatibility
The complete report is still offline. Static PNGs are bounded, structurally checked and embedded as data; no arbitrary SVG/HTML uploads execute. Per-image limit 2 MB, total captures 4 MB, whole report 8 MiB. Use reviewed, sanitised captures; the renderer does not scrub personal information from pixels. PNG sources participate in the same freshness manifest as plans and code. Image tags are enabled only for this data-only extension.
Old saved HTML remains unchanged, and non-UI or historical/draft input remains readable. New application-approval UI inputs require the rc.7 fields below and the complete renderer; older renderers may ignore fields, so verify the selected-element details and current tool version rather than assuming forward support. Presented reports are never overwritten. Render the new projection only when the plan/review actually changes, preserving the task and approved history. This feature adds no new UI manifest or separate prototype app.
Inspect default/zoom/Fit, source links, keyboard access, small screens, long labels, image aspect ratio and missing images. Browser checks verify structure/interaction; human design acceptance and live host behaviour remain separate. Without JavaScript, radio tabs, readable native-size scrolling and native details remain available; UI Fit requires the optional viewer enhancement.
## Reused elements (D14; replaces the rc.7 strict kit section)
The existing shell, styles, diagrams, source sections and viewer remain unchanged. Application UI follows rule-maker design.md: never write CSS; reuse only classes/IDs that already exist (design kit or existing code), mirroring class and tag; when nothing matches, ask which existing element to mimic; plain unstyled HTML only on the user's explicit instruction. Current modes and the `elements` list are described above; `functionality-only` and `design-authoring` are retained only for historical/draft compatibility, not new implementation authority.
A **legacy** `approved-kit` report (a project catalogue bound with `kit_binding`) in `awaiting-approval`/`approved`, or claiming accepted design, needs real captures and these additional data-only fields:
- `ui_review.kit_binding`: `{ "source": "KIT", "scope": "src/settings" }`. The source is the selected local catalogue using ui-design/CATALOGUE.md's JSON profile. A catalogue's status does not authenticate consent. For an authoritative Markdown or other local catalogue, add `projection` containing the same schema-1 catalogue structure inside this existing report input; keep `source` pointing to that actual record. This is a generated projection, not another maintained file or a requirement to migrate the project catalogue. Review semantic correspondence; the checker cannot derive it from prose.
- Each screen's `elements`: one to twenty objects with exactly `id`, `item`, `role`, `variant`, `state`, `example_source`, `example_locator`, `changes`. `item` must resolve to the chosen kit; variant/state must be declared (use literal `not-applicable` only when the entry has no such options). `example_source` identifies a supplied actual example covered by that entry's sources. `changes` explains content/event wiring, not permission to style. Include the approved composition pattern, not only individual controls.
- Each captured screen's `capture_context`: `source`, `locator`, `summary`, `capture_sha256`, `inputs`. Reference the existing findings/evidence, explain environment/viewport/brand and limitations, and list `{path, sha256}` for the source inputs used in that capture. Include selected components, examples, theme and listed transitive dependencies; extra actual input references are allowed. No additional capture-record file is required. A source hash change must not be “fixed” by rewriting the capture's recorded inputs without recapture/evidence.
- A capture marker can use `{ "element": "E1", "position": [0.5, 0.4] }` to open that exact element row. Existing decision markers remain supported. IDs are scoped per screen.
**Inspect screen** shows exactly which entry, variant/state, example, component source and intended change is selected. Labels distinguish captures from missing provenance; checked source references are not proof that a component actually produced the screenshot. Verify the rendering evidence and actual human decision. The renderer rejects missing/deleted/stale/out-of-scope or unapproved selected inputs and unknown variants. It snapshots listed kit dependencies for future `--check`; it does not discover every dynamic dependency or perform runtime tests.
Existing historical HTML is never rewritten. Legacy/draft schematic examples remain readable and visibly non-authoritative. A layout sketch is drawn only from reused elements and never stands in for a capture the legacy kit mode requires; no styled mockups. No source badge grants application implementation or release permission.