agentic-workflow-manager 8.4.0 → 8.5.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 +160 -0
- package/dist/src/core/dashboard/collect.js +201 -5
- package/dist/src/core/dashboard/render-html.js +9 -3
- package/dist/src/core/dashboard/render-terminal.js +2 -0
- package/dist/src/core/evidence/capture.js +66 -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/core/dashboard/collect.test.js +83 -4
- package/dist/tests/core/dashboard/production-adapters.test.js +89 -0
- package/dist/tests/core/dashboard/render-html.test.js +16 -0
- package/dist/tests/core/dashboard/render-terminal.test.js +7 -0
- package/dist/tests/core/evidence/capture.test.js +82 -0
- package/dist/tests/core/evidence/command.test.js +60 -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 +47 -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/dist/tests/structural/release-registry-provenance.test.js +21 -0
- package/package.json +1 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PLAN_STATES = void 0;
|
|
4
|
+
exports.validateCycleEvidence = validateCycleEvidence;
|
|
5
|
+
exports.PLAN_STATES = ['active', 'blocked', 'qa_pending', 'retro_pending', 'executed', 'legacy_unverifiable'];
|
|
6
|
+
const DIGEST = /^[a-f0-9]{64}$/;
|
|
7
|
+
const SAFE_ID = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
|
|
8
|
+
function record(value, label) {
|
|
9
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value))
|
|
10
|
+
throw new Error(`${label} must be an object`);
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function keys(value, label, allowed) {
|
|
14
|
+
if (Object.keys(value).some((key) => !allowed.includes(key)))
|
|
15
|
+
throw new Error(`${label} has unsupported fields`);
|
|
16
|
+
}
|
|
17
|
+
function timestamp(value, label) {
|
|
18
|
+
if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value) || Number.isNaN(Date.parse(value)) || new Date(value).toISOString() !== value)
|
|
19
|
+
throw new Error(`${label} must be a canonical ISO timestamp`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function count(value, label) {
|
|
23
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0)
|
|
24
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function digest(value, label) {
|
|
28
|
+
if (typeof value !== 'string' || !DIGEST.test(value))
|
|
29
|
+
throw new Error(`${label} must be a SHA-256 digest`);
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
function relativePlanRef(value) {
|
|
33
|
+
if (typeof value !== 'string' || !value || value.includes('\\') || value.startsWith('/') || /^[A-Za-z]:/.test(value)
|
|
34
|
+
|| value.split('/').some((part) => part === '' || part === '.' || part === '..'))
|
|
35
|
+
throw new Error('plan.ref must be repo-relative');
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
/** Strict public durable-boundary validator: accepted evidence intentionally has no prose or identities. */
|
|
39
|
+
function validateCycleEvidence(input) {
|
|
40
|
+
const value = record(input, 'cycle evidence');
|
|
41
|
+
keys(value, 'cycle evidence', ['schema', 'cycleId', 'startedAt', 'endedAt', 'durationMs', 'cycleState', 'plan', 'tasks', 'qa', 'gates', 'cures', 'pr']);
|
|
42
|
+
if (value.schema !== 1)
|
|
43
|
+
throw new Error('cycle evidence schema must be 1');
|
|
44
|
+
const startedAt = timestamp(value.startedAt, 'startedAt');
|
|
45
|
+
const endedAt = timestamp(value.endedAt, 'endedAt');
|
|
46
|
+
const durationMs = count(value.durationMs, 'durationMs');
|
|
47
|
+
if (Date.parse(endedAt) < Date.parse(startedAt) || durationMs !== Date.parse(endedAt) - Date.parse(startedAt))
|
|
48
|
+
throw new Error('cycle duration is invalid');
|
|
49
|
+
if (value.cycleState !== 'completed' && value.cycleState !== 'blocked')
|
|
50
|
+
throw new Error('cycleState is invalid');
|
|
51
|
+
const plan = record(value.plan, 'plan');
|
|
52
|
+
keys(plan, 'plan', ['ref', 'state']);
|
|
53
|
+
if (typeof plan.state !== 'string' || !exports.PLAN_STATES.includes(plan.state))
|
|
54
|
+
throw new Error('plan state is invalid');
|
|
55
|
+
if (!Array.isArray(value.tasks))
|
|
56
|
+
throw new Error('tasks must be an array');
|
|
57
|
+
const tasks = value.tasks.map((item, index) => {
|
|
58
|
+
const task = record(item, `tasks[${index}]`);
|
|
59
|
+
keys(task, `tasks[${index}]`, ['id', 'attempts', 'retries']);
|
|
60
|
+
if (typeof task.id !== 'string' || !SAFE_ID.test(task.id))
|
|
61
|
+
throw new Error('task id is invalid');
|
|
62
|
+
const attempts = count(task.attempts, 'task attempts');
|
|
63
|
+
const retries = count(task.retries, 'task retries');
|
|
64
|
+
if (retries !== Math.max(attempts - 1, 0))
|
|
65
|
+
throw new Error('task retries must derive from attempts');
|
|
66
|
+
return { id: task.id, attempts, retries };
|
|
67
|
+
});
|
|
68
|
+
const qa = record(value.qa, 'qa');
|
|
69
|
+
keys(qa, 'qa', ['findings', 'fixes', 'signatures']);
|
|
70
|
+
if (!Array.isArray(qa.signatures))
|
|
71
|
+
throw new Error('qa signatures must be an array');
|
|
72
|
+
const signatures = qa.signatures.map((signature, index) => digest(signature, `qa.signatures[${index}]`));
|
|
73
|
+
const findings = count(qa.findings, 'qa findings');
|
|
74
|
+
const fixes = count(qa.fixes, 'qa fixes');
|
|
75
|
+
if (signatures.length !== findings || fixes > findings)
|
|
76
|
+
throw new Error('qa counts are inconsistent');
|
|
77
|
+
const gates = record(value.gates, 'gates');
|
|
78
|
+
keys(gates, 'gates', ['required', 'firstEvaluationsPassed', 'firstPass']);
|
|
79
|
+
const required = count(gates.required, 'required gates');
|
|
80
|
+
if (!Array.isArray(gates.firstEvaluationsPassed) || !gates.firstEvaluationsPassed.every((passed) => typeof passed === 'boolean') || gates.firstEvaluationsPassed.length !== required || typeof gates.firstPass !== 'boolean' || gates.firstPass !== gates.firstEvaluationsPassed.every(Boolean))
|
|
81
|
+
throw new Error('gate evaluations are inconsistent');
|
|
82
|
+
if (!Array.isArray(value.cures))
|
|
83
|
+
throw new Error('cures must be an array');
|
|
84
|
+
const cures = value.cures.map((item, index) => { const cure = record(item, `cures[${index}]`); keys(cure, `cures[${index}]`, ['signature', 'curedAt']); return { signature: digest(cure.signature, 'cure signature'), curedAt: timestamp(cure.curedAt, 'curedAt') }; });
|
|
85
|
+
let pr;
|
|
86
|
+
if (value.pr !== undefined) {
|
|
87
|
+
const raw = record(value.pr, 'pr');
|
|
88
|
+
keys(raw, 'pr', ['provider', 'number']);
|
|
89
|
+
if (raw.provider !== 'github' && raw.provider !== 'gitlab' && raw.provider !== 'other')
|
|
90
|
+
throw new Error('pr provider is invalid');
|
|
91
|
+
const number = count(raw.number, 'pr number');
|
|
92
|
+
if (number < 1)
|
|
93
|
+
throw new Error('pr number is invalid');
|
|
94
|
+
pr = { provider: raw.provider, number };
|
|
95
|
+
}
|
|
96
|
+
return { schema: 1, cycleId: digest(value.cycleId, 'cycleId'), startedAt, endedAt, durationMs, cycleState: value.cycleState, plan: { ref: relativePlanRef(plan.ref), state: plan.state }, tasks, qa: { findings, fixes, signatures }, gates: { required, firstEvaluationsPassed: [...gates.firstEvaluationsPassed], firstPass: gates.firstPass }, cures, ...(pr ? { pr } : {}) };
|
|
97
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -41,6 +41,7 @@ const agent_1 = require("./commands/agent");
|
|
|
41
41
|
const job_1 = require("./commands/job");
|
|
42
42
|
const watch_1 = require("./commands/watch");
|
|
43
43
|
const track_1 = require("./commands/track");
|
|
44
|
+
const evidence_1 = require("./commands/evidence");
|
|
44
45
|
const add_1 = require("./commands/add");
|
|
45
46
|
const sync_1 = require("./commands/sync");
|
|
46
47
|
const update_1 = require("./commands/update");
|
|
@@ -733,6 +734,7 @@ miroCmd.command('sync <storyMapPath>')
|
|
|
733
734
|
(0, job_1.registerJobCommand)(program);
|
|
734
735
|
(0, watch_1.registerWatchCommand)(program);
|
|
735
736
|
(0, track_1.registerTrackCommand)(program);
|
|
737
|
+
(0, evidence_1.registerEvidenceCommand)(program);
|
|
736
738
|
// Commander only waits for async action handlers through parseAsync(). The CLI has
|
|
737
739
|
// async commands (including `sensors coverage`), so returning its promise keeps the
|
|
738
740
|
// process alive until their JSON/output contract has been completed.
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const collect_1 = require("../../../src/core/dashboard/collect");
|
|
4
7
|
const sanitize_1 = require("../../../src/core/dashboard/sanitize");
|
|
8
|
+
const store_1 = require("../../../src/core/evidence/store");
|
|
9
|
+
const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
5
13
|
const fixedNow = '2026-08-22T00:00:00.000Z';
|
|
6
14
|
describe('collectDashboardSnapshot', () => {
|
|
7
15
|
it('returns only a healthy machine section outside a project', () => {
|
|
@@ -35,13 +43,13 @@ describe('collectDashboardSnapshot', () => {
|
|
|
35
43
|
machine: () => ({ findings: [{ id: 'machine.preferences.missing', label: 'Preferences', state: 'missing' }] }),
|
|
36
44
|
project: () => ({ label: 'Demo', findings: [{ id: 'project.profile.missing', label: 'Profile', state: 'missing' }] }),
|
|
37
45
|
plans: () => Array.from({ length: 2000 }, (_, index) => ({ id: `plan.${index}`, label: `Plan ${index}`, state: 'ok' })),
|
|
38
|
-
execution: () => ({
|
|
46
|
+
execution: () => ({}),
|
|
39
47
|
},
|
|
40
48
|
});
|
|
41
49
|
expect(snapshot.sections.map((section) => section.id)).toEqual(['machine', 'project', 'planning', 'execution', 'qa', 'retro', 'history']);
|
|
42
50
|
expect(snapshot.sections.find((section) => section.id === 'machine')?.items[0].remediation).toBe('awm init');
|
|
43
51
|
expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toHaveLength(2000);
|
|
44
|
-
expect(snapshot.sections.find((section) => section.id === 'history')?.items).toHaveLength(
|
|
52
|
+
expect(snapshot.sections.find((section) => section.id === 'history')?.items).toHaveLength(0);
|
|
45
53
|
expect(JSON.stringify(snapshot)).not.toMatch(/score|ranking/i);
|
|
46
54
|
});
|
|
47
55
|
it('isolates optional adapter failures and omits unverified remediation', () => {
|
|
@@ -62,7 +70,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
62
70
|
cwd: process.cwd(), now: fixedNow,
|
|
63
71
|
adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined },
|
|
64
72
|
});
|
|
65
|
-
for (const id of ['execution', 'qa', 'retro'
|
|
73
|
+
for (const id of ['execution', 'qa', 'retro']) {
|
|
66
74
|
expect(snapshot.sections.find((section) => section.id === id)?.availability).toBe('unavailable');
|
|
67
75
|
}
|
|
68
76
|
});
|
|
@@ -83,7 +91,7 @@ describe('collectDashboardSnapshot', () => {
|
|
|
83
91
|
expect(snapshot.sections.find((section) => section.id === 'project')?.availability).toBe('unavailable');
|
|
84
92
|
expect(snapshot.sections.find((section) => section.id === 'planning')?.availability).toBe('available');
|
|
85
93
|
});
|
|
86
|
-
it.each(['execution', 'qa', 'retro'
|
|
94
|
+
it.each(['execution', 'qa', 'retro'])('isolates malformed %s findings', (key) => {
|
|
87
95
|
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: process.cwd(), now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => ({ [key]: [{ id: '', label: 'Profile', state: 'ok' }] }) } });
|
|
88
96
|
expect(snapshot.sections.find((section) => section.id === key)?.availability).toBe('unavailable');
|
|
89
97
|
});
|
|
@@ -142,6 +150,77 @@ describe('collectDashboardSnapshot', () => {
|
|
|
142
150
|
expect(snapshot.sections.map((section) => section.id)).toEqual(['machine']);
|
|
143
151
|
expect(snapshot.overall).toBe('degraded');
|
|
144
152
|
});
|
|
153
|
+
it('loads validated local evidence only after a project is detected and renders every cycle fact', () => {
|
|
154
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
|
|
155
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
156
|
+
(0, store_1.writeCycleEvidence)(root, (0, evidence_fixtures_1.cycleEvidenceFixture)());
|
|
157
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
158
|
+
const history = snapshot.sections.find((section) => section.id === 'history');
|
|
159
|
+
expect(snapshot.confidence).toBe('provisional');
|
|
160
|
+
expect(history?.availability).toBe('available');
|
|
161
|
+
expect(history?.items[0]?.detail).toContain('retries 1; QA 1/1; first-pass yes; cures awaiting_observation');
|
|
162
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
163
|
+
});
|
|
164
|
+
it('renders a blocked evidence cycle as an actionable history item with a verified remedy', () => {
|
|
165
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
|
|
166
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
167
|
+
(0, store_1.writeCycleEvidence)(root, { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleState: 'blocked', plan: { ref: 'docs/plans/current.md', state: 'blocked' } });
|
|
168
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
169
|
+
expect(snapshot.sections.find((section) => section.id === 'history')?.items).toEqual([
|
|
170
|
+
expect.objectContaining({ state: 'attention', remediation: 'awm preflight' }),
|
|
171
|
+
]);
|
|
172
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
173
|
+
});
|
|
174
|
+
it.each(['.awm', 'evidence', 'cycles'])('does not follow a symlinked %s evidence ancestor', (ancestor) => {
|
|
175
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
|
|
176
|
+
const external = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-external-'));
|
|
177
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
178
|
+
const externalCycles = ancestor === '.awm' ? path_1.default.join(external, 'evidence', 'cycles') : path_1.default.join(external, 'cycles');
|
|
179
|
+
fs_1.default.mkdirSync(externalCycles, { recursive: true });
|
|
180
|
+
fs_1.default.writeFileSync(path_1.default.join(externalCycles, `${'a'.repeat(64)}.json`), JSON.stringify((0, evidence_fixtures_1.cycleEvidenceFixture)()));
|
|
181
|
+
if (ancestor === '.awm')
|
|
182
|
+
fs_1.default.symlinkSync(external, path_1.default.join(root, '.awm'));
|
|
183
|
+
else if (ancestor === 'evidence') {
|
|
184
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.awm'));
|
|
185
|
+
fs_1.default.symlinkSync(external, path_1.default.join(root, '.awm', 'evidence'));
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.awm', 'evidence'), { recursive: true });
|
|
189
|
+
fs_1.default.symlinkSync(external, path_1.default.join(root, '.awm', 'evidence', 'cycles'));
|
|
190
|
+
}
|
|
191
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
192
|
+
const history = snapshot.sections.find((section) => section.id === 'history');
|
|
193
|
+
expect(history).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
|
|
194
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
195
|
+
fs_1.default.rmSync(external, { recursive: true, force: true });
|
|
196
|
+
});
|
|
197
|
+
it('does not follow a symlinked evidence file', () => {
|
|
198
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
|
|
199
|
+
const external = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-external-'));
|
|
200
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
201
|
+
const cycleId = 'a'.repeat(64);
|
|
202
|
+
const externalFile = path_1.default.join(external, `${cycleId}.json`);
|
|
203
|
+
fs_1.default.writeFileSync(externalFile, JSON.stringify((0, evidence_fixtures_1.cycleEvidenceFixture)()));
|
|
204
|
+
const directory = path_1.default.join(root, '.awm', 'evidence', 'cycles');
|
|
205
|
+
fs_1.default.mkdirSync(directory, { recursive: true });
|
|
206
|
+
fs_1.default.symlinkSync(externalFile, path_1.default.join(directory, `${cycleId}.json`));
|
|
207
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
208
|
+
expect(snapshot.sections.find((section) => section.id === 'history')).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
|
|
209
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
210
|
+
fs_1.default.rmSync(external, { recursive: true, force: true });
|
|
211
|
+
});
|
|
212
|
+
it.each(['duplicate', 'mismatch'])('does not accept %s evidence filenames', (caseName) => {
|
|
213
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-dashboard-'));
|
|
214
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
215
|
+
(0, store_1.writeCycleEvidence)(root, (0, evidence_fixtures_1.cycleEvidenceFixture)());
|
|
216
|
+
const directory = path_1.default.join(root, '.awm', 'evidence', 'cycles');
|
|
217
|
+
const record = (0, evidence_fixtures_1.cycleEvidenceFixture)();
|
|
218
|
+
const filename = caseName === 'duplicate' ? `${'b'.repeat(64)}.json` : `${'b'.repeat(64)}.json`;
|
|
219
|
+
fs_1.default.writeFileSync(path_1.default.join(directory, filename), JSON.stringify(caseName === 'duplicate' ? record : { ...record, cycleId: 'c'.repeat(64) }));
|
|
220
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({ cwd: root, now: fixedNow, adapters: { machine: () => ({ findings: [] }), project: () => ({ findings: [] }), plans: () => [], execution: () => undefined } });
|
|
221
|
+
expect(snapshot.sections.find((section) => section.id === 'history')).toEqual(expect.objectContaining({ availability: 'unavailable', items: [] }));
|
|
222
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
223
|
+
});
|
|
145
224
|
});
|
|
146
225
|
describe('sanitizeDashboardSource', () => {
|
|
147
226
|
it('removes hostile paths and secrets before rendering', () => {
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const collect_1 = require("../../../src/core/dashboard/collect");
|
|
7
|
+
const store_1 = require("../../../src/core/evidence/store");
|
|
8
|
+
const store_2 = require("../../../src/core/journal/store");
|
|
9
|
+
const types_1 = require("../../../src/core/journal/types");
|
|
10
|
+
const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const os_1 = __importDefault(require("os"));
|
|
14
|
+
const path_1 = __importDefault(require("path"));
|
|
4
15
|
function context() {
|
|
5
16
|
return {
|
|
6
17
|
machine: {},
|
|
@@ -67,4 +78,82 @@ describe('productionDashboardAdapters', () => {
|
|
|
67
78
|
]));
|
|
68
79
|
expect(JSON.stringify(snapshot)).not.toMatch(/remediationVerified|private|0\.145\.0|token/i);
|
|
69
80
|
});
|
|
81
|
+
it('derives plan lifecycle and execution evidence from validated local cycle records', () => {
|
|
82
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-production-evidence-'));
|
|
83
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
84
|
+
(0, store_1.writeCycleEvidence)(root, {
|
|
85
|
+
...(0, evidence_fixtures_1.cycleEvidenceFixture)(),
|
|
86
|
+
plan: { ref: 'docs/plans/current.md', state: 'executed' },
|
|
87
|
+
tasks: [{ id: 'task-1', attempts: 2, retries: 1 }],
|
|
88
|
+
qa: { findings: 1, fixes: 1, signatures: ['a'.repeat(64)] },
|
|
89
|
+
});
|
|
90
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
91
|
+
cwd: root, now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(context()),
|
|
92
|
+
});
|
|
93
|
+
expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toEqual([
|
|
94
|
+
expect.objectContaining({ detail: 'executed', state: 'ok' }),
|
|
95
|
+
]);
|
|
96
|
+
expect(snapshot.sections.find((section) => section.id === 'execution')?.items).toEqual([
|
|
97
|
+
expect.objectContaining({ state: 'ok' }),
|
|
98
|
+
]);
|
|
99
|
+
expect(snapshot.sections.find((section) => section.id === 'qa')?.items).toEqual([
|
|
100
|
+
expect.objectContaining({ state: 'ok' }),
|
|
101
|
+
]);
|
|
102
|
+
expect(snapshot.sections.find((section) => section.id === 'retro')?.items).toEqual([
|
|
103
|
+
expect.objectContaining({ state: 'ok' }),
|
|
104
|
+
]);
|
|
105
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
106
|
+
});
|
|
107
|
+
it.each([
|
|
108
|
+
['IN_PROGRESS', 'active', 'ok'],
|
|
109
|
+
['BLOCKED', 'blocked', 'attention'],
|
|
110
|
+
])('overlays the current branch journal %s over prior executed evidence', (status, expectedLifecycle, expectedState) => {
|
|
111
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-journal-overlay-'));
|
|
112
|
+
const branch = 'dashboard-overlay';
|
|
113
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
114
|
+
(0, child_process_1.execFileSync)('git', ['init', '--initial-branch', branch], { cwd: root, stdio: 'ignore' });
|
|
115
|
+
(0, child_process_1.execFileSync)('git', ['config', 'user.email', 'dashboard@example.test'], { cwd: root, stdio: 'ignore' });
|
|
116
|
+
(0, child_process_1.execFileSync)('git', ['config', 'user.name', 'Dashboard Test'], { cwd: root, stdio: 'ignore' });
|
|
117
|
+
(0, child_process_1.execFileSync)('git', ['commit', '--allow-empty', '-m', 'fixture'], { cwd: root, stdio: 'ignore' });
|
|
118
|
+
(0, store_1.writeCycleEvidence)(root, { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), plan: { ref: 'docs/plans/current.md', state: 'executed' } });
|
|
119
|
+
(0, store_2.initJournal)(root, branch);
|
|
120
|
+
const journal = (0, types_1.emptyState)(branch);
|
|
121
|
+
journal.cycle.status = status;
|
|
122
|
+
(0, store_2.writeJournal)(root, branch, journal);
|
|
123
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
124
|
+
cwd: root, now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(context()),
|
|
125
|
+
});
|
|
126
|
+
expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toEqual([
|
|
127
|
+
expect.objectContaining({ detail: expectedLifecycle, state: expectedState }),
|
|
128
|
+
]);
|
|
129
|
+
expect(JSON.stringify(snapshot)).not.toMatch(/dashboard-overlay|dashboard@example|Dashboard Test/i);
|
|
130
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
131
|
+
});
|
|
132
|
+
it.each([
|
|
133
|
+
['IN_PROGRESS', 'active', 'ok'],
|
|
134
|
+
['BLOCKED', 'blocked', 'attention'],
|
|
135
|
+
])('renders the current branch journal %s without prior evidence', (status, expectedLifecycle, expectedState) => {
|
|
136
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-dashboard-journal-only-'));
|
|
137
|
+
const branch = 'dashboard-journal-only';
|
|
138
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{}');
|
|
139
|
+
(0, child_process_1.execFileSync)('git', ['init', '--initial-branch', branch], { cwd: root, stdio: 'ignore' });
|
|
140
|
+
(0, child_process_1.execFileSync)('git', ['config', 'user.email', 'dashboard@example.test'], { cwd: root, stdio: 'ignore' });
|
|
141
|
+
(0, child_process_1.execFileSync)('git', ['config', 'user.name', 'Dashboard Test'], { cwd: root, stdio: 'ignore' });
|
|
142
|
+
(0, child_process_1.execFileSync)('git', ['commit', '--allow-empty', '-m', 'fixture'], { cwd: root, stdio: 'ignore' });
|
|
143
|
+
(0, store_2.initJournal)(root, branch);
|
|
144
|
+
const journal = (0, types_1.emptyState)(branch);
|
|
145
|
+
journal.cycle.status = status;
|
|
146
|
+
(0, store_2.writeJournal)(root, branch, journal);
|
|
147
|
+
const snapshot = (0, collect_1.collectDashboardSnapshot)({
|
|
148
|
+
cwd: root, now: '2026-08-22T00:00:00.000Z', adapters: (0, collect_1.productionDashboardAdapters)(context()),
|
|
149
|
+
});
|
|
150
|
+
expect(snapshot.sections.find((section) => section.id === 'planning')?.items).toEqual([
|
|
151
|
+
expect.objectContaining({ detail: expectedLifecycle, state: expectedState }),
|
|
152
|
+
]);
|
|
153
|
+
expect(snapshot.sections.find((section) => section.id === 'execution')).toEqual(expect.objectContaining({
|
|
154
|
+
availability: 'available', items: [expect.objectContaining({ state: expectedState })],
|
|
155
|
+
}));
|
|
156
|
+
expect(JSON.stringify(snapshot)).not.toMatch(/dashboard-journal-only|dashboard@example|Dashboard Test/i);
|
|
157
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
158
|
+
});
|
|
70
159
|
});
|
|
@@ -172,4 +172,20 @@ describe('renderDashboardHtml', () => {
|
|
|
172
172
|
expect(first).toContain('Cycle 499');
|
|
173
173
|
expect(first).toContain('Task 1999');
|
|
174
174
|
});
|
|
175
|
+
it('renders supplied plans and evidence facts in the dense project panels', () => {
|
|
176
|
+
const html = (0, render_html_1.renderDashboardHtml)((0, validate_1.validateDashboardSnapshotV1)(snapshot({
|
|
177
|
+
confidence: 'supported',
|
|
178
|
+
sections: [
|
|
179
|
+
{ id: 'machine', availability: 'available', items: [] }, { id: 'project', availability: 'available', items: [] },
|
|
180
|
+
{ 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: [] },
|
|
182
|
+
{ 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' }] },
|
|
183
|
+
],
|
|
184
|
+
})));
|
|
185
|
+
expect(html).toContain('Confidence: supported');
|
|
186
|
+
expect(html).toContain('Impact plan');
|
|
187
|
+
expect(html).toContain('Cycle abc');
|
|
188
|
+
expect(html).toContain('retries 2; QA 2/2; first-pass yes; cures supported');
|
|
189
|
+
expect(html).not.toContain('Progreso: sin observación');
|
|
190
|
+
});
|
|
175
191
|
});
|
|
@@ -62,4 +62,11 @@ describe('renderFullTerminal', () => {
|
|
|
62
62
|
expect(first).toContain('Cycle 499');
|
|
63
63
|
expect(first).not.toMatch(/score|ranking/i);
|
|
64
64
|
});
|
|
65
|
+
it('renders explicit confidence and every evidence metric without improvement claims', () => {
|
|
66
|
+
const output = (0, render_terminal_1.renderFullTerminal)((0, validate_1.validateDashboardSnapshotV1)(completeSnapshot({ confidence: 'supported' })));
|
|
67
|
+
expect(output).toContain('Confidence: supported');
|
|
68
|
+
expect(output).toContain('Eligible evidence rows: 1');
|
|
69
|
+
expect(output).toContain('5 minutes');
|
|
70
|
+
expect(output).not.toMatch(/trend|percentage|improvement/i);
|
|
71
|
+
});
|
|
65
72
|
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const capture_1 = require("../../../src/core/evidence/capture");
|
|
4
|
+
describe('captureCycleEvidence', () => {
|
|
5
|
+
test('derives counts and opaque signatures without journal prose or identities', () => {
|
|
6
|
+
const evidence = (0, capture_1.captureCycleEvidence)({
|
|
7
|
+
root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git',
|
|
8
|
+
planPath: 'plans/release.md',
|
|
9
|
+
journal: {
|
|
10
|
+
cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:10.000Z' },
|
|
11
|
+
journalId: 'local-repository-identity',
|
|
12
|
+
tasks: [{ id: 'task-a', attempts: 3 }],
|
|
13
|
+
verdicts: [{ id: 'v1', result: 'fail', fingerprint: 'unsafe-input', detail: 'Alice saw secret prompt', receivedAt: '2026-08-22T10:00:05.000Z' }],
|
|
14
|
+
fixes: [{ verdictId: 'v1', closed: true }],
|
|
15
|
+
},
|
|
16
|
+
gates: [{ required: true, passed: true }],
|
|
17
|
+
ledger: [{ signature: 'unsafe-input', polarity: 'win', ts: '2026-08-22T10:00:06.000Z' }],
|
|
18
|
+
pr: { provider: 'github', number: 12 },
|
|
19
|
+
});
|
|
20
|
+
expect(evidence).toMatchObject({
|
|
21
|
+
schema: 1, cycleState: 'completed', durationMs: 10_000,
|
|
22
|
+
plan: { ref: 'plans/release.md' }, tasks: [{ id: 'task-a', attempts: 3, retries: 2 }],
|
|
23
|
+
qa: { findings: 1, fixes: 1 }, gates: { required: 1, firstEvaluationsPassed: [true], firstPass: true },
|
|
24
|
+
pr: { provider: 'github', number: 12 },
|
|
25
|
+
});
|
|
26
|
+
expect(evidence.cycleId).toMatch(/^[a-f0-9]{64}$/);
|
|
27
|
+
expect(evidence.qa.signatures[0]).toMatch(/^[a-f0-9]{64}$/);
|
|
28
|
+
expect(evidence.cures[0]).toMatchObject({ signature: expect.stringMatching(/^[a-f0-9]{64}$/) });
|
|
29
|
+
expect(evidence.cures[0].signature).toBe(evidence.qa.signatures[0]);
|
|
30
|
+
expect(JSON.stringify(evidence)).not.toContain('Alice');
|
|
31
|
+
expect(JSON.stringify(evidence)).not.toContain('secret prompt');
|
|
32
|
+
});
|
|
33
|
+
test('derives cycle identity from stable repository identity, not checkout or journal identity', () => {
|
|
34
|
+
const source = {
|
|
35
|
+
planPath: 'plans/release.md', journal: { journalId: 'repository-identity', cycle: { status: 'BLOCKED', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [] }, gates: [], ledger: [],
|
|
36
|
+
};
|
|
37
|
+
const first = (0, capture_1.captureCycleEvidence)({ ...source, root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git' });
|
|
38
|
+
const second = (0, capture_1.captureCycleEvidence)({ ...source, root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', journal: { ...source.journal, journalId: 'different-journal-identity' } });
|
|
39
|
+
const distinct = (0, capture_1.captureCycleEvidence)({ ...source, root: process.cwd(), repositoryIdentity: 'git@example.test:other/repository.git' });
|
|
40
|
+
expect(first.cycleId).toBe(second.cycleId);
|
|
41
|
+
expect(first.cycleId).not.toBe(distinct.cycleId);
|
|
42
|
+
expect(first.cycleState).toBe('blocked');
|
|
43
|
+
});
|
|
44
|
+
test('captures a blocked cycle from its durable controller heartbeat when completedAt is absent', () => {
|
|
45
|
+
const evidence = (0, capture_1.captureCycleEvidence)({ root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md', journal: { cycle: { status: 'BLOCKED', startedAt: '2026-08-22T10:00:00.000Z' }, controllerHeartbeatAt: '2026-08-22T10:00:03.000Z', tasks: [], verdicts: [], fixes: [] }, gates: [], ledger: [] });
|
|
46
|
+
expect(evidence).toMatchObject({ cycleState: 'blocked', endedAt: '2026-08-22T10:00:03.000Z', durationMs: 3_000 });
|
|
47
|
+
});
|
|
48
|
+
test('counts inconclusive verdicts as adverse findings because they create fix obligations', () => {
|
|
49
|
+
const evidence = (0, capture_1.captureCycleEvidence)({
|
|
50
|
+
root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md',
|
|
51
|
+
journal: {
|
|
52
|
+
cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' },
|
|
53
|
+
tasks: [], verdicts: [{ id: 'v1', result: 'inconclusive', fingerprint: 'probe-unavailable', receivedAt: '2026-08-22T10:00:00.000Z' }],
|
|
54
|
+
fixes: [{ verdictId: 'v1', closed: true }],
|
|
55
|
+
}, gates: [], ledger: [],
|
|
56
|
+
});
|
|
57
|
+
expect(evidence.qa).toMatchObject({ findings: 1, fixes: 1, signatures: [expect.stringMatching(/^[a-f0-9]{64}$/)] });
|
|
58
|
+
});
|
|
59
|
+
test('counts only closed fixes that reference an adverse verdict', () => {
|
|
60
|
+
const evidence = (0, capture_1.captureCycleEvidence)({
|
|
61
|
+
root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md',
|
|
62
|
+
journal: {
|
|
63
|
+
cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [],
|
|
64
|
+
verdicts: [
|
|
65
|
+
{ id: 'v-fail', result: 'fail', fingerprint: 'failure', receivedAt: '2026-08-22T10:00:00.000Z' },
|
|
66
|
+
{ id: 'v-inconclusive', result: 'inconclusive', fingerprint: 'unknown', receivedAt: '2026-08-22T10:00:00.000Z' },
|
|
67
|
+
],
|
|
68
|
+
fixes: [{ verdictId: 'v-fail', closed: true }, { verdictId: 'v-inconclusive', closed: false }],
|
|
69
|
+
}, gates: [], ledger: [],
|
|
70
|
+
});
|
|
71
|
+
expect(evidence.qa).toMatchObject({ findings: 2, fixes: 1 });
|
|
72
|
+
});
|
|
73
|
+
test.each([
|
|
74
|
+
{ verdicts: [], verdictId: 'missing' },
|
|
75
|
+
{ verdicts: [{ id: 'v-pass', result: 'pass', fingerprint: 'passed', receivedAt: '2026-08-22T10:00:00.000Z' }], verdictId: 'v-pass' },
|
|
76
|
+
])('rejects a fix whose verdict reference is absent or not adverse', ({ verdicts, verdictId }) => {
|
|
77
|
+
expect(() => (0, capture_1.captureCycleEvidence)({
|
|
78
|
+
root: process.cwd(), repositoryIdentity: 'git@example.test:team/repository.git', planPath: 'plans/release.md',
|
|
79
|
+
journal: { cycle: { status: 'COMPLETE', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts, fixes: [{ verdictId, closed: true }] }, gates: [], ledger: [],
|
|
80
|
+
})).toThrow(/fix verdictId/i);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
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 os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const evidence_1 = require("../../../src/commands/evidence");
|
|
10
|
+
describe('evidence capture CLI boundary', () => {
|
|
11
|
+
let root;
|
|
12
|
+
beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-command-')); });
|
|
13
|
+
afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
14
|
+
test('returns exit 2 for a missing or invalid plan', () => {
|
|
15
|
+
expect((0, evidence_1.runEvidenceCapture)(root, undefined).code).toBe(2);
|
|
16
|
+
expect((0, evidence_1.runEvidenceCapture)(root, '../secret.md').code).toBe(2);
|
|
17
|
+
});
|
|
18
|
+
test('returns exactly the captured cycle id on success', () => {
|
|
19
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
|
|
20
|
+
expect((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: [] })).toEqual(expect.objectContaining({ code: 0, stdout: expect.stringMatching(/^[a-f0-9]{64}\n$/) }));
|
|
21
|
+
});
|
|
22
|
+
test('uses the first attempted gate evaluation instead of final satisfaction', () => {
|
|
23
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
|
|
24
|
+
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: [], cycleVerificationPlan: [{ id: 'gate', kind: 'test', satisfiedBy: 'retry' }], jobs: { first: { id: 'first', satisfies: ['gate'], verdict: 'fail', phaseTimestamps: { received: '2026-08-22T10:00:00.000Z' } }, retry: { id: 'retry', satisfies: ['gate'], attemptOf: 'first', verdict: 'pass', phaseTimestamps: { received: '2026-08-22T10:00:01.000Z' } } } }, ledger: [] });
|
|
25
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).gates.firstEvaluationsPassed).toEqual([false]);
|
|
26
|
+
});
|
|
27
|
+
test('orders independent root evaluations by durable timestamp then id', () => {
|
|
28
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
|
|
29
|
+
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:03.000Z' }, tasks: [], verdicts: [], fixes: [], cycleVerificationPlan: [{ id: 'gate', kind: 'test', satisfiedBy: 'late-pass' }], jobs: { late: { id: 'late-pass', satisfies: ['gate'], verdict: 'pass', phaseTimestamps: { received: '2026-08-22T10:00:02.000Z' } }, early: { id: 'early-fail', satisfies: ['gate'], verdict: 'fail', phaseTimestamps: { received: '2026-08-22T10:00:01.000Z' } } } }, ledger: [] });
|
|
30
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).gates.firstEvaluationsPassed).toEqual([false]);
|
|
31
|
+
});
|
|
32
|
+
test('uses the first review verdict rather than satisfiedBy final verdict', () => {
|
|
33
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n');
|
|
34
|
+
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: [], fixes: [], cycleVerificationPlan: [{ id: 'review-gate', kind: 'review', satisfiedBy: 'final' }], jobs: {}, verdicts: [{ id: 'first', obligationId: 'review-gate', result: 'fail', fingerprint: 'review-gate', receivedAt: '2026-08-22T10:00:00.000Z' }, { id: 'final', obligationId: 'review-gate', result: 'pass', fingerprint: 'review-gate', receivedAt: '2026-08-22T10:00:01.000Z' }] }, ledger: [] });
|
|
35
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).gates.firstEvaluationsPassed).toEqual([false]);
|
|
36
|
+
});
|
|
37
|
+
test('derives retro_pending from completed checklist tasks and the QA marker in the plan file', () => {
|
|
38
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n<!-- awm-qa-complete: 2026-08-22 -->\n- [x] Build\n- [X] Verify\n');
|
|
39
|
+
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: [] });
|
|
40
|
+
expect(result.code).toBe(0);
|
|
41
|
+
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');
|
|
42
|
+
});
|
|
43
|
+
test('uses a blocked journal state over completed plan markers', () => {
|
|
44
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n<!-- awm-qa-complete: 2026-08-22 -->\n<!-- awm-retro-complete: 2026-08-22 -->\n- [x] Build\n');
|
|
45
|
+
const result = (0, evidence_1.runEvidenceCapture)(root, 'plan.md', { repositoryIdentity: 'git@example.test:team/repository.git', journal: { journalId: 'ignored', cycle: { status: 'BLOCKED', startedAt: '2026-08-22T10:00:00.000Z', completedAt: '2026-08-22T10:00:01.000Z' }, tasks: [], verdicts: [], fixes: [], jobs: {}, cycleVerificationPlan: [] }, ledger: [] });
|
|
46
|
+
expect(result.code).toBe(0);
|
|
47
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(root, '.awm', 'evidence', 'cycles', result.stdout.trim() + '.json'), 'utf8')).plan.state).toBe('blocked');
|
|
48
|
+
});
|
|
49
|
+
test('does not let Release A markers classify a multi-release plan whose current Release B has no lifecycle markers', () => {
|
|
50
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n<!-- awm-qa-complete: Release A / #86 -->\n<!-- awm-retro-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');
|
|
51
|
+
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: [] });
|
|
52
|
+
expect(result.code).toBe(0);
|
|
53
|
+
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');
|
|
54
|
+
});
|
|
55
|
+
test('rejects a malformed checklist instead of silently classifying a plan', () => {
|
|
56
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'plan.md'), '# plan\n- [z] Unknown state\n');
|
|
57
|
+
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
|
+
expect(result).toEqual(expect.objectContaining({ code: 2, error: expect.stringMatching(/invalid checklist/i) }));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const history_1 = require("../../../src/core/evidence/history");
|
|
4
|
+
const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
|
|
5
|
+
describe('evidence history', () => {
|
|
6
|
+
it.each([[0, 'none'], [1, 'provisional'], [2, 'observing'], [4, 'observing'], [5, 'supported']])('classifies %i eligible cycles as %s', (count, expected) => expect((0, history_1.confidenceForCycles)(count)).toBe(expected));
|
|
7
|
+
it.each([
|
|
8
|
+
[0, false, 'awaiting_observation'], [1, false, 'observing'], [2, false, 'observing'],
|
|
9
|
+
[3, false, 'supported'], [1, true, 'recurred'],
|
|
10
|
+
])('classifies cure observation honestly', (laterEligibleCycles, recurred, expected) => {
|
|
11
|
+
expect((0, history_1.classifyCure)({ laterEligibleCycles, recurred })).toBe(expected);
|
|
12
|
+
});
|
|
13
|
+
it('validates every record, retains every eligible row, and sorts stably by timestamp and cycle id', () => {
|
|
14
|
+
const later = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'd'.repeat(64), startedAt: '2026-08-22T11:00:00.000Z', endedAt: '2026-08-22T11:01:00.000Z', cures: [] };
|
|
15
|
+
const sameTimestamp = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: '0'.repeat(64), cures: [] };
|
|
16
|
+
const history = (0, history_1.buildEvidenceHistory)([later, (0, evidence_fixtures_1.cycleEvidenceFixture)(), sameTimestamp]);
|
|
17
|
+
expect(history.confidence).toBe('observing');
|
|
18
|
+
expect(history.empty).toBe(false);
|
|
19
|
+
expect(history.cycles.map((cycle) => cycle.cycleId)).toEqual(['0'.repeat(64), 'a'.repeat(64), 'd'.repeat(64)]);
|
|
20
|
+
expect(() => (0, history_1.buildEvidenceHistory)([{ ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), durationMs: -1 }])).toThrow(/duration/i);
|
|
21
|
+
});
|
|
22
|
+
it('reports zero cycles explicitly without a trend, percentage, or improvement claim', () => {
|
|
23
|
+
const history = (0, history_1.buildEvidenceHistory)([]);
|
|
24
|
+
expect(history).toEqual(expect.objectContaining({ empty: true, confidence: 'none', cycles: [] }));
|
|
25
|
+
expect(JSON.stringify(history)).not.toMatch(/trend|percentage|improvement/i);
|
|
26
|
+
});
|
|
27
|
+
it('retains blocked records but excludes them from confidence and cure observation windows', () => {
|
|
28
|
+
const blocked = Array.from({ length: 5 }, (_, index) => ({
|
|
29
|
+
...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: `${index}`.repeat(64), cycleState: 'blocked', cures: [],
|
|
30
|
+
}));
|
|
31
|
+
const history = (0, history_1.buildEvidenceHistory)(blocked);
|
|
32
|
+
expect(history.cycles).toHaveLength(5);
|
|
33
|
+
expect(history.confidence).toBe('none');
|
|
34
|
+
const cured = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'c'.repeat(64), endedAt: '2026-08-22T10:01:00.000Z' };
|
|
35
|
+
const laterBlocked = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'd'.repeat(64), cycleState: 'blocked', startedAt: '2026-08-22T11:00:00.000Z', endedAt: '2026-08-22T11:01:00.000Z', cures: [] };
|
|
36
|
+
const laterCompleted = { ...(0, evidence_fixtures_1.cycleEvidenceFixture)(), cycleId: 'e'.repeat(64), startedAt: '2026-08-22T12:00:00.000Z', endedAt: '2026-08-22T12:01:00.000Z', cures: [] };
|
|
37
|
+
const mixed = (0, history_1.buildEvidenceHistory)([cured, laterBlocked, laterCompleted]);
|
|
38
|
+
expect(mixed.confidence).toBe('observing');
|
|
39
|
+
expect(mixed.cycles[0].cureEfficacy[0].efficacy).toBe('observing');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
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 os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const evidence_fixtures_1 = require("../../helpers/evidence-fixtures");
|
|
10
|
+
const store_1 = require("../../../src/core/evidence/store");
|
|
11
|
+
describe('cycle evidence store', () => {
|
|
12
|
+
let root;
|
|
13
|
+
beforeEach(() => { root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-')); });
|
|
14
|
+
afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
15
|
+
test('durably replaces one observation for the same opaque cycle id', () => {
|
|
16
|
+
const evidence = (0, evidence_fixtures_1.cycleEvidenceFixture)();
|
|
17
|
+
(0, store_1.writeCycleEvidence)(root, evidence);
|
|
18
|
+
(0, store_1.writeCycleEvidence)(root, { ...evidence, qa: { ...evidence.qa, fixes: 0 } });
|
|
19
|
+
const dir = path_1.default.join(root, '.awm', 'evidence', 'cycles');
|
|
20
|
+
expect(fs_1.default.readdirSync(dir)).toEqual([`${evidence.cycleId}.json`]);
|
|
21
|
+
expect(JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, `${evidence.cycleId}.json`), 'utf8')).qa.fixes).toBe(0);
|
|
22
|
+
});
|
|
23
|
+
test('rejects symlinked evidence ancestors without writing outside root', () => {
|
|
24
|
+
if (process.platform === 'win32')
|
|
25
|
+
return;
|
|
26
|
+
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-evidence-outside-'));
|
|
27
|
+
fs_1.default.symlinkSync(outside, path_1.default.join(root, '.awm'));
|
|
28
|
+
expect(() => (0, store_1.writeCycleEvidence)(root, (0, evidence_fixtures_1.cycleEvidenceFixture)())).toThrow(/symlink/);
|
|
29
|
+
expect(fs_1.default.readdirSync(outside)).toEqual([]);
|
|
30
|
+
fs_1.default.rmSync(outside, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
});
|