@codyswann/lisa 4.48.0 → 4.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (426) hide show
  1. package/all/copy-overwrite/scripts/check-conflict-markers.mjs +4 -1
  2. package/all/copy-overwrite/scripts/lib/gate-failure-diagnosis.mjs +163 -0
  3. package/all/copy-overwrite/scripts/lisa-enforcement-fallback.sh +178 -1
  4. package/all/copy-overwrite/scripts/lisa-hooks/block-direct-issue-create.sh +89 -10
  5. package/all/copy-overwrite/scripts/lisa-hooks/parity-safety-net.sh +44 -0
  6. package/all/copy-overwrite/scripts/lisa-lint-staged-preflight.mjs +1 -1
  7. package/all/copy-overwrite/scripts/lisa-run-gates.mjs +24 -24
  8. package/all/copy-overwrite/scripts/lisa-work-item.mjs +490 -81
  9. package/cdk/package-lisa/package.lisa.json +45 -15
  10. package/dist/configs/vitest/base.d.ts +71 -1
  11. package/dist/configs/vitest/base.d.ts.map +1 -1
  12. package/dist/configs/vitest/base.js +141 -23
  13. package/dist/configs/vitest/base.js.map +1 -1
  14. package/dist/configs/vitest/fleet-admission-global-setup.d.ts +34 -0
  15. package/dist/configs/vitest/fleet-admission-global-setup.d.ts.map +1 -0
  16. package/dist/configs/vitest/fleet-admission-global-setup.js +123 -0
  17. package/dist/configs/vitest/fleet-admission-global-setup.js.map +1 -0
  18. package/dist/configs/vitest/fleet-admission.d.ts +129 -0
  19. package/dist/configs/vitest/fleet-admission.d.ts.map +1 -0
  20. package/dist/configs/vitest/fleet-admission.js +239 -0
  21. package/dist/configs/vitest/fleet-admission.js.map +1 -0
  22. package/dist/configs/vitest/synthesis-scratch-reclaim.d.ts +86 -0
  23. package/dist/configs/vitest/synthesis-scratch-reclaim.d.ts.map +1 -0
  24. package/dist/configs/vitest/synthesis-scratch-reclaim.js +226 -0
  25. package/dist/configs/vitest/synthesis-scratch-reclaim.js.map +1 -0
  26. package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
  27. package/dist/core/lisa-owned-hash-ledger.js +76 -0
  28. package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
  29. package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
  30. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  31. package/dist/core/upstream-evidence-manifest.js +129 -228
  32. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  33. package/dist/opencode/plugin-templates/lisa-block-direct-issue-create.ts +45 -1
  34. package/dist/opencode/plugin-templates/parity-safety-net.sh +44 -0
  35. package/expo/package-lisa/package.lisa.json +83 -53
  36. package/harper-fabric/package-lisa/package.lisa.json +62 -32
  37. package/nestjs/package-lisa/package.lisa.json +57 -27
  38. package/npm-package/package-lisa/package.lisa.json +43 -7
  39. package/package.json +11 -7
  40. package/phaser/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
  41. package/phaser/package-lisa/package.lisa.json +62 -32
  42. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  43. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  44. package/plugins/lisa/.codex-plugin/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  45. package/plugins/lisa/.codex-plugin/skills/lisa-github-build-intake/SKILL.md +4 -0
  46. package/plugins/lisa/.codex-plugin/skills/lisa-jira-build-intake/SKILL.md +4 -0
  47. package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +4 -0
  48. package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +4 -1
  49. package/plugins/lisa/hooks/block-direct-issue-create.sh +89 -10
  50. package/plugins/lisa/hooks/discharge-work-item-gates.sh +7 -1
  51. package/plugins/lisa/hooks/parity-safety-net.sh +44 -0
  52. package/plugins/lisa/hooks/threshold-ratchet-families.mjs +12 -0
  53. package/plugins/lisa/rules/eager/00-rule-index.md +79 -0
  54. package/plugins/lisa/rules/eager/base-rules.md +10 -0
  55. package/plugins/lisa/rules/eager/config-resolution.md +1 -88
  56. package/plugins/lisa/rules/reference/automation-runbook-contract.md +97 -0
  57. package/plugins/lisa/rules/reference/bdd-e2e-coverage.md +56 -0
  58. package/plugins/lisa/rules/reference/blocker-containment.md +58 -0
  59. package/plugins/lisa/rules/reference/claim-archaeology.md +42 -0
  60. package/plugins/lisa/rules/reference/claim-evidence-mapping.md +83 -0
  61. package/plugins/lisa/rules/reference/claim-time-guards.md +37 -0
  62. package/plugins/lisa/rules/reference/config-resolution.md +97 -0
  63. package/plugins/lisa/rules/reference/control-reachability.md +44 -0
  64. package/plugins/lisa/rules/reference/convergent-review.md +31 -0
  65. package/plugins/lisa/rules/reference/credential-substrate-precedence.md +57 -0
  66. package/plugins/lisa/rules/reference/dependency-decision-records.md +37 -0
  67. package/plugins/lisa/rules/reference/dependency-internalization-kit.md +47 -0
  68. package/plugins/lisa/rules/reference/dependency-trust-classes.md +48 -0
  69. package/plugins/lisa/rules/reference/deployed-state-readback.md +52 -0
  70. package/plugins/lisa/rules/reference/derived-branch-plan.md +52 -0
  71. package/plugins/lisa/rules/reference/design-source-of-truth.md +43 -0
  72. package/plugins/lisa/rules/reference/design-value-binding.md +103 -0
  73. package/plugins/lisa/rules/reference/documentation-source-paths.md +18 -0
  74. package/plugins/lisa/rules/reference/leaf-only-lifecycle.md +75 -0
  75. package/plugins/lisa/rules/reference/observability-audit.md +20 -0
  76. package/plugins/lisa/rules/reference/prd-definition-of-ready.md +37 -0
  77. package/plugins/lisa/rules/reference/prd-lifecycle-rollup.md +36 -0
  78. package/plugins/lisa/rules/reference/pre-flight-autofill.md +88 -0
  79. package/plugins/lisa/rules/reference/promotion-contract.md +21 -0
  80. package/plugins/lisa/rules/reference/readiness-rubric.md +63 -0
  81. package/plugins/lisa/rules/reference/ready-role-filing.md +34 -0
  82. package/plugins/lisa/rules/reference/rejection-detection.md +69 -0
  83. package/plugins/lisa/rules/reference/repo-scope-split.md +48 -0
  84. package/plugins/lisa/rules/reference/reset-seed-coverage.md +55 -0
  85. package/plugins/lisa/rules/reference/usage-accounting.md +38 -0
  86. package/plugins/lisa/rules/reference/work-item-definition-of-ready.md +32 -0
  87. package/plugins/lisa/scripts/intake-blocker-reprobe.mjs +158 -1
  88. package/plugins/lisa/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  89. package/plugins/lisa/skills/lisa-github-build-intake/SKILL.md +4 -0
  90. package/plugins/lisa/skills/lisa-jira-build-intake/SKILL.md +4 -0
  91. package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +4 -0
  92. package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +4 -1
  93. package/plugins/lisa-agy/hooks/block-direct-issue-create.sh +89 -10
  94. package/plugins/lisa-agy/hooks/parity-safety-net.sh +44 -0
  95. package/plugins/lisa-agy/plugin.json +1 -1
  96. package/plugins/lisa-agy/scripts/intake-blocker-reprobe.mjs +158 -1
  97. package/plugins/lisa-agy/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  98. package/plugins/lisa-agy/skills/lisa-github-build-intake/SKILL.md +4 -0
  99. package/plugins/lisa-agy/skills/lisa-jira-build-intake/SKILL.md +4 -0
  100. package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +4 -0
  101. package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +4 -1
  102. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  103. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  104. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  105. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  106. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  107. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  108. package/plugins/lisa-copilot/hooks/block-direct-issue-create.sh +89 -10
  109. package/plugins/lisa-copilot/hooks/discharge-work-item-gates.sh +7 -1
  110. package/plugins/lisa-copilot/hooks/parity-safety-net.sh +44 -0
  111. package/plugins/lisa-copilot/hooks/threshold-ratchet-families.mjs +12 -0
  112. package/plugins/lisa-copilot/rules/eager/00-rule-index.md +79 -0
  113. package/plugins/lisa-copilot/rules/eager/base-rules.md +10 -0
  114. package/plugins/lisa-copilot/rules/eager/config-resolution.md +1 -88
  115. package/plugins/lisa-copilot/rules/reference/automation-runbook-contract.md +97 -0
  116. package/plugins/lisa-copilot/rules/reference/bdd-e2e-coverage.md +56 -0
  117. package/plugins/lisa-copilot/rules/reference/blocker-containment.md +58 -0
  118. package/plugins/lisa-copilot/rules/reference/claim-archaeology.md +42 -0
  119. package/plugins/lisa-copilot/rules/reference/claim-evidence-mapping.md +83 -0
  120. package/plugins/lisa-copilot/rules/reference/claim-time-guards.md +37 -0
  121. package/plugins/lisa-copilot/rules/reference/config-resolution.md +97 -0
  122. package/plugins/lisa-copilot/rules/reference/control-reachability.md +44 -0
  123. package/plugins/lisa-copilot/rules/reference/convergent-review.md +31 -0
  124. package/plugins/lisa-copilot/rules/reference/credential-substrate-precedence.md +57 -0
  125. package/plugins/lisa-copilot/rules/reference/dependency-decision-records.md +37 -0
  126. package/plugins/lisa-copilot/rules/reference/dependency-internalization-kit.md +47 -0
  127. package/plugins/lisa-copilot/rules/reference/dependency-trust-classes.md +48 -0
  128. package/plugins/lisa-copilot/rules/reference/deployed-state-readback.md +52 -0
  129. package/plugins/lisa-copilot/rules/reference/derived-branch-plan.md +52 -0
  130. package/plugins/lisa-copilot/rules/reference/design-source-of-truth.md +43 -0
  131. package/plugins/lisa-copilot/rules/reference/design-value-binding.md +103 -0
  132. package/plugins/lisa-copilot/rules/reference/documentation-source-paths.md +18 -0
  133. package/plugins/lisa-copilot/rules/reference/leaf-only-lifecycle.md +75 -0
  134. package/plugins/lisa-copilot/rules/reference/observability-audit.md +20 -0
  135. package/plugins/lisa-copilot/rules/reference/prd-definition-of-ready.md +37 -0
  136. package/plugins/lisa-copilot/rules/reference/prd-lifecycle-rollup.md +36 -0
  137. package/plugins/lisa-copilot/rules/reference/pre-flight-autofill.md +88 -0
  138. package/plugins/lisa-copilot/rules/reference/promotion-contract.md +21 -0
  139. package/plugins/lisa-copilot/rules/reference/readiness-rubric.md +63 -0
  140. package/plugins/lisa-copilot/rules/reference/ready-role-filing.md +34 -0
  141. package/plugins/lisa-copilot/rules/reference/rejection-detection.md +69 -0
  142. package/plugins/lisa-copilot/rules/reference/repo-scope-split.md +48 -0
  143. package/plugins/lisa-copilot/rules/reference/reset-seed-coverage.md +55 -0
  144. package/plugins/lisa-copilot/rules/reference/usage-accounting.md +38 -0
  145. package/plugins/lisa-copilot/rules/reference/work-item-definition-of-ready.md +32 -0
  146. package/plugins/lisa-copilot/scripts/intake-blocker-reprobe.mjs +158 -1
  147. package/plugins/lisa-copilot/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  148. package/plugins/lisa-copilot/skills/lisa-github-build-intake/SKILL.md +4 -0
  149. package/plugins/lisa-copilot/skills/lisa-jira-build-intake/SKILL.md +4 -0
  150. package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +4 -0
  151. package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +4 -1
  152. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  153. package/plugins/lisa-cursor/hooks/block-direct-issue-create.sh +89 -10
  154. package/plugins/lisa-cursor/hooks/discharge-work-item-gates.sh +7 -1
  155. package/plugins/lisa-cursor/hooks/parity-safety-net.sh +44 -0
  156. package/plugins/lisa-cursor/hooks/threshold-ratchet-families.mjs +12 -0
  157. package/plugins/lisa-cursor/rules/00-rule-index.mdc +84 -0
  158. package/plugins/lisa-cursor/rules/automation-runbook-contract-reference.mdc +97 -0
  159. package/plugins/lisa-cursor/rules/base-rules.mdc +10 -0
  160. package/plugins/lisa-cursor/rules/bdd-e2e-coverage-reference.mdc +56 -0
  161. package/plugins/lisa-cursor/rules/blocker-containment-reference.mdc +58 -0
  162. package/plugins/lisa-cursor/rules/claim-archaeology-reference.mdc +42 -0
  163. package/plugins/lisa-cursor/rules/claim-evidence-mapping-reference.mdc +83 -0
  164. package/plugins/lisa-cursor/rules/claim-time-guards-reference.mdc +37 -0
  165. package/plugins/lisa-cursor/rules/config-resolution-reference.mdc +97 -0
  166. package/plugins/lisa-cursor/rules/config-resolution.mdc +1 -88
  167. package/plugins/lisa-cursor/rules/control-reachability-reference.mdc +44 -0
  168. package/plugins/lisa-cursor/rules/convergent-review-reference.mdc +31 -0
  169. package/plugins/lisa-cursor/rules/credential-substrate-precedence-reference.mdc +57 -0
  170. package/plugins/lisa-cursor/rules/dependency-decision-records-reference.mdc +37 -0
  171. package/plugins/lisa-cursor/rules/dependency-internalization-kit-reference.mdc +47 -0
  172. package/plugins/lisa-cursor/rules/dependency-trust-classes-reference.mdc +48 -0
  173. package/plugins/lisa-cursor/rules/deployed-state-readback-reference.mdc +52 -0
  174. package/plugins/lisa-cursor/rules/derived-branch-plan-reference.mdc +52 -0
  175. package/plugins/lisa-cursor/rules/design-source-of-truth-reference.mdc +43 -0
  176. package/plugins/lisa-cursor/rules/design-value-binding-reference.mdc +103 -0
  177. package/plugins/lisa-cursor/rules/documentation-source-paths-reference.mdc +18 -0
  178. package/plugins/lisa-cursor/rules/leaf-only-lifecycle-reference.mdc +75 -0
  179. package/plugins/lisa-cursor/rules/observability-audit-reference.mdc +20 -0
  180. package/plugins/lisa-cursor/rules/prd-definition-of-ready-reference.mdc +37 -0
  181. package/plugins/lisa-cursor/rules/prd-lifecycle-rollup-reference.mdc +36 -0
  182. package/plugins/lisa-cursor/rules/pre-flight-autofill-reference.mdc +88 -0
  183. package/plugins/lisa-cursor/rules/promotion-contract-reference.mdc +21 -0
  184. package/plugins/lisa-cursor/rules/readiness-rubric-reference.mdc +63 -0
  185. package/plugins/lisa-cursor/rules/ready-role-filing-reference.mdc +34 -0
  186. package/plugins/lisa-cursor/rules/rejection-detection-reference.mdc +69 -0
  187. package/plugins/lisa-cursor/rules/repo-scope-split-reference.mdc +48 -0
  188. package/plugins/lisa-cursor/rules/reset-seed-coverage-reference.mdc +55 -0
  189. package/plugins/lisa-cursor/rules/usage-accounting-reference.mdc +38 -0
  190. package/plugins/lisa-cursor/rules/work-item-definition-of-ready-reference.mdc +32 -0
  191. package/plugins/lisa-cursor/scripts/intake-blocker-reprobe.mjs +158 -1
  192. package/plugins/lisa-cursor/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  193. package/plugins/lisa-cursor/skills/lisa-github-build-intake/SKILL.md +4 -0
  194. package/plugins/lisa-cursor/skills/lisa-jira-build-intake/SKILL.md +4 -0
  195. package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +4 -0
  196. package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +4 -1
  197. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  198. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  199. package/plugins/lisa-expo-agy/plugin.json +1 -1
  200. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  201. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  202. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  203. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  204. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  205. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  206. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  207. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  208. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  209. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  210. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  211. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  212. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  213. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  214. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  215. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  216. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  217. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  218. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  219. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  220. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  221. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  222. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  223. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  224. package/plugins/lisa-rails-agy/plugin.json +1 -1
  225. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  226. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  227. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  228. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  229. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  230. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  231. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  232. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  233. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  234. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  235. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  236. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  237. package/plugins/src/base/hooks/block-direct-issue-create.sh +89 -10
  238. package/plugins/src/base/hooks/discharge-work-item-gates.sh +7 -1
  239. package/plugins/src/base/hooks/parity-safety-net.sh +44 -0
  240. package/plugins/src/base/hooks/threshold-ratchet-families.mjs +12 -0
  241. package/plugins/src/base/rules/eager/00-rule-index.md +79 -0
  242. package/plugins/src/base/rules/eager/base-rules.md +10 -0
  243. package/plugins/src/base/rules/eager/config-resolution.md +1 -88
  244. package/plugins/src/base/rules/reference/automation-runbook-contract.md +97 -0
  245. package/plugins/src/base/rules/reference/bdd-e2e-coverage.md +56 -0
  246. package/plugins/src/base/rules/reference/blocker-containment.md +58 -0
  247. package/plugins/src/base/rules/reference/claim-archaeology.md +42 -0
  248. package/plugins/src/base/rules/reference/claim-evidence-mapping.md +83 -0
  249. package/plugins/src/base/rules/reference/claim-time-guards.md +37 -0
  250. package/plugins/src/base/rules/reference/config-resolution.md +97 -0
  251. package/plugins/src/base/rules/reference/control-reachability.md +44 -0
  252. package/plugins/src/base/rules/reference/convergent-review.md +31 -0
  253. package/plugins/src/base/rules/reference/credential-substrate-precedence.md +57 -0
  254. package/plugins/src/base/rules/reference/dependency-decision-records.md +37 -0
  255. package/plugins/src/base/rules/reference/dependency-internalization-kit.md +47 -0
  256. package/plugins/src/base/rules/reference/dependency-trust-classes.md +48 -0
  257. package/plugins/src/base/rules/reference/deployed-state-readback.md +52 -0
  258. package/plugins/src/base/rules/reference/derived-branch-plan.md +52 -0
  259. package/plugins/src/base/rules/reference/design-source-of-truth.md +43 -0
  260. package/plugins/src/base/rules/reference/design-value-binding.md +103 -0
  261. package/plugins/src/base/rules/reference/documentation-source-paths.md +18 -0
  262. package/plugins/src/base/rules/reference/leaf-only-lifecycle.md +75 -0
  263. package/plugins/src/base/rules/reference/observability-audit.md +20 -0
  264. package/plugins/src/base/rules/reference/prd-definition-of-ready.md +37 -0
  265. package/plugins/src/base/rules/reference/prd-lifecycle-rollup.md +36 -0
  266. package/plugins/src/base/rules/reference/pre-flight-autofill.md +88 -0
  267. package/plugins/src/base/rules/reference/promotion-contract.md +21 -0
  268. package/plugins/src/base/rules/reference/readiness-rubric.md +63 -0
  269. package/plugins/src/base/rules/reference/ready-role-filing.md +34 -0
  270. package/plugins/src/base/rules/reference/rejection-detection.md +69 -0
  271. package/plugins/src/base/rules/reference/repo-scope-split.md +48 -0
  272. package/plugins/src/base/rules/reference/reset-seed-coverage.md +55 -0
  273. package/plugins/src/base/rules/reference/usage-accounting.md +38 -0
  274. package/plugins/src/base/rules/reference/work-item-definition-of-ready.md +32 -0
  275. package/plugins/src/base/scripts/intake-blocker-reprobe.mjs +158 -1
  276. package/plugins/src/base/skills/lisa-drive-pr-to-merge/SKILL.md +17 -1
  277. package/plugins/src/base/skills/lisa-github-build-intake/SKILL.md +4 -0
  278. package/plugins/src/base/skills/lisa-jira-build-intake/SKILL.md +4 -0
  279. package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +4 -0
  280. package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +4 -1
  281. package/rails/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
  282. package/rails/copy-overwrite/scripts/threshold-ratchet-families.mjs +12 -0
  283. package/scripts/check-eager-rule-budget.mjs +176 -0
  284. package/scripts/check-generated-artifact-merge-coverage.mjs +7 -0
  285. package/scripts/check-rules-pairing.sh +45 -14
  286. package/scripts/check-shipped-surface-removals.mjs +851 -0
  287. package/scripts/cleanup-worktrees.sh +69 -16
  288. package/scripts/generate-export-surface.mjs +318 -0
  289. package/scripts/lib/shipped-surface.mjs +212 -0
  290. package/scripts/lisa-enforcement-fallback.sh +178 -1
  291. package/scripts/remedy-conformance.mjs +435 -0
  292. package/scripts/two-channel-couplings.json +2 -2
  293. package/typescript/copy-contents/.husky/pre-push +147 -2
  294. package/typescript/copy-overwrite/ast-grep/rules/no-day-truncating-file-age-predicate.yml +50 -0
  295. package/typescript/copy-overwrite/audit.ignore.config.json +0 -10
  296. package/typescript/copy-overwrite/scripts/lisa-mutation.mjs +180 -29
  297. package/typescript/copy-overwrite/scripts/threshold-ratchet-families.mjs +12 -0
  298. package/typescript/package-lisa/package.lisa.json +72 -42
  299. package/plugins/lisa/rules/eager/automation-runbook-contract.md +0 -92
  300. package/plugins/lisa/rules/eager/bdd-e2e-coverage.md +0 -51
  301. package/plugins/lisa/rules/eager/blocker-containment.md +0 -53
  302. package/plugins/lisa/rules/eager/claim-archaeology.md +0 -37
  303. package/plugins/lisa/rules/eager/claim-evidence-mapping.md +0 -78
  304. package/plugins/lisa/rules/eager/claim-time-guards.md +0 -32
  305. package/plugins/lisa/rules/eager/control-reachability.md +0 -39
  306. package/plugins/lisa/rules/eager/convergent-review.md +0 -24
  307. package/plugins/lisa/rules/eager/credential-substrate-precedence.md +0 -54
  308. package/plugins/lisa/rules/eager/dependency-decision-records.md +0 -32
  309. package/plugins/lisa/rules/eager/dependency-internalization-kit.md +0 -42
  310. package/plugins/lisa/rules/eager/dependency-trust-classes.md +0 -43
  311. package/plugins/lisa/rules/eager/deployed-state-readback.md +0 -47
  312. package/plugins/lisa/rules/eager/derived-branch-plan.md +0 -47
  313. package/plugins/lisa/rules/eager/design-source-of-truth.md +0 -38
  314. package/plugins/lisa/rules/eager/design-value-binding.md +0 -98
  315. package/plugins/lisa/rules/eager/documentation-source-paths.md +0 -13
  316. package/plugins/lisa/rules/eager/factory-model.md +0 -41
  317. package/plugins/lisa/rules/eager/leaf-only-lifecycle.md +0 -70
  318. package/plugins/lisa/rules/eager/observability-audit.md +0 -15
  319. package/plugins/lisa/rules/eager/prd-definition-of-ready.md +0 -32
  320. package/plugins/lisa/rules/eager/prd-lifecycle-rollup.md +0 -31
  321. package/plugins/lisa/rules/eager/pre-flight-autofill.md +0 -84
  322. package/plugins/lisa/rules/eager/project-learnings.md +0 -22
  323. package/plugins/lisa/rules/eager/promotion-contract.md +0 -18
  324. package/plugins/lisa/rules/eager/readiness-rubric.md +0 -58
  325. package/plugins/lisa/rules/eager/ready-role-filing.md +0 -29
  326. package/plugins/lisa/rules/eager/rejection-detection.md +0 -64
  327. package/plugins/lisa/rules/eager/repo-scope-split.md +0 -43
  328. package/plugins/lisa/rules/eager/reset-seed-coverage.md +0 -50
  329. package/plugins/lisa/rules/eager/usage-accounting.md +0 -33
  330. package/plugins/lisa/rules/eager/work-item-definition-of-ready.md +0 -27
  331. package/plugins/lisa-copilot/rules/eager/automation-runbook-contract.md +0 -92
  332. package/plugins/lisa-copilot/rules/eager/bdd-e2e-coverage.md +0 -51
  333. package/plugins/lisa-copilot/rules/eager/blocker-containment.md +0 -53
  334. package/plugins/lisa-copilot/rules/eager/claim-archaeology.md +0 -37
  335. package/plugins/lisa-copilot/rules/eager/claim-evidence-mapping.md +0 -78
  336. package/plugins/lisa-copilot/rules/eager/claim-time-guards.md +0 -32
  337. package/plugins/lisa-copilot/rules/eager/control-reachability.md +0 -39
  338. package/plugins/lisa-copilot/rules/eager/convergent-review.md +0 -24
  339. package/plugins/lisa-copilot/rules/eager/credential-substrate-precedence.md +0 -54
  340. package/plugins/lisa-copilot/rules/eager/dependency-decision-records.md +0 -32
  341. package/plugins/lisa-copilot/rules/eager/dependency-internalization-kit.md +0 -42
  342. package/plugins/lisa-copilot/rules/eager/dependency-trust-classes.md +0 -43
  343. package/plugins/lisa-copilot/rules/eager/deployed-state-readback.md +0 -47
  344. package/plugins/lisa-copilot/rules/eager/derived-branch-plan.md +0 -47
  345. package/plugins/lisa-copilot/rules/eager/design-source-of-truth.md +0 -38
  346. package/plugins/lisa-copilot/rules/eager/design-value-binding.md +0 -98
  347. package/plugins/lisa-copilot/rules/eager/documentation-source-paths.md +0 -13
  348. package/plugins/lisa-copilot/rules/eager/factory-model.md +0 -41
  349. package/plugins/lisa-copilot/rules/eager/leaf-only-lifecycle.md +0 -70
  350. package/plugins/lisa-copilot/rules/eager/observability-audit.md +0 -15
  351. package/plugins/lisa-copilot/rules/eager/prd-definition-of-ready.md +0 -32
  352. package/plugins/lisa-copilot/rules/eager/prd-lifecycle-rollup.md +0 -31
  353. package/plugins/lisa-copilot/rules/eager/pre-flight-autofill.md +0 -84
  354. package/plugins/lisa-copilot/rules/eager/project-learnings.md +0 -22
  355. package/plugins/lisa-copilot/rules/eager/promotion-contract.md +0 -18
  356. package/plugins/lisa-copilot/rules/eager/readiness-rubric.md +0 -58
  357. package/plugins/lisa-copilot/rules/eager/ready-role-filing.md +0 -29
  358. package/plugins/lisa-copilot/rules/eager/rejection-detection.md +0 -64
  359. package/plugins/lisa-copilot/rules/eager/repo-scope-split.md +0 -43
  360. package/plugins/lisa-copilot/rules/eager/reset-seed-coverage.md +0 -50
  361. package/plugins/lisa-copilot/rules/eager/usage-accounting.md +0 -33
  362. package/plugins/lisa-copilot/rules/eager/work-item-definition-of-ready.md +0 -27
  363. package/plugins/lisa-cursor/rules/automation-runbook-contract.mdc +0 -97
  364. package/plugins/lisa-cursor/rules/bdd-e2e-coverage.mdc +0 -56
  365. package/plugins/lisa-cursor/rules/blocker-containment.mdc +0 -58
  366. package/plugins/lisa-cursor/rules/claim-archaeology.mdc +0 -42
  367. package/plugins/lisa-cursor/rules/claim-evidence-mapping.mdc +0 -83
  368. package/plugins/lisa-cursor/rules/claim-time-guards.mdc +0 -37
  369. package/plugins/lisa-cursor/rules/control-reachability.mdc +0 -44
  370. package/plugins/lisa-cursor/rules/convergent-review.mdc +0 -29
  371. package/plugins/lisa-cursor/rules/credential-substrate-precedence.mdc +0 -59
  372. package/plugins/lisa-cursor/rules/dependency-decision-records.mdc +0 -37
  373. package/plugins/lisa-cursor/rules/dependency-internalization-kit.mdc +0 -47
  374. package/plugins/lisa-cursor/rules/dependency-trust-classes.mdc +0 -48
  375. package/plugins/lisa-cursor/rules/deployed-state-readback.mdc +0 -52
  376. package/plugins/lisa-cursor/rules/derived-branch-plan.mdc +0 -52
  377. package/plugins/lisa-cursor/rules/design-source-of-truth.mdc +0 -43
  378. package/plugins/lisa-cursor/rules/design-value-binding.mdc +0 -103
  379. package/plugins/lisa-cursor/rules/documentation-source-paths.mdc +0 -18
  380. package/plugins/lisa-cursor/rules/factory-model.mdc +0 -46
  381. package/plugins/lisa-cursor/rules/leaf-only-lifecycle.mdc +0 -75
  382. package/plugins/lisa-cursor/rules/observability-audit.mdc +0 -20
  383. package/plugins/lisa-cursor/rules/prd-definition-of-ready.mdc +0 -37
  384. package/plugins/lisa-cursor/rules/prd-lifecycle-rollup.mdc +0 -36
  385. package/plugins/lisa-cursor/rules/pre-flight-autofill.mdc +0 -89
  386. package/plugins/lisa-cursor/rules/project-learnings.mdc +0 -27
  387. package/plugins/lisa-cursor/rules/promotion-contract.mdc +0 -23
  388. package/plugins/lisa-cursor/rules/readiness-rubric.mdc +0 -63
  389. package/plugins/lisa-cursor/rules/ready-role-filing.mdc +0 -34
  390. package/plugins/lisa-cursor/rules/rejection-detection.mdc +0 -69
  391. package/plugins/lisa-cursor/rules/repo-scope-split.mdc +0 -48
  392. package/plugins/lisa-cursor/rules/reset-seed-coverage.mdc +0 -55
  393. package/plugins/lisa-cursor/rules/usage-accounting.mdc +0 -38
  394. package/plugins/lisa-cursor/rules/work-item-definition-of-ready.mdc +0 -32
  395. package/plugins/src/base/rules/eager/automation-runbook-contract.md +0 -92
  396. package/plugins/src/base/rules/eager/bdd-e2e-coverage.md +0 -51
  397. package/plugins/src/base/rules/eager/blocker-containment.md +0 -53
  398. package/plugins/src/base/rules/eager/claim-archaeology.md +0 -37
  399. package/plugins/src/base/rules/eager/claim-evidence-mapping.md +0 -78
  400. package/plugins/src/base/rules/eager/claim-time-guards.md +0 -32
  401. package/plugins/src/base/rules/eager/control-reachability.md +0 -39
  402. package/plugins/src/base/rules/eager/convergent-review.md +0 -24
  403. package/plugins/src/base/rules/eager/credential-substrate-precedence.md +0 -54
  404. package/plugins/src/base/rules/eager/dependency-decision-records.md +0 -32
  405. package/plugins/src/base/rules/eager/dependency-internalization-kit.md +0 -42
  406. package/plugins/src/base/rules/eager/dependency-trust-classes.md +0 -43
  407. package/plugins/src/base/rules/eager/deployed-state-readback.md +0 -47
  408. package/plugins/src/base/rules/eager/derived-branch-plan.md +0 -47
  409. package/plugins/src/base/rules/eager/design-source-of-truth.md +0 -38
  410. package/plugins/src/base/rules/eager/design-value-binding.md +0 -98
  411. package/plugins/src/base/rules/eager/documentation-source-paths.md +0 -13
  412. package/plugins/src/base/rules/eager/factory-model.md +0 -41
  413. package/plugins/src/base/rules/eager/leaf-only-lifecycle.md +0 -70
  414. package/plugins/src/base/rules/eager/observability-audit.md +0 -15
  415. package/plugins/src/base/rules/eager/prd-definition-of-ready.md +0 -32
  416. package/plugins/src/base/rules/eager/prd-lifecycle-rollup.md +0 -31
  417. package/plugins/src/base/rules/eager/pre-flight-autofill.md +0 -84
  418. package/plugins/src/base/rules/eager/project-learnings.md +0 -22
  419. package/plugins/src/base/rules/eager/promotion-contract.md +0 -18
  420. package/plugins/src/base/rules/eager/readiness-rubric.md +0 -58
  421. package/plugins/src/base/rules/eager/ready-role-filing.md +0 -29
  422. package/plugins/src/base/rules/eager/rejection-detection.md +0 -64
  423. package/plugins/src/base/rules/eager/repo-scope-split.md +0 -43
  424. package/plugins/src/base/rules/eager/reset-seed-coverage.md +0 -50
  425. package/plugins/src/base/rules/eager/usage-accounting.md +0 -33
  426. package/plugins/src/base/rules/eager/work-item-definition-of-ready.md +0 -27
