hadara 0.3.0-rc.1 → 0.3.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.
@@ -63,7 +63,13 @@ function validateTaskCapsule(projectRoot, taskId, options = {}) {
63
63
  severity: 'error',
64
64
  code: 'MISSING_TASK_FILE',
65
65
  message: `Required Task Capsule file is missing: ${fileName}`,
66
- path: relativePath
66
+ path: relativePath,
67
+ fixHint: `Create ${fileName} using the Task Capsule scaffold for this task.`,
68
+ remediationHint: {
69
+ path: relativePath,
70
+ requiredChange: `Create the missing required Task Capsule file ${fileName}.`,
71
+ blocking: true
72
+ }
67
73
  });
68
74
  }
69
75
  }
@@ -103,7 +109,15 @@ function validateTaskMarkdown(projectRoot, task, issues) {
103
109
  severity: 'error',
104
110
  code: 'TASK_SECTION_MISSING',
105
111
  message: `TASK.md is missing required section: ${heading}`,
106
- path: relativePath
112
+ path: relativePath,
113
+ heading,
114
+ fixHint: `Add the ${heading} section to TASK.md and fill it with task-specific content.`,
115
+ remediationHint: {
116
+ path: relativePath,
117
+ heading,
118
+ requiredChange: `Add the missing ${heading} section to TASK.md.`,
119
+ blocking: true
120
+ }
107
121
  });
108
122
  }
109
123
  }
@@ -142,7 +156,15 @@ function validateMarkdownFile(projectRoot, task, issues, fileName, checks) {
142
156
  severity: 'error',
143
157
  code: check.code,
144
158
  message: `${fileName} is missing standard Task Capsule format marker: ${check.anyText.join(' or ')}`,
145
- path: relativePath
159
+ path: relativePath,
160
+ fixHint: `Restore the standard ${fileName} Task Capsule frame: ${check.anyText.join(' or ')}.`,
161
+ example: check.anyText[0],
162
+ remediationHint: {
163
+ path: relativePath,
164
+ requiredChange: `Restore the standard ${fileName} format marker.`,
165
+ example: check.anyText[0],
166
+ blocking: true
167
+ }
146
168
  });
147
169
  }
148
170
  }
@@ -159,7 +181,17 @@ function validateEvidenceMarkdown(projectRoot, task, issues) {
159
181
  severity: 'error',
160
182
  code: 'EVIDENCE_TABLE_INVALID',
161
183
  message: 'EVIDENCE.md must contain the standard evidence table header.',
162
- path: relativePath
184
+ path: relativePath,
185
+ heading: 'Evidence',
186
+ fixHint: 'Restore the standard EVIDENCE.md table header.',
187
+ example: '| Time | Kind | Summary | Result | Visibility | JSONL |',
188
+ remediationHint: {
189
+ path: relativePath,
190
+ heading: 'Evidence',
191
+ requiredChange: 'Restore the standard EVIDENCE.md table header.',
192
+ example: '| Time | Kind | Summary | Result | Visibility | JSONL |',
193
+ blocking: true
194
+ }
163
195
  });
164
196
  }
165
197
  }
@@ -327,7 +359,17 @@ function validateTaskMetadataComplete(projectRoot, task, issues) {
327
359
  severity: 'error',
328
360
  code: 'TASK_METADATA_PLACEHOLDER',
329
361
  message: `Done-level validation requires TASK.md metadata field(s) to be concrete dates, not TBD: ${missing.join(', ')}.`,
330
- path: relativePath
362
+ path: relativePath,
363
+ heading: 'Metadata',
364
+ fixHint: 'Replace TASK.md Created and Updated metadata placeholders with YYYY-MM-DD dates.',
365
+ example: '| Created | 2026-06-12 |',
366
+ remediationHint: {
367
+ path: relativePath,
368
+ heading: 'Metadata',
369
+ requiredChange: 'Replace Created and Updated metadata placeholders with concrete YYYY-MM-DD dates.',
370
+ example: '| Created | 2026-06-12 |',
371
+ blocking: true
372
+ }
331
373
  });
