hadara 0.2.0-rc.2 → 0.2.0-rc.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 +12 -8
- package/dist/cli/ci.js +28 -0
- package/dist/cli/evidence-json.js +10 -13
- package/dist/cli/evidence.js +23 -6
- package/dist/cli/init.js +6 -6
- package/dist/cli/main.js +16 -1
- package/dist/cli/proof.js +27 -0
- package/dist/evidence/evidence.js +150 -42
- package/dist/services/ci-gate.js +125 -0
- package/dist/services/proof-status.js +151 -0
- package/dist/task/task-close.js +6 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<img alt="Release candidate" src="https://img.shields.io/badge/release-0.2.0--rc.
|
|
8
|
+
<img alt="Release candidate" src="https://img.shields.io/badge/release-0.2.0--rc.3-blue">
|
|
9
9
|
<img alt="Node.js" src="https://img.shields.io/badge/node-%3E%3D22-brightgreen">
|
|
10
10
|
<img alt="License" src="https://img.shields.io/badge/license-MIT-lightgrey">
|
|
11
11
|
</p>
|
|
@@ -23,10 +23,10 @@ This repository is both the HADARA source checkout and the HADARA protocol works
|
|
|
23
23
|
The current source checkout targets:
|
|
24
24
|
|
|
25
25
|
```text
|
|
26
|
-
hadara@0.2.0-rc.
|
|
26
|
+
hadara@0.2.0-rc.3
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
T-
|
|
29
|
+
T-0287 prepares the source checkout for `0.2.0-rc.3` after the rc2 dogfooding findings and the T-0284 through T-0286 proof reliability hardening. `hadara@0.2.0-rc.3` is a publish candidate until an operator explicitly runs an approval-gated publish capsule. The latest npm-published release candidate remains `hadara@0.2.0-rc.2`.
|
|
30
30
|
|
|
31
31
|
Current publish boundaries:
|
|
32
32
|
|
|
@@ -35,8 +35,9 @@ Current publish boundaries:
|
|
|
35
35
|
| npm package | Primary release target. |
|
|
36
36
|
| `hadara@0.1.0-rc.0` | Published first RC. |
|
|
37
37
|
| `hadara@0.2.0-rc.0` | Superseded internal publish candidate after recycle findings. |
|
|
38
|
-
| `hadara@0.2.0-rc.1` |
|
|
39
|
-
| `hadara@0.2.0-rc.2` | Current
|
|
38
|
+
| `hadara@0.2.0-rc.1` | Previous published npm RC. |
|
|
39
|
+
| `hadara@0.2.0-rc.2` | Current published npm RC. |
|
|
40
|
+
| `hadara@0.2.0-rc.3` | Current source publish candidate; not published by this capsule. |
|
|
40
41
|
| GitHub Release | Secondary target, still approval-gated. |
|
|
41
42
|
| Docker image | Deferred. |
|
|
42
43
|
| PyPI/Python package | `hadara==0.2.0rc1` published preview bridge. |
|
|
@@ -48,7 +49,7 @@ No release command should publish, create a GitHub Release, build Docker images,
|
|
|
48
49
|
|
|
49
50
|
Requires Node.js 22.
|
|
50
51
|
|
|
51
|
-
Install the current RC:
|
|
52
|
+
Install the current published RC:
|
|
52
53
|
|
|
53
54
|
```bash
|
|
54
55
|
npm install -g hadara@0.2.0-rc.2
|
|
@@ -64,7 +65,7 @@ npx hadara@0.2.0-rc.2 doctor --json
|
|
|
64
65
|
npx hadara@0.2.0-rc.2 tools list --json
|
|
65
66
|
```
|
|
66
67
|
|
|
67
|
-
|
|
68
|
+
After an operator publishes rc3, use `hadara@0.2.0-rc.3` in the install and npx commands above. Previous published RCs remain available on npm for comparison or rollback.
|
|
68
69
|
|
|
69
70
|
## What HADARA Gives You
|
|
70
71
|
|
|
@@ -114,6 +115,9 @@ Evidence and handoff:
|
|
|
114
115
|
hadara evidence collect --task T-0001 --json
|
|
115
116
|
hadara evidence add-command --task T-0001 --summary "Focused validation passed." --result passed --json
|
|
116
117
|
hadara evidence list --task T-0001 --json
|
|
118
|
+
hadara proof status --task T-0001 --json
|
|
119
|
+
hadara proof explain --task T-0001 --json
|
|
120
|
+
hadara ci gate --mode advisory --task T-0001 --json
|
|
117
121
|
hadara handoff suggest --task T-0001 --json
|
|
118
122
|
```
|
|
119
123
|
|
|
@@ -151,7 +155,7 @@ hadara task status --task T-XXXX --json
|
|
|
151
155
|
|
|
152
156
|
# Do the scoped work.
|
|
153
157
|
|
|
154
|
-
hadara evidence add-command --task T-XXXX --summary "..." --result passed --json
|
|
158
|
+
hadara evidence add-command --task T-XXXX --summary "..." --result passed --idempotency-key "command:T-XXXX:check" --json
|
|
155
159
|
|
|
156
160
|
hadara task finish --task T-XXXX --json
|
|
157
161
|
hadara task finish --task T-XXXX --execute --json
|
package/dist/cli/ci.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleCiCommand = handleCiCommand;
|
|
4
|
+
const args_1 = require("./args");
|
|
5
|
+
const ci_gate_1 = require("../services/ci-gate");
|
|
6
|
+
function handleCiCommand(input) {
|
|
7
|
+
if (input.args[0] !== 'ci' || input.args[1] !== 'gate')
|
|
8
|
+
return false;
|
|
9
|
+
const mode = parseCiGateMode((0, args_1.getStringOption)(input.args, '--mode', 'advisory') ?? 'advisory');
|
|
10
|
+
const report = (0, ci_gate_1.createCiGateReport)(input.projectRoot, mode, {
|
|
11
|
+
taskId: (0, args_1.getStringOption)(input.args, '--task'),
|
|
12
|
+
allowEmpty: (0, args_1.getFlag)(input.args, '--allow-empty')
|
|
13
|
+
});
|
|
14
|
+
if (input.jsonOutput) {
|
|
15
|
+
console.log(JSON.stringify(report, null, 2));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.log(`[HADARA] ci gate ${mode}: ${report.ok ? 'ok' : 'blocked'} | blockers ${report.blockers.length} | warnings ${report.warnings.length}`);
|
|
19
|
+
}
|
|
20
|
+
if (!report.ok)
|
|
21
|
+
process.exitCode = 6;
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
function parseCiGateMode(value) {
|
|
25
|
+
if (value === 'advisory' || value === 'strict')
|
|
26
|
+
return value;
|
|
27
|
+
throw new Error(`unsupported ci gate mode: ${value}`);
|
|
28
|
+
}
|
|
@@ -4,7 +4,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createEvidenceCollectReport = createEvidenceCollectReport;
|
|
7
|
-
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
7
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
8
|
const evidence_1 = require("../evidence/evidence");
|
|
10
9
|
const task_capsule_1 = require("../task/task-capsule");
|
|
@@ -25,19 +24,20 @@ function createEvidenceCollectReport(projectRoot, input) {
|
|
|
25
24
|
]
|
|
26
25
|
};
|
|
27
26
|
}
|
|
28
|
-
let
|
|
27
|
+
let appendResult;
|
|
29
28
|
try {
|
|
30
|
-
|
|
29
|
+
appendResult = (0, evidence_1.appendEvidenceWithResult)(projectRoot, {
|
|
31
30
|
taskId: input.taskId,
|
|
32
31
|
kind: input.kind,
|
|
33
32
|
path: input.path,
|
|
34
33
|
summary: input.summary,
|
|
35
34
|
result: input.result,
|
|
36
|
-
visibility: input.visibility
|
|
35
|
+
visibility: input.visibility,
|
|
36
|
+
idempotencyKey: input.idempotencyKey
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
catch (error) {
|
|
40
|
-
if (error instanceof workspace_1.WorkspaceFileError || error instanceof evidence_1.EvidenceArtifactPolicyError) {
|
|
40
|
+
if (error instanceof workspace_1.WorkspaceFileError || error instanceof evidence_1.EvidenceArtifactPolicyError || error instanceof evidence_1.EvidenceAppendLockError) {
|
|
41
41
|
return {
|
|
42
42
|
schemaVersion: 'hadara.evidence.collect.v1',
|
|
43
43
|
command: 'evidence.collect',
|
|
@@ -53,23 +53,20 @@ function createEvidenceCollectReport(projectRoot, input) {
|
|
|
53
53
|
}
|
|
54
54
|
throw error;
|
|
55
55
|
}
|
|
56
|
-
const indexRecord = readLastEvidenceIndexRecord(task.dir);
|
|
57
56
|
return {
|
|
58
57
|
schemaVersion: 'hadara.evidence.collect.v1',
|
|
59
58
|
command: 'evidence.collect',
|
|
60
59
|
ok: true,
|
|
61
60
|
evidence: {
|
|
62
|
-
...
|
|
63
|
-
markdownPath: toPortablePath(node_path_1.default.relative(projectRoot, markdownPath))
|
|
61
|
+
...appendResult.evidence,
|
|
62
|
+
markdownPath: toPortablePath(node_path_1.default.relative(projectRoot, appendResult.markdownPath)),
|
|
63
|
+
markdownAppended: appendResult.markdownAppended,
|
|
64
|
+
jsonlAppended: appendResult.jsonlAppended,
|
|
65
|
+
existing: appendResult.existing
|
|
64
66
|
},
|
|
65
67
|
issues: []
|
|
66
68
|
};
|
|
67
69
|
}
|
|
68
|
-
function readLastEvidenceIndexRecord(taskDir) {
|
|
69
|
-
const indexPath = node_path_1.default.join(taskDir, 'evidence.jsonl');
|
|
70
|
-
const lines = node_fs_1.default.readFileSync(indexPath, 'utf8').trim().split(/\r?\n/);
|
|
71
|
-
return JSON.parse(lines.at(-1) ?? '{}');
|
|
72
|
-
}
|
|
73
70
|
function toPortablePath(value) {
|
|
74
71
|
return value.split(node_path_1.default.sep).join('/');
|
|
75
72
|
}
|
package/dist/cli/evidence.js
CHANGED
|
@@ -78,21 +78,28 @@ function handleEvidenceCommand(input) {
|
|
|
78
78
|
const summary = (0, args_1.getStringOption)(input.args, '--summary') ?? 'Command completed.';
|
|
79
79
|
const result = parseEvidenceResult((0, args_1.getStringOption)(input.args, '--result', 'unknown') ?? 'unknown');
|
|
80
80
|
const visibility = parseEvidenceVisibility((0, args_1.getStringOption)(input.args, '--visibility', 'public') ?? 'public', (0, args_1.getFlag)(input.args, '--private'));
|
|
81
|
+
const idempotencyKey = (0, args_1.getStringOption)(input.args, '--idempotency-key');
|
|
81
82
|
if (input.jsonOutput) {
|
|
82
83
|
const report = (0, evidence_json_1.createEvidenceCollectReport)(input.projectRoot, {
|
|
83
84
|
taskId,
|
|
84
85
|
kind: 'command-log',
|
|
85
86
|
summary,
|
|
86
87
|
result,
|
|
87
|
-
visibility
|
|
88
|
+
visibility,
|
|
89
|
+
idempotencyKey
|
|
88
90
|
});
|
|
89
91
|
console.log(JSON.stringify({ ...report, command: 'evidence.add-command' }, null, 2));
|
|
90
92
|
if (!report.ok)
|
|
91
93
|
process.exitCode = 6;
|
|
92
94
|
}
|
|
93
95
|
else {
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
+
const appendResult = (0, evidence_1.appendEvidenceWithResult)(input.projectRoot, { taskId, kind: 'command-log', summary, result, visibility, idempotencyKey });
|
|
97
|
+
if (appendResult.existing) {
|
|
98
|
+
console.log(`[HADARA] Command evidence already exists: ${persistedEvidenceId(appendResult.evidence)}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
console.log(`[HADARA] Command evidence recorded: ${appendResult.markdownPath}`);
|
|
102
|
+
}
|
|
96
103
|
}
|
|
97
104
|
return true;
|
|
98
105
|
}
|
|
@@ -104,6 +111,7 @@ function handleEvidenceCommand(input) {
|
|
|
104
111
|
const result = parseEvidenceResult((0, args_1.getStringOption)(input.args, '--result', 'unknown') ?? 'unknown');
|
|
105
112
|
const evidenceFile = (0, args_1.getStringOption)(input.args, '--path');
|
|
106
113
|
const visibility = parseEvidenceVisibility((0, args_1.getStringOption)(input.args, '--visibility', 'public') ?? 'public', (0, args_1.getFlag)(input.args, '--private'));
|
|
114
|
+
const idempotencyKey = (0, args_1.getStringOption)(input.args, '--idempotency-key');
|
|
107
115
|
if (input.jsonOutput) {
|
|
108
116
|
const report = (0, evidence_json_1.createEvidenceCollectReport)(input.projectRoot, {
|
|
109
117
|
taskId,
|
|
@@ -111,18 +119,27 @@ function handleEvidenceCommand(input) {
|
|
|
111
119
|
path: evidenceFile,
|
|
112
120
|
summary,
|
|
113
121
|
result,
|
|
114
|
-
visibility
|
|
122
|
+
visibility,
|
|
123
|
+
idempotencyKey
|
|
115
124
|
});
|
|
116
125
|
console.log(JSON.stringify(report, null, 2));
|
|
117
126
|
if (!report.ok)
|
|
118
127
|
process.exitCode = 6;
|
|
119
128
|
}
|
|
120
129
|
else {
|
|
121
|
-
const
|
|
122
|
-
|
|
130
|
+
const appendResult = (0, evidence_1.appendEvidenceWithResult)(input.projectRoot, { taskId, kind, path: evidenceFile, summary, result, visibility, idempotencyKey });
|
|
131
|
+
if (appendResult.existing) {
|
|
132
|
+
console.log(`[HADARA] Evidence already exists: ${persistedEvidenceId(appendResult.evidence)}`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
console.log(`[HADARA] Evidence recorded: ${appendResult.markdownPath}`);
|
|
136
|
+
}
|
|
123
137
|
}
|
|
124
138
|
return true;
|
|
125
139
|
}
|
|
140
|
+
function persistedEvidenceId(record) {
|
|
141
|
+
return record.schemaVersion === 'hadara.evidence.v2' && record.id ? record.id : 'evidence.jsonl';
|
|
142
|
+
}
|
|
126
143
|
function parseEvidenceKind(value) {
|
|
127
144
|
if (['test-log', 'command-log', 'diff-summary', 'screenshot', 'note'].includes(value)) {
|
|
128
145
|
return value;
|
package/dist/cli/init.js
CHANGED
|
@@ -963,7 +963,7 @@ hadara task status --task T-XXXX --json
|
|
|
963
963
|
|
|
964
964
|
# Do the scoped work.
|
|
965
965
|
|
|
966
|
-
hadara evidence add-command --task T-XXXX --summary "..." --result passed --json
|
|
966
|
+
hadara evidence add-command --task T-XXXX --summary "..." --result passed --idempotency-key "command:T-XXXX:check" --json
|
|
967
967
|
|
|
968
968
|
hadara task finish --task T-XXXX --json
|
|
969
969
|
hadara task finish --task T-XXXX --execute --json
|
|
@@ -985,7 +985,7 @@ hadara task audit-close --task T-XXXX --json
|
|
|
985
985
|
|---|---|---|
|
|
986
986
|
| \`task next\` | Read-only | Recommends work; does not create tasks. |
|
|
987
987
|
| \`task status\` | Read-only | \`ok\` means report generation succeeded; readiness is in \`state.ready\`, \`summary.blockers\`, and \`issues\`. |
|
|
988
|
-
| \`evidence add-command\` | Write | Appends command-log evidence; does not execute shell commands. |
|
|
988
|
+
| \`evidence add-command\` | Write | Appends command-log evidence; does not execute shell commands; optional \`--idempotency-key\` prevents duplicate same-key records. |
|
|
989
989
|
| \`task ready\` | Read-only | Checks readiness; does not mutate evidence or status docs. |
|
|
990
990
|
| \`task finish\` | Dry-run by default; writes only with \`--execute\` | Bounded to \`TASK.md\` and \`docs/TASK_BOARD.md\`. |
|
|
991
991
|
| \`task close\` | Dry-run by default; writes only with \`--execute\` | Bounded to close evidence append. |
|
|
@@ -1010,7 +1010,7 @@ Before running \`task ready\` and \`task close\`, finish all close-source edits:
|
|
|
1010
1010
|
1. Do not hand-edit Task Capsule \`evidence.jsonl\`.
|
|
1011
1011
|
2. Append evidence through HADARA commands so schema, visibility, and artifact-safety checks run consistently.
|
|
1012
1012
|
3. Record failed or blocked checks honestly. Do not replace them later with optimistic summaries; add newer evidence that explains the fix or residual risk.
|
|
1013
|
-
4. Use \`hadara evidence add-command --task <task-id> --summary <text> --result passed|failed|blocked|unknown --json\` for command results when no artifact file is attached.
|
|
1013
|
+
4. Use \`hadara evidence add-command --task <task-id> --summary <text> --result passed|failed|blocked|unknown --json\` for command results when no artifact file is attached. Add \`--idempotency-key <key>\` when rerunning the same logical check should report one durable evidence identity instead of appending duplicates.
|
|
1014
1014
|
5. Use \`hadara evidence lint --task <task-id> --json\` when evidence drift is suspected or before close if evidence files were touched manually by mistake.
|
|
1015
1015
|
|
|
1016
1016
|
## Session End
|
|
@@ -1177,7 +1177,7 @@ hadara task status --task T-XXXX --json
|
|
|
1177
1177
|
|
|
1178
1178
|
# Do the scoped work.
|
|
1179
1179
|
|
|
1180
|
-
hadara evidence add-command --task T-XXXX --summary "..." --result passed --json
|
|
1180
|
+
hadara evidence add-command --task T-XXXX --summary "..." --result passed --idempotency-key "command:T-XXXX:check" --json
|
|
1181
1181
|
|
|
1182
1182
|
hadara task finish --task T-XXXX --json
|
|
1183
1183
|
hadara task finish --task T-XXXX --execute --json
|
|
@@ -1210,7 +1210,7 @@ Before close, finish all close-source edits: Task Capsule docs, acceptance/tests
|
|
|
1210
1210
|
| \`task next\` | Read-only | Recommends work; does not create tasks. |
|
|
1211
1211
|
| \`task status\` | Read-only | \`ok\` means report generation succeeded; readiness is in \`state.ready\`, \`summary.blockers\`, and \`issues\`. |
|
|
1212
1212
|
| \`task create\` | Write | Creates a Draft Task Capsule and Task Board row. It does not imply the task is ready or done. |
|
|
1213
|
-
| \`evidence add-command\` | Write | Appends operator-supplied command-log evidence. It does not execute shell commands or capture stdout/stderr. |
|
|
1213
|
+
| \`evidence add-command\` | Write | Appends operator-supplied command-log evidence. It does not execute shell commands or capture stdout/stderr; optional \`--idempotency-key\` prevents duplicate same-key records. |
|
|
1214
1214
|
| \`task finish\` | Dry-run by default; writes only with \`--execute\` | Updates only \`TASK.md\` status bookkeeping and the matching \`docs/TASK_BOARD.md\` row. |
|
|
1215
1215
|
| \`task ready\` | Read-only | Checks whether the task can satisfy the requested readiness level after finish. |
|
|
1216
1216
|
| \`task complete\` | Read-only | Summarizes the current completion stage and next command; it does not execute lifecycle writes. |
|
|
@@ -1224,7 +1224,7 @@ Before close, finish all close-source edits: Task Capsule docs, acceptance/tests
|
|
|
1224
1224
|
- \`task ready\` checks readiness; it does not write evidence or status.
|
|
1225
1225
|
- \`harness validate\` is a direct diagnostic for Task Capsule structure and done-level gates; it is not a replacement for close evidence.
|
|
1226
1226
|
- \`task complete\` is a read-only workflow compressor. It may report the next lifecycle command, but it must not execute finish, ready, close, or audit commands.
|
|
1227
|
-
- \`evidence add-command\` records an operator-supplied command result; it does not run the command.
|
|
1227
|
+
- \`evidence add-command\` records an operator-supplied command result; it does not run the command. \`--idempotency-key\` is optional; when supplied, same-key repeats return the existing record without appending duplicate Markdown or JSONL rows.
|
|
1228
1228
|
- \`task finish\` may update only the Task Capsule \`TASK.md\` status and matching \`docs/TASK_BOARD.md\` status/path row.
|
|
1229
1229
|
- \`task close\` may append only close evidence. It must not update status docs, Task Board rows, handoff, Project State, roadmap docs, or arbitrary evidence.
|
|
1230
1230
|
- After \`task close --execute --json\`, close-source document edits intentionally invalidate the previous close proof. Make those edits before close, or rerun ready/close/audit if the edit is unavoidable.
|
package/dist/cli/main.js
CHANGED
|
@@ -62,10 +62,13 @@ Usage:
|
|
|
62
62
|
hadara task audit-close --task <task-id> [--json]
|
|
63
63
|
hadara task ready --task <task-id> [--level done] [--json]
|
|
64
64
|
hadara evidence collect --task <task-id> [--kind note|test-log|command-log|diff-summary|screenshot] [--path <path>] [--summary <text>] [--result passed|failed|blocked|unknown] [--private|--visibility public|private]
|
|
65
|
-
hadara evidence add-command --task <task-id> --summary <text> [--result passed|failed|blocked|unknown] [--private|--visibility public|private] [--json]
|
|
65
|
+
hadara evidence add-command --task <task-id> --summary <text> [--result passed|failed|blocked|unknown] [--idempotency-key <key>] [--private|--visibility public|private] [--json]
|
|
66
66
|
hadara evidence list --task <task-id> [--limit <n>] [--include-private] [--json]
|
|
67
67
|
hadara evidence lint --task <task-id> [--json]
|
|
68
68
|
hadara evidence migrate --task <task-id> --to v2 [--execute --before-hash <hash>] [--json]
|
|
69
|
+
hadara proof status --task <task-id> [--json]
|
|
70
|
+
hadara proof explain --task <task-id> [--json]
|
|
71
|
+
hadara ci gate [--mode advisory|strict] [--task <task-id>] [--allow-empty] [--json]
|
|
69
72
|
hadara debt list [--json]
|
|
70
73
|
hadara debt show <id> [--json]
|
|
71
74
|
hadara protocol doctor [--json]
|
|
@@ -150,6 +153,18 @@ async function main(args = process.argv.slice(2)) {
|
|
|
150
153
|
return;
|
|
151
154
|
break;
|
|
152
155
|
}
|
|
156
|
+
case 'proof': {
|
|
157
|
+
const { handleProofCommand } = await Promise.resolve().then(() => __importStar(require('./proof')));
|
|
158
|
+
if (handleProofCommand({ args, projectRoot: paths.projectRoot, jsonOutput }))
|
|
159
|
+
return;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
case 'ci': {
|
|
163
|
+
const { handleCiCommand } = await Promise.resolve().then(() => __importStar(require('./ci')));
|
|
164
|
+
if (handleCiCommand({ args, projectRoot: paths.projectRoot, jsonOutput }))
|
|
165
|
+
return;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
153
168
|
case 'tools': {
|
|
154
169
|
const { handleToolsCommand } = await Promise.resolve().then(() => __importStar(require('./tools')));
|
|
155
170
|
if (handleToolsCommand({ args, jsonOutput }))
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.handleProofCommand = handleProofCommand;
|
|
4
|
+
const args_1 = require("./args");
|
|
5
|
+
const proof_status_1 = require("../services/proof-status");
|
|
6
|
+
function handleProofCommand(input) {
|
|
7
|
+
if (input.args[0] !== 'proof')
|
|
8
|
+
return false;
|
|
9
|
+
const sub = input.args[1];
|
|
10
|
+
if (sub !== 'status' && sub !== 'explain')
|
|
11
|
+
return false;
|
|
12
|
+
const taskId = (0, args_1.getRequiredStringOption)(input.args, '--task');
|
|
13
|
+
const report = (0, proof_status_1.createProofStatusReport)(input.projectRoot, taskId, sub);
|
|
14
|
+
if (input.jsonOutput) {
|
|
15
|
+
console.log(JSON.stringify(report, null, 2));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.log(`[HADARA] proof ${sub} ${taskId}: ${report.verdict}`);
|
|
19
|
+
console.log(`freshness: ${report.freshness.status}`);
|
|
20
|
+
for (const issue of [...report.blockers, ...report.warnings]) {
|
|
21
|
+
console.log(`[${issue.severity}] ${issue.code}: ${issue.message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (!report.ok)
|
|
25
|
+
process.exitCode = 6;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.EvidenceArtifactPolicyError = void 0;
|
|
6
|
+
exports.EvidenceAppendLockError = exports.EvidenceArtifactPolicyError = void 0;
|
|
7
7
|
exports.persistedEvidenceKind = persistedEvidenceKind;
|
|
8
8
|
exports.persistedEvidenceResult = persistedEvidenceResult;
|
|
9
9
|
exports.persistedEvidencePath = persistedEvidencePath;
|
|
@@ -11,6 +11,7 @@ exports.appendEvidence = appendEvidence;
|
|
|
11
11
|
exports.appendEvidenceWithResult = appendEvidenceWithResult;
|
|
12
12
|
exports.appendEvidenceTextArtifact = appendEvidenceTextArtifact;
|
|
13
13
|
exports.createPublicEvidenceArtifactPolicyReport = createPublicEvidenceArtifactPolicyReport;
|
|
14
|
+
exports.persistedEvidenceIdempotencyKey = persistedEvidenceIdempotencyKey;
|
|
14
15
|
exports.createSessionEvidenceDirs = createSessionEvidenceDirs;
|
|
15
16
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
16
17
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -39,6 +40,14 @@ class EvidenceArtifactPolicyError extends Error {
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
exports.EvidenceArtifactPolicyError = EvidenceArtifactPolicyError;
|
|
43
|
+
class EvidenceAppendLockError extends Error {
|
|
44
|
+
code = 'EVIDENCE_APPEND_LOCK_TIMEOUT';
|
|
45
|
+
constructor(message) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = 'EvidenceAppendLockError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.EvidenceAppendLockError = EvidenceAppendLockError;
|
|
42
51
|
function appendEvidence(projectRoot, record) {
|
|
43
52
|
return appendEvidenceWithResult(projectRoot, record).markdownPath;
|
|
44
53
|
}
|
|
@@ -49,8 +58,14 @@ function appendEvidenceWithResult(projectRoot, record) {
|
|
|
49
58
|
}
|
|
50
59
|
const time = new Date().toISOString();
|
|
51
60
|
const visibility = record.visibility ?? 'public';
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
return appendEvidenceRecord({
|
|
62
|
+
projectRoot,
|
|
63
|
+
taskDir,
|
|
64
|
+
time,
|
|
65
|
+
record,
|
|
66
|
+
visibility,
|
|
67
|
+
createAttachedPath: () => copyPublicEvidenceArtifact({ projectRoot, taskDir, kind: record.kind, sourcePath: record.path, time, visibility })
|
|
68
|
+
});
|
|
54
69
|
}
|
|
55
70
|
function appendEvidenceTextArtifact(projectRoot, record, artifact, options = {}) {
|
|
56
71
|
const taskDir = findTaskDir(projectRoot, record.taskId);
|
|
@@ -59,18 +74,24 @@ function appendEvidenceTextArtifact(projectRoot, record, artifact, options = {})
|
|
|
59
74
|
}
|
|
60
75
|
const time = new Date().toISOString();
|
|
61
76
|
const visibility = record.visibility ?? 'public';
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
return appendEvidenceRecord({
|
|
78
|
+
projectRoot,
|
|
79
|
+
taskDir,
|
|
80
|
+
time,
|
|
81
|
+
record,
|
|
82
|
+
visibility,
|
|
83
|
+
createAttachedPath: () => visibility === 'public'
|
|
84
|
+
? writePublicEvidenceTextArtifact({
|
|
85
|
+
taskDir,
|
|
86
|
+
kind: record.kind,
|
|
87
|
+
time,
|
|
88
|
+
fileName: artifact.fileName,
|
|
89
|
+
content: artifact.content,
|
|
90
|
+
artifactDirName: artifact.artifactDirName,
|
|
91
|
+
policyOptions: options
|
|
92
|
+
})
|
|
93
|
+
: undefined
|
|
94
|
+
});
|
|
74
95
|
}
|
|
75
96
|
function createPublicEvidenceArtifactPolicyReport(content, options = {}) {
|
|
76
97
|
const redaction = (0, redaction_1.createRedactionReport)(content, { patterns: options.redactionPatterns });
|
|
@@ -95,41 +116,128 @@ function createPublicEvidenceArtifactPolicyReport(content, options = {}) {
|
|
|
95
116
|
function appendEvidenceIndex(taskDir, record) {
|
|
96
117
|
node_fs_1.default.appendFileSync(node_path_1.default.join(taskDir, 'evidence.jsonl'), `${JSON.stringify(record)}\n`, 'utf8');
|
|
97
118
|
}
|
|
119
|
+
function readEvidenceIndex(taskDir) {
|
|
120
|
+
const indexPath = node_path_1.default.join(taskDir, 'evidence.jsonl');
|
|
121
|
+
if (!node_fs_1.default.existsSync(indexPath))
|
|
122
|
+
return [];
|
|
123
|
+
return node_fs_1.default
|
|
124
|
+
.readFileSync(indexPath, 'utf8')
|
|
125
|
+
.split(/\r?\n/)
|
|
126
|
+
.filter((line) => line.trim() !== '')
|
|
127
|
+
.map((line) => JSON.parse(line));
|
|
128
|
+
}
|
|
129
|
+
function persistedEvidenceIdempotencyKey(record) {
|
|
130
|
+
if (record.schemaVersion !== 'hadara.evidence.v2')
|
|
131
|
+
return undefined;
|
|
132
|
+
if (record.idempotencyKey)
|
|
133
|
+
return record.idempotencyKey;
|
|
134
|
+
const tag = record.tags.find((item) => item.startsWith('idempotency:'));
|
|
135
|
+
return tag ? tag.replace(/^idempotency:/, '') : undefined;
|
|
136
|
+
}
|
|
137
|
+
function withEvidenceAppendLock(projectRoot, taskId, fn) {
|
|
138
|
+
const lockRoot = node_path_1.default.join(projectRoot, '.hadara', 'local', 'locks', 'evidence');
|
|
139
|
+
(0, fs_1.ensureDir)(lockRoot);
|
|
140
|
+
const lockDir = node_path_1.default.join(lockRoot, `${safeFilePart(taskId)}.lock`);
|
|
141
|
+
const lockPortablePath = toPortablePath(node_path_1.default.relative(projectRoot, lockDir));
|
|
142
|
+
const started = Date.now();
|
|
143
|
+
const timeoutMs = 5000;
|
|
144
|
+
while (true) {
|
|
145
|
+
try {
|
|
146
|
+
node_fs_1.default.mkdirSync(lockDir);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error.code !== 'EEXIST')
|
|
151
|
+
throw error;
|
|
152
|
+
if (Date.now() - started >= timeoutMs) {
|
|
153
|
+
throw new EvidenceAppendLockError(`Timed out waiting for the evidence append lock for ${taskId}. Lock directory: ${lockPortablePath}. ` +
|
|
154
|
+
`If no HADARA process is writing evidence, the lock is stale (inspect ${lockPortablePath}/lock.json for the owning pid); remove the lock directory and retry.`);
|
|
155
|
+
}
|
|
156
|
+
sleepSync(25);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
writeLockMetadata(lockDir, taskId);
|
|
160
|
+
try {
|
|
161
|
+
return fn();
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
try {
|
|
165
|
+
node_fs_1.default.rmSync(lockDir, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Best-effort cleanup; later writers fail closed through the timeout.
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function writeLockMetadata(lockDir, taskId) {
|
|
173
|
+
try {
|
|
174
|
+
node_fs_1.default.writeFileSync(node_path_1.default.join(lockDir, 'lock.json'), `${JSON.stringify({ pid: process.pid, taskId, command: 'evidence.append', createdAt: new Date().toISOString() })}\n`, 'utf8');
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// Lock ownership is held by the directory; metadata is a best-effort diagnostic aid only.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function sleepSync(ms) {
|
|
181
|
+
const signal = new Int32Array(new SharedArrayBuffer(4));
|
|
182
|
+
Atomics.wait(signal, 0, 0, ms);
|
|
183
|
+
}
|
|
98
184
|
function appendEvidenceRecord(input) {
|
|
99
185
|
const summary = (0, redaction_1.redactSecrets)(input.record.summary.replace(/\|/g, '/'));
|
|
100
186
|
const markdownPath = node_path_1.default.join(input.taskDir, 'EVIDENCE.md');
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
attachedPath
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
(
|
|
123
|
-
|
|
187
|
+
return withEvidenceAppendLock(input.projectRoot, input.record.taskId, () => {
|
|
188
|
+
const idempotencyKey = input.record.idempotencyKey;
|
|
189
|
+
if (idempotencyKey) {
|
|
190
|
+
const existing = readEvidenceIndex(input.taskDir).find((record) => persistedEvidenceIdempotencyKey(record) === idempotencyKey);
|
|
191
|
+
if (existing) {
|
|
192
|
+
return {
|
|
193
|
+
markdownPath,
|
|
194
|
+
evidence: existing,
|
|
195
|
+
markdownAppended: false,
|
|
196
|
+
jsonlAppended: false,
|
|
197
|
+
existing: true
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const attachedPath = input.createAttachedPath();
|
|
202
|
+
const rowSummary = input.visibility === 'private' || !attachedPath ? summary : `${summary} (${attachedPath})`;
|
|
203
|
+
const jsonlMarker = input.visibility === 'public' && attachedPath ? attachedPath : 'evidence.jsonl';
|
|
204
|
+
const row = `| ${input.time} | ${input.record.kind} | ${rowSummary} | ${input.record.result} | ${input.visibility} | ${jsonlMarker} |\n`;
|
|
205
|
+
if (!node_fs_1.default.existsSync(markdownPath)) {
|
|
206
|
+
node_fs_1.default.writeFileSync(markdownPath, '# Evidence\n\n| Time | Kind | Summary | Result | Visibility | JSONL |\n|---|---|---|---|---|---|\n', 'utf8');
|
|
207
|
+
}
|
|
208
|
+
node_fs_1.default.appendFileSync(markdownPath, row, 'utf8');
|
|
209
|
+
const evidence = createEvidenceV2Record({
|
|
210
|
+
time: input.time,
|
|
124
211
|
taskId: input.record.taskId,
|
|
125
212
|
kind: input.record.kind,
|
|
126
213
|
summary,
|
|
127
214
|
result: input.record.result,
|
|
128
|
-
|
|
129
|
-
|
|
215
|
+
visibility: input.visibility,
|
|
216
|
+
attachedPath,
|
|
217
|
+
tags: input.record.tags,
|
|
218
|
+
idempotencyKey: input.record.idempotencyKey,
|
|
219
|
+
actor: input.record.actor
|
|
130
220
|
});
|
|
131
|
-
|
|
132
|
-
|
|
221
|
+
appendEvidenceIndex(input.taskDir, evidence);
|
|
222
|
+
if (input.visibility === 'private') {
|
|
223
|
+
(0, private_manifest_1.writePrivateEvidenceManifest)({
|
|
224
|
+
projectRoot: input.projectRoot,
|
|
225
|
+
taskId: input.record.taskId,
|
|
226
|
+
kind: input.record.kind,
|
|
227
|
+
summary,
|
|
228
|
+
result: input.record.result,
|
|
229
|
+
sourcePath: input.record.path,
|
|
230
|
+
time: input.time
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
markdownPath,
|
|
235
|
+
evidence,
|
|
236
|
+
markdownAppended: true,
|
|
237
|
+
jsonlAppended: true,
|
|
238
|
+
existing: false
|
|
239
|
+
};
|
|
240
|
+
});
|
|
133
241
|
}
|
|
134
242
|
function createEvidenceV2Record(input) {
|
|
135
243
|
const legacy = {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createCiGateReport = createCiGateReport;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const proof_status_1 = require("./proof-status");
|
|
10
|
+
const evidence_lint_1 = require("./evidence-lint");
|
|
11
|
+
const protocol_consistency_1 = require("./protocol-consistency");
|
|
12
|
+
const task_capsule_1 = require("../task/task-capsule");
|
|
13
|
+
function createCiGateReport(projectRoot, mode, options = {}) {
|
|
14
|
+
const allowEmpty = options.allowEmpty ?? false;
|
|
15
|
+
const tasks = selectTasks(projectRoot, options.taskId);
|
|
16
|
+
const checks = [];
|
|
17
|
+
const blockers = [];
|
|
18
|
+
const warnings = [];
|
|
19
|
+
applyScopeGuard({ taskId: options.taskId, mode, allowEmpty, taskCount: tasks.length, checks, blockers, warnings });
|
|
20
|
+
const protocol = options.taskId ? (0, protocol_consistency_1.createTaskProtocolConsistencyReport)(projectRoot, options.taskId) : (0, protocol_consistency_1.createAllProtocolConsistencyReport)(projectRoot);
|
|
21
|
+
checks.push({
|
|
22
|
+
id: options.taskId ? `protocol:${options.taskId}` : 'protocol:all',
|
|
23
|
+
source: 'protocol',
|
|
24
|
+
ok: protocol.ok,
|
|
25
|
+
...(options.taskId ? { taskId: options.taskId } : {}),
|
|
26
|
+
summary: protocol.ok ? 'Protocol checks passed.' : 'Protocol checks reported issues.'
|
|
27
|
+
});
|
|
28
|
+
for (const issue of protocol.issues) {
|
|
29
|
+
const target = issue.severity === 'error' ? blockers : warnings;
|
|
30
|
+
target.push({ severity: issue.severity, source: 'protocol', code: issue.code, message: issue.message, path: issue.path });
|
|
31
|
+
}
|
|
32
|
+
for (const task of tasks) {
|
|
33
|
+
const evidence = (0, evidence_lint_1.createEvidenceLintReport)(projectRoot, task.id);
|
|
34
|
+
checks.push({
|
|
35
|
+
id: `evidence:${task.id}`,
|
|
36
|
+
source: 'evidence',
|
|
37
|
+
ok: evidence.ok,
|
|
38
|
+
taskId: task.id,
|
|
39
|
+
summary: evidence.ok ? 'Evidence lint passed.' : 'Evidence lint reported issues.'
|
|
40
|
+
});
|
|
41
|
+
for (const issue of evidence.issues) {
|
|
42
|
+
const target = issue.severity === 'error' ? blockers : warnings;
|
|
43
|
+
target.push({ severity: issue.severity, source: 'evidence', code: issue.code, message: issue.message, taskId: task.id, path: issue.path });
|
|
44
|
+
}
|
|
45
|
+
if (taskLooksDone(task.dir) || options.taskId) {
|
|
46
|
+
const proof = (0, proof_status_1.createProofStatusReport)(projectRoot, task.id);
|
|
47
|
+
checks.push(toProofCheck(task.id, proof));
|
|
48
|
+
for (const issue of proof.blockers)
|
|
49
|
+
blockers.push({ ...issue, source: 'proof', taskId: task.id });
|
|
50
|
+
for (const issue of proof.warnings)
|
|
51
|
+
warnings.push({ ...issue, source: 'proof', taskId: task.id });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
checks.push({
|
|
55
|
+
id: 'release:deferred',
|
|
56
|
+
source: 'release',
|
|
57
|
+
ok: true,
|
|
58
|
+
summary: 'Release gate aggregation is deferred unless release work is explicitly requested.'
|
|
59
|
+
});
|
|
60
|
+
return {
|
|
61
|
+
schemaVersion: 'hadara.ci.gate.v1',
|
|
62
|
+
command: 'ci.gate',
|
|
63
|
+
ok: mode === 'advisory' ? true : blockers.length === 0,
|
|
64
|
+
mode,
|
|
65
|
+
scope: { ...(options.taskId ? { taskId: options.taskId } : {}), taskCount: tasks.length, allowEmpty },
|
|
66
|
+
checks,
|
|
67
|
+
blockers,
|
|
68
|
+
warnings
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function applyScopeGuard(input) {
|
|
72
|
+
if (input.taskCount > 0) {
|
|
73
|
+
input.checks.push({
|
|
74
|
+
id: 'scope:tasks',
|
|
75
|
+
source: 'proof',
|
|
76
|
+
ok: true,
|
|
77
|
+
...(input.taskId ? { taskId: input.taskId } : {}),
|
|
78
|
+
summary: `Validating ${input.taskCount} task capsule${input.taskCount === 1 ? '' : 's'}.`
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (input.taskId) {
|
|
83
|
+
input.checks.push({ id: 'scope:tasks', source: 'proof', ok: false, taskId: input.taskId, summary: `Requested task ${input.taskId} was not found.` });
|
|
84
|
+
input.blockers.push({
|
|
85
|
+
severity: 'error',
|
|
86
|
+
source: 'proof',
|
|
87
|
+
code: 'CI_GATE_TASK_NOT_FOUND',
|
|
88
|
+
message: `CI gate found no Task Capsule for ${input.taskId}. Pass an existing task id.`,
|
|
89
|
+
taskId: input.taskId
|
|
90
|
+
});
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const emptyScopeIsBlocking = input.mode === 'strict' && !input.allowEmpty;
|
|
94
|
+
input.checks.push({ id: 'scope:tasks', source: 'proof', ok: !emptyScopeIsBlocking, summary: 'No Done Task Capsules were found to validate.' });
|
|
95
|
+
const issue = {
|
|
96
|
+
severity: emptyScopeIsBlocking ? 'error' : 'warning',
|
|
97
|
+
source: 'proof',
|
|
98
|
+
code: 'CI_GATE_NO_DONE_TASKS',
|
|
99
|
+
message: emptyScopeIsBlocking
|
|
100
|
+
? 'Strict CI gate found no Done Task Capsules to validate. Pass --task <id> to scope a specific task, or --allow-empty only for bootstrap projects.'
|
|
101
|
+
: 'CI gate found no Done Task Capsules to validate; proof checks were skipped.'
|
|
102
|
+
};
|
|
103
|
+
(emptyScopeIsBlocking ? input.blockers : input.warnings).push(issue);
|
|
104
|
+
}
|
|
105
|
+
function selectTasks(projectRoot, taskId) {
|
|
106
|
+
const tasks = (0, task_capsule_1.listTaskCapsules)(projectRoot);
|
|
107
|
+
if (taskId)
|
|
108
|
+
return tasks.filter((task) => task.id === taskId);
|
|
109
|
+
return tasks.filter((task) => taskLooksDone(task.dir));
|
|
110
|
+
}
|
|
111
|
+
function taskLooksDone(taskDir) {
|
|
112
|
+
const taskPath = node_path_1.default.join(taskDir, 'TASK.md');
|
|
113
|
+
if (!node_fs_1.default.existsSync(taskPath))
|
|
114
|
+
return false;
|
|
115
|
+
return /\| Status \| Done \|/.test(node_fs_1.default.readFileSync(taskPath, 'utf8'));
|
|
116
|
+
}
|
|
117
|
+
function toProofCheck(taskId, proof) {
|
|
118
|
+
return {
|
|
119
|
+
id: `proof:${taskId}`,
|
|
120
|
+
source: 'proof',
|
|
121
|
+
ok: proof.ok,
|
|
122
|
+
taskId,
|
|
123
|
+
summary: `Proof verdict ${proof.verdict}; freshness ${proof.freshness.status}.`
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createProofStatusReport = createProofStatusReport;
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const normalizer_1 = require("../evidence/normalizer");
|
|
9
|
+
const semantics_1 = require("../evidence/semantics");
|
|
10
|
+
const task_capsule_1 = require("../task/task-capsule");
|
|
11
|
+
const task_close_1 = require("../task/task-close");
|
|
12
|
+
const evidence_lint_1 = require("./evidence-lint");
|
|
13
|
+
function createProofStatusReport(projectRoot, taskId, mode = 'status') {
|
|
14
|
+
const task = (0, task_capsule_1.findTaskCapsule)(projectRoot, taskId);
|
|
15
|
+
if (!task) {
|
|
16
|
+
return {
|
|
17
|
+
schemaVersion: mode === 'explain' ? 'hadara.proof.explain.v1' : 'hadara.proof.status.v1',
|
|
18
|
+
command: mode === 'explain' ? 'proof.explain' : 'proof.status',
|
|
19
|
+
ok: false,
|
|
20
|
+
target: { kind: 'task', taskId },
|
|
21
|
+
claim: 'task-readiness',
|
|
22
|
+
verdict: 'unknown',
|
|
23
|
+
freshness: { status: 'unknown', checkedSources: [] },
|
|
24
|
+
summary: { passed: 0, failed: 0, blocked: 0, privateOnlySubstantive: 0, substantivePositive: 0 },
|
|
25
|
+
supportingEvidence: [],
|
|
26
|
+
blockers: [{ severity: 'error', code: 'TASK_NOT_FOUND', message: `Task Capsule not found: ${taskId}` }],
|
|
27
|
+
warnings: [],
|
|
28
|
+
nextActions: [{ id: 'create-task', message: `Create or select an existing Task Capsule for ${taskId}.` }],
|
|
29
|
+
...(mode === 'explain' ? { explanation: createExplanation([], []) } : {})
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const lint = (0, evidence_lint_1.createEvidenceLintReport)(projectRoot, taskId);
|
|
33
|
+
const audit = (0, task_close_1.createTaskAuditCloseReport)(projectRoot, taskId);
|
|
34
|
+
const normalized = (0, normalizer_1.normalizeEvidenceRecordsInMemoryOrder)(lint.records, { taskDir: task.dir });
|
|
35
|
+
const semanticIssues = lint.issues.filter((issue) => issue.code.startsWith('TASK_DONE_'));
|
|
36
|
+
const freshnessIssues = audit.issues.map((issue) => ({
|
|
37
|
+
severity: 'warning',
|
|
38
|
+
code: issue.code,
|
|
39
|
+
message: issue.message,
|
|
40
|
+
path: issue.path
|
|
41
|
+
}));
|
|
42
|
+
const blockers = semanticIssues.filter((issue) => issue.severity === 'error').map(toProofIssue);
|
|
43
|
+
const warnings = [...semanticIssues.filter((issue) => issue.severity === 'warning').map(toProofIssue), ...freshnessIssues];
|
|
44
|
+
const summary = summarizeProofEvidence(normalized);
|
|
45
|
+
const freshness = createFreshness(projectRoot, task.dir, audit);
|
|
46
|
+
const verdict = selectVerdict({ blockers, warnings, summary, freshnessStatus: freshness.status });
|
|
47
|
+
return {
|
|
48
|
+
schemaVersion: mode === 'explain' ? 'hadara.proof.explain.v1' : 'hadara.proof.status.v1',
|
|
49
|
+
command: mode === 'explain' ? 'proof.explain' : 'proof.status',
|
|
50
|
+
ok: blockers.length === 0,
|
|
51
|
+
target: { kind: 'task', taskId },
|
|
52
|
+
claim: 'task-readiness',
|
|
53
|
+
verdict,
|
|
54
|
+
freshness,
|
|
55
|
+
summary,
|
|
56
|
+
supportingEvidence: normalized.filter((record) => (0, semantics_1.classifyEvidenceStrength)(record) === 'substantive-positive').slice(-5).map(toEvidenceRef),
|
|
57
|
+
blockers,
|
|
58
|
+
warnings,
|
|
59
|
+
nextActions: createNextActions(taskId, verdict, blockers, freshness.status),
|
|
60
|
+
...(mode === 'explain' ? { explanation: createExplanation(semanticIssues, freshnessIssues) } : {})
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function summarizeProofEvidence(records) {
|
|
64
|
+
const substantive = records.filter((record) => (0, semantics_1.classifyEvidenceStrength)(record) === 'substantive-positive');
|
|
65
|
+
return {
|
|
66
|
+
passed: records.filter((record) => record.outcome === 'passed').length,
|
|
67
|
+
failed: records.filter((record) => record.outcome === 'failed').length,
|
|
68
|
+
blocked: records.filter((record) => record.outcome === 'blocked').length,
|
|
69
|
+
privateOnlySubstantive: substantive.length > 0 && substantive.every((record) => record.visibility === 'private') ? substantive.length : 0,
|
|
70
|
+
substantivePositive: substantive.length
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function createFreshness(projectRoot, taskDir, audit) {
|
|
74
|
+
// Freshness is derived from the task close audit, whose source hash covers the full
|
|
75
|
+
// close-relevant document set; expose that same set plus the evidence files the proof reads.
|
|
76
|
+
const checkedSources = Array.from(new Set([
|
|
77
|
+
...(0, task_close_1.closeRelevantSourceRelativePaths)(projectRoot, taskDir),
|
|
78
|
+
toPortablePath(node_path_1.default.relative(projectRoot, node_path_1.default.join(taskDir, 'evidence.jsonl'))),
|
|
79
|
+
toPortablePath(node_path_1.default.relative(projectRoot, node_path_1.default.join(taskDir, 'EVIDENCE.md')))
|
|
80
|
+
])).sort();
|
|
81
|
+
const verdict = audit.auditVerdict.verdict;
|
|
82
|
+
const status = !audit.auditVerdict.closeEvidenceFound
|
|
83
|
+
? 'missing'
|
|
84
|
+
: audit.auditVerdict.verdict === 'closed-valid'
|
|
85
|
+
? 'fresh'
|
|
86
|
+
: 'stale';
|
|
87
|
+
return {
|
|
88
|
+
status,
|
|
89
|
+
checkedSources,
|
|
90
|
+
closeVerdict: verdict,
|
|
91
|
+
...(audit.auditVerdict.reportHashMatches !== undefined ? { reportHashMatches: audit.auditVerdict.reportHashMatches } : {}),
|
|
92
|
+
...(audit.auditVerdict.sourceHashMatches !== undefined ? { sourceHashMatches: audit.auditVerdict.sourceHashMatches } : {})
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function selectVerdict(input) {
|
|
96
|
+
const codes = new Set(input.blockers.map((issue) => issue.code));
|
|
97
|
+
if (codes.has('TASK_DONE_WITH_FAILED_EVIDENCE') || codes.has('TASK_DONE_WITH_UNEXPLAINED_BLOCKED_EVIDENCE'))
|
|
98
|
+
return 'blocked';
|
|
99
|
+
if (input.summary.substantivePositive === 0 || codes.has('TASK_DONE_WITHOUT_SUBSTANTIVE_EVIDENCE') || codes.has('TASK_DONE_WITH_ONLY_WEAK_EVIDENCE')) {
|
|
100
|
+
return 'insufficient';
|
|
101
|
+
}
|
|
102
|
+
if (input.blockers.length > 0)
|
|
103
|
+
return 'blocked';
|
|
104
|
+
if (input.warnings.length > 0 || input.summary.privateOnlySubstantive > 0 || input.freshnessStatus !== 'fresh')
|
|
105
|
+
return 'warning';
|
|
106
|
+
return 'sufficient';
|
|
107
|
+
}
|
|
108
|
+
function createNextActions(taskId, verdict, blockers, freshnessStatus) {
|
|
109
|
+
const actions = [];
|
|
110
|
+
if (blockers.length > 0)
|
|
111
|
+
actions.push({ id: 'inspect-evidence-lint', command: `hadara evidence lint --task ${taskId} --json`, message: 'Inspect semantic evidence blockers.' });
|
|
112
|
+
if (freshnessStatus !== 'fresh')
|
|
113
|
+
actions.push({ id: 'refresh-close-proof', command: `hadara task close --task ${taskId} --json`, message: 'Review close proof freshness and append a fresh close proof when appropriate.' });
|
|
114
|
+
if (verdict === 'insufficient')
|
|
115
|
+
actions.push({ id: 'add-substantive-evidence', command: `hadara evidence add-command --task ${taskId} --summary "..." --result passed --json`, message: 'Record substantive public evidence for the readiness claim.' });
|
|
116
|
+
return actions;
|
|
117
|
+
}
|
|
118
|
+
function createExplanation(semanticIssues, freshnessIssues) {
|
|
119
|
+
return {
|
|
120
|
+
rules: [
|
|
121
|
+
'Substantive passed evidence is required for a sufficient task-readiness claim.',
|
|
122
|
+
'Unresolved failed or unexplained blocked evidence makes the proof blocked.',
|
|
123
|
+
'Private-only substantive evidence and stale or missing close proof produce warnings.',
|
|
124
|
+
'Freshness is derived from task close audit source/report hash comparison.'
|
|
125
|
+
],
|
|
126
|
+
semanticIssueCodes: semanticIssues.map((issue) => issue.code),
|
|
127
|
+
freshnessIssueCodes: freshnessIssues.map((issue) => issue.code)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function toProofIssue(issue) {
|
|
131
|
+
return {
|
|
132
|
+
severity: issue.severity,
|
|
133
|
+
code: issue.code,
|
|
134
|
+
message: issue.message,
|
|
135
|
+
...(issue.evidenceId ? { evidenceId: issue.evidenceId } : {}),
|
|
136
|
+
...(issue.path ? { path: issue.path } : {})
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function toEvidenceRef(record) {
|
|
140
|
+
return {
|
|
141
|
+
id: record.id,
|
|
142
|
+
time: record.time,
|
|
143
|
+
category: record.category,
|
|
144
|
+
outcome: record.outcome,
|
|
145
|
+
visibility: record.visibility,
|
|
146
|
+
summary: record.summary
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function toPortablePath(value) {
|
|
150
|
+
return value.split(node_path_1.default.sep).join('/');
|
|
151
|
+
}
|
package/dist/task/task-close.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createTaskCloseReport = createTaskCloseReport;
|
|
7
|
+
exports.closeRelevantSourceRelativePaths = closeRelevantSourceRelativePaths;
|
|
7
8
|
exports.executeTaskCloseEvidence = executeTaskCloseEvidence;
|
|
8
9
|
exports.createTaskAuditCloseReport = createTaskAuditCloseReport;
|
|
9
10
|
exports.formatTaskAuditCloseReport = formatTaskAuditCloseReport;
|
|
@@ -298,8 +299,8 @@ function hashValidationInputs(validation, evidenceLint, protocolDoctor) {
|
|
|
298
299
|
});
|
|
299
300
|
return `sha256:${node_crypto_1.default.createHash('sha256').update(payload, 'utf8').digest('hex')}`;
|
|
300
301
|
}
|
|
301
|
-
function
|
|
302
|
-
|
|
302
|
+
function closeRelevantSourceRelativePaths(projectRoot, taskDir) {
|
|
303
|
+
return [
|
|
303
304
|
node_path_1.default.relative(projectRoot, node_path_1.default.join(taskDir, 'TASK.md')),
|
|
304
305
|
node_path_1.default.relative(projectRoot, node_path_1.default.join(taskDir, 'PLAN.md')),
|
|
305
306
|
node_path_1.default.relative(projectRoot, node_path_1.default.join(taskDir, 'CONTEXT.md')),
|
|
@@ -313,6 +314,9 @@ function hashCloseRelevantSource(projectRoot, taskDir) {
|
|
|
313
314
|
]
|
|
314
315
|
.map(toPortablePath)
|
|
315
316
|
.sort();
|
|
317
|
+
}
|
|
318
|
+
function hashCloseRelevantSource(projectRoot, taskDir) {
|
|
319
|
+
const relativePaths = closeRelevantSourceRelativePaths(projectRoot, taskDir);
|
|
316
320
|
const payload = relativePaths.map((relativePath) => {
|
|
317
321
|
const absolutePath = node_path_1.default.join(projectRoot, relativePath);
|
|
318
322
|
return {
|