@ahmed-g-gad/apothem 0.1.1

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 (674) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +21 -0
  3. package/LICENSES/MIT.txt +18 -0
  4. package/LICENSES/PSF-2.0.txt +47 -0
  5. package/README.md +549 -0
  6. package/bin/README.md +37 -0
  7. package/bin/apothem.mjs +78 -0
  8. package/package.json +75 -0
  9. package/pyproject.toml +347 -0
  10. package/src/apothem/README.md +52 -0
  11. package/src/apothem/__init__.py +66 -0
  12. package/src/apothem/__main__.py +28 -0
  13. package/src/apothem/_vendor/.keep +0 -0
  14. package/src/apothem/_vendor/__init__.py +25 -0
  15. package/src/apothem/_vendor/attr/__init__.py +104 -0
  16. package/src/apothem/_vendor/attr/__init__.pyi +389 -0
  17. package/src/apothem/_vendor/attr/_cmp.py +160 -0
  18. package/src/apothem/_vendor/attr/_cmp.pyi +13 -0
  19. package/src/apothem/_vendor/attr/_compat.py +99 -0
  20. package/src/apothem/_vendor/attr/_config.py +31 -0
  21. package/src/apothem/_vendor/attr/_funcs.py +497 -0
  22. package/src/apothem/_vendor/attr/_make.py +3406 -0
  23. package/src/apothem/_vendor/attr/_next_gen.py +674 -0
  24. package/src/apothem/_vendor/attr/_typing_compat.pyi +15 -0
  25. package/src/apothem/_vendor/attr/_version_info.py +89 -0
  26. package/src/apothem/_vendor/attr/_version_info.pyi +9 -0
  27. package/src/apothem/_vendor/attr/converters.py +162 -0
  28. package/src/apothem/_vendor/attr/converters.pyi +19 -0
  29. package/src/apothem/_vendor/attr/exceptions.py +95 -0
  30. package/src/apothem/_vendor/attr/exceptions.pyi +17 -0
  31. package/src/apothem/_vendor/attr/filters.py +72 -0
  32. package/src/apothem/_vendor/attr/filters.pyi +6 -0
  33. package/src/apothem/_vendor/attr/py.typed +0 -0
  34. package/src/apothem/_vendor/attr/setters.py +79 -0
  35. package/src/apothem/_vendor/attr/setters.pyi +20 -0
  36. package/src/apothem/_vendor/attr/validators.py +750 -0
  37. package/src/apothem/_vendor/attr/validators.pyi +140 -0
  38. package/src/apothem/_vendor/attr.LICENSE +21 -0
  39. package/src/apothem/_vendor/attrs/__init__.py +72 -0
  40. package/src/apothem/_vendor/attrs/__init__.pyi +314 -0
  41. package/src/apothem/_vendor/attrs/converters.py +3 -0
  42. package/src/apothem/_vendor/attrs/exceptions.py +3 -0
  43. package/src/apothem/_vendor/attrs/filters.py +3 -0
  44. package/src/apothem/_vendor/attrs/py.typed +0 -0
  45. package/src/apothem/_vendor/attrs/setters.py +3 -0
  46. package/src/apothem/_vendor/attrs/validators.py +3 -0
  47. package/src/apothem/_vendor/attrs.LICENSE +21 -0
  48. package/src/apothem/_vendor/jsonschema/__init__.py +120 -0
  49. package/src/apothem/_vendor/jsonschema/__main__.py +6 -0
  50. package/src/apothem/_vendor/jsonschema/_format.py +546 -0
  51. package/src/apothem/_vendor/jsonschema/_keywords.py +449 -0
  52. package/src/apothem/_vendor/jsonschema/_legacy_keywords.py +449 -0
  53. package/src/apothem/_vendor/jsonschema/_types.py +204 -0
  54. package/src/apothem/_vendor/jsonschema/_typing.py +29 -0
  55. package/src/apothem/_vendor/jsonschema/_utils.py +355 -0
  56. package/src/apothem/_vendor/jsonschema/benchmarks/__init__.py +5 -0
  57. package/src/apothem/_vendor/jsonschema/benchmarks/const_vs_enum.py +30 -0
  58. package/src/apothem/_vendor/jsonschema/benchmarks/contains.py +28 -0
  59. package/src/apothem/_vendor/jsonschema/benchmarks/import_benchmark.py +31 -0
  60. package/src/apothem/_vendor/jsonschema/benchmarks/issue232/issue.json +2653 -0
  61. package/src/apothem/_vendor/jsonschema/benchmarks/issue232.py +25 -0
  62. package/src/apothem/_vendor/jsonschema/benchmarks/json_schema_test_suite.py +12 -0
  63. package/src/apothem/_vendor/jsonschema/benchmarks/nested_schemas.py +56 -0
  64. package/src/apothem/_vendor/jsonschema/benchmarks/subcomponents.py +42 -0
  65. package/src/apothem/_vendor/jsonschema/benchmarks/unused_registry.py +35 -0
  66. package/src/apothem/_vendor/jsonschema/benchmarks/useless_applicator_schemas.py +106 -0
  67. package/src/apothem/_vendor/jsonschema/benchmarks/useless_keywords.py +32 -0
  68. package/src/apothem/_vendor/jsonschema/benchmarks/validator_creation.py +14 -0
  69. package/src/apothem/_vendor/jsonschema/cli.py +292 -0
  70. package/src/apothem/_vendor/jsonschema/exceptions.py +490 -0
  71. package/src/apothem/_vendor/jsonschema/protocols.py +230 -0
  72. package/src/apothem/_vendor/jsonschema/validators.py +1410 -0
  73. package/src/apothem/_vendor/jsonschema.LICENSE +19 -0
  74. package/src/apothem/_vendor/jsonschema_specifications/__init__.py +12 -0
  75. package/src/apothem/_vendor/jsonschema_specifications/_core.py +38 -0
  76. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/metaschema.json +42 -0
  77. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/applicator +56 -0
  78. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/content +17 -0
  79. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/core +57 -0
  80. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/format +14 -0
  81. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/meta-data +37 -0
  82. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft201909/vocabularies/validation +98 -0
  83. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/metaschema.json +58 -0
  84. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/applicator +48 -0
  85. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/content +17 -0
  86. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/core +51 -0
  87. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/format-annotation +14 -0
  88. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/format-assertion +14 -0
  89. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/meta-data +37 -0
  90. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/unevaluated +15 -0
  91. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft202012/vocabularies/validation +98 -0
  92. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft3/metaschema.json +172 -0
  93. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft4/metaschema.json +149 -0
  94. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft6/metaschema.json +153 -0
  95. package/src/apothem/_vendor/jsonschema_specifications/schemas/draft7/metaschema.json +166 -0
  96. package/src/apothem/_vendor/jsonschema_specifications.LICENSE +19 -0
  97. package/src/apothem/_vendor/referencing/__init__.py +7 -0
  98. package/src/apothem/_vendor/referencing/_attrs.py +31 -0
  99. package/src/apothem/_vendor/referencing/_attrs.pyi +21 -0
  100. package/src/apothem/_vendor/referencing/_core.py +739 -0
  101. package/src/apothem/_vendor/referencing/exceptions.py +165 -0
  102. package/src/apothem/_vendor/referencing/jsonschema.py +642 -0
  103. package/src/apothem/_vendor/referencing/py.typed +0 -0
  104. package/src/apothem/_vendor/referencing/retrieval.py +94 -0
  105. package/src/apothem/_vendor/referencing/typing.py +61 -0
  106. package/src/apothem/_vendor/referencing.LICENSE +19 -0
  107. package/src/apothem/_vendor/rpds/__init__.py +251 -0
  108. package/src/apothem/_vendor/typing_extensions.LICENSE +279 -0
  109. package/src/apothem/_vendor/typing_extensions.py +4317 -0
  110. package/src/apothem/_vendor/vendor.txt +22 -0
  111. package/src/apothem/_vendor/yaml/__init__.py +389 -0
  112. package/src/apothem/_vendor/yaml/composer.py +138 -0
  113. package/src/apothem/_vendor/yaml/constructor.py +748 -0
  114. package/src/apothem/_vendor/yaml/cyaml.py +100 -0
  115. package/src/apothem/_vendor/yaml/dumper.py +61 -0
  116. package/src/apothem/_vendor/yaml/emitter.py +1137 -0
  117. package/src/apothem/_vendor/yaml/error.py +74 -0
  118. package/src/apothem/_vendor/yaml/events.py +85 -0
  119. package/src/apothem/_vendor/yaml/loader.py +63 -0
  120. package/src/apothem/_vendor/yaml/nodes.py +48 -0
  121. package/src/apothem/_vendor/yaml/parser.py +588 -0
  122. package/src/apothem/_vendor/yaml/reader.py +185 -0
  123. package/src/apothem/_vendor/yaml/representer.py +388 -0
  124. package/src/apothem/_vendor/yaml/resolver.py +226 -0
  125. package/src/apothem/_vendor/yaml/scanner.py +1435 -0
  126. package/src/apothem/_vendor/yaml/serializer.py +110 -0
  127. package/src/apothem/_vendor/yaml/tokens.py +103 -0
  128. package/src/apothem/_vendor/yaml.LICENSE +20 -0
  129. package/src/apothem/agents/README.md +60 -0
  130. package/src/apothem/agents/codebase-explorer.md +91 -0
  131. package/src/apothem/agents/convention-auditor.md +93 -0
  132. package/src/apothem/agents/dependency-auditor.md +97 -0
  133. package/src/apothem/agents/fact-checker.md +84 -0
  134. package/src/apothem/agents/mcp-builder.md +86 -0
  135. package/src/apothem/agents/memory-auditor.md +93 -0
  136. package/src/apothem/agents/prompt-evaluator.md +87 -0
  137. package/src/apothem/agents/quality-gate.md +103 -0
  138. package/src/apothem/agents/refactor-surgeon.md +74 -0
  139. package/src/apothem/agents/research-scout.md +73 -0
  140. package/src/apothem/agents/security-scanner.md +83 -0
  141. package/src/apothem/agents/test-runner.md +84 -0
  142. package/src/apothem/audit/README.md +73 -0
  143. package/src/apothem/audit/_scan_lib.py +182 -0
  144. package/src/apothem/audit/analyze_graph.py +260 -0
  145. package/src/apothem/audit/build_capability_graph.py +607 -0
  146. package/src/apothem/audit/build_inventory.py +657 -0
  147. package/src/apothem/audit/build_plans_provenance.py +997 -0
  148. package/src/apothem/audit/check_links.py +389 -0
  149. package/src/apothem/audit/classify_artifacts.py +381 -0
  150. package/src/apothem/audit/deprecated-tokens.txt +10 -0
  151. package/src/apothem/audit/execute_plans_migration.py +491 -0
  152. package/src/apothem/audit/known-projects.txt +15 -0
  153. package/src/apothem/audit/render_capability_index.py +467 -0
  154. package/src/apothem/audit/render_inventory.py +405 -0
  155. package/src/apothem/audit/scan_ai_surfaces.py +1125 -0
  156. package/src/apothem/audit/scan_ai_surfaces_coarse.py +261 -0
  157. package/src/apothem/audit/scan_drift_features.py +143 -0
  158. package/src/apothem/audit/scan_frontmatter.py +293 -0
  159. package/src/apothem/audit/scan_header_coverage.py +1134 -0
  160. package/src/apothem/audit/scan_plan_leakage.py +540 -0
  161. package/src/apothem/audit/scan_plans_discipline.py +188 -0
  162. package/src/apothem/audit/scan_secrets_pii.py +245 -0
  163. package/src/apothem/audit/scan_stale_tokens.py +296 -0
  164. package/src/apothem/audit/synthesize_drift.py +205 -0
  165. package/src/apothem/benchmarks/README.md +33 -0
  166. package/src/apothem/benchmarks/__init__.py +3 -0
  167. package/src/apothem/benchmarks/bench_agents.py +63 -0
  168. package/src/apothem/benchmarks/bench_hooks.py +93 -0
  169. package/src/apothem/benchmarks/bench_install.py +58 -0
  170. package/src/apothem/benchmarks/bench_tests.py +93 -0
  171. package/src/apothem/benchmarks/bench_validate_ecosystem.py +84 -0
  172. package/src/apothem/cli/README.md +33 -0
  173. package/src/apothem/cli/__init__.py +229 -0
  174. package/src/apothem/cli/_cmd_completion.py +88 -0
  175. package/src/apothem/cli/_cmd_diff.py +181 -0
  176. package/src/apothem/cli/_cmd_doctor.py +143 -0
  177. package/src/apothem/cli/_cmd_harnesses.py +167 -0
  178. package/src/apothem/cli/_cmd_install.py +327 -0
  179. package/src/apothem/cli/_cmd_migrate_workspace.py +143 -0
  180. package/src/apothem/cli/_cmd_profile.py +341 -0
  181. package/src/apothem/cli/_cmd_status.py +180 -0
  182. package/src/apothem/cli/_cmd_uninstall.py +215 -0
  183. package/src/apothem/cli/_cmd_update.py +397 -0
  184. package/src/apothem/cli/_cmd_verify.py +194 -0
  185. package/src/apothem/cli/_common_flags.py +90 -0
  186. package/src/apothem/cli/_epilogs.py +296 -0
  187. package/src/apothem/cli/_helpers.py +857 -0
  188. package/src/apothem/cli/_json_formatter.py +21 -0
  189. package/src/apothem/cli/_materialize.py +376 -0
  190. package/src/apothem/cli/completions/apothem.bash +30 -0
  191. package/src/apothem/cli/completions/apothem.fish +19 -0
  192. package/src/apothem/cli/completions/apothem.ps1 +27 -0
  193. package/src/apothem/cli/completions/apothem.zsh +42 -0
  194. package/src/apothem/cli/reference_export.py +126 -0
  195. package/src/apothem/commands/README.md +125 -0
  196. package/src/apothem/commands/a11y-audit.md +203 -0
  197. package/src/apothem/commands/architecture-review.md +194 -0
  198. package/src/apothem/commands/audit.md +165 -0
  199. package/src/apothem/commands/code-audit.md +218 -0
  200. package/src/apothem/commands/code-review.md +193 -0
  201. package/src/apothem/commands/dependency-audit.md +209 -0
  202. package/src/apothem/commands/docs-review.md +199 -0
  203. package/src/apothem/commands/elevate.md +285 -0
  204. package/src/apothem/commands/eval.md +149 -0
  205. package/src/apothem/commands/fortress.md +172 -0
  206. package/src/apothem/commands/freshify.md +168 -0
  207. package/src/apothem/commands/github-deploy-fresh.md +178 -0
  208. package/src/apothem/commands/github-deploy-next.md +167 -0
  209. package/src/apothem/commands/perf-audit.md +198 -0
  210. package/src/apothem/commands/plan-amend.md +104 -0
  211. package/src/apothem/commands/plan-audit.md +127 -0
  212. package/src/apothem/commands/plan-design.md +257 -0
  213. package/src/apothem/commands/plan-execute.md +495 -0
  214. package/src/apothem/commands/plan-generate.md +351 -0
  215. package/src/apothem/commands/plan-review.md +555 -0
  216. package/src/apothem/commands/plan-spec.md +359 -0
  217. package/src/apothem/commands/plan-status.md +222 -0
  218. package/src/apothem/commands/plan.md +173 -0
  219. package/src/apothem/commands/projectify.md +142 -0
  220. package/src/apothem/commands/release-readiness.md +142 -0
  221. package/src/apothem/commands/research-analysis.md +241 -0
  222. package/src/apothem/commands/research-design.md +231 -0
  223. package/src/apothem/commands/research-disseminate.md +225 -0
  224. package/src/apothem/commands/research-experiment.md +232 -0
  225. package/src/apothem/commands/research-ideate.md +213 -0
  226. package/src/apothem/commands/research-paper.md +252 -0
  227. package/src/apothem/commands/research-proposal.md +220 -0
  228. package/src/apothem/commands/research-publish.md +255 -0
  229. package/src/apothem/commands/research-review.md +251 -0
  230. package/src/apothem/commands/research-sources.md +266 -0
  231. package/src/apothem/commands/research-spec.md +255 -0
  232. package/src/apothem/commands/research-synthesis.md +233 -0
  233. package/src/apothem/commands/research-theory.md +218 -0
  234. package/src/apothem/commands/research.md +181 -0
  235. package/src/apothem/commands/security-audit.md +196 -0
  236. package/src/apothem/commands/supply-chain-audit.md +192 -0
  237. package/src/apothem/commands/test-suite.md +146 -0
  238. package/src/apothem/commands/threat-model-audit.md +199 -0
  239. package/src/apothem/commands/ux-review.md +202 -0
  240. package/src/apothem/commands/workflow.md +162 -0
  241. package/src/apothem/conformity/README.md +173 -0
  242. package/src/apothem/conformity/__init__.py +1 -0
  243. package/src/apothem/conformity/_grep_base.py +93 -0
  244. package/src/apothem/conformity/agent_capability_grep.py +306 -0
  245. package/src/apothem/conformity/agents_md_coverage_grep.py +382 -0
  246. package/src/apothem/conformity/agnosticism_grep.py +311 -0
  247. package/src/apothem/conformity/always_on_budget_grep.py +318 -0
  248. package/src/apothem/conformity/bare_except_grep.py +115 -0
  249. package/src/apothem/conformity/binding_reciprocity_grep.py +151 -0
  250. package/src/apothem/conformity/brand_mark_grep.py +272 -0
  251. package/src/apothem/conformity/commented_out_code_grep.py +176 -0
  252. package/src/apothem/conformity/completion_claim_grep.py +169 -0
  253. package/src/apothem/conformity/conventional_commit_grep.py +319 -0
  254. package/src/apothem/conformity/copilot_instructions_presence_grep.py +324 -0
  255. package/src/apothem/conformity/cross_platform_matrix_grep.py +297 -0
  256. package/src/apothem/conformity/determinism_grep.py +306 -0
  257. package/src/apothem/conformity/diagram_staleness_grep.py +154 -0
  258. package/src/apothem/conformity/dynamism_grep.py +284 -0
  259. package/src/apothem/conformity/editorconfig_presence_grep.py +281 -0
  260. package/src/apothem/conformity/file_header_grep.py +502 -0
  261. package/src/apothem/conformity/freshness_token_grep.py +233 -0
  262. package/src/apothem/conformity/frontmatter_grep.py +274 -0
  263. package/src/apothem/conformity/frontmatter_value_grep.py +386 -0
  264. package/src/apothem/conformity/gate.py +1386 -0
  265. package/src/apothem/conformity/gitattributes_presence_grep.py +238 -0
  266. package/src/apothem/conformity/harden_runner_grep.py +320 -0
  267. package/src/apothem/conformity/hedging_grep.py +129 -0
  268. package/src/apothem/conformity/license_author_consistency_grep.py +204 -0
  269. package/src/apothem/conformity/link_check.py +327 -0
  270. package/src/apothem/conformity/magic_number_grep.py +182 -0
  271. package/src/apothem/conformity/multi_surface_coherence_grep.py +620 -0
  272. package/src/apothem/conformity/naming_grep.py +224 -0
  273. package/src/apothem/conformity/no_global_plans_grep.py +339 -0
  274. package/src/apothem/conformity/no_toplevel_docs_grep.py +120 -0
  275. package/src/apothem/conformity/oidc_trusted_publishing_grep.py +291 -0
  276. package/src/apothem/conformity/option_annotation_grep.py +352 -0
  277. package/src/apothem/conformity/orphan_output_grep.py +206 -0
  278. package/src/apothem/conformity/permissions_minimum_scope_grep.py +299 -0
  279. package/src/apothem/conformity/plain_language_grep.py +559 -0
  280. package/src/apothem/conformity/plan_next_step_consistency_grep.py +450 -0
  281. package/src/apothem/conformity/plan_suite_structure_grep.py +534 -0
  282. package/src/apothem/conformity/plans_discipline_language_grep.py +245 -0
  283. package/src/apothem/conformity/production_ready_pr_grep.py +200 -0
  284. package/src/apothem/conformity/recommend_next_step_grep.py +250 -0
  285. package/src/apothem/conformity/redundancy_grep.py +401 -0
  286. package/src/apothem/conformity/reference_token_grep.py +230 -0
  287. package/src/apothem/conformity/registry_capability_consistency_grep.py +368 -0
  288. package/src/apothem/conformity/secret_leak_grep.py +193 -0
  289. package/src/apothem/conformity/semver_stability_grep.py +358 -0
  290. package/src/apothem/conformity/smoke_install_grep.py +194 -0
  291. package/src/apothem/conformity/static_version_grep.py +284 -0
  292. package/src/apothem/conformity/token_efficiency_grep.py +185 -0
  293. package/src/apothem/conformity/unpinned_action_grep.py +115 -0
  294. package/src/apothem/conformity/user_confirm_grep.py +74 -0
  295. package/src/apothem/conformity/workflow_concurrency_grep.py +283 -0
  296. package/src/apothem/harnesses/README.md +63 -0
  297. package/src/apothem/harnesses/__init__.py +16 -0
  298. package/src/apothem/harnesses/_shared/README.md +36 -0
  299. package/src/apothem/harnesses/_shared/__init__.py +12 -0
  300. package/src/apothem/harnesses/_shared/install_driver.py +281 -0
  301. package/src/apothem/harnesses/_shared/install_driver_apply.py +612 -0
  302. package/src/apothem/harnesses/_shared/install_driver_backup.py +535 -0
  303. package/src/apothem/harnesses/_shared/install_driver_converters.py +310 -0
  304. package/src/apothem/harnesses/_shared/install_driver_lifecycle.py +495 -0
  305. package/src/apothem/harnesses/_shared/install_driver_materialize.py +675 -0
  306. package/src/apothem/harnesses/_shared/install_driver_merge.py +656 -0
  307. package/src/apothem/harnesses/_shared/install_driver_pathsafety.py +137 -0
  308. package/src/apothem/harnesses/_shared/install_driver_planvalidation.py +240 -0
  309. package/src/apothem/harnesses/_shared/install_driver_removal.py +366 -0
  310. package/src/apothem/harnesses/_shared/install_driver_treeops.py +248 -0
  311. package/src/apothem/harnesses/_shared/install_driver_types.py +330 -0
  312. package/src/apothem/harnesses/_shared/wrapper_factories.py +448 -0
  313. package/src/apothem/harnesses/antigravity/STANDARD-CONVENTION-PIN.md +91 -0
  314. package/src/apothem/harnesses/antigravity/__init__.py +70 -0
  315. package/src/apothem/harnesses/antigravity/capabilities.yml +40 -0
  316. package/src/apothem/harnesses/antigravity/install.py +63 -0
  317. package/src/apothem/harnesses/antigravity/templates/GEMINI.md +40 -0
  318. package/src/apothem/harnesses/antigravity/templates/plugin.json +5 -0
  319. package/src/apothem/harnesses/antigravity/uninstall.py +22 -0
  320. package/src/apothem/harnesses/antigravity/update.py +10 -0
  321. package/src/apothem/harnesses/antigravity/verify.py +11 -0
  322. package/src/apothem/harnesses/claude_code/STANDARD-CONVENTION-PIN.md +65 -0
  323. package/src/apothem/harnesses/claude_code/__init__.py +107 -0
  324. package/src/apothem/harnesses/claude_code/capabilities.yml +42 -0
  325. package/src/apothem/harnesses/claude_code/install.py +147 -0
  326. package/src/apothem/harnesses/claude_code/templates/settings.json +351 -0
  327. package/src/apothem/harnesses/claude_code/uninstall.py +23 -0
  328. package/src/apothem/harnesses/claude_code/update.py +10 -0
  329. package/src/apothem/harnesses/claude_code/verify.py +11 -0
  330. package/src/apothem/harnesses/codebuddy/STANDARD-CONVENTION-PIN.md +74 -0
  331. package/src/apothem/harnesses/codebuddy/__init__.py +49 -0
  332. package/src/apothem/harnesses/codebuddy/capabilities.yml +34 -0
  333. package/src/apothem/harnesses/codebuddy/install.py +40 -0
  334. package/src/apothem/harnesses/codebuddy/templates/apothem-rules.md +37 -0
  335. package/src/apothem/harnesses/codebuddy/uninstall.py +25 -0
  336. package/src/apothem/harnesses/codebuddy/update.py +10 -0
  337. package/src/apothem/harnesses/codebuddy/verify.py +11 -0
  338. package/src/apothem/harnesses/codex/STANDARD-CONVENTION-PIN.md +79 -0
  339. package/src/apothem/harnesses/codex/__init__.py +72 -0
  340. package/src/apothem/harnesses/codex/capabilities.yml +40 -0
  341. package/src/apothem/harnesses/codex/install.py +69 -0
  342. package/src/apothem/harnesses/codex/templates/AGENTS.md +40 -0
  343. package/src/apothem/harnesses/codex/templates/hooks.json +127 -0
  344. package/src/apothem/harnesses/codex/uninstall.py +23 -0
  345. package/src/apothem/harnesses/codex/update.py +10 -0
  346. package/src/apothem/harnesses/codex/verify.py +11 -0
  347. package/src/apothem/harnesses/cursor/STANDARD-CONVENTION-PIN.md +79 -0
  348. package/src/apothem/harnesses/cursor/__init__.py +48 -0
  349. package/src/apothem/harnesses/cursor/capabilities.yml +42 -0
  350. package/src/apothem/harnesses/cursor/install.py +38 -0
  351. package/src/apothem/harnesses/cursor/templates/apothem-rules.mdc +40 -0
  352. package/src/apothem/harnesses/cursor/uninstall.py +25 -0
  353. package/src/apothem/harnesses/cursor/update.py +10 -0
  354. package/src/apothem/harnesses/cursor/verify.py +11 -0
  355. package/src/apothem/harnesses/gemini_cli/STANDARD-CONVENTION-PIN.md +102 -0
  356. package/src/apothem/harnesses/gemini_cli/__init__.py +52 -0
  357. package/src/apothem/harnesses/gemini_cli/capabilities.yml +43 -0
  358. package/src/apothem/harnesses/gemini_cli/install.py +43 -0
  359. package/src/apothem/harnesses/gemini_cli/templates/GEMINI.md +38 -0
  360. package/src/apothem/harnesses/gemini_cli/uninstall.py +25 -0
  361. package/src/apothem/harnesses/gemini_cli/update.py +10 -0
  362. package/src/apothem/harnesses/gemini_cli/verify.py +11 -0
  363. package/src/apothem/harnesses/github_copilot/STANDARD-CONVENTION-PIN.md +84 -0
  364. package/src/apothem/harnesses/github_copilot/__init__.py +47 -0
  365. package/src/apothem/harnesses/github_copilot/capabilities.yml +42 -0
  366. package/src/apothem/harnesses/github_copilot/install.py +40 -0
  367. package/src/apothem/harnesses/github_copilot/templates/copilot-instructions.md +33 -0
  368. package/src/apothem/harnesses/github_copilot/uninstall.py +25 -0
  369. package/src/apothem/harnesses/github_copilot/update.py +10 -0
  370. package/src/apothem/harnesses/github_copilot/verify.py +11 -0
  371. package/src/apothem/harnesses/glm/STANDARD-CONVENTION-PIN.md +77 -0
  372. package/src/apothem/harnesses/glm/__init__.py +56 -0
  373. package/src/apothem/harnesses/glm/capabilities.yml +33 -0
  374. package/src/apothem/harnesses/glm/install.py +45 -0
  375. package/src/apothem/harnesses/glm/templates/glm.toml +58 -0
  376. package/src/apothem/harnesses/glm/uninstall.py +25 -0
  377. package/src/apothem/harnesses/glm/update.py +10 -0
  378. package/src/apothem/harnesses/glm/verify.py +11 -0
  379. package/src/apothem/harnesses/hermes/STANDARD-CONVENTION-PIN.md +57 -0
  380. package/src/apothem/harnesses/hermes/__init__.py +33 -0
  381. package/src/apothem/harnesses/hermes/capabilities.yml +36 -0
  382. package/src/apothem/harnesses/hermes/install.py +17 -0
  383. package/src/apothem/harnesses/hermes/materializer.py +35 -0
  384. package/src/apothem/harnesses/hermes/uninstall.py +33 -0
  385. package/src/apothem/harnesses/hermes/update.py +10 -0
  386. package/src/apothem/harnesses/hermes/verify.py +11 -0
  387. package/src/apothem/harnesses/kimi_code/STANDARD-CONVENTION-PIN.md +128 -0
  388. package/src/apothem/harnesses/kimi_code/__init__.py +59 -0
  389. package/src/apothem/harnesses/kimi_code/capabilities.yml +40 -0
  390. package/src/apothem/harnesses/kimi_code/install.py +42 -0
  391. package/src/apothem/harnesses/kimi_code/templates/AGENTS.md +43 -0
  392. package/src/apothem/harnesses/kimi_code/uninstall.py +27 -0
  393. package/src/apothem/harnesses/kimi_code/update.py +10 -0
  394. package/src/apothem/harnesses/kimi_code/verify.py +11 -0
  395. package/src/apothem/harnesses/kiro/STANDARD-CONVENTION-PIN.md +77 -0
  396. package/src/apothem/harnesses/kiro/__init__.py +49 -0
  397. package/src/apothem/harnesses/kiro/capabilities.yml +36 -0
  398. package/src/apothem/harnesses/kiro/install.py +39 -0
  399. package/src/apothem/harnesses/kiro/templates/apothem-rules.md +36 -0
  400. package/src/apothem/harnesses/kiro/uninstall.py +25 -0
  401. package/src/apothem/harnesses/kiro/update.py +10 -0
  402. package/src/apothem/harnesses/kiro/verify.py +11 -0
  403. package/src/apothem/harnesses/open_claw/STANDARD-CONVENTION-PIN.md +62 -0
  404. package/src/apothem/harnesses/open_claw/__init__.py +35 -0
  405. package/src/apothem/harnesses/open_claw/capabilities.yml +35 -0
  406. package/src/apothem/harnesses/open_claw/install.py +17 -0
  407. package/src/apothem/harnesses/open_claw/materializer.py +36 -0
  408. package/src/apothem/harnesses/open_claw/uninstall.py +32 -0
  409. package/src/apothem/harnesses/open_claw/update.py +10 -0
  410. package/src/apothem/harnesses/open_claw/verify.py +11 -0
  411. package/src/apothem/harnesses/opencode/STANDARD-CONVENTION-PIN.md +76 -0
  412. package/src/apothem/harnesses/opencode/__init__.py +35 -0
  413. package/src/apothem/harnesses/opencode/capabilities.yml +43 -0
  414. package/src/apothem/harnesses/opencode/install.py +17 -0
  415. package/src/apothem/harnesses/opencode/materializer.py +31 -0
  416. package/src/apothem/harnesses/opencode/uninstall.py +34 -0
  417. package/src/apothem/harnesses/opencode/update.py +10 -0
  418. package/src/apothem/harnesses/opencode/verify.py +11 -0
  419. package/src/apothem/harnesses/qwen_code/STANDARD-CONVENTION-PIN.md +87 -0
  420. package/src/apothem/harnesses/qwen_code/__init__.py +37 -0
  421. package/src/apothem/harnesses/qwen_code/capabilities.yml +43 -0
  422. package/src/apothem/harnesses/qwen_code/install.py +19 -0
  423. package/src/apothem/harnesses/qwen_code/materializer.py +174 -0
  424. package/src/apothem/harnesses/qwen_code/templates/QWEN.md +30 -0
  425. package/src/apothem/harnesses/qwen_code/uninstall.py +34 -0
  426. package/src/apothem/harnesses/qwen_code/update.py +10 -0
  427. package/src/apothem/harnesses/qwen_code/verify.py +11 -0
  428. package/src/apothem/harnesses/trae/STANDARD-CONVENTION-PIN.md +70 -0
  429. package/src/apothem/harnesses/trae/__init__.py +49 -0
  430. package/src/apothem/harnesses/trae/capabilities.yml +34 -0
  431. package/src/apothem/harnesses/trae/install.py +38 -0
  432. package/src/apothem/harnesses/trae/templates/apothem-rules.md +37 -0
  433. package/src/apothem/harnesses/trae/uninstall.py +25 -0
  434. package/src/apothem/harnesses/trae/update.py +10 -0
  435. package/src/apothem/harnesses/trae/verify.py +11 -0
  436. package/src/apothem/harnesses/windsurf/STANDARD-CONVENTION-PIN.md +91 -0
  437. package/src/apothem/harnesses/windsurf/__init__.py +52 -0
  438. package/src/apothem/harnesses/windsurf/capabilities.yml +40 -0
  439. package/src/apothem/harnesses/windsurf/install.py +41 -0
  440. package/src/apothem/harnesses/windsurf/templates/apothem-rules.md +37 -0
  441. package/src/apothem/harnesses/windsurf/uninstall.py +25 -0
  442. package/src/apothem/harnesses/windsurf/update.py +10 -0
  443. package/src/apothem/harnesses/windsurf/verify.py +11 -0
  444. package/src/apothem/harnesses/zed/STANDARD-CONVENTION-PIN.md +92 -0
  445. package/src/apothem/harnesses/zed/__init__.py +57 -0
  446. package/src/apothem/harnesses/zed/capabilities.yml +38 -0
  447. package/src/apothem/harnesses/zed/install.py +41 -0
  448. package/src/apothem/harnesses/zed/templates/apothem-rules.md +32 -0
  449. package/src/apothem/harnesses/zed/uninstall.py +28 -0
  450. package/src/apothem/harnesses/zed/update.py +10 -0
  451. package/src/apothem/harnesses/zed/verify.py +11 -0
  452. package/src/apothem/hooks/README.md +81 -0
  453. package/src/apothem/hooks/__init__.py +24 -0
  454. package/src/apothem/hooks/askuserquestion_validator.py +380 -0
  455. package/src/apothem/hooks/dispatch.py +296 -0
  456. package/src/apothem/hooks/emit_hook_context.py +444 -0
  457. package/src/apothem/hooks/hooks.json +318 -0
  458. package/src/apothem/hooks/lib/README.md +39 -0
  459. package/src/apothem/hooks/lib/__init__.py +18 -0
  460. package/src/apothem/hooks/lib/bootstrap.ps1 +129 -0
  461. package/src/apothem/hooks/lib/bootstrap.sh +103 -0
  462. package/src/apothem/hooks/lib/events.py +51 -0
  463. package/src/apothem/hooks/lib/find-pwsh.ps1 +78 -0
  464. package/src/apothem/hooks/lib/find-pwsh.sh +76 -0
  465. package/src/apothem/hooks/lib/find-python.ps1 +63 -0
  466. package/src/apothem/hooks/lib/find-python.sh +97 -0
  467. package/src/apothem/hooks/lib/log.py +43 -0
  468. package/src/apothem/hooks/lib/resolve_root.py +264 -0
  469. package/src/apothem/hooks/messages/postcompact.md +14 -0
  470. package/src/apothem/hooks/messages/posttooluse-proactive-compaction.md +46 -0
  471. package/src/apothem/hooks/messages/precompact.md +14 -0
  472. package/src/apothem/hooks/messages/pretooluse-askuserquestion-recommended.md +65 -0
  473. package/src/apothem/hooks/messages/pretooluse-bash-plan-guard.md +97 -0
  474. package/src/apothem/hooks/messages/pretooluse-bash.md +39 -0
  475. package/src/apothem/hooks/messages/pretooluse-conformity.md +70 -0
  476. package/src/apothem/hooks/messages/pretooluse-dependency-guard.md +21 -0
  477. package/src/apothem/hooks/messages/pretooluse-edit-header-guard.md +61 -0
  478. package/src/apothem/hooks/messages/pretooluse-edit.md +21 -0
  479. package/src/apothem/hooks/messages/pretooluse-eval-guard.md +39 -0
  480. package/src/apothem/hooks/messages/pretooluse-notebookedit.md +11 -0
  481. package/src/apothem/hooks/messages/pretooluse-write-header-guard.md +45 -0
  482. package/src/apothem/hooks/messages/pretooluse-write-plan-guard.md +72 -0
  483. package/src/apothem/hooks/messages/pretooluse-write.md +21 -0
  484. package/src/apothem/hooks/messages/sessionstart.md +15 -0
  485. package/src/apothem/hooks/messages/stop.md +27 -0
  486. package/src/apothem/hooks/proactive_compaction_tracker.py +327 -0
  487. package/src/apothem/hooks/session_start_bootstrap.py +472 -0
  488. package/src/apothem/lib/README.md +42 -0
  489. package/src/apothem/lib/__init__.py +13 -0
  490. package/src/apothem/lib/atomic_io.py +189 -0
  491. package/src/apothem/lib/auditor.py +687 -0
  492. package/src/apothem/lib/clean_slate.py +396 -0
  493. package/src/apothem/lib/contexts.py +352 -0
  494. package/src/apothem/lib/data_home.py +255 -0
  495. package/src/apothem/lib/frontmatter.py +101 -0
  496. package/src/apothem/lib/harness_materializer.py +213 -0
  497. package/src/apothem/lib/harness_protocol.py +59 -0
  498. package/src/apothem/lib/harness_registry.py +282 -0
  499. package/src/apothem/lib/harness_registry_data.py +843 -0
  500. package/src/apothem/lib/install_ledger.py +347 -0
  501. package/src/apothem/lib/learning.py +540 -0
  502. package/src/apothem/lib/memory.py +347 -0
  503. package/src/apothem/lib/parallel_sweep.py +234 -0
  504. package/src/apothem/lib/plan_tiers.py +200 -0
  505. package/src/apothem/lib/plugin_bootstrap.py +132 -0
  506. package/src/apothem/lib/plugin_tree.py +599 -0
  507. package/src/apothem/lib/profile.py +755 -0
  508. package/src/apothem/lib/profile_projection.py +198 -0
  509. package/src/apothem/lib/propagation-manifest.yaml +878 -0
  510. package/src/apothem/lib/propagation.py +220 -0
  511. package/src/apothem/lib/python_resolver.py +189 -0
  512. package/src/apothem/lib/reporter.py +62 -0
  513. package/src/apothem/lib/workspace_migration.py +323 -0
  514. package/src/apothem/output-styles/README.md +41 -0
  515. package/src/apothem/output-styles/concise-engineer.md +49 -0
  516. package/src/apothem/output-styles/default-architect.md +52 -0
  517. package/src/apothem/output-styles/default.md +113 -0
  518. package/src/apothem/output-styles/forensic-auditor.md +63 -0
  519. package/src/apothem/py.typed +0 -0
  520. package/src/apothem/rules/README.md +121 -0
  521. package/src/apothem/rules/agent-capability-discipline-matrix.md +89 -0
  522. package/src/apothem/rules/agent-capability-discipline.md +78 -0
  523. package/src/apothem/rules/agent-orchestration-patterns.md +144 -0
  524. package/src/apothem/rules/agent-orchestration.md +65 -0
  525. package/src/apothem/rules/agents-md-convention.md +86 -0
  526. package/src/apothem/rules/agile-sprints-elements.md +135 -0
  527. package/src/apothem/rules/agile-sprints.md +64 -0
  528. package/src/apothem/rules/agnostic-posture-checklist.md +47 -0
  529. package/src/apothem/rules/agnostic-posture.md +48 -0
  530. package/src/apothem/rules/authoritative-referencing-quotation.md +50 -0
  531. package/src/apothem/rules/authoritative-referencing.md +66 -0
  532. package/src/apothem/rules/authority-inquiry-categories.md +58 -0
  533. package/src/apothem/rules/authority-inquiry.md +54 -0
  534. package/src/apothem/rules/auto-memory-topic-files.md +86 -0
  535. package/src/apothem/rules/auto-memory.md +67 -0
  536. package/src/apothem/rules/bidirectional-binding.md +123 -0
  537. package/src/apothem/rules/canonical-layout-reporting-tiers.md +212 -0
  538. package/src/apothem/rules/canonical-layout.md +60 -0
  539. package/src/apothem/rules/clean-architecture-layers.md +186 -0
  540. package/src/apothem/rules/clean-room-generation-protocols.md +124 -0
  541. package/src/apothem/rules/clean-room-generation.md +59 -0
  542. package/src/apothem/rules/code-craft-conventions.md +101 -0
  543. package/src/apothem/rules/code-craft-markdown.md +138 -0
  544. package/src/apothem/rules/code-craft-python.md +154 -0
  545. package/src/apothem/rules/code-craft-shell.md +192 -0
  546. package/src/apothem/rules/cognitive-identity-techniques.md +180 -0
  547. package/src/apothem/rules/cognitive-identity.md +81 -0
  548. package/src/apothem/rules/context-management-budget.md +46 -0
  549. package/src/apothem/rules/context-management-protocol.md +161 -0
  550. package/src/apothem/rules/context-management-scratch.md +128 -0
  551. package/src/apothem/rules/context-management.md +85 -0
  552. package/src/apothem/rules/definitiveness-virtues.md +67 -0
  553. package/src/apothem/rules/definitiveness.md +58 -0
  554. package/src/apothem/rules/determinism.md +81 -0
  555. package/src/apothem/rules/disclosure-ledger-markers.md +58 -0
  556. package/src/apothem/rules/disclosure-ledger.md +52 -0
  557. package/src/apothem/rules/dynamism.md +38 -0
  558. package/src/apothem/rules/etc-extension.md +57 -0
  559. package/src/apothem/rules/expertise-posture-elements.md +68 -0
  560. package/src/apothem/rules/expertise-posture.md +54 -0
  561. package/src/apothem/rules/freshness-facade.md +64 -0
  562. package/src/apothem/rules/harness-adapter-shape-schemas.md +162 -0
  563. package/src/apothem/rules/harness-adapter-shape.md +42 -0
  564. package/src/apothem/rules/host-discovery-manifests.md +50 -0
  565. package/src/apothem/rules/host-discovery.md +56 -0
  566. package/src/apothem/rules/i18n-discipline-locale-cohorts.md +120 -0
  567. package/src/apothem/rules/i18n-discipline.md +70 -0
  568. package/src/apothem/rules/interactive-questions-canonical-shapes.md +590 -0
  569. package/src/apothem/rules/interactive-questions-detail.md +41 -0
  570. package/src/apothem/rules/interactive-questions-sweep-matchers.md +184 -0
  571. package/src/apothem/rules/interactive-questions.md +89 -0
  572. package/src/apothem/rules/large-file-generation.md +112 -0
  573. package/src/apothem/rules/large-file-reading.md +59 -0
  574. package/src/apothem/rules/living-docs.md +85 -0
  575. package/src/apothem/rules/multi-agent-workflow.md +57 -0
  576. package/src/apothem/rules/operational-mandates-expanded.md +78 -0
  577. package/src/apothem/rules/operational-mandates.md +88 -0
  578. package/src/apothem/rules/option-annotation-form.md +60 -0
  579. package/src/apothem/rules/option-annotation.md +45 -0
  580. package/src/apothem/rules/own-voice-reimplementation.md +86 -0
  581. package/src/apothem/rules/performance-discipline.md +91 -0
  582. package/src/apothem/rules/persistent-conventions-vigilance-checklist.md +54 -0
  583. package/src/apothem/rules/persistent-conventions-vigilance.md +61 -0
  584. package/src/apothem/rules/plain-language.md +56 -0
  585. package/src/apothem/rules/planning-techniques.md +130 -0
  586. package/src/apothem/rules/pre-emission-gate-bars.md +86 -0
  587. package/src/apothem/rules/pre-emission-gate.md +54 -0
  588. package/src/apothem/rules/production-ready-prs-surfaces.md +162 -0
  589. package/src/apothem/rules/production-ready-prs.md +83 -0
  590. package/src/apothem/rules/propagation.md +63 -0
  591. package/src/apothem/rules/recommend-next-step.md +106 -0
  592. package/src/apothem/rules/refactoring-discipline.md +76 -0
  593. package/src/apothem/rules/session-closure.md +44 -0
  594. package/src/apothem/rules/sota-elevation-exemplars.md +76 -0
  595. package/src/apothem/rules/sota-elevation.md +52 -0
  596. package/src/apothem/rules/source-accessibility.md +58 -0
  597. package/src/apothem/rules/surgical-manipulation.md +48 -0
  598. package/src/apothem/rules/systemic-participation-relations.md +108 -0
  599. package/src/apothem/rules/systemic-participation.md +70 -0
  600. package/src/apothem/rules/ten-dimension-check-dimensions.md +52 -0
  601. package/src/apothem/rules/ten-dimension-check.md +59 -0
  602. package/src/apothem/rules/token-budget-discipline.md +81 -0
  603. package/src/apothem/rules/token-efficiency-rewrite-protocol.md +79 -0
  604. package/src/apothem/rules/token-efficiency-rewrite.md +77 -0
  605. package/src/apothem/rules/tool-use-discipline.md +48 -0
  606. package/src/apothem/rules/visual-leverage.md +102 -0
  607. package/src/apothem/schemas/NOTICE.md +9 -0
  608. package/src/apothem/schemas/README.md +104 -0
  609. package/src/apothem/schemas/__init__.py +176 -0
  610. package/src/apothem/schemas/advisory-finding.schema.json +111 -0
  611. package/src/apothem/schemas/agent.schema.json +106 -0
  612. package/src/apothem/schemas/authorship-header.txt +1 -0
  613. package/src/apothem/schemas/cohort-manifest.yaml +248 -0
  614. package/src/apothem/schemas/cohort-metadata-vocabulary.yaml +168 -0
  615. package/src/apothem/schemas/cohort.schema.json +113 -0
  616. package/src/apothem/schemas/command.schema.json +68 -0
  617. package/src/apothem/schemas/compatibility-matrix.yaml +432 -0
  618. package/src/apothem/schemas/context-fragment.schema.json +64 -0
  619. package/src/apothem/schemas/freshness-token-denylist.txt +51 -0
  620. package/src/apothem/schemas/handoff-manifest.yaml +353 -0
  621. package/src/apothem/schemas/header-exceptions.txt +141 -0
  622. package/src/apothem/schemas/header-visibility.yaml +39 -0
  623. package/src/apothem/schemas/learning-signal.schema.json +46 -0
  624. package/src/apothem/schemas/memory-record.schema.json +61 -0
  625. package/src/apothem/schemas/output-style.schema.json +40 -0
  626. package/src/apothem/schemas/plan.schema.json +51 -0
  627. package/src/apothem/schemas/plugin.schema.json +83 -0
  628. package/src/apothem/schemas/profile.example.yaml +70 -0
  629. package/src/apothem/schemas/profile.minimal.yaml +6 -0
  630. package/src/apothem/schemas/profile.schema.json +396 -0
  631. package/src/apothem/schemas/reference-token-denylist.txt +25 -0
  632. package/src/apothem/schemas/skill.schema.json +75 -0
  633. package/src/apothem/skills/README.md +93 -0
  634. package/src/apothem/skills/dependency-upgrade/SKILL.md +105 -0
  635. package/src/apothem/skills/dev-toolkit/SKILL.md +120 -0
  636. package/src/apothem/skills/diagram-authoring/SKILL.md +113 -0
  637. package/src/apothem/skills/document-authoring/SKILL.md +118 -0
  638. package/src/apothem/skills/ecosystem-audit/SKILL.md +108 -0
  639. package/src/apothem/skills/ecosystem-audit/references/audit-fortress.md +85 -0
  640. package/src/apothem/skills/ecosystem-audit/references/procedure.md +162 -0
  641. package/src/apothem/skills/eval-harness/SKILL.md +88 -0
  642. package/src/apothem/skills/incident-runbook/SKILL.md +92 -0
  643. package/src/apothem/skills/multi-source-research/SKILL.md +90 -0
  644. package/src/apothem/skills/plan-suite/SKILL.md +118 -0
  645. package/src/apothem/skills/plan-suite/master_template.md +1324 -0
  646. package/src/apothem/skills/projectify/SKILL.md +117 -0
  647. package/src/apothem/skills/prompt-engineering/SKILL.md +122 -0
  648. package/src/apothem/skills/refactor-extract/SKILL.md +85 -0
  649. package/src/apothem/skills/research-suite/SKILL.md +170 -0
  650. package/src/apothem/skills/research-suite/references/directory-structure.md +47 -0
  651. package/src/apothem/skills/research-suite/references/lifecycle.md +67 -0
  652. package/src/apothem/skills/research-suite/references/principal-investigator-framework.md +37 -0
  653. package/src/apothem/skills/research-suite/references/rigor-mandates.md +30 -0
  654. package/src/apothem/skills/research-suite/research_template.md +476 -0
  655. package/src/apothem/skills/secret-rotation/SKILL.md +87 -0
  656. package/src/apothem/skills/source-synthesis/SKILL.md +92 -0
  657. package/src/apothem/skills/surgical-guard/SKILL.md +118 -0
  658. package/src/apothem/skills/test-authoring/SKILL.md +85 -0
  659. package/src/apothem/skills/vuln-triage/SKILL.md +91 -0
  660. package/src/apothem/skills/workflow/SKILL.md +139 -0
  661. package/src/apothem/statuslines/README.md +26 -0
  662. package/src/apothem/statuslines/__init__.py +20 -0
  663. package/src/apothem/statuslines/conformity.json +5 -0
  664. package/src/apothem/statuslines/render.py +334 -0
  665. package/src/apothem/statuslines/statusline.md +50 -0
  666. package/src/apothem/templates/README.md +43 -0
  667. package/src/apothem/templates/agents-md-template.md +80 -0
  668. package/src/apothem/templates/consideration-log.md +39 -0
  669. package/src/apothem/templates/expertise-gap-log.md +56 -0
  670. package/src/apothem/templates/master-index-template.md +93 -0
  671. package/src/apothem/templates/potency-map.md +53 -0
  672. package/src/apothem/templates/preservation-audit.md +60 -0
  673. package/src/apothem/templates/question-resolution-audit.md +52 -0
  674. package/src/apothem/templates/trace-matrix-template.md +77 -0
