hunter-harness 0.2.0 → 0.2.2

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.
Files changed (564) hide show
  1. package/dist/bin.js +679 -232
  2. package/package.json +10 -8
  3. package/resources/harness/bundles/general/claude-code/.harness-build.json +0 -6
  4. package/resources/harness/bundles/general/claude-code/CONTEXT.md +0 -65
  5. package/resources/harness/bundles/general/claude-code/README.md +0 -478
  6. package/resources/harness/bundles/general/claude-code/agents/harness-evaluator.md +0 -96
  7. package/resources/harness/bundles/general/claude-code/agents/harness-explorer.md +0 -72
  8. package/resources/harness/bundles/general/claude-code/agents/harness-reviewer.md +0 -69
  9. package/resources/harness/bundles/general/claude-code/harness-archive/SKILL.md +0 -215
  10. package/resources/harness/bundles/general/claude-code/harness-archive/checklist.md +0 -52
  11. package/resources/harness/bundles/general/claude-code/harness-archive/reference.md +0 -103
  12. package/resources/harness/bundles/general/claude-code/harness-archive/scripts/gen-manifest.ps1 +0 -31
  13. package/resources/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +0 -171
  14. package/resources/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +0 -128
  15. package/resources/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +0 -112
  16. package/resources/harness/bundles/general/claude-code/harness-codebase-map/checklist.md +0 -126
  17. package/resources/harness/bundles/general/claude-code/harness-codebase-map/reference.md +0 -364
  18. package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
  19. package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/CONCERNS.md +0 -49
  20. package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/CONVENTIONS.md +0 -57
  21. package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
  22. package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/STACK.md +0 -55
  23. package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/STRUCTURE.md +0 -69
  24. package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/TESTING.md +0 -64
  25. package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/map-manifest.schema.json +0 -73
  26. package/resources/harness/bundles/general/claude-code/harness-codebase-map/templates/map-summary.md +0 -64
  27. package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +0 -246
  28. package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/design.md +0 -842
  29. package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
  30. package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/mcp-config.example.json +0 -10
  31. package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/reference.md +0 -309
  32. package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
  33. package/resources/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
  34. package/resources/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +0 -164
  35. package/resources/harness/bundles/general/claude-code/harness-plan/SKILL.md +0 -127
  36. package/resources/harness/bundles/general/claude-code/harness-plan/checklist.md +0 -243
  37. package/resources/harness/bundles/general/claude-code/harness-plan/protocols.md +0 -124
  38. package/resources/harness/bundles/general/claude-code/harness-plan/reference.md +0 -376
  39. package/resources/harness/bundles/general/claude-code/harness-review/SKILL.md +0 -160
  40. package/resources/harness/bundles/general/claude-code/harness-review/checklist.md +0 -124
  41. package/resources/harness/bundles/general/claude-code/harness-review/protocols.md +0 -68
  42. package/resources/harness/bundles/general/claude-code/harness-review/reference.md +0 -86
  43. package/resources/harness/bundles/general/claude-code/harness-run/SKILL.md +0 -132
  44. package/resources/harness/bundles/general/claude-code/harness-run/checklist.md +0 -259
  45. package/resources/harness/bundles/general/claude-code/harness-run/protocols.md +0 -78
  46. package/resources/harness/bundles/general/claude-code/harness-run/reference.md +0 -834
  47. package/resources/harness/bundles/general/claude-code/harness-submit/SKILL.md +0 -159
  48. package/resources/harness/bundles/general/claude-code/harness-submit/checklist.md +0 -407
  49. package/resources/harness/bundles/general/claude-code/harness-submit/reference.md +0 -152
  50. package/resources/harness/bundles/general/claude-code/harness-sync/SKILL.md +0 -82
  51. package/resources/harness/bundles/general/claude-code/harness-sync/reference.md +0 -153
  52. package/resources/harness/bundles/general/claude-code/harness-test/SKILL.md +0 -180
  53. package/resources/harness/bundles/general/claude-code/harness-test/checklist.md +0 -319
  54. package/resources/harness/bundles/general/claude-code/harness-test/pitfalls.md +0 -260
  55. package/resources/harness/bundles/general/claude-code/harness-test/reference.md +0 -791
  56. package/resources/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +0 -175
  57. package/resources/harness/bundles/general/claude-code/protocols/evidence-based-reporting-protocol.md +0 -134
  58. package/resources/harness/bundles/general/claude-code/protocols/ledger-protocol.md +0 -270
  59. package/resources/harness/bundles/general/claude-code/protocols/powershell-protocol.md +0 -181
  60. package/resources/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +0 -150
  61. package/resources/harness/bundles/general/claude-code/protocols/sensitive-info-protocol.md +0 -92
  62. package/resources/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +0 -131
  63. package/resources/harness/bundles/general/claude-code/protocols/submit-protocol.md +0 -78
  64. package/resources/harness/bundles/general/claude-code/scripts/harness_archive.py +0 -2319
  65. package/resources/harness/bundles/general/claude-code/scripts/harness_deploy.py +0 -674
  66. package/resources/harness/bundles/general/claude-code/scripts/harness_events.py +0 -680
  67. package/resources/harness/bundles/general/claude-code/scripts/harness_ledger.py +0 -671
  68. package/resources/harness/bundles/general/claude-code/scripts/harness_preflight.py +0 -780
  69. package/resources/harness/bundles/general/claude-code/scripts/harness_service.py +0 -1331
  70. package/resources/harness/bundles/general/codebuddy/.harness-build.json +0 -6
  71. package/resources/harness/bundles/general/codebuddy/CONTEXT.md +0 -65
  72. package/resources/harness/bundles/general/codebuddy/README.md +0 -478
  73. package/resources/harness/bundles/general/codebuddy/agents/harness-evaluator.md +0 -94
  74. package/resources/harness/bundles/general/codebuddy/agents/harness-explorer.md +0 -53
  75. package/resources/harness/bundles/general/codebuddy/agents/harness-reviewer.md +0 -52
  76. package/resources/harness/bundles/general/codebuddy/harness-archive/SKILL.md +0 -198
  77. package/resources/harness/bundles/general/codebuddy/harness-archive/checklist.md +0 -52
  78. package/resources/harness/bundles/general/codebuddy/harness-archive/reference.md +0 -103
  79. package/resources/harness/bundles/general/codebuddy/harness-archive/scripts/gen-manifest.ps1 +0 -31
  80. package/resources/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +0 -171
  81. package/resources/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +0 -128
  82. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +0 -95
  83. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/checklist.md +0 -126
  84. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/reference.md +0 -364
  85. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
  86. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/CONCERNS.md +0 -49
  87. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/CONVENTIONS.md +0 -57
  88. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
  89. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/STACK.md +0 -55
  90. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/STRUCTURE.md +0 -69
  91. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/TESTING.md +0 -64
  92. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/map-manifest.schema.json +0 -73
  93. package/resources/harness/bundles/general/codebuddy/harness-codebase-map/templates/map-summary.md +0 -64
  94. package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +0 -229
  95. package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/design.md +0 -842
  96. package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
  97. package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/mcp-config.example.json +0 -10
  98. package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/reference.md +0 -309
  99. package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
  100. package/resources/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
  101. package/resources/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +0 -147
  102. package/resources/harness/bundles/general/codebuddy/harness-plan/SKILL.md +0 -109
  103. package/resources/harness/bundles/general/codebuddy/harness-plan/checklist.md +0 -243
  104. package/resources/harness/bundles/general/codebuddy/harness-plan/protocols.md +0 -124
  105. package/resources/harness/bundles/general/codebuddy/harness-plan/reference.md +0 -376
  106. package/resources/harness/bundles/general/codebuddy/harness-review/SKILL.md +0 -142
  107. package/resources/harness/bundles/general/codebuddy/harness-review/checklist.md +0 -124
  108. package/resources/harness/bundles/general/codebuddy/harness-review/protocols.md +0 -68
  109. package/resources/harness/bundles/general/codebuddy/harness-review/reference.md +0 -86
  110. package/resources/harness/bundles/general/codebuddy/harness-run/SKILL.md +0 -113
  111. package/resources/harness/bundles/general/codebuddy/harness-run/checklist.md +0 -259
  112. package/resources/harness/bundles/general/codebuddy/harness-run/protocols.md +0 -78
  113. package/resources/harness/bundles/general/codebuddy/harness-run/reference.md +0 -834
  114. package/resources/harness/bundles/general/codebuddy/harness-submit/SKILL.md +0 -142
  115. package/resources/harness/bundles/general/codebuddy/harness-submit/checklist.md +0 -407
  116. package/resources/harness/bundles/general/codebuddy/harness-submit/reference.md +0 -152
  117. package/resources/harness/bundles/general/codebuddy/harness-sync/SKILL.md +0 -65
  118. package/resources/harness/bundles/general/codebuddy/harness-sync/reference.md +0 -153
  119. package/resources/harness/bundles/general/codebuddy/harness-test/SKILL.md +0 -160
  120. package/resources/harness/bundles/general/codebuddy/harness-test/checklist.md +0 -319
  121. package/resources/harness/bundles/general/codebuddy/harness-test/pitfalls.md +0 -260
  122. package/resources/harness/bundles/general/codebuddy/harness-test/reference.md +0 -791
  123. package/resources/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +0 -175
  124. package/resources/harness/bundles/general/codebuddy/protocols/evidence-based-reporting-protocol.md +0 -134
  125. package/resources/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +0 -270
  126. package/resources/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +0 -181
  127. package/resources/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +0 -150
  128. package/resources/harness/bundles/general/codebuddy/protocols/sensitive-info-protocol.md +0 -92
  129. package/resources/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +0 -131
  130. package/resources/harness/bundles/general/codebuddy/protocols/submit-protocol.md +0 -78
  131. package/resources/harness/bundles/general/codebuddy/scripts/harness_archive.py +0 -2319
  132. package/resources/harness/bundles/general/codebuddy/scripts/harness_deploy.py +0 -674
  133. package/resources/harness/bundles/general/codebuddy/scripts/harness_events.py +0 -680
  134. package/resources/harness/bundles/general/codebuddy/scripts/harness_ledger.py +0 -671
  135. package/resources/harness/bundles/general/codebuddy/scripts/harness_preflight.py +0 -780
  136. package/resources/harness/bundles/general/codebuddy/scripts/harness_service.py +0 -1331
  137. package/resources/harness/bundles/general/codex/.harness-build.json +0 -6
  138. package/resources/harness/bundles/general/codex/CONTEXT.md +0 -65
  139. package/resources/harness/bundles/general/codex/README.md +0 -478
  140. package/resources/harness/bundles/general/codex/harness-archive/SKILL.md +0 -198
  141. package/resources/harness/bundles/general/codex/harness-archive/checklist.md +0 -52
  142. package/resources/harness/bundles/general/codex/harness-archive/reference.md +0 -103
  143. package/resources/harness/bundles/general/codex/harness-archive/scripts/gen-manifest.ps1 +0 -31
  144. package/resources/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +0 -171
  145. package/resources/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +0 -128
  146. package/resources/harness/bundles/general/codex/harness-codebase-map/SKILL.md +0 -95
  147. package/resources/harness/bundles/general/codex/harness-codebase-map/checklist.md +0 -126
  148. package/resources/harness/bundles/general/codex/harness-codebase-map/reference.md +0 -364
  149. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
  150. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/CONCERNS.md +0 -49
  151. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/CONVENTIONS.md +0 -57
  152. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
  153. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/STACK.md +0 -55
  154. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/STRUCTURE.md +0 -69
  155. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/TESTING.md +0 -64
  156. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/map-manifest.schema.json +0 -73
  157. package/resources/harness/bundles/general/codex/harness-codebase-map/templates/map-summary.md +0 -64
  158. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +0 -229
  159. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/design.md +0 -842
  160. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
  161. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/mcp-config.example.json +0 -10
  162. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/reference.md +0 -309
  163. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
  164. package/resources/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
  165. package/resources/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +0 -147
  166. package/resources/harness/bundles/general/codex/harness-plan/SKILL.md +0 -109
  167. package/resources/harness/bundles/general/codex/harness-plan/checklist.md +0 -243
  168. package/resources/harness/bundles/general/codex/harness-plan/protocols.md +0 -124
  169. package/resources/harness/bundles/general/codex/harness-plan/reference.md +0 -376
  170. package/resources/harness/bundles/general/codex/harness-review/SKILL.md +0 -137
  171. package/resources/harness/bundles/general/codex/harness-review/checklist.md +0 -124
  172. package/resources/harness/bundles/general/codex/harness-review/protocols.md +0 -68
  173. package/resources/harness/bundles/general/codex/harness-review/reference.md +0 -86
  174. package/resources/harness/bundles/general/codex/harness-run/SKILL.md +0 -113
  175. package/resources/harness/bundles/general/codex/harness-run/checklist.md +0 -259
  176. package/resources/harness/bundles/general/codex/harness-run/protocols.md +0 -78
  177. package/resources/harness/bundles/general/codex/harness-run/reference.md +0 -834
  178. package/resources/harness/bundles/general/codex/harness-submit/SKILL.md +0 -142
  179. package/resources/harness/bundles/general/codex/harness-submit/checklist.md +0 -407
  180. package/resources/harness/bundles/general/codex/harness-submit/reference.md +0 -152
  181. package/resources/harness/bundles/general/codex/harness-sync/SKILL.md +0 -65
  182. package/resources/harness/bundles/general/codex/harness-sync/reference.md +0 -153
  183. package/resources/harness/bundles/general/codex/harness-test/SKILL.md +0 -160
  184. package/resources/harness/bundles/general/codex/harness-test/checklist.md +0 -319
  185. package/resources/harness/bundles/general/codex/harness-test/pitfalls.md +0 -260
  186. package/resources/harness/bundles/general/codex/harness-test/reference.md +0 -791
  187. package/resources/harness/bundles/general/codex/protocols/archive-report-protocol.md +0 -175
  188. package/resources/harness/bundles/general/codex/protocols/evidence-based-reporting-protocol.md +0 -134
  189. package/resources/harness/bundles/general/codex/protocols/ledger-protocol.md +0 -270
  190. package/resources/harness/bundles/general/codex/protocols/powershell-protocol.md +0 -181
  191. package/resources/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +0 -150
  192. package/resources/harness/bundles/general/codex/protocols/sensitive-info-protocol.md +0 -92
  193. package/resources/harness/bundles/general/codex/protocols/state-layout-protocol.md +0 -131
  194. package/resources/harness/bundles/general/codex/protocols/submit-protocol.md +0 -78
  195. package/resources/harness/bundles/general/codex/scripts/harness_archive.py +0 -2319
  196. package/resources/harness/bundles/general/codex/scripts/harness_deploy.py +0 -674
  197. package/resources/harness/bundles/general/codex/scripts/harness_events.py +0 -680
  198. package/resources/harness/bundles/general/codex/scripts/harness_ledger.py +0 -671
  199. package/resources/harness/bundles/general/codex/scripts/harness_preflight.py +0 -780
  200. package/resources/harness/bundles/general/codex/scripts/harness_service.py +0 -1331
  201. package/resources/harness/bundles/general/cursor/.harness-build.json +0 -6
  202. package/resources/harness/bundles/general/cursor/CONTEXT.md +0 -65
  203. package/resources/harness/bundles/general/cursor/README.md +0 -478
  204. package/resources/harness/bundles/general/cursor/harness-archive/SKILL.md +0 -198
  205. package/resources/harness/bundles/general/cursor/harness-archive/checklist.md +0 -52
  206. package/resources/harness/bundles/general/cursor/harness-archive/reference.md +0 -103
  207. package/resources/harness/bundles/general/cursor/harness-archive/scripts/gen-manifest.ps1 +0 -31
  208. package/resources/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +0 -171
  209. package/resources/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +0 -128
  210. package/resources/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +0 -95
  211. package/resources/harness/bundles/general/cursor/harness-codebase-map/checklist.md +0 -126
  212. package/resources/harness/bundles/general/cursor/harness-codebase-map/reference.md +0 -364
  213. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
  214. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/CONCERNS.md +0 -49
  215. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/CONVENTIONS.md +0 -57
  216. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
  217. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/STACK.md +0 -55
  218. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/STRUCTURE.md +0 -69
  219. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/TESTING.md +0 -64
  220. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/map-manifest.schema.json +0 -73
  221. package/resources/harness/bundles/general/cursor/harness-codebase-map/templates/map-summary.md +0 -64
  222. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +0 -229
  223. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/design.md +0 -842
  224. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
  225. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/mcp-config.example.json +0 -10
  226. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/reference.md +0 -309
  227. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
  228. package/resources/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
  229. package/resources/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +0 -147
  230. package/resources/harness/bundles/general/cursor/harness-plan/SKILL.md +0 -109
  231. package/resources/harness/bundles/general/cursor/harness-plan/checklist.md +0 -243
  232. package/resources/harness/bundles/general/cursor/harness-plan/protocols.md +0 -124
  233. package/resources/harness/bundles/general/cursor/harness-plan/reference.md +0 -376
  234. package/resources/harness/bundles/general/cursor/harness-review/SKILL.md +0 -137
  235. package/resources/harness/bundles/general/cursor/harness-review/checklist.md +0 -124
  236. package/resources/harness/bundles/general/cursor/harness-review/protocols.md +0 -68
  237. package/resources/harness/bundles/general/cursor/harness-review/reference.md +0 -86
  238. package/resources/harness/bundles/general/cursor/harness-run/SKILL.md +0 -113
  239. package/resources/harness/bundles/general/cursor/harness-run/checklist.md +0 -259
  240. package/resources/harness/bundles/general/cursor/harness-run/protocols.md +0 -78
  241. package/resources/harness/bundles/general/cursor/harness-run/reference.md +0 -834
  242. package/resources/harness/bundles/general/cursor/harness-submit/SKILL.md +0 -142
  243. package/resources/harness/bundles/general/cursor/harness-submit/checklist.md +0 -407
  244. package/resources/harness/bundles/general/cursor/harness-submit/reference.md +0 -152
  245. package/resources/harness/bundles/general/cursor/harness-sync/SKILL.md +0 -65
  246. package/resources/harness/bundles/general/cursor/harness-sync/reference.md +0 -153
  247. package/resources/harness/bundles/general/cursor/harness-test/SKILL.md +0 -160
  248. package/resources/harness/bundles/general/cursor/harness-test/checklist.md +0 -319
  249. package/resources/harness/bundles/general/cursor/harness-test/pitfalls.md +0 -260
  250. package/resources/harness/bundles/general/cursor/harness-test/reference.md +0 -791
  251. package/resources/harness/bundles/general/cursor/protocols/archive-report-protocol.md +0 -175
  252. package/resources/harness/bundles/general/cursor/protocols/evidence-based-reporting-protocol.md +0 -134
  253. package/resources/harness/bundles/general/cursor/protocols/ledger-protocol.md +0 -270
  254. package/resources/harness/bundles/general/cursor/protocols/powershell-protocol.md +0 -181
  255. package/resources/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +0 -150
  256. package/resources/harness/bundles/general/cursor/protocols/sensitive-info-protocol.md +0 -92
  257. package/resources/harness/bundles/general/cursor/protocols/state-layout-protocol.md +0 -131
  258. package/resources/harness/bundles/general/cursor/protocols/submit-protocol.md +0 -78
  259. package/resources/harness/bundles/general/cursor/scripts/harness_archive.py +0 -2319
  260. package/resources/harness/bundles/general/cursor/scripts/harness_deploy.py +0 -674
  261. package/resources/harness/bundles/general/cursor/scripts/harness_events.py +0 -680
  262. package/resources/harness/bundles/general/cursor/scripts/harness_ledger.py +0 -671
  263. package/resources/harness/bundles/general/cursor/scripts/harness_preflight.py +0 -780
  264. package/resources/harness/bundles/general/cursor/scripts/harness_service.py +0 -1331
  265. package/resources/harness/bundles/java/claude-code/.harness-build.json +0 -6
  266. package/resources/harness/bundles/java/claude-code/CONTEXT.md +0 -65
  267. package/resources/harness/bundles/java/claude-code/README.md +0 -478
  268. package/resources/harness/bundles/java/claude-code/agents/harness-evaluator.md +0 -96
  269. package/resources/harness/bundles/java/claude-code/agents/harness-explorer.md +0 -72
  270. package/resources/harness/bundles/java/claude-code/agents/harness-reviewer.md +0 -69
  271. package/resources/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +0 -86
  272. package/resources/harness/bundles/java/claude-code/harness-apidoc/checklist.md +0 -142
  273. package/resources/harness/bundles/java/claude-code/harness-apidoc/reference.md +0 -233
  274. package/resources/harness/bundles/java/claude-code/harness-archive/SKILL.md +0 -215
  275. package/resources/harness/bundles/java/claude-code/harness-archive/checklist.md +0 -52
  276. package/resources/harness/bundles/java/claude-code/harness-archive/reference.md +0 -103
  277. package/resources/harness/bundles/java/claude-code/harness-archive/scripts/gen-manifest.ps1 +0 -31
  278. package/resources/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +0 -171
  279. package/resources/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +0 -128
  280. package/resources/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +0 -112
  281. package/resources/harness/bundles/java/claude-code/harness-codebase-map/checklist.md +0 -126
  282. package/resources/harness/bundles/java/claude-code/harness-codebase-map/reference.md +0 -364
  283. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
  284. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/CONCERNS.md +0 -49
  285. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/CONVENTIONS.md +0 -57
  286. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
  287. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/STACK.md +0 -55
  288. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/STRUCTURE.md +0 -69
  289. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/TESTING.md +0 -64
  290. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/map-manifest.schema.json +0 -73
  291. package/resources/harness/bundles/java/claude-code/harness-codebase-map/templates/map-summary.md +0 -64
  292. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +0 -246
  293. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/design.md +0 -842
  294. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
  295. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/mcp-config.example.json +0 -10
  296. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/reference.md +0 -309
  297. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
  298. package/resources/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
  299. package/resources/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +0 -164
  300. package/resources/harness/bundles/java/claude-code/harness-package/SKILL.md +0 -87
  301. package/resources/harness/bundles/java/claude-code/harness-package/checklist.md +0 -322
  302. package/resources/harness/bundles/java/claude-code/harness-package/reference.md +0 -210
  303. package/resources/harness/bundles/java/claude-code/harness-plan/SKILL.md +0 -127
  304. package/resources/harness/bundles/java/claude-code/harness-plan/checklist.md +0 -243
  305. package/resources/harness/bundles/java/claude-code/harness-plan/protocols.md +0 -124
  306. package/resources/harness/bundles/java/claude-code/harness-plan/reference.md +0 -376
  307. package/resources/harness/bundles/java/claude-code/harness-review/SKILL.md +0 -160
  308. package/resources/harness/bundles/java/claude-code/harness-review/checklist.md +0 -124
  309. package/resources/harness/bundles/java/claude-code/harness-review/protocols.md +0 -68
  310. package/resources/harness/bundles/java/claude-code/harness-review/reference.md +0 -86
  311. package/resources/harness/bundles/java/claude-code/harness-run/SKILL.md +0 -148
  312. package/resources/harness/bundles/java/claude-code/harness-run/checklist.md +0 -250
  313. package/resources/harness/bundles/java/claude-code/harness-run/protocols.md +0 -78
  314. package/resources/harness/bundles/java/claude-code/harness-run/reference.md +0 -814
  315. package/resources/harness/bundles/java/claude-code/harness-submit/SKILL.md +0 -166
  316. package/resources/harness/bundles/java/claude-code/harness-submit/checklist.md +0 -407
  317. package/resources/harness/bundles/java/claude-code/harness-submit/reference.md +0 -152
  318. package/resources/harness/bundles/java/claude-code/harness-sync/SKILL.md +0 -82
  319. package/resources/harness/bundles/java/claude-code/harness-sync/reference.md +0 -153
  320. package/resources/harness/bundles/java/claude-code/harness-test/SKILL.md +0 -192
  321. package/resources/harness/bundles/java/claude-code/harness-test/checklist.md +0 -317
  322. package/resources/harness/bundles/java/claude-code/harness-test/pitfalls-java.md +0 -16
  323. package/resources/harness/bundles/java/claude-code/harness-test/pitfalls.md +0 -260
  324. package/resources/harness/bundles/java/claude-code/harness-test/reference.md +0 -788
  325. package/resources/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +0 -175
  326. package/resources/harness/bundles/java/claude-code/protocols/evidence-based-reporting-protocol.md +0 -134
  327. package/resources/harness/bundles/java/claude-code/protocols/ledger-protocol.md +0 -270
  328. package/resources/harness/bundles/java/claude-code/protocols/powershell-protocol.md +0 -181
  329. package/resources/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +0 -150
  330. package/resources/harness/bundles/java/claude-code/protocols/sensitive-info-protocol.md +0 -92
  331. package/resources/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +0 -131
  332. package/resources/harness/bundles/java/claude-code/protocols/submit-protocol.md +0 -78
  333. package/resources/harness/bundles/java/claude-code/scripts/harness_archive.py +0 -2319
  334. package/resources/harness/bundles/java/claude-code/scripts/harness_deploy.py +0 -674
  335. package/resources/harness/bundles/java/claude-code/scripts/harness_events.py +0 -680
  336. package/resources/harness/bundles/java/claude-code/scripts/harness_ledger.py +0 -671
  337. package/resources/harness/bundles/java/claude-code/scripts/harness_preflight.py +0 -780
  338. package/resources/harness/bundles/java/claude-code/scripts/harness_service.py +0 -1331
  339. package/resources/harness/bundles/java/codebuddy/.harness-build.json +0 -6
  340. package/resources/harness/bundles/java/codebuddy/CONTEXT.md +0 -65
  341. package/resources/harness/bundles/java/codebuddy/README.md +0 -478
  342. package/resources/harness/bundles/java/codebuddy/agents/harness-evaluator.md +0 -94
  343. package/resources/harness/bundles/java/codebuddy/agents/harness-explorer.md +0 -53
  344. package/resources/harness/bundles/java/codebuddy/agents/harness-reviewer.md +0 -52
  345. package/resources/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +0 -68
  346. package/resources/harness/bundles/java/codebuddy/harness-apidoc/checklist.md +0 -142
  347. package/resources/harness/bundles/java/codebuddy/harness-apidoc/reference.md +0 -233
  348. package/resources/harness/bundles/java/codebuddy/harness-archive/SKILL.md +0 -198
  349. package/resources/harness/bundles/java/codebuddy/harness-archive/checklist.md +0 -52
  350. package/resources/harness/bundles/java/codebuddy/harness-archive/reference.md +0 -103
  351. package/resources/harness/bundles/java/codebuddy/harness-archive/scripts/gen-manifest.ps1 +0 -31
  352. package/resources/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +0 -171
  353. package/resources/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +0 -128
  354. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +0 -95
  355. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/checklist.md +0 -126
  356. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/reference.md +0 -364
  357. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
  358. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/CONCERNS.md +0 -49
  359. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/CONVENTIONS.md +0 -57
  360. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
  361. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/STACK.md +0 -55
  362. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/STRUCTURE.md +0 -69
  363. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/TESTING.md +0 -64
  364. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/map-manifest.schema.json +0 -73
  365. package/resources/harness/bundles/java/codebuddy/harness-codebase-map/templates/map-summary.md +0 -64
  366. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +0 -229
  367. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/design.md +0 -842
  368. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
  369. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/mcp-config.example.json +0 -10
  370. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/reference.md +0 -309
  371. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
  372. package/resources/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
  373. package/resources/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +0 -147
  374. package/resources/harness/bundles/java/codebuddy/harness-package/SKILL.md +0 -69
  375. package/resources/harness/bundles/java/codebuddy/harness-package/checklist.md +0 -322
  376. package/resources/harness/bundles/java/codebuddy/harness-package/reference.md +0 -210
  377. package/resources/harness/bundles/java/codebuddy/harness-plan/SKILL.md +0 -109
  378. package/resources/harness/bundles/java/codebuddy/harness-plan/checklist.md +0 -243
  379. package/resources/harness/bundles/java/codebuddy/harness-plan/protocols.md +0 -124
  380. package/resources/harness/bundles/java/codebuddy/harness-plan/reference.md +0 -376
  381. package/resources/harness/bundles/java/codebuddy/harness-review/SKILL.md +0 -142
  382. package/resources/harness/bundles/java/codebuddy/harness-review/checklist.md +0 -124
  383. package/resources/harness/bundles/java/codebuddy/harness-review/protocols.md +0 -68
  384. package/resources/harness/bundles/java/codebuddy/harness-review/reference.md +0 -86
  385. package/resources/harness/bundles/java/codebuddy/harness-run/SKILL.md +0 -129
  386. package/resources/harness/bundles/java/codebuddy/harness-run/checklist.md +0 -250
  387. package/resources/harness/bundles/java/codebuddy/harness-run/protocols.md +0 -78
  388. package/resources/harness/bundles/java/codebuddy/harness-run/reference.md +0 -814
  389. package/resources/harness/bundles/java/codebuddy/harness-submit/SKILL.md +0 -149
  390. package/resources/harness/bundles/java/codebuddy/harness-submit/checklist.md +0 -407
  391. package/resources/harness/bundles/java/codebuddy/harness-submit/reference.md +0 -152
  392. package/resources/harness/bundles/java/codebuddy/harness-sync/SKILL.md +0 -65
  393. package/resources/harness/bundles/java/codebuddy/harness-sync/reference.md +0 -153
  394. package/resources/harness/bundles/java/codebuddy/harness-test/SKILL.md +0 -172
  395. package/resources/harness/bundles/java/codebuddy/harness-test/checklist.md +0 -317
  396. package/resources/harness/bundles/java/codebuddy/harness-test/pitfalls-java.md +0 -16
  397. package/resources/harness/bundles/java/codebuddy/harness-test/pitfalls.md +0 -260
  398. package/resources/harness/bundles/java/codebuddy/harness-test/reference.md +0 -788
  399. package/resources/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +0 -175
  400. package/resources/harness/bundles/java/codebuddy/protocols/evidence-based-reporting-protocol.md +0 -134
  401. package/resources/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +0 -270
  402. package/resources/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +0 -181
  403. package/resources/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +0 -150
  404. package/resources/harness/bundles/java/codebuddy/protocols/sensitive-info-protocol.md +0 -92
  405. package/resources/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +0 -131
  406. package/resources/harness/bundles/java/codebuddy/protocols/submit-protocol.md +0 -78
  407. package/resources/harness/bundles/java/codebuddy/scripts/harness_archive.py +0 -2319
  408. package/resources/harness/bundles/java/codebuddy/scripts/harness_deploy.py +0 -674
  409. package/resources/harness/bundles/java/codebuddy/scripts/harness_events.py +0 -680
  410. package/resources/harness/bundles/java/codebuddy/scripts/harness_ledger.py +0 -671
  411. package/resources/harness/bundles/java/codebuddy/scripts/harness_preflight.py +0 -780
  412. package/resources/harness/bundles/java/codebuddy/scripts/harness_service.py +0 -1331
  413. package/resources/harness/bundles/java/codex/.harness-build.json +0 -6
  414. package/resources/harness/bundles/java/codex/CONTEXT.md +0 -65
  415. package/resources/harness/bundles/java/codex/README.md +0 -478
  416. package/resources/harness/bundles/java/codex/harness-apidoc/SKILL.md +0 -68
  417. package/resources/harness/bundles/java/codex/harness-apidoc/checklist.md +0 -142
  418. package/resources/harness/bundles/java/codex/harness-apidoc/reference.md +0 -233
  419. package/resources/harness/bundles/java/codex/harness-archive/SKILL.md +0 -198
  420. package/resources/harness/bundles/java/codex/harness-archive/checklist.md +0 -52
  421. package/resources/harness/bundles/java/codex/harness-archive/reference.md +0 -103
  422. package/resources/harness/bundles/java/codex/harness-archive/scripts/gen-manifest.ps1 +0 -31
  423. package/resources/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +0 -171
  424. package/resources/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +0 -128
  425. package/resources/harness/bundles/java/codex/harness-codebase-map/SKILL.md +0 -95
  426. package/resources/harness/bundles/java/codex/harness-codebase-map/checklist.md +0 -126
  427. package/resources/harness/bundles/java/codex/harness-codebase-map/reference.md +0 -364
  428. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
  429. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/CONCERNS.md +0 -49
  430. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/CONVENTIONS.md +0 -57
  431. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
  432. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/STACK.md +0 -55
  433. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/STRUCTURE.md +0 -69
  434. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/TESTING.md +0 -64
  435. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/map-manifest.schema.json +0 -73
  436. package/resources/harness/bundles/java/codex/harness-codebase-map/templates/map-summary.md +0 -64
  437. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +0 -229
  438. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/design.md +0 -842
  439. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
  440. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/mcp-config.example.json +0 -10
  441. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/reference.md +0 -309
  442. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
  443. package/resources/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
  444. package/resources/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +0 -147
  445. package/resources/harness/bundles/java/codex/harness-package/SKILL.md +0 -69
  446. package/resources/harness/bundles/java/codex/harness-package/checklist.md +0 -322
  447. package/resources/harness/bundles/java/codex/harness-package/reference.md +0 -210
  448. package/resources/harness/bundles/java/codex/harness-plan/SKILL.md +0 -109
  449. package/resources/harness/bundles/java/codex/harness-plan/checklist.md +0 -243
  450. package/resources/harness/bundles/java/codex/harness-plan/protocols.md +0 -124
  451. package/resources/harness/bundles/java/codex/harness-plan/reference.md +0 -376
  452. package/resources/harness/bundles/java/codex/harness-review/SKILL.md +0 -137
  453. package/resources/harness/bundles/java/codex/harness-review/checklist.md +0 -124
  454. package/resources/harness/bundles/java/codex/harness-review/protocols.md +0 -68
  455. package/resources/harness/bundles/java/codex/harness-review/reference.md +0 -86
  456. package/resources/harness/bundles/java/codex/harness-run/SKILL.md +0 -129
  457. package/resources/harness/bundles/java/codex/harness-run/checklist.md +0 -250
  458. package/resources/harness/bundles/java/codex/harness-run/protocols.md +0 -78
  459. package/resources/harness/bundles/java/codex/harness-run/reference.md +0 -814
  460. package/resources/harness/bundles/java/codex/harness-submit/SKILL.md +0 -149
  461. package/resources/harness/bundles/java/codex/harness-submit/checklist.md +0 -407
  462. package/resources/harness/bundles/java/codex/harness-submit/reference.md +0 -152
  463. package/resources/harness/bundles/java/codex/harness-sync/SKILL.md +0 -65
  464. package/resources/harness/bundles/java/codex/harness-sync/reference.md +0 -153
  465. package/resources/harness/bundles/java/codex/harness-test/SKILL.md +0 -172
  466. package/resources/harness/bundles/java/codex/harness-test/checklist.md +0 -317
  467. package/resources/harness/bundles/java/codex/harness-test/pitfalls-java.md +0 -16
  468. package/resources/harness/bundles/java/codex/harness-test/pitfalls.md +0 -260
  469. package/resources/harness/bundles/java/codex/harness-test/reference.md +0 -788
  470. package/resources/harness/bundles/java/codex/protocols/archive-report-protocol.md +0 -175
  471. package/resources/harness/bundles/java/codex/protocols/evidence-based-reporting-protocol.md +0 -134
  472. package/resources/harness/bundles/java/codex/protocols/ledger-protocol.md +0 -270
  473. package/resources/harness/bundles/java/codex/protocols/powershell-protocol.md +0 -181
  474. package/resources/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +0 -150
  475. package/resources/harness/bundles/java/codex/protocols/sensitive-info-protocol.md +0 -92
  476. package/resources/harness/bundles/java/codex/protocols/state-layout-protocol.md +0 -131
  477. package/resources/harness/bundles/java/codex/protocols/submit-protocol.md +0 -78
  478. package/resources/harness/bundles/java/codex/scripts/harness_archive.py +0 -2319
  479. package/resources/harness/bundles/java/codex/scripts/harness_deploy.py +0 -674
  480. package/resources/harness/bundles/java/codex/scripts/harness_events.py +0 -680
  481. package/resources/harness/bundles/java/codex/scripts/harness_ledger.py +0 -671
  482. package/resources/harness/bundles/java/codex/scripts/harness_preflight.py +0 -780
  483. package/resources/harness/bundles/java/codex/scripts/harness_service.py +0 -1331
  484. package/resources/harness/bundles/java/cursor/.harness-build.json +0 -6
  485. package/resources/harness/bundles/java/cursor/CONTEXT.md +0 -65
  486. package/resources/harness/bundles/java/cursor/README.md +0 -478
  487. package/resources/harness/bundles/java/cursor/harness-apidoc/SKILL.md +0 -68
  488. package/resources/harness/bundles/java/cursor/harness-apidoc/checklist.md +0 -142
  489. package/resources/harness/bundles/java/cursor/harness-apidoc/reference.md +0 -233
  490. package/resources/harness/bundles/java/cursor/harness-archive/SKILL.md +0 -198
  491. package/resources/harness/bundles/java/cursor/harness-archive/checklist.md +0 -52
  492. package/resources/harness/bundles/java/cursor/harness-archive/reference.md +0 -103
  493. package/resources/harness/bundles/java/cursor/harness-archive/scripts/gen-manifest.ps1 +0 -31
  494. package/resources/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +0 -171
  495. package/resources/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +0 -128
  496. package/resources/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +0 -95
  497. package/resources/harness/bundles/java/cursor/harness-codebase-map/checklist.md +0 -126
  498. package/resources/harness/bundles/java/cursor/harness-codebase-map/reference.md +0 -364
  499. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/ARCHITECTURE.md +0 -57
  500. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/CONCERNS.md +0 -49
  501. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/CONVENTIONS.md +0 -57
  502. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/INTEGRATIONS.md +0 -52
  503. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/STACK.md +0 -55
  504. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/STRUCTURE.md +0 -69
  505. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/TESTING.md +0 -64
  506. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/map-manifest.schema.json +0 -73
  507. package/resources/harness/bundles/java/cursor/harness-codebase-map/templates/map-summary.md +0 -64
  508. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +0 -229
  509. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/design.md +0 -842
  510. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +0 -42
  511. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/mcp-config.example.json +0 -10
  512. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/reference.md +0 -309
  513. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +0 -4082
  514. package/resources/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +0 -273
  515. package/resources/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +0 -147
  516. package/resources/harness/bundles/java/cursor/harness-package/SKILL.md +0 -69
  517. package/resources/harness/bundles/java/cursor/harness-package/checklist.md +0 -322
  518. package/resources/harness/bundles/java/cursor/harness-package/reference.md +0 -210
  519. package/resources/harness/bundles/java/cursor/harness-plan/SKILL.md +0 -109
  520. package/resources/harness/bundles/java/cursor/harness-plan/checklist.md +0 -243
  521. package/resources/harness/bundles/java/cursor/harness-plan/protocols.md +0 -124
  522. package/resources/harness/bundles/java/cursor/harness-plan/reference.md +0 -376
  523. package/resources/harness/bundles/java/cursor/harness-review/SKILL.md +0 -137
  524. package/resources/harness/bundles/java/cursor/harness-review/checklist.md +0 -124
  525. package/resources/harness/bundles/java/cursor/harness-review/protocols.md +0 -68
  526. package/resources/harness/bundles/java/cursor/harness-review/reference.md +0 -86
  527. package/resources/harness/bundles/java/cursor/harness-run/SKILL.md +0 -129
  528. package/resources/harness/bundles/java/cursor/harness-run/checklist.md +0 -250
  529. package/resources/harness/bundles/java/cursor/harness-run/protocols.md +0 -78
  530. package/resources/harness/bundles/java/cursor/harness-run/reference.md +0 -814
  531. package/resources/harness/bundles/java/cursor/harness-submit/SKILL.md +0 -149
  532. package/resources/harness/bundles/java/cursor/harness-submit/checklist.md +0 -407
  533. package/resources/harness/bundles/java/cursor/harness-submit/reference.md +0 -152
  534. package/resources/harness/bundles/java/cursor/harness-sync/SKILL.md +0 -65
  535. package/resources/harness/bundles/java/cursor/harness-sync/reference.md +0 -153
  536. package/resources/harness/bundles/java/cursor/harness-test/SKILL.md +0 -172
  537. package/resources/harness/bundles/java/cursor/harness-test/checklist.md +0 -317
  538. package/resources/harness/bundles/java/cursor/harness-test/pitfalls-java.md +0 -16
  539. package/resources/harness/bundles/java/cursor/harness-test/pitfalls.md +0 -260
  540. package/resources/harness/bundles/java/cursor/harness-test/reference.md +0 -788
  541. package/resources/harness/bundles/java/cursor/protocols/archive-report-protocol.md +0 -175
  542. package/resources/harness/bundles/java/cursor/protocols/evidence-based-reporting-protocol.md +0 -134
  543. package/resources/harness/bundles/java/cursor/protocols/ledger-protocol.md +0 -270
  544. package/resources/harness/bundles/java/cursor/protocols/powershell-protocol.md +0 -181
  545. package/resources/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +0 -150
  546. package/resources/harness/bundles/java/cursor/protocols/sensitive-info-protocol.md +0 -92
  547. package/resources/harness/bundles/java/cursor/protocols/state-layout-protocol.md +0 -131
  548. package/resources/harness/bundles/java/cursor/protocols/submit-protocol.md +0 -78
  549. package/resources/harness/bundles/java/cursor/scripts/harness_archive.py +0 -2319
  550. package/resources/harness/bundles/java/cursor/scripts/harness_deploy.py +0 -674
  551. package/resources/harness/bundles/java/cursor/scripts/harness_events.py +0 -680
  552. package/resources/harness/bundles/java/cursor/scripts/harness_ledger.py +0 -671
  553. package/resources/harness/bundles/java/cursor/scripts/harness_preflight.py +0 -780
  554. package/resources/harness/bundles/java/cursor/scripts/harness_service.py +0 -1331
  555. package/resources/harness/manifests/general/claude-code.json +0 -277
  556. package/resources/harness/manifests/general/codebuddy.json +0 -277
  557. package/resources/harness/manifests/general/codex.json +0 -265
  558. package/resources/harness/manifests/general/cursor.json +0 -265
  559. package/resources/harness/manifests/java/claude-code.json +0 -305
  560. package/resources/harness/manifests/java/codebuddy.json +0 -305
  561. package/resources/harness/manifests/java/codex.json +0 -293
  562. package/resources/harness/manifests/java/cursor.json +0 -293
  563. package/resources/harness/migrations/0.1.1/general.json +0 -358
  564. package/resources/harness/migrations/0.1.1/java.json +0 -398
