agentic-workflow-manager 8.3.0 → 8.5.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.
- package/dist/src/commands/doctor.js +40 -1
- package/dist/src/commands/evidence/index.js +95 -0
- package/dist/src/core/dashboard/collect.js +223 -0
- package/dist/src/core/dashboard/plan-state.js +44 -0
- package/dist/src/core/dashboard/render-html.js +88 -0
- package/dist/src/core/dashboard/render-terminal.js +45 -0
- package/dist/src/core/dashboard/sanitize.js +62 -0
- package/dist/src/core/dashboard/styles.js +51 -0
- package/dist/src/core/dashboard/types.js +21 -0
- package/dist/src/core/dashboard/validate.js +106 -0
- package/dist/src/core/dashboard/write-html.js +88 -0
- package/dist/src/core/diagnostics/context.js +1 -1
- package/dist/src/core/evidence/capture.js +46 -0
- package/dist/src/core/evidence/history.js +50 -0
- package/dist/src/core/evidence/store.js +32 -0
- package/dist/src/core/evidence/types.js +97 -0
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/doctor-is-read-only.test.js +54 -1
- package/dist/tests/commands/doctor.test.js +160 -0
- package/dist/tests/core/dashboard/collect.test.js +242 -0
- package/dist/tests/core/dashboard/contracts.test.js +92 -0
- package/dist/tests/core/dashboard/plan-state.test.js +32 -0
- package/dist/tests/core/dashboard/production-adapters.test.js +70 -0
- package/dist/tests/core/dashboard/render-html.test.js +191 -0
- package/dist/tests/core/dashboard/render-terminal.test.js +72 -0
- package/dist/tests/core/dashboard/write-html.test.js +112 -0
- package/dist/tests/core/evidence/capture.test.js +48 -0
- package/dist/tests/core/evidence/command.test.js +37 -0
- package/dist/tests/core/evidence/history.test.js +41 -0
- package/dist/tests/core/evidence/store.test.js +32 -0
- package/dist/tests/core/evidence/types.test.js +30 -0
- package/dist/tests/helpers/dashboard-fixtures.js +66 -0
- package/dist/tests/helpers/evidence-fixtures.js +17 -0
- package/dist/tests/integration/doctor-dashboard.e2e.test.js +177 -0
- package/dist/tests/integration/published-doctor-evidence.e2e.test.js +180 -0
- package/package.json +1 -1
|
@@ -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,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.registerEvidenceCommand = registerEvidenceCommand;
|
|
7
|
+
exports.runEvidenceCapture = runEvidenceCapture;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
const capture_1 = require("../../core/evidence/capture");
|
|
12
|
+
const store_1 = require("../../core/evidence/store");
|
|
13
|
+
const store_2 = require("../../core/journal/store");
|
|
14
|
+
const store_3 = require("../../core/ledger/store");
|
|
15
|
+
function assertRepoRelativePlan(value) {
|
|
16
|
+
if (typeof value !== 'string' || !value || value.startsWith('--') || path_1.default.isAbsolute(value)
|
|
17
|
+
|| value.includes('\\') || value.split('/').some((part) => part === '' || part === '.' || part === '..')) {
|
|
18
|
+
throw new Error('--plan requires a repo-relative path');
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function registerEvidenceCommand(program) {
|
|
23
|
+
const evidence = program.command('evidence').description('durable privacy-preserving cycle observations');
|
|
24
|
+
evidence.command('capture')
|
|
25
|
+
.description('capture one local cycle observation')
|
|
26
|
+
.option('--plan <path>', 'repo-relative plan path')
|
|
27
|
+
.option('--pr-provider <provider>', 'github | gitlab | other')
|
|
28
|
+
.option('--pr-number <number>', 'pull request number')
|
|
29
|
+
.action((opts) => {
|
|
30
|
+
const result = runEvidenceCapture(process.cwd(), opts.plan, { prProvider: opts.prProvider, prNumber: opts.prNumber });
|
|
31
|
+
if (result.code === 0)
|
|
32
|
+
process.stdout.write(result.stdout);
|
|
33
|
+
else {
|
|
34
|
+
process.stderr.write(`awm evidence capture: ${result.error}\n`);
|
|
35
|
+
process.exitCode = 2;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
function firstEvaluationGates(state) {
|
|
40
|
+
return state.cycleVerificationPlan.map((gate) => {
|
|
41
|
+
if (gate.kind === 'review') {
|
|
42
|
+
const verdict = state.verdicts.filter((candidate) => candidate.obligationId === gate.id)
|
|
43
|
+
.sort((left, right) => left.receivedAt.localeCompare(right.receivedAt) || left.id.localeCompare(right.id))[0];
|
|
44
|
+
return { required: true, passed: verdict?.result === 'pass' };
|
|
45
|
+
}
|
|
46
|
+
const evaluated = Object.values(state.jobs).filter((job) => job.satisfies?.includes(gate.id));
|
|
47
|
+
const first = evaluated.filter((job) => job.attemptOf === undefined || !evaluated.some((candidate) => candidate.id === job.attemptOf))
|
|
48
|
+
.sort((left, right) => jobTimestamp(left).localeCompare(jobTimestamp(right)) || left.id.localeCompare(right.id))[0];
|
|
49
|
+
return { required: true, passed: first?.verdict === 'pass' };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function jobTimestamp(job) {
|
|
53
|
+
const timestamp = job.result?.endedAt ?? job.phaseTimestamps.received ?? job.phaseTimestamps.exited;
|
|
54
|
+
if (typeof timestamp !== 'string' || Number.isNaN(Date.parse(timestamp)))
|
|
55
|
+
throw new Error(`gate job ${job.id} lacks a durable evaluation timestamp`);
|
|
56
|
+
return timestamp;
|
|
57
|
+
}
|
|
58
|
+
function repositoryIdentity(root, supplied) {
|
|
59
|
+
if (supplied !== undefined) {
|
|
60
|
+
if (typeof supplied !== 'string' || !supplied || supplied.length > 4096 || /[\r\n]/.test(supplied))
|
|
61
|
+
throw new Error('repository identity is invalid');
|
|
62
|
+
return supplied;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const remote = (0, child_process_1.execFileSync)('git', ['config', '--get', 'remote.origin.url'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 3000 }).trim();
|
|
66
|
+
if (!remote || remote.length > 4096 || /[\r\n]/.test(remote))
|
|
67
|
+
throw new Error('invalid');
|
|
68
|
+
return remote;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
throw new Error('repository identity unavailable: configure remote.origin.url');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function runEvidenceCapture(root, plan, overrides) {
|
|
75
|
+
try {
|
|
76
|
+
const planPath = assertRepoRelativePlan(plan);
|
|
77
|
+
if (!fs_1.default.existsSync(path_1.default.join(root, planPath)))
|
|
78
|
+
throw new Error('--plan must reference an existing file');
|
|
79
|
+
const branch = (0, store_3.detectBranch)(root);
|
|
80
|
+
const read = overrides?.journal === undefined ? (0, store_2.readJournal)(root, branch) : { corrupt: false, state: overrides.journal };
|
|
81
|
+
if (read.corrupt || !read.state)
|
|
82
|
+
throw new Error('current journal is unavailable or corrupt');
|
|
83
|
+
let pr;
|
|
84
|
+
if (overrides?.prProvider !== undefined || overrides?.prNumber !== undefined) {
|
|
85
|
+
if (overrides.prProvider === undefined || overrides.prNumber === undefined || typeof overrides.prNumber !== 'string' || !/^\d+$/.test(overrides.prNumber))
|
|
86
|
+
throw new Error('--pr-provider and --pr-number must be supplied together');
|
|
87
|
+
pr = { provider: overrides.prProvider, number: Number(overrides.prNumber) };
|
|
88
|
+
}
|
|
89
|
+
const saved = (0, store_1.writeCycleEvidence)(root, (0, capture_1.captureCycleEvidence)({ root, repositoryIdentity: repositoryIdentity(root, overrides?.repositoryIdentity), planPath, journal: read.state, gates: firstEvaluationGates(read.state), ledger: overrides?.ledger ?? (0, store_3.listEntries)(root, branch), pr }));
|
|
90
|
+
return { code: 0, stdout: `${saved.cycleId}\n` };
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
return { code: 2, stdout: '', error: error.message };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.REMEDIATION_BY_FINDING_ID = void 0;
|
|
7
|
+
exports.productionDashboardAdapters = productionDashboardAdapters;
|
|
8
|
+
exports.collectDashboardSnapshot = collectDashboardSnapshot;
|
|
9
|
+
const profile_1 = require("../profile");
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const history_1 = require("../evidence/history");
|
|
13
|
+
const types_1 = require("../evidence/types");
|
|
14
|
+
const sanitize_1 = require("./sanitize");
|
|
15
|
+
const validate_1 = require("./validate");
|
|
16
|
+
const plan_state_1 = require("./plan-state");
|
|
17
|
+
exports.REMEDIATION_BY_FINDING_ID = {
|
|
18
|
+
'machine.preferences.missing': 'awm init',
|
|
19
|
+
'machine.registries.stale': 'awm update',
|
|
20
|
+
'project.profile.missing': 'awm init',
|
|
21
|
+
'project.sensors.unavailable': 'awm sensors status',
|
|
22
|
+
'project.preflight.degraded': 'awm preflight',
|
|
23
|
+
'planning.source.unavailable': 'awm preflight',
|
|
24
|
+
'execution.source.unavailable': 'awm sensors status',
|
|
25
|
+
};
|
|
26
|
+
const EMPTY_ADAPTERS = {
|
|
27
|
+
machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined,
|
|
28
|
+
};
|
|
29
|
+
const SAFE_REMEDIATIONS = new Set(['awm init', 'awm update', 'awm sync', 'awm sensors status', 'awm preflight']);
|
|
30
|
+
const HEALTHY_PROVIDER_STATES = new Set(['supported', 'healthy', 'shared', 'delivered']);
|
|
31
|
+
const INAPPLICABLE_PROVIDER_STATES = new Set(['unsupported']);
|
|
32
|
+
function providerState(state) {
|
|
33
|
+
if (HEALTHY_PROVIDER_STATES.has(state))
|
|
34
|
+
return 'ok';
|
|
35
|
+
if (INAPPLICABLE_PROVIDER_STATES.has(state))
|
|
36
|
+
return 'not_applicable';
|
|
37
|
+
return 'attention';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Maps the already-gathered diagnostics matrix into the snapshot's read-only
|
|
41
|
+
* source seam. It deliberately does not re-read providers, execute sensors, or
|
|
42
|
+
* relay provider detail/remediation prose: those values can contain local paths,
|
|
43
|
+
* command output, or credentials. The fixed IDs are derived solely from the
|
|
44
|
+
* provider/check enums and therefore remain stable across runs.
|
|
45
|
+
*/
|
|
46
|
+
function productionDashboardAdapters(context) {
|
|
47
|
+
if (!context || typeof context !== 'object' || !Array.isArray(context.providers))
|
|
48
|
+
throw new Error('productionDashboardAdapters requires gathered provider diagnostics');
|
|
49
|
+
const machineFindings = context.providers.flatMap((provider) => provider.checks.flatMap((check) => {
|
|
50
|
+
const finding = {
|
|
51
|
+
id: `machine.provider.${provider.id}.${check.id}`,
|
|
52
|
+
// Provider labels are configuration prose. The provider id and check id
|
|
53
|
+
// are enum-controlled and sufficient for a stable public observation.
|
|
54
|
+
label: `Provider ${provider.id}: ${check.id}`,
|
|
55
|
+
state: providerState(check.state),
|
|
56
|
+
// Provider remediation is intentionally not forwarded. It is free-form
|
|
57
|
+
// diagnostics text, not a dashboard-approved canonical command.
|
|
58
|
+
remediationVerified: false,
|
|
59
|
+
};
|
|
60
|
+
// These two legacy diagnosis states are the only provider observations
|
|
61
|
+
// with a pre-existing, exact dashboard command mapping.
|
|
62
|
+
if (check.id === 'skills.global' && check.state === 'absent') {
|
|
63
|
+
return [finding, { id: 'machine.preferences.missing', label: 'Preferences', state: 'missing', remediationVerified: true }];
|
|
64
|
+
}
|
|
65
|
+
if (check.id === 'skills.global' && check.state === 'stale') {
|
|
66
|
+
return [finding, { id: 'machine.registries.stale', label: 'Registries', state: 'attention', remediationVerified: true }];
|
|
67
|
+
}
|
|
68
|
+
return [finding];
|
|
69
|
+
}));
|
|
70
|
+
const project = context.project;
|
|
71
|
+
const projectFindings = !project ? [] : [
|
|
72
|
+
{ id: project.profile.present ? 'project.profile.present' : 'project.profile.missing', label: 'Profile', state: project.profile.present ? 'ok' : 'missing', remediation: 'awm init', remediationVerified: true },
|
|
73
|
+
{ id: 'project.extensions.configured', label: 'Extensions', state: project.profile.extensions.length > 0 ? 'ok' : 'not_applicable' },
|
|
74
|
+
{ id: 'project.registry-pins.present', label: 'Registry pins', state: project.profile.registries && Object.keys(project.profile.registries).length > 0 ? 'ok' : 'not_applicable' },
|
|
75
|
+
{ id: 'project.bundles.coherent', label: 'Active bundles', state: project.activeBundles.broken.length === 0 ? 'ok' : 'attention', remediation: 'awm sync', remediationVerified: true },
|
|
76
|
+
{ id: 'project.context.present', label: 'Project context', state: project.context.present ? 'ok' : 'missing', remediation: 'awm init', remediationVerified: true },
|
|
77
|
+
{ id: 'project.constitution.present', label: 'Constitution', state: project.constitution.present ? 'ok' : 'missing' },
|
|
78
|
+
{ id: project.sensors.present ? 'project.sensors.present' : 'project.sensors.unavailable', label: 'Sensors', state: project.sensors.present ? 'ok' : 'unavailable', remediation: 'awm sensors status', remediationVerified: true },
|
|
79
|
+
// `preflight()` is async because static tool inspection is async. Doctor's
|
|
80
|
+
// synchronous legacy API must not dispatch it here; make the absence of that
|
|
81
|
+
// observation explicit rather than inventing a readiness verdict.
|
|
82
|
+
{ id: 'project.preflight.not_collected', label: 'Static preflight', state: 'not_applicable' },
|
|
83
|
+
];
|
|
84
|
+
return {
|
|
85
|
+
machine: () => ({ findings: machineFindings }),
|
|
86
|
+
project: () => ({ label: 'Project detected', findings: projectFindings }),
|
|
87
|
+
plans: () => [],
|
|
88
|
+
execution: () => undefined,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function findings(items, optional = false) {
|
|
92
|
+
if (optional && items === undefined)
|
|
93
|
+
return [];
|
|
94
|
+
if (!Array.isArray(items))
|
|
95
|
+
throw new Error('Dashboard findings must be an array');
|
|
96
|
+
return items.flatMap((item) => {
|
|
97
|
+
if (!item || typeof item !== 'object' || typeof item.id !== 'string' || item.id.trim() === '' || typeof item.label !== 'string' || item.label.trim() === '')
|
|
98
|
+
throw new Error('Dashboard finding is invalid');
|
|
99
|
+
if (!['ok', 'attention', 'missing', 'unavailable', 'not_applicable'].includes(item.state))
|
|
100
|
+
throw new Error('Dashboard finding state is invalid');
|
|
101
|
+
const remediation = exports.REMEDIATION_BY_FINDING_ID[item.id]
|
|
102
|
+
?? (item.remediationVerified === true && typeof item.remediation === 'string' && SAFE_REMEDIATIONS.has(item.remediation) ? item.remediation : undefined);
|
|
103
|
+
if (item.state !== 'ok' && item.state !== 'not_applicable' && !remediation)
|
|
104
|
+
return [];
|
|
105
|
+
return [{ id: item.id, label: item.label, state: item.state, ...(item.detail ? { detail: item.detail } : {}), ...(remediation ? { remediation } : {}) }];
|
|
106
|
+
}).sort((left, right) => left.id.localeCompare(right.id));
|
|
107
|
+
}
|
|
108
|
+
function section(id, availability, items = []) {
|
|
109
|
+
return { id, availability, items };
|
|
110
|
+
}
|
|
111
|
+
function optional(source) {
|
|
112
|
+
try {
|
|
113
|
+
return { value: source(), failed: false, failure: {} };
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const findingId = error && typeof error === 'object' && typeof error.findingId === 'string'
|
|
117
|
+
? error.findingId : undefined;
|
|
118
|
+
const remediationVerified = error !== null && typeof error === 'object'
|
|
119
|
+
? error.remediationVerified === true : false;
|
|
120
|
+
return { failed: true, failure: { findingId, remediationVerified } };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function canonicalOptionalFailure(failure) {
|
|
124
|
+
if (!failure.remediationVerified || !failure.findingId || !Object.hasOwn(exports.REMEDIATION_BY_FINDING_ID, failure.findingId))
|
|
125
|
+
return [];
|
|
126
|
+
return [{ id: failure.findingId, label: 'Optional source unavailable', state: 'unavailable', remediation: exports.REMEDIATION_BY_FINDING_ID[failure.findingId] }];
|
|
127
|
+
}
|
|
128
|
+
function evidenceHistoryItems(root) {
|
|
129
|
+
const awmDirectory = path_1.default.join(root, '.awm');
|
|
130
|
+
const evidenceDirectory = path_1.default.join(awmDirectory, 'evidence');
|
|
131
|
+
const directory = path_1.default.join(evidenceDirectory, 'cycles');
|
|
132
|
+
for (const ancestor of [awmDirectory, evidenceDirectory, directory]) {
|
|
133
|
+
let stat;
|
|
134
|
+
try {
|
|
135
|
+
stat = fs_1.default.lstatSync(ancestor);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
if (error && typeof error === 'object' && error.code === 'ENOENT')
|
|
139
|
+
return { confidence: 'none', items: [] };
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
143
|
+
throw new Error('evidence history directory is unsafe');
|
|
144
|
+
}
|
|
145
|
+
const seenCycleIds = new Set();
|
|
146
|
+
const records = fs_1.default.readdirSync(directory, { withFileTypes: true })
|
|
147
|
+
.sort((left, right) => left.name.localeCompare(right.name))
|
|
148
|
+
.map((entry) => {
|
|
149
|
+
if (!entry.isFile() || !entry.name.endsWith('.json'))
|
|
150
|
+
throw new Error('evidence history contains an unsupported entry');
|
|
151
|
+
const file = path_1.default.join(directory, entry.name);
|
|
152
|
+
if (fs_1.default.lstatSync(file).isSymbolicLink())
|
|
153
|
+
throw new Error('evidence history file is unsafe');
|
|
154
|
+
const evidence = (0, types_1.validateCycleEvidence)(JSON.parse(fs_1.default.readFileSync(file, 'utf8')));
|
|
155
|
+
if (entry.name !== `${evidence.cycleId}.json` || seenCycleIds.has(evidence.cycleId))
|
|
156
|
+
throw new Error('evidence history filename is invalid');
|
|
157
|
+
seenCycleIds.add(evidence.cycleId);
|
|
158
|
+
return evidence;
|
|
159
|
+
});
|
|
160
|
+
const history = (0, history_1.buildEvidenceHistory)(records);
|
|
161
|
+
return {
|
|
162
|
+
confidence: history.confidence,
|
|
163
|
+
items: history.cycles.map((cycle) => {
|
|
164
|
+
const cures = cycle.cureEfficacy.length === 0 ? 'none' : cycle.cureEfficacy.map((cure) => cure.efficacy).join(', ');
|
|
165
|
+
return {
|
|
166
|
+
id: `history.cycle.${cycle.cycleId}`,
|
|
167
|
+
label: `Cycle ${cycle.cycleId.slice(0, 12)}`,
|
|
168
|
+
state: cycle.cycleState === 'blocked' ? 'attention' : 'ok',
|
|
169
|
+
detail: `plan ${cycle.plan.state}; tasks ${cycle.tasks.length}; retries ${cycle.retries}; QA ${cycle.qa.findings}/${cycle.qa.fixes}; first-pass ${cycle.gates.firstPass ? 'yes' : 'no'}; cures ${cures}`,
|
|
170
|
+
};
|
|
171
|
+
}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function isolatedFindings(items) {
|
|
175
|
+
return optional(() => findings(items, true));
|
|
176
|
+
}
|
|
177
|
+
/** Pure read-only aggregation over injected source adapters. */
|
|
178
|
+
function collectDashboardSnapshot(options) {
|
|
179
|
+
if (!options || typeof options.cwd !== 'string' || options.cwd.length === 0 || typeof options.now !== 'string' || Number.isNaN(Date.parse(options.now)))
|
|
180
|
+
throw new Error('collectDashboardSnapshot requires cwd and valid now');
|
|
181
|
+
const adapters = { ...EMPTY_ADAPTERS, ...(options.adapters ?? {}) };
|
|
182
|
+
const machine = adapters.machine({ cwd: options.cwd });
|
|
183
|
+
if (!Array.isArray(machine?.findings))
|
|
184
|
+
throw new Error('Dashboard findings must be an array');
|
|
185
|
+
const root = (0, profile_1.findProjectRoot)(options.cwd);
|
|
186
|
+
const machineItems = findings((0, sanitize_1.sanitizeDashboardSource)(machine).findings);
|
|
187
|
+
const machineSection = section('machine', 'available', machineItems);
|
|
188
|
+
if (!root) {
|
|
189
|
+
const degraded = machineSection.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable');
|
|
190
|
+
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] });
|
|
191
|
+
}
|
|
192
|
+
const projectResult = optional(() => (0, sanitize_1.sanitizeDashboardSource)(adapters.project({ root })));
|
|
193
|
+
const plansResult = optional(() => (0, sanitize_1.sanitizeDashboardSource)(adapters.plans({ root })));
|
|
194
|
+
const executionResult = optional(() => {
|
|
195
|
+
const source = adapters.execution({ root });
|
|
196
|
+
return source === undefined ? undefined : (0, sanitize_1.sanitizeDashboardSource)(source);
|
|
197
|
+
});
|
|
198
|
+
const projectSource = projectResult.value;
|
|
199
|
+
const execution = executionResult.value;
|
|
200
|
+
const projectItemsResult = projectSource ? isolatedFindings(projectSource.findings) : { value: [], failed: false, failure: {} };
|
|
201
|
+
const planItemsResult = plansResult.value ? optional(() => findings(plansResult.value.map((plan) => plan.lifecycle
|
|
202
|
+
? { ...plan, detail: (0, plan_state_1.classifyPlanState)(plan.lifecycle) } : plan))) : { value: [], failed: false, failure: {} };
|
|
203
|
+
const executionItems = isolatedFindings(execution?.execution);
|
|
204
|
+
const qaItems = isolatedFindings(execution?.qa);
|
|
205
|
+
const retroItems = isolatedFindings(execution?.retro);
|
|
206
|
+
const evidenceResult = optional(() => evidenceHistoryItems(root));
|
|
207
|
+
const executionUnavailable = !executionResult.failed && execution === undefined;
|
|
208
|
+
const sections = [
|
|
209
|
+
machineSection,
|
|
210
|
+
section('project', projectResult.failed || projectItemsResult.failed ? 'unavailable' : 'available', projectResult.failed
|
|
211
|
+
? canonicalOptionalFailure(projectResult.failure) : projectItemsResult.failed ? [] : projectItemsResult.value),
|
|
212
|
+
section('planning', plansResult.failed || planItemsResult.failed ? 'unavailable' : 'available', plansResult.failed
|
|
213
|
+
? canonicalOptionalFailure(plansResult.failure) : planItemsResult.failed ? [] : planItemsResult.value),
|
|
214
|
+
section('execution', executionResult.failed || executionUnavailable || executionItems.failed ? 'unavailable' : 'available', executionResult.failed ? canonicalOptionalFailure(executionResult.failure) : executionUnavailable ? canonicalOptionalFailure({ findingId: 'execution.source.unavailable', remediationVerified: true }) : executionItems.value),
|
|
215
|
+
// There is no read-only QA, retro, or history adapter in Release A. An
|
|
216
|
+
// absent execution source is not evidence of a successful empty cycle.
|
|
217
|
+
section('qa', executionResult.failed || executionUnavailable || qaItems.failed ? 'unavailable' : 'available', qaItems.value),
|
|
218
|
+
section('retro', executionResult.failed || executionUnavailable || retroItems.failed ? 'unavailable' : 'available', retroItems.value),
|
|
219
|
+
section('history', evidenceResult.failed ? 'unavailable' : 'available', evidenceResult.failed ? [] : evidenceResult.value.items),
|
|
220
|
+
];
|
|
221
|
+
const degraded = sections.some((entry) => entry.availability === 'unavailable' || entry.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable'));
|
|
222
|
+
return (0, validate_1.validateDashboardSnapshotV1)({ schema: 1, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: true, label: projectSource?.label || 'Project detected' }, confidence: evidenceResult.failed ? 'none' : evidenceResult.value.confidence, sections });
|
|
223
|
+
}
|
|
@@ -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,88 @@
|
|
|
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); }
|
|
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 & 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(snapshot) {
|
|
49
|
+
const planning = snapshot.sections.find((section) => section.id === 'planning')?.items ?? [];
|
|
50
|
+
const history = snapshot.sections.find((section) => section.id === 'history')?.items ?? [];
|
|
51
|
+
const planCards = planning.length === 0
|
|
52
|
+
? `<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>`
|
|
53
|
+
: planning.map((plan) => `<div class="plan-card ${plan.detail === 'blocked' ? 'blocked' : 'active'}" data-plan-card="${plan.detail === 'blocked' ? 'blocked' : 'active'}"><div><strong>${escapeHtml(plan.label)}</strong><span class="state ${plan.state}">${STATE_GLYPH[plan.state]} ${STATE_TEXT[plan.state]}</span></div><p>Estado: ${escapeHtml(plan.detail ?? 'sin observación')}</p><button type="button" disabled aria-describedby="static-controls-note">Ver plan (estático)</button></div>`).join('');
|
|
54
|
+
const evidenceRows = history.length === 0 ? '<tr><td colspan="4" class="empty">No hay evidencia de impacto disponible en este snapshot.</td></tr>' : history.map((item) => `<tr><td>ciclo</td><td>${escapeHtml(item.label)}</td><td><span class="state ${item.state}">${STATE_GLYPH[item.state]} ${STATE_TEXT[item.state]}</span><br>${escapeHtml(item.detail ?? '—')}</td><td>—</td></tr>`).join('');
|
|
55
|
+
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>${planCards}</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>${evidenceRows}</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>`;
|
|
56
|
+
}
|
|
57
|
+
function projectComposition(snapshot) {
|
|
58
|
+
const stageSections = ['planning', 'execution', 'qa', 'retro', 'history'];
|
|
59
|
+
const byId = new Map(snapshot.sections.map((section) => [section.id, section]));
|
|
60
|
+
const stages = stageSections.map((id) => {
|
|
61
|
+
const section = byId.get(id);
|
|
62
|
+
const stage = id === 'history' ? 'evidence' : id;
|
|
63
|
+
const available = section?.availability === 'available';
|
|
64
|
+
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>`;
|
|
65
|
+
}).join('');
|
|
66
|
+
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>' : '';
|
|
67
|
+
const prepItems = byId.get('machine')?.items ?? [];
|
|
68
|
+
const prepNames = ['installation', 'sensors', 'persistence'];
|
|
69
|
+
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>`;
|
|
70
|
+
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}`;
|
|
71
|
+
const historySupplement = projectEvidenceComposition(snapshot);
|
|
72
|
+
return snapshot.sections.map((section) => sectionHtml(section, section.id === 'machine' ? machineSupplement : section.id === 'history' ? historySupplement : '')).join('');
|
|
73
|
+
}
|
|
74
|
+
/** Renders a portable, static, share-safe dashboard document. */
|
|
75
|
+
function renderDashboardHtml(input) {
|
|
76
|
+
const snapshot = (0, validate_1.validateDashboardSnapshotV1)(input);
|
|
77
|
+
const overall = escapeHtml(snapshot.overall);
|
|
78
|
+
const project = escapeHtml(snapshot.project.label);
|
|
79
|
+
const machineItems = snapshot.sections.find((section) => section.id === 'machine')?.items ?? [];
|
|
80
|
+
const sections = snapshot.project.detected ? projectComposition(snapshot) : `${machineBento(machineItems)}${privacyAndActions(snapshot)}${snapshot.sections.map((section) => sectionHtml(section)).join('')}`;
|
|
81
|
+
const links = '<li>Inicio</li><li>Estado</li><li class="active">Configuración</li><li>Terminal</li>';
|
|
82
|
+
const projectDetected = snapshot.project.detected;
|
|
83
|
+
const heading = projectDetected ? 'Project lifecycle' : 'Machine configuration';
|
|
84
|
+
const intro = projectDetected ? 'Readiness, lifecycle state, and eligible observations presented directly for operator review.' : 'Machine readiness and safe configuration state outside a project.';
|
|
85
|
+
const navLabel = projectDetected ? 'Dashboard sections' : 'Machine configuration sections';
|
|
86
|
+
const context = projectDetected ? `Project: ${project}` : 'No project detected';
|
|
87
|
+
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`;
|
|
88
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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.id === 'history')
|
|
32
|
+
lines.push(` Eligible evidence rows: ${section.items.length}`);
|
|
33
|
+
if (section.availability !== 'available')
|
|
34
|
+
lines.push(` ⊘ source ${section.availability.replace('_', ' ')}`);
|
|
35
|
+
if (section.items.length === 0)
|
|
36
|
+
lines.push(' No observations reported.');
|
|
37
|
+
for (const item of section.items) {
|
|
38
|
+
const presentation = STATE_PRESENTATION[item.state];
|
|
39
|
+
lines.push(` ${presentation.glyph} ${item.label} [${item.id}]${item.detail ? ` — ${item.detail}` : ` — ${presentation.text}`}`);
|
|
40
|
+
if (item.remediation)
|
|
41
|
+
lines.push(` → ${item.remediation}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|