agentic-workflow-manager 8.5.2 → 9.0.1
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/evidence/index.js +15 -4
- package/dist/src/core/dashboard/collect.js +26 -11
- package/dist/src/core/dashboard/plan-state.js +5 -3
- package/dist/src/core/dashboard/render-html.js +2 -2
- package/dist/src/core/dashboard/render-terminal.js +2 -0
- package/dist/src/core/dashboard/sanitize.js +2 -2
- package/dist/src/core/dashboard/types.js +2 -2
- package/dist/src/core/dashboard/validate.js +3 -3
- package/dist/tests/core/dashboard/collect.test.js +37 -9
- package/dist/tests/core/dashboard/contracts.test.js +6 -4
- package/dist/tests/core/dashboard/plan-state.test.js +43 -5
- package/dist/tests/core/dashboard/production-adapters.test.js +18 -0
- package/dist/tests/core/dashboard/render-html.test.js +8 -3
- package/dist/tests/core/dashboard/render-terminal.test.js +5 -1
- package/dist/tests/core/evidence/command.test.js +52 -0
- package/dist/tests/structural/documentation-phase-is-mechanized.test.js +29 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +8 -3
- package/package.json +1 -1
|
@@ -28,12 +28,19 @@ function currentRelease(lines) {
|
|
|
28
28
|
return releases.length > 1 ? releases.at(-1) : undefined;
|
|
29
29
|
}
|
|
30
30
|
function marker(lines, name, release) {
|
|
31
|
-
|
|
31
|
+
// Split into two non-overlapping alternatives (no-colon vs colon) so no `\s*`
|
|
32
|
+
// ever sits directly adjacent to the lazy `[^\r\n]*?` group — that adjacency
|
|
33
|
+
// is what turns a long unterminated marker line into catastrophic backtracking.
|
|
34
|
+
const expression = new RegExp(`^\\s*<!--\\s*${name}(?:\\s*-->\\s*$|\\s*:[^\\r\\n]*?-->\\s*$)`);
|
|
32
35
|
if (release === undefined)
|
|
33
36
|
return lines.some((line) => expression.test(line));
|
|
34
37
|
const releaseExpression = new RegExp(`\\bRelease\\s+${release.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}\\b`, 'i');
|
|
35
38
|
return lines.some((line) => expression.test(line) && releaseExpression.test(line));
|
|
36
39
|
}
|
|
40
|
+
/** `docs_pending` es un estado vivo del dashboard; el registro durable no lo conoce (schema 1). */
|
|
41
|
+
function forEvidence(state) {
|
|
42
|
+
return state === 'docs_pending' ? 'retro_pending' : state;
|
|
43
|
+
}
|
|
37
44
|
/** Reads only structural lifecycle syntax; plan prose never crosses into evidence. */
|
|
38
45
|
function planState(root, planPath, journal) {
|
|
39
46
|
const file = path_1.default.join(root, planPath);
|
|
@@ -78,11 +85,15 @@ function planState(root, planPath, journal) {
|
|
|
78
85
|
if (status !== 'IN_PROGRESS' && status !== 'COMPLETE' && status !== 'BLOCKED')
|
|
79
86
|
throw new Error('journal cycle status is invalid');
|
|
80
87
|
const release = currentRelease(visibleLines);
|
|
81
|
-
return (0, plan_state_1.classifyPlanState)({
|
|
88
|
+
return forEvidence((0, plan_state_1.classifyPlanState)({
|
|
82
89
|
...(status === 'IN_PROGRESS' ? { journal: { state: 'active' } } : status === 'BLOCKED' ? { journal: { state: 'blocked' } } : {}),
|
|
83
|
-
markers: {
|
|
90
|
+
markers: {
|
|
91
|
+
qaComplete: marker(visibleLines, 'awm-qa-complete', release),
|
|
92
|
+
docsComplete: marker(visibleLines, 'awm-docs-complete', release),
|
|
93
|
+
retroComplete: marker(visibleLines, 'awm-retro-complete', release),
|
|
94
|
+
},
|
|
84
95
|
tasks: { total, completed },
|
|
85
|
-
});
|
|
96
|
+
}));
|
|
86
97
|
}
|
|
87
98
|
function registerEvidenceCommand(program) {
|
|
88
99
|
const evidence = program.command('evidence').description('durable privacy-preserving cycle observations');
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.REMEDIATION_BY_FINDING_ID = void 0;
|
|
7
7
|
exports.productionDashboardAdapters = productionDashboardAdapters;
|
|
8
|
+
exports.lifecycleForCycle = lifecycleForCycle;
|
|
8
9
|
exports.collectDashboardSnapshot = collectDashboardSnapshot;
|
|
9
10
|
const profile_1 = require("../profile");
|
|
10
11
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -122,6 +123,7 @@ function productionDashboardAdapters(context) {
|
|
|
122
123
|
return {
|
|
123
124
|
execution: history.cycles.map((cycle) => cycleFinding('execution', 'Static preflight', cycle, cycle.cycleState === 'blocked')),
|
|
124
125
|
qa: history.cycles.map((cycle) => cycleFinding('qa', 'Sensors', cycle, cycle.qa.fixes < cycle.qa.findings)),
|
|
126
|
+
docs: history.cycles.map((cycle) => cycleFinding('docs', 'Documentation', cycle, !lifecycleForCycle(cycle).markers.docsComplete)),
|
|
125
127
|
retro: history.cycles.map((cycle) => cycleFinding('retro', 'Project context', cycle, cycle.plan.state !== 'executed')),
|
|
126
128
|
};
|
|
127
129
|
},
|
|
@@ -144,25 +146,32 @@ function journalExecutionFinding(overlay) {
|
|
|
144
146
|
id: 'execution.current-journal', label: 'Static preflight', state: blocked ? 'attention' : 'ok',
|
|
145
147
|
...(blocked ? { remediation: BLOCKED_CYCLE_REMEDIATION, remediationVerified: true } : {}),
|
|
146
148
|
}],
|
|
147
|
-
qa: [], retro: [],
|
|
149
|
+
qa: [], docs: [], retro: [],
|
|
148
150
|
};
|
|
149
151
|
}
|
|
150
152
|
function lifecycleForJournal(overlay) {
|
|
151
|
-
return { journal: { state: overlay.state }, markers: { qaComplete: false, retroComplete: false }, tasks: overlay.tasks };
|
|
153
|
+
return { journal: { state: overlay.state }, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: overlay.tasks };
|
|
152
154
|
}
|
|
155
|
+
/** Reconstructs a PlanStateInput (markers + tasks) from a stored historical
|
|
156
|
+
* evidence record's plan.state for display purposes — the inverse of the
|
|
157
|
+
* live→durable direction. `retro_pending` predates the docs phase: that
|
|
158
|
+
* evidence was written before `docsComplete` existed, so treating it as
|
|
159
|
+
* docs-done is the only non-regressive reading; reconstructing it as
|
|
160
|
+
* `docsComplete: false` would invent retroactive docs debt on every
|
|
161
|
+
* historical cycle. */
|
|
153
162
|
function lifecycleForCycle(cycle) {
|
|
154
163
|
const total = cycle.tasks.length;
|
|
155
164
|
if (cycle.plan.state === 'blocked' || cycle.cycleState === 'blocked')
|
|
156
|
-
return { journal: { state: 'blocked' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
|
|
165
|
+
return { journal: { state: 'blocked' }, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
|
|
157
166
|
if (cycle.plan.state === 'active')
|
|
158
|
-
return { journal: { state: 'active' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
|
|
167
|
+
return { journal: { state: 'active' }, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total, completed: 0 } };
|
|
159
168
|
if (cycle.plan.state === 'executed')
|
|
160
|
-
return { markers: { qaComplete: true, retroComplete: true }, tasks: { total, completed: total } };
|
|
169
|
+
return { markers: { qaComplete: true, docsComplete: true, retroComplete: true }, tasks: { total, completed: total } };
|
|
161
170
|
if (cycle.plan.state === 'retro_pending')
|
|
162
|
-
return { markers: { qaComplete: true, retroComplete: false }, tasks: { total, completed: total } };
|
|
171
|
+
return { markers: { qaComplete: true, docsComplete: true, retroComplete: false }, tasks: { total, completed: total } };
|
|
163
172
|
if (cycle.plan.state === 'qa_pending')
|
|
164
|
-
return { markers: { qaComplete: false, retroComplete: false }, tasks: { total, completed: total } };
|
|
165
|
-
return { markers: { qaComplete: false, retroComplete: false }, tasks: { total: 0, completed: 0 } };
|
|
173
|
+
return { markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total, completed: total } };
|
|
174
|
+
return { markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 0, completed: 0 } };
|
|
166
175
|
}
|
|
167
176
|
function cycleFinding(section, label, cycle, actionable) {
|
|
168
177
|
return {
|
|
@@ -330,7 +339,7 @@ function collectDashboardSnapshot(options) {
|
|
|
330
339
|
const machineSection = section('machine', 'available', machineItems);
|
|
331
340
|
if (!root) {
|
|
332
341
|
const degraded = machineSection.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable');
|
|
333
|
-
return (0, validate_1.validateDashboardSnapshotV1)({ schema:
|
|
342
|
+
return (0, validate_1.validateDashboardSnapshotV1)({ schema: 2, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: false, label: 'No project detected' }, confidence: 'none', sections: [machineSection] });
|
|
334
343
|
}
|
|
335
344
|
const projectResult = optional(() => (0, sanitize_1.sanitizeDashboardSource)(adapters.project({ root })));
|
|
336
345
|
const plansResult = optional(() => (0, sanitize_1.sanitizeDashboardSource)(adapters.plans({ root })));
|
|
@@ -345,6 +354,7 @@ function collectDashboardSnapshot(options) {
|
|
|
345
354
|
? { ...plan, detail: (0, plan_state_1.classifyPlanState)(plan.lifecycle) } : plan))) : { value: [], failed: false, failure: {} };
|
|
346
355
|
const executionItems = isolatedFindings(execution?.execution);
|
|
347
356
|
const qaItems = isolatedFindings(execution?.qa);
|
|
357
|
+
const docsItems = isolatedFindings(execution?.docs);
|
|
348
358
|
const retroItems = isolatedFindings(execution?.retro);
|
|
349
359
|
const evidenceResult = optional(() => evidenceHistoryItems(root));
|
|
350
360
|
const executionUnavailable = !executionResult.failed && execution === undefined;
|
|
@@ -355,12 +365,17 @@ function collectDashboardSnapshot(options) {
|
|
|
355
365
|
section('planning', plansResult.failed || planItemsResult.failed ? 'unavailable' : 'available', plansResult.failed
|
|
356
366
|
? canonicalOptionalFailure(plansResult.failure) : planItemsResult.failed ? [] : planItemsResult.value),
|
|
357
367
|
section('execution', executionResult.failed || executionUnavailable || executionItems.failed ? 'unavailable' : 'available', executionResult.failed ? canonicalOptionalFailure(executionResult.failure) : executionUnavailable ? canonicalOptionalFailure({ findingId: 'execution.source.unavailable', remediationVerified: true }) : executionItems.value),
|
|
358
|
-
// There is no read-only QA, retro, or history adapter in Release A. An
|
|
368
|
+
// There is no read-only QA, docs, retro, or history adapter in Release A. An
|
|
359
369
|
// absent execution source is not evidence of a successful empty cycle.
|
|
360
370
|
section('qa', executionResult.failed || executionUnavailable || qaItems.failed ? 'unavailable' : 'available', qaItems.value),
|
|
371
|
+
section('docs', executionResult.failed || executionUnavailable || docsItems.failed ? 'unavailable' : 'available', docsItems.value),
|
|
361
372
|
section('retro', executionResult.failed || executionUnavailable || retroItems.failed ? 'unavailable' : 'available', retroItems.value),
|
|
362
373
|
section('history', evidenceResult.failed ? 'unavailable' : 'available', evidenceResult.failed ? [] : evidenceResult.value.items),
|
|
374
|
+
// `processes` is a reserved section id with no adapter yet: a later
|
|
375
|
+
// release (R1) populates it. `not_applicable` here means exactly
|
|
376
|
+
// "this section doesn't apply here", not dead code.
|
|
377
|
+
section('processes', 'not_applicable', []),
|
|
363
378
|
];
|
|
364
379
|
const degraded = sections.some((entry) => entry.availability === 'unavailable' || entry.items.some((item) => item.state !== 'ok' && item.state !== 'not_applicable'));
|
|
365
|
-
return (0, validate_1.validateDashboardSnapshotV1)({ schema:
|
|
380
|
+
return (0, validate_1.validateDashboardSnapshotV1)({ schema: 2, generatedAt: options.now, overall: degraded ? 'degraded' : 'healthy', project: { detected: true, label: projectSource?.label || 'Project detected' }, confidence: evidenceResult.failed ? 'none' : evidenceResult.value.confidence, sections });
|
|
366
381
|
}
|
|
@@ -13,12 +13,12 @@ function classifyPlanState(input) {
|
|
|
13
13
|
assertRecord(input, 'state input');
|
|
14
14
|
assertKeys(input, 'state input', ['journal', 'markers', 'tasks']);
|
|
15
15
|
assertRecord(input.markers, 'markers');
|
|
16
|
-
assertKeys(input.markers, 'markers', ['qaComplete', 'retroComplete']);
|
|
16
|
+
assertKeys(input.markers, 'markers', ['qaComplete', 'docsComplete', 'retroComplete']);
|
|
17
17
|
assertRecord(input.tasks, 'tasks');
|
|
18
18
|
assertKeys(input.tasks, 'tasks', ['total', 'completed']);
|
|
19
19
|
const markers = input.markers;
|
|
20
20
|
const tasks = input.tasks;
|
|
21
|
-
if (typeof markers.qaComplete !== 'boolean' || typeof markers.retroComplete !== 'boolean')
|
|
21
|
+
if (typeof markers.qaComplete !== 'boolean' || typeof markers.docsComplete !== 'boolean' || typeof markers.retroComplete !== 'boolean')
|
|
22
22
|
throw new Error('Plan markers must be boolean');
|
|
23
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
24
|
throw new Error('Plan task counts are invalid');
|
|
@@ -36,8 +36,10 @@ function classifyPlanState(input) {
|
|
|
36
36
|
return 'active';
|
|
37
37
|
if (markers.retroComplete)
|
|
38
38
|
return 'executed';
|
|
39
|
-
if (markers.
|
|
39
|
+
if (markers.docsComplete)
|
|
40
40
|
return 'retro_pending';
|
|
41
|
+
if (markers.qaComplete)
|
|
42
|
+
return 'docs_pending';
|
|
41
43
|
if (tasks.total > 0 && tasks.completed === tasks.total)
|
|
42
44
|
return 'qa_pending';
|
|
43
45
|
return 'legacy_unverifiable';
|
|
@@ -4,7 +4,7 @@ exports.renderDashboardHtml = renderDashboardHtml;
|
|
|
4
4
|
const styles_1 = require("./styles");
|
|
5
5
|
const validate_1 = require("./validate");
|
|
6
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' };
|
|
7
|
+
const SECTION_TITLES = { machine: 'Machine / install', project: 'Project readiness', planning: 'Design / planning', execution: 'Execution', qa: 'QA', docs: 'Docs', retro: 'Retro', history: 'Final / history', processes: 'Processes' };
|
|
8
8
|
const STATE_TEXT = { ok: 'OK', attention: 'Attention', missing: 'Missing', unavailable: 'Unavailable', not_applicable: 'Not applicable' };
|
|
9
9
|
const STATE_GLYPH = { ok: '●', attention: '▲', missing: '×', unavailable: '⊘', not_applicable: '—' };
|
|
10
10
|
function escapeHtml(value) { return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); }
|
|
@@ -55,7 +55,7 @@ function projectEvidenceComposition(snapshot) {
|
|
|
55
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
56
|
}
|
|
57
57
|
function projectComposition(snapshot) {
|
|
58
|
-
const stageSections = ['planning', 'execution', 'qa', 'retro', 'history'];
|
|
58
|
+
const stageSections = ['planning', 'execution', 'qa', 'docs', 'retro', 'history'];
|
|
59
59
|
const byId = new Map(snapshot.sections.map((section) => [section.id, section]));
|
|
60
60
|
const stages = stageSections.map((id) => {
|
|
61
61
|
const section = byId.get(id);
|
|
@@ -8,8 +8,10 @@ const SECTION_TITLES = {
|
|
|
8
8
|
planning: 'Design / planning',
|
|
9
9
|
execution: 'Execution',
|
|
10
10
|
qa: 'QA',
|
|
11
|
+
docs: 'Docs',
|
|
11
12
|
retro: 'Retro',
|
|
12
13
|
history: 'Final / history',
|
|
14
|
+
processes: 'Processes',
|
|
13
15
|
};
|
|
14
16
|
const STATE_PRESENTATION = {
|
|
15
17
|
ok: { glyph: '✔', text: 'ok' },
|
|
@@ -3,10 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.sanitizeDashboardSource = sanitizeDashboardSource;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
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']);
|
|
6
|
+
const ALLOWED_KEYS = new Set(['findings', 'label', 'id', 'state', 'detail', 'remediation', 'remediationVerified', 'execution', 'qa', 'docs', 'retro', 'history', 'lifecycle', 'journal', 'markers', 'tasks', 'total', 'completed', 'qaComplete', 'docsComplete', 'retroComplete']);
|
|
7
7
|
const CANONICAL_LABELS = new Set([
|
|
8
8
|
'Preferences', 'Registries', 'Profile', 'Sensors', 'Optional source unavailable',
|
|
9
|
-
'Extensions', 'Registry pins', 'Active bundles', 'Project context', 'Constitution', 'Static preflight',
|
|
9
|
+
'Extensions', 'Registry pins', 'Active bundles', 'Project context', 'Constitution', 'Static preflight', 'Documentation',
|
|
10
10
|
]);
|
|
11
11
|
const CANONICAL_FINDING_IDS = new Set([
|
|
12
12
|
'machine.preferences.missing', 'machine.registries.stale', 'project.profile.missing',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.dashboardSnapshot = dashboardSnapshot;
|
|
4
|
-
const PROJECT_SECTION_IDS = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'];
|
|
4
|
+
const PROJECT_SECTION_IDS = ['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes'];
|
|
5
5
|
/** Safe, deterministic V1 fixture and renderer input. */
|
|
6
6
|
function dashboardSnapshot(overrides = {}) {
|
|
7
7
|
const { project: projectOverride, sections: sectionsOverride, ...rest } = overrides;
|
|
@@ -10,7 +10,7 @@ function dashboardSnapshot(overrides = {}) {
|
|
|
10
10
|
? PROJECT_SECTION_IDS.map((id) => ({ id, availability: id === 'machine' ? 'available' : 'not_applicable', items: [] }))
|
|
11
11
|
: [{ id: 'machine', availability: 'available', items: [] }]);
|
|
12
12
|
return {
|
|
13
|
-
schema:
|
|
13
|
+
schema: 2,
|
|
14
14
|
generatedAt: '2026-08-22T00:00:00.000Z',
|
|
15
15
|
overall: 'healthy',
|
|
16
16
|
project,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DashboardValidationError = void 0;
|
|
4
4
|
exports.validateDashboardSnapshotV1 = validateDashboardSnapshotV1;
|
|
5
|
-
const SECTION_ORDER = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'];
|
|
5
|
+
const SECTION_ORDER = ['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes'];
|
|
6
6
|
const ITEM_STATES = ['ok', 'attention', 'missing', 'unavailable', 'not_applicable'];
|
|
7
7
|
const AVAILABILITIES = ['available', 'unavailable', 'not_applicable'];
|
|
8
8
|
const OVERALLS = ['healthy', 'degraded'];
|
|
@@ -42,8 +42,8 @@ function assertEnum(value, values, path) {
|
|
|
42
42
|
function validateDashboardSnapshotV1(value) {
|
|
43
43
|
assertRecord(value, 'snapshot');
|
|
44
44
|
assertOnlyKeys(value, ['schema', 'generatedAt', 'overall', 'project', 'confidence', 'sections'], 'snapshot');
|
|
45
|
-
if (value.schema !==
|
|
46
|
-
throw new DashboardValidationError('schema must be version
|
|
45
|
+
if (value.schema !== 2)
|
|
46
|
+
throw new DashboardValidationError('schema must be version 2');
|
|
47
47
|
assertNonEmptyString(value.generatedAt, 'generatedAt');
|
|
48
48
|
if (Number.isNaN(Date.parse(value.generatedAt)))
|
|
49
49
|
throw new DashboardValidationError('generatedAt must be a valid date-time');
|
|
@@ -5,11 +5,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const collect_1 = require("../../../src/core/dashboard/collect");
|
|
7
7
|
const sanitize_1 = require("../../../src/core/dashboard/sanitize");
|
|
8
|
+
const plan_state_1 = require("../../../src/core/dashboard/plan-state");
|
|
8
9
|
const store_1 = require("../../../src/core/evidence/store");
|
|
9
10
|
const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
|
|
10
11
|
const fs_1 = __importDefault(require("fs"));
|
|
11
12
|
const os_1 = __importDefault(require("os"));
|
|
12
13
|
const path_1 = __importDefault(require("path"));
|
|
14
|
+
function lifecycleForCycleFixture(planState) {
|
|
15
|
+
const cycle = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), schema: 1, plan: { ref: 'plans/current.md', state: planState }, retries: 0, cureEfficacy: [] };
|
|
16
|
+
return (0, collect_1.lifecycleForCycle)(cycle);
|
|
17
|
+
}
|
|
13
18
|
const fixedNow = '2026-08-22T00:00:00.000Z';
|
|
14
19
|
describe('collectDashboardSnapshot', () => {
|
|
15
20
|
it('returns only a healthy machine section outside a project', () => {
|
|
@@ -46,12 +51,22 @@ describe('collectDashboardSnapshot', () => {
|
|
|
46
51
|
execution: () => ({}),
|
|
47
52
|
},
|
|
48
53
|
});
|
|
49
|
-
expect(snapshot.sections.map((section) => section.id)).toEqual(
|
|
54
|
+
expect(snapshot.sections.map((section) => section.id)).toEqual(// verifies R6.3
|
|
55
|
+
['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes']);
|
|
50
56
|
expect(snapshot.sections.find((section) => section.id === 'machine')?.items[0].remediation).toBe('awm init');
|
|
51
57
|
expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toHaveLength(2000);
|
|
52
58
|
expect(snapshot.sections.find((section) => section.id === 'history')?.items).toHaveLength(0);
|
|
53
59
|
expect(JSON.stringify(snapshot)).not.toMatch(/score|ranking/i);
|
|
54
60
|
});
|
|
61
|
+
it('el snapshot declara schema 2', () => {
|
|
62
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
63
|
+
expect(snapshot.schema).toBe(2);
|
|
64
|
+
});
|
|
65
|
+
it('processes queda declarada no aplicable hasta R1', () => {
|
|
66
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
67
|
+
const processes = snapshot.sections.find((section) => section.id === 'processes');
|
|
68
|
+
expect(processes).toEqual({ id: 'processes', availability: 'not_applicable', items: [] });
|
|
69
|
+
});
|
|
55
70
|
it('isolates optional adapter failures and omits unverified remediation', () => {
|
|
56
71
|
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
57
72
|
cwd: process.cwd(), now: fixedNow,
|
|
@@ -70,7 +85,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
70
85
|
cwd: process.cwd(), now: fixedNow,
|
|
71
86
|
adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined },
|
|
72
87
|
});
|
|
73
|
-
for (const id of ['execution', 'qa', 'retro']) {
|
|
88
|
+
for (const id of ['execution', 'qa', 'docs', 'retro']) {
|
|
74
89
|
expect(snapshot.sections.find((section) => section.id === id)?.availability).toBe('unavailable');
|
|
75
90
|
}
|
|
76
91
|
});
|
|
@@ -91,7 +106,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
91
106
|
expect(snapshot.sections.find((section) => section.id === 'project')?.availability).toBe('unavailable');
|
|
92
107
|
expect(snapshot.sections.find((section) => section.id === 'planning')?.availability).toBe('available');
|
|
93
108
|
});
|
|
94
|
-
it.each(['execution', 'qa', 'retro'])('isolates malformed %s findings', (key) => {
|
|
109
|
+
it.each(['execution', 'qa', 'docs', 'retro'])('isolates malformed %s findings', (key) => {
|
|
95
110
|
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => ({ [key]: [{ id: '', label: 'Profile', state: 'ok' }] }) } });
|
|
96
111
|
expect(snapshot.sections.find((section) => section.id === key)?.availability).toBe('unavailable');
|
|
97
112
|
});
|
|
@@ -133,12 +148,12 @@ describe('collectDashboardSnapshot', () => {
|
|
|
133
148
|
expect(snapshot.sections[0].items.map((item) => item.id)).toEqual(['machine.preferences.missing', 'machine.registries.stale']);
|
|
134
149
|
});
|
|
135
150
|
it.each(['blocked', 'active', 'executed', 'retro_pending', 'qa_pending', 'legacy_unverifiable'])('integrates lifecycle state %s into plan detail', (expected) => {
|
|
136
|
-
const lifecycle = expected === 'blocked' ? { journal: { state: 'blocked' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total: 1, completed: 0 } }
|
|
137
|
-
: expected === 'active' ? { journal: { state: 'active' }, markers: { qaComplete: false, retroComplete: false }, tasks: { total: 1, completed: 0 } }
|
|
138
|
-
: expected === 'executed' ? { markers: { qaComplete: true, retroComplete: true }, tasks: { total: 1, completed: 1 } }
|
|
139
|
-
: expected === 'retro_pending' ? { markers: { qaComplete: true, retroComplete: false }, tasks: { total: 1, completed: 1 } }
|
|
140
|
-
: expected === 'qa_pending' ? { markers: { qaComplete: false, retroComplete: false }, tasks: { total: 1, completed: 1 } }
|
|
141
|
-
: { markers: { qaComplete: false, retroComplete: false }, tasks: { total: 0, completed: 0 } };
|
|
151
|
+
const lifecycle = expected === 'blocked' ? { journal: { state: 'blocked' }, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 1, completed: 0 } }
|
|
152
|
+
: expected === 'active' ? { journal: { state: 'active' }, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 1, completed: 0 } }
|
|
153
|
+
: expected === 'executed' ? { markers: { qaComplete: true, docsComplete: true, retroComplete: true }, tasks: { total: 1, completed: 1 } }
|
|
154
|
+
: expected === 'retro_pending' ? { markers: { qaComplete: true, docsComplete: true, retroComplete: false }, tasks: { total: 1, completed: 1 } }
|
|
155
|
+
: expected === 'qa_pending' ? { markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 1, completed: 1 } }
|
|
156
|
+
: { markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 0, completed: 0 } };
|
|
142
157
|
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [{ id: 'plan.lifecycle', label: 'Profile', state: 'ok', lifecycle }], execution: () => undefined } });
|
|
143
158
|
expect(snapshot.sections.find((section) => section.id === 'planning')?.items[0].detail).toBe(expected);
|
|
144
159
|
});
|
|
@@ -246,6 +261,19 @@ describe('sanitizeDashboardSource', () => {
|
|
|
246
261
|
expect(JSON.stringify(safe)).not.toMatch(/Error|ghp_|\/tmp|TOKEN=/i);
|
|
247
262
|
expect(JSON.stringify(safe)).not.toContain('detail');
|
|
248
263
|
});
|
|
264
|
+
it('sanitize conserva docsComplete', () => {
|
|
265
|
+
const cleaned = (0, sanitize_1.sanitizeDashboardSource)({
|
|
266
|
+
lifecycle: { markers: { qaComplete: true, docsComplete: true, retroComplete: false }, tasks: { total: 1, completed: 1 } },
|
|
267
|
+
});
|
|
268
|
+
expect(cleaned.lifecycle.markers.docsComplete).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
it('un ciclo historico retro_pending no retrocede a docs_pending', () => {
|
|
271
|
+
// Evidencia escrita antes de que existiera la fase: docs se considera hecho.
|
|
272
|
+
expect((0, plan_state_1.classifyPlanState)(lifecycleForCycleFixture('retro_pending'))).toBe('retro_pending');
|
|
273
|
+
});
|
|
274
|
+
it.each(['blocked', 'active', 'executed', 'qa_pending', 'legacy_unverifiable'])('lifecycleForCycle reconstruye %s de forma que classifyPlanState lo re-deriva igual', (planState) => {
|
|
275
|
+
expect((0, plan_state_1.classifyPlanState)(lifecycleForCycleFixture(planState))).toBe(planState);
|
|
276
|
+
});
|
|
249
277
|
});
|
|
250
278
|
test('exports canonical remediation commands', () => {
|
|
251
279
|
expect(collect_1.REMEDIATION_BY_FINDING_ID['machine.preferences.missing']).toBe('awm init');
|
|
@@ -4,7 +4,7 @@ const validate_1 = require("../../../src/core/dashboard/validate");
|
|
|
4
4
|
const types_1 = require("../../../src/core/dashboard/types");
|
|
5
5
|
function snapshot() {
|
|
6
6
|
return {
|
|
7
|
-
schema:
|
|
7
|
+
schema: 2,
|
|
8
8
|
generatedAt: '2026-08-22T00:00:00.000Z',
|
|
9
9
|
overall: 'healthy',
|
|
10
10
|
project: { detected: true, label: 'Demo project' },
|
|
@@ -15,15 +15,17 @@ function snapshot() {
|
|
|
15
15
|
{ id: 'planning', availability: 'not_applicable', items: [] },
|
|
16
16
|
{ id: 'execution', availability: 'not_applicable', items: [] },
|
|
17
17
|
{ id: 'qa', availability: 'not_applicable', items: [] },
|
|
18
|
+
{ id: 'docs', availability: 'not_applicable', items: [] },
|
|
18
19
|
{ id: 'retro', availability: 'not_applicable', items: [] },
|
|
19
20
|
{ id: 'history', availability: 'not_applicable', items: [] },
|
|
21
|
+
{ id: 'processes', availability: 'not_applicable', items: [] },
|
|
20
22
|
],
|
|
21
23
|
};
|
|
22
24
|
}
|
|
23
25
|
describe('validateDashboardSnapshotV1', () => {
|
|
24
26
|
it('creates the safe deterministic dashboard snapshot default', () => {
|
|
25
27
|
expect((0, types_1.dashboardSnapshot)()).toEqual({
|
|
26
|
-
schema:
|
|
28
|
+
schema: 2,
|
|
27
29
|
generatedAt: '2026-08-22T00:00:00.000Z',
|
|
28
30
|
overall: 'healthy',
|
|
29
31
|
project: { detected: false, label: 'No project detected' },
|
|
@@ -36,7 +38,7 @@ describe('validateDashboardSnapshotV1', () => {
|
|
|
36
38
|
});
|
|
37
39
|
it('builds a complete canonical section set when callers select a project', () => {
|
|
38
40
|
const value = (0, types_1.dashboardSnapshot)({ project: { detected: true, label: 'Demo project' }, confidence: 'provisional' });
|
|
39
|
-
expect((0, validate_1.validateDashboardSnapshotV1)(value).sections.map((section) => section.id)).toEqual(['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history']);
|
|
41
|
+
expect((0, validate_1.validateDashboardSnapshotV1)(value).sections.map((section) => section.id)).toEqual(['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes']);
|
|
40
42
|
});
|
|
41
43
|
it('returns a valid V1 snapshot', () => {
|
|
42
44
|
expect((0, validate_1.validateDashboardSnapshotV1)(snapshot())).toEqual(snapshot());
|
|
@@ -53,7 +55,7 @@ describe('validateDashboardSnapshotV1', () => {
|
|
|
53
55
|
});
|
|
54
56
|
it('rejects an invalid schema version', () => {
|
|
55
57
|
const value = snapshot();
|
|
56
|
-
value.schema =
|
|
58
|
+
value.schema = 3;
|
|
57
59
|
expect(() => (0, validate_1.validateDashboardSnapshotV1)(value)).toThrow(validate_1.DashboardValidationError);
|
|
58
60
|
});
|
|
59
61
|
it('rejects invalid public enums', () => {
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const plan_state_1 = require("../../../src/core/dashboard/plan-state");
|
|
4
|
-
const base = { journal: undefined, markers: { qaComplete: false, retroComplete: false }, tasks: { total: 2, completed: 0 } };
|
|
4
|
+
const base = { journal: undefined, markers: { qaComplete: false, docsComplete: false, retroComplete: false }, tasks: { total: 2, completed: 0 } };
|
|
5
5
|
describe('classifyPlanState', () => {
|
|
6
6
|
it('gives blocked journal state highest precedence', () => {
|
|
7
|
-
expect((0, plan_state_1.classifyPlanState)({ ...base, journal: { state: 'blocked' }, markers: { qaComplete: true, retroComplete: true }, tasks: { total: 2, completed: 2 } })).toBe('blocked');
|
|
7
|
+
expect((0, plan_state_1.classifyPlanState)({ ...base, journal: { state: 'blocked' }, markers: { qaComplete: true, docsComplete: true, retroComplete: true }, tasks: { total: 2, completed: 2 } })).toBe('blocked');
|
|
8
8
|
});
|
|
9
9
|
it('gives active journal state precedence over completed markers', () => {
|
|
10
|
-
expect((0, plan_state_1.classifyPlanState)({ ...base, journal: { state: 'active' }, markers: { qaComplete: true, retroComplete: true }, tasks: { total: 2, completed: 2 } })).toBe('active');
|
|
10
|
+
expect((0, plan_state_1.classifyPlanState)({ ...base, journal: { state: 'active' }, markers: { qaComplete: true, docsComplete: true, retroComplete: true }, tasks: { total: 2, completed: 2 } })).toBe('active');
|
|
11
11
|
});
|
|
12
12
|
it.each([
|
|
13
|
-
[{ ...base, markers: { qaComplete: true, retroComplete: true } }, 'executed'],
|
|
14
|
-
[{ ...base, markers: { qaComplete: true, retroComplete: false } }, 'retro_pending'],
|
|
13
|
+
[{ ...base, markers: { qaComplete: true, docsComplete: true, retroComplete: true } }, 'executed'],
|
|
14
|
+
[{ ...base, markers: { qaComplete: true, docsComplete: true, retroComplete: false } }, 'retro_pending'],
|
|
15
15
|
[{ ...base, tasks: { total: 2, completed: 2 } }, 'qa_pending'],
|
|
16
16
|
[base, 'legacy_unverifiable'],
|
|
17
17
|
])('classifies lifecycle state %s', (input, expected) => {
|
|
@@ -30,3 +30,41 @@ describe('classifyPlanState', () => {
|
|
|
30
30
|
expect(() => (0, plan_state_1.classifyPlanState)(input)).toThrow(error);
|
|
31
31
|
});
|
|
32
32
|
});
|
|
33
|
+
describe('fase de documentacion', () => {
|
|
34
|
+
it('clasifica docs_pending con QA hecha y documentacion pendiente', () => {
|
|
35
|
+
expect((0, plan_state_1.classifyPlanState)({
|
|
36
|
+
markers: { qaComplete: true, docsComplete: false, retroComplete: false },
|
|
37
|
+
tasks: { total: 3, completed: 3 },
|
|
38
|
+
})).toBe('docs_pending');
|
|
39
|
+
});
|
|
40
|
+
it('vuelve a retro_pending cuando la documentacion esta hecha', () => {
|
|
41
|
+
expect((0, plan_state_1.classifyPlanState)({
|
|
42
|
+
markers: { qaComplete: true, docsComplete: true, retroComplete: false },
|
|
43
|
+
tasks: { total: 3, completed: 3 },
|
|
44
|
+
})).toBe('retro_pending');
|
|
45
|
+
});
|
|
46
|
+
it('conserva executed con retro hecho — el significado no cambia', () => {
|
|
47
|
+
expect((0, plan_state_1.classifyPlanState)({
|
|
48
|
+
markers: { qaComplete: true, docsComplete: true, retroComplete: true },
|
|
49
|
+
tasks: { total: 3, completed: 3 },
|
|
50
|
+
})).toBe('executed');
|
|
51
|
+
});
|
|
52
|
+
it('rechaza un marker desconocido', () => {
|
|
53
|
+
expect(() => (0, plan_state_1.classifyPlanState)({
|
|
54
|
+
markers: { qaComplete: true, docsComplete: false, retroComplete: false, bogusComplete: true },
|
|
55
|
+
tasks: { total: 1, completed: 1 },
|
|
56
|
+
})).toThrow(/unsupported fields/);
|
|
57
|
+
});
|
|
58
|
+
it('rechaza docsComplete no booleano', () => {
|
|
59
|
+
expect(() => (0, plan_state_1.classifyPlanState)({
|
|
60
|
+
markers: { qaComplete: true, docsComplete: 'yes', retroComplete: false },
|
|
61
|
+
tasks: { total: 1, completed: 1 },
|
|
62
|
+
})).toThrow(/must be boolean/);
|
|
63
|
+
});
|
|
64
|
+
it('docsComplete solo, sin qaComplete, ya alcanza retro_pending — el orden de la cadena no exige qaComplete primero', () => {
|
|
65
|
+
expect((0, plan_state_1.classifyPlanState)({
|
|
66
|
+
markers: { qaComplete: false, docsComplete: true, retroComplete: false },
|
|
67
|
+
tasks: { total: 3, completed: 3 },
|
|
68
|
+
})).toBe('retro_pending');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -99,11 +99,29 @@ describe('productionDashboardAdapters', () => {
|
|
|
99
99
|
expect(snapshot.sections.find((section) => section.id === 'qa')?.items).toEqual([
|
|
100
100
|
expect.objectContaining({ state: 'ok' }),
|
|
101
101
|
]);
|
|
102
|
+
expect(snapshot.sections.find((section) => section.id === 'docs')?.items).toEqual([
|
|
103
|
+
expect.objectContaining({ state: 'ok' }),
|
|
104
|
+
]);
|
|
102
105
|
expect(snapshot.sections.find((section) => section.id === 'retro')?.items).toEqual([
|
|
103
106
|
expect.objectContaining({ state: 'ok' }),
|
|
104
107
|
]);
|
|
105
108
|
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
106
109
|
});
|
|
110
|
+
it('flags the docs section as attention when a captured cycle has not documented yet', () => {
|
|
111
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-docs-attention-'));
|
|
112
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
113
|
+
(0, store_1.writeCycleEvidence)(root, {
|
|
114
|
+
...(0, evidence_fixtures_1.cycleEvidenceFixture)(),
|
|
115
|
+
plan: { ref: 'docs/plans/current.md', state: 'qa_pending' },
|
|
116
|
+
});
|
|
117
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
118
|
+
cwd: root, now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(context()),
|
|
119
|
+
});
|
|
120
|
+
expect(snapshot.sections.find((section) => section.id === 'docs')?.items).toEqual([
|
|
121
|
+
expect.objectContaining({ state: 'attention' }),
|
|
122
|
+
]);
|
|
123
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
124
|
+
});
|
|
107
125
|
it.each([
|
|
108
126
|
['IN_PROGRESS', 'active', 'ok'],
|
|
109
127
|
['BLOCKED', 'blocked', 'attention'],
|
|
@@ -13,8 +13,10 @@ function snapshot(overrides = {}) {
|
|
|
13
13
|
{ id: 'planning', availability: 'available', items: [] },
|
|
14
14
|
{ id: 'execution', availability: 'available', items: [] },
|
|
15
15
|
{ id: 'qa', availability: 'available', items: [] },
|
|
16
|
+
{ id: 'docs', availability: 'available', items: [] },
|
|
16
17
|
{ id: 'retro', availability: 'available', items: [] },
|
|
17
18
|
{ id: 'history', availability: 'available', items: [] },
|
|
19
|
+
{ id: 'processes', availability: 'not_applicable', items: [] },
|
|
18
20
|
],
|
|
19
21
|
...overrides,
|
|
20
22
|
});
|
|
@@ -27,7 +29,9 @@ describe('renderDashboardHtml', () => {
|
|
|
27
29
|
{ id: 'machine', availability: 'available', items: [{ id: 'hostile', label: '<script>alert(1)</script>', state: 'attention', detail: '"quoted"', remediation: 'awm sync && echo <unsafe>' }] },
|
|
28
30
|
{ id: 'project', availability: 'not_applicable', items: [] }, { id: 'planning', availability: 'not_applicable', items: [] },
|
|
29
31
|
{ id: 'execution', availability: 'not_applicable', items: [] }, { id: 'qa', availability: 'not_applicable', items: [] },
|
|
32
|
+
{ id: 'docs', availability: 'not_applicable', items: [] },
|
|
30
33
|
{ id: 'retro', availability: 'not_applicable', items: [] }, { id: 'history', availability: 'not_applicable', items: [] },
|
|
34
|
+
{ id: 'processes', availability: 'not_applicable', items: [] },
|
|
31
35
|
],
|
|
32
36
|
})));
|
|
33
37
|
expect(html).toContain(`<meta http-equiv="Content-Security-Policy" content="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'">`);
|
|
@@ -153,7 +157,7 @@ describe('renderDashboardHtml', () => {
|
|
|
153
157
|
});
|
|
154
158
|
it('renders every canonical project section once and in lifecycle order', () => {
|
|
155
159
|
const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({ confidence: 'provisional' })));
|
|
156
|
-
const ids = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'];
|
|
160
|
+
const ids = ['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes'];
|
|
157
161
|
const positions = ids.map((id) => html.indexOf(`<section id="${id}"`));
|
|
158
162
|
expect(positions.every((position) => position >= 0)).toBe(true);
|
|
159
163
|
expect([...positions].sort((left, right) => left - right)).toEqual(positions);
|
|
@@ -162,7 +166,7 @@ describe('renderDashboardHtml', () => {
|
|
|
162
166
|
expect(html.match(/<section[\s>]/g)).toHaveLength(ids.length);
|
|
163
167
|
});
|
|
164
168
|
it('is deterministic and preserves every history and task observation without privacy leakage', () => {
|
|
165
|
-
const sections = ['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history'].map((id) => ({
|
|
169
|
+
const sections = ['machine', 'project', 'planning', 'execution', 'qa', 'docs', 'retro', 'history', 'processes'].map((id) => ({
|
|
166
170
|
id: id, availability: 'available',
|
|
167
171
|
items: id === 'history' ? Array.from({ length: 500 }, (_, index) => ({ id: `history.${index}`, label: `Cycle ${index}`, state: 'ok' })) : id === 'execution' ? Array.from({ length: 2000 }, (_, index) => ({ id: `execution.${index}`, label: `Task ${index}`, state: 'ok' })) : [],
|
|
168
172
|
}));
|
|
@@ -178,8 +182,9 @@ describe('renderDashboardHtml', () => {
|
|
|
178
182
|
sections: [
|
|
179
183
|
{ id: 'machine', availability: 'available', items: [] }, { id: 'project', availability: 'available', items: [] },
|
|
180
184
|
{ id: 'planning', availability: 'available', items: [{ id: 'plan.alpha', label: 'Impact plan', state: 'ok', detail: 'executed' }] },
|
|
181
|
-
{ id: 'execution', availability: 'available', items: [] }, { id: 'qa', availability: 'available', items: [] }, { id: 'retro', availability: 'available', items: [] },
|
|
185
|
+
{ id: 'execution', availability: 'available', items: [] }, { id: 'qa', availability: 'available', items: [] }, { id: 'docs', availability: 'available', items: [] }, { id: 'retro', availability: 'available', items: [] },
|
|
182
186
|
{ id: 'history', availability: 'available', items: [{ id: 'history.cycle.abc', label: 'Cycle abc', state: 'ok', detail: 'plan executed; tasks 3; retries 2; QA 2/2; first-pass yes; cures supported' }] },
|
|
187
|
+
{ id: 'processes', availability: 'not_applicable', items: [] },
|
|
183
188
|
],
|
|
184
189
|
})));
|
|
185
190
|
expect(html).toContain('Confidence: supported');
|
|
@@ -14,8 +14,10 @@ function completeSnapshot(overrides = {}) {
|
|
|
14
14
|
{ id: 'planning', availability: 'available', items: [{ id: 'planning.plan', label: 'Active plan', state: 'ok', detail: 'executing' }] },
|
|
15
15
|
{ id: 'execution', availability: 'unavailable', items: [{ id: 'execution.source', label: 'Execution source', state: 'unavailable', remediation: 'awm doctor --full' }] },
|
|
16
16
|
{ id: 'qa', availability: 'available', items: [{ id: 'qa.gate', label: 'Verification gate', state: 'missing', remediation: 'awm sensors' }] },
|
|
17
|
+
{ id: 'docs', availability: 'available', items: [{ id: 'docs.marker', label: 'Documentation', state: 'not_applicable' }] },
|
|
17
18
|
{ id: 'retro', availability: 'available', items: [{ id: 'retro.cure', label: 'Cure observations', state: 'not_applicable' }] },
|
|
18
19
|
{ id: 'history', availability: 'available', items: [{ id: 'history.cycle', label: 'Eligible cycle', state: 'ok', detail: '5 minutes' }] },
|
|
20
|
+
{ id: 'processes', availability: 'not_applicable', items: [] },
|
|
19
21
|
],
|
|
20
22
|
...overrides,
|
|
21
23
|
});
|
|
@@ -23,7 +25,7 @@ function completeSnapshot(overrides = {}) {
|
|
|
23
25
|
describe('renderFullTerminal', () => {
|
|
24
26
|
it('renders all lifecycle sections in canonical order with status and remediation', () => {
|
|
25
27
|
const output = (0, render_terminal_1.renderFullTerminal)((0, validate_1.validateDashboardSnapshotV1)(completeSnapshot()));
|
|
26
|
-
const headings = ['Machine / install', 'Project readiness', 'Design / planning', 'Execution', 'QA', 'Retro', 'Final / history'];
|
|
28
|
+
const headings = ['Machine / install', 'Project readiness', 'Design / planning', 'Execution', 'QA', 'Docs', 'Retro', 'Final / history', 'Processes'];
|
|
27
29
|
expect(headings.map((heading) => output.indexOf(heading))).toEqual([...headings.map((_, index) => expect.any(Number))].map((_, index) => expect.any(Number)));
|
|
28
30
|
for (let index = 1; index < headings.length; index++)
|
|
29
31
|
expect(output.indexOf(headings[index])).toBeGreaterThan(output.indexOf(headings[index - 1]));
|
|
@@ -53,8 +55,10 @@ describe('renderFullTerminal', () => {
|
|
|
53
55
|
{ id: 'planning', availability: 'available', items: [] },
|
|
54
56
|
{ id: 'execution', availability: 'available', items: execution },
|
|
55
57
|
{ id: 'qa', availability: 'available', items: [] },
|
|
58
|
+
{ id: 'docs', availability: 'available', items: [] },
|
|
56
59
|
{ id: 'retro', availability: 'available', items: [] },
|
|
57
60
|
{ id: 'history', availability: 'available', items: history },
|
|
61
|
+
{ id: 'processes', availability: 'not_applicable', items: [] },
|
|
58
62
|
] }));
|
|
59
63
|
const first = (0, render_terminal_1.renderFullTerminal)(snapshot);
|
|
60
64
|
expect((0, render_terminal_1.renderFullTerminal)(snapshot)).toBe(first);
|
|
@@ -7,6 +7,8 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
7
7
|
const os_1 = __importDefault(require("os"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const evidence_1 = require("../../../src/commands/evidence");
|
|
10
|
+
const types_1 = require("../../../src/core/evidence/types");
|
|
11
|
+
const completeJournal = { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], jobs: {}, cycleVerificationPlan: [] };
|
|
10
12
|
describe('evidence capture CLI boundary', () => {
|
|
11
13
|
let root;
|
|
12
14
|
beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-command-')); });
|
|
@@ -57,4 +59,54 @@ describe('evidence capture CLI boundary', () => {
|
|
|
57
59
|
const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] });
|
|
58
60
|
expect(result).toEqual(expect.objectContaining({ code: 2, error: expect.stringMatching(/invalid checklist/i) }));
|
|
59
61
|
});
|
|
62
|
+
test('sin el marker de docs, el estado vivo seria docs_pending — se remapea a retro_pending en la evidencia', () => {
|
|
63
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# Plan\n<!-- awm-qa-complete: 2026-08-23 -->\n- [x] Task 1\n');
|
|
64
|
+
const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: completeJournal, ledger: [] });
|
|
65
|
+
expect(result.code).toBe(0);
|
|
66
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('retro_pending');
|
|
67
|
+
});
|
|
68
|
+
test('lee el marker awm-docs-complete del plan — sin awm-qa-complete, docsComplete solo alcanza para retro_pending', () => {
|
|
69
|
+
// Discrimina de verdad: si el parser NO leyera awm-docs-complete, este plan (sin qa-complete,
|
|
70
|
+
// sin retro-complete) clasificaria qa_pending por conteo de tasks. Si SI lo lee, va directo a
|
|
71
|
+
// retro_pending — la unica forma de que este resultado ocurra es que el marker se haya parseado.
|
|
72
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# Plan\n<!-- awm-docs-complete: 2026-08-23 -->\n- [x] Task 1\n');
|
|
73
|
+
const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: completeJournal, ledger: [] });
|
|
74
|
+
expect(result.code).toBe(0);
|
|
75
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('retro_pending');
|
|
76
|
+
});
|
|
77
|
+
test('no deja que un awm-docs-complete de otro release clasifique el release actual', () => {
|
|
78
|
+
// Mismo patron que el test analogo de qa/retro: el marker debe respetar el scoping por release.
|
|
79
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n<!-- awm-docs-complete: Release A / #86 -->\n\n## Delivery order\n1. **Release A / #86:** dashboard\n2. **Release B / #87:** evidence\n\n- [x] Release B task\n');
|
|
80
|
+
const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] });
|
|
81
|
+
expect(result.code).toBe(0);
|
|
82
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('qa_pending');
|
|
83
|
+
});
|
|
84
|
+
test('no filtra docs_pending al registro durable de evidencia', () => {
|
|
85
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# Plan\n<!-- awm-qa-complete: 2026-08-23 -->\n- [x] Task 1\n');
|
|
86
|
+
const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: completeJournal, ledger: [] });
|
|
87
|
+
expect(result.code).toBe(0);
|
|
88
|
+
expect(types_1.PLAN_STATES).not.toContain('docs_pending');
|
|
89
|
+
const state = JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state;
|
|
90
|
+
expect(types_1.PLAN_STATES).toContain(state);
|
|
91
|
+
});
|
|
92
|
+
test('con docs y retro completos el estado sigue siendo executed', () => {
|
|
93
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# Plan\n<!-- awm-qa-complete: 2026-08-23 -->\n<!-- awm-docs-complete: 2026-08-23 -->\n<!-- awm-retro-complete: 2026-08-23 -->\n- [x] Task 1\n');
|
|
94
|
+
const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: completeJournal, ledger: [] });
|
|
95
|
+
expect(result.code).toBe(0);
|
|
96
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('executed');
|
|
97
|
+
});
|
|
98
|
+
test('un marker sin cerrar y muy largo no cuelga el parseo (ReDoS)', () => {
|
|
99
|
+
// Regresion: el regex original tenia \s* adyacente al grupo lazo [^\r\n]*?,
|
|
100
|
+
// lo que producia backtracking catastrofico ante una linea larga sin "-->".
|
|
101
|
+
const unterminated = `<!-- awm-qa-complete: ${' '.repeat(200000)}`;
|
|
102
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), `# Plan\n${unterminated}\n- [x] Task 1\n`);
|
|
103
|
+
const start = Date.now();
|
|
104
|
+
const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: completeJournal, ledger: [] });
|
|
105
|
+
const elapsedMs = Date.now() - start;
|
|
106
|
+
expect(elapsedMs).toBeLessThan(1000);
|
|
107
|
+
expect(result.code).toBe(0);
|
|
108
|
+
// linea sin cerrar: el marker no matchea, y sin awm-qa-complete el plan
|
|
109
|
+
// no llega a docs_pending/retro_pending por conteo de tasks (1/1 completa).
|
|
110
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('qa_pending');
|
|
111
|
+
});
|
|
60
112
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const ROOT = path_1.default.resolve(__dirname, '../../..');
|
|
9
|
+
const read = (file) => fs_1.default.readFileSync(path_1.default.join(ROOT, file), 'utf8');
|
|
10
|
+
describe('la fase de documentacion esta mecanizada, no solo enunciada', () => {
|
|
11
|
+
it('CONSTITUTION.md enuncia la regla y nombra el marker', () => {
|
|
12
|
+
const text = read('CONSTITUTION.md');
|
|
13
|
+
expect(text).toMatch(/awm-docs-complete/);
|
|
14
|
+
expect(text).toMatch(/post-implementation-docs/);
|
|
15
|
+
});
|
|
16
|
+
it('CONSTITUTION.md apunta al mecanismo, no lo reemplaza', () => {
|
|
17
|
+
const text = read('CONSTITUTION.md');
|
|
18
|
+
// debe citar el archivo que efectivamente hace cumplir la regla
|
|
19
|
+
expect(text).toMatch(/plan-state\.ts/);
|
|
20
|
+
expect(text).toMatch(/development-process/);
|
|
21
|
+
});
|
|
22
|
+
it('el enunciado NO es el unico lugar donde la fase existe', () => {
|
|
23
|
+
// Si esto pasa solo por CONSTITUTION.md, la regla es decorativa.
|
|
24
|
+
expect(read('cli/src/core/dashboard/plan-state.ts')).toMatch(/docsComplete/);
|
|
25
|
+
expect(read('cli/src/core/dashboard/plan-state.ts')).toMatch(/docs_pending/);
|
|
26
|
+
expect(read('cli/src/core/dashboard/sanitize.ts')).toMatch(/docsComplete/);
|
|
27
|
+
expect(read('cli/src/commands/evidence/index.ts')).toMatch(/awm-docs-complete/);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -8,7 +8,6 @@ const path_1 = __importDefault(require("path"));
|
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const CLI_ROOT = path_1.default.resolve(__dirname, '../..');
|
|
10
10
|
const DIST_ENTRYPOINT = path_1.default.join(CLI_ROOT, 'dist', 'src', 'index.js');
|
|
11
|
-
const R3_MAJOR_VERSION = 8;
|
|
12
11
|
function readJson(file) {
|
|
13
12
|
return JSON.parse(fs_1.default.readFileSync(path_1.default.join(CLI_ROOT, file), 'utf8'));
|
|
14
13
|
}
|
|
@@ -22,13 +21,19 @@ function runCompiledCli(...args) {
|
|
|
22
21
|
return `${result.stdout}${result.stderr}`;
|
|
23
22
|
}
|
|
24
23
|
describe('R3 public major CLI release contract', () => {
|
|
25
|
-
|
|
24
|
+
// #92: the bump wrote only package.json, so the lockfile trailed by one
|
|
25
|
+
// version on EVERY release. This is the real invariant — package.json,
|
|
26
|
+
// package-lock.json, and its root entry must always agree. A hardcoded
|
|
27
|
+
// expected major (originally 8, from when this test was written) goes
|
|
28
|
+
// stale on every legitimate `!:`/BREAKING major bump and was never the
|
|
29
|
+
// actual contract, so it isn't asserted here.
|
|
30
|
+
it('retains a consistent version across package metadata and the lockfile root', () => {
|
|
26
31
|
const pkg = readJson('package.json');
|
|
27
32
|
const lock = readJson('package-lock.json');
|
|
28
33
|
const root = lock.packages[''];
|
|
29
34
|
const version = pkg.version;
|
|
30
35
|
expect(typeof version).toBe('string');
|
|
31
|
-
expect(version).toMatch(
|
|
36
|
+
expect(version).toMatch(/^\d+\.\d+\.\d+$/);
|
|
32
37
|
expect(lock.version).toBe(version);
|
|
33
38
|
expect(root.version).toBe(version);
|
|
34
39
|
});
|