332
374
  return;
333
375
  }
@@ -336,7 +378,17 @@ function validateTaskMetadataComplete(projectRoot, task, issues) {
336
378
  severity: 'error',
337
379
  code: 'TASK_METADATA_DATE_INVALID',
338
380
  message: 'Done-level validation requires TASK.md Created and Updated metadata to use YYYY-MM-DD dates.',
339
- path: relativePath
381
+ path: relativePath,
382
+ heading: 'Metadata',
383
+ fixHint: 'Use YYYY-MM-DD values for TASK.md Created and Updated metadata.',
384
+ example: '| Updated | 2026-06-12 |',
385
+ remediationHint: {
386
+ path: relativePath,
387
+ heading: 'Metadata',
388
+ requiredChange: 'Use YYYY-MM-DD values for Created and Updated metadata.',
389
+ example: '| Updated | 2026-06-12 |',
390
+ blocking: true
391
+ }
340
392
  });
341
393
  }
342
394
  }
@@ -351,7 +403,17 @@ function validateTaskStatusDone(projectRoot, task, issues) {
351
403
  severity: 'error',
352
404
  code: 'TASK_STATUS_NOT_DONE',
353
405
  message: 'Done-level validation requires TASK.md status to be Done.',
354
- path: relativePath
406
+ path: relativePath,
407
+ heading: 'Status',
408
+ fixHint: 'Run `hadara task finish --task <task-id> --execute --json` or set the TASK.md status section to Done after the capsule is actually complete.',
409
+ example: '## Status\n\nDone',
410
+ remediationHint: {
411
+ path: relativePath,
412
+ heading: 'Status',
413
+ requiredChange: 'Set TASK.md status to Done after the capsule is complete.',
414
+ example: '## Status\n\nDone',
415
+ blocking: true
416
+ }
355
417
  });
356
418
  }
357
419
  }
@@ -366,7 +428,17 @@ function validateTaskStatusHistoryDone(projectRoot, task, issues) {
366
428
  severity: 'error',
367
429
  code: 'TASK_STATUS_HISTORY_NOT_DONE',
368
430
  message: 'Done-level validation requires TASK.md Status History to end with Done.',
369
- path: relativePath
431
+ path: relativePath,
432
+ heading: 'Status History',
433
+ fixHint: 'Run `hadara task finish --task <task-id> --execute --json` so Status History ends with a Done row.',
434
+ example: '| 2026-06-12 | Done | Finished task capsule. | `hadara task finish --execute` |',
435
+ remediationHint: {
436
+ path: relativePath,
437
+ heading: 'Status History',
438
+ requiredChange: 'Append or repair the latest Status History row so it records Done.',
439
+ example: '| 2026-06-12 | Done | Finished task capsule. | `hadara task finish --execute` |',
440
+ blocking: true
441
+ }
370
442
  });
371
443
  }
372
444
  }
@@ -391,7 +463,17 @@ function validateAcceptanceDone(projectRoot, task, issues) {
391
463
  severity: 'error',
392
464
  code: 'ACCEPTANCE_INCOMPLETE',
393
465
  message: 'Done-level validation requires all acceptance criteria to be complete.',
394
- path: relativePath
466
+ path: relativePath,
467
+ heading: 'Acceptance Criteria',
468
+ fixHint: 'Mark each acceptance criterion complete with concrete evidence, or replace placeholder checklist rows with completed task-specific criteria.',
469
+ example: '| AC-1 | Scope is implemented. | Done | evidence id or summary |',
470
+ remediationHint: {
471
+ path: relativePath,
472
+ heading: 'Acceptance Criteria',
473
+ requiredChange: 'Complete every acceptance criterion with evidence before closing.',
474
+ example: '| AC-1 | Scope is implemented. | Done | evidence id or summary |',
475
+ blocking: true
476
+ }
395
477
  });
396
478
  }
397
479
  }