@@ -10,7 +10,12 @@
10
10
  # ~/.codex/worktrees, .lisa-worktrees, /tmp) OR its branch matches an
11
11
  # agent naming pattern (claude/*, worktree-*, codex/*) OR its branch is
12
12
  # fully merged into an environment branch (main/dev/staging).
13
- # 3. No modified/staged TRACKED files ("real work" is never deleted).
13
+ # 3. Git could actually REPORT on it, and reported no modified/staged TRACKED
14
+ # files ("real work" is never deleted). "I looked and it was clean" and "I
15
+ # could not look" are different answers: a `git status` that FAILS is kept
16
+ # in its own bucket, never treated as clean. This mirrors the TypeScript
17
+ # entitlement rule (src/cli/worktree-prune-policy.ts), which blocks on
18
+ # `unreadable` rather than defaulting an unproven worktree to eligible.
14
19
  # 4. Its HEAD commit is reachable from some remote ref (nothing unpushed).
15
20
  # 5. It is older than --min-age-days (default 7) by directory mtime.
16
21
  # 6. Its uncommitted content is reachable from some commit — asked of the
@@ -70,7 +75,7 @@ fi
70
75
 
71
76
  NOW=$(date +%s)
72
77
  MIN_AGE_SECS=$((MIN_AGE_DAYS * 86400))
