hadara 0.3.0 → 0.3.2-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,10 +3,11 @@ 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.EvidenceAppendLockError = exports.EvidenceArtifactPolicyError = void 0;
6
+ exports.EvidenceTaskDirectoryError = exports.EvidenceResultOutcomeMismatchError = exports.EvidenceAppendLockError = exports.EvidenceArtifactPolicyError = void 0;
7
7
  exports.persistedEvidenceKind = persistedEvidenceKind;
8
8
  exports.persistedEvidenceResult = persistedEvidenceResult;
9
9
  exports.persistedEvidencePath = persistedEvidencePath;
10
+ exports.validateEvidenceResultOutcomeCompatibility = validateEvidenceResultOutcomeCompatibility;
10
11
  exports.appendEvidence = appendEvidence;
11
12
  exports.appendEvidenceWithResult = appendEvidenceWithResult;
12
13
  exports.appendEvidenceTextArtifact = appendEvidenceTextArtifact;
@@ -48,13 +49,53 @@ class EvidenceAppendLockError extends Error {
48
49
  }
49
50
  }
50
51
  exports.EvidenceAppendLockError = EvidenceAppendLockError;
52
+ class EvidenceResultOutcomeMismatchError extends Error {
53
+ issue;
54
+ code = 'EVIDENCE_RESULT_OUTCOME_MISMATCH';
55
+ constructor(issue) {
56
+ super(issue.message);
57
+ this.issue = issue;
58
+ this.name = 'EvidenceResultOutcomeMismatchError';
59
+ }
60
+ }
61
+ exports.EvidenceResultOutcomeMismatchError = EvidenceResultOutcomeMismatchError;
62
+ class EvidenceTaskDirectoryError extends Error {
63
+ code;
64
+ constructor(code, message) {
65
+ super(message);
66
+ this.code = code;
67
+ this.name = 'EvidenceTaskDirectoryError';
68
+ }
69
+ }
70
+ exports.EvidenceTaskDirectoryError = EvidenceTaskDirectoryError;
71
+ function validateEvidenceResultOutcomeCompatibility(input) {
72
+ if (!input.outcome)
73
+ return undefined;
74
+ if (input.outcome === 'passed' || input.outcome === 'failed' || input.outcome === 'blocked' || input.outcome === 'unknown') {
75
+ if (input.result === input.outcome)
76
+ return undefined;
77
+ return {
78
+ severity: 'error',
79
+ code: 'EVIDENCE_RESULT_OUTCOME_MISMATCH',
80
+ message: `Evidence result ${input.result} conflicts with outcome ${input.outcome}; matching evidence outcomes must use the same legacy result.`
81
+ };
82
+ }
83
+ if ((input.outcome === 'recorded' || input.outcome === 'not-applicable') && input.result !== 'unknown') {
84
+ return {
85
+ severity: 'error',
86
+ code: 'EVIDENCE_RESULT_OUTCOME_MISMATCH',
87
+ message: `Evidence outcome ${input.outcome} is record-only/non-applicable evidence and requires legacy result unknown.`
88
+ };
89
+ }
90
+ return undefined;
91
+ }
51
92
  function appendEvidence(projectRoot, record) {
52
93
  return appendEvidenceWithResult(projectRoot, record).markdownPath;
53
94
  }