@@ -453,7 +535,17 @@ function validateDoneLevelScaffoldContent(projectRoot, task, issues) {
453
535
  severity: 'error',
454
536
  code: check.code,
455
537
  message: check.message,
456
- path: toPortablePath(node_path_1.default.relative(projectRoot, filePath))
538
+ path: toPortablePath(node_path_1.default.relative(projectRoot, filePath)),
539
+ heading: scaffoldHeadingForFile(check.fileName),
540
+ fixHint: scaffoldFixHint(check.fileName),
541
+ example: scaffoldExample(check.fileName),
542
+ remediationHint: {
543
+ path: toPortablePath(node_path_1.default.relative(projectRoot, filePath)),
544
+ heading: scaffoldHeadingForFile(check.fileName),
545
+ requiredChange: scaffoldRequiredChange(check.fileName),
546
+ example: scaffoldExample(check.fileName),
547
+ blocking: true
548
+ }
457
549
  });
458
550
  }
459
551
  }
@@ -468,7 +560,15 @@ function validateEvidenceIndexHasRecords(projectRoot, task, issues) {
468
560
  severity: 'error',
469
561
  code: 'EVIDENCE_REQUIRED',
470
562
  message: 'Done-level validation requires at least one evidence.jsonl record.',
471
- path: relativePath
563
+ path: relativePath,
564
+ fixHint: 'Record validation evidence with `hadara evidence add-command --task <task-id> --summary "..." --result passed --json`.',
565
+ example: 'hadara evidence add-command --task T-0001 --summary "Focused tests passed." --result passed --json',
566
+ remediationHint: {
567
+ path: relativePath,
568
+ requiredChange: 'Append at least one substantive evidence record through the canonical evidence writer.',
569
+ example: 'hadara evidence add-command --task T-0001 --summary "Focused tests passed." --result passed --json',
570
+ blocking: true
571
+ }
472
572
  });
473
573
  }
474
574
  }
@@ -481,7 +581,15 @@ function validateEvidenceSemanticGates(projectRoot, task, issues) {
481
581
  severity: issue.severity === 'warning' ? 'warning' : 'error',
482
582
  code: issue.code,
483
583
  message: issue.message,
484
- path: issue.path
584
+ path: issue.path,
585
+ fixHint: issue.severity === 'warning' ? 'Review evidence quality and add public validation evidence if needed.' : 'Add substantive passed validation evidence before closing.',
586
+ remediationHint: issue.path
587
+ ? {
588
+ path: issue.path,
589
+ requiredChange: issue.severity === 'warning' ? 'Review evidence quality for the task.' : 'Add substantive passed validation evidence for the task.',
590
+ blocking: issue.severity !== 'warning'
591
+ }
592
+ : undefined
485
593
  });
486
594
  }
487
595
  }
@@ -497,7 +605,17 @@ function validateEvidenceMarkdownSingleTable(projectRoot, task, issues) {
497
605
  severity: 'error',
498
606
  code: 'EVIDENCE_TABLE_DUPLICATE_HEADER',
499
607
  message: `Done-level validation requires EVIDENCE.md to contain exactly one evidence table header; found ${tableHeaderCount}.`,
500
- path: relativePath
608
+ path: relativePath,
609
+ heading: 'Evidence',
610
+ fixHint: 'Remove duplicate EVIDENCE.md table headers and keep one canonical evidence table.',
611
+ example: '| Time | Kind | Summary | Result | Visibility | JSONL |',
612
+ remediationHint: {
613
+ path: relativePath,
614
+ heading: 'Evidence',
615
+ requiredChange: 'Remove duplicate EVIDENCE.md table headers and keep one canonical table.',
616
+ example: '| Time | Kind | Summary | Result | Visibility | JSONL |',
617
+ blocking: true
618
+ }
501
619
  });
502
620
  }
503
621
  }