@@ -1,4082 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Build and query a local Harness knowledge index.
3
-
4
- This script is intentionally dependency-free. It turns existing
5
- .harness/archive/**/reports/final/summary-data.json files into a project-local
6
- .harness/knowledge index with candidate entries, SQLite FTS5, Obsidian views,
7
- and query context packs.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import argparse
13
- import datetime as dt
14
- import hashlib
15
- import json
16
- import os
17
- import re
18
- import shutil
19
- import sqlite3
20
- import subprocess
21
- import sys
22
- from difflib import SequenceMatcher
23
- from pathlib import Path
24
- from typing import Any
25
-
26
-
27
- if hasattr(sys.stdout, "reconfigure"):
28
- sys.stdout.reconfigure(encoding="utf-8")
29
- if hasattr(sys.stderr, "reconfigure"):
30
- sys.stderr.reconfigure(encoding="utf-8")
31
-
32
-
33
- ENTRY_TYPES = {
34
- "requirement",
35
- "decision",
36
- "implementation",
37
- "risk",
38
- "test-evidence",
39
- "pitfall",
40
- "api-contract",
41
- }
42
-
43
- DEFAULT_AUTO_KNOWLEDGE_CONFIG = {
44
- "autoPromote": {
45
- "enabled": True,
46
- "minConfidence": 0.82,
47
- "allowedTypes": ["decision", "api-contract", "requirement", "pitfall"],
48
- "requireValidators": False,
49
- "allowStale": False,
50
- "maxPerRun": 50,
51
- },
52
- "confidence": {
53
- "ttlHalfLifeDays": 45,
54
- "sourceChangePenalty": 0.25,
55
- "stalePenalty": 0.35,
56
- "validatorPassBonus": 0.15,
57
- "validatorFailPenalty": 0.5,
58
- "supersededPenalty": 0.8,
59
- "conflictPenalty": 0.8,
60
- },
61
- }
62
-
63
-
64
- def now_iso() -> str:
65
- return dt.datetime.now().astimezone().isoformat(timespec="seconds")
66
-
67
-
68
- def timestamp() -> str:
69
- return dt.datetime.now().strftime("%Y%m%d-%H%M%S")
70
-
71
-
72
- def read_json(path: Path) -> Any:
73
- return json.loads(path.read_text(encoding="utf-8"))
74
-
75
-
76
- def write_json(path: Path, data: Any) -> None:
77
- path.parent.mkdir(parents=True, exist_ok=True)
78
- path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
79
-
80
-
81
- def write_text(path: Path, text: str) -> None:
82
- path.parent.mkdir(parents=True, exist_ok=True)
83
- path.write_text(text, encoding="utf-8")
84
-
85
-
86
- def json_clone(value: Any) -> Any:
87
- return json.loads(json.dumps(value, ensure_ascii=False))
88
-
89
-
90
- def load_config(knowledge: Path) -> dict[str, Any]:
91
- config_path = knowledge / "config.json"
92
- if not config_path.exists():
93
- return {}
94
- try:
95
- config = read_json(config_path)
96
- except (OSError, json.JSONDecodeError):
97
- return {}
98
- return config if isinstance(config, dict) else {}
99
-
100
-
101
- def ensure_auto_knowledge_config(knowledge: Path) -> dict[str, Any]:
102
- config_path = knowledge / "config.json"
103
- if config_path.exists():
104
- config = load_config(knowledge)
105
- return {
106
- "path": str(config_path),
107
- "created": False,
108
- "autoPromoteEnabled": auto_promote_config(config)["enabled"],
109
- "appliedBy": "existing",
110
- "candidateAutoPromoted": None,
111
- }
112
- config = json_clone(DEFAULT_AUTO_KNOWLEDGE_CONFIG)
113
- write_json(config_path, config)
114
- return {
115
- "path": str(config_path),
116
- "created": True,
117
- "autoPromoteEnabled": True,
118
- "appliedBy": None,
119
- "candidateAutoPromoted": None,
120
- }
121
-
122
-
123
- def active_lifecycle_config(config: dict[str, Any]) -> dict[str, Any]:
124
- raw = config.get("activeLifecycle")
125
- if not isinstance(raw, dict):
126
- return {"autoDemote": False, "targetStatus": "stale"}
127
- target_status = str(raw.get("targetStatus") or "stale")
128
- if target_status not in {"candidate", "stale"}:
129
- target_status = "stale"
130
- return {
131
- "autoDemote": bool(raw.get("autoDemote")),
132
- "targetStatus": target_status,
133
- }
134
-
135
-
136
- def knowledge_validation_config(config: dict[str, Any]) -> dict[str, Any]:
137
- raw = config.get("knowledgeValidation")
138
- if not isinstance(raw, dict):
139
- raw = {}
140
- target_status = str(raw.get("defaultTargetStatus") or "stale")
141
- if target_status not in {"candidate", "stale"}:
142
- target_status = "stale"
143
- return {
144
- "enabled": bool(raw.get("enabled", True)),
145
- "autoDemoteActive": bool(raw.get("autoDemoteActive", False)),
146
- "defaultTargetStatus": target_status,
147
- "allowCommandValidators": bool(raw.get("allowCommandValidators", False)),
148
- "commandTimeoutSeconds": int(raw.get("commandTimeoutSeconds") or 60),
149
- }
150
-
151
-
152
- def confidence_config(config: dict[str, Any]) -> dict[str, Any]:
153
- raw = config.get("confidence")
154
- if not isinstance(raw, dict):
155
- raw = {}
156
- return {
157
- "ttlHalfLifeDays": max(1, int(raw.get("ttlHalfLifeDays") or 45)),
158
- "sourceChangePenalty": float(raw.get("sourceChangePenalty", 0.25)),
159
- "stalePenalty": float(raw.get("stalePenalty", 0.35)),
160
- "validatorPassBonus": float(raw.get("validatorPassBonus", 0.15)),
161
- "validatorFailPenalty": float(raw.get("validatorFailPenalty", 0.5)),
162
- "supersededPenalty": float(raw.get("supersededPenalty", 0.8)),
163
- "conflictPenalty": float(raw.get("conflictPenalty", 0.8)),
164
- }
165
-
166
-
167
- def auto_promote_config(config: dict[str, Any]) -> dict[str, Any]:
168
- raw = config.get("autoPromote")
169
- if not isinstance(raw, dict):
170
- raw = {}
171
- allowed = raw.get("allowedTypes")
172
- if not isinstance(allowed, list):
173
- allowed = ["decision", "api-contract", "requirement", "pitfall"]
174
- allowed_types = [str(item) for item in allowed if str(item) in ENTRY_TYPES]
175
- if not allowed_types:
176
- allowed_types = ["decision", "api-contract", "requirement", "pitfall"]
177
- max_per_run = int(raw.get("maxPerRun") or 50)
178
- return {
179
- "enabled": bool(raw.get("enabled", False)),
180
- "minConfidence": float(raw.get("minConfidence", 0.82)),
181
- "allowedTypes": allowed_types,
182
- "requireValidators": bool(raw.get("requireValidators", False)),
183
- "allowStale": bool(raw.get("allowStale", False)),
184
- "maxPerRun": max(0, max_per_run),
185
- }
186
-
187
-
188
- def sha256_file(path: Path) -> str:
189
- h = hashlib.sha256()
190
- with path.open("rb") as f:
191
- for chunk in iter(lambda: f.read(1024 * 1024), b""):
192
- h.update(chunk)
193
- return h.hexdigest()
194
-
195
-
196
- def short_hash(value: str, length: int = 10) -> str:
197
- return hashlib.sha1(value.encode("utf-8")).hexdigest()[:length]
198
-
199
-
200
- def slugify(value: str, max_len: int = 64) -> str:
201
- value = value.lower().strip()
202
- value = re.sub(r"[^\w\u4e00-\u9fff]+", "-", value, flags=re.UNICODE)
203
- value = re.sub(r"-+", "-", value).strip("-")
204
- if not value:
205
- value = "item"
206
- return value[:max_len].strip("-") or "item"
207
-
208
-
209
- def safe_filename(value: str) -> str:
210
- slug = slugify(value, 72)
211
- return re.sub(r'[<>:"/\\|?*\x00-\x1f]', "-", slug)
212
-
213
-
214
- def entry_filename(entry: dict) -> str:
215
- """生成 entry 文件名:可读前缀(截断安全)+ 完整 10 字符哈希(永不被截)。
216
-
217
- id 形如 ``project.archive.type.hash10``;rsplit 取末段为消歧哈希,
218
- 拼在 safe_filename 截断之外,避免长归档名让 72 上限切掉哈希导致撞名。
219
- 末段不足 6 字符或 id 无 hash 段时回退为 short_hash(id, 10);
220
- 末段含文件系统非法字符时按 safe_filename 同款规则清理,避免非法文件名。
221
- """
222
- entry_id = entry["id"]
223
- parts = entry_id.rsplit(".", 1)
224
- prefix = parts[0] if len(parts) == 2 else entry_id
225
- digest = parts[1] if len(parts) == 2 and len(parts[1]) >= 6 else short_hash(entry_id, 10)
226
- digest = re.sub(r'[<>:"/\\|?*\x00-\x1f]', "-", digest)
227
- return safe_filename(prefix) + "-" + digest + ".json"
228
-
229
-
230
- def rel_to_project(project: Path, path: Path) -> str:
231
- try:
232
- return path.resolve().relative_to(project.resolve()).as_posix()
233
- except ValueError:
234
- return path.as_posix()
235
-
236
-
237
- def project_id(project: Path) -> str:
238
- config = project / ".harness" / "project.yaml"
239
- if config.exists():
240
- text = config.read_text(encoding="utf-8", errors="ignore")
241
- match = re.search(r"(?m)^project[_-]?id:\s*['\"]?([^'\"\n#]+)", text)
242
- if match:
243
- return slugify(match.group(1).strip(), 80)
244
- return slugify(project.name, 80)
245
-
246
-
247
- def git_head(project: Path) -> str | None:
248
- result = run_git(project, ["rev-parse", "HEAD"])
249
- return result.stdout.strip() if result.returncode == 0 else None
250
-
251
-
252
- def is_git_repo(project: Path) -> bool:
253
- result = run_git(project, ["rev-parse", "--is-inside-work-tree"])
254
- return result.returncode == 0 and result.stdout.strip() == "true"
255
-
256
-
257
- def git_commit_exists(project: Path, commit: str) -> bool:
258
- result = run_git(project, ["cat-file", "-e", f"{commit}^{{commit}}"])
259
- return result.returncode == 0
260
-
261
-
262
- def run_git(project: Path, args: list[str]) -> subprocess.CompletedProcess[str]:
263
- try:
264
- return subprocess.run(
265
- ["git", "-C", str(project), *args],
266
- text=True,
267
- stdout=subprocess.PIPE,
268
- stderr=subprocess.PIPE,
269
- timeout=20,
270
- )
271
- except (OSError, subprocess.TimeoutExpired) as exc:
272
- return subprocess.CompletedProcess(args, 1, "", str(exc))
273
-
274
-
275
- def archive_name(summary_path: Path) -> str:
276
- parts = summary_path.parts
277
- try:
278
- idx = parts.index("archive")
279
- return parts[idx + 1]
280
- except (ValueError, IndexError):
281
- return summary_path.parent.name
282
-
283
-
284
- def archive_dir_from_summary(summary_path: Path) -> Path:
285
- archive = archive_name(summary_path)
286
- current = summary_path
287
- for parent in summary_path.parents:
288
- if parent.name == archive and parent.parent.name == "archive":
289
- return parent
290
- current = parent
291
- return current
292
-
293
-
294
- def changed_file_paths(summary: dict[str, Any]) -> list[str]:
295
- files = []
296
- for item in summary.get("changedFiles") or []:
297
- path = item.get("path") if isinstance(item, dict) else None
298
- if path and path not in files:
299
- files.append(str(path))
300
- return files
301
-
302
-
303
- def archive_summary_records(project: Path, summary_paths: list[Path]) -> list[dict[str, Any]]:
304
- records = []
305
- for summary_path in summary_paths:
306
- stat = summary_path.stat()
307
- records.append(
308
- {
309
- "archive": rel_to_project(project, archive_dir_from_summary(summary_path)),
310
- "summaryData": rel_to_project(project, summary_path),
311
- "summarySha256": sha256_file(summary_path),
312
- "mtime": dt.datetime.fromtimestamp(stat.st_mtime, dt.timezone.utc).isoformat(timespec="seconds"),
313
- }
314
- )
315
- return records
316
-
317
-
318
- def make_entry(
319
- *,
320
- project: Path,
321
- project_name: str,
322
- summary_path: Path,
323
- summary_hash: str,
324
- summary: dict[str, Any],
325
- entry_type: str,
326
- title: str,
327
- body: str,
328
- source_files: list[str],
329
- keywords: list[str],
330
- confidence: str = "medium",
331
- ) -> dict[str, Any]:
332
- if entry_type not in ENTRY_TYPES:
333
- raise ValueError(f"unsupported entry type: {entry_type}")
334
-
335
- archive = archive_name(summary_path)
336
- final_commit = str(summary.get("finalCommit") or summary.get("final_commit") or "")
337
- base_commit = str(summary.get("baseCommit") or summary.get("base_commit") or "")
338
- status = "candidate"
339
- stale_reasons: list[str] = []
340
-
341
- if final_commit and is_git_repo(project):
342
- if not git_commit_exists(project, final_commit):
343
- stale_reasons.append(
344
- "source commit missing from local git history: " + final_commit[:12]
345
- )
346
- elif source_files:
347
- diff = run_git(project, ["diff", "--name-only", f"{final_commit}..HEAD", "--", *source_files])
348
- if diff.returncode == 0:
349
- changed = [line.strip() for line in diff.stdout.splitlines() if line.strip()]
350
- if changed:
351
- stale_reasons.append(
352
- "source files changed after source commit: " + ", ".join(changed[:8])
353
- )
354
- else:
355
- detail = first_sentence(diff.stderr.strip() or f"git diff exited {diff.returncode}")
356
- stale_reasons.append("source commit could not be compared with current HEAD: " + detail)
357
-
358
- if stale_reasons:
359
- status = "stale"
360
-
361
- identity = "|".join([project_name, archive, entry_type, title, body[:160]])
362
- entry_id = ".".join([project_name, slugify(archive, 80), entry_type, short_hash(identity)])
363
- archive_dir = archive_dir_from_summary(summary_path)
364
-
365
- return {
366
- "schemaVersion": 1,
367
- "id": entry_id,
368
- "projectId": project_name,
369
- "type": entry_type,
370
- "status": status,
371
- "title": title.strip(),
372
- "summary": first_sentence(body),
373
- "body": body.strip(),
374
- "keywords": sorted({kw for kw in keywords if kw}),
375
- "source": {
376
- "archive": rel_to_project(project, archive_dir),
377
- "summaryData": rel_to_project(project, summary_path),
378
- "summarySha256": summary_hash,
379
- "sourceCommit": final_commit,
380
- "baseCommit": base_commit,
381
- "changeName": str(summary.get("changeName") or archive),
382
- "finalStatus": str(summary.get("finalStatus") or ""),
383
- },
384
- "scope": {
385
- "sourceFiles": source_files,
386
- "staleIfPathsChanged": stale_patterns(source_files),
387
- },
388
- "lifecycle": {
389
- "createdAt": now_iso(),
390
- "verifiedAt": summary.get("archivedAt") or now_iso(),
391
- "lastCheckedAt": now_iso(),
392
- "confidence": confidence,
393
- "supersedes": [],
394
- "supersededBy": None,
395
- "conflictsWith": [],
396
- "staleReasons": stale_reasons,
397
- },
398
- }
399
-
400
-
401
- def first_sentence(text: str) -> str:
402
- clean = re.sub(r"\s+", " ", text).strip()
403
- if len(clean) <= 180:
404
- return clean
405
- return clean[:177].rstrip() + "..."
406
-
407
-
408
- def stale_patterns(files: list[str]) -> list[str]:
409
- patterns = []
410
- for path in files:
411
- if path not in patterns:
412
- patterns.append(path)
413
- parent = str(Path(path).parent).replace("\\", "/")
414
- if parent and parent != ".":
415
- pattern = parent + "/**"
416
- if pattern not in patterns:
417
- patterns.append(pattern)
418
- return patterns
419
-
420
-
421
- def keyword_candidates(summary: dict[str, Any], extra: list[str] | None = None) -> list[str]:
422
- values = [
423
- str(summary.get("changeName") or ""),
424
- str(summary.get("businessGoal") or ""),
425
- str(summary.get("finalStatus") or ""),
426
- ]
427
- values.extend(extra or [])
428
- tokens: list[str] = []
429
- for value in values:
430
- for token in re.findall(r"[\w\u4e00-\u9fff]{2,}", value, flags=re.UNICODE):
431
- tokens.append(token.lower())
432
- return tokens[:40]
433
-
434
-
435
- def extract_entries(project: Path, project_name: str, summary_path: Path) -> list[dict[str, Any]]:
436
- summary = read_json(summary_path)
437
- summary_hash = sha256_file(summary_path)
438
- files = changed_file_paths(summary)
439
- entries: list[dict[str, Any]] = []
440
- goal = str(summary.get("businessGoal") or "").strip()
441
-
442
- if goal:
443
- entries.append(
444
- make_entry(
445
- project=project,
446
- project_name=project_name,
447
- summary_path=summary_path,
448
- summary_hash=summary_hash,
449
- summary=summary,
450
- entry_type="requirement",
451
- title=f"{summary.get('changeName') or archive_name(summary_path)}: {first_sentence(goal)}",
452
- body=goal,
453
- source_files=files,
454
- keywords=keyword_candidates(summary, ["requirement", "goal"]),
455
- confidence="high",
456
- )
457
- )
458
-
459
- for idx, note in enumerate(summary.get("maintenanceNotes") or [], start=1):
460
- text = str(note).strip()
461
- if not text:
462
- continue
463
- entry_type = "decision" if looks_like_decision(text) else "implementation"
464
- entries.append(
465
- make_entry(
466
- project=project,
467
- project_name=project_name,
468
- summary_path=summary_path,
469
- summary_hash=summary_hash,
470
- summary=summary,
471
- entry_type=entry_type,
472
- title=f"{summary.get('changeName') or archive_name(summary_path)} note {idx}: {first_sentence(text)}",
473
- body=text,
474
- source_files=files,
475
- keywords=keyword_candidates(summary, ["maintenance", entry_type]),
476
- confidence="medium",
477
- )
478
- )
479
-
480
- for item in summary.get("changedFiles") or []:
481
- if not isinstance(item, dict) or not item.get("path"):
482
- continue
483
- path = str(item.get("path"))
484
- text = str(item.get("summary") or path)
485
- entry_type = "api-contract" if looks_like_contract_path(path) else "implementation"
486
- entries.append(
487
- make_entry(
488
- project=project,
489
- project_name=project_name,
490
- summary_path=summary_path,
491
- summary_hash=summary_hash,
492
- summary=summary,
493
- entry_type=entry_type,
494
- title=f"{path}: {first_sentence(text)}",
495
- body=f"{path}: {text}",
496
- source_files=[path],
497
- keywords=keyword_candidates(summary, [path, entry_type]),
498
- confidence="medium",
499
- )
500
- )
501
-
502
- for idx, risk in enumerate(summary.get("knownRisks") or [], start=1):
503
- text = str(risk).strip()
504
- if not text:
505
- continue
506
- entries.append(
507
- make_entry(
508
- project=project,
509
- project_name=project_name,
510
- summary_path=summary_path,
511
- summary_hash=summary_hash,
512
- summary=summary,
513
- entry_type="risk",
514
- title=f"{summary.get('changeName') or archive_name(summary_path)} risk {idx}: {first_sentence(text)}",
515
- body=text,
516
- source_files=files,
517
- keywords=keyword_candidates(summary, ["risk"]),
518
- confidence="medium",
519
- )
520
- )
521
-
522
- for idx, action in enumerate(summary.get("manualActions") or [], start=1):
523
- text = str(action).strip()
524
- if not text:
525
- continue
526
- entries.append(
527
- make_entry(
528
- project=project,
529
- project_name=project_name,
530
- summary_path=summary_path,
531
- summary_hash=summary_hash,
532
- summary=summary,
533
- entry_type="risk",
534
- title=f"{summary.get('changeName') or archive_name(summary_path)} manual action {idx}: {first_sentence(text)}",
535
- body="Manual action: " + text,
536
- source_files=files,
537
- keywords=keyword_candidates(summary, ["manual", "action", "risk"]),
538
- confidence="medium",
539
- )
540
- )
541
-
542
- verification = summary.get("verification")
543
- if verification:
544
- body = json.dumps(verification, ensure_ascii=False, indent=2)
545
- entries.append(
546
- make_entry(
547
- project=project,
548
- project_name=project_name,
549
- summary_path=summary_path,
550
- summary_hash=summary_hash,
551
- summary=summary,
552
- entry_type="test-evidence",
553
- title=f"{summary.get('changeName') or archive_name(summary_path)} verification evidence",
554
- body=body,
555
- source_files=files,
556
- keywords=keyword_candidates(summary, ["test", "verification", "evidence"]),
557
- confidence="high",
558
- )
559
- )
560
-
561
- review = summary.get("reviewSummary")
562
- if isinstance(review, dict) and review.get("summary"):
563
- body = str(review.get("summary"))
564
- entries.append(
565
- make_entry(
566
- project=project,
567
- project_name=project_name,
568
- summary_path=summary_path,
569
- summary_hash=summary_hash,
570
- summary=summary,
571
- entry_type="risk",
572
- title=f"{summary.get('changeName') or archive_name(summary_path)} review: {first_sentence(body)}",
573
- body=body,
574
- source_files=files,
575
- keywords=keyword_candidates(summary, ["review", "risk"]),
576
- confidence="medium",
577
- )
578
- )
579
-
580
- return entries
581
-
582
-
583
- def looks_like_decision(text: str) -> bool:
584
- needles = ["不新增", "唯一", "保持", "改为", "取代", "决策", "wontfix", "tradeoff", "instead"]
585
- lower = text.lower()
586
- return any(needle in lower for needle in needles)
587
-
588
-
589
- def looks_like_contract_path(path: str) -> bool:
590
- lower = path.lower()
591
- return any(part in lower for part in ["openapi", "contract", "schema", "protocol", "api"])
592
-
593
-
594
- def reset_generated_knowledge(knowledge: Path) -> None:
595
- for sub in ["entries/candidate", "entries/stale", "entries/conflicted", "entries/superseded"]:
596
- path = knowledge / sub
597
- if path.exists():
598
- if sub in {"entries/candidate", "entries/stale"}:
599
- for entry_path in sorted(path.glob("*.json")):
600
- try:
601
- entry = read_json(entry_path)
602
- except (OSError, json.JSONDecodeError):
603
- entry_path.unlink(missing_ok=True)
604
- continue
605
- if not isinstance(entry, dict) or not entry.get("lifecycle", {}).get("demotedAt"):
606
- entry_path.unlink(missing_ok=True)
607
- else:
608
- shutil.rmtree(path)
609
- for sub in [
610
- "entries/candidate",
611
- "entries/active",
612
- "entries/stale",
613
- "entries/superseded",
614
- "entries/conflicted",
615
- "cache/archive-entries",
616
- "reports",
617
- "views",
618
- "context-packs",
619
- ]:
620
- (knowledge / sub).mkdir(parents=True, exist_ok=True)
621
-
622
-
623
- def load_entries_from_dir(path: Path) -> list[dict[str, Any]]:
624
- entries: list[dict[str, Any]] = []
625
- if not path.exists():
626
- return entries
627
- for entry_path in sorted(path.glob("*.json")):
628
- try:
629
- entry = read_json(entry_path)
630
- except (OSError, json.JSONDecodeError):
631
- continue
632
- if isinstance(entry, dict) and entry.get("id"):
633
- entries.append(entry)
634
- return entries
635
-
636
-
637
- def archive_entry_cache_path(project: Path, knowledge: Path, summary_path: Path, summary_hash: str) -> Path:
638
- rel_summary = rel_to_project(project, summary_path)
639
- cache_id = short_hash(rel_summary + "|" + summary_hash, 20)
640
- filename = f"{safe_filename(archive_name(summary_path))}-{cache_id}.json"
641
- return knowledge / "cache" / "archive-entries" / filename
642
-
643
-
644
- def load_cached_archive_entries(
645
- cache_path: Path,
646
- *,
647
- summary_path: str,
648
- summary_hash: str,
649
- head_commit: str | None,
650
- ) -> list[dict[str, Any]] | None:
651
- if not cache_path.exists():
652
- return None
653
- try:
654
- payload = read_json(cache_path)
655
- except (OSError, json.JSONDecodeError):
656
- return None
657
- if not isinstance(payload, dict):
658
- return None
659
- if payload.get("schemaVersion") != 1:
660
- return None
661
- if payload.get("summaryData") != summary_path:
662
- return None
663
- if payload.get("summarySha256") != summary_hash:
664
- return None
665
- if payload.get("headCommit") != head_commit:
666
- return None
667
- entries = payload.get("entries")
668
- if not isinstance(entries, list):
669
- return None
670
- if not all(isinstance(entry, dict) and entry.get("id") for entry in entries):
671
- return None
672
- return json_clone(entries)
673
-
674
-
675
- def write_cached_archive_entries(
676
- cache_path: Path,
677
- *,
678
- summary_path: str,
679
- summary_hash: str,
680
- head_commit: str | None,
681
- entries: list[dict[str, Any]],
682
- ) -> None:
683
- write_json(
684
- cache_path,
685
- {
686
- "schemaVersion": 1,
687
- "generatedAt": now_iso(),
688
- "summaryData": summary_path,
689
- "summarySha256": summary_hash,
690
- "headCommit": head_commit,
691
- "entries": entries,
692
- },
693
- )
694
-
695
-
696
- def load_preserved_entries(knowledge: Path) -> list[dict[str, Any]]:
697
- active_entries = load_entries_from_dir(knowledge / "entries" / "active")
698
- demoted_entries = [
699
- entry
700
- for status in ["candidate", "stale"]
701
- for entry in load_entries_from_dir(knowledge / "entries" / status)
702
- if entry.get("lifecycle", {}).get("demotedAt")
703
- ]
704
- return active_entries + demoted_entries
705
-
706
-
707
- def combine_generated_with_preserved(knowledge: Path, generated: list[dict[str, Any]]) -> list[dict[str, Any]]:
708
- preserved_entries = load_preserved_entries(knowledge)
709
- preserved_ids = {entry["id"] for entry in preserved_entries}
710
- return preserved_entries + [entry for entry in generated if entry["id"] not in preserved_ids]
711
-
712
-
713
- def archive_sort_key(entry: dict[str, Any]) -> tuple[str, str]:
714
- archive = entry.get("source", {}).get("archive", "")
715
- match = re.search(r"(\d{4}-\d{2}-\d{2})", archive)
716
- return (match.group(1) if match else "", archive)
717
-
718
-
719
- def archive_date(entry: dict[str, Any]) -> dt.date | None:
720
- date_text = archive_sort_key(entry)[0]
721
- if not date_text:
722
- return None
723
- try:
724
- return dt.date.fromisoformat(date_text)
725
- except ValueError:
726
- return None
727
-
728
-
729
- def clamp_score(value: float) -> float:
730
- return max(0.0, min(1.0, value))
731
-
732
-
733
- def confidence_level(score: float) -> str:
734
- if score >= 0.82:
735
- return "high"
736
- if score >= 0.55:
737
- return "medium"
738
- return "low"
739
-
740
-
741
- def entry_age_days(entry: dict[str, Any]) -> int | None:
742
- date = archive_date(entry)
743
- if date is None:
744
- return None
745
- return max(0, (dt.date.today() - date).days)
746
-
747
-
748
- def validation_status(entry: dict[str, Any]) -> str | None:
749
- validation = entry.get("lifecycle", {}).get("validation")
750
- if not isinstance(validation, dict):
751
- return None
752
- status = validation.get("status")
753
- return str(status) if status else None
754
-
755
-
756
- def calculate_confidence(entry: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]:
757
- policy = confidence_config(config)
758
- legacy = str(entry.get("lifecycle", {}).get("confidence") or "medium")
759
- score = {"high": 0.76, "medium": 0.58, "low": 0.36}.get(legacy, 0.58)
760
- signals = [f"base:{legacy}"]
761
-
762
- type_bonus = {
763
- "decision": 0.14,
764
- "api-contract": 0.14,
765
- "requirement": 0.12,
766
- "pitfall": 0.10,
767
- "test-evidence": 0.04,
768
- "implementation": 0.00,
769
- "risk": -0.04,
770
- }.get(str(entry.get("type")), 0.0)
771
- if type_bonus:
772
- score += type_bonus
773
- signals.append(f"type_bonus:{entry.get('type')}:{type_bonus:+.2f}")
774
-
775
- final_status = str(entry.get("source", {}).get("finalStatus") or "").strip().lower()
776
- if final_status in {"ok", "success", "passed", "pass"}:
777
- score += 0.04
778
- signals.append("source_final_status_ok:+0.04")
779
- elif final_status in {"fail", "failed", "error", "warn"}:
780
- score -= 0.08
781
- signals.append("source_final_status_not_ok:-0.08")
782
-
783
- if entry.get("scope", {}).get("sourceFiles"):
784
- score += 0.03
785
- signals.append("has_source_files:+0.03")
786
-
787
- text = normalized_entry_text(entry)
788
- if has_stability_signal(text) or str(entry.get("type")) in {"decision", "api-contract"}:
789
- score += 0.06
790
- signals.append("long_lived_signal:+0.06")
791
-
792
- status = str(entry.get("status") or "")
793
- stale_reasons = entry.get("lifecycle", {}).get("staleReasons") or []
794
- if status == "stale":
795
- score -= policy["stalePenalty"]
796
- signals.append("status_stale_penalty")
797
- if any("source files changed" in str(reason) for reason in stale_reasons):
798
- score -= policy["sourceChangePenalty"]
799
- signals.append("source_change_penalty")
800
- if status == "superseded" or entry.get("lifecycle", {}).get("supersededBy"):
801
- score -= policy["supersededPenalty"]
802
- signals.append("superseded_penalty")
803
- if status == "conflicted" or entry.get("lifecycle", {}).get("conflictsWith"):
804
- score -= policy["conflictPenalty"]
805
- signals.append("conflict_penalty")
806
-
807
- validator_status = validation_status(entry)
808
- if validator_status == "passed":
809
- score += policy["validatorPassBonus"]
810
- signals.append("validator_pass_bonus")
811
- elif validator_status == "failed":
812
- score -= policy["validatorFailPenalty"]
813
- signals.append("validator_fail_penalty")
814
- elif validator_status == "skipped":
815
- score -= 0.05
816
- signals.append("validator_skipped_penalty")
817
-
818
- age_days = entry_age_days(entry)
819
- if age_days is not None:
820
- age_penalty = min(0.25, (age_days / policy["ttlHalfLifeDays"]) * 0.06)
821
- if age_penalty:
822
- score -= age_penalty
823
- signals.append(f"age_penalty:{age_days}d:-{age_penalty:.2f}")
824
-
825
- score = clamp_score(score)
826
- return {
827
- "score": round(score, 3),
828
- "level": confidence_level(score),
829
- "signals": signals,
830
- "lastCalculatedAt": now_iso(),
831
- }
832
-
833
-
834
- def apply_confidence_scores(entries: list[dict[str, Any]], config: dict[str, Any]) -> None:
835
- for entry in entries:
836
- entry["confidence"] = calculate_confidence(entry, config)
837
-
838
-
839
- def should_auto_promote(entry: dict[str, Any], policy: dict[str, Any]) -> bool:
840
- if not policy["enabled"]:
841
- return False
842
- if entry.get("type") not in set(policy["allowedTypes"]):
843
- return False
844
- if entry.get("status") == "stale" and not policy["allowStale"]:
845
- return False
846
- if entry.get("status") not in {"candidate", "stale"}:
847
- return False
848
- if entry.get("status") == "candidate" and entry.get("lifecycle", {}).get("staleReasons"):
849
- return False
850
- confidence = entry.get("confidence") if isinstance(entry.get("confidence"), dict) else {}
851
- if float(confidence.get("score") or 0.0) < policy["minConfidence"]:
852
- return False
853
- if policy["requireValidators"] and validation_status(entry) != "passed":
854
- return False
855
- if entry.get("lifecycle", {}).get("conflictsWith") or entry.get("lifecycle", {}).get("supersededBy"):
856
- return False
857
- return True
858
-
859
-
860
- def apply_auto_promote_policy(entries: list[dict[str, Any]], config: dict[str, Any]) -> list[dict[str, Any]]:
861
- policy = auto_promote_config(config)
862
- if not policy["enabled"] or policy["maxPerRun"] <= 0:
863
- return []
864
- eligible = [entry for entry in entries if should_auto_promote(entry, policy)]
865
- eligible.sort(
866
- key=lambda entry: (
867
- float(entry.get("confidence", {}).get("score") or 0.0),
868
- archive_sort_key(entry),
869
- entry.get("id", ""),
870
- ),
871
- reverse=True,
872
- )
873
- actions: list[dict[str, Any]] = []
874
- for entry in eligible[: policy["maxPerRun"]]:
875
- entry["status"] = "active"
876
- lifecycle = entry.setdefault("lifecycle", {})
877
- score = float(entry.get("confidence", {}).get("score") or 0.0)
878
- note = f"autoPromote: confidence {score:.3f} >= {policy['minConfidence']:.3f}"
879
- lifecycle["promotedAt"] = now_iso()
880
- lifecycle["promotionNote"] = note
881
- lifecycle["autoPromoted"] = True
882
- lifecycle["lastCheckedAt"] = now_iso()
883
- confidence = entry.setdefault("confidence", {})
884
- signals = confidence.setdefault("signals", [])
885
- if isinstance(signals, list):
886
- signals.append("auto_promoted")
887
- actions.append(
888
- {
889
- "id": entry["id"],
890
- "type": entry.get("type"),
891
- "score": score,
892
- "status": "active",
893
- "reason": note,
894
- }
895
- )
896
- return actions
897
-
898
-
899
- def persist_entry_updates(knowledge: Path, entries: list[dict[str, Any]]) -> None:
900
- for entry in entries:
901
- status = str(entry.get("status") or "")
902
- if status not in {"candidate", "active", "stale", "superseded", "conflicted"}:
903
- continue
904
- path = knowledge / "entries" / status / entry_filename(entry)
905
- if path.exists():
906
- write_json(path, entry)
907
-
908
-
909
- def normalized_entry_text(entry: dict[str, Any]) -> str:
910
- return " ".join(
911
- str(value or "")
912
- for value in [
913
- entry.get("title"),
914
- entry.get("summary"),
915
- entry.get("body"),
916
- " ".join(entry.get("keywords") or []),
917
- ]
918
- ).lower()
919
-
920
-
921
- def subject_terms(entry: dict[str, Any]) -> set[str]:
922
- text = normalized_entry_text(entry)
923
- terms = set(re.findall(r"[a-zA-Z_][a-zA-Z0-9_.-]{2,}", text))
924
- stop = {
925
- "the",
926
- "and",
927
- "for",
928
- "with",
929
- "from",
930
- "this",
931
- "that",
932
- "status",
933
- "note",
934
- "risk",
935
- "requirement",
936
- "implementation",
937
- }
938
- return {term for term in terms if term not in stop}
939
-
940
-
941
- def has_replacement_signal(text: str) -> bool:
942
- needles = [
943
- "不再",
944
- "替代",
945
- "取代",
946
- "改为",
947
- "移除",
948
- "废弃",
949
- "deprecated",
950
- "instead",
951
- "replace",
952
- "replaced",
953
- "remove",
954
- "removed",
955
- ]
956
- return any(needle in text for needle in needles)
957
-
958
-
959
- def has_stability_signal(text: str) -> bool:
960
- needles = ["唯一", "复用", "保持", "source of truth", "single source", "only source"]
961
- return any(needle in text for needle in needles)
962
-
963
-
964
- def entries_conflict(left: dict[str, Any], right: dict[str, Any]) -> bool:
965
- comparable_types = {"requirement", "decision", "api-contract"}
966
- if left.get("type") != right.get("type") or left.get("type") not in comparable_types:
967
- return False
968
- if left.get("status") in {"active", "stale", "superseded", "conflicted"}:
969
- return False
970
- if right.get("status") in {"active", "stale", "superseded", "conflicted"}:
971
- return False
972
-
973
- left_files = set(left.get("scope", {}).get("sourceFiles") or [])
974
- right_files = set(right.get("scope", {}).get("sourceFiles") or [])
975
- if not left_files or not right_files or not (left_files & right_files):
976
- return False
977
-
978
- shared_terms = subject_terms(left) & subject_terms(right)
979
- if not shared_terms:
980
- return False
981
-
982
- left_text = normalized_entry_text(left)
983
- right_text = normalized_entry_text(right)
984
- return (
985
- has_replacement_signal(left_text)
986
- and has_stability_signal(right_text)
987
- ) or (
988
- has_replacement_signal(right_text)
989
- and has_stability_signal(left_text)
990
- )
991
-
992
-
993
- def entries_conflict_for_review(left: dict[str, Any], right: dict[str, Any]) -> bool:
994
- comparable_types = {"requirement", "decision", "api-contract"}
995
- if left.get("type") != right.get("type") or left.get("type") not in comparable_types:
996
- return False
997
-
998
- left_files = set(left.get("scope", {}).get("sourceFiles") or [])
999
- right_files = set(right.get("scope", {}).get("sourceFiles") or [])
1000
- if not left_files or not right_files or not (left_files & right_files):
1001
- return False
1002
-
1003
- shared_terms = subject_terms(left) & subject_terms(right)
1004
- if not shared_terms:
1005
- return False
1006
-
1007
- left_text = normalized_entry_text(left)
1008
- right_text = normalized_entry_text(right)
1009
- return (
1010
- has_replacement_signal(left_text)
1011
- and has_stability_signal(right_text)
1012
- ) or (
1013
- has_replacement_signal(right_text)
1014
- and has_stability_signal(left_text)
1015
- )
1016
-
1017
-
1018
- def mark_conflict(left: dict[str, Any], right: dict[str, Any]) -> None:
1019
- for entry, other in [(left, right), (right, left)]:
1020
- entry["status"] = "conflicted"
1021
- lifecycle = entry.setdefault("lifecycle", {})
1022
- conflicts = lifecycle.setdefault("conflictsWith", [])
1023
- if other["id"] not in conflicts:
1024
- conflicts.append(other["id"])
1025
- reasons = lifecycle.setdefault("staleReasons", [])
1026
- reason = "potential conflict with: " + other["id"]
1027
- if reason not in reasons:
1028
- reasons.append(reason)
1029
- lifecycle["lastCheckedAt"] = now_iso()
1030
-
1031
-
1032
- def mark_conflicting_generated_entries(entries: list[dict[str, Any]]) -> None:
1033
- ordered = sorted(entries, key=archive_sort_key)
1034
- for idx, left in enumerate(ordered):
1035
- for right in ordered[idx + 1 :]:
1036
- if entries_conflict(left, right):
1037
- mark_conflict(left, right)
1038
-
1039
-
1040
- def number_value(value: Any) -> int | None:
1041
- if isinstance(value, bool):
1042
- return None
1043
- if isinstance(value, int):
1044
- return value
1045
- if isinstance(value, float):
1046
- return int(value)
1047
- if isinstance(value, str):
1048
- value = value.strip()
1049
- if value.isdigit():
1050
- return int(value)
1051
- return None
1052
-
1053
-
1054
- def parse_pass_rate(value: Any) -> tuple[int | None, int | None]:
1055
- if not isinstance(value, str):
1056
- return (None, None)
1057
- match = re.search(r"(\d+)\s*/\s*(\d+)", value)
1058
- if not match:
1059
- return (None, None)
1060
- return (int(match.group(1)), int(match.group(2)))
1061
-
1062
-
1063
- def collect_verification_metrics(value: Any) -> dict[str, Any]:
1064
- metrics: dict[str, Any] = {
1065
- "failed": 0,
1066
- "passed": 0,
1067
- "total": 0,
1068
- "failedStatus": False,
1069
- }
1070
-
1071
- def visit(node: Any) -> None:
1072
- if isinstance(node, dict):
1073
- local_passed = None
1074
- local_total = None
1075
- local_ratio_found = False
1076
- for key, raw in node.items():
1077
- key_l = str(key).lower()
1078
- if key_l in {"failures", "failed", "failure", "errors", "error"}:
1079
- number = number_value(raw)
1080
- if number is not None:
1081
- metrics["failed"] += number
1082
- elif key_l in {"passed", "pass", "successes", "succeeded"}:
1083
- local_passed = number_value(raw)
1084
- elif key_l in {"total", "run", "tests", "count"}:
1085
- local_total = number_value(raw)
1086
- elif key_l in {"passrate", "pass_rate"}:
1087
- passed, total = parse_pass_rate(raw)
1088
- if passed is not None and total is not None:
1089
- metrics["passed"] += passed
1090
- metrics["total"] += total
1091
- local_ratio_found = True
1092
- elif key_l == "status" and isinstance(raw, str):
1093
- status = raw.strip().lower()
1094
- if status in {"fail", "failed", "failure", "error", "errored", "red"}:
1095
- metrics["failedStatus"] = True
1096
- visit(raw)
1097
- if not local_ratio_found and local_passed is not None and local_total is not None:
1098
- metrics["passed"] += local_passed
1099
- metrics["total"] += local_total
1100
- elif isinstance(node, list):
1101
- for item in node:
1102
- visit(item)
1103
-
1104
- visit(value)
1105
- if metrics["total"]:
1106
- metrics["passRatio"] = metrics["passed"] / metrics["total"]
1107
- else:
1108
- metrics["passRatio"] = None
1109
- return metrics
1110
-
1111
-
1112
- def verification_metrics(entry: dict[str, Any]) -> dict[str, Any] | None:
1113
- try:
1114
- verification = json.loads(entry.get("body", "") or "{}")
1115
- except json.JSONDecodeError:
1116
- return None
1117
- if not isinstance(verification, (dict, list)):
1118
- return None
1119
- return collect_verification_metrics(verification)
1120
-
1121
-
1122
- def verification_is_degraded(old_metrics: dict[str, Any] | None, new_metrics: dict[str, Any] | None) -> bool:
1123
- if not new_metrics:
1124
- return False
1125
- if new_metrics.get("failedStatus") or int(new_metrics.get("failed") or 0) > 0:
1126
- return True
1127
- if not old_metrics:
1128
- return False
1129
- old_ratio = old_metrics.get("passRatio")
1130
- new_ratio = new_metrics.get("passRatio")
1131
- if isinstance(old_ratio, (int, float)) and isinstance(new_ratio, (int, float)):
1132
- return new_ratio < old_ratio
1133
- return False
1134
-
1135
-
1136
- def verification_degradation_detail(metrics: dict[str, Any]) -> str:
1137
- parts: list[str] = []
1138
- if metrics.get("failedStatus"):
1139
- parts.append("failed status")
1140
- failed = int(metrics.get("failed") or 0)
1141
- if failed:
1142
- parts.append(f"failures/errors={failed}")
1143
- ratio = metrics.get("passRatio")
1144
- if isinstance(ratio, (int, float)):
1145
- parts.append(f"passRatio={ratio:.2f}")
1146
- return ", ".join(parts) or "verification metrics worsened"
1147
-
1148
-
1149
- def mark_degraded_test_evidence(entries: list[dict[str, Any]]) -> None:
1150
- ordered = sorted(entries, key=archive_sort_key)
1151
- for idx, older in enumerate(ordered):
1152
- if older.get("type") != "test-evidence":
1153
- continue
1154
- if older.get("status") in {"active", "superseded", "conflicted"}:
1155
- continue
1156
- older_files = set(older.get("scope", {}).get("sourceFiles") or [])
1157
- if not older_files:
1158
- continue
1159
- old_metrics = verification_metrics(older)
1160
- for newer in ordered[idx + 1 :]:
1161
- if newer.get("type") != "test-evidence":
1162
- continue
1163
- if archive_sort_key(newer) <= archive_sort_key(older):
1164
- continue
1165
- newer_files = set(newer.get("scope", {}).get("sourceFiles") or [])
1166
- if not newer_files or not (older_files & newer_files):
1167
- continue
1168
- new_metrics = verification_metrics(newer)
1169
- if not verification_is_degraded(old_metrics, new_metrics):
1170
- continue
1171
- older["status"] = "stale"
1172
- lifecycle = older.setdefault("lifecycle", {})
1173
- reasons = lifecycle.setdefault("staleReasons", [])
1174
- detail = verification_degradation_detail(new_metrics or {})
1175
- reason = "newer verification degraded: " + newer.get("source", {}).get("archive", "")
1176
- if detail:
1177
- reason += f" ({detail})"
1178
- if reason not in reasons:
1179
- reasons.append(reason)
1180
- lifecycle["lastCheckedAt"] = now_iso()
1181
- break
1182
-
1183
-
1184
- def active_review_items(entries: list[dict[str, Any]], limit: int | None = None) -> list[dict[str, Any]]:
1185
- comparable_types = {"requirement", "decision", "implementation", "api-contract"}
1186
- ordered = sorted(entries, key=archive_sort_key)
1187
- items: list[dict[str, Any]] = []
1188
- for active in [entry for entry in ordered if entry.get("status") == "active"]:
1189
- reasons: list[str] = []
1190
- active_files = set(active.get("scope", {}).get("sourceFiles") or [])
1191
- if not active_files:
1192
- continue
1193
- active_metrics = verification_metrics(active) if active.get("type") == "test-evidence" else None
1194
- for newer in ordered:
1195
- if newer.get("id") == active.get("id"):
1196
- continue
1197
- if archive_sort_key(newer) <= archive_sort_key(active):
1198
- continue
1199
- newer_files = set(newer.get("scope", {}).get("sourceFiles") or [])
1200
- if not newer_files or not (active_files & newer_files):
1201
- continue
1202
- if entries_conflict_for_review(active, newer):
1203
- reasons.append(f"requires manual review: potential conflict with {newer['id']}")
1204
- elif active.get("type") in comparable_types and newer.get("type") in comparable_types:
1205
- reasons.append(
1206
- "requires manual review: newer overlapping archive may supersede active entry: "
1207
- + newer.get("source", {}).get("archive", "")
1208
- )
1209
- if active.get("type") == "test-evidence" and newer.get("type") == "test-evidence":
1210
- newer_metrics = verification_metrics(newer)
1211
- if verification_is_degraded(active_metrics, newer_metrics):
1212
- reasons.append(
1213
- "requires manual review: newer verification degraded: "
1214
- + newer.get("source", {}).get("archive", "")
1215
- )
1216
- if reasons:
1217
- item = json_clone(active)
1218
- item["reviewReasons"] = sorted(set(reasons))
1219
- items.append(item)
1220
- ranked = sorted(items, key=lambda entry: (len(entry.get("reviewReasons") or []), archive_sort_key(entry)), reverse=True)
1221
- return ranked[:limit] if limit is not None else ranked
1222
-
1223
-
1224
- def apply_active_lifecycle_policy(
1225
- knowledge: Path,
1226
- entries: list[dict[str, Any]],
1227
- config: dict[str, Any],
1228
- ) -> list[dict[str, Any]]:
1229
- policy = active_lifecycle_config(config)
1230
- if not policy["autoDemote"]:
1231
- return []
1232
-
1233
- target_status = policy["targetStatus"]
1234
- actions: list[dict[str, Any]] = []
1235
- for review_entry in active_review_items(entries):
1236
- found = find_entry_file(knowledge, review_entry["id"], ["active"])
1237
- if found is None:
1238
- continue
1239
- source_path, entry = found
1240
- reasons = review_entry.get("reviewReasons") or []
1241
- reason = "activeLifecycle auto-demotion"
1242
- if reasons:
1243
- reason += ": " + str(reasons[0])
1244
- entry["status"] = target_status
1245
- lifecycle = entry.setdefault("lifecycle", {})
1246
- lifecycle["demotedAt"] = now_iso()
1247
- lifecycle["demotionReason"] = reason
1248
- lifecycle["autoDemoted"] = True
1249
- lifecycle["lastCheckedAt"] = now_iso()
1250
- if target_status == "stale":
1251
- stale_reasons = lifecycle.setdefault("staleReasons", [])
1252
- stale_reason = "auto demotion: " + reason
1253
- if stale_reason not in stale_reasons:
1254
- stale_reasons.append(stale_reason)
1255
- target_path = knowledge / "entries" / target_status / entry_filename(entry)
1256
- write_json(target_path, entry)
1257
- if source_path != target_path and source_path.exists():
1258
- source_path.unlink()
1259
- actions.append(
1260
- {
1261
- "id": entry["id"],
1262
- "status": target_status,
1263
- "reason": reason,
1264
- "path": str(target_path),
1265
- }
1266
- )
1267
- return actions
1268
-
1269
-
1270
- def load_entry_files(
1271
- knowledge: Path,
1272
- statuses: list[str] | None = None,
1273
- ) -> list[tuple[Path, dict[str, Any]]]:
1274
- selected = statuses or ["candidate", "active", "stale", "superseded", "conflicted"]
1275
- found: list[tuple[Path, dict[str, Any]]] = []
1276
- for status in selected:
1277
- for path in sorted((knowledge / "entries" / status).glob("*.json")):
1278
- try:
1279
- entry = read_json(path)
1280
- except (OSError, json.JSONDecodeError):
1281
- continue
1282
- if isinstance(entry, dict) and entry.get("id"):
1283
- found.append((path, entry))
1284
- return found
1285
-
1286
-
1287
- def safe_project_path(project: Path, rel_path: str) -> Path | None:
1288
- if not rel_path:
1289
- return None
1290
- candidate = (project / rel_path).resolve()
1291
- try:
1292
- candidate.relative_to(project.resolve())
1293
- except ValueError:
1294
- return None
1295
- return candidate
1296
-
1297
-
1298
- def validator_result(
1299
- validator: dict[str, Any],
1300
- status: str,
1301
- message: str,
1302
- ) -> dict[str, Any]:
1303
- result = {
1304
- "type": str(validator.get("type") or "unknown"),
1305
- "status": status,
1306
- "message": message,
1307
- }
1308
- description = validator.get("description")
1309
- if description:
1310
- result["description"] = str(description)
1311
- for key in ["path", "pattern", "symbol", "command"]:
1312
- if key in validator:
1313
- result[key] = validator[key]
1314
- return result
1315
-
1316
-
1317
- def read_project_text(project: Path, rel_path: str) -> tuple[Path | None, str | None, str | None]:
1318
- path = safe_project_path(project, rel_path)
1319
- if path is None:
1320
- return None, None, f"path escapes project root: {rel_path}"
1321
- if not path.exists():
1322
- return path, None, f"file missing: {rel_path}"
1323
- if not path.is_file():
1324
- return path, None, f"path is not a file: {rel_path}"
1325
- try:
1326
- return path, path.read_text(encoding="utf-8", errors="ignore"), None
1327
- except OSError as exc:
1328
- return path, None, f"file could not be read: {exc}"
1329
-
1330
-
1331
- def evaluate_file_exists(project: Path, validator: dict[str, Any]) -> dict[str, Any]:
1332
- rel_path = str(validator.get("path") or "")
1333
- path = safe_project_path(project, rel_path)
1334
- if path is None:
1335
- return validator_result(validator, "failed", f"path escapes project root: {rel_path}")
1336
- if path.exists():
1337
- return validator_result(validator, "passed", f"file exists: {rel_path}")
1338
- return validator_result(validator, "failed", f"file missing: {rel_path}")
1339
-
1340
-
1341
- def evaluate_file_contains(project: Path, validator: dict[str, Any]) -> dict[str, Any]:
1342
- rel_path = str(validator.get("path") or "")
1343
- pattern = str(validator.get("pattern") or "")
1344
- if not pattern:
1345
- return validator_result(validator, "failed", "file_contains validator requires pattern")
1346
- _, text, error = read_project_text(project, rel_path)
1347
- if error is not None:
1348
- return validator_result(validator, "failed", error)
1349
- if pattern in (text or ""):
1350
- return validator_result(validator, "passed", f"file contains pattern: {rel_path}")
1351
- return validator_result(validator, "failed", f"pattern not found in file: {rel_path}")
1352
-
1353
-
1354
- def evaluate_symbol_exists(project: Path, entry: dict[str, Any], validator: dict[str, Any]) -> dict[str, Any]:
1355
- symbol = str(validator.get("symbol") or "")
1356
- if not symbol:
1357
- return validator_result(validator, "failed", "symbol_exists validator requires symbol")
1358
- raw_files = validator.get("files")
1359
- files = raw_files if isinstance(raw_files, list) else entry.get("scope", {}).get("sourceFiles") or []
1360
- files = [str(path) for path in files if path]
1361
- if not files:
1362
- return validator_result(validator, "failed", "symbol_exists validator requires files or sourceFiles")
1363
- checked: list[str] = []
1364
- missing: list[str] = []
1365
- for rel_path in files:
1366
- checked.append(rel_path)
1367
- _, text, error = read_project_text(project, rel_path)
1368
- if error is not None:
1369
- missing.append(error)
1370
- continue
1371
- if symbol in (text or ""):
1372
- return validator_result(validator, "passed", f"symbol found in {rel_path}")
1373
- detail = "; ".join(missing[:3]) if missing else "symbol not found"
1374
- return validator_result(validator, "failed", f"{detail}; checked: {', '.join(checked[:8])}")
1375
-
1376
-
1377
- def evaluate_command_validator(project: Path, validator: dict[str, Any], config: dict[str, Any]) -> dict[str, Any]:
1378
- if not config["allowCommandValidators"]:
1379
- return validator_result(validator, "skipped", "command validators disabled by knowledgeValidation.allowCommandValidators")
1380
- command = validator.get("command")
1381
- if not isinstance(command, list) or not all(isinstance(part, str) for part in command):
1382
- return validator_result(validator, "failed", "command validator requires command as a string array")
1383
- timeout = max(1, int(config.get("commandTimeoutSeconds") or 60))
1384
- try:
1385
- completed = subprocess.run(
1386
- command,
1387
- cwd=str(project),
1388
- text=True,
1389
- encoding="utf-8",
1390
- stdout=subprocess.PIPE,
1391
- stderr=subprocess.PIPE,
1392
- timeout=timeout,
1393
- )
1394
- except subprocess.TimeoutExpired:
1395
- return validator_result(validator, "failed", f"command timed out after {timeout}s")
1396
- except OSError as exc:
1397
- return validator_result(validator, "failed", f"command could not be started: {exc}")
1398
- if completed.returncode == 0:
1399
- return validator_result(validator, "passed", "command exited 0")
1400
- detail = first_sentence((completed.stderr or completed.stdout or "").strip())
1401
- return validator_result(validator, "failed", f"command exited {completed.returncode}: {detail}")
1402
-
1403
-
1404
- def evaluate_validator(
1405
- project: Path,
1406
- entry: dict[str, Any],
1407
- validator: dict[str, Any],
1408
- config: dict[str, Any],
1409
- ) -> dict[str, Any]:
1410
- validator_type = str(validator.get("type") or "")
1411
- if validator_type == "file_exists":
1412
- return evaluate_file_exists(project, validator)
1413
- if validator_type == "file_contains":
1414
- return evaluate_file_contains(project, validator)
1415
- if validator_type == "symbol_exists":
1416
- return evaluate_symbol_exists(project, entry, validator)
1417
- if validator_type == "command":
1418
- return evaluate_command_validator(project, validator, config)
1419
- return validator_result(validator, "failed", f"unknown validator type: {validator_type or 'missing'}")
1420
-
1421
-
1422
- def evaluate_entry_validators(
1423
- project: Path,
1424
- entry: dict[str, Any],
1425
- config: dict[str, Any],
1426
- ) -> dict[str, Any] | None:
1427
- validators = entry.get("validators")
1428
- if not isinstance(validators, list) or not validators:
1429
- return None
1430
- results = [
1431
- evaluate_validator(project, entry, validator, config)
1432
- if isinstance(validator, dict)
1433
- else {"type": "invalid", "status": "failed", "message": "validator must be an object"}
1434
- for validator in validators
1435
- ]
1436
- statuses = [str(result.get("status")) for result in results]
1437
- if "failed" in statuses:
1438
- status = "failed"
1439
- elif "passed" in statuses:
1440
- status = "passed"
1441
- else:
1442
- status = "skipped"
1443
- validation = {
1444
- "validatedAt": now_iso(),
1445
- "status": status,
1446
- "results": results,
1447
- }
1448
- lifecycle = entry.setdefault("lifecycle", {})
1449
- lifecycle["validation"] = validation
1450
- lifecycle["lastCheckedAt"] = validation["validatedAt"]
1451
- return validation
1452
-
1453
-
1454
- def first_failed_validator_message(validation: dict[str, Any]) -> str:
1455
- for result in validation.get("results") or []:
1456
- if isinstance(result, dict) and result.get("status") == "failed":
1457
- description = result.get("description")
1458
- message = result.get("message")
1459
- if description and message:
1460
- return f"{description}: {message}"
1461
- return str(message or description or "validator failed")
1462
- return "validator failed"
1463
-
1464
-
1465
- def apply_knowledge_validation(
1466
- project: Path,
1467
- knowledge: Path,
1468
- config: dict[str, Any],
1469
- ) -> dict[str, Any]:
1470
- policy = knowledge_validation_config(config)
1471
- summary: dict[str, Any] = {
1472
- "enabled": policy["enabled"],
1473
- "checked": 0,
1474
- "passed": 0,
1475
- "failed": 0,
1476
- "skipped": 0,
1477
- "autoDemoted": 0,
1478
- "entries": [],
1479
- }
1480
- if not policy["enabled"]:
1481
- return summary
1482
-
1483
- for source_path, entry in load_entry_files(knowledge):
1484
- validation = evaluate_entry_validators(project, entry, policy)
1485
- if validation is None:
1486
- continue
1487
- summary["checked"] += 1
1488
- summary[validation["status"]] += 1
1489
- entry_summary = {
1490
- "id": entry["id"],
1491
- "status": entry.get("status"),
1492
- "validationStatus": validation["status"],
1493
- "results": validation["results"],
1494
- }
1495
- if validation["status"] == "failed" and entry.get("status") == "active" and policy["autoDemoteActive"]:
1496
- target_status = policy["defaultTargetStatus"]
1497
- reason = "validator failed: " + first_failed_validator_message(validation)
1498
- lifecycle = entry.setdefault("lifecycle", {})
1499
- lifecycle["previousStatus"] = str(entry.get("status") or "active")
1500
- entry["status"] = target_status
1501
- lifecycle["demotedAt"] = now_iso()
1502
- lifecycle["demotionReason"] = reason
1503
- lifecycle["autoDemoted"] = True
1504
- stale_reasons = lifecycle.setdefault("staleReasons", [])
1505
- if reason not in stale_reasons:
1506
- stale_reasons.append(reason)
1507
- target_path = knowledge / "entries" / target_status / entry_filename(entry)
1508
- write_json(target_path, entry)
1509
- if source_path != target_path and source_path.exists():
1510
- source_path.unlink()
1511
- summary["autoDemoted"] += 1
1512
- entry_summary["status"] = target_status
1513
- entry_summary["autoDemoted"] = True
1514
- else:
1515
- write_json(source_path, entry)
1516
- summary["entries"].append(entry_summary)
1517
- return summary
1518
-
1519
-
1520
- def supersede_overlapping_generated_entries(entries: list[dict[str, Any]]) -> None:
1521
- comparable_types = {"requirement", "decision", "implementation", "api-contract"}
1522
- ordered = sorted(entries, key=archive_sort_key)
1523
- for idx, older in enumerate(ordered):
1524
- if older.get("status") in {"active", "conflicted"} or older.get("type") not in comparable_types:
1525
- continue
1526
- older_files = set(older.get("scope", {}).get("sourceFiles") or [])
1527
- if not older_files:
1528
- continue
1529
- for newer in ordered[idx + 1 :]:
1530
- if newer.get("status") == "conflicted":
1531
- continue
1532
- if newer.get("type") not in comparable_types:
1533
- continue
1534
- if archive_sort_key(newer) <= archive_sort_key(older):
1535
- continue
1536
- newer_files = set(newer.get("scope", {}).get("sourceFiles") or [])
1537
- overlap = sorted(older_files & newer_files)
1538
- if not overlap:
1539
- continue
1540
- older["status"] = "superseded"
1541
- lifecycle = older.setdefault("lifecycle", {})
1542
- lifecycle["supersededBy"] = newer["id"]
1543
- reasons = lifecycle.setdefault("staleReasons", [])
1544
- reason = "overlapped by newer archive: " + newer.get("source", {}).get("archive", "")
1545
- if reason not in reasons:
1546
- reasons.append(reason)
1547
- lifecycle["lastCheckedAt"] = now_iso()
1548
- break
1549
-
1550
-
1551
- def ttl_days_from_config(config: dict[str, Any]) -> int | None:
1552
- raw = config.get("staleTtlDays")
1553
- if raw is None:
1554
- return None
1555
- try:
1556
- ttl_days = int(raw)
1557
- except (TypeError, ValueError):
1558
- return None
1559
- return ttl_days if ttl_days > 0 else None
1560
-
1561
-
1562
- def apply_ttl_stale(entries: list[dict[str, Any]], config: dict[str, Any]) -> None:
1563
- ttl_days = ttl_days_from_config(config)
1564
- if ttl_days is None:
1565
- return
1566
-
1567
- today = dt.date.today()
1568
- for entry in entries:
1569
- if entry.get("status") in {"active", "superseded", "conflicted"}:
1570
- continue
1571
- archived_on = archive_date(entry)
1572
- if archived_on is None:
1573
- continue
1574
- age_days = (today - archived_on).days
1575
- if age_days <= ttl_days:
1576
- continue
1577
-
1578
- entry["status"] = "stale"
1579
- lifecycle = entry.setdefault("lifecycle", {})
1580
- reasons = lifecycle.setdefault("staleReasons", [])
1581
- reason = f"ttl expired: archive age {age_days} days exceeds staleTtlDays {ttl_days}"
1582
- if reason not in reasons:
1583
- reasons.append(reason)
1584
- lifecycle["lastCheckedAt"] = now_iso()
1585
-
1586
-
1587
- def build_index(project: Path, incremental: bool = True) -> dict[str, Any]:
1588
- project = project.resolve()
1589
- harness = project / ".harness"
1590
- archive_root = harness / "archive"
1591
- knowledge = harness / "knowledge"
1592
- pname = project_id(project)
1593
- config = load_config(knowledge)
1594
- current_head = git_head(project)
1595
- reset_generated_knowledge(knowledge)
1596
-
1597
- summary_paths = sorted(archive_root.glob("*/reports/final/summary-data.json"))
1598
- entries: list[dict[str, Any]] = []
1599
- failures: list[dict[str, str]] = []
1600
- ingest_mode: dict[str, Any] = {
1601
- "incremental": incremental,
1602
- "archivesExtracted": 0,
1603
- "archivesReused": 0,
1604
- "cacheWrites": 0,
1605
- "activeAutoDemoted": 0,
1606
- "confidenceScored": 0,
1607
- "candidateAutoPromoted": 0,
1608
- "validationChecked": 0,
1609
- "validationFailed": 0,
1610
- "validationAutoDemoted": 0,
1611
- }
1612
-
1613
- for summary_path in summary_paths:
1614
- summary_rel = rel_to_project(project, summary_path)
1615
- summary_hash = sha256_file(summary_path)
1616
- cache_path = archive_entry_cache_path(project, knowledge, summary_path, summary_hash)
1617
- try:
1618
- archive_entries = None
1619
- if incremental:
1620
- archive_entries = load_cached_archive_entries(
1621
- cache_path,
1622
- summary_path=summary_rel,
1623
- summary_hash=summary_hash,
1624
- head_commit=current_head,
1625
- )
1626
- if archive_entries is None:
1627
- archive_entries = extract_entries(project, pname, summary_path)
1628
- ingest_mode["archivesExtracted"] += 1
1629
- if incremental:
1630
- write_cached_archive_entries(
1631
- cache_path,
1632
- summary_path=summary_rel,
1633
- summary_hash=summary_hash,
1634
- head_commit=current_head,
1635
- entries=archive_entries,
1636
- )
1637
- ingest_mode["cacheWrites"] += 1
1638
- else:
1639
- ingest_mode["archivesReused"] += 1
1640
- entries.extend(archive_entries)
1641
- except Exception as exc: # keep one bad archive from blocking the index
1642
- failures.append({"path": rel_to_project(project, summary_path), "error": str(exc)})
1643
-
1644
- seen: set[str] = set()
1645
- deduped: list[dict[str, Any]] = []
1646
- duplicates = 0
1647
- for entry in entries:
1648
- fingerprint = (
1649
- entry["type"],
1650
- entry["title"],
1651
- entry["body"],
1652
- entry["source"]["archive"],
1653
- )
1654
- fp = json.dumps(fingerprint, ensure_ascii=False, sort_keys=True)
1655
- if fp in seen:
1656
- duplicates += 1
1657
- continue
1658
- seen.add(fp)
1659
- deduped.append(entry)
1660
-
1661
- near_dedupe = dedupe_near_duplicates(deduped)
1662
- ingest_mode["nearDuplicatesMerged"] = near_dedupe["merged"]
1663
-
1664
- mark_conflicting_generated_entries(deduped)
1665
- mark_degraded_test_evidence(deduped)
1666
- supersede_overlapping_generated_entries(deduped)
1667
- apply_ttl_stale(deduped, config)
1668
- apply_confidence_scores(deduped, config)
1669
- auto_promotions = apply_auto_promote_policy(deduped, config)
1670
- ingest_mode["confidenceScored"] = len(deduped)
1671
- ingest_mode["candidateAutoPromoted"] = len(auto_promotions)
1672
-
1673
- preserved_ids = {entry["id"] for entry in load_preserved_entries(knowledge)}
1674
- for entry in deduped:
1675
- if entry["id"] in preserved_ids:
1676
- continue
1677
- status = entry["status"]
1678
- filename = entry_filename(entry)
1679
- target = knowledge / "entries" / status / filename
1680
- if target.exists():
1681
- try:
1682
- existing = read_json(target)
1683
- except (OSError, json.JSONDecodeError):
1684
- existing = None
1685
- if isinstance(existing, dict) and existing.get("id") != entry["id"]:
1686
- failures.append({"id": entry["id"], "reason": "filename collision",
1687
- "path": str(target), "conflictsWith": existing.get("id")})
1688
- continue
1689
- write_json(target, entry)
1690
-
1691
- indexed_entries = combine_generated_with_preserved(knowledge, deduped)
1692
- apply_confidence_scores(indexed_entries, config)
1693
- persist_entry_updates(knowledge, indexed_entries)
1694
- auto_demotions = apply_active_lifecycle_policy(knowledge, indexed_entries, config)
1695
- ingest_mode["activeAutoDemoted"] = len(auto_demotions)
1696
- validation = apply_knowledge_validation(project, knowledge, config)
1697
- ingest_mode["validationChecked"] = validation["checked"]
1698
- ingest_mode["validationFailed"] = validation["failed"]
1699
- ingest_mode["validationAutoDemoted"] = validation["autoDemoted"]
1700
- if auto_demotions:
1701
- indexed_entries = combine_generated_with_preserved(knowledge, deduped)
1702
- if validation["checked"]:
1703
- indexed_entries = [entry for _, entry in load_entry_files(knowledge)]
1704
- apply_confidence_scores(indexed_entries, config)
1705
- persist_entry_updates(knowledge, indexed_entries)
1706
- ingest_mode["confidenceScored"] = len(indexed_entries)
1707
- archive_records = archive_summary_records(project, summary_paths)
1708
- write_sqlite(knowledge / "index.sqlite", indexed_entries)
1709
- index = make_manifest(
1710
- project,
1711
- pname,
1712
- summary_paths,
1713
- archive_records,
1714
- indexed_entries,
1715
- failures,
1716
- duplicates,
1717
- ingest_mode,
1718
- )
1719
- write_json(knowledge / "index.json", index)
1720
- write_views(knowledge, index, indexed_entries)
1721
- write_ingest_report(knowledge, index, failures, duplicates)
1722
- return index
1723
-
1724
-
1725
- def find_entry_file(knowledge: Path, entry_id: str, statuses: list[str]) -> tuple[Path, dict[str, Any]] | None:
1726
- for status in statuses:
1727
- for path in sorted((knowledge / "entries" / status).glob("*.json")):
1728
- try:
1729
- entry = read_json(path)
1730
- except (OSError, json.JSONDecodeError):
1731
- continue
1732
- if isinstance(entry, dict) and entry.get("id") == entry_id:
1733
- return path, entry
1734
- return None
1735
-
1736
-
1737
- def promote_entry(project: Path, entry_id: str, note: str, allow_stale: bool = False) -> dict[str, Any]:
1738
- project = project.resolve()
1739
- knowledge = project / ".harness" / "knowledge"
1740
- if not (knowledge / "index.json").exists():
1741
- build_index(project)
1742
-
1743
- statuses = ["candidate"]
1744
- if allow_stale:
1745
- statuses.append("stale")
1746
- found = find_entry_file(knowledge, entry_id, statuses)
1747
- if found is None:
1748
- stale_found = find_entry_file(knowledge, entry_id, ["stale"])
1749
- if stale_found is not None and not allow_stale:
1750
- raise ValueError("entry is stale; rerun with --allow-stale only after manual verification")
1751
- raise ValueError(f"entry not found in promotable statuses: {entry_id}")
1752
-
1753
- source_path, entry = found
1754
- entry["status"] = "active"
1755
- lifecycle = entry.setdefault("lifecycle", {})
1756
- lifecycle["promotedAt"] = now_iso()
1757
- lifecycle["promotionNote"] = note
1758
- lifecycle["lastCheckedAt"] = now_iso()
1759
- active_path = knowledge / "entries" / "active" / entry_filename(entry)
1760
- write_json(active_path, entry)
1761
- if source_path != active_path and source_path.exists():
1762
- source_path.unlink()
1763
- index = build_index(project)
1764
- return {
1765
- "id": entry["id"],
1766
- "status": "active",
1767
- "activePath": str(active_path),
1768
- "index": str(knowledge / "index.json"),
1769
- "stats": index["stats"],
1770
- }
1771
-
1772
-
1773
- def demote_entry(project: Path, entry_id: str, target_status: str, reason: str) -> dict[str, Any]:
1774
- if target_status not in {"candidate", "stale"}:
1775
- raise ValueError("demote target status must be candidate or stale")
1776
- project = project.resolve()
1777
- knowledge = project / ".harness" / "knowledge"
1778
- if not (knowledge / "index.json").exists():
1779
- build_index(project)
1780
-
1781
- found = find_entry_file(knowledge, entry_id, ["active"])
1782
- if found is None:
1783
- raise ValueError(f"active entry not found: {entry_id}")
1784
-
1785
- source_path, entry = found
1786
- previous = str(entry.get("status") or "active")
1787
- entry["status"] = target_status
1788
- lifecycle = entry.setdefault("lifecycle", {})
1789
- lifecycle["previousStatus"] = previous
1790
- lifecycle["demotedAt"] = now_iso()
1791
- lifecycle["demotionReason"] = reason
1792
- lifecycle["lastCheckedAt"] = now_iso()
1793
- if target_status == "stale":
1794
- stale_reasons = lifecycle.setdefault("staleReasons", [])
1795
- stale_reason = "manual demotion: " + reason
1796
- if stale_reason not in stale_reasons:
1797
- stale_reasons.append(stale_reason)
1798
-
1799
- target_path = knowledge / "entries" / target_status / entry_filename(entry)
1800
- write_json(target_path, entry)
1801
- if source_path != target_path and source_path.exists():
1802
- source_path.unlink()
1803
- index = build_index(project)
1804
- return {
1805
- "id": entry["id"],
1806
- "status": target_status,
1807
- "path": str(target_path),
1808
- "index": str(knowledge / "index.json"),
1809
- "stats": index["stats"],
1810
- }
1811
-
1812
-
1813
- def read_sqlite_entries(sqlite_path: Path) -> list[dict[str, Any]]:
1814
- con = sqlite3.connect(sqlite_path)
1815
- try:
1816
- rows = con.execute("select entry_json from entries").fetchall()
1817
- return [json.loads(row[0]) for row in rows]
1818
- finally:
1819
- con.close()
1820
-
1821
-
1822
- def load_indexed_entries(project: Path) -> list[dict[str, Any]]:
1823
- project = project.resolve()
1824
- sqlite_path = project / ".harness" / "knowledge" / "index.sqlite"
1825
- if not sqlite_path.exists():
1826
- build_index(project)
1827
- return read_sqlite_entries(sqlite_path)
1828
-
1829
-
1830
- def audit_entries(project: Path, limit: int = 10) -> dict[str, Any]:
1831
- project = project.resolve()
1832
- knowledge = project / ".harness" / "knowledge"
1833
- sync = sync_status(project)
1834
- if not sync["upToDate"]:
1835
- build_index(project)
1836
- entries = load_indexed_entries(project)
1837
- candidates = sorted(
1838
- [entry for entry in entries if entry["status"] == "candidate"],
1839
- key=lambda entry: score_entry(entry, " ".join(entry.get("keywords") or [])),
1840
- reverse=True,
1841
- )[:limit]
1842
- stale = sorted(
1843
- [entry for entry in entries if entry["status"] == "stale"],
1844
- key=lambda entry: (len(entry.get("lifecycle", {}).get("staleReasons") or []), score_entry(entry, "")),
1845
- reverse=True,
1846
- )[:limit]
1847
- superseded = sorted(
1848
- [entry for entry in entries if entry["status"] == "superseded"],
1849
- key=archive_sort_key,
1850
- reverse=True,
1851
- )[:limit]
1852
- conflicted = sorted(
1853
- [entry for entry in entries if entry["status"] == "conflicted"],
1854
- key=archive_sort_key,
1855
- reverse=True,
1856
- )[:limit]
1857
- active_review = active_review_items(entries, limit)
1858
- report_path = knowledge / "reports" / f"audit-report-{timestamp()}.md"
1859
- lines = [
1860
- "# Harness Knowledge Audit Report",
1861
- "",
1862
- f"- generatedAt: {now_iso()}",
1863
- f"- project: `{project}`",
1864
- f"- limit: {limit}",
1865
- "",
1866
- "## Candidate Review",
1867
- "",
1868
- ]
1869
- if not candidates:
1870
- lines.append("No candidate entries found.")
1871
- for entry in candidates:
1872
- lines.extend(render_audit_entry(entry))
1873
- lines.extend(["", "## Stale Review", ""])
1874
- if not stale:
1875
- lines.append("No stale entries found.")
1876
- for entry in stale:
1877
- lines.extend(render_audit_entry(entry, include_reasons=True))
1878
- lines.extend(["", "## Superseded Review", ""])
1879
- if not superseded:
1880
- lines.append("No superseded entries found.")
1881
- for entry in superseded:
1882
- lines.extend(render_audit_entry(entry, include_reasons=True))
1883
- lines.extend(["", "## Conflict Review", ""])
1884
- if not conflicted:
1885
- lines.append("No conflicted entries found.")
1886
- for entry in conflicted:
1887
- lines.extend(render_audit_entry(entry, include_reasons=True, include_conflicts=True))
1888
- lines.extend(["", "## Active Review", ""])
1889
- if not active_review:
1890
- lines.append("No active entries require manual review.")
1891
- for entry in active_review:
1892
- lines.extend(render_audit_entry(entry, include_review=True))
1893
- write_text(report_path, "\n".join(lines) + "\n")
1894
- return {
1895
- "project": str(project),
1896
- "limit": limit,
1897
- "report": str(report_path),
1898
- "candidateReview": [audit_summary(entry) for entry in candidates],
1899
- "staleReview": [audit_summary(entry) for entry in stale],
1900
- "supersededReview": [audit_summary(entry) for entry in superseded],
1901
- "conflictReview": [audit_summary(entry) for entry in conflicted],
1902
- "activeReview": [audit_summary(entry) for entry in active_review],
1903
- }
1904
-
1905
-
1906
- def audit_summary(entry: dict[str, Any]) -> dict[str, Any]:
1907
- summary = {
1908
- "id": entry["id"],
1909
- "type": entry["type"],
1910
- "status": entry["status"],
1911
- "title": entry["title"],
1912
- "sourceArchive": entry["source"]["archive"],
1913
- "sourceFiles": entry["scope"]["sourceFiles"],
1914
- }
1915
- if entry.get("reviewReasons"):
1916
- summary["reviewReasons"] = entry["reviewReasons"]
1917
- return summary
1918
-
1919
-
1920
- def render_audit_entry(
1921
- entry: dict[str, Any],
1922
- include_reasons: bool = False,
1923
- include_conflicts: bool = False,
1924
- include_review: bool = False,
1925
- ) -> list[str]:
1926
- lines = [
1927
- f"- {entry['title']}",
1928
- f" - id: `{entry['id']}`",
1929
- f" - type/status: `{entry['type']}` / `{entry['status']}`",
1930
- f" - source: `{entry['source']['archive']}`",
1931
- ]
1932
- if entry["scope"]["sourceFiles"]:
1933
- lines.append(" - files: " + ", ".join(f"`{path}`" for path in entry["scope"]["sourceFiles"][:6]))
1934
- if include_reasons:
1935
- for reason in entry.get("lifecycle", {}).get("staleReasons") or []:
1936
- lines.append(f" - reason: {reason}")
1937
- if include_conflicts:
1938
- for conflict_id in entry.get("lifecycle", {}).get("conflictsWith") or []:
1939
- lines.append(f" - conflictsWith: `{conflict_id}`")
1940
- if include_review:
1941
- for reason in entry.get("reviewReasons") or []:
1942
- lines.append(f" - review: {reason}")
1943
- return lines
1944
-
1945
-
1946
- def make_manifest(
1947
- project: Path,
1948
- pname: str,
1949
- summary_paths: list[Path],
1950
- archive_records: list[dict[str, Any]],
1951
- entries: list[dict[str, Any]],
1952
- failures: list[dict[str, str]],
1953
- duplicates: int,
1954
- ingest_mode: dict[str, Any] | None = None,
1955
- ) -> dict[str, Any]:
1956
- stats = {status: 0 for status in ["candidate", "active", "stale", "superseded", "deprecated", "conflicted"]}
1957
- by_type = {entry_type: 0 for entry_type in sorted(ENTRY_TYPES)}
1958
- for entry in entries:
1959
- stats[entry["status"]] = stats.get(entry["status"], 0) + 1
1960
- by_type[entry["type"]] = by_type.get(entry["type"], 0) + 1
1961
-
1962
- manifest_entries = [
1963
- {
1964
- "id": e["id"],
1965
- "type": e["type"],
1966
- "status": e["status"],
1967
- "title": e["title"],
1968
- "sourceArchive": e["source"]["archive"],
1969
- "sourceCommit": e["source"]["sourceCommit"],
1970
- "sourceFiles": e["scope"]["sourceFiles"],
1971
- "confidence": e.get("confidence", {}),
1972
- }
1973
- for e in sorted(entries, key=lambda item: (item["source"]["archive"], item["type"], item["title"]))
1974
- ]
1975
-
1976
- return {
1977
- "schemaVersion": 1,
1978
- "generatedAt": now_iso(),
1979
- "projectId": pname,
1980
- "projectRoot": str(project),
1981
- "headCommit": git_head(project),
1982
- "archives": {
1983
- "scanned": len(summary_paths),
1984
- "indexed": len(summary_paths) - len(failures),
1985
- "failed": len(failures),
1986
- "items": archive_records,
1987
- },
1988
- "stats": stats,
1989
- "byType": by_type,
1990
- "duplicatesSkipped": duplicates,
1991
- "ingestMode": ingest_mode or {
1992
- "incremental": False,
1993
- "archivesExtracted": len(summary_paths) - len(failures),
1994
- "archivesReused": 0,
1995
- "cacheWrites": 0,
1996
- "activeAutoDemoted": 0,
1997
- "confidenceScored": 0,
1998
- "candidateAutoPromoted": 0,
1999
- "validationChecked": 0,
2000
- "validationFailed": 0,
2001
- "validationAutoDemoted": 0,
2002
- },
2003
- "failures": failures,
2004
- "entries": manifest_entries,
2005
- }
2006
-
2007
-
2008
- def write_sqlite(path: Path, entries: list[dict[str, Any]]) -> None:
2009
- path.parent.mkdir(parents=True, exist_ok=True)
2010
- if path.exists():
2011
- path.unlink()
2012
- con = sqlite3.connect(path)
2013
- try:
2014
- con.execute("pragma journal_mode=wal")
2015
- con.execute(
2016
- """
2017
- create table entries (
2018
- id text primary key,
2019
- project_id text not null,
2020
- type text not null,
2021
- status text not null,
2022
- title text not null,
2023
- summary text not null,
2024
- body text not null,
2025
- source_archive text not null,
2026
- source_commit text,
2027
- source_files_json text not null,
2028
- keywords_json text not null,
2029
- entry_json text not null
2030
- )
2031
- """
2032
- )
2033
- con.execute(
2034
- """
2035
- create table entry_files (
2036
- entry_id text not null,
2037
- source_file text not null,
2038
- primary key (entry_id, source_file),
2039
- foreign key (entry_id) references entries(id)
2040
- )
2041
- """
2042
- )
2043
- con.execute("create virtual table entries_fts using fts5(id, title, summary, body, keywords)")
2044
- con.execute("create index idx_entries_status on entries(status)")
2045
- con.execute("create index idx_entries_type on entries(type)")
2046
- con.execute("create index idx_entries_source_archive on entries(source_archive)")
2047
- con.execute("create index idx_entry_files_source_file on entry_files(source_file)")
2048
- for entry in entries:
2049
- source_files_json = json.dumps(entry["scope"]["sourceFiles"], ensure_ascii=False)
2050
- keywords_json = json.dumps(entry["keywords"], ensure_ascii=False)
2051
- entry_json = json.dumps(entry, ensure_ascii=False)
2052
- con.execute(
2053
- """
2054
- insert into entries (
2055
- id, project_id, type, status, title, summary, body, source_archive,
2056
- source_commit, source_files_json, keywords_json, entry_json
2057
- ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2058
- """,
2059
- (
2060
- entry["id"],
2061
- entry["projectId"],
2062
- entry["type"],
2063
- entry["status"],
2064
- entry["title"],
2065
- entry["summary"],
2066
- entry["body"],
2067
- entry["source"]["archive"],
2068
- entry["source"]["sourceCommit"],
2069
- source_files_json,
2070
- keywords_json,
2071
- entry_json,
2072
- ),
2073
- )
2074
- con.execute(
2075
- "insert into entries_fts (id, title, summary, body, keywords) values (?, ?, ?, ?, ?)",
2076
- (
2077
- entry["id"],
2078
- entry["title"],
2079
- entry["summary"],
2080
- entry["body"],
2081
- " ".join(entry["keywords"]),
2082
- ),
2083
- )
2084
- for source_file in entry["scope"]["sourceFiles"]:
2085
- con.execute(
2086
- "insert or ignore into entry_files (entry_id, source_file) values (?, ?)",
2087
- (entry["id"], source_file),
2088
- )
2089
- con.commit()
2090
- finally:
2091
- con.close()
2092
-
2093
-
2094
- def write_views(knowledge: Path, index: dict[str, Any], entries: list[dict[str, Any]]) -> None:
2095
- dashboard = [
2096
- "# Harness Knowledge Dashboard",
2097
- "",
2098
- f"- generatedAt: {index['generatedAt']}",
2099
- f"- projectId: {index['projectId']}",
2100
- f"- archives: {index['archives']['indexed']}/{index['archives']['scanned']}",
2101
- f"- entries: {len(entries)}",
2102
- "",
2103
- "## Stats",
2104
- "",
2105
- "| status | count |",
2106
- "|---|---:|",
2107
- ]
2108
- for status, count in index["stats"].items():
2109
- dashboard.append(f"| {status} | {count} |")
2110
- dashboard.extend(["", "## Recent Entries", ""])
2111
- for entry in entries[:50]:
2112
- dashboard.append(f"- **{entry['type']}** `{entry['status']}` {entry['title']}")
2113
- dashboard.append(f" - source: `{entry['source']['archive']}`")
2114
- write_text(knowledge / "views" / "knowledge-dashboard.md", "\n".join(dashboard) + "\n")
2115
-
2116
- by_file: dict[str, list[dict[str, Any]]] = {}
2117
- for entry in entries:
2118
- for source_file in entry["scope"]["sourceFiles"]:
2119
- by_file.setdefault(source_file, []).append(entry)
2120
- lines = ["# Harness Knowledge By File", ""]
2121
- for source_file in sorted(by_file):
2122
- lines.extend([f"## `{source_file}`", ""])
2123
- for entry in by_file[source_file]:
2124
- lines.append(f"- **{entry['type']}** `{entry['status']}` {entry['title']}")
2125
- lines.append("")
2126
- write_text(knowledge / "views" / "by-file.md", "\n".join(lines))
2127
-
2128
- stale = [entry for entry in entries if entry["status"] == "stale"]
2129
- lines = ["# Harness Stale Knowledge", ""]
2130
- if not stale:
2131
- lines.append("No stale knowledge detected in this run.")
2132
- for entry in stale:
2133
- lines.append(f"- **{entry['type']}** {entry['title']}")
2134
- for reason in entry["lifecycle"]["staleReasons"]:
2135
- lines.append(f" - {reason}")
2136
- write_text(knowledge / "views" / "stale-items.md", "\n".join(lines) + "\n")
2137
-
2138
- superseded = [entry for entry in entries if entry["status"] == "superseded"]
2139
- lines = ["# Harness Superseded Knowledge", ""]
2140
- if not superseded:
2141
- lines.append("No superseded knowledge detected in this run.")
2142
- for entry in superseded:
2143
- lines.append(f"- **{entry['type']}** {entry['title']}")
2144
- superseded_by = entry.get("lifecycle", {}).get("supersededBy")
2145
- if superseded_by:
2146
- lines.append(f" - supersededBy: `{superseded_by}`")
2147
- for reason in entry["lifecycle"].get("staleReasons") or []:
2148
- lines.append(f" - {reason}")
2149
- write_text(knowledge / "views" / "superseded-items.md", "\n".join(lines) + "\n")
2150
-
2151
- conflicted = [entry for entry in entries if entry["status"] == "conflicted"]
2152
- lines = ["# Harness Conflicted Knowledge", ""]
2153
- if not conflicted:
2154
- lines.append("No conflicted knowledge detected in this run.")
2155
- for entry in conflicted:
2156
- lines.append(f"- **{entry['type']}** {entry['title']}")
2157
- for conflict_id in entry.get("lifecycle", {}).get("conflictsWith") or []:
2158
- lines.append(f" - conflictsWith: `{conflict_id}`")
2159
- for reason in entry["lifecycle"].get("staleReasons") or []:
2160
- lines.append(f" - {reason}")
2161
- write_text(knowledge / "views" / "conflicted-items.md", "\n".join(lines) + "\n")
2162
-
2163
- active_review = active_review_items(entries)
2164
- lines = ["# Harness Active Review", ""]
2165
- if not active_review:
2166
- lines.append("No active entries require manual review.")
2167
- for entry in active_review:
2168
- lines.append(f"- **{entry['type']}** {entry['title']}")
2169
- lines.append(f" - id: `{entry['id']}`")
2170
- for reason in entry.get("reviewReasons") or []:
2171
- lines.append(f" - {reason}")
2172
- write_text(knowledge / "views" / "active-review.md", "\n".join(lines) + "\n")
2173
-
2174
- base = [
2175
- 'filters:',
2176
- ' and:',
2177
- ' - \'file.inFolder(".harness/knowledge/entries")\'',
2178
- ' - \'file.ext == "json"\'',
2179
- '',
2180
- 'properties:',
2181
- ' file.name:',
2182
- ' displayName: "Entry File"',
2183
- ' file.folder:',
2184
- ' displayName: "Lifecycle Folder"',
2185
- ' file.mtime:',
2186
- ' displayName: "Modified"',
2187
- '',
2188
- 'views:',
2189
- ' - type: table',
2190
- ' name: "Lifecycle Table"',
2191
- ' order:',
2192
- ' - file.name',
2193
- ' - file.folder',
2194
- ' - file.mtime',
2195
- ' - type: table',
2196
- ' name: "Needs Review"',
2197
- ' filters:',
2198
- ' or:',
2199
- ' - \'file.inFolder(".harness/knowledge/entries/stale")\'',
2200
- ' - \'file.inFolder(".harness/knowledge/entries/conflicted")\'',
2201
- ' - \'file.inFolder(".harness/knowledge/entries/superseded")\'',
2202
- ' order:',
2203
- ' - file.name',
2204
- ' - file.folder',
2205
- ' - file.mtime',
2206
- ]
2207
- write_text(knowledge / "views" / "knowledge.base", "\n".join(base) + "\n")
2208
-
2209
-
2210
- def write_ingest_report(
2211
- knowledge: Path, index: dict[str, Any], failures: list[dict[str, str]], duplicates: int
2212
- ) -> None:
2213
- lines = [
2214
- "# Harness Knowledge Ingest Report",
2215
- "",
2216
- f"- generatedAt: {index['generatedAt']}",
2217
- f"- projectId: {index['projectId']}",
2218
- f"- archives scanned: {index['archives']['scanned']}",
2219
- f"- archives indexed: {index['archives']['indexed']}",
2220
- f"- entries: {len(index['entries'])}",
2221
- f"- duplicates skipped: {duplicates}",
2222
- f"- incremental: {index.get('ingestMode', {}).get('incremental', False)}",
2223
- f"- archives extracted: {index.get('ingestMode', {}).get('archivesExtracted', 0)}",
2224
- f"- archives reused: {index.get('ingestMode', {}).get('archivesReused', 0)}",
2225
- f"- confidence scored: {index.get('ingestMode', {}).get('confidenceScored', 0)}",
2226
- f"- candidate auto-promoted: {index.get('ingestMode', {}).get('candidateAutoPromoted', 0)}",
2227
- f"- active auto-demoted: {index.get('ingestMode', {}).get('activeAutoDemoted', 0)}",
2228
- f"- validators checked: {index.get('ingestMode', {}).get('validationChecked', 0)}",
2229
- f"- validators failed: {index.get('ingestMode', {}).get('validationFailed', 0)}",
2230
- f"- validator auto-demoted: {index.get('ingestMode', {}).get('validationAutoDemoted', 0)}",
2231
- "",
2232
- "## Status Counts",
2233
- "",
2234
- "| status | count |",
2235
- "|---|---:|",
2236
- ]
2237
- for status, count in index["stats"].items():
2238
- lines.append(f"| {status} | {count} |")
2239
- if failures:
2240
- lines.extend(["", "## Failures", ""])
2241
- for failure in failures:
2242
- lines.append(f"- `{failure['path']}`: {failure.get('error') or failure.get('reason')}")
2243
- write_text(knowledge / "reports" / f"ingest-report-{timestamp()}.md", "\n".join(lines) + "\n")
2244
-
2245
-
2246
- def write_verification_report(knowledge: Path, summary: dict[str, Any]) -> Path:
2247
- lines = [
2248
- "# Harness Knowledge Verification Report",
2249
- "",
2250
- f"- generatedAt: {summary['generatedAt']}",
2251
- f"- project: {summary['project']}",
2252
- f"- enabled: {summary['enabled']}",
2253
- f"- checked: {summary['checked']}",
2254
- f"- passed: {summary['passed']}",
2255
- f"- failed: {summary['failed']}",
2256
- f"- skipped: {summary['skipped']}",
2257
- f"- auto-demoted: {summary['autoDemoted']}",
2258
- "",
2259
- "## Entries",
2260
- "",
2261
- ]
2262
- if not summary["entries"]:
2263
- lines.append("- No entries with validators.")
2264
- for entry in summary["entries"]:
2265
- lines.append(f"- `{entry['id']}` `{entry['validationStatus']}` status=`{entry['status']}`")
2266
- for result in entry.get("results") or []:
2267
- description = result.get("description") or result.get("type")
2268
- message = result.get("message") or ""
2269
- lines.append(f" - {result.get('status')}: {description} - {message}")
2270
- path = knowledge / "reports" / f"verification-report-{timestamp()}.md"
2271
- write_text(path, "\n".join(lines) + "\n")
2272
- return path
2273
-
2274
-
2275
- def refresh_outputs_from_entry_files(
2276
- project: Path,
2277
- knowledge: Path,
2278
- ingest_mode: dict[str, Any] | None = None,
2279
- ) -> dict[str, Any]:
2280
- summary_paths = sorted((project / ".harness" / "archive").glob("*/reports/final/summary-data.json"))
2281
- archive_records = archive_summary_records(project, summary_paths)
2282
- existing: dict[str, Any] = {}
2283
- index_path = knowledge / "index.json"
2284
- if index_path.exists():
2285
- try:
2286
- loaded = read_json(index_path)
2287
- if isinstance(loaded, dict):
2288
- existing = loaded
2289
- except (OSError, json.JSONDecodeError):
2290
- existing = {}
2291
- file_entries = [entry for _, entry in load_entry_files(knowledge)]
2292
- file_by_id = {entry["id"]: entry for entry in file_entries}
2293
- sqlite_path = knowledge / "index.sqlite"
2294
- if sqlite_path.exists():
2295
- try:
2296
- sqlite_entries = read_sqlite_entries(sqlite_path)
2297
- except (OSError, sqlite3.Error, json.JSONDecodeError):
2298
- sqlite_entries = []
2299
- else:
2300
- sqlite_entries = []
2301
- if sqlite_entries:
2302
- entries = [file_by_id.get(entry["id"], entry) for entry in sqlite_entries]
2303
- existing_ids = {entry["id"] for entry in entries}
2304
- entries.extend(entry for entry in file_entries if entry["id"] not in existing_ids)
2305
- else:
2306
- entries = file_entries
2307
- index = make_manifest(
2308
- project,
2309
- project_id(project),
2310
- summary_paths,
2311
- archive_records,
2312
- entries,
2313
- existing.get("failures") or [],
2314
- int(existing.get("duplicatesSkipped") or 0),
2315
- ingest_mode or existing.get("ingestMode") or {},
2316
- )
2317
- write_sqlite(knowledge / "index.sqlite", entries)
2318
- write_json(index_path, index)
2319
- write_views(knowledge, index, entries)
2320
- return index
2321
-
2322
-
2323
- def verify_knowledge(project: Path) -> dict[str, Any]:
2324
- project = project.resolve()
2325
- knowledge = project / ".harness" / "knowledge"
2326
- if not (knowledge / "index.json").exists():
2327
- build_index(project)
2328
- config = load_config(knowledge)
2329
- summary = apply_knowledge_validation(project, knowledge, config)
2330
- summary["generatedAt"] = now_iso()
2331
- summary["project"] = str(project)
2332
- report = write_verification_report(knowledge, summary)
2333
- index = refresh_outputs_from_entry_files(project, knowledge)
2334
- summary["report"] = str(report)
2335
- summary["paths"] = {
2336
- "index": str(knowledge / "index.json"),
2337
- "sqlite": str(knowledge / "index.sqlite"),
2338
- "report": str(report),
2339
- }
2340
- summary["stats"] = index["stats"]
2341
- return summary
2342
-
2343
-
2344
- VALIDATOR_TOKEN_STOPWORDS = {
2345
- "api",
2346
- "apps",
2347
- "async",
2348
- "body",
2349
- "cache",
2350
- "candidate",
2351
- "change",
2352
- "client",
2353
- "commit",
2354
- "config",
2355
- "data",
2356
- "entry",
2357
- "error",
2358
- "file",
2359
- "files",
2360
- "final",
2361
- "harness",
2362
- "index",
2363
- "json",
2364
- "knowledge",
2365
- "manual",
2366
- "project",
2367
- "result",
2368
- "source",
2369
- "status",
2370
- "store",
2371
- "summary",
2372
- "test",
2373
- "tests",
2374
- "type",
2375
- }
2376
-
2377
-
2378
- def validator_candidate_tokens(entry: dict[str, Any]) -> list[str]:
2379
- text = " ".join(
2380
- str(value or "")
2381
- for value in [
2382
- entry.get("title"),
2383
- entry.get("summary"),
2384
- entry.get("body"),
2385
- " ".join(entry.get("keywords") or []),
2386
- ]
2387
- )
2388
- tokens = []
2389
- seen = set()
2390
- for token in re.findall(r"[A-Za-z_][A-Za-z0-9_.]{3,}", text):
2391
- lower = token.lower().strip("._")
2392
- if lower in VALIDATOR_TOKEN_STOPWORDS:
2393
- continue
2394
- if token not in seen:
2395
- seen.add(token)
2396
- tokens.append(token)
2397
- tokens.sort(key=lambda token: (("." not in token and not any(c.isupper() for c in token)), len(token)))
2398
- return tokens[:12]
2399
-
2400
-
2401
- def suggest_validators_for_entry(project: Path, entry: dict[str, Any]) -> list[dict[str, Any]]:
2402
- if isinstance(entry.get("validators"), list) and entry["validators"]:
2403
- return []
2404
- source_files = [str(path) for path in entry.get("scope", {}).get("sourceFiles") or [] if path]
2405
- suggestions: list[dict[str, Any]] = []
2406
- tokens = validator_candidate_tokens(entry)
2407
- for rel_path in source_files[:3]:
2408
- path = safe_project_path(project, rel_path)
2409
- if path is None or not path.exists() or not path.is_file():
2410
- continue
2411
- suggestions.append(
2412
- {
2413
- "type": "file_exists",
2414
- "path": rel_path,
2415
- "description": f"source file still exists: {rel_path}",
2416
- }
2417
- )
2418
- try:
2419
- text = path.read_text(encoding="utf-8", errors="ignore")
2420
- except OSError:
2421
- continue
2422
- for token in tokens:
2423
- if token in text:
2424
- suggestions.append(
2425
- {
2426
- "type": "file_contains",
2427
- "path": rel_path,
2428
- "pattern": token,
2429
- "description": f"source file still contains {token}",
2430
- }
2431
- )
2432
- break
2433
- return suggestions[:4]
2434
-
2435
-
2436
- def write_validator_suggestions_report(
2437
- knowledge: Path,
2438
- summary: dict[str, Any],
2439
- ) -> Path:
2440
- lines = [
2441
- "# Harness Validator Suggestions",
2442
- "",
2443
- f"- generatedAt: {summary['generatedAt']}",
2444
- f"- project: {summary['project']}",
2445
- f"- suggested entries: {summary['suggested']}",
2446
- f"- applied entries: {summary['applied']}",
2447
- "",
2448
- "## Suggestions",
2449
- "",
2450
- ]
2451
- if not summary["entries"]:
2452
- lines.append("- No validator suggestions found.")
2453
- for item in summary["entries"]:
2454
- lines.append(f"- `{item['id']}` `{item['status']}` {item['title']}")
2455
- for validator in item["validators"]:
2456
- detail = validator.get("pattern") or validator.get("path") or validator.get("type")
2457
- lines.append(f" - `{validator['type']}` {detail}: {validator.get('description', '')}")
2458
- path = knowledge / "reports" / f"validator-suggestions-{timestamp()}.md"
2459
- write_text(path, "\n".join(lines) + "\n")
2460
- return path
2461
-
2462
-
2463
- def suggest_validators(
2464
- project: Path,
2465
- limit: int = 20,
2466
- statuses: list[str] | None = None,
2467
- apply: bool = False,
2468
- ) -> dict[str, Any]:
2469
- project = project.resolve()
2470
- knowledge = project / ".harness" / "knowledge"
2471
- if not (knowledge / "index.json").exists():
2472
- build_index(project)
2473
- selected_statuses = statuses or ["active", "candidate"]
2474
- suggestions: list[dict[str, Any]] = []
2475
- applied = 0
2476
- for entry_path, entry in load_entry_files(knowledge, selected_statuses):
2477
- validators = suggest_validators_for_entry(project, entry)
2478
- if not validators:
2479
- continue
2480
- item = {
2481
- "id": entry["id"],
2482
- "status": entry.get("status"),
2483
- "title": entry.get("title"),
2484
- "path": str(entry_path),
2485
- "validators": validators,
2486
- }
2487
- suggestions.append(item)
2488
- if apply:
2489
- entry["validators"] = validators
2490
- write_json(entry_path, entry)
2491
- applied += 1
2492
- if len(suggestions) >= max(1, limit):
2493
- break
2494
- summary = {
2495
- "generatedAt": now_iso(),
2496
- "project": str(project),
2497
- "statuses": selected_statuses,
2498
- "limit": limit,
2499
- "suggested": len(suggestions),
2500
- "applied": applied,
2501
- "entries": suggestions,
2502
- }
2503
- report = write_validator_suggestions_report(knowledge, summary)
2504
- if apply and applied:
2505
- index = refresh_outputs_from_entry_files(project, knowledge)
2506
- summary["stats"] = index["stats"]
2507
- summary["report"] = str(report)
2508
- return summary
2509
-
2510
-
2511
- def auto_knowledge(
2512
- project: Path,
2513
- *,
2514
- limit: int = 20,
2515
- suggest_statuses: list[str] | None = None,
2516
- apply_suggestions: bool = False,
2517
- incremental: bool = True,
2518
- audit_limit: int = 10,
2519
- ) -> dict[str, Any]:
2520
- project = project.resolve()
2521
- knowledge = project / ".harness" / "knowledge"
2522
- config_summary = ensure_auto_knowledge_config(knowledge)
2523
- sync = sync_status(project, update=True, incremental=incremental)
2524
- if config_summary["created"]:
2525
- if sync.get("action") == "ingested":
2526
- ingest_mode = sync.get("index", {}).get("ingestMode", {})
2527
- config_summary["appliedBy"] = "sync"
2528
- config_summary["candidateAutoPromoted"] = ingest_mode.get("candidateAutoPromoted", 0)
2529
- elif sync.get("upToDate"):
2530
- index = build_index(project, incremental=incremental)
2531
- config_summary["appliedBy"] = "auto-rebuild"
2532
- config_summary["candidateAutoPromoted"] = index.get("ingestMode", {}).get("candidateAutoPromoted", 0)
2533
- sync = sync_status(project, update=False, incremental=incremental)
2534
- suggestions = suggest_validators(
2535
- project,
2536
- limit=limit,
2537
- statuses=suggest_statuses or [],
2538
- apply=apply_suggestions,
2539
- )
2540
- verification = verify_knowledge(project)
2541
- audit = audit_entries(project, limit=audit_limit)
2542
- return {
2543
- "project": str(project),
2544
- "generatedAt": now_iso(),
2545
- "mode": {
2546
- "applySuggestions": apply_suggestions,
2547
- "suggestStatuses": suggest_statuses or ["active", "candidate"],
2548
- "limit": limit,
2549
- "auditLimit": audit_limit,
2550
- "incremental": incremental,
2551
- },
2552
- "config": config_summary,
2553
- "sync": {
2554
- "upToDate": sync["upToDate"],
2555
- "action": sync["action"],
2556
- "reasons": sync["reasons"],
2557
- "archiveCount": sync["archiveCount"],
2558
- "paths": sync["paths"],
2559
- },
2560
- "suggestions": {
2561
- "suggested": suggestions["suggested"],
2562
- "applied": suggestions["applied"],
2563
- "report": suggestions["report"],
2564
- },
2565
- "verification": {
2566
- "checked": verification["checked"],
2567
- "passed": verification["passed"],
2568
- "failed": verification["failed"],
2569
- "skipped": verification["skipped"],
2570
- "autoDemoted": verification["autoDemoted"],
2571
- "report": verification["report"],
2572
- },
2573
- "audit": {
2574
- "report": audit["report"],
2575
- "candidateReview": len(audit["candidateReview"]),
2576
- "staleReview": len(audit["staleReview"]),
2577
- "supersededReview": len(audit["supersededReview"]),
2578
- "conflictReview": len(audit["conflictReview"]),
2579
- "activeReview": len(audit["activeReview"]),
2580
- },
2581
- }
2582
-
2583
-
2584
- def fts_query(query: str) -> str:
2585
- tokens = re.findall(r"[\w\u4e00-\u9fff]{2,}", query, flags=re.UNICODE)
2586
- if not tokens:
2587
- return '"' + query.replace('"', '""') + '"'
2588
- return " OR ".join('"' + token.replace('"', '""') + '"' for token in tokens[:12])
2589
-
2590
-
2591
- def query_index(
2592
- project: Path,
2593
- query: str,
2594
- limit: int = 10,
2595
- file_filters: list[str] | None = None,
2596
- statuses: list[str] | None = None,
2597
- types: list[str] | None = None,
2598
- ) -> dict[str, Any]:
2599
- project = project.resolve()
2600
- knowledge = project / ".harness" / "knowledge"
2601
- sqlite_path = knowledge / "index.sqlite"
2602
- if not sqlite_path.exists():
2603
- build_index(project)
2604
- sync = sync_status(project)
2605
- if not sync["upToDate"]:
2606
- build_index(project)
2607
- entries = search_entries(sqlite_path, query, limit, file_filters, statuses, types)
2608
- context_path = write_context_pack(project, knowledge, query, entries)
2609
- filters = {
2610
- "files": file_filters or [],
2611
- "statuses": statuses or [],
2612
- "types": types or [],
2613
- }
2614
- return {
2615
- "query": query,
2616
- "matchCount": len(entries),
2617
- "contextPack": str(context_path),
2618
- "filters": filters,
2619
- "planInput": {
2620
- "kind": "harness-knowledge-context-pack",
2621
- "path": str(context_path),
2622
- "requiredBefore": "harness-plan",
2623
- "usage": "Read this context pack before design, planning, code exploration, or implementation.",
2624
- },
2625
- "matches": [
2626
- {
2627
- "id": entry["id"],
2628
- "type": entry["type"],
2629
- "status": entry["status"],
2630
- "title": entry["title"],
2631
- "sourceArchive": entry["source"]["archive"],
2632
- "sourceFiles": entry["scope"]["sourceFiles"],
2633
- }
2634
- for entry in entries
2635
- ],
2636
- }
2637
-
2638
-
2639
- def summarize_index(index: dict[str, Any]) -> dict[str, Any]:
2640
- project_root = Path(index["projectRoot"])
2641
- knowledge = project_root / ".harness" / "knowledge"
2642
- return {
2643
- "projectId": index["projectId"],
2644
- "generatedAt": index["generatedAt"],
2645
- "headCommit": index["headCommit"],
2646
- "archives": index["archives"],
2647
- "stats": index["stats"],
2648
- "byType": index["byType"],
2649
- "duplicatesSkipped": index["duplicatesSkipped"],
2650
- "ingestMode": index.get("ingestMode", {}),
2651
- "failures": index["failures"],
2652
- "paths": {
2653
- "index": str(knowledge / "index.json"),
2654
- "sqlite": str(knowledge / "index.sqlite"),
2655
- "dashboard": str(knowledge / "views" / "knowledge-dashboard.md"),
2656
- "byFile": str(knowledge / "views" / "by-file.md"),
2657
- "staleItems": str(knowledge / "views" / "stale-items.md"),
2658
- },
2659
- }
2660
-
2661
-
2662
- def sync_status(project: Path, update: bool = False, incremental: bool = True) -> dict[str, Any]:
2663
- project = project.resolve()
2664
- knowledge = project / ".harness" / "knowledge"
2665
- index_path = knowledge / "index.json"
2666
- sqlite_path = knowledge / "index.sqlite"
2667
- archive_root = project / ".harness" / "archive"
2668
- summary_paths = sorted(archive_root.glob("*/reports/final/summary-data.json"))
2669
- current_records = archive_summary_records(project, summary_paths)
2670
- reasons: list[str] = []
2671
- index: dict[str, Any] | None = None
2672
-
2673
- if not index_path.exists():
2674
- reasons.append("index.json missing")
2675
- else:
2676
- try:
2677
- index = read_json(index_path)
2678
- except (OSError, json.JSONDecodeError) as exc:
2679
- reasons.append("index.json unreadable: " + first_sentence(str(exc)))
2680
-
2681
- if not sqlite_path.exists():
2682
- reasons.append("index.sqlite missing")
2683
-
2684
- if index is not None:
2685
- indexed_records = index.get("archives", {}).get("items")
2686
- if not isinstance(indexed_records, list):
2687
- reasons.append("archive checksums missing from index")
2688
- else:
2689
- indexed_by_path = {record.get("summaryData"): record for record in indexed_records}
2690
- current_by_path = {record.get("summaryData"): record for record in current_records}
2691
- indexed_paths = set(indexed_by_path)
2692
- current_paths = set(current_by_path)
2693
- for path in sorted(current_paths - indexed_paths):
2694
- reasons.append("archive added: " + str(path))
2695
- for path in sorted(indexed_paths - current_paths):
2696
- reasons.append("archive removed: " + str(path))
2697
- for path in sorted(current_paths & indexed_paths):
2698
- if current_by_path[path].get("summarySha256") != indexed_by_path[path].get("summarySha256"):
2699
- reasons.append("archive checksum changed: " + str(path))
2700
-
2701
- current_head = git_head(project)
2702
- if current_head != index.get("headCommit"):
2703
- reasons.append("head commit changed since last ingest")
2704
-
2705
- action = "none"
2706
- refreshed: dict[str, Any] | None = None
2707
- if reasons and update:
2708
- refreshed = build_index(project, incremental=incremental)
2709
- action = "ingested"
2710
- reasons = []
2711
-
2712
- result = {
2713
- "project": str(project),
2714
- "upToDate": not reasons,
2715
- "action": action,
2716
- "reasons": reasons,
2717
- "archiveCount": len(current_records),
2718
- "paths": {
2719
- "index": str(index_path),
2720
- "sqlite": str(sqlite_path),
2721
- },
2722
- }
2723
- if refreshed is not None:
2724
- result["index"] = summarize_index(refreshed)
2725
- return result
2726
-
2727
-
2728
- def search_entries(
2729
- sqlite_path: Path,
2730
- query: str,
2731
- limit: int,
2732
- file_filters: list[str] | None = None,
2733
- statuses: list[str] | None = None,
2734
- types: list[str] | None = None,
2735
- ) -> list[dict[str, Any]]:
2736
- con = sqlite3.connect(sqlite_path)
2737
- try:
2738
- con.row_factory = sqlite3.Row
2739
- rows: list[sqlite3.Row] = []
2740
- fetch_limit = max(limit * 5, 50)
2741
- try:
2742
- rows.extend(
2743
- con.execute(
2744
- """
2745
- select e.entry_json
2746
- from entries_fts f
2747
- join entries e on e.id = f.id
2748
- where entries_fts match ?
2749
- limit ?
2750
- """,
2751
- (fts_query(query), fetch_limit),
2752
- ).fetchall()
2753
- )
2754
- except sqlite3.OperationalError:
2755
- like = f"%{query}%"
2756
- rows.extend(
2757
- con.execute(
2758
- """
2759
- select entry_json
2760
- from entries
2761
- where title like ? or summary like ? or body like ?
2762
- limit ?
2763
- """,
2764
- (like, like, like, fetch_limit),
2765
- ).fetchall()
2766
- )
2767
-
2768
- like_terms = [f"%{token}%" for token in query_tokens(query)]
2769
- for like in like_terms[:10]:
2770
- rows.extend(
2771
- con.execute(
2772
- """
2773
- select entry_json
2774
- from entries
2775
- where title like ? or summary like ? or body like ? or keywords_json like ?
2776
- limit ?
2777
- """,
2778
- (like, like, like, like, fetch_limit),
2779
- ).fetchall()
2780
- )
2781
-
2782
- for source_file in file_filters or []:
2783
- rows.extend(
2784
- con.execute(
2785
- """
2786
- select e.entry_json
2787
- from entry_files ef
2788
- join entries e on e.id = ef.entry_id
2789
- where ef.source_file = ? or ef.source_file like ?
2790
- limit ?
2791
- """,
2792
- (source_file, f"%{source_file}%", fetch_limit),
2793
- ).fetchall()
2794
- )
2795
-
2796
- entries_by_id: dict[str, dict[str, Any]] = {}
2797
- for row in rows:
2798
- entry = json.loads(row["entry_json"])
2799
- if not entry_matches_filters(entry, file_filters, statuses, types):
2800
- continue
2801
- entries_by_id[entry["id"]] = entry
2802
- ranked = sorted(
2803
- entries_by_id.values(),
2804
- key=lambda entry: score_entry(entry, query),
2805
- reverse=True,
2806
- )
2807
- return ranked[:limit]
2808
- finally:
2809
- con.close()
2810
-
2811
-
2812
- def entry_matches_filters(
2813
- entry: dict[str, Any],
2814
- file_filters: list[str] | None = None,
2815
- statuses: list[str] | None = None,
2816
- types: list[str] | None = None,
2817
- ) -> bool:
2818
- if statuses and entry.get("status") not in statuses:
2819
- return False
2820
- if types and entry.get("type") not in types:
2821
- return False
2822
- if file_filters:
2823
- source_files = entry.get("scope", {}).get("sourceFiles") or []
2824
- for wanted in file_filters:
2825
- if not any(wanted == actual or wanted in actual or actual in wanted for actual in source_files):
2826
- return False
2827
- return True
2828
-
2829
-
2830
- def query_tokens(query: str) -> list[str]:
2831
- tokens = re.findall(r"[\w\u4e00-\u9fff]{2,}", query.lower(), flags=re.UNICODE)
2832
- return list(dict.fromkeys(tokens))
2833
-
2834
-
2835
- def score_entry(entry: dict[str, Any], query: str) -> int:
2836
- tokens = query_tokens(query)
2837
- haystack = " ".join(
2838
- [
2839
- entry.get("title", ""),
2840
- entry.get("summary", ""),
2841
- entry.get("body", ""),
2842
- " ".join(entry.get("keywords") or []),
2843
- entry.get("source", {}).get("archive", ""),
2844
- " ".join(entry.get("scope", {}).get("sourceFiles") or []),
2845
- ]
2846
- ).lower()
2847
- score = 0
2848
- q = query.lower().strip()
2849
- if q and q in haystack:
2850
- score += 20
2851
- for token in tokens:
2852
- if token in haystack:
2853
- score += 8
2854
- if token in entry.get("title", "").lower():
2855
- score += 4
2856
- score += {
2857
- "requirement": 12,
2858
- "decision": 8,
2859
- "risk": 7,
2860
- "test-evidence": 5,
2861
- "api-contract": 4,
2862
- "implementation": 3,
2863
- "pitfall": 6,
2864
- }.get(entry.get("type"), 0)
2865
- score += {
2866
- "active": 6,
2867
- "candidate": 4,
2868
- "stale": 1,
2869
- "superseded": -3,
2870
- "deprecated": -4,
2871
- "conflicted": -2,
2872
- }.get(entry.get("status"), 0)
2873
- archive = entry.get("source", {}).get("archive", "")
2874
- match = re.search(r"(\d{4}-\d{2}-\d{2})", archive)
2875
- if match:
2876
- try:
2877
- days = (dt.date.today() - dt.date.fromisoformat(match.group(1))).days
2878
- score += max(0, 10 - days)
2879
- except ValueError:
2880
- pass
2881
- return score
2882
-
2883
-
2884
- def write_context_pack(project: Path, knowledge: Path, query: str, entries: list[dict[str, Any]]) -> Path:
2885
- filename = f"{timestamp()}-{safe_filename(query) or 'query'}-{short_hash(query, 6)}.md"
2886
- path = knowledge / "context-packs" / filename
2887
- active_like = [e for e in entries if e["status"] in {"active", "candidate"}]
2888
- stale = [e for e in entries if e["status"] == "stale"]
2889
- risks = [e for e in entries if e["type"] == "risk"]
2890
- files: list[str] = []
2891
- for entry in entries:
2892
- for source_file in entry["scope"]["sourceFiles"]:
2893
- if source_file not in files:
2894
- files.append(source_file)
2895
-
2896
- lines = [
2897
- "# Knowledge Context Pack",
2898
- "",
2899
- "## Query",
2900
- "",
2901
- query,
2902
- "",
2903
- "## Before planning",
2904
- "",
2905
- "- Treat candidate entries as useful history, not current truth.",
2906
- "- Re-check stale entries against the current code before relying on them.",
2907
- "- Open the source archive and suggested files before making implementation decisions.",
2908
- "",
2909
- "## High-confidence relevant history",
2910
- "",
2911
- ]
2912
- if not active_like:
2913
- lines.append("No active or candidate history matched this query.")
2914
- for entry in active_like:
2915
- lines.extend(render_entry_for_context(entry))
2916
-
2917
- lines.extend(["", "## Potentially stale history", ""])
2918
- if not stale:
2919
- lines.append("No stale matched history detected.")
2920
- for entry in stale:
2921
- lines.extend(render_entry_for_context(entry, include_stale=True))
2922
-
2923
- lines.extend(["", "## Related risks", ""])
2924
- if not risks:
2925
- lines.append("No related risks matched this query.")
2926
- for entry in risks[:8]:
2927
- lines.append(f"- {entry['title']}")
2928
- lines.append(f" - source: `{entry['source']['archive']}`")
2929
-
2930
- lines.extend(["", "## Suggested files to inspect next", ""])
2931
- if not files:
2932
- lines.append("No source files were linked by matched entries.")
2933
- for source_file in files[:20]:
2934
- lines.append(f"- {source_file}")
2935
-
2936
- lines.extend(["", "## Source index", ""])
2937
- lines.append(f"- project: `{project}`")
2938
- lines.append(f"- generatedAt: {now_iso()}")
2939
- write_text(path, "\n".join(lines) + "\n")
2940
- write_json(
2941
- knowledge / "context-packs" / "latest.json",
2942
- {
2943
- "schemaVersion": 1,
2944
- "generatedAt": now_iso(),
2945
- "query": query,
2946
- "contextPack": str(path),
2947
- "matchIds": [entry["id"] for entry in entries],
2948
- },
2949
- )
2950
- return path
2951
-
2952
-
2953
- def render_entry_for_context(entry: dict[str, Any], include_stale: bool = False) -> list[str]:
2954
- lines = [
2955
- f"- {entry['title']}",
2956
- f" - type: `{entry['type']}`",
2957
- f" - status: `{entry['status']}`",
2958
- f" - source: `{entry['source']['archive']}`",
2959
- f" - commit: `{entry['source']['sourceCommit'] or 'unknown'}`",
2960
- f" - key takeaway: {entry['summary']}",
2961
- ]
2962
- if entry["scope"]["sourceFiles"]:
2963
- lines.append(" - source files: " + ", ".join(f"`{f}`" for f in entry["scope"]["sourceFiles"][:6]))
2964
- if include_stale:
2965
- reasons = entry["lifecycle"].get("staleReasons") or []
2966
- for reason in reasons:
2967
- lines.append(f" - stale reason: {reason}")
2968
- return lines
2969
-
2970
-
2971
- # ---------------------------------------------------------------------------
2972
- # P0-3: rule automation + AI judgement interface (DESIGN.md D4)
2973
- # ---------------------------------------------------------------------------
2974
-
2975
- NEAR_DUPLICATE_THRESHOLD = 0.88
2976
- AUTO_SUPERSEDE_SIMILARITY = 0.75
2977
- AUTO_SUPERSEDE_WEAK_SIMILARITY = 0.55
2978
- JUDGE_ACTIONS = {"promote", "drop", "supersede", "keep-conflict"}
2979
- ENTRY_STATUSES = {"candidate", "active", "stale", "superseded", "conflicted"}
2980
-
2981
-
2982
- def load_harness_project_config(project: Path) -> dict[str, Any]:
2983
- config_path = project / ".harness" / "config" / "harness.json"
2984
- if not config_path.exists():
2985
- return {}
2986
- try:
2987
- config = read_json(config_path)
2988
- except (OSError, json.JSONDecodeError):
2989
- return {}
2990
- return config if isinstance(config, dict) else {}
2991
-
2992
-
2993
- def manual_review_enabled(project: Path) -> bool:
2994
- harness_cfg = load_harness_project_config(project)
2995
- knowledge_section = harness_cfg.get("knowledge")
2996
- if isinstance(knowledge_section, dict) and "manualReview" in knowledge_section:
2997
- return bool(knowledge_section.get("manualReview"))
2998
- knowledge_cfg = load_config(project / ".harness" / "knowledge")
2999
- if "manualReview" in knowledge_cfg:
3000
- return bool(knowledge_cfg.get("manualReview"))
3001
- nested = knowledge_cfg.get("knowledge")
3002
- if isinstance(nested, dict) and "manualReview" in nested:
3003
- return bool(nested.get("manualReview"))
3004
- return False
3005
-
3006
-
3007
- def normalize_similarity_text(text: str) -> str:
3008
- lowered = str(text or "").lower()
3009
- lowered = re.sub(r"[\W_]+", " ", lowered, flags=re.UNICODE)
3010
- return re.sub(r"\s+", " ", lowered).strip()
3011
-
3012
-
3013
- def entry_compare_text(entry: dict[str, Any]) -> str:
3014
- return normalize_similarity_text(
3015
- " ".join(
3016
- [
3017
- str(entry.get("title") or ""),
3018
- str(entry.get("summary") or ""),
3019
- str(entry.get("body") or ""),
3020
- ]
3021
- )
3022
- )
3023
-
3024
-
3025
- def entry_similarity(left: dict[str, Any], right: dict[str, Any]) -> float:
3026
- left_text = entry_compare_text(left)
3027
- right_text = entry_compare_text(right)
3028
- if not left_text or not right_text:
3029
- return 0.0
3030
- return SequenceMatcher(None, left_text, right_text).ratio()
3031
-
3032
-
3033
- def status_rank(status: str | None) -> int:
3034
- return {
3035
- "active": 5,
3036
- "candidate": 4,
3037
- "stale": 3,
3038
- "conflicted": 2,
3039
- "superseded": 1,
3040
- }.get(str(status or ""), 0)
3041
-
3042
-
3043
- def prefer_entry(left: dict[str, Any], right: dict[str, Any]) -> dict[str, Any]:
3044
- left_key = (
3045
- status_rank(left.get("status")),
3046
- len(str(left.get("body") or "")),
3047
- len(left.get("scope", {}).get("sourceFiles") or []),
3048
- str(left.get("id") or ""),
3049
- )
3050
- right_key = (
3051
- status_rank(right.get("status")),
3052
- len(str(right.get("body") or "")),
3053
- len(right.get("scope", {}).get("sourceFiles") or []),
3054
- str(right.get("id") or ""),
3055
- )
3056
- return left if left_key >= right_key else right
3057
-
3058
-
3059
- def merge_entry_provenance(keeper: dict[str, Any], absorbed: dict[str, Any]) -> None:
3060
- keeper["keywords"] = sorted(
3061
- {
3062
- str(item)
3063
- for item in list(keeper.get("keywords") or []) + list(absorbed.get("keywords") or [])
3064
- if item
3065
- }
3066
- )
3067
- scope = keeper.setdefault("scope", {})
3068
- files = list(scope.get("sourceFiles") or [])
3069
- for path in absorbed.get("scope", {}).get("sourceFiles") or []:
3070
- if path and path not in files:
3071
- files.append(path)
3072
- scope["sourceFiles"] = files
3073
- if scope.get("staleIfPathsChanged") is not None or absorbed.get("scope", {}).get("staleIfPathsChanged"):
3074
- patterns = list(scope.get("staleIfPathsChanged") or [])
3075
- for pattern in absorbed.get("scope", {}).get("staleIfPathsChanged") or []:
3076
- if pattern and pattern not in patterns:
3077
- patterns.append(pattern)
3078
- scope["staleIfPathsChanged"] = patterns
3079
-
3080
- lifecycle = keeper.setdefault("lifecycle", {})
3081
- merged_from = lifecycle.setdefault("mergedFrom", [])
3082
- if absorbed.get("id") and absorbed["id"] not in merged_from:
3083
- merged_from.append(absorbed["id"])
3084
- supersedes = lifecycle.setdefault("supersedes", [])
3085
- for sid in absorbed.get("lifecycle", {}).get("supersedes") or []:
3086
- if sid and sid not in supersedes:
3087
- supersedes.append(sid)
3088
- if absorbed.get("id") and absorbed["id"] not in supersedes:
3089
- supersedes.append(absorbed["id"])
3090
- for sid in absorbed.get("lifecycle", {}).get("mergedFrom") or []:
3091
- if sid and sid not in merged_from:
3092
- merged_from.append(sid)
3093
- lifecycle["lastCheckedAt"] = now_iso()
3094
-
3095
-
3096
- def supersede_entry(entry: dict[str, Any], newer_id: str, reason: str) -> None:
3097
- previous = str(entry.get("status") or "candidate")
3098
- entry["status"] = "superseded"
3099
- lifecycle = entry.setdefault("lifecycle", {})
3100
- lifecycle["previousStatus"] = previous
3101
- lifecycle["supersededBy"] = newer_id
3102
- reasons = lifecycle.setdefault("staleReasons", [])
3103
- if reason not in reasons:
3104
- reasons.append(reason)
3105
- lifecycle["lastCheckedAt"] = now_iso()
3106
-
3107
-
3108
- def dedupe_near_duplicates(
3109
- entries: list[dict[str, Any]],
3110
- threshold: float = NEAR_DUPLICATE_THRESHOLD,
3111
- ) -> dict[str, Any]:
3112
- """Merge near-duplicate entries within the same archive (in-place)."""
3113
- by_archive: dict[str, list[dict[str, Any]]] = {}
3114
- for entry in entries:
3115
- if entry.get("status") == "superseded":
3116
- continue
3117
- archive = str(entry.get("source", {}).get("archive") or "")
3118
- by_archive.setdefault(archive, []).append(entry)
3119
-
3120
- merges: list[dict[str, Any]] = []
3121
- for archive, group in by_archive.items():
3122
- remaining = list(group)
3123
- while remaining:
3124
- current = remaining.pop(0)
3125
- if current.get("status") == "superseded":
3126
- continue
3127
- cluster = [current]
3128
- still: list[dict[str, Any]] = []
3129
- for other in remaining:
3130
- if other.get("status") == "superseded":
3131
- continue
3132
- if other.get("type") != current.get("type"):
3133
- still.append(other)
3134
- continue
3135
- if entry_similarity(current, other) >= threshold:
3136
- cluster.append(other)
3137
- else:
3138
- still.append(other)
3139
- remaining = still
3140
- if len(cluster) < 2:
3141
- continue
3142
- keeper = cluster[0]
3143
- for candidate in cluster[1:]:
3144
- keeper = prefer_entry(keeper, candidate)
3145
- for absorbed in cluster:
3146
- if absorbed is keeper or absorbed.get("id") == keeper.get("id"):
3147
- continue
3148
- merge_entry_provenance(keeper, absorbed)
3149
- supersede_entry(
3150
- absorbed,
3151
- str(keeper["id"]),
3152
- "near-duplicate merged into: " + str(keeper["id"]),
3153
- )
3154
- merges.append(
3155
- {
3156
- "keptId": keeper["id"],
3157
- "mergedId": absorbed["id"],
3158
- "archive": archive,
3159
- "similarity": round(entry_similarity(keeper, absorbed), 3),
3160
- }
3161
- )
3162
- return {"merged": len(merges), "merges": merges}
3163
-
3164
-
3165
- def relocate_entry_file(
3166
- knowledge: Path,
3167
- entry: dict[str, Any],
3168
- source_path: Path | None = None,
3169
- ) -> Path:
3170
- status = str(entry.get("status") or "candidate")
3171
- if status not in ENTRY_STATUSES:
3172
- status = "candidate"
3173
- entry["status"] = status
3174
- target = knowledge / "entries" / status / entry_filename(entry)
3175
- write_json(target, entry)
3176
- if source_path is not None and source_path.exists() and source_path.resolve() != target.resolve():
3177
- source_path.unlink()
3178
- return target
3179
-
3180
-
3181
- def persist_entries_by_status(
3182
- knowledge: Path,
3183
- path_by_id: dict[str, Path],
3184
- entries: list[dict[str, Any]],
3185
- ) -> None:
3186
- for entry in entries:
3187
- entry_id = str(entry.get("id") or "")
3188
- if not entry_id:
3189
- continue
3190
- source_path = path_by_id.get(entry_id)
3191
- relocate_entry_file(knowledge, entry, source_path)
3192
-
3193
-
3194
- def dedupe_knowledge(project: Path, threshold: float = NEAR_DUPLICATE_THRESHOLD) -> dict[str, Any]:
3195
- project = project.resolve()
3196
- knowledge = project / ".harness" / "knowledge"
3197
- if not (knowledge / "index.json").exists():
3198
- build_index(project)
3199
- loaded = load_entry_files(knowledge)
3200
- path_by_id = {entry["id"]: path for path, entry in loaded}
3201
- entries = [entry for _, entry in loaded]
3202
- result = dedupe_near_duplicates(entries, threshold=threshold)
3203
- if result["merged"]:
3204
- persist_entries_by_status(knowledge, path_by_id, entries)
3205
- index = refresh_outputs_from_entry_files(project, knowledge)
3206
- else:
3207
- index = read_json(knowledge / "index.json") if (knowledge / "index.json").exists() else {}
3208
- return {
3209
- "project": str(project),
3210
- "generatedAt": now_iso(),
3211
- "merged": result["merged"],
3212
- "merges": result["merges"],
3213
- "stats": index.get("stats") if isinstance(index, dict) else {},
3214
- "paths": {
3215
- "index": str(knowledge / "index.json"),
3216
- "sqlite": str(knowledge / "index.sqlite"),
3217
- },
3218
- }
3219
-
3220
-
3221
- def is_clear_topic_evolution(older: dict[str, Any], newer: dict[str, Any]) -> bool:
3222
- """Return True only when older→newer is an obvious same-topic evolution."""
3223
- if older.get("type") != newer.get("type"):
3224
- return False
3225
- if older.get("status") in {"superseded", "conflicted"}:
3226
- return False
3227
- if newer.get("status") in {"superseded", "conflicted"}:
3228
- return False
3229
- older_archive = str(older.get("source", {}).get("archive") or "")
3230
- newer_archive = str(newer.get("source", {}).get("archive") or "")
3231
- if not older_archive or not newer_archive or older_archive == newer_archive:
3232
- return False
3233
- if archive_sort_key(newer) <= archive_sort_key(older):
3234
- return False
3235
- older_files = set(older.get("scope", {}).get("sourceFiles") or [])
3236
- newer_files = set(newer.get("scope", {}).get("sourceFiles") or [])
3237
- if not older_files or not newer_files or not (older_files & newer_files):
3238
- return False
3239
- # Semantic conflicts are left for judge.
3240
- if entries_conflict_for_review(older, newer):
3241
- return False
3242
- shared_terms = subject_terms(older) & subject_terms(newer)
3243
- similarity = entry_similarity(older, newer)
3244
- same_title = normalize_similarity_text(str(older.get("title") or "")) == normalize_similarity_text(
3245
- str(newer.get("title") or "")
3246
- )
3247
- if similarity >= AUTO_SUPERSEDE_SIMILARITY:
3248
- return True
3249
- if same_title and (shared_terms or similarity >= AUTO_SUPERSEDE_WEAK_SIMILARITY):
3250
- return True
3251
- if len(shared_terms) >= 2 and similarity >= AUTO_SUPERSEDE_WEAK_SIMILARITY:
3252
- return True
3253
- return False
3254
-
3255
-
3256
- def auto_supersede_entries(entries: list[dict[str, Any]]) -> list[dict[str, Any]]:
3257
- actions: list[dict[str, Any]] = []
3258
- ordered = sorted(entries, key=archive_sort_key)
3259
- for idx, older in enumerate(ordered):
3260
- if older.get("status") in {"superseded", "conflicted"}:
3261
- continue
3262
- for newer in ordered[idx + 1 :]:
3263
- if not is_clear_topic_evolution(older, newer):
3264
- continue
3265
- supersede_entry(
3266
- older,
3267
- str(newer["id"]),
3268
- "auto-superseded by newer same-topic entry: " + str(newer["id"]),
3269
- )
3270
- newer_life = newer.setdefault("lifecycle", {})
3271
- supersedes = newer_life.setdefault("supersedes", [])
3272
- if older["id"] not in supersedes:
3273
- supersedes.append(older["id"])
3274
- actions.append(
3275
- {
3276
- "id": older["id"],
3277
- "supersededBy": newer["id"],
3278
- "type": older.get("type"),
3279
- "overlap": sorted(
3280
- set(older.get("scope", {}).get("sourceFiles") or [])
3281
- & set(newer.get("scope", {}).get("sourceFiles") or [])
3282
- ),
3283
- }
3284
- )
3285
- break
3286
- return actions
3287
-
3288
-
3289
- def auto_supersede_knowledge(project: Path) -> dict[str, Any]:
3290
- project = project.resolve()
3291
- knowledge = project / ".harness" / "knowledge"
3292
- if not (knowledge / "index.json").exists():
3293
- build_index(project)
3294
- loaded = load_entry_files(knowledge)
3295
- path_by_id = {entry["id"]: path for path, entry in loaded}
3296
- entries = [entry for _, entry in loaded]
3297
- actions = auto_supersede_entries(entries)
3298
- if actions:
3299
- persist_entries_by_status(knowledge, path_by_id, entries)
3300
- index = refresh_outputs_from_entry_files(project, knowledge)
3301
- else:
3302
- index = read_json(knowledge / "index.json") if (knowledge / "index.json").exists() else {}
3303
- return {
3304
- "project": str(project),
3305
- "generatedAt": now_iso(),
3306
- "superseded": len(actions),
3307
- "actions": actions,
3308
- "stats": index.get("stats") if isinstance(index, dict) else {},
3309
- "paths": {
3310
- "index": str(knowledge / "index.json"),
3311
- "sqlite": str(knowledge / "index.sqlite"),
3312
- },
3313
- }
3314
-
3315
-
3316
- def previous_status_for_restore(entry: dict[str, Any]) -> str:
3317
- lifecycle = entry.get("lifecycle") or {}
3318
- previous = lifecycle.get("previousStatus")
3319
- if previous in ENTRY_STATUSES and previous != "stale":
3320
- return str(previous)
3321
- if lifecycle.get("demotedAt") or lifecycle.get("autoDemoted") or lifecycle.get("promotedAt"):
3322
- return "active"
3323
- return "candidate"
3324
-
3325
-
3326
- def reverify_stale_knowledge(project: Path) -> dict[str, Any]:
3327
- project = project.resolve()
3328
- knowledge = project / ".harness" / "knowledge"
3329
- if not (knowledge / "index.json").exists():
3330
- build_index(project)
3331
- config = knowledge_validation_config(load_config(knowledge))
3332
- restored: list[dict[str, Any]] = []
3333
- kept_stale: list[dict[str, Any]] = []
3334
- skipped: list[dict[str, Any]] = []
3335
-
3336
- for source_path, entry in load_entry_files(knowledge, ["stale"]):
3337
- validators = entry.get("validators")
3338
- if not isinstance(validators, list) or not validators:
3339
- skipped.append({"id": entry["id"], "reason": "no validators"})
3340
- continue
3341
- usable = [
3342
- validator
3343
- for validator in validators
3344
- if isinstance(validator, dict)
3345
- and str(validator.get("type") or "") in {"file_exists", "file_contains"}
3346
- ]
3347
- if not usable:
3348
- skipped.append({"id": entry["id"], "reason": "no file_exists/file_contains validators"})
3349
- continue
3350
-
3351
- results = [evaluate_validator(project, entry, validator, config) for validator in usable]
3352
- statuses = [str(result.get("status")) for result in results]
3353
- validation = {
3354
- "validatedAt": now_iso(),
3355
- "status": "failed" if "failed" in statuses else ("passed" if "passed" in statuses else "skipped"),
3356
- "results": results,
3357
- "reverify": True,
3358
- }
3359
- lifecycle = entry.setdefault("lifecycle", {})
3360
- lifecycle["validation"] = validation
3361
- lifecycle["lastCheckedAt"] = validation["validatedAt"]
3362
-
3363
- if validation["status"] == "passed":
3364
- target_status = previous_status_for_restore(entry)
3365
- entry["status"] = target_status
3366
- lifecycle.pop("demotedAt", None)
3367
- lifecycle.pop("demotionReason", None)
3368
- lifecycle.pop("autoDemoted", None)
3369
- stale_reasons = [
3370
- reason
3371
- for reason in (lifecycle.get("staleReasons") or [])
3372
- if "validator failed" not in str(reason) and "reverify failed" not in str(reason)
3373
- ]
3374
- lifecycle["staleReasons"] = stale_reasons
3375
- lifecycle["reverifiedAt"] = validation["validatedAt"]
3376
- relocate_entry_file(knowledge, entry, source_path)
3377
- restored.append({"id": entry["id"], "status": target_status})
3378
- else:
3379
- reason = "reverify failed: " + first_failed_validator_message(validation)
3380
- reasons = lifecycle.setdefault("staleReasons", [])
3381
- if reason not in reasons:
3382
- reasons.append(reason)
3383
- write_json(source_path, entry)
3384
- kept_stale.append({"id": entry["id"], "reason": reason})
3385
-
3386
- index = refresh_outputs_from_entry_files(project, knowledge)
3387
- return {
3388
- "project": str(project),
3389
- "generatedAt": now_iso(),
3390
- "restored": len(restored),
3391
- "keptStale": len(kept_stale),
3392
- "skipped": len(skipped),
3393
- "entries": {
3394
- "restored": restored,
3395
- "keptStale": kept_stale,
3396
- "skipped": skipped,
3397
- },
3398
- "stats": index.get("stats", {}),
3399
- "paths": {
3400
- "index": str(knowledge / "index.json"),
3401
- "sqlite": str(knowledge / "index.sqlite"),
3402
- },
3403
- }
3404
-
3405
-
3406
- def snapshot_entry_state(entry: dict[str, Any]) -> dict[str, Any]:
3407
- return json_clone(
3408
- {
3409
- "schemaVersion": entry.get("schemaVersion", 1),
3410
- "id": entry.get("id"),
3411
- "projectId": entry.get("projectId"),
3412
- "status": entry.get("status"),
3413
- "title": entry.get("title"),
3414
- "type": entry.get("type"),
3415
- "lifecycle": entry.get("lifecycle") or {},
3416
- "scope": entry.get("scope") or {},
3417
- "keywords": entry.get("keywords") or [],
3418
- "body": entry.get("body"),
3419
- "summary": entry.get("summary"),
3420
- "source": entry.get("source") or {},
3421
- "validators": entry.get("validators"),
3422
- "confidence": entry.get("confidence"),
3423
- }
3424
- )
3425
-
3426
-
3427
- def judge_export(project: Path) -> dict[str, Any]:
3428
- project = project.resolve()
3429
- knowledge = project / ".harness" / "knowledge"
3430
- if not (knowledge / "index.json").exists():
3431
- build_index(project)
3432
- entries = [entry for _, entry in load_entry_files(knowledge)]
3433
- by_id = {entry["id"]: entry for entry in entries}
3434
-
3435
- conflicts: list[dict[str, Any]] = []
3436
- seen_pairs: set[tuple[str, str]] = set()
3437
- for entry in entries:
3438
- if entry.get("status") != "conflicted":
3439
- continue
3440
- for other_id in entry.get("lifecycle", {}).get("conflictsWith") or []:
3441
- pair = tuple(sorted([entry["id"], str(other_id)]))
3442
- if pair in seen_pairs:
3443
- continue
3444
- seen_pairs.add(pair)
3445
- other = by_id.get(str(other_id))
3446
- conflicts.append(
3447
- {
3448
- "kind": "conflict",
3449
- "ids": list(pair),
3450
- "entries": [
3451
- {
3452
- "id": entry["id"],
3453
- "type": entry.get("type"),
3454
- "status": entry.get("status"),
3455
- "title": entry.get("title"),
3456
- "body": entry.get("body"),
3457
- "summary": entry.get("summary"),
3458
- "source": entry.get("source"),
3459
- "scope": entry.get("scope"),
3460
- "lifecycle": {
3461
- "conflictsWith": entry.get("lifecycle", {}).get("conflictsWith") or [],
3462
- "staleReasons": entry.get("lifecycle", {}).get("staleReasons") or [],
3463
- },
3464
- },
3465
- {
3466
- "id": other.get("id") if other else other_id,
3467
- "type": other.get("type") if other else None,
3468
- "status": other.get("status") if other else None,
3469
- "title": other.get("title") if other else None,
3470
- "body": other.get("body") if other else None,
3471
- "summary": other.get("summary") if other else None,
3472
- "source": other.get("source") if other else None,
3473
- "scope": other.get("scope") if other else None,
3474
- "lifecycle": {
3475
- "conflictsWith": (other.get("lifecycle", {}) or {}).get("conflictsWith") or [],
3476
- "staleReasons": (other.get("lifecycle", {}) or {}).get("staleReasons") or [],
3477
- }
3478
- if other
3479
- else {},
3480
- },
3481
- ],
3482
- }
3483
- )
3484
-
3485
- promote_candidates: list[dict[str, Any]] = []
3486
- for entry in entries:
3487
- if entry.get("status") != "candidate":
3488
- continue
3489
- promote_candidates.append(
3490
- {
3491
- "kind": "promote-candidate",
3492
- "id": entry["id"],
3493
- "type": entry.get("type"),
3494
- "status": entry.get("status"),
3495
- "title": entry.get("title"),
3496
- "body": entry.get("body"),
3497
- "summary": entry.get("summary"),
3498
- "source": entry.get("source"),
3499
- "scope": entry.get("scope"),
3500
- "confidence": entry.get("confidence"),
3501
- "lifecycle": {
3502
- "staleReasons": entry.get("lifecycle", {}).get("staleReasons") or [],
3503
- "conflictsWith": entry.get("lifecycle", {}).get("conflictsWith") or [],
3504
- },
3505
- }
3506
- )
3507
-
3508
- payload = {
3509
- "schemaVersion": 1,
3510
- "generatedAt": now_iso(),
3511
- "project": str(project),
3512
- "manualReview": manual_review_enabled(project),
3513
- "counts": {
3514
- "conflicts": len(conflicts),
3515
- "promoteCandidates": len(promote_candidates),
3516
- "pending": len(conflicts) + len(promote_candidates),
3517
- },
3518
- "conflicts": conflicts,
3519
- "promoteCandidates": promote_candidates,
3520
- "actions": sorted(JUDGE_ACTIONS),
3521
- }
3522
- export_path = knowledge / "reports" / f"judge-export-{timestamp()}.json"
3523
- write_json(export_path, payload)
3524
- payload["exportPath"] = str(export_path)
3525
- return payload
3526
-
3527
-
3528
- def apply_judge_decision(
3529
- knowledge: Path,
3530
- entry: dict[str, Any],
3531
- source_path: Path,
3532
- decision: dict[str, Any],
3533
- by_id: dict[str, dict[str, Any]],
3534
- path_by_id: dict[str, Path],
3535
- ) -> dict[str, Any]:
3536
- action = str(decision.get("action") or "")
3537
- reason = str(decision.get("reason") or "")
3538
- before = snapshot_entry_state(entry)
3539
- after_status = entry.get("status")
3540
-
3541
- if action == "promote":
3542
- entry["status"] = "active"
3543
- lifecycle = entry.setdefault("lifecycle", {})
3544
- lifecycle["promotedAt"] = now_iso()
3545
- lifecycle["promotionNote"] = reason or "judge promote"
3546
- lifecycle["judgeAction"] = action
3547
- lifecycle["lastCheckedAt"] = now_iso()
3548
- # Clear conflict markers when promoting a resolved conflict side.
3549
- lifecycle["conflictsWith"] = []
3550
- stale_reasons = [
3551
- item
3552
- for item in (lifecycle.get("staleReasons") or [])
3553
- if "potential conflict with" not in str(item)
3554
- ]
3555
- lifecycle["staleReasons"] = stale_reasons
3556
- relocate_entry_file(knowledge, entry, source_path)
3557
- path_by_id[entry["id"]] = knowledge / "entries" / "active" / entry_filename(entry)
3558
- after_status = "active"
3559
- elif action == "drop":
3560
- supersede_entry(entry, str(decision.get("supersededBy") or entry["id"]), reason or "judge drop")
3561
- lifecycle = entry.setdefault("lifecycle", {})
3562
- lifecycle["judgeAction"] = action
3563
- if not decision.get("supersededBy"):
3564
- lifecycle["supersededBy"] = None
3565
- lifecycle["droppedByJudge"] = True
3566
- relocate_entry_file(knowledge, entry, source_path)
3567
- path_by_id[entry["id"]] = knowledge / "entries" / "superseded" / entry_filename(entry)
3568
- after_status = "superseded"
3569
- elif action == "supersede":
3570
- target_id = str(decision.get("supersededBy") or "")
3571
- if not target_id:
3572
- raise ValueError(f"supersede action requires supersededBy for entry {entry['id']}")
3573
- supersede_entry(entry, target_id, reason or ("judge supersede by: " + target_id))
3574
- entry.setdefault("lifecycle", {})["judgeAction"] = action
3575
- target = by_id.get(target_id)
3576
- if target is not None:
3577
- supersedes = target.setdefault("lifecycle", {}).setdefault("supersedes", [])
3578
- if entry["id"] not in supersedes:
3579
- supersedes.append(entry["id"])
3580
- target_path = path_by_id.get(target_id)
3581
- relocate_entry_file(knowledge, target, target_path)
3582
- path_by_id[target_id] = knowledge / "entries" / str(target.get("status")) / entry_filename(target)
3583
- relocate_entry_file(knowledge, entry, source_path)
3584
- path_by_id[entry["id"]] = knowledge / "entries" / "superseded" / entry_filename(entry)
3585
- after_status = "superseded"
3586
- elif action == "keep-conflict":
3587
- entry["status"] = "conflicted"
3588
- lifecycle = entry.setdefault("lifecycle", {})
3589
- lifecycle["judgeAction"] = action
3590
- lifecycle["judgeReason"] = reason
3591
- lifecycle["lastCheckedAt"] = now_iso()
3592
- relocate_entry_file(knowledge, entry, source_path)
3593
- path_by_id[entry["id"]] = knowledge / "entries" / "conflicted" / entry_filename(entry)
3594
- after_status = "conflicted"
3595
- else:
3596
- raise ValueError(f"unsupported judge action: {action}")
3597
-
3598
- return {
3599
- "id": entry["id"],
3600
- "action": action,
3601
- "reason": reason,
3602
- "before": before,
3603
- "after": {"status": after_status, "supersededBy": entry.get("lifecycle", {}).get("supersededBy")},
3604
- "supersededBy": decision.get("supersededBy"),
3605
- }
3606
-
3607
-
3608
- def judge_apply(project: Path, decisions_path: Path, force: bool = False) -> dict[str, Any]:
3609
- project = project.resolve()
3610
- knowledge = project / ".harness" / "knowledge"
3611
- if not (knowledge / "index.json").exists():
3612
- build_index(project)
3613
- if manual_review_enabled(project) and not force:
3614
- raise ValueError("knowledge.manualReview=true; pass --force to apply judgements")
3615
-
3616
- raw = read_json(decisions_path)
3617
- if isinstance(raw, dict):
3618
- decisions = raw.get("decisions")
3619
- else:
3620
- decisions = raw
3621
- if not isinstance(decisions, list):
3622
- raise ValueError("decisions file must be a list or an object with decisions[]")
3623
-
3624
- loaded = load_entry_files(knowledge)
3625
- path_by_id = {entry["id"]: path for path, entry in loaded}
3626
- by_id = {entry["id"]: entry for _, entry in loaded}
3627
- applied: list[dict[str, Any]] = []
3628
- errors: list[dict[str, str]] = []
3629
-
3630
- for item in decisions:
3631
- if not isinstance(item, dict):
3632
- errors.append({"error": "decision must be an object"})
3633
- continue
3634
- entry_id = str(item.get("id") or "")
3635
- action = str(item.get("action") or "")
3636
- if not entry_id or action not in JUDGE_ACTIONS:
3637
- errors.append({"id": entry_id, "error": f"invalid decision: action={action}"})
3638
- continue
3639
- found = by_id.get(entry_id)
3640
- source_path = path_by_id.get(entry_id)
3641
- if found is None or source_path is None:
3642
- errors.append({"id": entry_id, "error": "entry not found"})
3643
- continue
3644
- try:
3645
- record = apply_judge_decision(knowledge, found, source_path, item, by_id, path_by_id)
3646
- applied.append(record)
3647
- except ValueError as exc:
3648
- errors.append({"id": entry_id, "error": str(exc)})
3649
-
3650
- judgement = {
3651
- "schemaVersion": 1,
3652
- "generatedAt": now_iso(),
3653
- "project": str(project),
3654
- "sourceDecisions": str(decisions_path),
3655
- "manualReview": manual_review_enabled(project),
3656
- "forced": force,
3657
- "applied": applied,
3658
- "errors": errors,
3659
- }
3660
- judgement_path = knowledge / "reports" / f"judgements-{timestamp()}.json"
3661
- write_json(judgement_path, judgement)
3662
- index = refresh_outputs_from_entry_files(project, knowledge)
3663
- return {
3664
- "project": str(project),
3665
- "generatedAt": judgement["generatedAt"],
3666
- "judgement": str(judgement_path),
3667
- "applied": len(applied),
3668
- "errors": errors,
3669
- "stats": index.get("stats", {}),
3670
- "paths": {
3671
- "judgement": str(judgement_path),
3672
- "index": str(knowledge / "index.json"),
3673
- "sqlite": str(knowledge / "index.sqlite"),
3674
- },
3675
- }
3676
-
3677
-
3678
- def restore_entry_from_snapshot(knowledge: Path, before: dict[str, Any], current_path: Path | None) -> Path:
3679
- entry = json_clone(before)
3680
- if not entry.get("projectId"):
3681
- entry["projectId"] = "unknown"
3682
- if "schemaVersion" not in entry:
3683
- entry["schemaVersion"] = 1
3684
- status = str(entry.get("status") or "candidate")
3685
- if status not in ENTRY_STATUSES:
3686
- status = "candidate"
3687
- entry["status"] = status
3688
- target = knowledge / "entries" / status / entry_filename(entry)
3689
- write_json(target, entry)
3690
- if current_path is not None and current_path.exists() and current_path.resolve() != target.resolve():
3691
- current_path.unlink()
3692
- # Remove any stray copies of the same id in other status dirs.
3693
- for status_name in ENTRY_STATUSES:
3694
- for path in (knowledge / "entries" / status_name).glob("*.json"):
3695
- if path.resolve() == target.resolve():
3696
- continue
3697
- try:
3698
- other = read_json(path)
3699
- except (OSError, json.JSONDecodeError):
3700
- continue
3701
- if isinstance(other, dict) and other.get("id") == entry.get("id"):
3702
- path.unlink()
3703
- return target
3704
-
3705
-
3706
- def rollback_judgement(project: Path, judgement_path: Path) -> dict[str, Any]:
3707
- project = project.resolve()
3708
- knowledge = project / ".harness" / "knowledge"
3709
- judgement = read_json(judgement_path)
3710
- if not isinstance(judgement, dict):
3711
- raise ValueError("judgement file must be a JSON object")
3712
- applied = judgement.get("applied")
3713
- if not isinstance(applied, list):
3714
- raise ValueError("judgement file missing applied[]")
3715
-
3716
- restored: list[dict[str, Any]] = []
3717
- for item in reversed(applied):
3718
- if not isinstance(item, dict):
3719
- continue
3720
- before = item.get("before")
3721
- if not isinstance(before, dict) or not before.get("id"):
3722
- continue
3723
- entry_id = str(before["id"])
3724
- found = find_entry_file(knowledge, entry_id, list(ENTRY_STATUSES))
3725
- current_path = found[0] if found else None
3726
- restore_entry_from_snapshot(knowledge, before, current_path)
3727
- restored.append({"id": entry_id, "status": before.get("status")})
3728
-
3729
- index = refresh_outputs_from_entry_files(project, knowledge)
3730
- return {
3731
- "project": str(project),
3732
- "generatedAt": now_iso(),
3733
- "judgement": str(judgement_path),
3734
- "restored": len(restored),
3735
- "entries": restored,
3736
- "stats": index.get("stats", {}),
3737
- "paths": {
3738
- "index": str(knowledge / "index.json"),
3739
- "sqlite": str(knowledge / "index.sqlite"),
3740
- },
3741
- }
3742
-
3743
-
3744
- def _outbox_root(project: Path) -> Path:
3745
- return project / ".harness" / "knowledge" / "maintenance-outbox"
3746
-
3747
-
3748
- def _outbox_item_path(outbox_root: Path, status: str, archive_id: str) -> Path:
3749
- return outbox_root / status / f"{archive_id}.json"
3750
-
3751
-
3752
- def _move_outbox(
3753
- outbox_root: Path, archive_id: str, from_status: str, to_status: str, item: dict[str, Any]
3754
- ) -> None:
3755
- src = _outbox_item_path(outbox_root, from_status, archive_id)
3756
- dst = _outbox_item_path(outbox_root, to_status, archive_id)
3757
- dst.parent.mkdir(parents=True, exist_ok=True)
3758
- write_json(dst, item)
3759
- if src.exists() and src.resolve() != dst.resolve():
3760
- src.unlink()
3761
-
3762
-
3763
- def claim_outbox(outbox_root: Path, archive_id: str) -> tuple[dict[str, Any] | None, str | None]:
3764
- """Atomically claim a pending or failed item -> running. Retryable from failed.
3765
- Returns (item, current_status). If already running/completed, returns it as-is."""
3766
- for from_status in ("pending", "failed", "pending-judge"):
3767
- src = _outbox_item_path(outbox_root, from_status, archive_id)
3768
- if src.is_file():
3769
- try:
3770
- dst = _outbox_item_path(outbox_root, "running", archive_id)
3771
- dst.parent.mkdir(parents=True, exist_ok=True)
3772
- # os.replace is the ownership boundary: only the process that
3773
- # wins this rename may perform maintenance for this archive.
3774
- src.replace(dst)
3775
- item = read_json(dst)
3776
- except FileNotFoundError:
3777
- continue
3778
- except (OSError, json.JSONDecodeError):
3779
- item = {
3780
- "schemaVersion": 1,
3781
- "archiveId": archive_id,
3782
- "status": from_status,
3783
- "attempts": 0,
3784
- }
3785
- item["status"] = "running"
3786
- write_json(_outbox_item_path(outbox_root, "running", archive_id), item)
3787
- return item, "running"
3788
- for status in ("running", "completed", "pending-judge"):
3789
- p = _outbox_item_path(outbox_root, status, archive_id)
3790
- if p.is_file():
3791
- try:
3792
- return read_json(p), status
3793
- except (OSError, json.JSONDecodeError):
3794
- pass
3795
- return None, None
3796
-
3797
-
3798
- def maintain_knowledge(project: Path, archive_id: str) -> dict[str, Any]:
3799
- """§8.3: single-process maintenance. Claim pending->running, incremental
3800
- ingest (build_index already does in-memory near-dedupe -- do NOT re-run a
3801
- disk-based dedupe pass), auto-supersede, reverify-stale, export residual
3802
- judge checklist, running->completed (or completed_rules_pending_judge).
3803
- Idempotent for completed items. Failure -> failed, attempts+1, retryable."""
3804
- project = project.resolve()
3805
- outbox_root = _outbox_root(project)
3806
- outbox_root.mkdir(parents=True, exist_ok=True)
3807
-
3808
- item, status = claim_outbox(outbox_root, archive_id)
3809
- if item is None:
3810
- return {
3811
- "ok": False,
3812
- "archiveId": archive_id,
3813
- "status": "not-found",
3814
- "error": "no pending/failed/running/completed outbox item",
3815
- }
3816
- if status == "completed":
3817
- return {
3818
- "ok": True,
3819
- "archiveId": archive_id,
3820
- "status": str(item.get("status") or "completed"),
3821
- "pendingJudgements": int(item.get("pendingJudgements") or 0),
3822
- "idempotent": True,
3823
- }
3824
-
3825
- try:
3826
- # 2-3. incremental ingest (in-memory near-dedupe inside build_index)
3827
- build_index(project, incremental=True)
3828
- # A failure here means the archive has not completed maintenance.
3829
- # Never convert that failure into a successful completed outbox entry.
3830
- auto_supersede_knowledge(project)
3831
- reverify_stale_knowledge(project)
3832
- # 6. export residual judge checklist
3833
- judge_result = judge_export(project)
3834
- pending = int((judge_result.get("counts") or {}).get("pending") or 0)
3835
- if pending > 0:
3836
- pj_path = (
3837
- project
3838
- / ".harness"
3839
- / "knowledge"
3840
- / "reports"
3841
- / f"pending-judgements-{archive_id}.json"
3842
- )
3843
- write_json(pj_path, judge_result)
3844
- final_status = "pending-judge"
3845
- else:
3846
- final_status = "completed"
3847
- item["status"] = final_status
3848
- item["pendingJudgements"] = pending
3849
- item["completedAt"] = now_iso()
3850
- item["lastError"] = None
3851
- _move_outbox(outbox_root, archive_id, "running", final_status, item)
3852
- return {
3853
- "ok": True,
3854
- "archiveId": archive_id,
3855
- "status": final_status,
3856
- "pendingJudgements": pending,
3857
- }
3858
- except Exception as exc:
3859
- item["status"] = "failed"
3860
- item["attempts"] = int(item.get("attempts") or 0) + 1
3861
- item["lastError"] = str(exc)
3862
- item["failedAt"] = now_iso()
3863
- _move_outbox(outbox_root, archive_id, "running", "failed", item)
3864
- return {
3865
- "ok": False,
3866
- "archiveId": archive_id,
3867
- "status": "failed",
3868
- "error": str(exc),
3869
- "attempts": item["attempts"],
3870
- }
3871
-
3872
-
3873
- def main(argv: list[str] | None = None) -> int:
3874
- parser = argparse.ArgumentParser(description="Build and query Harness knowledge indexes.")
3875
- sub = parser.add_subparsers(dest="command", required=True)
3876
-
3877
- ingest = sub.add_parser("ingest", help="Build .harness/knowledge from .harness/archive")
3878
- ingest.add_argument("--project", default=".", help="Project root containing .harness/archive")
3879
- ingest.add_argument(
3880
- "--no-incremental",
3881
- action="store_true",
3882
- help="Re-extract every archive instead of reusing the archive entry cache",
3883
- )
3884
-
3885
- sync = sub.add_parser("sync", help="Check whether .harness/knowledge is current")
3886
- sync.add_argument("--project", default=".", help="Project root containing .harness/archive")
3887
- sync.add_argument("--update", action="store_true", help="Rebuild the index when it is out of date")
3888
- sync.add_argument(
3889
- "--no-incremental",
3890
- action="store_true",
3891
- help="When used with --update, rebuild without reusing the archive entry cache",
3892
- )
3893
-
3894
- auto = sub.add_parser("auto", help="Run the default automated knowledge maintenance workflow")
3895
- auto.add_argument("--project", default=".", help="Project root containing .harness/archive")
3896
- auto.add_argument("--limit", type=int, default=20, help="Maximum entries to suggest validators for")
3897
- auto.add_argument("--audit-limit", type=int, default=10, help="Maximum audit entries per review section")
3898
- auto.add_argument(
3899
- "--suggest-status",
3900
- action="append",
3901
- dest="suggest_statuses",
3902
- default=[],
3903
- help="Only suggest validators for this lifecycle status",
3904
- )
3905
- auto.add_argument("--apply-suggestions", action="store_true", help="Write validator suggestions into entry JSON files")
3906
- auto.add_argument("--no-incremental", action="store_true", help="Refresh without reusing the archive entry cache")
3907
-
3908
- audit = sub.add_parser("audit", help="Generate review lists for candidate, stale, and superseded entries")
3909
- audit.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3910
- audit.add_argument("--limit", type=int, default=10)
3911
-
3912
- verify = sub.add_parser("verify", help="Run configured validators against knowledge entries")
3913
- verify.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3914
-
3915
- suggest = sub.add_parser("suggest-validators", help="Suggest deterministic validators for knowledge entries")
3916
- suggest.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3917
- suggest.add_argument("--limit", type=int, default=20, help="Maximum entries to suggest validators for")
3918
- suggest.add_argument("--status", action="append", dest="statuses", default=[], help="Only suggest for this lifecycle status")
3919
- suggest.add_argument("--apply", action="store_true", help="Write suggested validators into entry JSON files")
3920
-
3921
- query = sub.add_parser("query", help="Query .harness/knowledge and generate a context pack")
3922
- query.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3923
- query.add_argument("--query", required=True, help="Need, question, keyword, or file path to search")
3924
- query.add_argument("--limit", type=int, default=10)
3925
- query.add_argument("--file", action="append", dest="files", default=[], help="Only return entries linked to this source file")
3926
- query.add_argument("--status", action="append", dest="statuses", default=[], help="Only return entries with this lifecycle status")
3927
- query.add_argument("--type", action="append", dest="types", default=[], help="Only return entries of this knowledge type")
3928
-
3929
- promote = sub.add_parser("promote", help="Promote a candidate knowledge entry to active")
3930
- promote.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3931
- promote.add_argument("--id", required=True, help="Knowledge entry id to promote")
3932
- promote.add_argument("--note", default="", help="Manual verification note")
3933
- promote.add_argument(
3934
- "--allow-stale",
3935
- action="store_true",
3936
- help="Allow promoting a stale entry after manual verification",
3937
- )
3938
-
3939
- demote = sub.add_parser("demote", help="Demote an active entry after manual review")
3940
- demote.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3941
- demote.add_argument("--id", required=True, help="Active knowledge entry id to demote")
3942
- demote.add_argument("--status", choices=["candidate", "stale"], required=True, help="Target lifecycle status")
3943
- demote.add_argument("--reason", required=True, help="Manual demotion reason")
3944
-
3945
- dedupe = sub.add_parser("dedupe", help="Merge near-duplicate entries within the same archive")
3946
- dedupe.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3947
- dedupe.add_argument("--json", action="store_true", help="Emit machine-readable JSON (default behavior)")
3948
-
3949
- auto_supersede = sub.add_parser(
3950
- "auto-supersede",
3951
- help="Supersede older same-topic entries when evolution is clear",
3952
- )
3953
- auto_supersede.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3954
- auto_supersede.add_argument("--json", action="store_true", help="Emit machine-readable JSON (default behavior)")
3955
-
3956
- reverify = sub.add_parser(
3957
- "reverify-stale",
3958
- help="Re-run file_exists/file_contains validators on stale entries",
3959
- )
3960
- reverify.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3961
- reverify.add_argument("--json", action="store_true", help="Emit machine-readable JSON (default behavior)")
3962
-
3963
- judge = sub.add_parser("judge", help="Export or apply AI judgement decisions")
3964
- judge.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3965
- judge_mode = judge.add_mutually_exclusive_group(required=True)
3966
- judge_mode.add_argument("--export", metavar="FILE", help="Write pending judgement checklist JSON")
3967
- judge_mode.add_argument("--apply", metavar="FILE", help="Apply decisions JSON and write judgement log")
3968
- judge.add_argument(
3969
- "--force",
3970
- action="store_true",
3971
- help="Required when knowledge.manualReview=true before applying judgements",
3972
- )
3973
- judge.add_argument("--json", action="store_true", help="Emit machine-readable JSON (default behavior)")
3974
-
3975
- rollback = sub.add_parser("rollback", help="Restore entry states from a judgement decision log")
3976
- rollback.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3977
- rollback.add_argument("--judgement", required=True, help="Path to judgements-*.json decision log")
3978
- rollback.add_argument("--json", action="store_true", help="Emit machine-readable JSON (default behavior)")
3979
-
3980
- maintain = sub.add_parser(
3981
- "maintain",
3982
- help="Single-process maintenance: claim outbox, ingest+dedupe, supersede, reverify, judge-export",
3983
- )
3984
- maintain.add_argument("--project", default=".", help="Project root containing .harness/knowledge")
3985
- maintain.add_argument("--archive-id", required=True, help="Archive id of the outbox item to maintain")
3986
- maintain.add_argument("--json", action="store_true", help="Emit machine-readable JSON (default behavior)")
3987
-
3988
- args = parser.parse_args(argv)
3989
- if args.command == "ingest":
3990
- index = build_index(Path(args.project), incremental=not args.no_incremental)
3991
- print(json.dumps(summarize_index(index), ensure_ascii=False, indent=2))
3992
- return 0
3993
- if args.command == "sync":
3994
- result = sync_status(Path(args.project), args.update, incremental=not args.no_incremental)
3995
- print(json.dumps(result, ensure_ascii=False, indent=2))
3996
- return 0
3997
- if args.command == "auto":
3998
- result = auto_knowledge(
3999
- Path(args.project),
4000
- limit=args.limit,
4001
- suggest_statuses=args.suggest_statuses,
4002
- apply_suggestions=args.apply_suggestions,
4003
- incremental=not args.no_incremental,
4004
- audit_limit=args.audit_limit,
4005
- )
4006
- print(json.dumps(result, ensure_ascii=False, indent=2))
4007
- return 0
4008
- if args.command == "audit":
4009
- result = audit_entries(Path(args.project), args.limit)
4010
- print(json.dumps(result, ensure_ascii=False, indent=2))
4011
- return 0
4012
- if args.command == "verify":
4013
- result = verify_knowledge(Path(args.project))
4014
- print(json.dumps(result, ensure_ascii=False, indent=2))
4015
- return 0
4016
- if args.command == "suggest-validators":
4017
- result = suggest_validators(Path(args.project), args.limit, args.statuses, args.apply)
4018
- print(json.dumps(result, ensure_ascii=False, indent=2))
4019
- return 0
4020
- if args.command == "query":
4021
- result = query_index(Path(args.project), args.query, args.limit, args.files, args.statuses, args.types)
4022
- print(json.dumps(result, ensure_ascii=False, indent=2))
4023
- return 0
4024
- if args.command == "promote":
4025
- try:
4026
- result = promote_entry(Path(args.project), args.id, args.note, args.allow_stale)
4027
- except ValueError as exc:
4028
- print(str(exc), file=sys.stderr)
4029
- return 1
4030
- print(json.dumps(result, ensure_ascii=False, indent=2))
4031
- return 0
4032
- if args.command == "demote":
4033
- try:
4034
- result = demote_entry(Path(args.project), args.id, args.status, args.reason)
4035
- except ValueError as exc:
4036
- print(str(exc), file=sys.stderr)
4037
- return 1
4038
- print(json.dumps(result, ensure_ascii=False, indent=2))
4039
- return 0
4040
- if args.command == "dedupe":
4041
- result = dedupe_knowledge(Path(args.project))
4042
- print(json.dumps(result, ensure_ascii=False, indent=2))
4043
- return 0
4044
- if args.command == "auto-supersede":
4045
- result = auto_supersede_knowledge(Path(args.project))
4046
- print(json.dumps(result, ensure_ascii=False, indent=2))
4047
- return 0
4048
- if args.command == "reverify-stale":
4049
- result = reverify_stale_knowledge(Path(args.project))
4050
- print(json.dumps(result, ensure_ascii=False, indent=2))
4051
- return 0
4052
- if args.command == "judge":
4053
- try:
4054
- if args.export:
4055
- result = judge_export(Path(args.project))
4056
- export_target = Path(args.export)
4057
- write_json(export_target, {k: v for k, v in result.items() if k != "exportPath"})
4058
- result["exportPath"] = str(export_target.resolve())
4059
- else:
4060
- result = judge_apply(Path(args.project), Path(args.apply), force=args.force)
4061
- except (OSError, json.JSONDecodeError, ValueError) as exc:
4062
- print(str(exc), file=sys.stderr)
4063
- return 1
4064
- print(json.dumps(result, ensure_ascii=False, indent=2))
4065
- return 0
4066
- if args.command == "rollback":
4067
- try:
4068
- result = rollback_judgement(Path(args.project), Path(args.judgement))
4069
- except (OSError, json.JSONDecodeError, ValueError) as exc:
4070
- print(str(exc), file=sys.stderr)
4071
- return 1
4072
- print(json.dumps(result, ensure_ascii=False, indent=2))
4073
- return 0
4074
- if args.command == "maintain":
4075
- result = maintain_knowledge(Path(args.project), args.archive_id)
4076
- print(json.dumps(result, ensure_ascii=False, indent=2))
4077
- return 0 if result.get("ok") else 1
4078
- return 2
4079
-
4080
-
4081
- if __name__ == "__main__":
4082
- raise SystemExit(main())