agentic-workflow-manager 8.3.0 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,10 @@ const paths_1 = require("../core/paths");
15
15
  // read-only y el segundo auto-vivifica el archivo. Ver la nota en config.ts.
16
16
  const config_1 = require("../utils/config");
17
17
  const agent_targets_1 = require("../core/agent-targets");
18
+ const collect_1 = require("../core/dashboard/collect");
19
+ const render_html_1 = require("../core/dashboard/render-html");
20
+ const render_terminal_1 = require("../core/dashboard/render-terminal");
21
+ const write_html_1 = require("../core/dashboard/write-html");
18
22
  function glyph(status) {
19
23
  if (status === 'ok')
20
24
  return picocolors_1.default.green('✔');
@@ -123,6 +127,38 @@ function renderProviderReport(report) {
123
127
  }
124
128
  function runDoctor(opts = {}) {
125
129
  const resolveTargets = opts.resolveTargets ?? agent_targets_1.resolveAgentTargets;
130
+ const invalid = (message) => { process.stderr.write(`awm doctor: ${message}\n`); return 2; };
131
+ const htmlRequested = opts.html !== undefined;
132
+ if (opts.json && opts.full)
133
+ return invalid('--json cannot be combined with --full');
134
+ if (opts.json && htmlRequested)
135
+ return invalid('--json cannot be combined with --html');
136
+ if (opts.full && htmlRequested)
137
+ return invalid('--full cannot be combined with --html');
138
+ if (opts.force && !htmlRequested)
139
+ return invalid('--force requires --html');
140
+ if (opts.full || htmlRequested) {
141
+ try {
142
+ const cwd = opts.cwd ?? process.cwd();
143
+ const target = htmlRequested ? (0, write_html_1.resolveHtmlTarget)({ cwd, target: opts.html, force: opts.force }) : undefined;
144
+ const targets = resolveTargets({ prefs: (0, config_1.readPreferences)(), explicit: opts.agent });
145
+ const collectSnapshot = opts.collectSnapshot ?? collect_1.collectDashboardSnapshot;
146
+ const context = (0, context_1.gatherContext)({ cwd, agents: targets });
147
+ const snapshot = collectSnapshot({ cwd, now: new Date().toISOString(), adapters: {
148
+ ...(0, collect_1.productionDashboardAdapters)(context),
149
+ } });
150
+ if (opts.full)
151
+ process.stdout.write((0, render_terminal_1.renderFullTerminal)(snapshot) + '\n');
152
+ if (htmlRequested) {
153
+ (0, write_html_1.writeHtmlAtomically)({ cwd, target: target, html: (0, render_html_1.renderDashboardHtml)(snapshot), force: opts.force });
154
+ process.stdout.write(`${target}\n`);
155
+ }
156
+ return snapshot.overall === 'healthy' ? 0 : 1;
157
+ }
158
+ catch (error) {
159
+ return invalid(error.message);
160
+ }
161
+ }
126
162
  // Validates --agent separately from the general diagnostic gathering below:
127
163
  // an unknown or disabled agent is a normal input-validation failure (the
128
164
  // user typo'd or forgot `awm init --agent <x>`), not an "internal error" —
@@ -159,8 +195,11 @@ function registerDoctorCommand(program) {
159
195
  program.command('doctor')
160
196
  .description('Read-only dashboard of the AWM harness state, per provider')
161
197
  .option('--json', 'Emit the diagnostic report as JSON')
198
+ .option('--full', 'Emit the full dashboard snapshot')
199
+ .option('--html [file]', 'Write the dashboard snapshot as HTML')
200
+ .option('--force', 'Allow replacing an existing HTML file')
162
201
  .option('-a, --agent <agent>', 'Target agent subset (comma-separated); defaults to every enabled agent')
163
202
  .action((options) => {
164
- process.exitCode = runDoctor({ json: options.json, agent: options.agent });
203
+ process.exitCode = runDoctor({ json: options.json, full: options.full, html: typeof options.html === 'string' ? options.html : options.html === true ? '' : undefined, force: options.force, agent: options.agent });
165
204
  });
166
205
  }
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REMEDIATION_BY_FINDING_ID = void 0;
4
+ exports.productionDashboardAdapters = productionDashboardAdapters;
5
+ exports.collectDashboardSnapshot = collectDashboardSnapshot;
6
+ const profile_1 = require("../profile");
7
+ const sanitize_1 = require("./sanitize");
8
+ const validate_1 = require("./validate");
9
+ const plan_state_1 = require("./plan-state");
10
+ exports.REMEDIATION_BY_FINDING_ID = {
11
+ 'machine.preferences.missing': 'awm init',
12
+ 'machine.registries.stale': 'awm update',
13
+ 'project.profile.missing': 'awm init',
14
+ 'project.sensors.unavailable': 'awm sensors status',
15
+ 'project.preflight.degraded': 'awm preflight',
16
+ 'planning.source.unavailable': 'awm preflight',
17
+ 'execution.source.unavailable': 'awm sensors status',
18
+ };
19
+ const EMPTY_ADAPTERS = {
20
+ machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined,
21
+ };
22
+ const SAFE_REMEDIATIONS = new Set(['awm init', 'awm update', 'awm sync', 'awm sensors status', 'awm preflight']);
23
+ const HEALTHY_PROVIDER_STATES = new Set(['supported', 'healthy', 'shared', 'delivered']);
24
+ const INAPPLICABLE_PROVIDER_STATES = new Set(['unsupported']);
25
+ function providerState(state) {
26
+ if (HEALTHY_PROVIDER_STATES.has(state))
27
+ return 'ok';
28
+ if (INAPPLICABLE_PROVIDER_STATES.has(state))
29
+ return 'not_applicable';
30
+ return 'attention';
31
+ }
32
+ /**
33
+ * Maps the already-gathered diagnostics matrix into the snapshot's read-only
34
+ * source seam. It deliberately does not re-read providers, execute sensors, or
35
+ * relay provider detail/remediation prose: those values can contain local paths,
36
+ * command output, or credentials. The fixed IDs are derived solely from the
37
+ * provider/check enums and therefore remain stable across runs.
38
+ */
39
+ function productionDashboardAdapters(context) {
40
+ if (!context || typeof context !== 'object' || !Array.isArray(context.providers))
41
+ throw new Error('productionDashboardAdapters requires gathered provider diagnostics');
42
+ const machineFindings = context.providers.flatMap((provider) => provider.checks.flatMap((check) => {
43
+ const finding = {
44
+ id: `machine.provider.${provider.id}.${check.id}`,
45
+ // Provider labels are configuration prose. The provider id and check id
46
+ // are enum-controlled and sufficient for a stable public observation.
47
+ label: `Provider ${provider.id}: ${check.id}`,
48
+ state: providerState(check.state),
49
+ // Provider remediation is intentionally not forwarded. It is free-form
50
+ // diagnostics text, not a dashboard-approved canonical command.
51
+ remediationVerified: false,
52
+ };
53
+ // These two legacy diagnosis states are the only provider observations
54
+ // with a pre-existing, exact dashboard command mapping.
55
+ if (check.id === 'skills.global' && check.state === 'absent') {
56
+ return [finding, { id: 'machine.preferences.missing', label: 'Preferences', state: 'missing', remediationVerified: true }];
57
+ }
58
+ if (check.id === 'skills.global' && check.state === 'stale') {
59
+ return [finding, { id: 'machine.registries.stale', label: 'Registries', state: 'attention', remediationVerified: true }];
60
+ }
61
+ return [finding];
62
+ }));
63
+ const project = context.project;
64
+ const projectFindings = !project ? [] : [
65
+ { id: project.profile.present ? 'project.profile.present' : 'project.profile.missing', label: 'Profile', state: project.profile.present ? 'ok' : 'missing', remediation: 'awm init', remediationVerified: true },
66
+ { id: 'project.extensions.configured', label: 'Extensions', state: project.profile.extensions.length > 0 ? 'ok' : 'not_applicable' },
67
+ { id: 'project.registry-pins.present', label: 'Registry pins', state: project.profile.registries && Object.keys(project.profile.registries).length > 0 ? 'ok' : 'not_applicable' },
68
+ { id: 'project.bundles.coherent', label: 'Active bundles', state: project.activeBundles.broken.length === 0 ? 'ok' : 'attention', remediation: 'awm sync', remediationVerified: true },
69
+ { id: 'project.context.present', label: 'Project context', state: project.context.present ? 'ok' : 'missing', remediation: 'awm init', remediationVerified: true },
70
+ { id: 'project.constitution.present', label: 'Constitution', state: project.constitution.present ? 'ok' : 'missing' },
71
+ { id: project.sensors.present ? 'project.sensors.present' : 'project.sensors.unavailable', label: 'Sensors', state: project.sensors.present ? 'ok' : 'unavailable', remediation: 'awm sensors status', remediationVerified: true },
72
+ // `preflight()` is async because static tool inspection is async. Doctor's
73
+ // synchronous legacy API must not dispatch it here; make the absence of that
74
+ // observation explicit rather than inventing a readiness verdict.
75
+ { id: 'project.preflight.not_collected', label: 'Static preflight', state: 'not_applicable' },
76
+ ];
77
+ return {
78
+ machine: () => ({ findings: machineFindings }),
79
+ project: () => ({ label: 'Project detected', findings: projectFindings }),
80
+ plans: () => [],
81
+ execution: () => undefined,
82
+ };
83
+ }
84
+ function findings(items, optional = false) {
85
+ if (optional && items === undefined)
86
+ return [];
87
+ if (!Array.isArray(items))
88
+ throw new Error('Dashboard findings must be an array');
89
+ return items.flatMap((item) => {
90
+ if (!item || typeof item !== 'object' || typeof item.id !== 'string' || item.id.trim() === '' || typeof item.label !== 'string' || item.label.trim() === '')
91
+ throw new Error('Dashboard finding is invalid');
92
+ if (!['ok', 'attention', 'missing', 'unavailable', 'not_applicable'].includes(item.state))
93
+ throw new Error('Dashboard finding state is invalid');
94
+ const remediation = exports.REMEDIATION_BY_FINDING_ID[item.id]
95
+ ?? (item.remediationVerified === true && typeof item.remediation === 'string' && SAFE_REMEDIATIONS.has(item.remediation) ? item.remediation : undefined);
96
+ if (item.state !== 'ok' && item.state !== 'not_applicable' && !remediation)
97
+ return [];
98
+ return [{ id: item.id, label: item.label, state: item.state, ...(item.detail ? { detail: item.detail } : {}), ...(remediation ? { remediation } : {}) }];
99
+ }).sort((left, right) => left.id.localeCompare(right.id));
100
+ }
101
+ function section(id, availability, items = []) {
102
+ return { id, availability, items };
103
+ }
104
+ function optional(source) {
105
+ try {
106
+ return { value: source(), failed: false, failure: {} };
107
+ }
108
+ catch (error) {
109
+ const findingId = error && typeof error === 'object' && typeof error.findingId === 'string'
110
+ ? error.findingId : undefined;
111
+ const remediationVerified = error !== null && typeof error === 'object'
112
+ ? error.remediationVerified === true : false;
113
+ return { failed: true, failure: { findingId, remediationVerified } };
114
+ }
115
+ }
116
+ function canonicalOptionalFailure(failure) {
117
+ if (!failure.remediationVerified || !failure.findingId || !Object.hasOwn(exports.REMEDIATION_BY_FINDING_ID, failure.findingId))
118
+ return [];
119
+ return [{ id: failure.findingId, label: 'Optional source unavailable', state: 'unavailable', remediation: exports.REMEDIATION_BY_FINDING_ID[failure.findingId] }];
120
+ }
121
+ function isolatedFindings(items) {
122
+ return optional(() => findings(items, true));
123
+ }
124
+ /** Pure read-only aggregation over injected source adapters. */
125
+ function collectDashboardSnapshot(options) {
126
+ if (!options || typeof options.cwd !== 'string' || options.cwd.length === 0 || typeof options.now !== 'string' || Number.isNaN(Date.parse(options.now)))
127
+ throw new Error('collectDashboardSnapshot requires cwd and valid now');
128
+ const adapters = { ...EMPTY_ADAPTERS, ...(options.adapters ?? {}) };
129
+ const machine = adapters.machine({ cwd: options.cwd });
130
+ if (!Array.isArray(machine?.findings))
131
+ throw new Error('Dashboard findings must be an array');
132
+ const root = (0, profile_1.findProjectRoot)(options.cwd);
133
+ const machineItems = findings((0, sanitize_1.sanitizeDashboardSource)(machine).findings);
134
+ const machineSection = section('machine', 'available', machineItems);
135
+ if (!root) {
136
+ const degraded = machineSection.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable');
137
+ return (0, validate_1.validateDashboardSnapshotV1)({ schema: 1, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: false, label: 'No project detected' }, confidence: 'none', sections: [machineSection] });
138
+ }
139
+ const projectResult = optional(() => (0, sanitize_1.sanitizeDashboardSource)(adapters.project({ root })));
140
+ const plansResult = optional(() => (0, sanitize_1.sanitizeDashboardSource)(adapters.plans({ root })));
141
+ const executionResult = optional(() => {
142
+ const source = adapters.execution({ root });
143
+ return source === undefined ? undefined : (0, sanitize_1.sanitizeDashboardSource)(source);
144
+ });
145
+ const projectSource = projectResult.value;
146
+ const execution = executionResult.value;
147
+ const projectItemsResult = projectSource ? isolatedFindings(projectSource.findings) : { value: [], failed: false, failure: {} };
148
+ const planItemsResult = plansResult.value ? optional(() => findings(plansResult.value.map((plan) => plan.lifecycle
149
+ ? { ...plan, detail: (0, plan_state_1.classifyPlanState)(plan.lifecycle) } : plan))) : { value: [], failed: false, failure: {} };
150
+ const executionItems = isolatedFindings(execution?.execution);
151
+ const qaItems = isolatedFindings(execution?.qa);
152
+ const retroItems = isolatedFindings(execution?.retro);
153
+ const historyItems = isolatedFindings(execution?.history);
154
+ const executionUnavailable = !executionResult.failed && execution === undefined;
155
+ const sections = [
156
+ machineSection,
157
+ section('project', projectResult.failed || projectItemsResult.failed ? 'unavailable' : 'available', projectResult.failed
158
+ ? canonicalOptionalFailure(projectResult.failure) : projectItemsResult.failed ? [] : projectItemsResult.value),
159
+ section('planning', plansResult.failed || planItemsResult.failed ? 'unavailable' : 'available', plansResult.failed
160
+ ? canonicalOptionalFailure(plansResult.failure) : planItemsResult.failed ? [] : planItemsResult.value),
161
+ section('execution', executionResult.failed || executionUnavailable || executionItems.failed ? 'unavailable' : 'available', executionResult.failed ? canonicalOptionalFailure(executionResult.failure) : executionUnavailable ? canonicalOptionalFailure({ findingId: 'execution.source.unavailable', remediationVerified: true }) : executionItems.value),
162
+ // There is no read-only QA, retro, or history adapter in Release A. An
163
+ // absent execution source is not evidence of a successful empty cycle.
164
+ section('qa', executionResult.failed || executionUnavailable || qaItems.failed ? 'unavailable' : 'available', qaItems.value),
165
+ section('retro', executionResult.failed || executionUnavailable || retroItems.failed ? 'unavailable' : 'available', retroItems.value),
166
+ section('history', executionResult.failed || executionUnavailable || historyItems.failed ? 'unavailable' : 'available', historyItems.value),
167
+ ];
168
+ const degraded = sections.some((entry) => entry.availability === 'unavailable' || entry.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable'));
169
+ return (0, validate_1.validateDashboardSnapshotV1)({ schema: 1, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: true, label: projectSource?.label || 'Project detected' }, confidence: 'provisional', sections });
170
+ }
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyPlanState = classifyPlanState;
4
+ function assertRecord(value, label) {
5
+ if (!value || typeof value !== 'object' || Array.isArray(value))
6
+ throw new Error(`Plan ${label} must be an object`);
7
+ }
8
+ function assertKeys(value, label, keys) {
9
+ if (Object.keys(value).some((key) => !keys.includes(key)))
10
+ throw new Error(`Plan ${label} has unsupported fields`);
11
+ }
12
+ function classifyPlanState(input) {
13
+ assertRecord(input, 'state input');
14
+ assertKeys(input, 'state input', ['journal', 'markers', 'tasks']);
15
+ assertRecord(input.markers, 'markers');
16
+ assertKeys(input.markers, 'markers', ['qaComplete', 'retroComplete']);
17
+ assertRecord(input.tasks, 'tasks');
18
+ assertKeys(input.tasks, 'tasks', ['total', 'completed']);
19
+ const markers = input.markers;
20
+ const tasks = input.tasks;
21
+ if (typeof markers.qaComplete !== 'boolean' || typeof markers.retroComplete !== 'boolean')
22
+ throw new Error('Plan markers must be boolean');
23
+ if (typeof tasks.total !== 'number' || typeof tasks.completed !== 'number' || !Number.isInteger(tasks.total) || !Number.isInteger(tasks.completed) || tasks.total < 0 || tasks.completed < 0 || tasks.completed > tasks.total)
24
+ throw new Error('Plan task counts are invalid');
25
+ let journalState;
26
+ if (input.journal !== undefined) {
27
+ assertRecord(input.journal, 'journal');
28
+ assertKeys(input.journal, 'journal', ['state']);
29
+ if (input.journal.state !== 'active' && input.journal.state !== 'blocked')
30
+ throw new Error('Plan journal state is invalid');
31
+ journalState = input.journal.state;
32
+ }
33
+ if (journalState === 'blocked')
34
+ return 'blocked';
35
+ if (journalState === 'active')
36
+ return 'active';
37
+ if (markers.retroComplete)
38
+ return 'executed';
39
+ if (markers.qaComplete)
40
+ return 'retro_pending';
41
+ if (tasks.total > 0 && tasks.completed === tasks.total)
42
+ return 'qa_pending';
43
+ return 'legacy_unverifiable';
44
+ }
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderDashboardHtml = renderDashboardHtml;
4
+ const styles_1 = require("./styles");
5
+ const validate_1 = require("./validate");
6
+ const CSP = "default-src 'none'; style-src 'unsafe-inline'; img-src data:; script-src 'none'; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'";
7
+ const SECTION_TITLES = { machine: 'Machine / install', project: 'Project readiness', planning: 'Design / planning', execution: 'Execution', qa: 'QA', retro: 'Retro', history: 'Final / history' };
8
+ const STATE_TEXT = { ok: 'OK', attention: 'Attention', missing: 'Missing', unavailable: 'Unavailable', not_applicable: 'Not applicable' };
9
+ const STATE_GLYPH = { ok: '●', attention: '▲', missing: '×', unavailable: '⊘', not_applicable: '—' };
10
+ function escapeHtml(value) { return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); }
11
+ function sectionHtml(section, supplement = '') {
12
+ const title = SECTION_TITLES[section.id];
13
+ const availability = section.availability === 'available' ? '' : `<p class="availability ${section.availability}">⊘ Source ${escapeHtml(section.availability.replace('_', ' '))}</p>`;
14
+ const rows = section.items.length === 0 ? '<p class="empty">No observations reported.</p>' : `<table><thead><tr><th scope="col">Observation</th><th scope="col">State</th><th scope="col">Detail</th><th scope="col">Remediation</th></tr></thead><tbody>${section.items.map((item) => `<tr><td data-label="Observation">${escapeHtml(item.label)}</td><td data-label="State"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span></td><td data-label="Detail">${item.detail ? escapeHtml(item.detail) : '—'}</td><td data-label="Remediation">${item.remediation ? `<code>${escapeHtml(item.remediation)}</code>` : '—'}</td></tr>`).join('')}</tbody></table>`;
15
+ return `<section id="${section.id}" aria-label="${title}"><header><h2>${title}</h2><span class="eyebrow">${escapeHtml(section.availability.replace('_', ' '))}</span></header><div class="section-body">${availability}${supplement}${rows}</div></section>`;
16
+ }
17
+ function diagnosticCards(items, attribute) {
18
+ const labels = ['installation', 'sensors', 'permissions'];
19
+ const cards = items.slice(0, 3).map((item, index) => `<li data-diagnostic-card="${labels[index] ?? 'diagnostic'}"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><strong>${escapeHtml(item.label)}</strong><span>${item.detail ? escapeHtml(item.detail) : 'No additional detail'}</span></li>`).join('') || '<li><span class="state not_applicable">— Not applicable</span><strong>No machine observations</strong><span>Machine diagnostics are not available.</span></li>';
20
+ if (attribute === 'data-machine-diagnostics')
21
+ return `<section data-machine-diagnostics aria-labelledby="machine-diagnostics-heading"><header><h2 id="machine-diagnostics-heading">Machine diagnostics</h2></header><div class="section-body"><ul class="diagnostic-grid">${cards}</ul></div></section>`;
22
+ return `<div class="machine-preparation-strip" data-machine-preparation role="group" aria-labelledby="machine-preparation-heading"><h3 id="machine-preparation-heading">Preparación de máquina</h3><ul class="diagnostic-grid">${cards}</ul></div>`;
23
+ }
24
+ function machineBento(items) {
25
+ const names = ['Instalación', 'Sensores globales', 'Persistencia'];
26
+ return `<div class="machine-bento" data-machine-bento>${names.map((name, index) => {
27
+ const item = items[index];
28
+ return `<article class="bento-card"><h2>${name}</h2>${item ? `<span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><strong>${escapeHtml(item.label)}</strong><span>${item.detail ? escapeHtml(item.detail) : 'Sin detalle adicional'}</span>` : '<span class="state unavailable">⊘ Unavailable</span><span>Sin observación disponible</span>'}</article>`;
29
+ }).join('')}</div>`;
30
+ }
31
+ function nextActions(snapshot) {
32
+ const actionable = snapshot.sections.flatMap((section) => section.items.filter((item) => item.remediation)).slice(0, 2);
33
+ const rows = [...actionable, { id: 'setup.initialize', label: 'Inicializar proyecto', state: 'attention', remediation: 'awm init' }];
34
+ return `<div data-next-actions role="group" aria-labelledby="next-actions-heading"><h2 id="next-actions-heading">Siguiente acción requerida</h2><ol class="action-list">${rows.map((item) => `<li data-next-action="${escapeHtml(item.id)}"><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><span>${escapeHtml(item.label)}</span><code>${item.remediation}</code><button type="button" disabled aria-describedby="static-controls-note">Copy command (static)</button></li>`).join('')}</ol></div>`;
35
+ }
36
+ function privacyAndActions(snapshot) {
37
+ return `<section data-privacy-security aria-label="Privacy and security"><header><h2>Privacy &amp; security</h2></header><div class="section-body privacy-body"><div><p class="lede">This portable view contains sanitized states and exact operator remedies only. It excludes paths, identities, environment values, secret-like values, raw command output, ledger prose, and error stacks.</p><p id="privacy-toggle-note" class="static-note">Static export: this checked setting documents the enforced share-safe boundary and cannot be changed in this file.</p></div><label class="static-toggle" data-static-privacy-toggle><span>Share-safe sanitization</span><input type="checkbox" checked disabled aria-describedby="privacy-toggle-note"><span aria-hidden="true">Enabled</span></label></div></section>${nextActions(snapshot)}`;
38
+ }
39
+ function dashboardToolbar() {
40
+ return `<header class="dashboard-toolbar" data-dashboard-toolbar><p class="toolbar-brand">AWM <span>Doctor dashboard</span></p><form role="search" aria-label="Search dashboard"><label class="sr-only" for="dashboard-search">Search resources</label><input id="dashboard-search" type="search" placeholder="Search resources" disabled aria-describedby="static-controls-note"></form><div class="toolbar-actions"><button type="button" disabled aria-describedby="static-controls-note">Notifications (static)</button><button type="button" disabled aria-describedby="static-controls-note">Help (static)</button><button type="button" disabled aria-describedby="static-controls-note">Export dashboard (static)</button><button type="button" disabled aria-describedby="static-controls-note">New deployment (static)</button></div><p id="static-controls-note" class="sr-only">Controls are shown for reference only; this exported dashboard does not run scripts.</p></header>`;
41
+ }
42
+ function projectHeaderActions() {
43
+ return `<div class="project-header-actions" data-project-header-actions role="group" aria-label="Project actions"><button type="button" disabled aria-describedby="static-controls-note">Export evidence (static)</button><button type="button" disabled aria-describedby="static-controls-note">Attach evidence (static)</button></div>`;
44
+ }
45
+ function projectChrome(project) {
46
+ return `<div class="project-chrome"><p class="eyebrow" data-project-breadcrumb>AWM / Proyecto / ${project}</p><nav data-project-nav aria-label="Project navigation"><span aria-current="page">Proyecto</span><span>Ejecución</span><span>Configuración</span></nav></div>`;
47
+ }
48
+ function projectEvidenceComposition() {
49
+ return `<div data-project-evidence role="group" aria-labelledby="project-evidence-heading"><h3 id="project-evidence-heading" class="sr-only">Project evidence</h3><div class="evidence-grid compact-composition"><div><h3>Planes de trabajo</h3><div class="plan-card active" data-plan-card="active"><div><strong>Plan activo</strong><span class="state attention">Sin observación</span></div><p>Progreso: sin observación</p><button type="button" disabled aria-describedby="static-controls-note">Ver plan (estático)</button></div><div class="plan-card blocked" data-plan-card="blocked"><div><strong>Plan bloqueado</strong><span class="state unavailable">Sin observación</span></div><p>Progreso: sin observación</p><button type="button" disabled aria-describedby="static-controls-note">Ver plan (estático)</button></div></div><div><h3>Impacto y trazabilidad</h3><table class="evidence-table"><thead><tr><th scope="col">Tipo</th><th scope="col">Fuente</th><th scope="col">Estado</th><th scope="col">Fecha</th></tr></thead><tbody><tr><td colspan="4" class="empty">No hay evidencia de impacto disponible en este snapshot.</td></tr></tbody></table></div></div><div class="closure-actions" data-closure-actions role="group" aria-labelledby="closure-actions-heading"><div><h3 id="closure-actions-heading">Qué falta para cerrar el ciclo</h3><p class="empty">La ausencia de marcador no equivale a cero hallazgos.</p></div><div><button type="button" disabled aria-describedby="static-controls-note">Completar QA (estático)</button><button type="button" disabled aria-describedby="static-controls-note">Registrar retro (estático)</button><button type="button" disabled aria-describedby="static-controls-note">Adjuntar evidencia (estático)</button></div></div></div>`;
50
+ }
51
+ function projectComposition(snapshot) {
52
+ const stageSections = ['planning', 'execution', 'qa', 'retro', 'history'];
53
+ const byId = new Map(snapshot.sections.map((section) => [section.id, section]));
54
+ const stages = stageSections.map((id) => {
55
+ const section = byId.get(id);
56
+ const stage = id === 'history' ? 'evidence' : id;
57
+ const available = section?.availability === 'available';
58
+ return `<li data-stage="${stage}"><span aria-hidden="true" class="timeline-marker"></span><strong>${stage === 'evidence' ? 'Evidence' : SECTION_TITLES[id]}</strong><span class="state ${available ? 'ok' : 'unavailable'}">${available ? '● Available' : '⊘ Unavailable'}</span></li>`;
59
+ }).join('');
60
+ const provisional = snapshot.confidence === 'provisional' ? '<aside data-provisional-evidence aria-label="Provisional evidence"><strong>Provisional evidence</strong><span>Current observations are still being verified by downstream QA and evidence capture.</span></aside>' : '';
61
+ const prepItems = byId.get('machine')?.items ?? [];
62
+ const prepNames = ['installation', 'sensors', 'persistence'];
63
+ const preparation = `<div class="machine-preparation-strip" data-machine-preparation role="group" aria-labelledby="machine-preparation-heading"><h3 id="machine-preparation-heading">Preparación de máquina</h3><ul class="diagnostic-grid">${prepNames.map((name, index) => { const item = prepItems[index]; return `<li data-machine-preparation-card="${name}">${item ? `<span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><strong>${escapeHtml(item.label)}</strong><span>${item.detail ? escapeHtml(item.detail) : 'Sin detalle adicional'}</span>` : '<span class="state unavailable">⊘ Unavailable</span><strong>Sin observación</strong><span>Fuente no disponible</span>'}</li>`; }).join('')}</ul></div>`;
64
+ const machineSupplement = `${preparation}<nav class="lifecycle-timeline" data-lifecycle-timeline aria-labelledby="lifecycle-timeline-heading"><h3 id="lifecycle-timeline-heading">Línea de ciclo</h3><ol class="timeline connected-timeline">${stages}</ol></nav>${provisional}`;
65
+ const historySupplement = projectEvidenceComposition();
66
+ return snapshot.sections.map((section) => sectionHtml(section, section.id === 'machine' ? machineSupplement : section.id === 'history' ? historySupplement : '')).join('');
67
+ }
68
+ /** Renders a portable, static, share-safe dashboard document. */
69
+ function renderDashboardHtml(input) {
70
+ const snapshot = (0, validate_1.validateDashboardSnapshotV1)(input);
71
+ const overall = escapeHtml(snapshot.overall);
72
+ const project = escapeHtml(snapshot.project.label);
73
+ const machineItems = snapshot.sections.find((section) => section.id === 'machine')?.items ?? [];
74
+ const sections = snapshot.project.detected ? projectComposition(snapshot) : `${machineBento(machineItems)}${privacyAndActions(snapshot)}${snapshot.sections.map((section) => sectionHtml(section)).join('')}`;
75
+ const links = '<li>Inicio</li><li>Estado</li><li class="active">Configuración</li><li>Terminal</li>';
76
+ const projectDetected = snapshot.project.detected;
77
+ const heading = projectDetected ? 'Project lifecycle' : 'Machine configuration';
78
+ const intro = projectDetected ? 'Readiness, lifecycle state, and eligible observations presented directly for operator review.' : 'Machine readiness and safe configuration state outside a project.';
79
+ const navLabel = projectDetected ? 'Dashboard sections' : 'Machine configuration sections';
80
+ const context = projectDetected ? `Project: ${project}` : 'No project detected';
81
+ return `<!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="${CSP}"><title>AWM Doctor dashboard</title><style>${styles_1.DASHBOARD_STYLES}</style></head><body><div class="shell"><nav data-operational-sidebar aria-label="${navLabel}"><p class="brand">AWM<small>Doctor dashboard</small></p><ul>${links}</ul><button data-sidebar-diagnose type="button" disabled aria-describedby="static-controls-note">Ejecutar diagnóstico</button><div data-sidebar-operator-controls><span>Operaciones</span><button type="button" disabled aria-describedby="static-controls-note">Soporte</button><button type="button" disabled aria-describedby="static-controls-note">Cerrar sesión</button></div></nav><main>${dashboardToolbar()}<div class="dashboard-content"><header class="page-header">${projectDetected ? projectChrome(project) : ''}<p class="eyebrow">Read-only diagnostic evidence</p><h1>${heading}</h1><p class="lede">${intro}</p><p><span class="status ${overall}">● ${overall}</span> <span class="status">Confidence: ${escapeHtml(snapshot.confidence)}</span></p><p class="eyebrow">${context}</p>${projectDetected ? projectHeaderActions() : ''}</header>${sections}<footer>Generated ${escapeHtml(snapshot.generatedAt)} · Static share-safe dashboard</footer></div></main></div></body></html>\n`;
82
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderFullTerminal = renderFullTerminal;
4
+ const validate_1 = require("./validate");
5
+ const SECTION_TITLES = {
6
+ machine: 'Machine / install',
7
+ project: 'Project readiness',
8
+ planning: 'Design / planning',
9
+ execution: 'Execution',
10
+ qa: 'QA',
11
+ retro: 'Retro',
12
+ history: 'Final / history',
13
+ };
14
+ const STATE_PRESENTATION = {
15
+ ok: { glyph: '✔', text: 'ok' },
16
+ attention: { glyph: '⚠', text: 'attention' },
17
+ missing: { glyph: '✖', text: 'missing' },
18
+ unavailable: { glyph: '⊘', text: 'unavailable' },
19
+ not_applicable: { glyph: '—', text: 'not applicable' },
20
+ };
21
+ /** Renders a complete, color-free dashboard suitable for terminals and logs. */
22
+ function renderFullTerminal(input) {
23
+ const snapshot = (0, validate_1.validateDashboardSnapshotV1)(input);
24
+ const lines = [
25
+ `AWM dashboard · ${snapshot.overall}`,
26
+ `Project: ${snapshot.project.label}`,
27
+ `Confidence: ${snapshot.confidence}`,
28
+ ];
29
+ for (const section of snapshot.sections) {
30
+ lines.push('', SECTION_TITLES[section.id]);
31
+ if (section.availability !== 'available')
32
+ lines.push(` ⊘ source ${section.availability.replace('_', ' ')}`);
33
+ if (section.items.length === 0)
34
+ lines.push(' No observations reported.');
35
+ for (const item of section.items) {
36
+ const presentation = STATE_PRESENTATION[item.state];
37
+ lines.push(` ${presentation.glyph} ${item.label} [${item.id}]${item.detail ? ` — ${item.detail}` : ` — ${presentation.text}`}`);
38
+ if (item.remediation)
39
+ lines.push(` → ${item.remediation}`);
40
+ }
41
+ }
42
+ return lines.join('\n');
43
+ }
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeDashboardSource = sanitizeDashboardSource;
4
+ const crypto_1 = require("crypto");
5
+ const STATES = new Set(['ok', 'attention', 'missing', 'unavailable', 'not_applicable', 'active', 'blocked']);
6
+ const ALLOWED_KEYS = new Set(['findings', 'label', 'id', 'state', 'detail', 'remediation', 'remediationVerified', 'execution', 'qa', 'retro', 'history', 'lifecycle', 'journal', 'markers', 'tasks', 'total', 'completed', 'qaComplete', 'retroComplete']);
7
+ const CANONICAL_LABELS = new Set([
8
+ 'Preferences', 'Registries', 'Profile', 'Sensors', 'Optional source unavailable',
9
+ 'Extensions', 'Registry pins', 'Active bundles', 'Project context', 'Constitution', 'Static preflight',
10
+ ]);
11
+ const CANONICAL_FINDING_IDS = new Set([
12
+ 'machine.preferences.missing', 'machine.registries.stale', 'project.profile.missing',
13
+ 'project.sensors.unavailable', 'project.preflight.degraded', 'planning.source.unavailable', 'execution.source.unavailable',
14
+ ]);
15
+ const PROVIDER_FINDING_ID = /^machine\.provider\.(?:claude-code|codex|opencode|cursor|copilot|antigravity)\.(?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|hook\.trust|guidance\.project|constitution\.delivery)$/;
16
+ const PROJECT_FINDING_ID = /^project\.(?:profile\.present|extensions\.configured|registry-pins\.present|bundles\.coherent|context\.present|constitution\.present|sensors\.present|preflight\.not_collected)$/;
17
+ const PROVIDER_LABEL = /^Provider (?:claude-code|codex|opencode|cursor|copilot|antigravity): (?:binary\.version|skills\.global|agents\.native|workflows\.global|context\.global|hook\.trust|guidance\.project|constitution\.delivery)$/;
18
+ const DANGEROUS = /(?:ghp_|sk-[A-Za-z]|<|>|\\\\[^\\\s]+\\[^\\\s]+|\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*|[A-Za-z]:\\|\b[A-Za-z_][A-Za-z0-9_]*=|token|secret|password)/iu;
19
+ function sanitize(value, key) {
20
+ if (value === null || typeof value === 'boolean')
21
+ return value;
22
+ if (typeof value === 'number') {
23
+ if (!Number.isFinite(value))
24
+ throw new Error('Dashboard source numbers must be finite');
25
+ return value;
26
+ }
27
+ if (typeof value === 'string') {
28
+ if (key === 'state' && !STATES.has(value))
29
+ throw new Error(`Dashboard source state is invalid: ${value}`);
30
+ // IDs are source-controlled and are rendered into snapshots. Preserve only
31
+ // the small canonical vocabulary; opaque IDs retain deterministic ordering
32
+ // without exporting repository names, emails, IPs, or local identifiers.
33
+ if (key === 'id') {
34
+ if (value.trim() === '')
35
+ throw new Error('Dashboard finding id is invalid');
36
+ return CANONICAL_FINDING_IDS.has(value) || PROVIDER_FINDING_ID.test(value) || PROJECT_FINDING_ID.test(value)
37
+ ? value : `item-${(0, crypto_1.createHash)('sha256').update(value).digest('hex').slice(0, 16)}`;
38
+ }
39
+ if (key === 'label' && !CANONICAL_LABELS.has(value) && !PROVIDER_LABEL.test(value))
40
+ return '[redacted]';
41
+ return DANGEROUS.test(value) ? '[redacted]' : value;
42
+ }
43
+ if (Array.isArray(value))
44
+ return value.map((entry) => sanitize(entry));
45
+ if (!value || typeof value !== 'object')
46
+ throw new Error('Dashboard source must contain JSON-compatible values');
47
+ const out = {};
48
+ for (const [entryKey, entryValue] of Object.entries(value)) {
49
+ if (!ALLOWED_KEYS.has(entryKey))
50
+ continue;
51
+ // Source details are untrusted command/error output. Renderers only receive
52
+ // canonical details produced after collection (for example lifecycle state).
53
+ if (entryKey === 'detail')
54
+ continue;
55
+ out[entryKey] = sanitize(entryValue, entryKey);
56
+ }
57
+ return out;
58
+ }
59
+ /** Removes dynamic credentials, local paths, and hostile markup from source observations. */
60
+ function sanitizeDashboardSource(value) {
61
+ return sanitize(value);
62
+ }
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DASHBOARD_STYLES = void 0;
4
+ /** Inline visual system derived from the approved Stitch dashboard artifacts. */
5
+ exports.DASHBOARD_STYLES = `
6
+ :root { color-scheme: dark; --canvas:#070d16; --surface:#101a29; --surface-raised:#172235; --surface-nav:#202a3b; --ink:#edf3ff; --muted:#b7c2d4; --border:#39455b; --indigo:#aebcff; --cyan:#8cecff; --amber:#ffc06a; --red:#ffb4ac; --green:#77e8be; --radius:4px; }
7
+ * { box-sizing:border-box; }
8
+ html { background:var(--canvas); color:var(--ink); font:14px/1.35 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
9
+ body { margin:0; background:var(--canvas); }
10
+ .shell { display:grid; grid-template-columns:12rem minmax(0,1fr); min-height:100vh; }
11
+ .shell > nav { background:var(--surface-nav); border-right:1px solid var(--border); padding:.85rem .65rem; }
12
+ .brand { margin:0 0:1rem; font-size:.9rem; font-weight:750; letter-spacing:-.03em; }
13
+ .brand small { display:block; color:var(--muted); font-size:.72rem; font-weight:500; letter-spacing:0; }
14
+ .shell > nav ul { list-style:none; margin:0; padding:0; display:grid; gap:.15rem; }
15
+ .shell > nav li { color:var(--muted); font-size:.78rem; padding:.4rem .45rem; border-left:2px solid transparent; } .shell > nav li.active { background:var(--indigo); color:#101a29; font-weight:750; }
16
+ .shell > nav > button { margin-top:1rem; width:100%; } [data-sidebar-operator-controls] { border-top:1px solid var(--border); color:var(--muted); display:grid; font-size:.72rem; gap:.35rem; margin-top:1rem; padding-top:.75rem; }
17
+ main { min-width:0; padding:0 clamp(.75rem,3vw,2.5rem) 3rem; }
18
+ .dashboard-content { margin:0 auto; width:min(100%,72rem); }
19
+ header { border-bottom:1px solid var(--border); padding-bottom:1.25rem; }
20
+ .dashboard-toolbar { align-items:center; display:flex; flex-wrap:wrap; gap:.45rem; justify-content:space-between; margin:0 calc(clamp(.75rem,3vw,2.5rem) * -1); min-height:2.8rem; padding:.4rem clamp(.75rem,3vw,2.5rem); }
21
+ .toolbar-brand { font-weight:750; letter-spacing:-.025em; margin:0; } .toolbar-brand span { color:var(--muted); font-size:.76rem; font-weight:500; margin-left:.35rem; }
22
+ .dashboard-toolbar form { margin-left:auto; } .dashboard-toolbar input { background:#080e18; border:1px solid var(--border); border-radius:var(--radius); color:var(--muted); min-width:12rem; padding:.4rem .55rem; } .dashboard-toolbar input:disabled { cursor:not-allowed; opacity:.72; }
23
+ .toolbar-actions,.project-header-actions,.closure-actions > div:last-child { display:flex; flex-wrap:wrap; gap:.4rem; }
24
+ button { background:var(--surface-raised); border:1px solid var(--border); border-radius:var(--radius); color:var(--ink); font:inherit; padding:.38rem .55rem; } button:disabled { cursor:not-allowed; opacity:.7; } button:focus-visible,input:focus-visible { outline:3px solid var(--cyan); outline-offset:3px; }
25
+ .page-header { padding:1.35rem 0 .8rem; } .project-header-actions { margin-top:.6rem; }
26
+ h1,h2,p { margin-top:0; } h1 { font-size:clamp(1.45rem,2.4vw,2rem); letter-spacing:-.035em; margin-bottom:.2rem; } h2 { font-size:.88rem; letter-spacing:.035em; margin:0; } h3 { font-size:.8rem; margin:.1rem 0 .45rem; }
27
+ .eyebrow,.status { color:var(--muted); font-family:ui-monospace,SFMono-Regular,Consolas,monospace; font-size:.78rem; }
28
+ .status { display:inline-flex; align-items:center; gap:.4rem; border:1px solid var(--border); border-radius:var(--radius); padding:.2rem .5rem; }
29
+ .status.degraded { color:var(--amber); border-color:#72552e; } .status.healthy { color:var(--green); border-color:#2f6c58; }
30
+ .lede { color:var(--muted); max-width:72ch; }
31
+ section { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); margin-top:.65rem; overflow:hidden; }
32
+ section > header { align-items:center; background:var(--surface-raised); border:0; display:flex; justify-content:space-between; padding:.55rem .75rem; }
33
+ .section-body { padding:.75rem; }
34
+ .availability { color:var(--muted); font-size:.85rem; margin-bottom:.75rem; } .availability.unavailable { color:var(--amber); }
35
+ table { border-collapse:collapse; width:100%; } th,td { border-bottom:1px solid var(--border); padding:.45rem .4rem; text-align:left; vertical-align:top; } th { color:var(--muted); font-size:.66rem; letter-spacing:.07em; text-transform:uppercase; } tr:last-child td { border-bottom:0; }
36
+ .state { font-weight:650; white-space:nowrap; } .state.ok { color:var(--green); } .state.attention { color:var(--amber); } .state.missing,.state.unavailable { color:var(--red); } .state.not_applicable { color:var(--muted); }
37
+ code { background:#080e18; border:1px solid var(--border); border-radius:2px; color:var(--cyan); font:inherit; padding:.12rem .3rem; white-space:pre-wrap; overflow-wrap:anywhere; }
38
+ .empty { color:var(--muted); margin:0; } footer { color:var(--muted); font-size:.8rem; padding:1.25rem 0; }
39
+ .diagnostic-grid,.timeline,.action-list { display:grid; gap:.45rem; list-style:none; margin:0; padding:0; } .diagnostic-grid { grid-template-columns:repeat(3,minmax(0,1fr)); } .machine-preparation-strip { background:#0b1320; border:1px solid var(--border); margin-bottom:.65rem; padding:.6rem; } .machine-preparation-strip h3 { color:var(--muted); font-size:.68rem; letter-spacing:.07em; text-transform:uppercase; } .diagnostic-grid li { border-right:1px solid var(--border); display:grid; gap:.2rem; padding:0 .5rem; } .diagnostic-grid li:last-child { border-right:0; } .diagnostic-grid strong { font-size:.78rem; } .diagnostic-grid li > span:last-child { color:var(--muted); font: .7rem ui-monospace,SFMono-Regular,Consolas,monospace; }
40
+ .lifecycle-timeline { background:#0b1320; border:1px solid var(--border); margin-bottom:.65rem; padding:.6rem; } .lifecycle-timeline h3 { color:var(--muted); font-size:.68rem; letter-spacing:.07em; text-transform:uppercase; } .timeline { grid-template-columns:repeat(5,minmax(0,1fr)); } .connected-timeline { border-top:1px solid var(--border); margin-top:.4rem; padding-top:.25rem; } .timeline li { background:#0b1320; display:grid; gap:.2rem; padding:.2rem; } .timeline-marker { background:var(--surface-raised); border:1px solid var(--border); border-radius:50%; height:.65rem; width:.65rem; } .timeline .state { font-size:.68rem; } .timeline .state.unavailable { background:#38141c; color:#fff0ed; display:inline-flex; padding:.08rem .25rem; width:max-content; } aside[data-provisional-evidence] { background:#261d10; border-left:3px solid var(--amber); color:var(--ink); display:grid; gap:.15rem; margin-top:.65rem; padding:.6rem .75rem; } aside[data-provisional-evidence] span { color:var(--muted); }
41
+ .privacy-body { align-items:center; display:flex; gap:1rem; justify-content:space-between; } .static-note { color:var(--muted); font-size:.78rem; margin:0; } .static-toggle { align-items:center; background:#0b1320; border:1px solid var(--border); display:flex; flex-wrap:wrap; gap:.5rem; padding:.65rem; } .static-toggle input { accent-color:var(--cyan); } .static-toggle input:disabled { opacity:1; }
42
+ .machine-bento { display:grid; gap:.65rem; grid-template-columns:repeat(3,minmax(0,1fr)); margin-top:.65rem; } .bento-card { background:var(--surface); border:1px solid var(--border); display:grid; gap:.35rem; min-height:8rem; padding:.75rem; } .bento-card h2 { color:var(--muted); font-size:.7rem; letter-spacing:.07em; text-transform:uppercase; } [data-next-actions] { background:var(--surface); border:1px solid var(--border); margin-top:.65rem; padding:.75rem; } [data-next-actions] h2 { margin-bottom:.65rem; } .project-chrome { display:flex; flex-wrap:wrap; gap:.5rem 1rem; justify-content:space-between; } [data-project-nav] { display:flex; gap:.65rem; font-size:.72rem; } [data-project-nav] [aria-current] { color:var(--cyan); font-weight:700; }
43
+ .evidence-grid.compact-composition { display:grid; gap:.65rem; grid-template-columns:repeat(2,minmax(0,1fr)); padding:.65rem; } .evidence-grid h2 { font-size:.9rem; margin-bottom:.4rem; } .plan-card { border:1px solid var(--border); display:grid; gap:.35rem; margin-top:.4rem; padding:.55rem; } .plan-card.active { border-left:3px solid var(--indigo); } .plan-card.blocked { border-left:3px solid var(--red); } .plan-card > div { align-items:center; display:flex; justify-content:space-between; gap:.5rem; } .plan-card p { color:var(--muted); font-size:.72rem; margin:0; }
44
+ .evidence-table { margin-top:.65rem; } .closure-actions { align-items:center; border-top:1px solid var(--border); display:flex; gap:1rem; justify-content:space-between; margin-top:1rem; padding:1rem; } .closure-actions h3 { font-size:.9rem; margin:0 0:.25rem; }
45
+ .action-list li { align-items:center; border-bottom:1px solid var(--border); display:grid; gap:.6rem; grid-template-columns:max-content minmax(8rem,1fr) minmax(10rem,auto) max-content; padding:.65rem 0; } .action-list li:last-child { border-bottom:0; }
46
+ .sr-only { clip:rect(0 0 0 0); clip-path:inset(50%); height:1px; overflow:hidden; position:absolute; white-space:nowrap; width:1px; }
47
+ a:focus-visible { outline:3px solid var(--cyan); outline-offset:3px; }
48
+ @media (max-width: 720px) { .shell { display:block; } nav { border-bottom:1px solid var(--border); border-right:0; } nav ul { grid-template-columns:repeat(2,minmax(0,1fr)); } main { padding:0 1rem 2rem; } .dashboard-toolbar { margin:0 -1rem; padding:.65rem 1rem; } .dashboard-toolbar form { margin-left:0; order:3; width:100%; } .dashboard-toolbar input { width:100%; } .privacy-body,.closure-actions { align-items:stretch; flex-direction:column; } .diagnostic-grid,.timeline,.evidence-grid,.machine-bento { grid-template-columns:1fr; } .action-list li { grid-template-columns:max-content 1fr; } .action-list code,.action-list button { grid-column:1 / -1; } table,thead,tbody,tr,th,td { display:block; } thead { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); } td { border:0; padding:.25rem 0; } tr { border-bottom:1px solid var(--border); padding:.7rem 0; } tr:last-child { border-bottom:0; } td::before { color:var(--muted); content:attr(data-label) ": "; font-size:.72rem; text-transform:uppercase; } }
49
+ @media (prefers-reduced-motion: reduce) { *,*::before,*::after { animation-duration:.01ms !important; animation-iteration-count:1 !important; scroll-behavior:auto !important; transition-duration:.01ms !important; } }
50
+ @media print { :root { color-scheme:light; } body { background:#fff; color:#000; } .shell { display:block; } nav { display:none; } main { max-width:none; padding:0; } section { break-inside:avoid; border-color:#666; } section > header { background:#eee; } .state,.availability,code { color:#000 !important; } }
51
+ `;
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dashboardSnapshot = dashboardSnapshot;
4
+ const PROJECT_SECTION_IDS = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'];
5
+ /** Safe, deterministic V1 fixture and renderer input. */
6
+ function dashboardSnapshot(overrides = {}) {
7
+ const { project: projectOverride, sections: sectionsOverride, ...rest } = overrides;
8
+ const project = projectOverride ?? { detected: false, label: 'No project detected' };
9
+ const sections = sectionsOverride ?? (project.detected
10
+ ? PROJECT_SECTION_IDS.map((id) => ({ id, availability: id === 'machine' ? 'available' : 'not_applicable', items: [] }))
11
+ : [{ id: 'machine', availability: 'available', items: [] }]);
12
+ return {
13
+ schema: 1,
14
+ generatedAt: '2026-08-22T00:00:00.000Z',
15
+ overall: 'healthy',
16
+ project,
17
+ confidence: 'none',
18
+ sections,
19
+ ...rest,
20
+ };
21
+ }