@@ -513,7 +631,17 @@ function validateHandoffDone(projectRoot, task, issues) {
513
631
  severity: 'error',
514
632
  code: 'HANDOFF_PLACEHOLDER',
515
633
  message: 'Done-level validation requires non-placeholder handoff sections.',
516
- path: relativePath
634
+ path: relativePath,
635
+ heading: 'Last Completed / Next Recommended Step',
636
+ fixHint: 'Replace HANDOFF.md placeholder rows with concrete last-completed work and next-step guidance.',
637
+ example: '| Continue with T-0002. | T-0001 is closed-valid. | docs/TASK_BOARD.md |',
638
+ remediationHint: {
639
+ path: relativePath,
640
+ heading: 'Last Completed / Next Recommended Step',
641
+ requiredChange: 'Replace handoff placeholders with concrete last-completed and next recommended step rows.',
642
+ example: '| Continue with T-0002. | T-0001 is closed-valid. | docs/TASK_BOARD.md |',
643
+ blocking: true
644
+ }
517
645
  });
518
646
  }
519
647
  }
@@ -526,7 +654,17 @@ function validateTaskBoardDone(projectRoot, task, issues, checkedFiles) {
526
654
  severity: 'error',
527
655
  code: 'TASK_BOARD_MISSING',
528
656
  message: 'Done-level validation requires docs/TASK_BOARD.md to contain the completed task row.',
529
- path: relativePath
657
+ path: relativePath,
658
+ heading: 'TASK_BOARD',
659
+ fixHint: 'Restore docs/TASK_BOARD.md with the canonical task table and a row for this task.',
660
+ example: '| ID | Title | Status | Capsule | Notes |',
661
+ remediationHint: {
662
+ path: relativePath,
663
+ heading: 'TASK_BOARD',
664
+ requiredChange: 'Restore docs/TASK_BOARD.md with the canonical task table and completed task row.',
665
+ example: '| ID | Title | Status | Capsule | Notes |',
666
+ blocking: true
667
+ }
530
668
  });
531
669
  return;
532
670
  }
@@ -536,7 +674,17 @@ function validateTaskBoardDone(projectRoot, task, issues, checkedFiles) {
536
674
  severity: 'error',
537
675
  code: 'TASK_BOARD_ROW_MISSING',
538
676
  message: `Done-level validation requires docs/TASK_BOARD.md to contain exactly one row for ${task.id}.`,
539
- path: relativePath
677
+ path: relativePath,
678
+ heading: 'TASK_BOARD',
679
+ fixHint: `Add a docs/TASK_BOARD.md row for ${task.id}, or rerun the task workflow command that should synchronize it.`,
680
+ example: `| ${task.id} | ${task.title} | Done | ${toPortablePath(node_path_1.default.relative(projectRoot, task.dir))} | |`,
681
+ remediationHint: {
682
+ path: relativePath,
683
+ heading: 'TASK_BOARD',
684
+ requiredChange: `Add exactly one Task Board row for ${task.id}.`,
685
+ example: `| ${task.id} | ${task.title} | Done | ${toPortablePath(node_path_1.default.relative(projectRoot, task.dir))} | |`,
686
+ blocking: true
687
+ }
540
688
  });
541
689
  return;
542
690
  }
@@ -545,7 +693,15 @@ function validateTaskBoardDone(projectRoot, task, issues, checkedFiles) {
545
693
  severity: 'error',
546
694
  code: 'TASK_BOARD_ROW_DUPLICATE',
547
695
  message: `docs/TASK_BOARD.md contains ${rows.length} rows for ${task.id}; expected exactly one.`,
548
- path: relativePath
696
+ path: relativePath,
697
+ heading: 'TASK_BOARD',
698
+ fixHint: `Remove duplicate docs/TASK_BOARD.md rows for ${task.id} so exactly one row remains.`,
699
+ remediationHint: {
700
+ path: relativePath,
701
+ heading: 'TASK_BOARD',
702
+ requiredChange: `Remove duplicate Task Board rows for ${task.id}.`,
703
+ blocking: true
704
+ }
549
705
  });
550
706
  return;
551
707
  }
