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.
- package/README.md +47 -28
- package/dist/cli/evidence-json.js +8 -1
- package/dist/cli/evidence.js +81 -3
- package/dist/cli/init.js +86 -15
- package/dist/cli/main.js +6 -0
- package/dist/cli/state.js +16 -0
- package/dist/cli/status.js +1 -1
- package/dist/core/schema.js +2 -0
- package/dist/evidence/evidence.js +62 -7
- package/dist/evidence/semantics.js +12 -3
- package/dist/harness/validate.js +110 -0
- package/dist/schemas/evidence-list.schema.json +22 -3
- package/dist/schemas/schema-index.json +7 -0
- package/dist/schemas/state-projection.schema.json +115 -0
- package/dist/services/capability-registry.js +35 -10
- package/dist/services/ci-gate.js +23 -0
- package/dist/services/docs-registry.js +3 -1
- package/dist/services/evidence-list.js +24 -6
- package/dist/services/operational-debt.js +2 -2
- package/dist/services/operations-status-service.js +11 -0
- package/dist/services/protocol-consistency.js +3 -0
- package/dist/services/protocol-profile.js +1 -1
- package/dist/services/release-publish.js +3 -2
- package/dist/services/state-projection.js +406 -0
- package/dist/task/task-capsule.js +9 -3
- package/dist/tui/read-model.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
|
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
|
|
379
|
-
|
|
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: `
|
|
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)
|
package/dist/harness/validate.js
CHANGED
|
@@ -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: [
|
|
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
|
|
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
|
|
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({
|
package/dist/services/ci-gate.js
CHANGED
|
@@ -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
|
-
|
|
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;
|