73
- REMOVED=0 KEPT_DIRTY=0 KEPT_UNPUSHED=0 KEPT_YOUNG=0 KEPT_UNTRACKED=0 ORPHANS=0 ERRORS=0
78
+ REMOVED=0 KEPT_DIRTY=0 KEPT_UNPUSHED=0 KEPT_YOUNG=0 KEPT_UNTRACKED=0 KEPT_UNREADABLE=0 ORPHANS=0 ERRORS=0
74
79
  KEPT_UNREACHABLE=0
75
80
 
76
81
  # Content-reachability guard. Absent node or an unresolvable script leaves the
@@ -162,7 +167,23 @@ process_worktree() {
162
167
  return
163
168
  fi
164
169
 
165
- if [ -n "$(git -C "$wt" status --porcelain --untracked-files=no 2>/dev/null)" ]; then
170
+ # Capture the OUTPUT and the EXIT STATUS separately. Written as
171
+ # `[ -n "$(git ... 2>/dev/null)" ]` — the way this and the untracked check
172
+ # below both used to be — a FAILED git collapses to the empty string, `-n ""`
173
+ # is false, and an unreadable worktree takes the same branch as a spotless
174
+ # one: it falls through every KEEP predicate to removal. A short/corrupt index
175
+ # (what a SIGKILLed git leaves behind, and the ordinary outcome of the
176
+ # concurrency pressure this script exists to relieve) reaches exactly that
177
+ # state: `git status` exits 128 while the ref queries below still answer.
178
+ local tracked_status tracked_rc
179
+ tracked_status=$(git -C "$wt" status --porcelain --untracked-files=no 2>/dev/null)
180
+ tracked_rc=$?
181
+ if [ "$tracked_rc" -ne 0 ]; then
182
+ KEPT_UNREADABLE=$((KEPT_UNREADABLE + 1))
183
+ log "KEEP (git could not report on it — unreadable, so nothing was proven) $wt"
184
+ return
185
+ fi
186
+ if [ -n "$tracked_status" ]; then
166
187
  KEPT_DIRTY=$((KEPT_DIRTY + 1))
167
188
  log "KEEP (modified tracked files) $wt"
168
189
  return
@@ -183,7 +204,15 @@ process_worktree() {
183
204
  fi
184
205
 
185
206
  local force_flag=()
186
- if [ -n "$(git -C "$wt" status --porcelain 2>/dev/null)" ]; then
207
+ local full_status full_rc
208
+ full_status=$(git -C "$wt" status --porcelain 2>/dev/null)
209
+ full_rc=$?
210
+ if [ "$full_rc" -ne 0 ]; then
211
+ KEPT_UNREADABLE=$((KEPT_UNREADABLE + 1))
212
+ log "KEEP (git could not report on it — unreadable, so nothing was proven) $wt"
213
+ return
214
+ fi
215
+ if [ -n "$full_status" ]; then
187
216
  if [ "$FORCE_UNTRACKED" = 1 ]; then
188
217
  force_flag=(--force)
189
218
  else
@@ -195,7 +224,12 @@ process_worktree() {
195
224
 
196
225
  act "$wt [branch: ${branch:-detached}]"
197
226
  if [ "$APPLY" = 1 ]; then
198
- if git -C "$REPO" worktree remove "${force_flag[@]}" "$wt" 2>/dev/null; then
227
+ # `${arr[@]+"${arr[@]}"}` rather than a plain `"${force_flag[@]}"`. Under
228
+ # `set -u`, bash before 4.4 — which includes the 3.2.57 that ships as
229
+ # /bin/bash on macOS, where this script mostly runs — treats an EMPTY array
230
+ # expansion as an unbound variable and aborts. The abort landed on the first
231
+ # removal candidate, so `--apply` exited 1 with no summary at all.
232
+ if git -C "$REPO" worktree remove ${force_flag[@]+"${force_flag[@]}"} "$wt" 2>/dev/null; then
199
233
  REMOVED=$((REMOVED + 1))
200
234
  if [ "$DELETE_BRANCHES" = 1 ] && [ -n "$branch" ] && ! is_env_branch "$branch"; then
201
235
  git -C "$REPO" branch -D "$branch" >/dev/null 2>&1 || true
@@ -232,24 +266,43 @@ while IFS= read -r line; do
232
266
  done < <(git -C "$REPO" worktree list --porcelain; echo)
233
267
 
234
268
  # Orphan directories under .claude/worktrees that git no longer tracks.
269
+ #
270
+ # This is the same "a failed read looks like a benign answer" hazard the status
271
+ # guards above fix, with a worse ending: the removal here is `rm -rf`, which
272
+ # bypasses `git worktree remove`'s own refusal to delete a dirty worktree. So
273
+ # the enumeration is done ONCE, up front, with its exit status checked. An empty
274
+ # result from a FAILED `git worktree list` would make `grep` match nothing, `!`
275
+ # invert to true, and a live registered worktree be deleted outright — and empty
276
+ # output from a killed git is the ordinary outcome of the very concurrency
277
+ # pressure this script exists to relieve. Nothing proven means nothing swept.
235
278
  wtroot="$REPO/.claude/worktrees"
236
279
  if [ -d "$wtroot" ]; then
237
- for d in "$wtroot"/*/; do
238
- d=${d%/}
239
- [ -d "$d" ] || continue
240
- if ! git -C "$REPO" worktree list --porcelain | grep -qF "worktree $d"; then
241
- if dir_age_ok "$d"; then
242
- ORPHANS=$((ORPHANS + 1))
243
- act "(orphan dir, not a registered worktree) $d"
244
- [ "$APPLY" = 1 ] && rm -rf "$d"
280
+ registered=$(git -C "$REPO" worktree list --porcelain 2>/dev/null)
281
+ registered_rc=$?
282
+ if [ "$registered_rc" -ne 0 ]; then
283
+ log "SKIP orphan sweep (git could not enumerate worktrees — nothing was proven)"
284
+ else
285
+ for d in "$wtroot"/*/; do
286
+ d=${d%/}
287
+ [ -d "$d" ] || continue
288
+ # `-x` anchors the whole line. A substring match let a registered
289
+ # `…/wt-280` mark a genuine orphan `…/wt-28` as registered forever; that
290
+ # direction only ever keeps too much, but the prefix collision is real and
291
+ # this repository has that collision class on record.
292
+ if ! printf '%s\n' "$registered" | grep -qxF "worktree $d"; then
293
+ if dir_age_ok "$d"; then
294
+ ORPHANS=$((ORPHANS + 1))
295
+ act "(orphan dir, not a registered worktree) $d"
296
+ [ "$APPLY" = 1 ] && rm -rf "$d"
297
+ fi
245
298
  fi
246
- fi
247
- done
299
+ done
300
+ fi
248
301
  fi
249
302
 
250
303
  log ""
251
304
  log "=== summary ==="
252
305
  log "removed (or would remove): $REMOVED orphan dirs: $ORPHANS"
253
- log "kept: $KEPT_DIRTY modified-tracked, $KEPT_UNPUSHED unpushed, $KEPT_UNTRACKED untracked-only, $KEPT_UNREACHABLE uncommitted-content, $KEPT_YOUNG too-young errors: $ERRORS"
306
+ log "kept: $KEPT_DIRTY modified-tracked, $KEPT_UNPUSHED unpushed, $KEPT_UNTRACKED untracked-only, $KEPT_UNREACHABLE uncommitted-content, $KEPT_YOUNG too-young, $KEPT_UNREADABLE unreadable errors: $ERRORS"
254
307
  [ "$APPLY" = 0 ] && log "(dry run — rerun with --apply to delete)"
255
308
  exit 0
@@ -0,0 +1,318 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Record the public export surface this package ships, so a removal is an
4
+ * observation rather than an inference.
5
+ *
6
+ * ## The defect this exists against
7
+ *
8
+ * A release shipped a changelog declaring ZERO breaking changes across a range
9
+ * whose diff had removed a public export. The changelog was accurate about its
10
+ * own contents and useless as a safety signal: a downstream reader did the
11
+ * right check, carefully, with a positive control, and got a confident wrong
12
+ * answer. Two consumer repositories were about to upgrade on the strength of it
13
+ * (CodySwannGT/lisa#3718).
14
+ *
15
+ * The mechanism is that the attestation is **derived from something adjacent**.
16
+ * "No breaking changes" is computed from commit-message conventions — whether
17
+ * anyone wrote `!` or a `BREAKING CHANGE:` footer — and a template rewrite that
18
+ * drops an exported symbol carries neither. So the claim is about what authors
19
+ * typed, and it is read as a claim about what happened to the exported surface.
20
+ *
21
+ * A claim and an observation are different things. `"no breaking changes"` is a
22
+ * claim. `"these names were exported before and are exported now"` is an
23
+ * observation, and it is the one a consumer actually needs.
24
+ *
25
+ * ## Why the surface is recorded rather than computed at release time
26
+ *
27
+ * A committed artifact makes the removal **visible in the pull request that
28
+ * causes it**, not only in the release notes afterwards. The diff is the signal.
29
+ * It also means the comparison is a pure git operation with no network — which
30
+ * matters, because the release path already depends on registry availability
31
+ * that has failed under us.
32
+ *
33
+ * ## What it covers, and what it does not
34
+ *
35
+ * The shipped `.mjs` scripts under the `files` allowlist. That is deliberately
36
+ * where the incident happened: a `copy-overwrite` template is installed into a
37
+ * consumer's own `scripts/` directory, and the consumer's repo-owned code
38
+ * imports from it. Those files are invisible to an import graph on either side
39
+ * — Lisa cannot see the consumer's importer, and the consumer has no dependency
40
+ * to declare — so the exported names are the only shared contract there is.
41
+ *
42
+ * It reads exports **statically**, by their declaration form. It does not
43
+ * execute the modules, and it therefore cannot see a name attached at runtime.
44
+ * That limit is deliberate: importing 130 shipped scripts to enumerate them
45
+ * would run their top-level code during a build.
46
+ * @module scripts/generate-export-surface
47
+ */
48
+ import { execFileSync } from "node:child_process";
49
+ import { readFileSync, writeFileSync } from "node:fs";
50
+ import path from "node:path";
51
+
52
+ /** Directories inside the `files` allowlist that ship executable `.mjs`. */
53
+ const SHIPPED_ROOTS = [
54
+ "all",
55
+ "typescript",
56
+ "rails",
57
+ "expo",
58
+ "nestjs",
59
+ "cdk",
60
+ "phaser",
61
+ "harper-fabric",
62
+ "scripts",
63
+ "npm-package",
64
+ ];
65
+
66
+ /**
67
+ * Where the recorded surface lives.
68
+ *
69
+ * JSON rather than a TypeScript module, and the reason is the diff. One
70
+ * exported name per line is what makes a removal show up as a DELETED line in
71
+ * the pull request that causes it — which is the entire point of recording the
72
+ * surface. At 974 names that is well past the 300-line ceiling `max-lines`
73
+ * applies to source, and the sibling generated artifacts buy their way past it
74
+ * with a file-wide `eslint-disable`. Data does not need to be source: as JSON
75
+ * the artifact keeps the per-name diff, needs no waiver, and its reader becomes
76
+ * `JSON.parse` instead of a hand-rolled line parser that could silently
77
+ * disagree with the renderer.
78
+ */
79
+ const ARTIFACT = "src/core/export-surface.json";
80
+
81
+ /**
82
+ * Declaration forms that introduce a named export.
83
+ *
84
+ * Anchored at line start, because an `export` inside a string or a comment is
85
+ * not a declaration and counting it would make the artifact disagree with the
86
+ * module. The corpus was surveyed before this list was written: 613 `function`,
87
+ * 306 `const`, 38 `async function`, 14 `class`, 4 brace-lists.
88
+ */
89
+ const DECLARATION =
90
+ /^export\s+(?:async\s+)?(?:function|const|let|var|class)\s+([A-Za-z_$][\w$]*)/;
91
+
92
+ /** `export { a, b as c }` — the re-export form. */
93
+ const BRACE_LIST = /^export\s*\{([^}]*)\}/;
94
+
95
+ /**
96
+ * Every tracked, shipped `.mjs` path.
97
+ * @param {string} cwd Repository root.
98
+ * @returns {string[]} Repo-relative paths, sorted.
99
+ */
100
+ export function shippedScripts(cwd = process.cwd()) {
101
+ const listed = execFileSync("git", ["ls-files"], {
102
+ cwd,
103
+ encoding: "utf8",
104
+ maxBuffer: 64 * 1024 * 1024,
105
+ }).split("\n");
106
+ return listed
107
+ .filter(file => file.endsWith(".mjs"))
108
+ .filter(file => SHIPPED_ROOTS.includes(file.split("/")[0]))
109
+ .sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
110
+ }
111
+
112
+ /**
113
+ * The names one module exports.
114
+ *
115
+ * `export default` is deliberately excluded: it has no name to compare, so
116
+ * recording it would add a row that can never change meaningfully.
117
+ * @param {string} source Module source.
118
+ * @returns {string[]} Exported names, sorted and deduplicated.
119
+ */
120
+ export function exportedNames(source) {
121
+ const names = new Set();
122
+ for (const line of source.split("\n")) {
123
+ const declared = DECLARATION.exec(line);
124
+ if (declared) {
125
+ names.add(declared[1]);
126
+ continue;
127
+ }
128
+ const braced = BRACE_LIST.exec(line);
129
+ if (!braced) continue;
130
+ for (const clause of braced[1].split(",")) {
131
+ // `a as b` exports b; the local name is not part of the surface.
132
+ const parts = clause.trim().split(/\s+as\s+/);
133
+ const exported = (parts.at(-1) ?? "").trim();
134
+ if (exported && exported !== "default") names.add(exported);
135
+ }
136
+ }
137
+ return [...names].sort((left, right) =>
138
+ left < right ? -1 : left > right ? 1 : 0
139
+ );
140
+ }
141
+
142
+ /**
143
+ * The surface as it stands in the working tree.
144
+ * @param {string} cwd Repository root.
145
+ * @returns {Record<string, string[]>} Path to exported names.
146
+ */
147
+ export function currentSurface(cwd = process.cwd()) {
148
+ const surface = {};
149
+ for (const file of shippedScripts(cwd)) {
150
+ const names = exportedNames(readFileSync(path.join(cwd, file), "utf8"));
151
+ if (names.length > 0) surface[file] = names;
152
+ }
153
+ return surface;
154
+ }
155
+
156
+ /**
157
+ * Render the artifact.
158
+ * @param {Record<string, string[]>} surface The recorded surface.
159
+ * @returns {string} Artifact JSON, newline-terminated.
160
+ */
161
+ export function renderArtifact(surface) {
162
+ const total = Object.values(surface).reduce(
163
+ (sum, names) => sum + names.length,
164
+ 0
165
+ );
166
+ return `${JSON.stringify(
167
+ {
168
+ $generatedBy: "scripts/generate-export-surface.mjs",
169
+ $doNotEdit:
170
+ "The public export surface this package ships, recorded so a removal is an OBSERVATION rather than an inference from commit-message conventions. This file's diff is what makes a removed export visible in the pull request that causes it (CodySwannGT/lisa#3718).",
171
+ $modules: Object.keys(surface).length,
172
+ $names: total,
173
+ surface,
174
+ },
175
+ null,
176
+ 2
177
+ )}\n`;
178
+ }
179
+
180
+ /**
181
+ * Read the recorded surface out of an artifact's source.
182
+ *
183
+ * Parsed from text rather than imported, so the same reader works on a
184
+ * `git show` of an older revision — which is what comparing against a previous
185
+ * release requires.
186
+ * @param {string} source Artifact source.
187
+ * @returns {Record<string, string[]>} Path to exported names.
188
+ */
189
+ export function parseArtifact(source) {
190
+ const parsed = JSON.parse(source);
191
+ return parsed.surface ?? {};
192
+ }
193
+
194
+ /**
195
+ * Names present in `before` and absent from `after`.
196
+ * @param {Record<string, string[]>} before Earlier surface.
197
+ * @param {Record<string, string[]>} after Later surface.
198
+ * @returns {Array<{file: string, name: string}>} Removals, sorted.
199
+ */
200
+ export function removedExports(before, after) {
201
+ const gone = [];
202
+ for (const [file, names] of Object.entries(before)) {
203
+ const kept = new Set(after[file] ?? []);
204
+ for (const name of names) if (!kept.has(name)) gone.push({ file, name });
205
+ }
206
+ return gone.sort((left, right) =>
207
+ left.file === right.file
208
+ ? left.name < right.name
209
+ ? -1
210
+ : 1
211
+ : left.file < right.file
212
+ ? -1
213
+ : 1
214
+ );
215
+ }
216
+
217
+ /**
218
+ * The changelog section a release carries when it removes exports.
219
+ *
220
+ * Emits nothing when nothing was removed — a release that removed no export
221
+ * should not carry a paragraph saying so, or the section becomes noise that
222
+ * readers learn to skip, which is the failure this whole file exists against.
223
+ * @param {Array<{file: string, name: string}>} removals From {@link removedExports}.
224
+ * @returns {string} Markdown, or an empty string.
225
+ */
226
+ export function changelogSection(removals) {
227
+ if (removals.length === 0) return "";
228
+ const rows = removals
229
+ .map(({ file, name }) => `- \`${name}\` — was exported by \`${file}\``)
230
+ .join("\n");
231
+ return `### ⚠ Removed public exports\n\n${String(removals.length)} exported name(s) present in the previous release are absent from this one. A consumer importing any of them will break on upgrade.\n\n${rows}\n`;
232
+ }
233
+
234
+ /**
235
+ * Read the artifact as it stood at a git revision.
236
+ * @param {string} ref Git revision.
237
+ * @param {string} cwd Repository root.
238
+ * @returns {Record<string, string[]>|null} The surface, or null when absent.
239
+ */
240
+ function surfaceAt(ref, cwd) {
241
+ try {
242
+ const source = execFileSync("git", ["show", `${ref}:${ARTIFACT}`], {
243
+ cwd,
244
+ encoding: "utf8",
245
+ maxBuffer: 64 * 1024 * 1024,
246
+ stdio: ["ignore", "pipe", "ignore"],
247
+ });
248
+ return parseArtifact(source);
249
+ } catch {
250
+ // The artifact did not exist at that revision. That is not "no exports
251
+ // were removed" — it is "no comparison is possible", and the two must not
252
+ // render the same, so the caller is told rather than shown an empty list.
253
+ //
254
+ // probe-direction: fail-closed — `main` renders this null as exit 2 and
255
+ // says so in words, which any caller treating non-zero as a block reads
256
+ // as a refusal. An unreadable revision costs a false block, never a
257
+ // release that reports zero removals because it could not look.
258
+ return null;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * CLI.
264
+ * @returns {number} Exit code.
265
+ */
266
+ function main() {
267
+ const cwd = process.cwd();
268
+ const argv = process.argv.slice(2);
269
+ const since = argv.includes("--removed-since")
270
+ ? argv[argv.indexOf("--removed-since") + 1]
271
+ : null;
272
+
273
+ if (since) {
274
+ const before = surfaceAt(since, cwd);
275
+ if (before === null) {
276
+ // Exit 2, not 0. "I could not compare" and "nothing was removed" are
277
+ // opposite facts, and this whole file exists because a release reported
278
+ // the second while meaning something closer to the first. A caller that
279
+ // treats any non-zero as a block gets the safe reading by default; one
280
+ // that wants to allow the very first release — before any surface has
281
+ // been recorded — can distinguish 2 from 1 deliberately.
282
+ process.stdout.write(
283
+ `No export surface recorded at ${since}; nothing to compare. This is NOT a clean result — it is an unanswered question.\n`
284
+ );
285
+ return 2;
286
+ }
287
+ const removals = removedExports(before, currentSurface(cwd));
288
+ const section = changelogSection(removals);
289
+ process.stdout.write(
290
+ section === ""
291
+ ? `No public exports removed since ${since}. ${String(Object.values(before).reduce((sum, names) => sum + names.length, 0))} names compared.\n`
292
+ : section
293
+ );
294
+ return removals.length > 0 ? 1 : 0;
295
+ }
296
+
297
+ const rendered = renderArtifact(currentSurface(cwd));
298
+ const target = path.join(cwd, ARTIFACT);
299
+ if (argv.includes("--check")) {
300
+ let existing = "";
301
+ try {
302
+ existing = readFileSync(target, "utf8");
303
+ } catch {
304
+ existing = "";
305
+ }
306
+ if (existing === rendered) return 0;
307
+ process.stderr.write(
308
+ `${ARTIFACT} is stale. Regenerate it in this commit:\n bun run build:export-surface\n`
309
+ );
310
+ return 1;
311
+ }
312
+ writeFileSync(target, rendered);
313
+ return 0;
314
+ }
315
+
316
+ if (import.meta.url === `file://${process.argv[1]}`) {
317
+ process.exit(main());
318
+ }
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Pure parsing and classification helpers for the shipped-surface removal gate
3
+ * (`scripts/check-shipped-surface-removals.mjs`, CodySwannGT/lisa#3849).
4
+ *
5
+ * Everything here is a total function over strings and plain objects: no git,
6
+ * no filesystem, no clock. The gate keeps its I/O in one file so these can be
7
+ * mutation-tested by hand without a repository fixture, which matters because
8
+ * `stryker.conf.json`'s `mutate` is a ten-file allowlist that a new `scripts/`
9
+ * module does not join - an automated run over this file reports
10
+ * `nothing-to-mutate` and says nothing at all.
11
+ *
12
+ * @module scripts/lib/shipped-surface
13
+ */
14
+ import path from "node:path";
15
+
16
+ /**
17
+ * Extensions a consumer can put behind a `package.json` script entry.
18
+ *
19
+ * The list is deliberately broader than "things Lisa runs itself": the
20
+ * question this gate asks is not what upstream invokes, it is what a HOST
21
+ * could have wired. See `isConsumerBindable`.
22
+ */
23
+ export const EXECUTABLE_EXTENSIONS = Object.freeze([
24
+ ".cjs",
25
+ ".js",
26
+ ".mjs",
27
+ ".py",
28
+ ".sh",
29
+ ".ts",
30
+ ]);
31
+
32
+ /** Destination directory whose contents a host routinely names in scripts. */
33
+ export const BINDABLE_ROOT = "scripts/";
34
+
35
+ /**
36
+ * Whether a destination path is one a consumer may have bound into their own
37
+ * `package.json` - and therefore one that must NOT be propagated as a deletion
38
+ * by default (#3849, revised scenario 3).
39
+ *
40
+ * The rule is the ticket's, stated as a question about the host rather than
41
+ * about upstream: not "does upstream still need it" but "could a consumer have
42
+ * wired this into their own scripts?" For an executable under `scripts/` the
43
+ * answer is usually yes, and the reported instance is exactly that shape - an
44
+ * importer with three npm script entries, a workflow, and a test in the host.
45
+ *
46
+ * @param {string} destination - path in the consumer's tree.
47
+ * @returns {boolean} true when a host may have bound it.
48
+ */
49
+ export function isConsumerBindable(destination) {
50
+ if (!destination.startsWith(BINDABLE_ROOT)) return false;
51
+ return EXECUTABLE_EXTENSIONS.includes(path.posix.extname(destination));
52
+ }
53
+
54
+ /** Declaration forms that name exactly one exported binding. */
55
+ const DECLARED_EXPORT_PATTERNS = Object.freeze([
56
+ /^\s*export\s+(?:async\s+)?function\s*\*?\s*([A-Za-z_$][\w$]*)/gm,
57
+ /^\s*export\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)/gm,
58
+ /^\s*export\s+class\s+([A-Za-z_$][\w$]*)/gm,
59
+ ]);
60
+
61
+ /** `export { a, b as c }`, with or without a `from` clause. */
62
+ const EXPORT_CLAUSE = /^\s*export\s*\{([^}]*)\}/gm;
63
+
64
+ /** `export * from "./x.mjs"` - a re-export whose names live in the target. */
65
+ const EXPORT_STAR = /^\s*export\s*\*\s*from\s*["']([^"']+)["']/gm;
66
+
67
+ /** `import { a, b as c } from "./x.mjs"` - named imports only. */
68
+ const IMPORT_CLAUSE = /^\s*import\s*\{([^}]*)\}\s*from\s*["']([^"']+)["']/gm;
69
+
70
+ /**
71
+ * The exported name a `{ ... }` clause member introduces: the alias when one
72
+ * is written, the original otherwise. `default` members are dropped - a
73
+ * default export is not a named one and the gate never asks about it.
74
+ *
75
+ * @param {string} member - one comma-separated clause member.
76
+ * @returns {string | null} the exported name, or null when there is none.
77
+ */
78
+ export function clauseExportedName(member) {
79
+ const trimmed = member.trim().replace(/^type\s+/, "");
80
+ if (trimmed === "") return null;
81
+ const parts = trimmed.split(/\s+as\s+/);
82
+ const name = (parts.length > 1 ? parts[1] : parts[0]).trim();
83
+ return name === "" || name === "default" ? null : name;
84
+ }
85
+
86
+ /**
87
+ * The imported local binding a `{ ... }` clause member requests from the
88
+ * target module: always the name BEFORE any `as`, because that is the symbol
89
+ * the target must actually export.
90
+ *
91
+ * @param {string} member - one comma-separated clause member.
92
+ * @returns {string | null} the requested export name, or null when there is none.
93
+ */
94
+ export function clauseImportedName(member) {
95
+ const trimmed = member.trim().replace(/^type\s+/, "");
96
+ if (trimmed === "") return null;
97
+ const name = trimmed.split(/\s+as\s+/)[0].trim();
98
+ return name === "" || name === "default" ? null : name;
99
+ }
100
+
101
+ /**
102
+ * Every named export a module source declares directly, plus the relative
103
+ * specifiers it re-exports wholesale.
104
+ *
105
+ * `export *` is returned rather than resolved: resolution needs the delivery
106
+ * view, which lives in the gate. Doing it here would make this module read the
107
+ * filesystem.
108
+ *
109
+ * @param {string} source - module source text.
110
+ * @returns {{ names: Set<string>, starFrom: string[] }} declared names and
111
+ * the relative specifiers whose names are inherited.
112
+ */
113
+ export function parseNamedExports(source) {
114
+ const names = new Set();
115
+ for (const pattern of DECLARED_EXPORT_PATTERNS) {
116
+ for (const match of source.matchAll(pattern)) names.add(match[1]);
117
+ }
118
+ for (const match of source.matchAll(EXPORT_CLAUSE)) {
119
+ for (const member of match[1].split(",")) {
120
+ const name = clauseExportedName(member);
121
+ if (name !== null) names.add(name);
122
+ }
123
+ }
124
+ const starFrom = [...source.matchAll(EXPORT_STAR)]
125
+ .map(match => match[1])
126
+ .filter(specifier => specifier.startsWith("."));
127
+ return { names, starFrom };
128
+ }
129
+
130
+ /**
131
+ * Every named import a module source requests from a RELATIVE specifier.
132
+ *
133
+ * Bare specifiers are skipped: they resolve through `node_modules` in the
134
+ * consumer's tree, which this repository cannot see and does not govern.
135
+ *
136
+ * @param {string} source - module source text.
137
+ * @returns {Array<{ specifier: string, names: string[] }>} one entry per
138
+ * relative named-import statement, in source order.
139
+ */
140
+ export function parseRelativeNamedImports(source) {
141
+ const requests = [];
142
+ for (const match of source.matchAll(IMPORT_CLAUSE)) {
143
+ const specifier = match[2];
144
+ if (!specifier.startsWith(".")) continue;
145
+ const names = match[1]
146
+ .split(",")
147
+ .map(member => clauseImportedName(member))
148
+ .filter(name => name !== null);
149
+ if (names.length > 0) requests.push({ names, specifier });
150
+ }
151
+ return requests;
152
+ }
153
+
154
+ /**
155
+ * The destination a relative specifier resolves to, given the destination
156
+ * path of the importing module.
157
+ *
158
+ * @param {string} importerDestination - the importer's path in the consumer tree.
159
+ * @param {string} specifier - a relative specifier from that importer.
160
+ * @returns {string} the resolved destination path.
161
+ */
162
+ export function resolveRelative(importerDestination, specifier) {
163
+ return path.posix.normalize(
164
+ path.posix.join(path.posix.dirname(importerDestination), specifier)
165
+ );
166
+ }
167
+
168
+ /**
169
+ * The ledger key for one governed surface.
170
+ *
171
+ * A whole-file removal is keyed by path alone; an export removal is keyed
172
+ * `path symbol`. A shipped path never contains a space, so the two key spaces
173
+ * cannot collide.
174
+ *
175
+ * @param {string} shippedPath - repo-relative path inside a delivery lane.
176
+ * @param {string} [symbol] - the exported name, when a symbol rather than a file.
177
+ * @returns {string} the key.
178
+ */
179
+ export function removalKey(shippedPath, symbol) {
180
+ return symbol === undefined ? shippedPath : `${shippedPath} ${symbol}`;
181
+ }
182
+
183
+ /**
184
+ * Index a removal ledger by the surface each entry governs.
185
+ *
186
+ * @param {ReadonlyArray<{ path: string, export?: string, note?: string }>} removals
187
+ * ledger entries.
188
+ * @returns {Map<string, { path: string, export?: string, note?: string }>} the index.
189
+ */
190
+ export function indexRemovals(removals) {
191
+ return new Map(
192
+ removals.map(entry => [removalKey(entry.path, entry.export), entry])
193
+ );
194
+ }
195
+
196
+ /**
197
+ * Whether a ledger entry actually records something an operator can act on.
198
+ *
199
+ * An entry with a blank note is treated as absent. A note that says nothing is
200
+ * the same silence the gate exists to break, and accepting it would let the
201
+ * ledger be satisfied by a placeholder.
202
+ *
203
+ * @param {{ note?: unknown } | undefined} entry - the indexed ledger entry.
204
+ * @returns {boolean} true when the entry carries a usable note.
205
+ */
206
+ export function hasUsableNote(entry) {
207
+ return (
208
+ entry !== undefined &&
209
+ typeof entry.note === "string" &&
210
+ entry.note.trim() !== ""
211
+ );
212
+ }