@@ -556,7 +712,17 @@ function validateTaskBoardDone(projectRoot, task, issues, checkedFiles) {
556
712
  severity: 'error',
557
713
  code: 'TASK_BOARD_STATUS_NOT_DONE',
558
714
  message: `Done-level validation requires docs/TASK_BOARD.md status for ${task.id} to be Done.`,
559
- path: relativePath
715
+ path: relativePath,
716
+ heading: 'TASK_BOARD',
717
+ fixHint: `Run \`hadara task finish --task ${task.id} --execute --json\` or update the Task Board status cell for ${task.id} to Done.`,
718
+ example: `| ${task.id} | ${row.title} | Done | ${row.capsule} | ${row.notes} |`,
719
+ remediationHint: {
720
+ path: relativePath,
721
+ heading: 'TASK_BOARD',
722
+ requiredChange: `Set the Task Board status cell for ${task.id} to Done.`,
723
+ example: `| ${task.id} | ${row.title} | Done | ${row.capsule} | ${row.notes} |`,
724
+ blocking: true
725
+ }
560
726
  });
561
727
  }
562
728
  if (row.capsule !== expectedCapsule) {
@@ -564,7 +730,17 @@ function validateTaskBoardDone(projectRoot, task, issues, checkedFiles) {
564
730
  severity: 'error',
565
731
  code: 'TASK_BOARD_CAPSULE_MISMATCH',
566
732
  message: `docs/TASK_BOARD.md capsule for ${task.id} is ${row.capsule || '(empty)'}, expected ${expectedCapsule}.`,
567
- path: relativePath
733
+ path: relativePath,
734
+ heading: 'TASK_BOARD',
735
+ fixHint: `Update the Task Board capsule cell for ${task.id} to ${expectedCapsule}.`,
736
+ example: `| ${task.id} | ${row.title} | ${row.status} | ${expectedCapsule} | ${row.notes} |`,
737
+ remediationHint: {
738
+ path: relativePath,
739
+ heading: 'TASK_BOARD',
740
+ requiredChange: `Set the Task Board capsule cell for ${task.id} to ${expectedCapsule}.`,
741
+ example: `| ${task.id} | ${row.title} | ${row.status} | ${expectedCapsule} | ${row.notes} |`,
742
+ blocking: true
743
+ }
568
744
  });
569
745
  }
570
746
  }
@@ -634,6 +810,43 @@ function isMetadataPlaceholder(value) {
634
810
  function isIsoDate(value) {
635
811
  return /^\d{4}-\d{2}-\d{2}$/.test(value.trim());
636
812
  }
813
+ function scaffoldHeadingForFile(fileName) {
814
+ const headings = {
815
+ 'TASK.md': 'Goal / Scope / Out of Scope',
816
+ 'PLAN.md': 'Plan',
817
+ 'CONTEXT.md': 'Context',
818
+ 'FILES.md': 'Files',
819
+ 'ACCEPTANCE.md': 'Acceptance Criteria',
820
+ 'TESTS.md': 'Tests',
821
+ 'RISKS.md': 'Risks',
822
+ 'DECISIONS.md': 'Decisions',
823
+ 'EVIDENCE.md': 'Evidence'
824
+ };
825
+ return headings[fileName];
826
+ }
827
+ function scaffoldRequiredChange(fileName) {
828
+ const heading = scaffoldHeadingForFile(fileName);
829
+ return heading
830
+ ? `Replace scaffold placeholder content in ${fileName} under ${heading} with task-specific content.`
831
+ : `Replace scaffold placeholder content in ${fileName} with task-specific content.`;
832
+ }
833
+ function scaffoldFixHint(fileName) {
834
+ return `${scaffoldRequiredChange(fileName)} Mark rows Done only after the work and evidence are real.`;
835
+ }
836
+ function scaffoldExample(fileName) {
837
+ const examples = {
838
+ 'TASK.md': '| Preserve Task Board notes. | `task finish` must not erase human-authored cells. |',
839
+ 'PLAN.md': '| 1 | Implement focused fix. | Done | evidence id or summary |',
840
+ 'CONTEXT.md': '| docs/TASK_BOARD.md | Task Board source of truth. | Read |',
841
+ 'FILES.md': '| src/task/task-close.ts | Modify | Add hints. | Done |',
842
+ 'ACCEPTANCE.md': '| AC-1 | Scope is implemented. | Done | evidence id or summary |',
843
+ 'TESTS.md': '| npm run test:focused -- tests/unit/task-ready.test.ts | Validate hints. | Yes | Passed | evidence id |',
844
+ 'RISKS.md': '| Consumer rejects additive fields. | Medium | Low | Keep fields optional. | Mitigated |',
845
+ 'DECISIONS.md': '| D-1 | Add additive hints only. | Accepted | Preserve issue-code compatibility. | Tests. |',
846
+ 'EVIDENCE.md': '| 2026-06-12T00:00:00.000Z | command-log | Focused tests passed. | passed | public | evidence.jsonl |'
847
+ };
848
+ return examples[fileName] ?? 'Replace TBD rows with task-specific content.';
849
+ }
637
850
  function toPortablePath(value) {
638
851
  return value.split(node_path_1.default.sep).join('/');
639
852
  }
@@ -35,6 +35,7 @@
35
35
  "type": "array",
36
36
  "items": { "type": "string", "minLength": 1 }
37
37
  },
