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.
@@ -8,6 +8,7 @@ exports.parseEvidenceIndexFile = parseEvidenceIndexFile;
8
8
  const node_fs_1 = __importDefault(require("node:fs"));
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const redaction_1 = require("../core/redaction");
11
+ const normalizer_1 = require("../evidence/normalizer");
11
12
  const task_capsule_1 = require("../task/task-capsule");
12
13
  const DEFAULT_LIMIT = 50;
13
14
  function createEvidenceListReport(projectRoot, input) {
@@ -73,7 +74,7 @@ function parseEvidenceIndexFile(indexPath, taskId) {
73
74
  });
74
75
  return;
75
76
  }
76
- records.push(normalizeEvidenceIndexRecord(record));
77
+ records.push(normalizeEvidenceIndexRecord(record, index + 1));
77
78
  return;
78
79
  }
79
80
  issues.push({
@@ -132,9 +133,10 @@ function isEvidenceV2IndexRecord(value) {
132
133
  isEvidenceResult(record.legacy.result) &&
133
134
  (record.legacy.evidencePath === undefined || typeof record.legacy.evidencePath === 'string'));
134
135
  }
135
- function normalizeEvidenceIndexRecord(record) {
136
+ function normalizeEvidenceIndexRecord(record, sourceLine) {
136
137
  if (record.schemaVersion === 'hadara.evidence.v2')
137
- return normalizeEvidenceV2IndexRecord(record);
138
+ return normalizeEvidenceV2IndexRecord(record, sourceLine);
139
+ const normalizedRecord = (0, normalizer_1.normalizeEvidenceRecord)(record, { lineNumber: sourceLine });
138
140
  const normalized = {
139
141
  schemaVersion: 'hadara.evidence.v1',
140
142
  time: record.time,
@@ -147,13 +149,29 @@ function normalizeEvidenceIndexRecord(record) {
147
149
  if (record.visibility === 'public' && record.evidencePath) {
148
150
  normalized.evidencePath = record.evidencePath;
149
151
  }
150
- return normalized;
152
+ return {
153
+ ...normalized,
154
+ id: normalizedRecord.id,
155
+ sourceLine,
156
+ idSource: 'line-fallback',
157
+ idStability: 'unstable-on-reorder',
158
+ persistedSchemaVersion: 'hadara.evidence.v1',
159
+ category: normalizedRecord.category,
160
+ outcome: normalizedRecord.outcome,
161
+ tags: normalizedRecord.tags,
162
+ legacy: {
163
+ kind: normalized.kind,
164
+ result: normalized.result,
165
+ ...(normalized.evidencePath ? { evidencePath: normalized.evidencePath } : {})
166
+ }
167
+ };
151
168
  }
152
- function normalizeEvidenceV2IndexRecord(record) {
169
+ function normalizeEvidenceV2IndexRecord(record, sourceLine) {
153
170
  return {
154
171
  schemaVersion: 'hadara.evidence.v2',
155
172
  id: record.id,
156
- ...(record.sourceLine ? { sourceLine: record.sourceLine } : {}),
173
+ sourceLine: record.sourceLine ?? sourceLine,
174
+ persistedSchemaVersion: 'hadara.evidence.v2',
157
175
  fingerprint: record.fingerprint,
158
176
  idSource: 'persisted',
159
177
  idStability: 'durable',
@@ -335,7 +335,7 @@ function checkPackageMetadataReadiness(packageJson, licenseText, testStrategy, r
335
335
  packageJson?.private === true &&
336
336
  bin.hadara === './dist/cli/main.js';
337
337
  const publishablePackageMetadataOk = packageJson?.name === 'hadara' &&
338
- (/^0\.\d+\.0-rc\.\d+$/.test(String(packageJson?.version ?? '')) || /^0\.\d+\.0$/.test(String(packageJson?.version ?? ''))) &&
338
+ /^0\.\d+\.\d+(?:-rc\.\d+)?$/.test(String(packageJson?.version ?? '')) &&
339
339
  packageJson?.private === false &&
340
340
  packageJson?.license === 'MIT' &&
341
341
  bin.hadara === './dist/cli/main.js' &&
@@ -351,7 +351,7 @@ function checkPackageMetadataReadiness(packageJson, licenseText, testStrategy, r
351
351
  'Current binary remains `bin.hadara` at `./dist/cli/main.js`',
352
352
  'Current `files` whitelist is `dist/`, `README.md`, `LICENSE`, and `package.json`',
353
353
  'Bootstrap metadata mode: version `0.0.0-bootstrap`, `private: true`, no package publishability',
354
- 'Release-candidate metadata mode: version `0.x.0-rc.N`, `private: false`, `files` whitelist present, `LICENSE` present, package smoke evidence present',
354
+ 'Release-candidate metadata mode: version `0.x.y-rc.N`, `private: false`, `files` whitelist present, `LICENSE` present, package smoke evidence present',
355
355
  'Scoped fallback decision: do not silently switch names',
356
356
  'Version policy:',
357
357
  'T-0142 transitions `private` to false only after the package files whitelist, root README, license decision, and package-smoke evidence gates exist',
@@ -12,6 +12,7 @@ const handoff_summary_parser_1 = require("./handoff-summary-parser");
12
12
  const markdown_table_1 = require("./markdown-table");
13
13
  const operational_debt_1 = require("./operational-debt");
14
14
  const project_read_model_1 = require("./project-read-model");
15
+ const state_projection_1 = require("./state-projection");
15
16
  const task_capsule_1 = require("../task/task-capsule");
16
17
  const EMPTY_DEBT_AGGREGATE = {
17
18
  total: 0,
@@ -35,6 +36,9 @@ function createOpsStatusReport(projectRoot, options = {}) {
35
36
  const validation = (0, handoff_summary_parser_1.extractValidationBaselineSummary)(sources.handoff.content, sources.validationHistory.content);
36
37
  const activeRun = (0, active_run_state_1.safeCreateActiveRunProjection)(projectRoot);
37
38
  const debtAggregate = includeDebt ? (0, operational_debt_1.createOperationalDebtReport)(projectRoot).aggregate : EMPTY_DEBT_AGGREGATE;
39
+ const stateConsistency = options.includeStateConsistency
40
+ ? (0, state_projection_1.toStateProjectionAdvisory)((0, state_projection_1.createStateProjectionReport)(projectRoot), options.stateIssueLimit ?? 10)
41
+ : undefined;
38
42
  const issues = [...collectIssues(sources, validation), ...activeRun.issues];
39
43
  return {
40
44
  schemaVersion: 'hadara.ops.status.v1',
@@ -56,6 +60,7 @@ function createOpsStatusReport(projectRoot, options = {}) {
56
60
  validation,
57
61
  activeRun,
58
62
  debt: debtAggregate,
63
+ ...(stateConsistency ? { stateConsistency } : {}),
59
64
  mcp: {
60
65
  defaultMode: 'read-only',
61
66
  evidenceAttach: {
@@ -78,6 +83,12 @@ function formatOpsStatusReport(report) {
78
83
  `branch: ${report.project.branch}`,
79
84
  `tasks: ${counts}`,
80
85
  `debt: open ${report.debt.open}, highOpen ${report.debt.highOpen}`,
86
+ ...(report.stateConsistency
87
+ ? [
88
+ `stateConsistency: ${report.stateConsistency.consistent ? 'consistent' : 'drift'} ` +
89
+ `(errors ${report.stateConsistency.issueCounts.error}, warnings ${report.stateConsistency.issueCounts.warning}, info ${report.stateConsistency.issueCounts.info})`
90
+ ]
91
+ : []),
81
92
  `lastCompleted: ${report.tasks.lastCompleted.join(', ') || 'none'}`,
82
93
  `nextRecommended: ${report.tasks.nextRecommended ?? 'none'}`
83
94
  ].join('\n');
@@ -12,6 +12,7 @@ const node_path_1 = __importDefault(require("node:path"));
12
12
  const protocol_profile_1 = require("./protocol-profile");
13
13
  const evidence_lint_1 = require("./evidence-lint");
14
14
  const markdown_table_1 = require("./markdown-table");
15
+ const state_projection_1 = require("./state-projection");
15
16
  const task_capsule_1 = require("../task/task-capsule");
16
17
  const REQUIRED_TASK_FILES = Object.keys(task_capsule_1.TASK_FILES);
17
18
  const DONE_STATUSES = new Set(['done']);
@@ -126,6 +127,7 @@ function createAllProtocolConsistencyReport(projectRoot, now = new Date()) {
126
127
  }
127
128
  }
128
129
  const counts = countIssues(issues);
130
+ const stateConsistency = (0, state_projection_1.toStateProjectionAdvisory)((0, state_projection_1.createStateProjectionReport)(projectRoot, now), 10);
129
131
  return {
130
132
  schemaVersion: 'hadara.protocol.consistency.v1',
131
133
  command: 'protocol.doctor',
@@ -141,6 +143,7 @@ function createAllProtocolConsistencyReport(projectRoot, now = new Date()) {
141
143
  profile: profileReport.summary.profile,
142
144
  issueCounts: counts
143
145
  },
146
+ stateConsistency,
144
147
  issues,
145
148
  remediations
146
149
  };
@@ -217,7 +217,7 @@ function requiredDocsForProfile(profile) {
217
217
  return CORE_PROJECT_DOCS;
218
218
  }
219
219
  function requiredReadingDocsForProfile(profile) {
220
- return requiredDocsForProfile(profile).filter((relativePath) => relativePath !== 'AGENTS.md');
220
+ return requiredDocsForProfile(profile).filter((relativePath) => relativePath !== 'AGENTS.md' && relativePath !== 'docs/REFACTOR_LOG.md');
221
221
  }
222
222
  function readProfileMetadata(projectRoot) {
223
223
  return {
@@ -10,11 +10,12 @@ const schema_1 = require("../core/schema");
10
10
  const audit_1 = require("../core/audit");
11
11
  const release_dry_run_1 = require("./release-dry-run");
12
12
  const CONFIRMATION_PHRASE = 'publish-deploy';
13
+ const NPM_PUBLISHABLE_VERSION_PATTERN = /^0\.\d+\.\d+(?:-rc\.\d+)?$/;
13
14
  function createReleasePublishReport(options) {
14
15
  const mode = options.mode ?? 'dry-run';
15
16
  const env = options.env ?? process.env;
16
17
  const metadata = readPackageMetadata(options.projectRoot);
17
- const metadataPublishable = !metadata.private && (/^0\.\d+\.0-rc\.\d+$/.test(metadata.packageVersion) || /^0\.\d+\.0$/.test(metadata.packageVersion));
18
+ const metadataPublishable = !metadata.private && NPM_PUBLISHABLE_VERSION_PATTERN.test(metadata.packageVersion);
18
19
  const dryRun = (0, release_dry_run_1.createReleaseDryRunReport)(options.projectRoot);
19
20
  const approval = {
20
21
  required: true,
@@ -35,7 +36,7 @@ function createReleasePublishReport(options) {
35
36
  status: metadataPublishable ? 'passed' : 'error',
36
37
  summary: metadataPublishable
37
38
  ? 'Package metadata is in publishable version mode.'
38
- : 'Package metadata must be 0.x.0-rc.N or 0.x.0 with private false before publish/deploy readiness can pass.'
39
+ : 'Package metadata must be 0.x.y-rc.N or 0.x.y with private false before publish/deploy readiness can pass.'
39
40
  },
40
41
  {
41
42
  code: 'APPROVAL_RECORD',
@@ -0,0 +1,406 @@
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.createStateProjectionReport = createStateProjectionReport;
7
+ exports.toStateProjectionAdvisory = toStateProjectionAdvisory;
8
+ exports.formatStateProjectionReport = formatStateProjectionReport;
9
+ const node_crypto_1 = __importDefault(require("node:crypto"));
10
+ const node_fs_1 = __importDefault(require("node:fs"));
11
+ const node_path_1 = __importDefault(require("node:path"));
12
+ const evidence_1 = require("../evidence/evidence");
13
+ const task_close_1 = require("../task/task-close");
14
+ const task_capsule_1 = require("../task/task-capsule");
15
+ const evidence_list_1 = require("./evidence-list");
16
+ const markdown_table_1 = require("./markdown-table");
17
+ const TASK_STATUS_TOKENS = new Set(['Draft', 'In Progress', 'Blocked', 'Done', 'Partial', 'Superseded', 'Archived']);
18
+ const CLOSE_STATE_TOKENS = new Set(['not-closed', 'closed-valid', 'closed-stale', 'closed-invalid', 'unknown']);
19
+ function createStateProjectionReport(projectRoot, now = new Date()) {
20
+ const issues = [];
21
+ const sourceTexts = readSources(projectRoot, issues);
22
+ const taskBoardRows = parseTaskBoardRows(sourceTexts.taskBoard.content);
23
+ const tasks = (0, task_capsule_1.listTaskCapsules)(projectRoot);
24
+ const taskById = new Map(tasks.map((task) => [task.id, task]));
25
+ const taskIds = new Set([...tasks.map((task) => task.id), ...taskBoardRows.map((row) => row.id)]);
26
+ const basicTaskStates = [...taskIds].sort().map((taskId) => ({
27
+ id: taskId,
28
+ taskStatus: taskById.get(taskId) ? readTaskStatus(node_path_1.default.join(taskById.get(taskId).dir, 'TASK.md')) : null,
29
+ taskBoardStatus: taskBoardRows.find((row) => row.id === taskId)?.status ?? null
30
+ }));
31
+ const latestDoneTaskId = latestTaskId(basicTaskStates.filter((task) => isDone(task.taskStatus) || isDone(task.taskBoardStatus)).map((task) => task.id));
32
+ const activeTaskIds = basicTaskStates.filter((task) => isActive(task.taskBoardStatus)).map((task) => task.id);
33
+ const deepCheckTaskIds = new Set([latestDoneTaskId, ...activeTaskIds].filter((value) => Boolean(value)));
34
+ const projectedTasks = [...taskIds]
35
+ .sort()
36
+ .map((taskId) => buildTaskProjection(projectRoot, taskId, taskById.get(taskId), taskBoardRows.find((row) => row.id === taskId), deepCheckTaskIds.has(taskId), issues));
37
+ checkLatestCloseProof(projectedTasks, latestDoneTaskId, issues);
38
+ const projectState = extractProjectState(sourceTexts.projectState);
39
+ const agentHandoff = extractAgentHandoff(sourceTexts.agentHandoff);
40
+ const developmentSlices = extractDevelopmentSlices(sourceTexts.developmentSlices);
41
+ const docsRegistry = extractDocsRegistry(projectRoot, issues);
42
+ const releaseReadiness = readSource(projectRoot, 'docs/RELEASE_READINESS.md');
43
+ if (!releaseReadiness.exists) {
44
+ issues.push(warning('STATE_RELEASE_READINESS_MISSING', releaseReadiness.path, 'docs/RELEASE_READINESS.md is missing.', 'Create or register release readiness docs before release work depends on this projection.'));
45
+ }
46
+ compareLatestTask('STATE_PROJECT_STATE_LATEST_MISMATCH', 'docs/PROJECT_STATE.md', projectState.latestCompletedTaskId, latestDoneTaskId, issues);
47
+ compareLatestTask('STATE_HANDOFF_LATEST_MISMATCH', 'docs/AGENT_HANDOFF.md', agentHandoff.latestCompletedTaskId, latestDoneTaskId, issues);
48
+ compareLatestTask('STATE_DEVELOPMENT_SLICES_LATEST_MISMATCH', 'docs/DEVELOPMENT_SLICES.md', developmentSlices.latestDoneTaskId, latestDoneTaskId, issues);
49
+ compareActiveTasks(projectState.activeTaskId, agentHandoff.activeTaskId, activeTaskIds, issues);
50
+ const counts = countIssues(issues);
51
+ return {
52
+ schemaVersion: 'hadara.stateProjection.v1',
53
+ command: 'state.projection',
54
+ ok: true,
55
+ generatedAt: now.toISOString(),
56
+ projectRoot,
57
+ summary: {
58
+ consistent: counts.error === 0 && counts.warning === 0,
59
+ issueCounts: counts,
60
+ latestDoneTaskId,
61
+ activeTaskIds,
62
+ checkedTasks: projectedTasks.length
63
+ },
64
+ sources: {
65
+ projectState: {
66
+ path: sourceTexts.projectState.path,
67
+ exists: sourceTexts.projectState.exists,
68
+ latestCompletedTaskId: projectState.latestCompletedTaskId,
69
+ activeTaskId: projectState.activeTaskId
70
+ },
71
+ agentHandoff: {
72
+ path: sourceTexts.agentHandoff.path,
73
+ exists: sourceTexts.agentHandoff.exists,
74
+ latestCompletedTaskId: agentHandoff.latestCompletedTaskId,
75
+ activeTaskId: agentHandoff.activeTaskId
76
+ },
77
+ taskBoard: {
78
+ path: sourceTexts.taskBoard.path,
79
+ exists: sourceTexts.taskBoard.exists,
80
+ rows: taskBoardRows.length,
81
+ latestDoneTaskId: latestTaskId(taskBoardRows.filter((row) => isDone(row.status)).map((row) => row.id)),
82
+ activeTaskIds
83
+ },
84
+ developmentSlices: {
85
+ path: sourceTexts.developmentSlices.path,
86
+ exists: sourceTexts.developmentSlices.exists,
87
+ latestDoneTaskId: developmentSlices.latestDoneTaskId
88
+ },
89
+ docsRegistry,
90
+ releaseReadiness: {
91
+ path: releaseReadiness.path,
92
+ exists: releaseReadiness.exists
93
+ }
94
+ },
95
+ tasks: projectedTasks,
96
+ issues
97
+ };
98
+ }
99
+ function toStateProjectionAdvisory(report, issueLimit = 10) {
100
+ const limit = Math.max(0, issueLimit);
101
+ return {
102
+ mode: 'advisory',
103
+ strictBlocking: false,
104
+ consistent: report.summary.consistent,
105
+ issueCounts: report.summary.issueCounts,
106
+ latestDoneTaskId: report.summary.latestDoneTaskId,
107
+ activeTaskIds: report.summary.activeTaskIds,
108
+ checkedTasks: report.summary.checkedTasks,
109
+ issues: report.issues.slice(0, limit),
110
+ truncatedIssues: Math.max(0, report.issues.length - limit)
111
+ };
112
+ }
113
+ function formatStateProjectionReport(report, issueLimit = 10) {
114
+ const advisory = toStateProjectionAdvisory(report, issueLimit);
115
+ const counts = `errors ${advisory.issueCounts.error}, warnings ${advisory.issueCounts.warning}, info ${advisory.issueCounts.info}`;
116
+ const lines = [
117
+ '[HADARA] State verify',
118
+ `consistent: ${advisory.consistent}`,
119
+ `issues: ${counts}`,
120
+ `latestDoneTaskId: ${advisory.latestDoneTaskId ?? 'none'}`,
121
+ `activeTaskIds: ${advisory.activeTaskIds.join(', ') || 'none'}`,
122
+ `checkedTasks: ${advisory.checkedTasks}`,
123
+ 'rollout: advisory only; strict gates do not block on state projection drift yet.'
124
+ ];
125
+ for (const issue of advisory.issues) {
126
+ lines.push(`- ${issue.severity} ${issue.code}: ${issue.message}${issue.path ? ` (${issue.path})` : ''}`);
127
+ }
128
+ if (advisory.truncatedIssues > 0)
129
+ lines.push(`- ... ${advisory.truncatedIssues} more issue(s) omitted`);
130
+ return lines.join('\n');
131
+ }
132
+ function readSources(projectRoot, issues) {
133
+ const projectState = readSource(projectRoot, 'docs/PROJECT_STATE.md');
134
+ const agentHandoff = readSource(projectRoot, 'docs/AGENT_HANDOFF.md');
135
+ const taskBoard = readSource(projectRoot, 'docs/TASK_BOARD.md');
136
+ const developmentSlices = readSource(projectRoot, 'docs/DEVELOPMENT_SLICES.md');
137
+ for (const source of [projectState, agentHandoff, taskBoard, developmentSlices]) {
138
+ if (!source.exists)
139
+ issues.push(warning('STATE_SOURCE_MISSING', source.path, `${source.path} is missing.`, `Restore ${source.path} or run the relevant HADARA init/profile remediation.`));
140
+ }
141
+ return { projectState, agentHandoff, taskBoard, developmentSlices };
142
+ }
143
+ function readSource(projectRoot, relativePath) {
144
+ const absolutePath = node_path_1.default.join(projectRoot, relativePath);
145
+ const exists = node_fs_1.default.existsSync(absolutePath);
146
+ return {
147
+ path: relativePath,
148
+ exists,
149
+ content: exists ? node_fs_1.default.readFileSync(absolutePath, 'utf8') : ''
150
+ };
151
+ }
152
+ function parseTaskBoardRows(content) {
153
+ return (0, markdown_table_1.parseMarkdownRows)(content)
154
+ .filter((row) => /^T-\d{4}$/.test(row[0] ?? ''))
155
+ .map((row) => ({
156
+ id: row[0],
157
+ title: row[1] ?? '',
158
+ status: row[2] ?? '',
159
+ capsule: row[3] ?? ''
160
+ }));
161
+ }
162
+ function buildTaskProjection(projectRoot, taskId, task, taskBoard, deepCheck, issues) {
163
+ const taskPath = task ? toPortablePath(node_path_1.default.relative(projectRoot, node_path_1.default.join(task.dir, 'TASK.md'))) : '';
164
+ const handoffPath = task ? toPortablePath(node_path_1.default.relative(projectRoot, node_path_1.default.join(task.dir, 'HANDOFF.md'))) : '';
165
+ const planPath = task ? toPortablePath(node_path_1.default.relative(projectRoot, node_path_1.default.join(task.dir, 'PLAN.md'))) : '';
166
+ const taskStatus = task ? readTaskStatus(node_path_1.default.join(task.dir, 'TASK.md')) : null;
167
+ const taskHandoff = task && deepCheck ? readTaskHandoff(node_path_1.default.join(task.dir, 'HANDOFF.md')) : { exists: task ? node_fs_1.default.existsSync(node_path_1.default.join(task.dir, 'HANDOFF.md')) : false, taskStatus: null, closeState: null };
168
+ const plan = task && deepCheck ? readPlanState(node_path_1.default.join(task.dir, 'PLAN.md')) : { exists: task ? node_fs_1.default.existsSync(node_path_1.default.join(task.dir, 'PLAN.md')) : false, totalRows: 0, doneRows: 0, pendingRows: 0, inProgressRows: 0 };
169
+ const closeProof = task && deepCheck ? readCloseProof(projectRoot, task) : { path: task ? toPortablePath(node_path_1.default.relative(projectRoot, node_path_1.default.join(task.dir, 'evidence.jsonl'))) : '', state: 'unknown', sourceHash: null, currentSourceHash: null };
170
+ const capsule = task ? toPortablePath(node_path_1.default.relative(projectRoot, task.dir)) : taskBoard?.capsule ?? '';
171
+ if (!task && taskBoard) {
172
+ issues.push(warning('STATE_TASK_BOARD_CAPSULE_MISSING', 'docs/TASK_BOARD.md', `Task Board row ${taskId} points at ${taskBoard.capsule || '(empty)'}, but no matching capsule was found.`, `Create the missing capsule or update/remove the ${taskId} Task Board row.`, taskId));
173
+ }
174
+ if (task && !taskBoard) {
175
+ issues.push(warning('STATE_TASK_BOARD_ROW_MISSING', 'docs/TASK_BOARD.md', `Task Capsule ${taskId} exists but has no Task Board row.`, `Run task workflow remediation or add a Task Board row for ${taskId}.`, taskId));
176
+ }
177
+ if (task && taskBoard && taskBoard.capsule !== capsule) {
178
+ issues.push(warning('STATE_TASK_BOARD_CAPSULE_DRIFT', 'docs/TASK_BOARD.md', `Task Board capsule for ${taskId} is ${taskBoard.capsule || '(empty)'}, expected ${capsule}.`, `Update the Task Board capsule cell for ${taskId}.`, taskId, capsule, taskBoard.capsule || '(empty)'));
179
+ }
180
+ if (taskStatus && taskBoard?.status && taskStatus !== taskBoard.status) {
181
+ issues.push(warning('STATE_TASK_BOARD_STATUS_DRIFT', 'docs/TASK_BOARD.md', `Task Board status for ${taskId} is ${taskBoard.status}, but TASK.md status is ${taskStatus}.`, `Run hadara task finish --task ${taskId} --execute --json after the capsule is complete, or align the status source intentionally.`, taskId, taskStatus, taskBoard.status));
182
+ }
183
+ if (deepCheck && taskHandoff.taskStatus && !TASK_STATUS_TOKENS.has(taskHandoff.taskStatus)) {
184
+ issues.push(warning('STATE_TASK_HANDOFF_STATUS_INVALID', handoffPath, `Task handoff TaskStatus for ${taskId} is not a canonical task status token: ${taskHandoff.taskStatus}.`, 'Use a canonical TaskStatus token; close proof state belongs in audit-close/proof/status read models.', taskId));
185
+ }
186
+ if (deepCheck && taskHandoff.taskStatus && /pending lifecycle close|closed-valid|not-closed/i.test(taskHandoff.taskStatus)) {
187
+ issues.push(warning('STATE_TASK_HANDOFF_STATUS_CLOSE_STATE_MIXED', handoffPath, `Task handoff TaskStatus for ${taskId} appears to mix task status and close proof state.`, 'Use TaskStatus: Done only; derive CloseState from audit-close/proof/status read models.', taskId));
188
+ }
189
+ if (deepCheck && taskHandoff.closeState) {
190
+ issues.push(warning('STATE_TASK_HANDOFF_CLOSE_STATE_PERSISTED', handoffPath, `Task handoff persists derived CloseState for ${taskId}: ${taskHandoff.closeState}.`, 'Remove CloseState from task-local HANDOFF.md; use audit-close/proof/status read models for derived close state.', taskId));
191
+ if (!CLOSE_STATE_TOKENS.has(taskHandoff.closeState)) {
192
+ issues.push(warning('STATE_TASK_HANDOFF_CLOSE_STATE_INVALID', handoffPath, `Task handoff CloseState for ${taskId} is not canonical: ${taskHandoff.closeState}.`, 'Remove the CloseState row from task-local HANDOFF.md.', taskId));
193
+ }
194
+ }
195
+ if (deepCheck && (isDone(taskStatus) || isDone(taskBoard?.status)) && (plan.pendingRows > 0 || plan.inProgressRows > 0)) {
196
+ issues.push(warning('STATE_TASK_PLAN_DRIFT', planPath, `Done task ${taskId} has PLAN rows still Pending or In Progress.`, 'Update PLAN.md rows to Done or record an explicit residual-risk decision before closing.', taskId));
197
+ }
198
+ return {
199
+ id: taskId,
200
+ title: task?.title ?? taskBoard?.title ?? 'Unknown',
201
+ capsule,
202
+ task: {
203
+ path: taskPath,
204
+ exists: Boolean(task),
205
+ status: taskStatus
206
+ },
207
+ taskBoard: {
208
+ path: 'docs/TASK_BOARD.md',
209
+ present: Boolean(taskBoard),
210
+ status: taskBoard?.status ?? null,
211
+ capsule: taskBoard?.capsule ?? null
212
+ },
213
+ handoff: {
214
+ path: handoffPath,
215
+ exists: taskHandoff.exists,
216
+ taskStatus: taskHandoff.taskStatus,
217
+ closeState: taskHandoff.closeState
218
+ },
219
+ plan: {
220
+ ...plan,
221
+ path: planPath
222
+ },
223
+ closeProof
224
+ };
225
+ }
226
+ function readTaskStatus(taskPath) {
227
+ if (!node_fs_1.default.existsSync(taskPath))
228
+ return null;
229
+ const section = (0, markdown_table_1.readMarkdownSection)(node_fs_1.default.readFileSync(taskPath, 'utf8'), '## Status');
230
+ return section.trim().split(/\r?\n/)[0]?.trim() || null;
231
+ }
232
+ function readTaskHandoff(handoffPath) {
233
+ if (!node_fs_1.default.existsSync(handoffPath))
234
+ return { exists: false, taskStatus: null, closeState: null };
235
+ const rows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(node_fs_1.default.readFileSync(handoffPath, 'utf8'), '## Current State');
236
+ return {
237
+ exists: true,
238
+ taskStatus: (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'TaskStatus')?.[1] ?? (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Status')?.[1] ?? null,
239
+ closeState: (0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'CloseState')?.[1] ?? null
240
+ };
241
+ }
242
+ function readPlanState(planPath) {
243
+ if (!node_fs_1.default.existsSync(planPath)) {
244
+ return { path: '', exists: false, totalRows: 0, doneRows: 0, pendingRows: 0, inProgressRows: 0 };
245
+ }
246
+ const rows = (0, markdown_table_1.parseMarkdownRows)(node_fs_1.default.readFileSync(planPath, 'utf8')).filter((row) => /^\d+$/.test(row[0] ?? ''));
247
+ return {
248
+ path: toPortablePath(planPath),
249
+ exists: true,
250
+ totalRows: rows.length,
251
+ doneRows: rows.filter((row) => normalizeStatus(row[2]) === 'done').length,
252
+ pendingRows: rows.filter((row) => normalizeStatus(row[2]) === 'pending').length,
253
+ inProgressRows: rows.filter((row) => normalizeStatus(row[2]) === 'inprogress').length
254
+ };
255
+ }
256
+ function readCloseProof(projectRoot, task) {
257
+ const evidencePath = node_path_1.default.join(task.dir, 'evidence.jsonl');
258
+ const relativeEvidencePath = toPortablePath(node_path_1.default.relative(projectRoot, evidencePath));
259
+ const records = (0, evidence_list_1.parseEvidenceIndexFile)(evidencePath, task.id).records.filter((record) => (0, evidence_1.persistedEvidenceKind)(record) === 'command-log' && /Task close validation .* before close evidence append/.test(record.summary));
260
+ const latest = records.at(-1);
261
+ const currentSourceHash = hashCloseRelevantSource(projectRoot, task.dir);
262
+ if (!latest)
263
+ return { path: relativeEvidencePath, state: 'not-closed', sourceHash: null, currentSourceHash };
264
+ const sourceHash = extractSourceHash(latest.summary);
265
+ const passed = (0, evidence_1.persistedEvidenceResult)(latest) === 'passed';
266
+ const state = !passed ? 'closed-invalid' : sourceHash && sourceHash !== currentSourceHash ? 'closed-stale' : 'closed-valid';
267
+ return { path: relativeEvidencePath, state, sourceHash, currentSourceHash };
268
+ }
269
+ function extractProjectState(source) {
270
+ const rows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(source.content, '## Metadata');
271
+ return {
272
+ latestCompletedTaskId: extractTaskId((0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Latest Completed Task')?.[1] ?? ''),
273
+ activeTaskId: extractTaskId((0, markdown_table_1.findMarkdownRowByCell)(rows, 0, 'Active Task')?.[1] ?? '')
274
+ };
275
+ }
276
+ function extractAgentHandoff(source) {
277
+ const currentRows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(source.content, '## Current State');
278
+ const latest = (0, markdown_table_1.findMarkdownRowByCell)(currentRows, 0, 'Latest Completed Task')?.[1] ?? '';
279
+ const active = (0, markdown_table_1.findMarkdownRowByCell)(currentRows, 0, 'Active / Next Task')?.[1] ?? '';
280
+ const lastThreeRows = (0, markdown_table_1.parseMarkdownRowsUnderHeading)(source.content, '## Last 3 Completed Tasks');
281
+ return {
282
+ latestCompletedTaskId: extractTaskId(latest) ?? extractTaskId(lastThreeRows.find((row) => /^T-\d{4}/.test(row[0] ?? ''))?.[0] ?? ''),
283
+ activeTaskId: extractTaskId(active)
284
+ };
285
+ }
286
+ function extractDevelopmentSlices(source) {
287
+ const rows = (0, markdown_table_1.parseMarkdownRows)(source.content);
288
+ const doneIds = rows
289
+ .filter((row) => /^T-\d{4}$/.test(row[2] ?? '') && /^Done\b/.test(row[4] ?? ''))
290
+ .map((row) => row[2]);
291
+ return { latestDoneTaskId: latestTaskId(doneIds) };
292
+ }
293
+ function extractDocsRegistry(projectRoot, issues) {
294
+ const source = readSource(projectRoot, '.hadara/docs-registry.json');
295
+ if (!source.exists) {
296
+ issues.push(warning('STATE_DOCS_REGISTRY_MISSING', source.path, '.hadara/docs-registry.json is missing.', 'Run docs registry generation or protocol migration before relying on docs state projection.'));
297
+ return { path: source.path, exists: false, registeredDocuments: null, statusCounts: {} };
298
+ }
299
+ try {
300
+ const parsed = JSON.parse(source.content);
301
+ const documents = Array.isArray(parsed.documents) ? parsed.documents : [];
302
+ return {
303
+ path: source.path,
304
+ exists: true,
305
+ registeredDocuments: documents.length,
306
+ statusCounts: documents.reduce((acc, doc) => {
307
+ const status = doc.status ?? 'unknown';
308
+ acc[status] = (acc[status] ?? 0) + 1;
309
+ return acc;
310
+ }, {})
311
+ };
312
+ }
313
+ catch (error) {
314
+ issues.push(warning('STATE_DOCS_REGISTRY_INVALID_JSON', source.path, `.hadara/docs-registry.json could not be parsed: ${error instanceof Error ? error.message : String(error)}`, 'Repair the docs registry JSON before relying on document state projection.'));
315
+ return { path: source.path, exists: true, registeredDocuments: null, statusCounts: {} };
316
+ }
317
+ }
318
+ function compareLatestTask(code, sourcePath, actual, expected, issues) {
319
+ if (!actual || !expected || actual === expected)
320
+ return;
321
+ issues.push(warning(code, sourcePath, `${sourcePath} points to latest completed task ${actual}, but the projected latest Done task is ${expected}.`, `Update ${sourcePath} latest completed task state to ${expected} or correct the Done task source.`, undefined, expected, actual));
322
+ }
323
+ function compareActiveTasks(projectActive, handoffActive, activeTaskIds, issues) {
324
+ const expected = activeTaskIds[0] ?? null;
325
+ if (activeTaskIds.length > 1) {
326
+ issues.push(warning('STATE_MULTIPLE_ACTIVE_TASKS', 'docs/TASK_BOARD.md', `Task Board has multiple active tasks: ${activeTaskIds.join(', ')}.`, 'Keep only one In Progress task unless a future coordinator explicitly supports parallel active work.'));
327
+ }
328
+ if (expected && projectActive && projectActive !== expected) {
329
+ issues.push(warning('STATE_PROJECT_ACTIVE_TASK_MISMATCH', 'docs/PROJECT_STATE.md', `Project State active task is ${projectActive}, but Task Board active task is ${expected}.`, `Update Project State Active Task to ${expected} or finish/reclassify the Task Board row.`, undefined, expected, projectActive));
330
+ }
331
+ if (expected && handoffActive && handoffActive !== expected) {
332
+ issues.push(warning('STATE_HANDOFF_ACTIVE_TASK_MISMATCH', 'docs/AGENT_HANDOFF.md', `Agent Handoff active task is ${handoffActive}, but Task Board active task is ${expected}.`, `Update Agent Handoff Active / Next Task to ${expected} or finish/reclassify the Task Board row.`, undefined, expected, handoffActive));
333
+ }
334
+ if (!expected && projectActive) {
335
+ issues.push(warning('STATE_PROJECT_ACTIVE_TASK_STALE', 'docs/PROJECT_STATE.md', `Project State names active task ${projectActive}, but Task Board has no In Progress task.`, 'Set Active Task to None or mark the active Task Board row In Progress.', undefined, 'None', projectActive));
336
+ }
337
+ }
338
+ function checkLatestCloseProof(projectedTasks, latestDoneTaskId, issues) {
339
+ if (!latestDoneTaskId)
340
+ return;
341
+ const task = projectedTasks.find((candidate) => candidate.id === latestDoneTaskId);
342
+ if (!task)
343
+ return;
344
+ if (task.closeProof.state === 'closed-valid')
345
+ return;
346
+ if (task.closeProof.state === 'not-closed') {
347
+ issues.push(warning('STATE_LATEST_CLOSE_PROOF_MISSING', task.closeProof.path, `Latest Done task ${latestDoneTaskId} has no close proof.`, `Run hadara task ready --task ${latestDoneTaskId} --level done --json, then task close/audit-close.`, latestDoneTaskId));
348
+ return;
349
+ }
350
+ if (task.closeProof.state === 'closed-stale') {
351
+ issues.push(warning('STATE_LATEST_CLOSE_PROOF_STALE', task.closeProof.path, `Latest Done task ${latestDoneTaskId} has stale close proof.`, `Rerun hadara task ready --task ${latestDoneTaskId} --level done --json, then task close/audit-close after intentional close-source edits.`, latestDoneTaskId, task.closeProof.currentSourceHash ?? undefined, task.closeProof.sourceHash ?? undefined));
352
+ return;
353
+ }
354
+ issues.push(warning('STATE_LATEST_CLOSE_PROOF_INVALID', task.closeProof.path, `Latest Done task ${latestDoneTaskId} close proof is ${task.closeProof.state}.`, `Rerun task close/audit-close for ${latestDoneTaskId} after resolving validation blockers.`, latestDoneTaskId));
355
+ }
356
+ function hashCloseRelevantSource(projectRoot, taskDir) {
357
+ const payload = (0, task_close_1.closeRelevantSourceRelativePaths)(projectRoot, taskDir).map((relativePath) => {
358
+ const absolutePath = node_path_1.default.join(projectRoot, relativePath);
359
+ return {
360
+ path: relativePath,
361
+ exists: node_fs_1.default.existsSync(absolutePath),
362
+ sha256: node_fs_1.default.existsSync(absolutePath) ? node_crypto_1.default.createHash('sha256').update(node_fs_1.default.readFileSync(absolutePath)).digest('hex') : null
363
+ };
364
+ });
365
+ return `sha256:${node_crypto_1.default.createHash('sha256').update(JSON.stringify(payload), 'utf8').digest('hex')}`;
366
+ }
367
+ function extractSourceHash(summary) {
368
+ return summary.match(/sourceHash\s+(sha256:[a-f0-9]+)/)?.[1] ?? null;
369
+ }
370
+ function extractTaskId(value) {
371
+ return value.match(/\bT-\d{4}\b/)?.[0] ?? null;
372
+ }
373
+ function latestTaskId(ids) {
374
+ return ids.sort().at(-1) ?? null;
375
+ }
376
+ function normalizeStatus(value) {
377
+ return (value ?? '').trim().toLowerCase().replace(/[\s_-]+/g, '');
378
+ }
379
+ function isDone(value) {
380
+ return normalizeStatus(value) === 'done';
381
+ }
382
+ function isActive(value) {
383
+ return normalizeStatus(value) === 'inprogress';
384
+ }
385
+ function countIssues(issues) {
386
+ return {
387
+ error: issues.filter((issue) => issue.severity === 'error').length,
388
+ warning: issues.filter((issue) => issue.severity === 'warning').length,
389
+ info: issues.filter((issue) => issue.severity === 'info').length
390
+ };
391
+ }
392
+ function warning(code, pathValue, message, fixHint, taskId, expected, actual) {
393
+ return {
394
+ severity: 'warning',
395
+ code,
396
+ path: pathValue,
397
+ ...(taskId ? { taskId } : {}),
398
+ message,
399
+ ...(expected ? { expected } : {}),
400
+ ...(actual ? { actual } : {}),
401
+ fixHint
402
+ };
403
+ }
404
+ function toPortablePath(value) {
405
+ return value.split(node_path_1.default.sep).join('/');
406
+ }
@@ -26,7 +26,7 @@ exports.TASK_FILES = {
26
26
  'DECISIONS.md': () => `# Decisions\n\n| ID | Decision | Status | Rationale | Evidence |\n|---|---|---|---|---|\n`,
27
27
  'EVIDENCE.md': () => `# Evidence\n\n| Time | Kind | Summary | Result | Visibility | JSONL |\n|---|---|---|---|---|---|\n`,
28
28
  'evidence.jsonl': () => '',
29
- 'HANDOFF.md': (task) => `# Handoff\n\n## Current State\n\n${(0, managed_sections_1.managedSectionBlock)('task-handoff-current-state', { schema: 'hadara.managedSection.v1', owner: 'handoff.update', kind: 'key-value-table', mode: 'update-row', version: 1, required: true, closeSourceRole: 'included' }, `| Field | Value |\n|---|---|\n| Task | ${task.id} |\n| Status | Draft |\n| Last Updated | TBD |\n`)}\n\n## Last Completed\n\n| Item | Evidence |\n|---|---|\n| TBD | TBD |\n\n## Next Recommended Step\n\n| Step | Reason | Required Reading |\n|---|---|---|\n| TBD | TBD | TBD |\n\n## Carry Forward Warnings\n\n| Warning | Impact | Mitigation |\n|---|---|---|\n`
29
+ 'HANDOFF.md': (task) => `# Handoff\n\n## Current State\n\n${(0, managed_sections_1.managedSectionBlock)('task-handoff-current-state', { schema: 'hadara.managedSection.v1', owner: 'handoff.update', kind: 'key-value-table', mode: 'update-row', version: 1, required: true, closeSourceRole: 'included' }, `| Field | Value |\n|---|---|\n| Task | ${task.id} |\n| TaskStatus | Draft |\n| Last Updated | TBD |\n`)}\n\n## Last Completed\n\n| Item | Evidence |\n|---|---|\n| TBD | TBD |\n\n## Next Recommended Step\n\n| Step | Reason | Required Reading |\n|---|---|---|\n| TBD | TBD | TBD |\n\n## Carry Forward Warnings\n\n| Warning | Impact | Mitigation |\n|---|---|---|\n`
30
30
  };
31
31
  function isTaskCapsuleScaffoldContent(task, fileName, content) {
32
32
  if (fileName === 'TASK.md') {
@@ -146,7 +146,11 @@ function listTaskCapsules(projectRoot) {
146
146
  return [];
147
147
  return node_fs_1.default
148
148
  .readdirSync(tasksDir, { withFileTypes: true })
149
- .filter((entry) => entry.isDirectory() && /^T-\d{4}-/.test(entry.name))
149
+ .filter((entry) => {
150
+ if (!entry.isDirectory() || !/^T-\d{4}-/.test(entry.name))
151
+ return false;
152
+ return node_fs_1.default.existsSync(node_path_1.default.join(tasksDir, entry.name, 'TASK.md'));
153
+ })
150
154
  .map((entry) => {
151
155
  const [id, ...slugParts] = entry.name.split('-');
152
156
  const number = slugParts.shift();
@@ -169,7 +173,9 @@ function findTaskCapsule(projectRoot, taskId) {
169
173
  return undefined;
170
174
  const entry = node_fs_1.default
171
175
  .readdirSync(tasksDir, { withFileTypes: true })
172
- .find((candidate) => candidate.isDirectory() && candidate.name.startsWith(`${taskId}-`));
176
+ .find((candidate) => candidate.isDirectory() &&
177
+ candidate.name.startsWith(`${taskId}-`) &&
178
+ node_fs_1.default.existsSync(node_path_1.default.join(tasksDir, candidate.name, 'TASK.md')));
173
179
  if (!entry)
174
180
  return undefined;
175
181
  const slug = entry.name.slice(`${taskId}-`.length);
@@ -349,7 +349,7 @@ function createSelectedTaskReadModel(projectRoot, summary, options = {}) {
349
349
  }
350
350
  function createFastSelectedTaskReadModel(projectRoot, summary, options) {
351
351
  const detail = createTaskDocumentReadReportFromSummary(projectRoot, summary, { includePrivate: false });
352
- const evidenceIndex = detail.evidenceIndex ?? [];
352
+ const evidenceIndex = (detail.evidenceIndex ?? []);
353
353
  const evidence = limitEvidenceListReport({
354
354
  schemaVersion: 'hadara.evidence.list.v1',
355
355
  command: 'evidence.list',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hadara",
3
- "version": "0.3.0",
3
+ "version": "0.3.2-rc.0",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Portable AI-assisted development workbench for evidence-backed task capsules, handoffs, and release gates.",