@@ -0,0 +1,1134 @@
1
+ # SPDX-License-Identifier: MIT
2
+
3
+ """Detailed authorship-header coverage scan against the inventory snapshot.
4
+
5
+ Why this tool exists. The inventory pass at the prior audit phase carries
6
+ a coarse four-mark heuristic for the authorship banner — sufficient to
7
+ size the work, insufficient to drive an injector. The downstream banner
8
+ ratification, fixture authoring, validator authoring, and injection
9
+ passes need a per-file authoritative record: applicability per the
10
+ exception fixture, header-status by byte-precise comparison against the
11
+ canonical variant for the file's filetype family, the line range the
12
+ banner occupies (if present), the malformation class (if malformed),
13
+ and a per-file injection plan with a unified-diff preview. This tool
14
+ emits ``header-coverage.json`` and ``header-coverage.md`` once; the
15
+ operator-confirm and injection phases consume them.
16
+
17
+ What the tool captures. Per applicable file: ``path``, ``variant-family``
18
+ (one of ``hash`` / ``double-slash`` / ``html`` / ``c-block`` /
19
+ ``semicolon`` / ``double-dash`` / ``exempt``), ``header-status`` (
20
+ ``present-canonical`` / ``present-malformed`` / ``absent`` /
21
+ ``not-applicable``), ``header-line-range`` (the inclusive 1-based span
22
+ the banner occupies, or ``null``), ``malformation-class`` (one of the
23
+ eight classification slots when ``present-malformed``), and an
24
+ ``injection-plan`` block carrying the unified-diff preview the injector
25
+ would apply. Aggregates: counts by variant family, counts by
26
+ malformation class, coverage percentage (``present-canonical /
27
+ applicable-total``).
28
+
29
+ Scope boundary. The tool ONLY inspects file heads (the first sixty
30
+ lines, which absorbs every shebang + frontmatter + banner shape the
31
+ canonical variants emit) and ONLY reads bytes — it never writes the
32
+ banner. The injection pass at the downstream phase consumes this
33
+ output's diff previews and applies them.
34
+ """
35
+
36
+ from __future__ import annotations
37
+
38
+ import argparse
39
+ import difflib
40
+ import hashlib
41
+ import json
42
+ import sys
43
+ from dataclasses import dataclass, field
44
+ from datetime import datetime, timezone
45
+ from pathlib import Path
46
+ from typing import Final
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Header-status taxonomy. Mirrors the inventory's four-value taxonomy.
50
+ # ---------------------------------------------------------------------------
51
+ HEADER_PRESENT_CANONICAL: Final[str] = "present-canonical"
52
+ HEADER_PRESENT_MALFORMED: Final[str] = "present-malformed"
53
+ HEADER_ABSENT: Final[str] = "absent"
54
+ HEADER_NOT_APPLICABLE: Final[str] = "not-applicable"
55
+
56
+ ALL_HEADER_STATUSES: Final[tuple[str, ...]] = (
57
+ HEADER_PRESENT_CANONICAL,
58
+ HEADER_PRESENT_MALFORMED,
59
+ HEADER_ABSENT,
60
+ HEADER_NOT_APPLICABLE,
61
+ )
62
+
63
+ # ---------------------------------------------------------------------------
64
+ # Variant-family taxonomy per spec §4.6.2.
65
+ # ---------------------------------------------------------------------------
66
+ VARIANT_HASH: Final[str] = "hash"
67
+ VARIANT_DOUBLE_SLASH: Final[str] = "double-slash"
68
+ VARIANT_HTML: Final[str] = "html"
69
+ VARIANT_C_BLOCK: Final[str] = "c-block"
70
+ VARIANT_SEMICOLON: Final[str] = "semicolon"
71
+ VARIANT_DOUBLE_DASH: Final[str] = "double-dash"
72
+ VARIANT_EXEMPT: Final[str] = "exempt"
73
+
74
+ ALL_VARIANTS: Final[tuple[str, ...]] = (
75
+ VARIANT_HASH,
76
+ VARIANT_DOUBLE_SLASH,
77
+ VARIANT_HTML,
78
+ VARIANT_C_BLOCK,
79
+ VARIANT_SEMICOLON,
80
+ VARIANT_DOUBLE_DASH,
81
+ VARIANT_EXEMPT,
82
+ )
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Header-malformation taxonomy consumed by the coverage scanner.
86
+ # ---------------------------------------------------------------------------
87
+ MALFORM_WRONG_VARIANT: Final[str] = "wrong-variant"
88
+ MALFORM_WRONG_LINE_ORDER: Final[str] = "wrong-line-order"
89
+ MALFORM_DRIFTED_CONTACT: Final[str] = "drifted-contact-info"
90
+ MALFORM_SMART_QUOTE: Final[str] = "smart-quote-pollution"
91
+ MALFORM_BOM_PREFIX: Final[str] = "bom-prefix"
92
+ MALFORM_TRAILING_WHITESPACE: Final[str] = "trailing-whitespace"
93
+ MALFORM_WRONG_LINE_COUNT: Final[str] = "wrong-line-count"
94
+ MALFORM_MIXED: Final[str] = "mixed"
95
+
96
+ ALL_MALFORMATIONS: Final[tuple[str, ...]] = (
97
+ MALFORM_WRONG_VARIANT,
98
+ MALFORM_WRONG_LINE_ORDER,
99
+ MALFORM_DRIFTED_CONTACT,
100
+ MALFORM_SMART_QUOTE,
101
+ MALFORM_BOM_PREFIX,
102
+ MALFORM_TRAILING_WHITESPACE,
103
+ MALFORM_WRONG_LINE_COUNT,
104
+ MALFORM_MIXED,
105
+ )
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Variant-family resolution per spec §4.6.2.
109
+ #
110
+ # The mapping is consulted by suffix (lowercase) first; basename overrides
111
+ # follow for files with no suffix or with a name-only convention
112
+ # (Makefile, Dockerfile, etc.). Files whose suffix is not registered fall
113
+ # to ``exempt`` — the exception fixture is the authoritative gate, so a
114
+ # fall-through here only matters when the path also escapes the fixture.
115
+ # ---------------------------------------------------------------------------
116
+ SUFFIX_VARIANT: Final[dict[str, str]] = {
117
+ # `#` family
118
+ ".sh": VARIANT_HASH,
119
+ ".bash": VARIANT_HASH,
120
+ ".zsh": VARIANT_HASH,
121
+ ".py": VARIANT_HASH,
122
+ ".rb": VARIANT_HASH,
123
+ ".pl": VARIANT_HASH,
124
+ ".ps1": VARIANT_HASH,
125
+ ".psm1": VARIANT_HASH,
126
+ ".psd1": VARIANT_HASH,
127
+ ".yml": VARIANT_HASH,
128
+ ".yaml": VARIANT_HASH,
129
+ ".toml": VARIANT_HASH,
130
+ ".cff": VARIANT_HASH,
131
+ ".gitignore": VARIANT_HASH,
132
+ ".gitattributes": VARIANT_HASH,
133
+ ".editorconfig": VARIANT_HASH,
134
+ ".shellcheckrc": VARIANT_HASH,
135
+ ".env.example": VARIANT_HASH,
136
+ ".cfg": VARIANT_HASH,
137
+ ".conf": VARIANT_HASH,
138
+ # `//` family
139
+ ".js": VARIANT_DOUBLE_SLASH,
140
+ ".jsx": VARIANT_DOUBLE_SLASH,
141
+ ".mjs": VARIANT_DOUBLE_SLASH,
142
+ ".cjs": VARIANT_DOUBLE_SLASH,
143
+ ".ts": VARIANT_DOUBLE_SLASH,
144
+ ".tsx": VARIANT_DOUBLE_SLASH,
145
+ ".go": VARIANT_DOUBLE_SLASH,
146
+ ".rs": VARIANT_DOUBLE_SLASH,
147
+ ".java": VARIANT_DOUBLE_SLASH,
148
+ ".kt": VARIANT_DOUBLE_SLASH,
149
+ ".swift": VARIANT_DOUBLE_SLASH,
150
+ ".scala": VARIANT_DOUBLE_SLASH,
151
+ ".dart": VARIANT_DOUBLE_SLASH,
152
+ ".cs": VARIANT_DOUBLE_SLASH,
153
+ ".jsonc": VARIANT_DOUBLE_SLASH,
154
+ # block-comment family (HTML-style wrapper)
155
+ ".html": VARIANT_HTML,
156
+ ".htm": VARIANT_HTML,
157
+ ".md": VARIANT_HTML,
158
+ ".markdown": VARIANT_HTML,
159
+ ".mdc": VARIANT_HTML,
160
+ ".xml": VARIANT_HTML,
161
+ ".vue": VARIANT_HTML,
162
+ ".php": VARIANT_HTML,
163
+ # `/* */` block family
164
+ ".c": VARIANT_C_BLOCK,
165
+ ".cc": VARIANT_C_BLOCK,
166
+ ".cpp": VARIANT_C_BLOCK,
167
+ ".h": VARIANT_C_BLOCK,
168
+ ".hpp": VARIANT_C_BLOCK,
169
+ ".css": VARIANT_C_BLOCK,
170
+ ".scss": VARIANT_C_BLOCK,
171
+ ".less": VARIANT_C_BLOCK,
172
+ ".sql": VARIANT_DOUBLE_DASH,
173
+ # `;` family
174
+ ".ini": VARIANT_SEMICOLON,
175
+ ".lisp": VARIANT_SEMICOLON,
176
+ ".scm": VARIANT_SEMICOLON,
177
+ # `--` family
178
+ ".lua": VARIANT_DOUBLE_DASH,
179
+ ".hs": VARIANT_DOUBLE_DASH,
180
+ ".elm": VARIANT_DOUBLE_DASH,
181
+ ".ada": VARIANT_DOUBLE_DASH,
182
+ }
183
+
184
+ BASENAME_VARIANT: Final[dict[str, str]] = {
185
+ "Makefile": VARIANT_HASH,
186
+ "Dockerfile": VARIANT_HASH,
187
+ "Procfile": VARIANT_HASH,
188
+ "CODEOWNERS": VARIANT_HASH,
189
+ "apothem": VARIANT_HASH,
190
+ "PKGBUILD": VARIANT_HASH,
191
+ ".gitignore": VARIANT_HASH,
192
+ ".gitattributes": VARIANT_HASH,
193
+ ".editorconfig": VARIANT_HASH,
194
+ ".shellcheckrc": VARIANT_HASH,
195
+ ".env.example": VARIANT_HASH,
196
+ }
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # Canonical header — the single SPDX-License-Identifier line (D-007).
200
+ #
201
+ # The header was narrowed from a six-line branded banner box to one
202
+ # machine-readable license-identifier line per comment-syntax variant.
203
+ # Only comment syntax varies between variants; the identifier text is
204
+ # invariant. Drift in the identifier line is a CI failure.
205
+ #
206
+ # SPDX_PREFIX_TEXT is the substring that identifies the SPDX line in any
207
+ # variant. LEGACY_AUTHOR_MARK is retained purely as a detection constant:
208
+ # a file still carrying the retired branded-banner author line (but not the
209
+ # narrowed SPDX line at the canonical site) is a not-yet-narrowed header and
210
+ # is counted malformed/uncovered, so the scan surfaces the remaining work.
211
+ # ---------------------------------------------------------------------------
212
+ SPDX_PREFIX_TEXT: Final[str] = "SPDX-License-Identifier:"
213
+ LEGACY_AUTHOR_MARK: Final[str] = "Copyright (c) Ahmed G. Gad"
214
+
215
+ # Smart-quote codepoints whose presence inside a banner is malformation
216
+ # class ``smart-quote-pollution``. Source-escaped so the
217
+ # scanner's own bytes never trigger the ambiguous-Unicode lint rule
218
+ # that would otherwise flag literal smart quotes.
219
+ SMART_QUOTES: Final[tuple[str, ...]] = (
220
+ "‘", # noqa: RUF001 - detection codepoint U+2018
221
+ "’", # noqa: RUF001 - detection codepoint U+2019
222
+ "“",
223
+ "”",
224
+ "–", # noqa: RUF001 - detection codepoint U+2013
225
+ "—",
226
+ )
227
+
228
+ BOM_PREFIX: Final[str] = ""
229
+
230
+ # Number of leading lines scanned for banner detection. Must accommodate
231
+ # shebang + (optional) interpreter-pragma + frontmatter + banner shape.
232
+ SCAN_LINE_BUDGET: Final[int] = 60
233
+
234
+ # Per-file unified-diff context lines. Three lines is unified-diff
235
+ # default; the injection-plan diff uses a tighter window because the
236
+ # patch is always at the head of the file.
237
+ DIFF_CONTEXT_LINES: Final[int] = 3
238
+
239
+ EXIT_OK: Final[int] = 0
240
+ EXIT_ERROR: Final[int] = 1
241
+
242
+
243
+ # ---------------------------------------------------------------------------
244
+ # Canonical header rendering per variant family.
245
+ #
246
+ # The load / render / canonical-block helpers mirror
247
+ # src/apothem/conformity/file_header_grep.py byte-for-byte so the scanner,
248
+ # the validator, and the injector agree on the canonical form without
249
+ # sharing a module. The fixture at src/apothem/schemas/authorship-header.txt
250
+ # is a single hash-form SPDX line; every other variant is rendered from it
251
+ # by comment-marker substitution (hash / double-slash / semicolon /
252
+ # double-dash) or by wrapper composition (html / c-block).
253
+ # ---------------------------------------------------------------------------
254
+ def _replace_marker(line: str, source: str, target: str) -> str:
255
+ """Substitute the leading comment marker on a header line.
256
+
257
+ The narrowed header line carries the comment marker on its leading edge
258
+ only; this swaps that edge. Mirrors scripts/inject-header.py and
259
+ file_header_grep.py to preserve injector / validator / scanner parity.
260
+ """
261
+ if not line.startswith(source):
262
+ return line
263
+ if len(line) >= 2 * len(source) and line.endswith(source):
264
+ inner = line[len(source) : -len(source)]
265
+ return f"{target}{inner}{target}"
266
+ return target + line[len(source) :]
267
+
268
+
269
+ def _render_variant(hash_form_line: str, spdx_text: str, variant: str) -> list[str]:
270
+ """Render the canonical SPDX header line for ``variant`` (no trailing blank)."""
271
+ if variant == VARIANT_HASH:
272
+ return [hash_form_line]
273
+ if variant == VARIANT_DOUBLE_SLASH:
274
+ return [_replace_marker(hash_form_line, "#", "//")]
275
+ if variant == VARIANT_SEMICOLON:
276
+ return [_replace_marker(hash_form_line, "#", ";")]
277
+ if variant == VARIANT_DOUBLE_DASH:
278
+ return [_replace_marker(hash_form_line, "#", "--")]
279
+ if variant == VARIANT_HTML:
280
+ return [f"<!-- {spdx_text} -->"]
281
+ if variant == VARIANT_C_BLOCK:
282
+ return [f"/* {spdx_text} */"]
283
+ raise ValueError(f"variant not renderable: {variant!r}")
284
+
285
+
286
+ def _render_canonical_block(
287
+ hash_form_line: str, spdx_text: str, variant: str
288
+ ) -> list[str]:
289
+ """Render the canonical block including the mandatory trailing blank line."""
290
+ return [*_render_variant(hash_form_line, spdx_text, variant), ""]
291
+
292
+
293
+ def _load_banner(schemas_dir: Path) -> tuple[str, str]:
294
+ """Load the narrowed SPDX-line header fixture.
295
+
296
+ Mirrors file_header_grep.py ``_load_banner``: the fixture is the single
297
+ ``# SPDX-License-Identifier: MIT`` line. Returns ``(hash_form_line,
298
+ spdx_text)`` on success, or two empty strings when the fixture is absent
299
+ or malformed (the caller treats the empty result as a skip).
300
+ """
301
+ banner_path = schemas_dir / "authorship-header.txt"
302
+ if not banner_path.is_file():
303
+ return "", ""
304
+ raw = banner_path.read_text(encoding="utf-8")
305
+ lines = [line for line in raw.splitlines() if line.strip()]
306
+ if len(lines) != 1:
307
+ return "", ""
308
+ spdx_line = lines[0]
309
+ if SPDX_PREFIX_TEXT not in spdx_line or not spdx_line.startswith("#"):
310
+ return "", ""
311
+ return spdx_line, spdx_line[1:].strip()
312
+
313
+
314
+ # Resolve the canonical header fixture once at import. The fixture lives at
315
+ # src/apothem/schemas/ relative to this file (audit/ → apothem/ → schemas/).
316
+ _SCHEMAS_DIR: Final[Path] = Path(__file__).resolve().parent.parent / "schemas"
317
+ _HASH_FORM_LINE, _SPDX_TEXT = _load_banner(_SCHEMAS_DIR)
318
+
319
+
320
+ def canonical_banner_lines(variant: str) -> list[str]:
321
+ """Return the canonical header lines for ``variant``.
322
+
323
+ The narrowed header is a single line per variant. Raises ``ValueError``
324
+ if ``variant`` is not a renderable family; the caller filters exempt
325
+ files before requesting a canonical form.
326
+ """
327
+ return _render_variant(_HASH_FORM_LINE, _SPDX_TEXT, variant)
328
+
329
+
330
+ def canonical_banner_text(variant: str) -> str:
331
+ """Return the canonical header as one newline-terminated text block."""
332
+ return "\n".join(canonical_banner_lines(variant)) + "\n"
333
+
334
+
335
+ # ---------------------------------------------------------------------------
336
+ # Variant resolution.
337
+ # ---------------------------------------------------------------------------
338
+ def variant_family_for(relative_path: Path) -> str:
339
+ """Resolve the canonical variant family for the file's filetype.
340
+
341
+ Suffix lookup is the primary signal; basename overrides cover
342
+ suffix-less artifacts (Makefile / Dockerfile / dotfiles whose
343
+ name encodes the convention). When neither resolves, the file is
344
+ treated as ``exempt`` — the exception fixture remains the
345
+ authoritative applicability gate, so a fall-through here only
346
+ matters for files the fixture also fails to cover.
347
+ """
348
+ name = relative_path.name
349
+ if name in BASENAME_VARIANT:
350
+ return BASENAME_VARIANT[name]
351
+
352
+ suffix = relative_path.suffix.lower()
353
+ if suffix in SUFFIX_VARIANT:
354
+ return SUFFIX_VARIANT[suffix]
355
+
356
+ return VARIANT_EXEMPT
357
+
358
+
359
+ # ---------------------------------------------------------------------------
360
+ # Exception-fixture parsing.
361
+ # ---------------------------------------------------------------------------
362
+ def load_exception_globs(fixture_path: Path) -> list[str]:
363
+ """Parse ``src/apothem/schemas/header-exceptions.txt`` into a glob list.
364
+
365
+ Comment lines (``#``-prefixed) and blank lines are stripped; every
366
+ other line becomes a pathspec. The order is preserved so a future
367
+ deny-list / allow-list extension can rely on first-match semantics.
368
+ """
369
+ if not fixture_path.is_file():
370
+ return []
371
+ globs: list[str] = []
372
+ for raw in fixture_path.read_text(encoding="utf-8").splitlines():
373
+ line = raw.strip()
374
+ if not line or line.startswith("#"):
375
+ continue
376
+ globs.append(line)
377
+ return globs
378
+
379
+
380
+ def matches_exception(relative_path: str, globs: list[str]) -> str | None:
381
+ """Return the first matching glob, or ``None`` when the path is in scope.
382
+
383
+ Patterns containing ``**`` are translated to fnmatch-friendly form by
384
+ treating ``**`` as ``*`` across path separators (fnmatch handles
385
+ cross-segment expansion when the path is expressed as POSIX-style).
386
+ """
387
+ posix = relative_path.replace("\\", "/")
388
+ for pattern in globs:
389
+ if _fnmatch_with_globstar(posix, pattern):
390
+ return pattern
391
+ return None
392
+
393
+
394
+ def _fnmatch_with_globstar(path: str, pattern: str) -> bool:
395
+ """Return ``True`` when ``pattern`` matches ``path`` under fnmatch
396
+ semantics extended for ``**`` cross-segment expansion.
397
+
398
+ fnmatch alone treats ``*`` as non-greedy across separators; ``**``
399
+ here matches zero or more path segments (the conventional gitignore
400
+ /gitattributes / Unix glob semantics). Translation is deliberately
401
+ direct — character-by-character regex synthesis with the four
402
+ glob-significant tokens (``**``, ``*``, ``?``, character classes)
403
+ handled inline; ordinary characters are escaped via ``re.escape``.
404
+ """
405
+ if (
406
+ "**" not in pattern
407
+ and "*" not in pattern
408
+ and "?" not in pattern
409
+ and "[" not in pattern
410
+ ):
411
+ return path == pattern
412
+
413
+ import re
414
+
415
+ regex_parts: list[str] = []
416
+ i = 0
417
+ pattern_length = len(pattern)
418
+ while i < pattern_length:
419
+ char = pattern[i]
420
+ if char == "*":
421
+ if i + 1 < pattern_length and pattern[i + 1] == "*":
422
+ # `**` token. When followed by `/`, the segment is
423
+ # optional — `**/foo` matches `foo` AND `a/foo`. When
424
+ # standalone (e.g. trailing `**`), the token matches
425
+ # any remaining suffix including empty.
426
+ if i + 2 < pattern_length and pattern[i + 2] == "/":
427
+ regex_parts.append("(?:.*/)?")
428
+ i += 3
429
+ continue
430
+ regex_parts.append(".*")
431
+ i += 2
432
+ continue
433
+ # Single `*` — match within a segment only.
434
+ regex_parts.append("[^/]*")
435
+ i += 1
436
+ continue
437
+ if char == "?":
438
+ regex_parts.append("[^/]")
439
+ i += 1
440
+ continue
441
+ regex_parts.append(re.escape(char))
442
+ i += 1
443
+
444
+ full = "^" + "".join(regex_parts) + "$"
445
+ return re.match(full, path) is not None
446
+
447
+
448
+ # ---------------------------------------------------------------------------
449
+ # Per-file scan.
450
+ # ---------------------------------------------------------------------------
451
+ @dataclass(slots=True)
452
+ class FileCoverage:
453
+ """Per-file coverage record emitted to ``header-coverage.json``."""
454
+
455
+ path: str
456
+ applicable: bool
457
+ exception_class: str | None
458
+ header_status: str
459
+ variant_family: str
460
+ header_line_range: tuple[int, int] | None
461
+ malformation_class: str | None
462
+ malformation_detail: str | None
463
+ injection_plan: dict[str, object] | None
464
+
465
+ def to_json(self) -> dict[str, object]:
466
+ return {
467
+ "path": self.path,
468
+ "applicable": self.applicable,
469
+ "exception-class": self.exception_class,
470
+ "header-status": self.header_status,
471
+ "variant-family": self.variant_family,
472
+ "header-line-range": (
473
+ list(self.header_line_range) if self.header_line_range else None
474
+ ),
475
+ "malformation-class": self.malformation_class,
476
+ "malformation-detail": self.malformation_detail,
477
+ "injection-plan": self.injection_plan,
478
+ }
479
+
480
+
481
+ @dataclass(slots=True)
482
+ class CoverageSummary:
483
+ """Aggregate counts emitted alongside the per-file array."""
484
+
485
+ total_files: int = 0
486
+ applicable_total: int = 0
487
+ present_canonical: int = 0
488
+ present_malformed: int = 0
489
+ absent: int = 0
490
+ not_applicable: int = 0
491
+ coverage_pct: float = 0.0
492
+ by_variant_family: dict[str, int] = field(default_factory=dict)
493
+ by_malformation_class: dict[str, int] = field(default_factory=dict)
494
+
495
+ def to_json(self) -> dict[str, object]:
496
+ return {
497
+ "total-files": self.total_files,
498
+ "applicable-total": self.applicable_total,
499
+ "present-canonical": self.present_canonical,
500
+ "present-malformed": self.present_malformed,
501
+ "absent": self.absent,
502
+ "not-applicable": self.not_applicable,
503
+ "coverage-pct": round(self.coverage_pct, 2),
504
+ "by-variant-family": dict(sorted(self.by_variant_family.items())),
505
+ "by-malformation-class": dict(sorted(self.by_malformation_class.items())),
506
+ }
507
+
508
+
509
+ def read_head(absolute_path: Path, line_budget: int = SCAN_LINE_BUDGET) -> list[str]:
510
+ """Return the file's leading lines (without trailing newlines).
511
+
512
+ UTF-8 with replacement fallback keeps mixed-encoding corpora
513
+ walkable. A binary file mis-classified as text yields garbled lines
514
+ but never crashes the scan.
515
+ """
516
+ try:
517
+ with absolute_path.open("r", encoding="utf-8", errors="replace") as handle:
518
+ head: list[str] = []
519
+ for index, line in enumerate(handle):
520
+ if index >= line_budget:
521
+ break
522
+ head.append(line.rstrip("\r\n"))
523
+ return head
524
+ except OSError:
525
+ return []
526
+
527
+
528
+ def has_shebang(head_lines: list[str]) -> bool:
529
+ """Return ``True`` when the first line begins with ``#!``."""
530
+ return bool(head_lines) and head_lines[0].startswith("#!")
531
+
532
+
533
+ def insertion_line(head_lines: list[str]) -> int:
534
+ """Return the 1-based line at which the canonical banner is inserted.
535
+
536
+ The banner is the file's first content with the single exception of
537
+ a shebang line (``#!``), which always remains line 1; the banner
538
+ starts on line 2 in that case. Frontmatter (``---``) follows the
539
+ banner per spec §4.6.3.
540
+ """
541
+ return 2 if has_shebang(head_lines) else 1
542
+
543
+
544
+ def scan_for_banner(
545
+ head_lines: list[str], variant: str
546
+ ) -> tuple[str, tuple[int, int] | None, str | None, str | None]:
547
+ """Inspect leading lines for the canonical header and return a verdict.
548
+
549
+ Returns a 4-tuple ``(header_status, line_range, malformation_class,
550
+ malformation_detail)``. ``line_range`` is ``None`` for absent headers;
551
+ the others carry detail when the header is present but malformed.
552
+
553
+ The narrowed header is a single line, so the verdict reduces to a
554
+ byte-exact comparison of the canonical block (the SPDX line plus its
555
+ mandatory trailing blank) at the insertion site — the same block the
556
+ validator at file_header_grep.py compares. A header present at the site
557
+ but not byte-exact is classified against the malformation slots a
558
+ one-line header can exhibit: bom-prefix, smart-quote-pollution,
559
+ trailing-whitespace, wrong-variant, and a wrong-line-count fall-through.
560
+ The retired branded-banner box (whose first line was the SPDX line but
561
+ which lacks the trailing blank) lands here as not-yet-narrowed; a file
562
+ with neither the SPDX line nor the legacy author mark is absent.
563
+ """
564
+ if not head_lines:
565
+ return HEADER_ABSENT, None, None, None
566
+
567
+ # BOM detection is independent of variant: any BOM byte before the
568
+ # header is itself a malformation, even if the rest is byte-exact.
569
+ bom_observed = head_lines[0].startswith(BOM_PREFIX)
570
+
571
+ # Canonical block = the variant line plus one trailing blank, mirroring
572
+ # _render_canonical_block / file_header_grep's _is_canonical_at_position.
573
+ canonical_block = _render_canonical_block(_HASH_FORM_LINE, _SPDX_TEXT, variant)
574
+
575
+ # The header sits at the insertion site: line index 1 after a shebang,
576
+ # else line index 0. The reported range is the SPDX line itself (the
577
+ # trailing blank completes the block but is not part of the header).
578
+ site_index = 1 if has_shebang(head_lines) else 0
579
+ if site_index >= len(head_lines):
580
+ # The file is shorter than the insertion site — header absent.
581
+ return HEADER_ABSENT, None, None, None
582
+
583
+ observed_line = head_lines[site_index]
584
+ line_range = (site_index + 1, site_index + 1)
585
+
586
+ # Byte-exact comparison of the full canonical block at the site.
587
+ block_end = site_index + len(canonical_block)
588
+ block_canonical = (
589
+ block_end <= len(head_lines)
590
+ and head_lines[site_index:block_end] == canonical_block
591
+ )
592
+ if block_canonical and not bom_observed:
593
+ return HEADER_PRESENT_CANONICAL, line_range, None, None
594
+
595
+ # The site does not carry the byte-exact canonical line. Decide whether
596
+ # any recognizable header is present at all: a SPDX line in some shape,
597
+ # or the retired branded-banner author mark anywhere in the head.
598
+ spdx_at_site = SPDX_PREFIX_TEXT in observed_line
599
+ legacy_present = any(LEGACY_AUTHOR_MARK in line for line in head_lines)
600
+
601
+ if not spdx_at_site and not legacy_present and not bom_observed:
602
+ return HEADER_ABSENT, None, None, None
603
+
604
+ # A recognizable-but-non-canonical header is present. Classify the
605
+ # malformation against the slots a one-line header can exhibit; multiple
606
+ # matches collapse to ``mixed``.
607
+ detected: list[tuple[str, str]] = []
608
+
609
+ if bom_observed:
610
+ detected.append((MALFORM_BOM_PREFIX, "U+FEFF byte order mark prefix"))
611
+
612
+ # Smart-quote pollution: any non-ASCII smart quote on the header line.
613
+ smart_hits = [q for q in SMART_QUOTES if q in observed_line]
614
+ if smart_hits:
615
+ detected.append(
616
+ (
617
+ MALFORM_SMART_QUOTE,
618
+ "non-ASCII typography in header: "
619
+ + ", ".join(repr(q) for q in smart_hits),
620
+ )
621
+ )
622
+
623
+ # Trailing whitespace: the header line carries whitespace beyond the
624
+ # canonical line's content.
625
+ if observed_line.rstrip() != observed_line:
626
+ detected.append(
627
+ (MALFORM_TRAILING_WHITESPACE, "trailing whitespace on header line"),
628
+ )
629
+
630
+ # Wrong-variant: the SPDX line uses a comment marker different from the
631
+ # canonical for this filetype.
632
+ if spdx_at_site:
633
+ wrong_variant = _detect_wrong_variant(observed_line, variant)
634
+ if wrong_variant is not None:
635
+ detected.append(
636
+ (MALFORM_WRONG_VARIANT, f"header uses {wrong_variant!r} marker"),
637
+ )
638
+
639
+ # Fall-through: present but not byte-exact and no finer class fired.
640
+ # The retired branded-banner box is a not-yet-narrowed header and lands
641
+ # here as a wrong-line-count (the multi-line legacy box vs. one line).
642
+ if not detected:
643
+ if legacy_present and not spdx_at_site:
644
+ detected.append(
645
+ (
646
+ MALFORM_WRONG_LINE_COUNT,
647
+ "retired branded-banner box present; expected single SPDX line",
648
+ ),
649
+ )
650
+ else:
651
+ detected.append(
652
+ (
653
+ MALFORM_WRONG_LINE_COUNT,
654
+ "header present but does not match canonical bytes",
655
+ ),
656
+ )
657
+
658
+ # Resolve to a single class: more than one finding ⇒ mixed.
659
+ if len(detected) == 1:
660
+ cls, detail = detected[0]
661
+ else:
662
+ cls = MALFORM_MIXED
663
+ detail = "; ".join(f"{c}: {d}" for c, d in detected)
664
+
665
+ return HEADER_PRESENT_MALFORMED, line_range, cls, detail
666
+
667
+
668
+ def _detect_wrong_variant(observed_line: str, target_variant: str) -> str | None:
669
+ """Return the comment-syntax marker observed on the SPDX header line when
670
+ it disagrees with the target variant; ``None`` when the marker is
671
+ correct or the line shape does not let us tell.
672
+
673
+ The comment-marker prefix (or html/c-block wrapper) determines which
674
+ variant the header *was* written in.
675
+ """
676
+ stripped = observed_line.lstrip()
677
+ if target_variant == VARIANT_HTML:
678
+ if not stripped.startswith("<!--"):
679
+ return "missing <!-- wrapper"
680
+ return None
681
+ if target_variant == VARIANT_C_BLOCK:
682
+ if not stripped.startswith("/*"):
683
+ return "missing /* wrapper"
684
+ return None
685
+
686
+ target_prefix_map = {
687
+ VARIANT_HASH: "#",
688
+ VARIANT_DOUBLE_SLASH: "//",
689
+ VARIANT_SEMICOLON: ";",
690
+ VARIANT_DOUBLE_DASH: "--",
691
+ }
692
+ target_prefix = target_prefix_map.get(target_variant)
693
+ if target_prefix is None:
694
+ return None
695
+ if not stripped.startswith(target_prefix):
696
+ # Sniff which marker the line uses instead.
697
+ for sniff_prefix in ("//", "--", ";", "#", "<!--", "/*"):
698
+ if stripped.startswith(sniff_prefix):
699
+ return sniff_prefix
700
+ return "unrecognized marker"
701
+ return None
702
+
703
+
704
+ # ---------------------------------------------------------------------------
705
+ # Injection-plan emission.
706
+ # ---------------------------------------------------------------------------
707
+ def build_injection_plan(
708
+ relative_path: str,
709
+ head_lines: list[str],
710
+ header_status: str,
711
+ variant: str,
712
+ header_line_range: tuple[int, int] | None,
713
+ ) -> dict[str, object] | None:
714
+ """Return the unified-diff-bearing injection plan for an absent or
715
+ malformed banner; ``None`` when no action is required.
716
+ """
717
+ if header_status == HEADER_PRESENT_CANONICAL:
718
+ return None
719
+ if header_status == HEADER_NOT_APPLICABLE:
720
+ return None
721
+ if variant == VARIANT_EXEMPT:
722
+ return None
723
+
724
+ canonical_lines = canonical_banner_lines(variant)
725
+ insert_at = insertion_line(head_lines)
726
+
727
+ if header_status == HEADER_ABSENT:
728
+ action = "insert"
729
+ before_lines = list(head_lines)
730
+ # Insertion: shebang preserved at line 1; banner begins at
731
+ # `insert_at`; existing content shifts down. A single blank
732
+ # separator follows the banner.
733
+ after_lines: list[str] = []
734
+ for idx, line in enumerate(before_lines, start=1):
735
+ if idx == insert_at:
736
+ after_lines.extend(canonical_lines)
737
+ after_lines.append("")
738
+ after_lines.append(line)
739
+ else:
740
+ after_lines.append(line)
741
+ if not before_lines:
742
+ after_lines = list(canonical_lines)
743
+ after_lines.append("")
744
+ replacement_lines = None
745
+ else:
746
+ # present-malformed — replace the malformed range with canonical.
747
+ action = "replace"
748
+ before_lines = list(head_lines)
749
+ if header_line_range is None:
750
+ return None
751
+ start_1based, end_1based = header_line_range
752
+ start_idx = start_1based - 1
753
+ end_idx = end_1based # slice is half-open
754
+ after_lines = (
755
+ before_lines[:start_idx] + canonical_lines + before_lines[end_idx:]
756
+ )
757
+ replacement_lines = [start_1based, end_1based]
758
+
759
+ diff = list(
760
+ difflib.unified_diff(
761
+ before_lines,
762
+ after_lines,
763
+ fromfile=f"a/{relative_path}",
764
+ tofile=f"b/{relative_path}",
765
+ n=DIFF_CONTEXT_LINES,
766
+ lineterm="",
767
+ )
768
+ )
769
+
770
+ truncated = False
771
+ diff_text = "\n".join(diff)
772
+ if len(diff_text) > 4_000:
773
+ diff_text = diff_text[:4_000] + "\n[... diff truncated ...]"
774
+ truncated = True
775
+
776
+ return {
777
+ "needed": True,
778
+ "action": action,
779
+ "insertion-line": insert_at if action == "insert" else None,
780
+ "replacement-lines": replacement_lines,
781
+ "diff": diff_text,
782
+ "diff-truncated": truncated,
783
+ }
784
+
785
+
786
+ # ---------------------------------------------------------------------------
787
+ # Top-level scan.
788
+ # ---------------------------------------------------------------------------
789
+ def scan_inventory(
790
+ inventory_path: Path,
791
+ fixture_path: Path,
792
+ root: Path,
793
+ ) -> tuple[list[FileCoverage], CoverageSummary, dict[str, str]]:
794
+ """Walk inventory records, classify every file, and emit coverage rows."""
795
+ raw = inventory_path.read_bytes()
796
+ inventory_sha = hashlib.sha256(raw).hexdigest()
797
+ inventory = json.loads(raw.decode("utf-8"))
798
+ records = inventory.get("files", [])
799
+
800
+ fixture_bytes = fixture_path.read_bytes() if fixture_path.is_file() else b""
801
+ fixture_sha = hashlib.sha256(fixture_bytes).hexdigest() if fixture_bytes else ""
802
+ globs = load_exception_globs(fixture_path)
803
+
804
+ rows: list[FileCoverage] = []
805
+ summary = CoverageSummary(total_files=len(records))
806
+ for variant in ALL_VARIANTS:
807
+ summary.by_variant_family[variant] = 0
808
+ for malform in ALL_MALFORMATIONS:
809
+ summary.by_malformation_class[malform] = 0
810
+
811
+ for record in records:
812
+ rel_path = str(record.get("path", ""))
813
+ if not rel_path:
814
+ continue
815
+ absolute = root / rel_path
816
+ relative_obj = Path(rel_path)
817
+
818
+ # Applicability: the exception fixture is the authoritative gate.
819
+ # A path matching any pattern is `not-applicable`. Otherwise the
820
+ # variant family resolution decides whether the file's filetype
821
+ # carries a banner at all.
822
+ exception_class = matches_exception(rel_path, globs)
823
+ variant = variant_family_for(relative_obj)
824
+
825
+ if exception_class is not None or variant == VARIANT_EXEMPT:
826
+ row = FileCoverage(
827
+ path=rel_path,
828
+ applicable=False,
829
+ exception_class=exception_class
830
+ or (None if variant != VARIANT_EXEMPT else "unsupported-extension"),
831
+ header_status=HEADER_NOT_APPLICABLE,
832
+ variant_family=variant,
833
+ header_line_range=None,
834
+ malformation_class=None,
835
+ malformation_detail=None,
836
+ injection_plan=None,
837
+ )
838
+ rows.append(row)
839
+ summary.not_applicable += 1
840
+ summary.by_variant_family[variant] += 1
841
+ continue
842
+
843
+ head = read_head(absolute)
844
+ status, line_range, malform, detail = scan_for_banner(head, variant)
845
+ plan = build_injection_plan(rel_path, head, status, variant, line_range)
846
+
847
+ row = FileCoverage(
848
+ path=rel_path,
849
+ applicable=True,
850
+ exception_class=None,
851
+ header_status=status,
852
+ variant_family=variant,
853
+ header_line_range=line_range,
854
+ malformation_class=malform,
855
+ malformation_detail=detail,
856
+ injection_plan=plan,
857
+ )
858
+ rows.append(row)
859
+ summary.applicable_total += 1
860
+ summary.by_variant_family[variant] += 1
861
+ if status == HEADER_PRESENT_CANONICAL:
862
+ summary.present_canonical += 1
863
+ elif status == HEADER_PRESENT_MALFORMED:
864
+ summary.present_malformed += 1
865
+ if malform is not None:
866
+ summary.by_malformation_class[malform] += 1
867
+ elif status == HEADER_ABSENT:
868
+ summary.absent += 1
869
+ else:
870
+ summary.not_applicable += 1
871
+
872
+ if summary.applicable_total > 0:
873
+ summary.coverage_pct = (
874
+ 100.0 * summary.present_canonical / summary.applicable_total
875
+ )
876
+
877
+ provenance = {
878
+ "inventory-source": str(inventory_path),
879
+ "inventory-sha256": inventory_sha,
880
+ "exception-fixture-source": str(fixture_path),
881
+ "exception-fixture-sha256": fixture_sha,
882
+ }
883
+ return rows, summary, provenance
884
+
885
+
886
+ # ---------------------------------------------------------------------------
887
+ # Markdown mirror emission.
888
+ # ---------------------------------------------------------------------------
889
+ def render_markdown(
890
+ rows: list[FileCoverage],
891
+ summary: CoverageSummary,
892
+ provenance: dict[str, str],
893
+ generated_at: str,
894
+ ) -> str:
895
+ """Render the human-readable coverage mirror."""
896
+ lines: list[str] = []
897
+ lines.append("# Authorship-Header Coverage Map")
898
+ lines.append("")
899
+ lines.append(f"- **Generated:** {generated_at}")
900
+ lines.append(f"- **Inventory source:** `{provenance['inventory-source']}`")
901
+ lines.append(f"- **Inventory SHA-256:** `{provenance['inventory-sha256']}`")
902
+ lines.append(f"- **Exception fixture:** `{provenance['exception-fixture-source']}`")
903
+ lines.append(
904
+ f"- **Exception fixture SHA-256:** `{provenance['exception-fixture-sha256']}`"
905
+ )
906
+ lines.append("")
907
+ lines.append("## Aggregate Statistics")
908
+ lines.append("")
909
+ lines.append("| Metric | Value |")
910
+ lines.append("|---|---:|")
911
+ lines.append(f"| Total files inventoried | {summary.total_files} |")
912
+ lines.append(f"| Applicable | {summary.applicable_total} |")
913
+ lines.append(f"| Present (canonical) | {summary.present_canonical} |")
914
+ lines.append(f"| Present (malformed) | {summary.present_malformed} |")
915
+ lines.append(f"| Absent | {summary.absent} |")
916
+ lines.append(f"| Not applicable | {summary.not_applicable} |")
917
+ lines.append(f"| Coverage % | {summary.coverage_pct:.2f}% |")
918
+ lines.append("")
919
+
920
+ lines.append("## Counts by Variant Family")
921
+ lines.append("")
922
+ lines.append("| Variant family | Count |")
923
+ lines.append("|---|---:|")
924
+ for variant in ALL_VARIANTS:
925
+ lines.append(f"| `{variant}` | {summary.by_variant_family.get(variant, 0)} |")
926
+ lines.append("")
927
+
928
+ lines.append("## Counts by Malformation Class")
929
+ lines.append("")
930
+ lines.append("| Malformation class | Count |")
931
+ lines.append("|---|---:|")
932
+ for malform in ALL_MALFORMATIONS:
933
+ lines.append(
934
+ f"| `{malform}` | {summary.by_malformation_class.get(malform, 0)} |"
935
+ )
936
+ lines.append("")
937
+
938
+ # Per-file injection-plan tables, partitioned by status.
939
+ absent_rows = [r for r in rows if r.header_status == HEADER_ABSENT]
940
+ malformed_rows = [r for r in rows if r.header_status == HEADER_PRESENT_MALFORMED]
941
+ canonical_rows = [r for r in rows if r.header_status == HEADER_PRESENT_CANONICAL]
942
+
943
+ if canonical_rows:
944
+ lines.append(f"## Files with Canonical Banner ({len(canonical_rows)})")
945
+ lines.append("")
946
+ lines.append("| Path | Variant | Lines |")
947
+ lines.append("|---|---|---|")
948
+ for row in sorted(canonical_rows, key=lambda r: r.path):
949
+ rng = (
950
+ f"{row.header_line_range[0]}-{row.header_line_range[1]}"
951
+ if row.header_line_range
952
+ else "—"
953
+ )
954
+ lines.append(f"| `{row.path}` | `{row.variant_family}` | {rng} |")
955
+ lines.append("")
956
+
957
+ if malformed_rows:
958
+ lines.append(f"## Files with Malformed Banner ({len(malformed_rows)})")
959
+ lines.append("")
960
+ lines.append("| Path | Variant | Lines | Class | Detail |")
961
+ lines.append("|---|---|---|---|---|")
962
+ for row in sorted(malformed_rows, key=lambda r: r.path):
963
+ rng = (
964
+ f"{row.header_line_range[0]}-{row.header_line_range[1]}"
965
+ if row.header_line_range
966
+ else "—"
967
+ )
968
+ detail = (row.malformation_detail or "").replace("|", r"\|")
969
+ lines.append(
970
+ f"| `{row.path}` | `{row.variant_family}` | {rng} | "
971
+ f"`{row.malformation_class}` | {detail} |"
972
+ )
973
+ lines.append("")
974
+
975
+ if absent_rows:
976
+ lines.append(f"## Files Missing the Banner ({len(absent_rows)})")
977
+ lines.append("")
978
+ lines.append("| Path | Variant | Insertion line |")
979
+ lines.append("|---|---|---:|")
980
+ for row in sorted(absent_rows, key=lambda r: r.path):
981
+ ip = row.injection_plan
982
+ insertion = (
983
+ str(ip.get("insertion-line"))
984
+ if isinstance(ip, dict) and ip.get("insertion-line") is not None
985
+ else "—"
986
+ )
987
+ lines.append(f"| `{row.path}` | `{row.variant_family}` | {insertion} |")
988
+ lines.append("")
989
+
990
+ # Sample injection-plan diffs (first three of each non-trivial class).
991
+ sample_rows = (malformed_rows + absent_rows)[:6]
992
+ if sample_rows:
993
+ lines.append("## Sample Injection-Plan Diffs")
994
+ lines.append("")
995
+ for row in sample_rows:
996
+ ip = row.injection_plan
997
+ if not isinstance(ip, dict):
998
+ continue
999
+ lines.append(
1000
+ f"### `{row.path}` — {row.header_status} / `{row.variant_family}`"
1001
+ )
1002
+ lines.append("")
1003
+ lines.append("```diff")
1004
+ diff_text = str(ip.get("diff", ""))
1005
+ for diff_line in diff_text.splitlines()[:30]:
1006
+ lines.append(diff_line)
1007
+ if len(diff_text.splitlines()) > 30:
1008
+ lines.append("[... truncated for readability ...]")
1009
+ lines.append("```")
1010
+ lines.append("")
1011
+
1012
+ # Exception-class breakdown so the operator can audit the fixture's
1013
+ # reach.
1014
+ not_applicable_rows = [r for r in rows if r.header_status == HEADER_NOT_APPLICABLE]
1015
+ if not_applicable_rows:
1016
+ lines.append(f"## Files Marked Not Applicable ({len(not_applicable_rows)})")
1017
+ lines.append("")
1018
+ from collections import Counter
1019
+
1020
+ class_counts = Counter(
1021
+ r.exception_class or "no-fixture-match" for r in not_applicable_rows
1022
+ )
1023
+ lines.append("| Exception class | Count |")
1024
+ lines.append("|---|---:|")
1025
+ for cls, count in class_counts.most_common():
1026
+ lines.append(f"| `{cls}` | {count} |")
1027
+ lines.append("")
1028
+
1029
+ return "\n".join(lines) + "\n"
1030
+
1031
+
1032
+ # ---------------------------------------------------------------------------
1033
+ # CLI.
1034
+ # ---------------------------------------------------------------------------
1035
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
1036
+ """Parse the CLI argument vector."""
1037
+ parser = argparse.ArgumentParser(
1038
+ description=(
1039
+ "Detailed authorship-header coverage scan against the inventory "
1040
+ "snapshot. Emits header-coverage.json and header-coverage.md."
1041
+ ),
1042
+ )
1043
+ parser.add_argument(
1044
+ "--root",
1045
+ type=Path,
1046
+ default=Path.cwd(),
1047
+ help="Working-tree root (defaults to the current directory).",
1048
+ )
1049
+ parser.add_argument(
1050
+ "--inventory",
1051
+ type=Path,
1052
+ default=Path(".audit/inventory.json"),
1053
+ help="Path to inventory.json (relative to --root or absolute).",
1054
+ )
1055
+ parser.add_argument(
1056
+ "--exception-fixture",
1057
+ type=Path,
1058
+ default=Path("src/apothem/schemas/header-exceptions.txt"),
1059
+ help="Path to the exception-list glob fixture.",
1060
+ )
1061
+ parser.add_argument(
1062
+ "--out-json",
1063
+ type=Path,
1064
+ default=Path(".audit/header-coverage.json"),
1065
+ help="Output path for the machine-readable coverage record.",
1066
+ )
1067
+ parser.add_argument(
1068
+ "--out-md",
1069
+ type=Path,
1070
+ default=Path(".audit/header-coverage.md"),
1071
+ help="Output path for the human-readable coverage mirror.",
1072
+ )
1073
+ return parser.parse_args(argv)
1074
+
1075
+
1076
+ def resolve_path(root: Path, candidate: Path) -> Path:
1077
+ """Resolve ``candidate`` against ``root`` when it is relative."""
1078
+ return candidate if candidate.is_absolute() else root / candidate
1079
+
1080
+
1081
+ def main(argv: list[str] | None = None) -> int:
1082
+ """CLI entry point — orchestrate the scan and emit outputs."""
1083
+ args = parse_args(argv)
1084
+ root = args.root.resolve()
1085
+ inventory_path = resolve_path(root, args.inventory)
1086
+ fixture_path = resolve_path(root, args.exception_fixture)
1087
+ out_json = resolve_path(root, args.out_json)
1088
+ out_md = resolve_path(root, args.out_md)
1089
+
1090
+ if not inventory_path.is_file():
1091
+ print(
1092
+ f"error: inventory not found at {inventory_path}",
1093
+ file=sys.stderr,
1094
+ )
1095
+ return EXIT_ERROR
1096
+
1097
+ rows, summary, provenance = scan_inventory(inventory_path, fixture_path, root)
1098
+
1099
+ generated_at = datetime.now(timezone.utc).isoformat()
1100
+ payload: dict[str, object] = {
1101
+ "scanner": "scan_header_coverage",
1102
+ "scanner-version": "1.0.0",
1103
+ "generated-at": generated_at,
1104
+ "root": str(root),
1105
+ **provenance,
1106
+ "summary": summary.to_json(),
1107
+ "files": [row.to_json() for row in rows],
1108
+ }
1109
+
1110
+ out_json.parent.mkdir(parents=True, exist_ok=True)
1111
+ out_json.write_text(
1112
+ json.dumps(payload, indent=2, sort_keys=False) + "\n",
1113
+ encoding="utf-8",
1114
+ )
1115
+
1116
+ out_md.parent.mkdir(parents=True, exist_ok=True)
1117
+ out_md.write_text(
1118
+ render_markdown(rows, summary, provenance, generated_at),
1119
+ encoding="utf-8",
1120
+ )
1121
+
1122
+ print(
1123
+ f"scan_header_coverage: {summary.applicable_total} applicable, "
1124
+ f"{summary.present_canonical} canonical "
1125
+ f"({summary.coverage_pct:.2f}%), "
1126
+ f"{summary.present_malformed} malformed, "
1127
+ f"{summary.absent} absent, "
1128
+ f"{summary.not_applicable} not-applicable",
1129
+ )
1130
+ return EXIT_OK
1131
+
1132
+
1133
+ if __name__ == "__main__":
1134
+ raise SystemExit(main())