38
+ "tier": { "$ref": "#/$defs/tier" },
38
39
  "reason": { "type": "string", "minLength": 1 }
39
40
  }
40
41
  },
@@ -45,9 +46,14 @@
45
46
  "properties": {
46
47
  "path": { "type": "string", "minLength": 1 },
47
48
  "status": { "type": "string", "enum": ["historical", "superseded", "archived"] },
49
+ "tier": { "$ref": "#/$defs/tier" },
48
50
  "reason": { "type": "string", "minLength": 1 }
49
51
  }
50
52
  },
53
+ "tier": {
54
+ "type": "string",
55
+ "enum": ["current-state", "task-work", "conditional-reference", "historical", "excluded"]
56
+ },
51
57
  "issue": {
52
58
  "type": "object",
53
59
  "additionalProperties": true,
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "hadara.harness.validate.v1",
4
+ "x-hadara-schema-id": "hadara.harness.validate.v1",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "required": ["schemaVersion", "command", "ok", "level", "task", "checkedFiles", "issues"],
8
+ "properties": {
9
+ "schemaVersion": { "const": "hadara.harness.validate.v1" },
10
+ "command": { "const": "harness.validate" },
11
+ "ok": { "type": "boolean" },
12
+ "level": { "enum": ["draft", "done"] },
13
+ "task": {
14
+ "type": "object",
15
+ "additionalProperties": true,
16
+ "required": ["id", "title", "capsule"],
17
+ "properties": {
18
+ "id": { "type": "string" },
19
+ "title": { "type": "string" },
20
+ "capsule": { "type": "string" }
21
+ }
22
+ },
23
+ "checkedFiles": { "type": "array", "items": { "type": "string" } },
24
+ "issues": {
25
+ "type": "array",
26
+ "items": { "$ref": "#/$defs/issue" }
27
+ }
28
+ },
29
+ "$defs": {
30
+ "issue": {
31
+ "type": "object",
32
+ "additionalProperties": true,
33
+ "required": ["severity", "code", "message"],
34
+ "properties": {
35
+ "severity": { "enum": ["error", "warning"] },
36
+ "code": { "type": "string" },
37
+ "message": { "type": "string" },
38
+ "path": { "type": "string" },
39
+ "heading": { "type": "string" },
40
+ "fixHint": { "type": "string" },
41
+ "example": { "type": "string" },
42
+ "remediationHint": { "$ref": "#/$defs/remediationHint" }
43
+ }
44
+ },
45
+ "remediationHint": {
46
+ "type": "object",
47
+ "additionalProperties": true,
48
+ "required": ["path", "requiredChange", "blocking"],
49
+ "properties": {
50
+ "path": { "type": "string" },
51
+ "heading": { "type": "string" },
52
+ "requiredChange": { "type": "string" },
53
+ "example": { "type": "string" },
54
+ "blocking": { "type": "boolean" }
55
+ }
56
+ }
57
+ }
58
+ }
@@ -344,6 +344,13 @@
344
344
  "owner": "feature-smoke/runner",