54
95
  function appendEvidenceWithResult(projectRoot, record) {
55
96
  const taskDir = findTaskDir(projectRoot, record.taskId);
56
97
  if (!taskDir) {
57
- throw new Error(`Task capsule not found: ${record.taskId}`);
98
+ throw new EvidenceTaskDirectoryError('TASK_NOT_FOUND', `Task Capsule not found: ${record.taskId}`);
58
99
  }
59
100
  const time = new Date().toISOString();
60
101
  const visibility = record.visibility ?? 'public';
@@ -70,7 +111,7 @@ function appendEvidenceWithResult(projectRoot, record) {
70
111
  function appendEvidenceTextArtifact(projectRoot, record, artifact, options = {}) {
71
112
  const taskDir = findTaskDir(projectRoot, record.taskId);
72
113
  if (!taskDir) {
73
- throw new Error(`Task capsule not found: ${record.taskId}`);
114
+ throw new EvidenceTaskDirectoryError('TASK_NOT_FOUND', `Task Capsule not found: ${record.taskId}`);
74
115
  }
75
116
  const time = new Date().toISOString();
76
117
  const visibility = record.visibility ?? 'public';
@@ -182,6 +223,13 @@ function sleepSync(ms) {
182
223
  Atomics.wait(signal, 0, 0, ms);
183
224
  }
184
225
  function appendEvidenceRecord(input) {
226
+ const resultOutcomeIssue = validateEvidenceResultOutcomeCompatibility({
227
+ result: input.record.result,
228
+ outcome: input.record.outcome
229
+ });
230
+ if (resultOutcomeIssue) {
231
+ throw new EvidenceResultOutcomeMismatchError(resultOutcomeIssue);
232
+ }
185
233
  const summary = (0, redaction_1.redactSecrets)(input.record.summary.replace(/\|/g, '/'));
186
234
  const markdownPath = node_path_1.default.join(input.taskDir, 'EVIDENCE.md');
187
235
  return withEvidenceAppendLock(input.projectRoot, input.record.taskId, () => {
@@ -212,6 +260,8 @@ function appendEvidenceRecord(input) {
212
260
  kind: input.record.kind,
213
261
  summary,
214
262
  result: input.record.result,
263
+ category: input.record.category,
264
+ outcome: input.record.outcome,
215
265
  visibility: input.visibility,
216
266
  attachedPath,
217
267
  tags: input.record.tags,
@@ -250,8 +300,8 @@ function createEvidenceV2Record(input) {
250
300
  schemaVersion: 'hadara.evidence.v2',
251
301
  time: input.time,
252
302
  taskId: input.taskId,
253
- category: deriveEvidenceCategory(input.kind, input.summary),
254
- outcome: normalizeEvidenceOutcome(input.result),
303
+ category: input.category ?? deriveEvidenceCategory(input.kind, input.summary),
304
+ outcome: input.outcome ?? normalizeEvidenceOutcome(input.result),
255
305
  visibility: input.visibility,
256
306
  summary: input.summary,
257
307
  artifacts: input.visibility === 'public' && input.attachedPath
@@ -375,8 +425,13 @@ function findTaskDir(projectRoot, taskId) {
375
425
  const tasksDir = node_path_1.default.join(projectRoot, 'tasks');
376
426
  if (!node_fs_1.default.existsSync(tasksDir))
377
427
  return null;
378
- const entry = node_fs_1.default.readdirSync(tasksDir).find((name) => name.startsWith(`${taskId}-`));
379
- return entry ? node_path_1.default.join(tasksDir, entry) : null;
428
+ const entries = node_fs_1.default
429
+ .readdirSync(tasksDir, { withFileTypes: true })
430
+ .filter((entry) => entry.isDirectory() && entry.name.startsWith(`${taskId}-`) && node_fs_1.default.existsSync(node_path_1.default.join(tasksDir, entry.name, 'TASK.md')));
431
+ if (entries.length > 1) {
432
+ throw new EvidenceTaskDirectoryError('TASK_CAPSULE_AMBIGUOUS', `Multiple Task Capsules found for ${taskId}; remove or repair duplicate TASK.md-bearing directories before recording evidence.`);
433
+ }
434
+ return entries[0] ? node_path_1.default.join(tasksDir, entries[0].name) : null;
380
435
  }
381
436
  function createSessionEvidenceDirs(dataRoot, sessionId) {
382
437
  const evidenceDir = node_path_1.default.join(dataRoot, 'sessions', sessionId, 'evidence');
@@ -82,10 +82,10 @@ function findUnresolvedFailedEvidence(records, taskDocs = {}) {
82
82
  if (record.outcome !== 'failed')
83
83
  return false;
84
84
  const laterRecords = records.slice(index + 1);
85
- if (laterRecords.some((candidate) => candidate.outcome === 'passed' && candidate.category === record.category))
86
- return false;
87
85
  if (laterRecords.some((candidate) => hasExactResolutionMarker(candidate, record.id)))
88
86
  return false;
87
+ if (usesLegacySameCategoryFallback(record, laterRecords))
88
+ return false;
89
89
  if (hasResidualRiskDocumentation(record, taskDocs))
90
90
  return false;
91
91
  return true;
@@ -137,7 +137,7 @@ function analyzeTaskEvidenceSemantics(input) {
137
137
  message: 'Task has unresolved failed evidence while marked Done.',
138
138
  evidenceId: record.id,
139
139
  path: `${input.taskDir}/evidence.jsonl`,
140
- expected: `later passed same-category evidence, supersedes:${record.id}, resolves:${record.id}, or explicit residual-risk documentation`,
140
+ expected: `supersedes:${record.id}, resolves:${record.id}, legacy same-category passed evidence, or explicit residual-risk documentation`,
141
141
  actual: 'failed evidence has no resolution signal'
142
142
  });
143
143
  }
@@ -191,8 +191,17 @@ function isRecordOnlyCategory(category) {
191
191
  return exports.RECORD_ONLY_EVIDENCE_CATEGORIES.includes(category);
192
192
  }
193
193
  function hasExactResolutionMarker(record, evidenceId) {
194
+ if (record.outcome !== 'passed' && record.outcome !== 'recorded')
195
+ return false;
194
196
  return record.tags.includes(`supersedes:${evidenceId}`) || record.tags.includes(`resolves:${evidenceId}`);
195
197
  }
198
+ function usesLegacySameCategoryFallback(record, laterRecords) {
199
+ if (record.persistedSchemaVersion !== 'hadara.evidence.v1')
200
+ return false;
201
+ return laterRecords.some((candidate) => candidate.persistedSchemaVersion === 'hadara.evidence.v1' &&
202
+ candidate.outcome === 'passed' &&
203
+ candidate.category === record.category);
204
+ }
196
205
  function hasResidualRiskDocumentation(record, taskDocs) {
197
206
  const content = joinTaskDocs(taskDocs);
198
207
  if (!content)
@@ -25,6 +25,8 @@ const REQUIRED_TASK_FILES = [
25
25
  const EVIDENCE_KINDS = new Set(['test-log', 'command-log', 'diff-summary', 'screenshot', 'note']);
26
26
  const EVIDENCE_RESULTS = new Set(['passed', 'failed', 'blocked', 'unknown']);
27
27
  const EVIDENCE_VISIBILITIES = new Set(['public', 'private']);
28
+ const TASK_STATUS_TOKENS = new Set(['draft', 'in progress', 'blocked', 'done', 'partial', 'superseded', 'archived']);
29
+ const STALE_PENDING_CLOSE_PATTERN = /\b(?:done\s+pending\s+lifecycle\s+close|pending\s+lifecycle\s+close)\b/i;
28
30
  const DONE_SEMANTIC_EVIDENCE_CODES = new Set([
29
31
  'TASK_DONE_WITHOUT_SUBSTANTIVE_EVIDENCE',
30
32
  'TASK_DONE_WITH_FAILED_EVIDENCE',
@@ -338,6 +340,8 @@ function validateDoneLevel(projectRoot, task, issues, checkedFiles) {
338
340
  validateEvidenceIndexHasRecords(projectRoot, task, issues);
339
341
  validateEvidenceSemanticGates(projectRoot, task, issues);
340
342
  validateHandoffDone(projectRoot, task, issues);
343
+ validateHandoffCurrentStateTokens(projectRoot, task, issues);
344
+ validatePlanStatusDrift(projectRoot, task, issues);
341
345
  validateTaskBoardDone(projectRoot, task, issues, checkedFiles);
342
346
  }
343
347
  function validateTaskMetadataComplete(projectRoot, task, issues) {
@@ -645,6 +649,112 @@ function validateHandoffDone(projectRoot, task, issues) {
645
649
  });
646
650
  }
647
651
  }
652
+ function validateHandoffCurrentStateTokens(projectRoot, task, issues) {
653
+ const handoffPath = node_path_1.default.join(task.dir, 'HANDOFF.md');
654
+ if (!node_fs_1.default.existsSync(handoffPath))
655
+ return;
656
+ const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, handoffPath));
657
+ const currentState = readSectionBody(handoffPath, '## Current State');
658
+ const fields = handoffCurrentStateFields(currentState);
659
+ const taskStatus = fields.get('taskstatus') ?? fields.get('task status');
660
+ const legacyStatus = fields.get('status');
661
+ const closeState = fields.get('closestate') ?? fields.get('close state');
662
+ if (taskStatus && !isTaskStatusToken(taskStatus)) {
663
+ issues.push(handoffStatusIssue(relativePath, `HANDOFF.md TaskStatus uses non-canonical value "${taskStatus}".`));
664
+ }
665
+ if (legacyStatus) {
666
+ if (STALE_PENDING_CLOSE_PATTERN.test(legacyStatus)) {
667
+ issues.push(handoffStatusIssue(relativePath, 'HANDOFF.md legacy Status mixes lifecycle state with pending close proof wording.'));
668
+ }
669
+ else if (!isTaskStatusToken(legacyStatus)) {
670
+ issues.push(handoffStatusIssue(relativePath, `HANDOFF.md legacy Status uses non-canonical value "${legacyStatus}".`));
671
+ }
672
+ }
673
+ if (STALE_PENDING_CLOSE_PATTERN.test(currentState) && (!legacyStatus || !STALE_PENDING_CLOSE_PATTERN.test(legacyStatus))) {
674
+ issues.push(handoffStatusIssue(relativePath, 'HANDOFF.md Current State contains stale pending lifecycle close wording.'));
675
+ }
676
+ if (closeState) {
677
+ issues.push({
678
+ severity: 'error',
679
+ code: 'TASK_HANDOFF_CLOSE_STATE_PERSISTED',
680
+ message: `HANDOFF.md persists derived CloseState value "${closeState}".`,
681
+ path: relativePath,
682
+ heading: 'Current State',
683
+ fixHint: 'Remove the CloseState row from task-local HANDOFF.md; use task status, audit-close, proof status, or state verify read models for derived close state.',
684
+ example: '| TaskStatus | Done |',
685
+ remediationHint: {
686
+ path: relativePath,
687
+ heading: 'Current State',
688
+ requiredChange: 'Remove persistent CloseState from this close-source handoff table.',
689
+ example: '| TaskStatus | Done |',
690
+ blocking: true
691
+ }
692
+ });
693
+ }
694
+ }
695
+ function handoffCurrentStateFields(currentState) {
696
+ const fields = new Map();
697
+ for (const cells of (0, markdown_table_1.parseMarkdownRows)(currentState)) {
698
+ if (cells.length < 2 || /^field$/i.test(cells[0] ?? ''))
699
+ continue;
700
+ fields.set(normalizeFieldName(cells[0] ?? ''), cells[1]?.trim() ?? '');
701
+ }
702
+ return fields;
703
+ }
704
+ function handoffStatusIssue(relativePath, message) {
705
+ return {
706
+ severity: 'error',
707
+ code: 'TASK_HANDOFF_STATUS_DRIFT',
708
+ message,
709
+ path: relativePath,
710
+ heading: 'Current State',
711
+ fixHint: 'Use `TaskStatus` for lifecycle state only; close proof state is derived from audit-close/proof/status read models.',
712
+ example: '| TaskStatus | Done |',
713
+ remediationHint: {
714
+ path: relativePath,
715
+ heading: 'Current State',
716
+ requiredChange: 'Replace ambiguous Status wording with canonical TaskStatus and remove close-proof wording from HANDOFF.md.',
717
+ example: '| TaskStatus | Done |',
718
+ blocking: true
719
+ }
720
+ };
721
+ }
722
+ function validatePlanStatusDrift(projectRoot, task, issues) {
723
+ const planPath = node_path_1.default.join(task.dir, 'PLAN.md');
724
+ if (!node_fs_1.default.existsSync(planPath))
725
+ return;
726
+ const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, planPath));
727
+ const rows = (0, markdown_table_1.parseMarkdownRows)(node_fs_1.default.readFileSync(planPath, 'utf8'));
728
+ const inProgressRows = rows.filter((cells) => {
729
+ if (/^step$/i.test(cells[0] ?? ''))
730
+ return false;
731
+ return (cells[2] ?? '').trim().toLowerCase() === 'in progress';
732
+ });
733
+ if (inProgressRows.length === 0)
734
+ return;
735
+ issues.push({
736
+ severity: 'error',
737
+ code: 'TASK_PLAN_STATUS_DRIFT',
738
+ message: `PLAN.md has ${inProgressRows.length} row(s) still marked In Progress while the task is Done.`,
739
+ path: relativePath,
740
+ heading: 'Plan',
741
+ fixHint: 'Before closing a Done task, mark completed plan rows Done or split/defer unfinished work explicitly instead of leaving rows In Progress.',
742
+ example: '| 3 | Commit the pre-publish preparation. | Done | `command:T-XXXX:check` |',
743
+ remediationHint: {
744
+ path: relativePath,
745
+ heading: 'Plan',
746
+ requiredChange: 'Update PLAN.md rows that are no longer active from In Progress to Done, Blocked, Partial, or a task-specific final status with evidence.',
747
+ example: '| 3 | Commit the pre-publish preparation. | Done | `command:T-XXXX:check` |',
748
+ blocking: true
749
+ }
750
+ });
751
+ }
752
+ function isTaskStatusToken(value) {
753
+ return TASK_STATUS_TOKENS.has(value.trim().toLowerCase());
754
+ }
755
+ function normalizeFieldName(value) {
756
+ return value.trim().toLowerCase().replace(/\s+/g, ' ');
757
+ }
648
758
  function validateTaskBoardDone(projectRoot, task, issues, checkedFiles) {
649
759
  const taskBoardPath = node_path_1.default.join(projectRoot, 'docs', 'TASK_BOARD.md');
650
760
  const relativePath = toPortablePath(node_path_1.default.relative(projectRoot, taskBoardPath));
@@ -25,25 +25,44 @@
25
25
  "evidenceV1": {
26
26
  "type": "object",
27
27
  "additionalProperties": true,
28
- "required": ["schemaVersion", "time", "taskId", "kind", "summary", "result", "visibility"],
28
+ "required": ["schemaVersion", "id", "sourceLine", "idSource", "idStability", "persistedSchemaVersion", "time", "taskId", "kind", "summary", "result", "visibility", "category", "outcome", "tags", "legacy"],
29
29
  "properties": {
30
30
  "schemaVersion": { "const": "hadara.evidence.v1" },
31
+ "id": { "type": "string", "pattern": "^legacy:T-[0-9]{4}:[0-9]+:[a-f0-9]{12}$" },
32
+ "sourceLine": { "type": "integer", "minimum": 1 },
33
+ "idSource": { "const": "line-fallback" },
34
+ "idStability": { "const": "unstable-on-reorder" },
35
+ "persistedSchemaVersion": { "const": "hadara.evidence.v1" },
31
36
  "time": { "type": "string", "minLength": 1 },
32
37
  "taskId": { "type": "string", "pattern": "^T-[0-9]{4}$" },
33
38
  "kind": { "type": "string", "enum": ["test-log", "command-log", "diff-summary", "screenshot", "note"] },
34
39
  "summary": { "type": "string", "minLength": 1 },
35
40
  "result": { "type": "string", "enum": ["passed", "failed", "blocked", "unknown"] },
36
- "visibility": { "type": "string", "enum": ["public", "private"] }
41
+ "visibility": { "type": "string", "enum": ["public", "private"] },
42
+ "category": { "type": "string", "enum": ["validation", "implementation", "release", "security", "policy", "operation", "decision", "handoff", "audit", "note", "observation"] },
43
+ "outcome": { "type": "string", "enum": ["passed", "failed", "blocked", "unknown", "recorded", "not-applicable"] },
44
+ "tags": { "type": "array", "items": { "type": "string" } },
45
+ "legacy": {
46
+ "type": "object",
47
+ "additionalProperties": true,
48
+ "required": ["kind", "result"],
49
+ "properties": {
50
+ "kind": { "type": "string", "enum": ["test-log", "command-log", "diff-summary", "screenshot", "note"] },
51
+ "result": { "type": "string", "enum": ["passed", "failed", "blocked", "unknown"] },
52
+ "evidencePath": { "type": "string", "minLength": 1 }
53
+ }
54
+ }
37
55
  }
38
56
  },
39
57
  "evidenceV2": {
40
58
  "type": "object",
41
59
  "additionalProperties": true,
42
- "required": ["schemaVersion", "id", "fingerprint", "idSource", "idStability", "time", "taskId", "category", "outcome", "visibility", "summary", "artifacts", "tags", "legacy"],
60
+ "required": ["schemaVersion", "id", "sourceLine", "persistedSchemaVersion", "fingerprint", "idSource", "idStability", "time", "taskId", "category", "outcome", "visibility", "summary", "artifacts", "tags", "legacy"],
43
61
  "properties": {
44
62
  "schemaVersion": { "const": "hadara.evidence.v2" },
45
63
  "id": { "type": "string", "minLength": 1 },
46
64
  "sourceLine": { "type": "integer", "minimum": 1 },
65
+ "persistedSchemaVersion": { "const": "hadara.evidence.v2" },
47
66
  "fingerprint": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
48
67
  "idSource": { "const": "persisted" },
49
68
  "idStability": { "const": "durable" },
@@ -274,6 +274,13 @@
274
274
  "owner": "runtime/version",
275
275
  "notes": "Documents read-only CLI origin, package, git, Node, and build freshness reports from hadara version --verbose."
276
276
  },
277
+ {
278
+ "id": "hadara.stateProjection.v1",
279
+ "path": "src/schemas/state-projection.schema.json",
280
+ "status": "fixture",
281
+ "owner": "services/state-projection",
282
+ "notes": "Documents the Phase 8.4 read-only state consistency projection over Task Board, Task Capsules, handoff, close evidence, docs registry, and release readiness state."
283
+ },
277
284
  {
278
285
  "id": "hadara.releaseGate.v1",
279
286
  "path": "src/schemas/release-gate.schema.json",
@@ -0,0 +1,115 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "hadara.stateProjection.v1",
4
+ "x-hadara-schema-id": "hadara.stateProjection.v1",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "required": ["schemaVersion", "command", "ok", "generatedAt", "projectRoot", "summary", "sources", "tasks", "issues"],
8
+ "properties": {
9
+ "schemaVersion": {
10
+ "const": "hadara.stateProjection.v1"
11
+ },
12
+ "command": {
13
+ "const": "state.projection"
14
+ },
15
+ "ok": {
16
+ "type": "boolean"
17
+ },
18
+ "generatedAt": {
19
+ "type": "string"
20
+ },
21
+ "projectRoot": {
22
+ "type": "string"
23
+ },
24
+ "summary": {
25
+ "type": "object",
26
+ "required": ["consistent", "issueCounts", "latestDoneTaskId", "activeTaskIds", "checkedTasks"],
27
+ "additionalProperties": true,
28
+ "properties": {
29
+ "consistent": {
30
+ "type": "boolean"
31
+ },
32
+ "issueCounts": {
33
+ "type": "object"
34
+ },
35
+ "latestDoneTaskId": {
36
+ "type": ["string", "null"]
37
+ },
38
+ "activeTaskIds": {
39
+ "type": "array",
40
+ "items": {
41
+ "type": "string"
42
+ }
43
+ },
44
+ "checkedTasks": {
45
+ "type": "number"
46
+ }
47
+ }
48
+ },
49
+ "sources": {
50
+ "type": "object"
51
+ },
52
+ "tasks": {
53
+ "type": "array",
54
+ "items": {
55
+ "type": "object",
56
+ "required": ["id", "title", "capsule", "task", "taskBoard", "handoff", "plan", "closeProof"],
57
+ "additionalProperties": true,
58
+ "properties": {
59
+ "id": {
60
+ "type": "string"
61
+ },
62
+ "title": {
63
+ "type": "string"
64
+ },
65
+ "capsule": {
66
+ "type": "string"
67
+ },
68
+ "task": {
69
+ "type": "object"
70
+ },
71
+ "taskBoard": {
72
+ "type": "object"
73
+ },
74
+ "handoff": {
75
+ "type": "object"
76
+ },
77
+ "plan": {
78
+ "type": "object"
79
+ },
80
+ "closeProof": {
81
+ "type": "object"
82
+ }
83
+ }
84
+ }
85
+ },
86
+ "issues": {
87
+ "type": "array",
88
+ "items": {
89
+ "type": "object",
90
+ "required": ["severity", "code", "message", "fixHint"],
91
+ "additionalProperties": true,
92
+ "properties": {
93
+ "severity": {
94
+ "enum": ["error", "warning", "info"]
95
+ },
96
+ "code": {
97
+ "type": "string"
98
+ },
99
+ "message": {
100
+ "type": "string"
101
+ },
102
+ "path": {
103
+ "type": "string"
104
+ },
105
+ "taskId": {
106
+ "type": "string"
107
+ },
108
+ "fixHint": {
109
+ "type": "string"
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
@@ -475,8 +475,8 @@ exports.HADARA_COMMAND_REGISTRY = [
475
475
  },
476
476
  {
477
477
  id: 'evidence.add-command',
478
- command: 'hadara evidence add-command --task <task-id> --summary <text> [--result <result>] [--idempotency-key <key>] [--json]',
479
- summary: 'Append command-log evidence to a Task Capsule.',
478
+ command: 'hadara evidence add-command --task <task-id> --summary <text> [--result <result>] [--outcome <outcome>] [--category <category>] [--resolves <id>] [--supersedes <id>] [--idempotency-key <key>] [--json]',
479
+ summary: 'Append command-log evidence to a Task Capsule, rejecting incompatible result/outcome metadata.',
480
480
  canonical: true,
481
481
  appearsInDefaultHelp: true,
482
482
  family: 'capsule-lifecycle',
@@ -490,14 +490,17 @@ exports.HADARA_COMMAND_REGISTRY = [
490
490
  status: 'stable',
491
491
  schemaVersion: 'hadara.evidence.collect.v1',
492
492
  docs: ['docs/IMPLEMENTATION_SOP.md'],
493
- examples: [example('Record command evidence', 'hadara evidence add-command --task T-0001 --summary "npm test passed" --result passed --json', 'After meaningful validation.')],
493
+ examples: [
494
+ example('Record command evidence', 'hadara evidence add-command --task T-0001 --summary "npm test passed" --result passed --category validation --json', 'After meaningful validation.')
495
+ ],
494
496
  related: ['evidence.list', 'evidence.lint', 'task.ready'],
497
+ notes: 'The collect response remains `hadara.evidence.collect.v1` with additive v2 metadata. A new add-command report schema id, check-id, and subject fields are deferred candidate scope.',
495
498
  conflictsWith: []
496
499
  },
497
500
  {
498
501
  id: 'evidence.list',
499
502
  command: 'hadara evidence list --task <task-id> [--limit <n>] [--include-private] [--json]',
500
- summary: 'List public evidence index records for a Task Capsule.',
503
+ summary: 'List Task Capsule evidence ids, category/outcome metadata, and sanitized evidence summaries.',
501
504
  canonical: true,
502
505
  appearsInDefaultHelp: false,
503
506
  family: 'capsule-lifecycle',
@@ -511,7 +514,7 @@ exports.HADARA_COMMAND_REGISTRY = [
511
514
  status: 'stable',
512
515
  schemaVersion: 'hadara.evidence.list.v1',
513
516
  docs: ['docs/IMPLEMENTATION_SOP.md'],
514
- examples: [example('List evidence', 'hadara evidence list --task T-0001 --json', 'When auditing a capsule evidence trail.')],
517
+ examples: [example('List evidence ids', 'hadara evidence list --task T-0001', 'Before copying a durable ev: id into --resolves or --supersedes.')],
515
518
  related: ['evidence.add-command', 'evidence.lint'],
516
519
  conflictsWith: []
517
520
  },
@@ -552,9 +555,10 @@ exports.HADARA_COMMAND_REGISTRY = [
552
555
  actor: 'operator',
553
556
  status: 'stable',
554
557
  schemaVersion: 'hadara.evidence.migration_preview.v1',
555
- docs: ['docs/EVIDENCE_SEMANTIC_CONTRACT.md'],
558
+ docs: ['docs/EVIDENCE_SEMANTIC_CONTRACT.md', 'docs/CLI_JSON_CONTRACT.md'],
556
559
  examples: [example('Preview evidence migration', 'hadara evidence migrate --task T-0001 --to v2 --json', 'When older evidence files need migration.')],
557
560
  related: ['evidence.lint', 'protocol.remediate'],
561
+ notes: 'This is an operator-selected JSONL migration surface, not evidence rebuild. 0.3.2 does not register or implement `hadara evidence rebuild`; `EVIDENCE.md` remains a non-canonical human summary.',
558
562
  conflictsWith: []
559
563
  },
560
564
  {
@@ -600,7 +604,7 @@ exports.HADARA_COMMAND_REGISTRY = [
600
604
  {
601
605
  id: 'ci.gate',
602
606
  command: 'hadara ci gate [--mode advisory|strict] [--task <task-id>] [--allow-empty] [--json]',
603
- summary: 'Evaluate CI-style task proof gates.',
607
+ summary: 'Evaluate CI-style task proof gates with advisory state consistency signals.',
604
608
  canonical: true,
605
609
  appearsInDefaultHelp: false,
606
610
  family: 'proof-diagnostics',
@@ -614,7 +618,28 @@ exports.HADARA_COMMAND_REGISTRY = [
614
618
  status: 'stable',
615
619
  docs: ['docs/IMPLEMENTATION_SOP.md'],
616
620
  examples: [example('Run strict CI gate', 'hadara ci gate --mode strict --task T-0001 --json', 'In automated validation paths.')],
617
- related: ['task.ready', 'proof.status'],
621
+ related: ['task.ready', 'proof.status', 'state.verify'],
622
+ conflictsWith: []
623
+ },
624
+ {
625
+ id: 'state.verify',
626
+ command: 'hadara state verify [--json]',
627
+ summary: 'Read the state consistency projection and concise drift issues.',
628
+ canonical: true,
629
+ appearsInDefaultHelp: false,
630
+ family: 'proof-diagnostics',
631
+ scope: 'project',
632
+ lifecycleStage: 'ready',
633
+ requiredness: 'diagnostic',
634
+ writeBoundary: 'read-only',
635
+ readOnly: true,
636
+ risk: 'low',
637
+ actor: 'agent-worker',
638
+ status: 'stable',
639
+ schemaVersion: 'hadara.stateProjection.v1',
640
+ docs: ['docs/COMMAND_SURFACE.md', 'docs/SCHEMAS.md'],
641
+ examples: [example('Verify state drift', 'hadara state verify --json', 'Before close or when shared-doc state looks inconsistent.')],
642
+ related: ['status', 'protocol.doctor', 'ci.gate'],
618
643
  conflictsWith: []
619
644
  },
620
645
  commandEntry({
@@ -677,7 +702,7 @@ exports.HADARA_COMMAND_REGISTRY = [
677
702
  schemaVersion: 'hadara.protocol.consistency.v1',
678
703
  docs: ['docs/IMPLEMENTATION_SOP.md'],
679
704
  examples: [example('Run protocol doctor', 'hadara protocol doctor --scope all --json', 'When project protocol files may be inconsistent.')],
680
- related: ['protocol.remediate', 'doctor'],
705
+ related: ['protocol.remediate', 'doctor', 'state.verify'],
681
706
  conflictsWith: []
682
707
  }),
683
708
  commandEntry({
@@ -1185,7 +1210,7 @@ exports.HADARA_COMMAND_REGISTRY = [
1185
1210
  schemaVersion: 'hadara.ops.status.v1',
1186
1211
  docs: ['docs/PROJECT_STATE.md'],
1187
1212
  examples: [example('Read status', 'hadara status --json', 'When checking project health and active task signals.')],
1188
- related: ['ops.status', 'doctor'],
1213
+ related: ['ops.status', 'doctor', 'state.verify'],
1189
1214
  conflictsWith: []
1190
1215
  }),
1191
1216
  commandEntry({
@@ -9,6 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const proof_status_1 = require("./proof-status");
10
10
  const evidence_lint_1 = require("./evidence-lint");
11
11
  const protocol_consistency_1 = require("./protocol-consistency");
12
+ const state_projection_1 = require("./state-projection");
12
13
  const task_capsule_1 = require("../task/task-capsule");
13
14
  function createCiGateReport(projectRoot, mode, options = {}) {
14
15
  const allowEmpty = options.allowEmpty ?? false;
@@ -29,6 +30,27 @@ function createCiGateReport(projectRoot, mode, options = {}) {
29
30
  const target = issue.severity === 'error' ? blockers : warnings;
30
31
  target.push({ severity: issue.severity, source: 'protocol', code: issue.code, message: issue.message, path: issue.path });
31
32
  }
33
+ const stateProjection = (0, state_projection_1.createStateProjectionReport)(projectRoot);
34
+ const stateConsistency = (0, state_projection_1.toStateProjectionAdvisory)(stateProjection, 10);
35
+ checks.push({
36
+ id: 'state:consistency',
37
+ source: 'state',
38
+ ok: true,
39
+ summary: stateConsistency.consistent
40
+ ? 'State consistency projection found no drift.'
41
+ : `State consistency projection is advisory: warnings ${stateConsistency.issueCounts.warning}, errors ${stateConsistency.issueCounts.error}.`
42
+ });
43
+ for (const issue of stateConsistency.issues) {
44
+ warnings.push({
45
+ severity: issue.severity,
46
+ source: 'state',
47
+ code: issue.code,
48
+ message: issue.message,
49
+ taskId: issue.taskId,
50
+ path: issue.path,
51
+ fixHint: issue.fixHint
52
+ });
53
+ }
32
54
  for (const task of tasks) {
33
55
  const evidence = (0, evidence_lint_1.createEvidenceLintReport)(projectRoot, task.id);
34
56
  checks.push({
@@ -63,6 +85,7 @@ function createCiGateReport(projectRoot, mode, options = {}) {
63
85
  ok: mode === 'advisory' ? true : blockers.length === 0,
64
86
  mode,
65
87
  scope: { ...(options.taskId ? { taskId: options.taskId } : {}), taskCount: tasks.length, allowEmpty },
88
+ stateConsistency,
66
89
  checks,
67
90
  blockers,
68
91
  warnings
@@ -14,6 +14,7 @@ exports.registryJson = registryJson;
14
14
  const node_fs_1 = __importDefault(require("node:fs"));
15
15
  const node_path_1 = __importDefault(require("node:path"));
16
16
  const managed_sections_1 = require("./managed-sections");
17
+ const markdown_table_1 = require("./markdown-table");
17
18
  exports.DOCS_REGISTRY_PATH = '.hadara/docs-registry.json';
18
19
  const VALID_STATUSES = ['canonical', 'active', 'reference', 'historical', 'superseded', 'archived'];
19
20
  const VALID_READ_WHEN = ['session-start', 'task-start', 'task-close', 'release-work', 'docs-work', 'debugging', 'integration-work', 'only-when-linked', 'never-default'];
@@ -294,7 +295,8 @@ function parseRequiredReading(projectRoot) {
294
295
  const found = new Set();
295
296
  for (const file of files) {
296
297
  const text = node_fs_1.default.existsSync(node_path_1.default.join(projectRoot, file)) ? node_fs_1.default.readFileSync(node_path_1.default.join(projectRoot, file), 'utf8') : '';
297
- for (const match of text.matchAll(/`([^`]+\.(?:md|MD))`/g)) {
298
+ const requiredReading = (0, markdown_table_1.readMarkdownSection)(text, '## Required Reading');
299
+ for (const match of requiredReading.matchAll(/`([^`]+\.(?:md|MD))`/g)) {
298
300
  const value = normalizePath(match[1]);
299
301
  if (value !== 'AGENTS.md' && !value.startsWith('docs/') && value !== '.hadara/context/HADARA_CONTEXT.md')
300
302
  continue;