hadara 0.3.2 → 0.3.3
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/README.md +49 -34
- package/dist/cli/context.js +110 -0
- package/dist/cli/help.js +6 -7
- package/dist/cli/init.js +56 -58
- package/dist/cli/main.js +12 -0
- package/dist/cli/session.js +37 -0
- package/dist/cli/task.js +52 -0
- package/dist/context/code-graph-extractor.js +123 -0
- package/dist/context/code-index.js +1154 -0
- package/dist/context/context-cache-store.js +925 -0
- package/dist/context/context-graph-builder.js +341 -0
- package/dist/context/context-graph.js +42 -0
- package/dist/context/context-pack.js +457 -0
- package/dist/context/context-slice-boundary.js +37 -0
- package/dist/context/context-slice.js +487 -0
- package/dist/context/document-extractors.js +343 -0
- package/dist/context/evidence-extractors.js +179 -0
- package/dist/context/extractor-contract.js +166 -0
- package/dist/context/registry-extractors.js +177 -0
- package/dist/context/release-extractors.js +175 -0
- package/dist/context/session-start.js +297 -0
- package/dist/context/source-manifest.js +566 -0
- package/dist/context/state-projection.js +209 -0
- package/dist/context/task-extractors.js +168 -0
- package/dist/core/schema.js +26 -0
- package/dist/harness/validate.js +7 -8
- package/dist/schemas/code-index.schema.json +173 -0
- package/dist/schemas/context-cache-record.schema.json +56 -0
- package/dist/schemas/context-cache-status.schema.json +167 -0
- package/dist/schemas/context-cache-warm.schema.json +222 -0
- package/dist/schemas/context-graph.schema.json +286 -0
- package/dist/schemas/context-pack.schema.json +246 -0
- package/dist/schemas/context-slice.schema.json +94 -0
- package/dist/schemas/context-source-manifest.schema.json +147 -0
- package/dist/schemas/schema-index.json +91 -0
- package/dist/schemas/session-start.schema.json +158 -0
- package/dist/schemas/task-close-repair-plan.schema.json +67 -0
- package/dist/schemas/task-context.schema.json +125 -0
- package/dist/schemas/task-finalize.schema.json +98 -0
- package/dist/schemas/task-lifecycle.schema.json +84 -0
- package/dist/services/capability-registry.js +266 -9
- package/dist/services/lifecycle-guide.js +23 -29
- package/dist/services/protocol-consistency.js +6 -8
- package/dist/services/workbench-next-actions.js +2 -0
- package/dist/task/acceptance.js +171 -0
- package/dist/task/task-close-repair-plan.js +190 -0
- package/dist/task/task-close.js +34 -35
- package/dist/task/task-finalize.js +377 -0
- package/dist/task/task-lifecycle.js +210 -0
- package/dist/task/task-next.js +10 -1
- package/dist/task/task-ready.js +4 -0
- package/package.json +1 -1
|
@@ -13,6 +13,7 @@ const protocol_profile_1 = require("./protocol-profile");
|
|
|
13
13
|
const evidence_lint_1 = require("./evidence-lint");
|
|
14
14
|
const markdown_table_1 = require("./markdown-table");
|
|
15
15
|
const state_projection_1 = require("./state-projection");
|
|
16
|
+
const acceptance_1 = require("../task/acceptance");
|
|
16
17
|
const task_capsule_1 = require("../task/task-capsule");
|
|
17
18
|
const REQUIRED_TASK_FILES = Object.keys(task_capsule_1.TASK_FILES);
|
|
18
19
|
const DONE_STATUSES = new Set(['done']);
|
|
@@ -656,25 +657,22 @@ function checkDoneAcceptance(projectRoot, task, taskLooksDone, issues) {
|
|
|
656
657
|
if (!node_fs_1.default.existsSync(acceptancePath))
|
|
657
658
|
return;
|
|
658
659
|
const content = node_fs_1.default.readFileSync(acceptancePath, 'utf8');
|
|
659
|
-
const
|
|
660
|
-
const pendingRows = rows.filter((cells) => {
|
|
661
|
-
const status = cells[2]?.trim().toLowerCase();
|
|
662
|
-
return !status || status === 'pending' || status === 'blocked';
|
|
663
|
-
});
|
|
660
|
+
const acceptance = (0, acceptance_1.analyzeAcceptanceReadiness)(content);
|
|
664
661
|
const checklistPending = content
|
|
665
662
|
.split(/\r?\n/)
|
|
666
663
|
.map((line) => line.trim())
|
|
667
664
|
.some((line) => /^-\s+\[\s\]/.test(line));
|
|
668
|
-
if (
|
|
665
|
+
if (acceptance.blockers.length > 0 || (acceptance.rows.length === 0 && checklistPending)) {
|
|
666
|
+
const blockerSummary = acceptance.blockers.length > 0 ? ` Blockers: ${acceptance.blockers.map((blocker) => `${blocker.code}(${blocker.row.id})`).join(', ')}.` : '';
|
|
669
667
|
pushIssue(issues, {
|
|
670
668
|
code: 'TASK_DONE_ACCEPTANCE_PENDING',
|
|
671
669
|
severity: 'error',
|
|
672
670
|
area: 'validation',
|
|
673
671
|
taskId: task.id,
|
|
674
672
|
path: toPortablePath(node_path_1.default.relative(projectRoot, acceptancePath)),
|
|
675
|
-
message:
|
|
673
|
+
message: `Task is marked Done but ACCEPTANCE.md still has pending, blocked, or in-progress criteria.${blockerSummary}`,
|
|
676
674
|
expected: 'all acceptance criteria complete',
|
|
677
|
-
actual: '
|
|
675
|
+
actual: 'incomplete criteria found'
|
|
678
676
|
});
|
|
679
677
|
}
|
|
680
678
|
}
|
|
@@ -5,6 +5,8 @@ exports.createWorkbenchNextAction = createWorkbenchNextAction;
|
|
|
5
5
|
function buildWorkbenchNextActions(input) {
|
|
6
6
|
const actions = new Map();
|
|
7
7
|
for (const issue of input.issues) {
|
|
8
|
+
if (input.closed && issue.severity !== 'error' && issue.code.includes('HANDOFF'))
|
|
9
|
+
continue;
|
|
8
10
|
addIssueAction(actions, input.taskId, issue);
|
|
9
11
|
}
|
|
10
12
|
if (input.evidenceRecords === 0) {
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.analyzeAcceptanceReadiness = analyzeAcceptanceReadiness;
|
|
4
|
+
exports.parseAcceptanceRows = parseAcceptanceRows;
|
|
5
|
+
const markdown_table_1 = require("../services/markdown-table");
|
|
6
|
+
function analyzeAcceptanceReadiness(content) {
|
|
7
|
+
const rows = parseAcceptanceRows(content);
|
|
8
|
+
const blockers = rows.flatMap(blockersForAcceptanceRow);
|
|
9
|
+
return {
|
|
10
|
+
rows,
|
|
11
|
+
blockers,
|
|
12
|
+
summary: {
|
|
13
|
+
total: rows.length,
|
|
14
|
+
required: rows.filter((row) => row.required).length,
|
|
15
|
+
met: rows.filter((row) => row.status === 'Met').length,
|
|
16
|
+
unresolvedRequired: blockers.filter((blocker) => blocker.code === 'ACCEPTANCE_REQUIRED_UNRESOLVED').length,
|
|
17
|
+
deferred: rows.filter((row) => row.status === 'Deferred').length,
|
|
18
|
+
followUps: rows.filter((row) => row.status === 'Follow-up Created').length,
|
|
19
|
+
acceptedRisks: rows.filter((row) => row.status === 'Accepted Risk').length
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function parseAcceptanceRows(content) {
|
|
24
|
+
const rows = (0, markdown_table_1.parseMarkdownRows)(content);
|
|
25
|
+
const headerIndex = rows.findIndex((row) => headerIndexFor(row, 'id') >= 0 && headerIndexFor(row, 'status') >= 0);
|
|
26
|
+
const header = headerIndex >= 0 ? rows[headerIndex] : [];
|
|
27
|
+
const dataRows = (headerIndex >= 0 ? rows.slice(headerIndex + 1) : rows).filter((cells) => /^AC-\d+$/i.test(cells[0] ?? ''));
|
|
28
|
+
return dataRows.map((cells) => {
|
|
29
|
+
const allText = cells.join(' ');
|
|
30
|
+
const status = normalizeAcceptanceStatus(cellByHeader(cells, header, 'status') ?? cells[2] ?? '');
|
|
31
|
+
return {
|
|
32
|
+
id: cells[0] ?? '',
|
|
33
|
+
criterion: cellByHeader(cells, header, 'criterion') ?? cells[1] ?? '',
|
|
34
|
+
origin: normalizeAcceptanceOrigin(cellByHeader(cells, header, 'origin')),
|
|
35
|
+
required: normalizeBooleanCell(cellByHeader(cells, header, 'required'), true),
|
|
36
|
+
deferrable: normalizeBooleanCell(cellByHeader(cells, header, 'deferrable'), false),
|
|
37
|
+
status,
|
|
38
|
+
rawStatus: cellByHeader(cells, header, 'status') ?? cells[2] ?? '',
|
|
39
|
+
evidenceRefs: extractRefs(allText, /\bev:T-\d{4}:[A-Za-z0-9]+\b/g),
|
|
40
|
+
decisionRefs: extractRefs(allText, /\bD-[A-Za-z0-9._-]+\b/g),
|
|
41
|
+
riskRefs: extractRefs(allText, /\bR-[A-Za-z0-9._-]+\b/g),
|
|
42
|
+
followUpRefs: extractRefs(allText, /\b(?:T-\d{4}|DEBT-[A-Za-z0-9._-]+|GH-\d+)\b|hadara\s+task\s+create\s+["'`][^"'`]+["'`]/gi)
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function blockersForAcceptanceRow(row) {
|
|
47
|
+
const blockers = [];
|
|
48
|
+
if (row.required && ['Not Met', 'In Progress', 'Pending', 'TBD', 'Blocked', 'Partial'].includes(row.status)) {
|
|
49
|
+
blockers.push({
|
|
50
|
+
row,
|
|
51
|
+
code: 'ACCEPTANCE_REQUIRED_UNRESOLVED',
|
|
52
|
+
message: `${row.id} is required but remains ${row.status}.`
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (row.status === 'Unknown') {
|
|
56
|
+
blockers.push({
|
|
57
|
+
row,
|
|
58
|
+
code: 'ACCEPTANCE_STATUS_UNKNOWN',
|
|
59
|
+
message: `${row.id} has unknown acceptance status "${row.rawStatus}".`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (['Deferred', 'Follow-up Created', 'Accepted Risk'].includes(row.status)) {
|
|
63
|
+
if (row.required && !row.deferrable) {
|
|
64
|
+
blockers.push({
|
|
65
|
+
row,
|
|
66
|
+
code: 'ACCEPTANCE_NONDEFERRABLE_DEFERRED',
|
|
67
|
+
message: `${row.id} is required and non-deferrable, so ${row.status} cannot close as Done.`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (row.decisionRefs.length === 0) {
|
|
71
|
+
blockers.push({
|
|
72
|
+
row,
|
|
73
|
+
code: 'ACCEPTANCE_DEFERRED_WITHOUT_DECISION',
|
|
74
|
+
message: `${row.id} is ${row.status} without a decision reference.`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (row.status === 'Follow-up Created' && row.followUpRefs.length === 0) {
|
|
79
|
+
blockers.push({
|
|
80
|
+
row,
|
|
81
|
+
code: 'ACCEPTANCE_FOLLOWUP_WITHOUT_FOLLOWUP',
|
|
82
|
+
message: `${row.id} is Follow-up Created without a concrete follow-up reference.`
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (row.status === 'Accepted Risk' && row.riskRefs.length === 0) {
|
|
86
|
+
blockers.push({
|
|
87
|
+
row,
|
|
88
|
+
code: 'ACCEPTANCE_ACCEPTED_RISK_WITHOUT_RISK_RECORD',
|
|
89
|
+
message: `${row.id} is Accepted Risk without a risk reference.`
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return blockers;
|
|
93
|
+
}
|
|
94
|
+
function cellByHeader(cells, header, name) {
|
|
95
|
+
const index = headerIndexFor(header, name);
|
|
96
|
+
return index >= 0 ? cells[index] : undefined;
|
|
97
|
+
}
|
|
98
|
+
function headerIndexFor(header, name) {
|
|
99
|
+
const expected = normalizeHeader(name);
|
|
100
|
+
return header.findIndex((cell) => normalizeHeader(cell) === expected);
|
|
101
|
+
}
|
|
102
|
+
function normalizeHeader(value) {
|
|
103
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
104
|
+
}
|
|
105
|
+
function normalizeAcceptanceStatus(value) {
|
|
106
|
+
const normalized = value.trim().toLowerCase().replace(/[-_]+/g, ' ').replace(/\s+/g, ' ');
|
|
107
|
+
switch (normalized) {
|
|
108
|
+
case 'met':
|
|
109
|
+
case 'done':
|
|
110
|
+
case 'complete':
|
|
111
|
+
case 'completed':
|
|
112
|
+
return 'Met';
|
|
113
|
+
case 'not met':
|
|
114
|
+
return 'Not Met';
|
|
115
|
+
case 'in progress':
|
|
116
|
+
return 'In Progress';
|
|
117
|
+
case 'pending':
|
|
118
|
+
return 'Pending';
|
|
119
|
+
case 'tbd':
|
|
120
|
+
return 'TBD';
|
|
121
|
+
case 'deferred':
|
|
122
|
+
return 'Deferred';
|
|
123
|
+
case 'blocked':
|
|
124
|
+
return 'Blocked';
|
|
125
|
+
case 'partial':
|
|
126
|
+
return 'Partial';
|
|
127
|
+
case 'not applicable':
|
|
128
|
+
case 'n/a':
|
|
129
|
+
case 'na':
|
|
130
|
+
return 'Not Applicable';
|
|
131
|
+
case 'superseded':
|
|
132
|
+
return 'Superseded';
|
|
133
|
+
case 'follow up created':
|
|
134
|
+
case 'followup created':
|
|
135
|
+
return 'Follow-up Created';
|
|
136
|
+
case 'accepted risk':
|
|
137
|
+
return 'Accepted Risk';
|
|
138
|
+
default:
|
|
139
|
+
return 'Unknown';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function normalizeAcceptanceOrigin(value) {
|
|
143
|
+
const normalized = (value ?? '').trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
144
|
+
switch (normalized) {
|
|
145
|
+
case 'planned':
|
|
146
|
+
return 'planned';
|
|
147
|
+
case 'discovered':
|
|
148
|
+
return 'discovered';
|
|
149
|
+
case 'reviewer-feedback':
|
|
150
|
+
return 'reviewer-feedback';
|
|
151
|
+
case 'generated':
|
|
152
|
+
return 'generated';
|
|
153
|
+
case 'migration':
|
|
154
|
+
return 'migration';
|
|
155
|
+
default:
|
|
156
|
+
return 'original';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function normalizeBooleanCell(value, defaultValue) {
|
|
160
|
+
const normalized = (value ?? '').trim().toLowerCase();
|
|
161
|
+
if (!normalized)
|
|
162
|
+
return defaultValue;
|
|
163
|
+
if (['yes', 'y', 'true', '1', 'required'].includes(normalized))
|
|
164
|
+
return true;
|
|
165
|
+
if (['no', 'n', 'false', '0', 'optional'].includes(normalized))
|
|
166
|
+
return false;
|
|
167
|
+
return defaultValue;
|
|
168
|
+
}
|
|
169
|
+
function extractRefs(value, pattern) {
|
|
170
|
+
return [...new Set([...value.matchAll(pattern)].map((match) => match[0]))];
|
|
171
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTaskCloseRepairPlanReport = createTaskCloseRepairPlanReport;
|
|
4
|
+
exports.formatTaskCloseRepairPlanReport = formatTaskCloseRepairPlanReport;
|
|
5
|
+
const lifecycle_next_actions_1 = require("./lifecycle-next-actions");
|
|
6
|
+
const task_close_1 = require("./task-close");
|
|
7
|
+
function createTaskCloseRepairPlanReport(projectRoot, taskId, options = {}) {
|
|
8
|
+
const actor = options.actor ?? (0, lifecycle_next_actions_1.defaultTaskLifecycleActor)();
|
|
9
|
+
const reports = {
|
|
10
|
+
close: (0, task_close_1.createTaskCloseReport)(projectRoot, taskId, 'dry-run', { actor }),
|
|
11
|
+
audit: (0, task_close_1.createTaskAuditCloseReport)(projectRoot, taskId, { actor })
|
|
12
|
+
};
|
|
13
|
+
const classification = classifyRepair(reports);
|
|
14
|
+
const causes = collectCauses(reports, classification);
|
|
15
|
+
const nextActions = createRepairNextActions(taskId, classification, reports);
|
|
16
|
+
const issues = collectIssues(reports);
|
|
17
|
+
return {
|
|
18
|
+
schemaVersion: 'hadara.task.closeRepairPlan.v1',
|
|
19
|
+
command: 'task.close-repair-plan',
|
|
20
|
+
ok: !reports.close.issues.some((issue) => issue.code === 'TASK_NOT_FOUND') && !reports.audit.issues.some((issue) => issue.code === 'TASK_NOT_FOUND'),
|
|
21
|
+
readOnly: true,
|
|
22
|
+
taskId,
|
|
23
|
+
generatedAt: new Date().toISOString(),
|
|
24
|
+
actor,
|
|
25
|
+
classification,
|
|
26
|
+
repairNeeded: classification !== 'closed-valid',
|
|
27
|
+
summary: summarizeClassification(classification, reports),
|
|
28
|
+
evidence: createEvidenceSummary(reports.audit),
|
|
29
|
+
causes,
|
|
30
|
+
...(nextActions[0] ? { primaryNextAction: nextActions[0] } : {}),
|
|
31
|
+
nextActions,
|
|
32
|
+
issues
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function formatTaskCloseRepairPlanReport(report) {
|
|
36
|
+
const lines = [`[HADARA] task close-repair-plan ${report.taskId}: ${report.classification}`];
|
|
37
|
+
lines.push(`readOnly=${report.readOnly} repairNeeded=${report.repairNeeded} ok=${report.ok}`);
|
|
38
|
+
lines.push(report.summary);
|
|
39
|
+
if (report.primaryNextAction)
|
|
40
|
+
lines.push(`next=${report.primaryNextAction.command ?? report.primaryNextAction.summary ?? report.primaryNextAction.id}`);
|
|
41
|
+
for (const cause of report.causes)
|
|
42
|
+
lines.push(`[${cause.severity}] ${cause.code}: ${cause.summary}`);
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
function classifyRepair(reports) {
|
|
46
|
+
const audit = reports.audit;
|
|
47
|
+
if (reports.close.issues.some((issue) => issue.code === 'TASK_NOT_FOUND') || audit.issues.some((issue) => issue.code === 'TASK_NOT_FOUND'))
|
|
48
|
+
return 'unknown';
|
|
49
|
+
if (!audit.auditVerdict.closeEvidenceFound)
|
|
50
|
+
return 'not-closed';
|
|
51
|
+
if ((audit.closeEvidenceAudit?.duplicateCloseEvidenceCount ?? 0) > 0)
|
|
52
|
+
return 'duplicate-close-proof';
|
|
53
|
+
if (!audit.auditVerdict.closeEvidenceValid || audit.auditVerdict.verdict === 'close-evidence-invalid')
|
|
54
|
+
return 'closed-invalid';
|
|
55
|
+
if (audit.auditVerdict.reportHashMatches === false || audit.auditVerdict.sourceHashMatches === false || audit.auditVerdict.verdict === 'closed-with-drift-warnings')
|
|
56
|
+
return 'closed-stale';
|
|
57
|
+
if (audit.auditVerdict.verdict === 'closed-valid')
|
|
58
|
+
return 'closed-valid';
|
|
59
|
+
return 'unknown';
|
|
60
|
+
}
|
|
61
|
+
function summarizeClassification(classification, reports) {
|
|
62
|
+
switch (classification) {
|
|
63
|
+
case 'not-closed':
|
|
64
|
+
return 'No valid close evidence exists. Run ready if needed, then review and execute close.';
|
|
65
|
+
case 'closed-stale':
|
|
66
|
+
return 'Close evidence exists but recorded source or validation hashes no longer match current state.';
|
|
67
|
+
case 'closed-invalid':
|
|
68
|
+
return 'Close-like evidence exists but the latest close proof is invalid or malformed.';
|
|
69
|
+
case 'duplicate-close-proof':
|
|
70
|
+
return 'Multiple close proofs share conflicting or duplicate identity; inspect latest proof and append a fresh proof if needed.';
|
|
71
|
+
case 'closed-valid':
|
|
72
|
+
return 'Close proof is current and valid. No repair is needed.';
|
|
73
|
+
case 'unknown':
|
|
74
|
+
return reports.close.issues.some((issue) => issue.code === 'TASK_NOT_FOUND') ? 'Task was not found.' : 'Close repair state could not be classified.';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function createEvidenceSummary(audit) {
|
|
78
|
+
return {
|
|
79
|
+
closeEvidenceFound: audit.auditVerdict.closeEvidenceFound,
|
|
80
|
+
closeEvidenceValid: audit.auditVerdict.closeEvidenceValid,
|
|
81
|
+
closeEvidenceRecords: audit.summary.closeEvidenceRecords,
|
|
82
|
+
...(audit.closeEvidenceAudit?.latestCloseEvidenceId ? { latestCloseEvidenceId: audit.closeEvidenceAudit.latestCloseEvidenceId } : {}),
|
|
83
|
+
...(audit.closeEvidenceAudit ? { duplicateCloseEvidenceCount: audit.closeEvidenceAudit.duplicateCloseEvidenceCount, supersededCloseEvidenceIds: audit.closeEvidenceAudit.supersededCloseEvidenceIds } : {}),
|
|
84
|
+
...(audit.auditVerdict.recordedValidationReportHash ? { recordedValidationReportHash: audit.auditVerdict.recordedValidationReportHash } : {}),
|
|
85
|
+
currentValidationReportHash: audit.auditVerdict.currentValidationReportHash,
|
|
86
|
+
...(typeof audit.auditVerdict.reportHashMatches === 'boolean' ? { reportHashMatches: audit.auditVerdict.reportHashMatches } : {}),
|
|
87
|
+
...(audit.auditVerdict.recordedSourceHash ? { recordedSourceHash: audit.auditVerdict.recordedSourceHash } : {}),
|
|
88
|
+
currentSourceHash: audit.auditVerdict.currentSourceHash,
|
|
89
|
+
...(typeof audit.auditVerdict.sourceHashMatches === 'boolean' ? { sourceHashMatches: audit.auditVerdict.sourceHashMatches } : {})
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function collectCauses(reports, classification) {
|
|
93
|
+
const causes = [];
|
|
94
|
+
if (classification === 'closed-valid') {
|
|
95
|
+
causes.push({ code: 'CLOSE_REPAIR_NOT_NEEDED', severity: 'info', summary: 'Audit reports closed-valid.', sourceReport: 'hadara.task.audit_close.v1' });
|
|
96
|
+
return causes;
|
|
97
|
+
}
|
|
98
|
+
if (classification === 'not-closed') {
|
|
99
|
+
causes.push({ code: 'CLOSE_REPAIR_NOT_CLOSED', severity: 'error', summary: 'No close evidence was found.', sourceReport: 'hadara.task.audit_close.v1' });
|
|
100
|
+
}
|
|
101
|
+
if (classification === 'duplicate-close-proof') {
|
|
102
|
+
causes.push({
|
|
103
|
+
code: 'CLOSE_REPAIR_DUPLICATE_PROOF',
|
|
104
|
+
severity: 'warning',
|
|
105
|
+
summary: `${reports.audit.closeEvidenceAudit?.duplicateCloseEvidenceCount ?? 0} duplicate close proof record(s) were found.`,
|
|
106
|
+
sourceReport: 'hadara.task.audit_close.v1'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (reports.audit.auditVerdict.reportHashMatches === false) {
|
|
110
|
+
causes.push({ code: 'CLOSE_REPAIR_REPORT_HASH_DRIFT', severity: 'warning', summary: 'Recorded validation report hash differs from the current validation hash.', sourceReport: 'hadara.task.audit_close.v1' });
|
|
111
|
+
}
|
|
112
|
+
if (reports.audit.auditVerdict.sourceHashMatches === false) {
|
|
113
|
+
causes.push({ code: 'CLOSE_REPAIR_SOURCE_HASH_DRIFT', severity: 'warning', summary: 'Recorded close-source hash differs from the current close-source hash.', sourceReport: 'hadara.task.audit_close.v1' });
|
|
114
|
+
}
|
|
115
|
+
if (!reports.audit.auditVerdict.closeEvidenceValid && reports.audit.auditVerdict.closeEvidenceFound) {
|
|
116
|
+
causes.push({ code: 'CLOSE_REPAIR_INVALID_PROOF', severity: 'error', summary: 'Latest close evidence is not a passed command-log close proof.', sourceReport: 'hadara.task.audit_close.v1' });
|
|
117
|
+
}
|
|
118
|
+
for (const issue of reports.audit.issues) {
|
|
119
|
+
if (causes.some((cause) => cause.code === issue.code))
|
|
120
|
+
continue;
|
|
121
|
+
causes.push({ code: issue.code, severity: issue.severity, summary: issue.message, sourceReport: 'hadara.task.audit_close.v1', ...(issue.path ? { path: issue.path } : {}) });
|
|
122
|
+
}
|
|
123
|
+
if (causes.length === 0)
|
|
124
|
+
causes.push({ code: 'CLOSE_REPAIR_UNKNOWN', severity: 'warning', summary: 'Repair state is not closed-valid but no specific cause was available.', sourceReport: 'hadara.task.audit_close.v1' });
|
|
125
|
+
return causes;
|
|
126
|
+
}
|
|
127
|
+
function createRepairNextActions(taskId, classification, reports) {
|
|
128
|
+
if (classification === 'closed-valid')
|
|
129
|
+
return [];
|
|
130
|
+
if (classification === 'unknown') {
|
|
131
|
+
return [
|
|
132
|
+
(0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
|
|
133
|
+
id: 'inspect-task',
|
|
134
|
+
kind: 'review',
|
|
135
|
+
required: true,
|
|
136
|
+
message: `Inspect task ${taskId} before repairing close state.`,
|
|
137
|
+
writeBoundary: 'read-only',
|
|
138
|
+
recommendedActorRole: 'reviewer',
|
|
139
|
+
requiresBeforeHash: false,
|
|
140
|
+
stalePlanRisk: 'none'
|
|
141
|
+
})
|
|
142
|
+
];
|
|
143
|
+
}
|
|
144
|
+
const actions = [];
|
|
145
|
+
if (!reports.close.ok) {
|
|
146
|
+
actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
|
|
147
|
+
id: 'run-ready',
|
|
148
|
+
required: true,
|
|
149
|
+
command: `hadara task ready --task ${taskId} --level done --json`,
|
|
150
|
+
message: 'Resolve readiness blockers before appending a fresh close proof.',
|
|
151
|
+
writeBoundary: 'read-only',
|
|
152
|
+
recommendedActorRole: 'worker',
|
|
153
|
+
requiresBeforeHash: false,
|
|
154
|
+
stalePlanRisk: 'none'
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
|
|
158
|
+
id: 'review-close-plan',
|
|
159
|
+
required: true,
|
|
160
|
+
command: `hadara task close --task ${taskId} --json`,
|
|
161
|
+
message: 'Review the current close plan before appending or replacing close evidence.',
|
|
162
|
+
writeBoundary: 'read-only',
|
|
163
|
+
recommendedActorRole: 'worker',
|
|
164
|
+
requiresBeforeHash: false,
|
|
165
|
+
stalePlanRisk: classification === 'closed-stale' ? 'medium' : 'low'
|
|
166
|
+
}));
|
|
167
|
+
actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
|
|
168
|
+
id: 'audit-after-repair',
|
|
169
|
+
required: false,
|
|
170
|
+
command: `hadara task audit-close --task ${taskId} --json`,
|
|
171
|
+
message: 'After close execute, audit the repaired close proof.',
|
|
172
|
+
writeBoundary: 'read-only',
|
|
173
|
+
recommendedActorRole: 'reviewer',
|
|
174
|
+
requiresBeforeHash: false,
|
|
175
|
+
stalePlanRisk: 'none'
|
|
176
|
+
}));
|
|
177
|
+
return actions;
|
|
178
|
+
}
|
|
179
|
+
function collectIssues(reports) {
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
const issues = [];
|
|
182
|
+
for (const issue of [...reports.close.issues, ...reports.audit.issues]) {
|
|
183
|
+
const key = `${issue.severity}:${issue.code}:${issue.path ?? ''}:${issue.message}`;
|
|
184
|
+
if (seen.has(key))
|
|
185
|
+
continue;
|
|
186
|
+
seen.add(key);
|
|
187
|
+
issues.push({ severity: issue.severity, code: issue.code, message: issue.message, ...(issue.path ? { path: issue.path } : {}) });
|
|
188
|
+
}
|
|
189
|
+
return issues;
|
|
190
|
+
}
|
package/dist/task/task-close.js
CHANGED
|
@@ -175,7 +175,36 @@ function createNextActions(taskId, ok, mode, closeEvidenceWrite) {
|
|
|
175
175
|
}
|
|
176
176
|
];
|
|
177
177
|
}
|
|
178
|
-
|
|
178
|
+
if (ok) {
|
|
179
|
+
if (closeEvidenceWrite?.duplicateAction === 'no-op') {
|
|
180
|
+
return [
|
|
181
|
+
(0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
|
|
182
|
+
id: 'audit-close',
|
|
183
|
+
required: false,
|
|
184
|
+
command: `hadara task audit-close --task ${taskId} --json`,
|
|
185
|
+
message: 'Matching close evidence already exists; audit the existing close record.',
|
|
186
|
+
writeBoundary: 'read-only',
|
|
187
|
+
recommendedActorRole: 'reviewer',
|
|
188
|
+
requiresBeforeHash: false,
|
|
189
|
+
stalePlanRisk: 'none'
|
|
190
|
+
})
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
return [
|
|
194
|
+
(0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
|
|
195
|
+
id: 'append-close-evidence',
|
|
196
|
+
required: true,
|
|
197
|
+
command: `hadara task close --task ${taskId} --execute --json`,
|
|
198
|
+
message: 'Append close audit evidence after reviewing this dry-run plan.',
|
|
199
|
+
writeBoundary: 'evidence-append',
|
|
200
|
+
recommendedActorRole: 'worker',
|
|
201
|
+
requiresBeforeHash: false,
|
|
202
|
+
stalePlanRisk: 'low',
|
|
203
|
+
loopBoundary: true
|
|
204
|
+
})
|
|
205
|
+
];
|
|
206
|
+
}
|
|
207
|
+
return [
|
|
179
208
|
(0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
|
|
180
209
|
id: 'run-done-validation',
|
|
181
210
|
required: true,
|
|
@@ -195,37 +224,8 @@ function createNextActions(taskId, ok, mode, closeEvidenceWrite) {
|
|
|
195
224
|
recommendedActorRole: 'worker',
|
|
196
225
|
requiresBeforeHash: false,
|
|
197
226
|
stalePlanRisk: 'none'
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
if (ok) {
|
|
201
|
-
if (closeEvidenceWrite?.duplicateAction === 'no-op') {
|
|
202
|
-
actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
|
|
203
|
-
id: 'audit-close',
|
|
204
|
-
required: false,
|
|
205
|
-
command: `hadara task audit-close --task ${taskId} --json`,
|
|
206
|
-
message: 'Matching close evidence already exists; audit the existing close record.',
|
|
207
|
-
writeBoundary: 'read-only',
|
|
208
|
-
recommendedActorRole: 'reviewer',
|
|
209
|
-
requiresBeforeHash: false,
|
|
210
|
-
stalePlanRisk: 'none'
|
|
211
|
-
}));
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
214
|
-
actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
|
|
215
|
-
id: 'append-close-evidence',
|
|
216
|
-
required: true,
|
|
217
|
-
command: `hadara task close --task ${taskId} --execute --json`,
|
|
218
|
-
message: 'Append close audit evidence after reviewing this dry-run plan.',
|
|
219
|
-
writeBoundary: 'evidence-append',
|
|
220
|
-
recommendedActorRole: 'worker',
|
|
221
|
-
requiresBeforeHash: false,
|
|
222
|
-
stalePlanRisk: 'low',
|
|
223
|
-
loopBoundary: true
|
|
224
|
-
}));
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
else {
|
|
228
|
-
actions.push((0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
|
|
227
|
+
}),
|
|
228
|
+
(0, lifecycle_next_actions_1.createTaskLifecycleNextAction)({
|
|
229
229
|
id: 'resolve-close-blockers',
|
|
230
230
|
required: true,
|
|
231
231
|
message: 'Resolve blocking issues before appending close evidence.',
|
|
@@ -234,9 +234,8 @@ function createNextActions(taskId, ok, mode, closeEvidenceWrite) {
|
|
|
234
234
|
requiresBeforeHash: false,
|
|
235
235
|
stalePlanRisk: 'none',
|
|
236
236
|
kind: 'review'
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
return actions;
|
|
237
|
+
})
|
|
238
|
+
];
|
|
240
239
|
}
|
|
241
240
|
function buildMissingTaskReport(projectRoot, taskId, mode, issues, actor = (0, lifecycle_next_actions_1.defaultTaskLifecycleActor)()) {
|
|
242
241
|
return {
|