345
345
  "notes": "Documents reduced read-only major feature smoke reports for core service/read-model validation; installed binary and launcher checks are deferred."
346
346
  },
347
+ {
348
+ "id": "hadara.harness.validate.v1",
349
+ "path": "src/schemas/harness-validate.schema.json",
350
+ "status": "fixture",
351
+ "owner": "harness/validate",
352
+ "notes": "Documents Task Capsule draft/done validation reports with additive remediation hints."
353
+ },
347
354
  {
348
355
  "id": "hadara.cleanCheckoutSmoke.v1",
349
356
  "path": "src/schemas/clean-checkout-smoke.schema.json",
@@ -133,9 +133,27 @@
133
133
  "severity": { "enum": ["error", "warning", "info"] },
134
134
  "code": { "type": "string" },
135
135
  "message": { "type": "string" },
136
- "path": { "type": "string" }
136
+ "path": { "type": "string" },
137
+ "heading": { "type": "string" },
138
+ "fixHint": { "type": "string" },
139
+ "example": { "type": "string" },
140
+ "remediationHint": { "$ref": "#/$defs/remediationHint" }
137
141
  }
138
142
  }
139
143
  }
144
+ },
145
+ "$defs": {
146
+ "remediationHint": {
147
+ "type": "object",
148
+ "additionalProperties": true,
149
+ "required": ["path", "requiredChange", "blocking"],
150
+ "properties": {
151
+ "path": { "type": "string" },
152
+ "heading": { "type": "string" },
153
+ "requiredChange": { "type": "string" },
154
+ "example": { "type": "string" },
155
+ "blocking": { "type": "boolean" }
156
+ }
157
+ }
140
158
  }
141
159
  }
@@ -45,6 +45,35 @@
45
45
  },
46
46
  "nextActions": { "type": "array", "items": { "type": "object", "additionalProperties": true } },
47
47
  "primaryNextAction": { "type": "object", "additionalProperties": true },
48
- "issues": { "type": "array", "items": { "type": "object", "additionalProperties": true } }
48
+ "issues": { "type": "array", "items": { "$ref": "#/$defs/issue" } }
49
+ },
50
+ "$defs": {
51
+ "issue": {
52
+ "type": "object",
53
+ "additionalProperties": true,
54
+ "required": ["severity", "code", "message"],
55
+ "properties": {
56
+ "severity": { "enum": ["error", "warning", "info"] },
57
+ "code": { "type": "string" },
58
+ "message": { "type": "string" },
59
+ "path": { "type": "string" },
60
+ "heading": { "type": "string" },
61
+ "fixHint": { "type": "string" },
62
+ "example": { "type": "string" },
63
+ "remediationHint": { "$ref": "#/$defs/remediationHint" }
64
+ }
65
+ },
66
+ "remediationHint": {
67
+ "type": "object",
68
+ "additionalProperties": true,
69
+ "required": ["path", "requiredChange", "blocking"],
70
+ "properties": {
71
+ "path": { "type": "string" },
72
+ "heading": { "type": "string" },
73
+ "requiredChange": { "type": "string" },
74
+ "example": { "type": "string" },
75
+ "blocking": { "type": "boolean" }
76
+ }
77
+ }
49
78
  }
50
79
  }
@@ -9,6 +9,7 @@ exports.createDocsRequiredReadingReport = createDocsRequiredReadingReport;
9
9
  const node_crypto_1 = __importDefault(require("node:crypto"));
10
10
  const node_fs_1 = __importDefault(require("node:fs"));
11
11
  const node_path_1 = __importDefault(require("node:path"));
12
+ const fs_1 = require("../core/fs");
12
13
  const docs_registry_1 = require("./docs-registry");
13
14
  const EXCLUDED_REQUIRED_READING_STATUSES = new Set(['historical', 'superseded', 'archived']);
14
15
  const ARCHIVE_STATUSES = new Set(['historical', 'superseded']);
@@ -34,13 +35,19 @@ function createDocsMarkReport(projectRoot, options) {
34
35
  if (options.mode === 'execute' && options.beforeHash && options.beforeHash !== state.beforeHash) {
35
36
  issues.push({ severity: 'error', code: 'DOC_CLEANUP_BEFORE_HASH_MISMATCH', path: docs_registry_1.DOCS_REGISTRY_PATH, message: `Registry hash ${state.beforeHash} does not match reviewed hash ${options.beforeHash}.` });
36
37
  }
37
- const ok = issues.every((issue) => issue.severity !== 'error');
38
+ let ok = issues.every((issue) => issue.severity !== 'error');
38
39
  if (options.mode === 'execute' && ok && state.registry && entry && afterStatus) {
39
40
  entry.status = afterStatus;
40
41
  entry.notes = options.reason ? `${entry.notes ? `${entry.notes} ` : ''}Cleanup reason: ${options.reason}` : entry.notes;
41
42
  if (afterStatus === 'superseded')
42
43
  entry.supersededBy = normalizePath(options.by ?? '');
43
- node_fs_1.default.writeFileSync(node_path_1.default.join(projectRoot, docs_registry_1.DOCS_REGISTRY_PATH), (0, docs_registry_1.registryJson)(state.registry), 'utf8');
44
+ try {
45
+ (0, fs_1.atomicWriteTextFile)(projectRoot, docs_registry_1.DOCS_REGISTRY_PATH, (0, docs_registry_1.registryJson)(state.registry));
46
+ }
47
+ catch (error) {
48
+ ok = false;
49
+ issues.push({ severity: 'error', code: 'DOC_CLEANUP_ATOMIC_WRITE_FAILED', path: docs_registry_1.DOCS_REGISTRY_PATH, message: `Could not write docs registry atomically: ${error instanceof Error ? error.message : String(error)}` });
50
+ }
44
51
  }
45
52
  return {
46
53
  schemaVersion: 'hadara.docs.mark.v1',
@@ -91,11 +98,12 @@ function createDocsRequiredReadingReport(projectRoot) {
91
98
  for (const doc of state.registry?.documents ?? []) {
92
99
  if (!doc.requiredReading)
93
100
  continue;
101
+ const tier = requiredReadingTier(doc);
94
102
  if (EXCLUDED_REQUIRED_READING_STATUSES.has(doc.status)) {
95
- excluded.push({ path: doc.path, status: doc.status, reason: `${doc.status} docs are not default required reading` });
103
+ excluded.push({ path: doc.path, status: doc.status, tier, reason: `${doc.status} docs are not default required reading` });
96
104
  }
97
105
  else {
98
- documents.push({ path: doc.path, status: doc.status, readWhen: doc.readWhen, reason: `${doc.status} ${doc.kind} doc` });
106
+ documents.push({ path: doc.path, status: doc.status, readWhen: doc.readWhen, tier, reason: `${doc.status} ${doc.kind} doc` });
99
107
  }
100
108
  }
101
109
  return {
@@ -107,6 +115,19 @@ function createDocsRequiredReadingReport(projectRoot) {
107
115
  issues
108
116
  };
109
117
  }
118
+ function requiredReadingTier(doc) {
119
+ if (doc.status === 'superseded' || doc.status === 'archived')
120
+ return 'excluded';
121
+ if (doc.status === 'historical')
122
+ return 'historical';
123
+ if (doc.kind === 'workflow-guide' || doc.kind === 'task-board' || doc.kind === 'task-capsule' || doc.readWhen.some((readWhen) => readWhen === 'task-start' || readWhen === 'task-close')) {
124
+ return 'task-work';
125
+ }
126
+ if (doc.kind === 'project-context' || doc.kind === 'project-state' || doc.kind === 'handoff' || doc.kind === 'protocol' || doc.readWhen.includes('session-start')) {
127
+ return 'current-state';
128
+ }
129
+ return 'conditional-reference';
130
+ }
110
131
  function validateTransition(entry, afterStatus, options) {
111
132
  const issues = [];
112
133
  const reasonRequired = afterStatus === 'historical' || afterStatus === 'superseded';