@event4u/agent-config 14.23.0 → 15.0.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 (503) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +198 -0
  5. package/README.md +1 -1
  6. package/dist/agent-src/commands/agent-handoff.md +8 -7
  7. package/dist/agent-src/commands/agents/init.md +1 -1
  8. package/dist/agent-src/commands/agents/user/accept.md +4 -4
  9. package/dist/agent-src/commands/agents/user/delete.md +2 -2
  10. package/dist/agent-src/commands/agents/user/init.md +3 -3
  11. package/dist/agent-src/commands/agents/user/review.md +3 -3
  12. package/dist/agent-src/commands/agents/user/show.md +3 -3
  13. package/dist/agent-src/commands/agents/user/update.md +2 -2
  14. package/dist/agent-src/commands/agents/user.md +6 -6
  15. package/dist/agent-src/commands/analytics/prune.md +1 -1
  16. package/dist/agent-src/commands/analytics/show.md +2 -2
  17. package/dist/agent-src/commands/analyze/decision.md +2 -2
  18. package/dist/agent-src/commands/analyze/inbox.md +5 -5
  19. package/dist/agent-src/commands/analyze/incident.md +3 -3
  20. package/dist/agent-src/commands/analyze/near-miss.md +3 -3
  21. package/dist/agent-src/commands/analyze/postmortem.md +4 -4
  22. package/dist/agent-src/commands/analyze/premortem.md +2 -2
  23. package/dist/agent-src/commands/analyze.md +2 -2
  24. package/dist/agent-src/commands/bug/fix.md +1 -1
  25. package/dist/agent-src/commands/bug/investigate.md +2 -2
  26. package/dist/agent-src/commands/cost/profile.md +3 -3
  27. package/dist/agent-src/commands/design.md +1 -1
  28. package/dist/agent-src/commands/feature/explore.md +1 -1
  29. package/dist/agent-src/commands/feature/plan.md +2 -2
  30. package/dist/agent-src/commands/feature/roadmap.md +2 -2
  31. package/dist/agent-src/commands/ghostwriter/fetch.md +4 -4
  32. package/dist/agent-src/commands/ghostwriter/list.md +1 -1
  33. package/dist/agent-src/commands/ghostwriter/show.md +1 -1
  34. package/dist/agent-src/commands/ghostwriter/write.md +9 -9
  35. package/dist/agent-src/commands/humanize.md +6 -6
  36. package/dist/agent-src/commands/jira-ticket.md +1 -1
  37. package/dist/agent-src/commands/judge/solo.md +1 -1
  38. package/dist/agent-src/commands/judge.md +1 -1
  39. package/dist/agent-src/commands/knowledge/cross-repo.md +1 -1
  40. package/dist/agent-src/commands/knowledge/forget.md +1 -1
  41. package/dist/agent-src/commands/knowledge/ingest.md +2 -2
  42. package/dist/agent-src/commands/knowledge/list.md +1 -1
  43. package/dist/agent-src/commands/memory/add.md +3 -3
  44. package/dist/agent-src/commands/memory/load.md +4 -4
  45. package/dist/agent-src/commands/memory/mine-session.md +1 -1
  46. package/dist/agent-src/commands/memory/promote.md +4 -4
  47. package/dist/agent-src/commands/memory/propose.md +2 -2
  48. package/dist/agent-src/commands/mode.md +5 -5
  49. package/dist/agent-src/commands/post-as/ghostwriter.md +3 -3
  50. package/dist/agent-src/commands/post-as/me.md +7 -7
  51. package/dist/agent-src/commands/pr/create.md +1 -1
  52. package/dist/agent-src/commands/pr/merge.md +1 -1
  53. package/dist/agent-src/commands/profile/activate.md +1 -1
  54. package/dist/agent-src/commands/profile/show.md +1 -1
  55. package/dist/agent-src/commands/research/deep.md +2 -2
  56. package/dist/agent-src/commands/research/report.md +2 -2
  57. package/dist/agent-src/commands/review/changes.md +1 -1
  58. package/dist/agent-src/commands/review/routing.md +1 -1
  59. package/dist/agent-src/commands/roadmap/create.md +1 -1
  60. package/dist/agent-src/commands/roadmap/materialize.md +2 -2
  61. package/dist/agent-src/commands/roadmap/next.md +1 -1
  62. package/dist/agent-src/commands/roadmap/process-full.md +2 -2
  63. package/dist/agent-src/commands/security-audit-config.md +3 -3
  64. package/dist/agent-src/commands/skill/preview.md +1 -1
  65. package/dist/agent-src/commands/skills/discover.md +1 -1
  66. package/dist/agent-src/commands/sync/agent-settings.md +2 -2
  67. package/dist/agent-src/commands/sync/gitignore/fix.md +1 -1
  68. package/dist/agent-src/commands/tests/create.md +1 -1
  69. package/dist/agent-src/commands/tests/e2e-heal.md +1 -1
  70. package/dist/agent-src/commands/tests/e2e-plan.md +1 -1
  71. package/dist/agent-src/contexts/authority/kernel-rule-edits.md +6 -6
  72. package/dist/agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +1 -2
  73. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +2 -2
  74. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +88 -1
  75. package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +1 -1
  76. package/dist/agent-src/contexts/contracts/frugality-charter.md +1 -1
  77. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +1 -1
  78. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +1 -1
  79. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +1 -1
  80. package/dist/agent-src/contexts/execution/cheap-question-mechanics.md +1 -1
  81. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +3 -3
  82. package/dist/agent-src/contexts/execution/mandated-lines.md +1 -1
  83. package/dist/agent-src/contexts/execution/non-interactive-contract.md +1 -1
  84. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +1 -1
  85. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +5 -5
  86. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +1 -1
  87. package/dist/agent-src/contexts/execution/subagent-modes-detail.md +1 -1
  88. package/dist/agent-src/contexts/execution/subagent-routing.md +2 -2
  89. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +1 -1
  90. package/dist/agent-src/contexts/execution/user-memory-channels.md +3 -3
  91. package/dist/agent-src/contexts/execution/verification-mechanics.md +1 -1
  92. package/dist/agent-src/contexts/judges/persona-voice-rubric.md +2 -2
  93. package/dist/agent-src/guidelines/abstraction-thresholds.md +88 -0
  94. package/dist/agent-src/guidelines/agent-infra/5w2h-analysis.md +260 -0
  95. package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +99 -0
  96. package/dist/agent-src/guidelines/agent-infra/agent-interaction-and-decision-quality.md +325 -0
  97. package/dist/agent-src/guidelines/agent-infra/api-cost-levers.md +177 -0
  98. package/dist/agent-src/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +222 -0
  99. package/dist/agent-src/guidelines/agent-infra/ask-when-uncertain-demos.md +134 -0
  100. package/dist/agent-src/guidelines/agent-infra/asking-and-brevity-examples.md +198 -0
  101. package/dist/agent-src/guidelines/agent-infra/break-glass-usage.md +113 -0
  102. package/dist/agent-src/guidelines/agent-infra/carve-out-predicates.md +17 -0
  103. package/dist/agent-src/guidelines/agent-infra/comparison-matrix.md +179 -0
  104. package/dist/agent-src/guidelines/agent-infra/context-hygiene-mechanics.md +165 -0
  105. package/dist/agent-src/guidelines/agent-infra/corpus-grounding-authoring.md +91 -0
  106. package/dist/agent-src/guidelines/agent-infra/critical-thinking.md +156 -0
  107. package/dist/agent-src/guidelines/agent-infra/cross-source-consistency-mechanics.md +109 -0
  108. package/dist/agent-src/guidelines/agent-infra/developer-judgment.md +82 -0
  109. package/dist/agent-src/guidelines/agent-infra/direct-answers-demos.md +142 -0
  110. package/dist/agent-src/guidelines/agent-infra/domain-adoption-gates.md +112 -0
  111. package/dist/agent-src/guidelines/agent-infra/domain-eval-anti-pattern.md +53 -0
  112. package/dist/agent-src/guidelines/agent-infra/domain-pack-architecture.md +56 -0
  113. package/dist/agent-src/guidelines/agent-infra/downstream-changes-mechanics.md +91 -0
  114. package/dist/agent-src/guidelines/agent-infra/emphasis-budget.md +45 -0
  115. package/dist/agent-src/guidelines/agent-infra/engineering-memory-data-format.md +210 -0
  116. package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +92 -0
  117. package/dist/agent-src/guidelines/agent-infra/existence-question-verdicts.md +80 -0
  118. package/dist/agent-src/guidelines/agent-infra/failure-signatures.md +70 -0
  119. package/dist/agent-src/guidelines/agent-infra/false-green.md +233 -0
  120. package/dist/agent-src/guidelines/agent-infra/first-principles.md +192 -0
  121. package/dist/agent-src/guidelines/agent-infra/framework-neutrality-patterns.md +66 -0
  122. package/dist/agent-src/guidelines/agent-infra/frontier-reasoning-operating-profile.md +169 -0
  123. package/dist/agent-src/guidelines/agent-infra/gate-authoring.md +177 -0
  124. package/dist/agent-src/guidelines/agent-infra/guarded-baseline.md +39 -0
  125. package/dist/agent-src/guidelines/agent-infra/installed-tools-manifest.md +142 -0
  126. package/dist/agent-src/guidelines/agent-infra/inversion-thinking.md +388 -0
  127. package/dist/agent-src/guidelines/agent-infra/ios-simulator-guide.md +378 -0
  128. package/dist/agent-src/guidelines/agent-infra/language-and-tone-examples.md +141 -0
  129. package/dist/agent-src/guidelines/agent-infra/layered-settings.md +317 -0
  130. package/dist/agent-src/guidelines/agent-infra/linked-projects-onboarding-gate.md +80 -0
  131. package/dist/agent-src/guidelines/agent-infra/mcp-request-signing.md +197 -0
  132. package/dist/agent-src/guidelines/agent-infra/memory-access.md +149 -0
  133. package/dist/agent-src/guidelines/agent-infra/mental-models.md +314 -0
  134. package/dist/agent-src/guidelines/agent-infra/minimal-safe-diff-mechanics.md +241 -0
  135. package/dist/agent-src/guidelines/agent-infra/missing-tool-handling.md +66 -0
  136. package/dist/agent-src/guidelines/agent-infra/model-recommendation.md +101 -0
  137. package/dist/agent-src/guidelines/agent-infra/naming.md +69 -0
  138. package/dist/agent-src/guidelines/agent-infra/notes-horizon-mechanics.md +90 -0
  139. package/dist/agent-src/guidelines/agent-infra/output-patterns.md +117 -0
  140. package/dist/agent-src/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  141. package/dist/agent-src/guidelines/agent-infra/reuse-verdict-mechanics.md +71 -0
  142. package/dist/agent-src/guidelines/agent-infra/review-routing-data-format.md +143 -0
  143. package/dist/agent-src/guidelines/agent-infra/roadmap-deferred-resolution-provenance.md +54 -0
  144. package/dist/agent-src/guidelines/agent-infra/roadmap-progress-mechanics.md +274 -0
  145. package/dist/agent-src/guidelines/agent-infra/role-contracts.md +209 -0
  146. package/dist/agent-src/guidelines/agent-infra/role-mode-router.md +89 -0
  147. package/dist/agent-src/guidelines/agent-infra/rule-body-migration-inventory.md +241 -0
  148. package/dist/agent-src/guidelines/agent-infra/rule-type-governance.md +73 -0
  149. package/dist/agent-src/guidelines/agent-infra/runtime-layer.md +89 -0
  150. package/dist/agent-src/guidelines/agent-infra/scqa-framework.md +526 -0
  151. package/dist/agent-src/guidelines/agent-infra/security-lint-containment.md +121 -0
  152. package/dist/agent-src/guidelines/agent-infra/self-improvement-pipeline.md +134 -0
  153. package/dist/agent-src/guidelines/agent-infra/simplicity-and-goal-demos.md +211 -0
  154. package/dist/agent-src/guidelines/agent-infra/six-hats.md +353 -0
  155. package/dist/agent-src/guidelines/agent-infra/size-and-scope.md +229 -0
  156. package/dist/agent-src/guidelines/agent-infra/skill-quality-checklist.md +157 -0
  157. package/dist/agent-src/guidelines/agent-infra/source-confidentiality-mechanics.md +109 -0
  158. package/dist/agent-src/guidelines/agent-infra/symptom-driven-harvest-loop.md +104 -0
  159. package/dist/agent-src/guidelines/agent-infra/systems-thinking.md +220 -0
  160. package/dist/agent-src/guidelines/agent-infra/tool-description-as-policy.md +35 -0
  161. package/dist/agent-src/guidelines/agent-infra/tool-integration.md +73 -0
  162. package/dist/agent-src/guidelines/agent-infra/traceability-field-mechanics.md +50 -0
  163. package/dist/agent-src/guidelines/agent-infra/untrusted-input-spotlighting.md +166 -0
  164. package/dist/agent-src/guidelines/agent-infra/verify-before-complete-demos.md +128 -0
  165. package/dist/agent-src/guidelines/augment-portability-patterns.md +70 -0
  166. package/dist/agent-src/guidelines/code-clarity.md +350 -0
  167. package/dist/agent-src/guidelines/component-oriented-and-oop-development.md +189 -0
  168. package/dist/agent-src/guidelines/cross-role-handoff.md +127 -0
  169. package/dist/agent-src/guidelines/design-antipatterns-triggers.json +18 -0
  170. package/dist/agent-src/guidelines/design-antipatterns.md +327 -0
  171. package/dist/agent-src/guidelines/design-asset-discipline.md +38 -0
  172. package/dist/agent-src/guidelines/design-canon.md +63 -0
  173. package/dist/agent-src/guidelines/design-fidelity-mechanics.md +293 -0
  174. package/dist/agent-src/guidelines/design-fidelity-routing.md +78 -0
  175. package/dist/agent-src/guidelines/design-handover-extraction.md +117 -0
  176. package/dist/agent-src/guidelines/design-modes.md +143 -0
  177. package/dist/agent-src/guidelines/design-reconciliation.md +172 -0
  178. package/dist/agent-src/guidelines/docs/readme-size-and-splitting.md +209 -0
  179. package/dist/agent-src/guidelines/e2e/playwright.md +363 -0
  180. package/dist/agent-src/guidelines/gtm-handoff.md +114 -0
  181. package/dist/agent-src/guidelines/monorepo-antipatterns.md +141 -0
  182. package/dist/agent-src/guidelines/php/api-design.md +115 -0
  183. package/dist/agent-src/guidelines/php/artisan-commands.md +81 -0
  184. package/dist/agent-src/guidelines/php/blade-ui.md +78 -0
  185. package/dist/agent-src/guidelines/php/controllers.md +90 -0
  186. package/dist/agent-src/guidelines/php/database.md +194 -0
  187. package/dist/agent-src/guidelines/php/eloquent.md +208 -0
  188. package/dist/agent-src/guidelines/php/flux.md +80 -0
  189. package/dist/agent-src/guidelines/php/general.md +207 -0
  190. package/dist/agent-src/guidelines/php/git.md +164 -0
  191. package/dist/agent-src/guidelines/php/jobs.md +111 -0
  192. package/dist/agent-src/guidelines/php/livewire.md +71 -0
  193. package/dist/agent-src/guidelines/php/logging.md +79 -0
  194. package/dist/agent-src/guidelines/php/naming.md +89 -0
  195. package/dist/agent-src/guidelines/php/patterns/dependency-injection.md +57 -0
  196. package/dist/agent-src/guidelines/php/patterns/dtos.md +199 -0
  197. package/dist/agent-src/guidelines/php/patterns/events.md +67 -0
  198. package/dist/agent-src/guidelines/php/patterns/factory.md +53 -0
  199. package/dist/agent-src/guidelines/php/patterns/pipelines.md +66 -0
  200. package/dist/agent-src/guidelines/php/patterns/policies.md +66 -0
  201. package/dist/agent-src/guidelines/php/patterns/repositories.md +122 -0
  202. package/dist/agent-src/guidelines/php/patterns/service-layer.md +64 -0
  203. package/dist/agent-src/guidelines/php/patterns/strategy.md +247 -0
  204. package/dist/agent-src/guidelines/php/patterns.md +28 -0
  205. package/dist/agent-src/guidelines/php/performance.md +92 -0
  206. package/dist/agent-src/guidelines/php/php-coding-patterns.md +80 -0
  207. package/dist/agent-src/guidelines/php/resources.md +100 -0
  208. package/dist/agent-src/guidelines/php/security.md +110 -0
  209. package/dist/agent-src/guidelines/php/sql.md +97 -0
  210. package/dist/agent-src/guidelines/php/validations.md +119 -0
  211. package/dist/agent-src/guidelines/php/websocket.md +100 -0
  212. package/dist/agent-src/guidelines/prompt-templates.md +189 -0
  213. package/dist/agent-src/guidelines/redundancy-taxonomy.md +208 -0
  214. package/dist/agent-src/guidelines/wing4-handoff.md +127 -0
  215. package/dist/agent-src/personas/README.md +1 -1
  216. package/dist/agent-src/personas/_template-specialist/persona.md +1 -1
  217. package/dist/agent-src/personas/product-owner.md +1 -1
  218. package/dist/agent-src/rules/active-remediation.md +1 -1
  219. package/dist/agent-src/rules/architecture.md +2 -2
  220. package/dist/agent-src/rules/artifact-drafting-protocol.md +1 -1
  221. package/dist/agent-src/rules/ask-when-uncertain.md +2 -2
  222. package/dist/agent-src/rules/brand-source-of-truth.md +9 -7
  223. package/dist/agent-src/rules/code-comment-discipline.md +2 -2
  224. package/dist/agent-src/rules/code-provenance.md +1 -1
  225. package/dist/agent-src/rules/content-quoting-floor.md +1 -1
  226. package/dist/agent-src/rules/context-hygiene.md +5 -5
  227. package/dist/agent-src/rules/cross-source-consistency.md +1 -1
  228. package/dist/agent-src/rules/design-fidelity.md +36 -46
  229. package/dist/agent-src/rules/direct-answers.md +2 -2
  230. package/dist/agent-src/rules/domain-adoption-policy.md +1 -1
  231. package/dist/agent-src/rules/evaluator-independence.md +1 -1
  232. package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +1 -1
  233. package/dist/agent-src/rules/icon-consistency.md +5 -1
  234. package/dist/agent-src/rules/improve-before-implement.md +1 -1
  235. package/dist/agent-src/rules/language-and-tone.md +3 -3
  236. package/dist/agent-src/rules/minimal-safe-diff.md +1 -1
  237. package/dist/agent-src/rules/notes-first-reasoning.md +2 -2
  238. package/dist/agent-src/rules/recurring-criticism.md +1 -1
  239. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  240. package/dist/agent-src/rules/role-mode-adherence.md +2 -2
  241. package/dist/agent-src/rules/security-sensitive-stop.md +1 -1
  242. package/dist/agent-src/rules/size-enforcement.md +1 -1
  243. package/dist/agent-src/rules/source-confidentiality.md +2 -2
  244. package/dist/agent-src/rules/think-before-action.md +1 -1
  245. package/dist/agent-src/rules/token-efficiency.md +7 -5
  246. package/dist/agent-src/rules/ui-audit-gate.md +3 -2
  247. package/dist/agent-src/rules/untrusted-input-defense.md +3 -3
  248. package/dist/agent-src/skills/activation-design/SKILL.md +2 -2
  249. package/dist/agent-src/skills/adr-create/SKILL.md +5 -5
  250. package/dist/agent-src/skills/ai-council/SKILL.md +1 -1
  251. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +1 -1
  252. package/dist/agent-src/skills/blade-ui/SKILL.md +1 -1
  253. package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +1 -1
  254. package/dist/agent-src/skills/brand-to-tokens/SKILL.md +24 -3
  255. package/dist/agent-src/skills/bug-analyzer/SKILL.md +1 -1
  256. package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
  257. package/dist/agent-src/skills/churn-prevention/SKILL.md +2 -2
  258. package/dist/agent-src/skills/code-refactoring/SKILL.md +3 -3
  259. package/dist/agent-src/skills/code-review/SKILL.md +3 -3
  260. package/dist/agent-src/skills/command-writing/SKILL.md +3 -3
  261. package/dist/agent-src/skills/comp-banding/SKILL.md +1 -1
  262. package/dist/agent-src/skills/competitive-moat-analysis/SKILL.md +1 -1
  263. package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
  264. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +1 -1
  265. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  266. package/dist/agent-src/skills/content-funnel-design/SKILL.md +2 -2
  267. package/dist/agent-src/skills/contracts-cognition/SKILL.md +1 -1
  268. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +2 -2
  269. package/dist/agent-src/skills/corpus-grounding/SKILL.md +2 -2
  270. package/dist/agent-src/skills/customer-research/SKILL.md +2 -2
  271. package/dist/agent-src/skills/dashboard-design/SKILL.md +1 -1
  272. package/dist/agent-src/skills/data-handling-judgment/SKILL.md +1 -1
  273. package/dist/agent-src/skills/deal-qualification-meddic/SKILL.md +2 -2
  274. package/dist/agent-src/skills/decision-record/SKILL.md +3 -3
  275. package/dist/agent-src/skills/decision-review/SKILL.md +8 -8
  276. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +22 -22
  277. package/dist/agent-src/skills/design-intelligence/SKILL.md +4 -4
  278. package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +2 -2
  279. package/dist/agent-src/skills/design-review/SKILL.md +4 -4
  280. package/dist/agent-src/skills/design-system-capture/references/design-system-json.md +20 -1
  281. package/dist/agent-src/skills/design-tokens/SKILL.md +17 -2
  282. package/dist/agent-src/skills/design-variations/SKILL.md +2 -2
  283. package/dist/agent-src/skills/developer-like-execution/SKILL.md +2 -2
  284. package/dist/agent-src/skills/discovery-interview/SKILL.md +2 -2
  285. package/dist/agent-src/skills/doc-coauthoring/SKILL.md +3 -3
  286. package/dist/agent-src/skills/editorial-calendar/SKILL.md +2 -2
  287. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +44 -1
  288. package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +2 -2
  289. package/dist/agent-src/skills/expansion-playbook/SKILL.md +2 -2
  290. package/dist/agent-src/skills/experiment-loop/SKILL.md +1 -1
  291. package/dist/agent-src/skills/experiment-loop/references/protocol.md +1 -1
  292. package/dist/agent-src/skills/fe-design/SKILL.md +3 -3
  293. package/dist/agent-src/skills/fe-design/references/design-patterns.md +1 -1
  294. package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +3 -3
  295. package/dist/agent-src/skills/fe-design/references/source-led-port.md +1 -1
  296. package/dist/agent-src/skills/feature-planning/SKILL.md +1 -1
  297. package/dist/agent-src/skills/flux/SKILL.md +1 -1
  298. package/dist/agent-src/skills/forecast-accuracy/SKILL.md +2 -2
  299. package/dist/agent-src/skills/forecasting/SKILL.md +2 -2
  300. package/dist/agent-src/skills/fundraising-narrative/SKILL.md +2 -2
  301. package/dist/agent-src/skills/funnel-analysis/SKILL.md +2 -2
  302. package/dist/agent-src/skills/gtm-launch/SKILL.md +2 -2
  303. package/dist/agent-src/skills/hiring-loop-design/SKILL.md +1 -1
  304. package/dist/agent-src/skills/html-deck/SKILL.md +5 -5
  305. package/dist/agent-src/skills/humanizer/SKILL.md +1 -1
  306. package/dist/agent-src/skills/humanizer/data/patterns.md +1 -1
  307. package/dist/agent-src/skills/iconography/SKILL.md +3 -3
  308. package/dist/agent-src/skills/judge-synthesis/SKILL.md +1 -1
  309. package/dist/agent-src/skills/laravel-api-endpoint/SKILL.md +2 -2
  310. package/dist/agent-src/skills/launch-readiness/SKILL.md +3 -3
  311. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +6 -6
  312. package/dist/agent-src/skills/livewire/SKILL.md +1 -1
  313. package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
  314. package/dist/agent-src/skills/mcp-builder/SKILL.md +1 -1
  315. package/dist/agent-src/skills/memory-consolidation/SKILL.md +3 -3
  316. package/dist/agent-src/skills/messaging-architecture/SKILL.md +2 -2
  317. package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
  318. package/dist/agent-src/skills/onboarding-design/SKILL.md +2 -2
  319. package/dist/agent-src/skills/onboarding-program/SKILL.md +1 -1
  320. package/dist/agent-src/skills/one-on-one-cadence/SKILL.md +1 -1
  321. package/dist/agent-src/skills/org-design/SKILL.md +1 -1
  322. package/dist/agent-src/skills/overbuild-review-lens/SKILL.md +3 -3
  323. package/dist/agent-src/skills/override-management/SKILL.md +1 -1
  324. package/dist/agent-src/skills/perf-feedback-craft/SKILL.md +1 -1
  325. package/dist/agent-src/skills/persona-writing/SKILL.md +1 -1
  326. package/dist/agent-src/skills/php-coder/SKILL.md +1 -1
  327. package/dist/agent-src/skills/pipeline-strategy/SKILL.md +2 -2
  328. package/dist/agent-src/skills/playwright-testing/SKILL.md +3 -3
  329. package/dist/agent-src/skills/po-discovery/SKILL.md +1 -1
  330. package/dist/agent-src/skills/positioning-strategy/SKILL.md +2 -2
  331. package/dist/agent-src/skills/privacy-review/SKILL.md +1 -1
  332. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +3 -3
  333. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  334. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -2
  335. package/dist/agent-src/skills/receiving-code-review/SKILL.md +1 -1
  336. package/dist/agent-src/skills/refine-ticket/SKILL.md +2 -2
  337. package/dist/agent-src/skills/release-comms/SKILL.md +2 -2
  338. package/dist/agent-src/skills/retention-loops/SKILL.md +2 -2
  339. package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
  340. package/dist/agent-src/skills/roadmap-management/SKILL.md +1 -1
  341. package/dist/agent-src/skills/roadmap-management/references/archival.md +1 -1
  342. package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
  343. package/dist/agent-src/skills/rule-writing/SKILL.md +6 -6
  344. package/dist/agent-src/skills/runway-cognition/SKILL.md +2 -2
  345. package/dist/agent-src/skills/scenario-modeling/SKILL.md +2 -2
  346. package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
  347. package/dist/agent-src/skills/skill-writing/references/procedure.md +1 -1
  348. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +3 -3
  349. package/dist/agent-src/skills/spreadsheet-authoring/SKILL.md +1 -1
  350. package/dist/agent-src/skills/stakeholder-tradeoff/SKILL.md +3 -3
  351. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +1 -1
  352. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +1 -1
  353. package/dist/agent-src/skills/systematic-debugging/SKILL.md +2 -2
  354. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +11 -5
  355. package/dist/agent-src/skills/throughput-vs-morale-tradeoff/SKILL.md +1 -1
  356. package/dist/agent-src/skills/token-optimizer/SKILL.md +1 -1
  357. package/dist/agent-src/skills/typography-system/SKILL.md +2 -2
  358. package/dist/agent-src/skills/ui-component-architect/SKILL.md +28 -8
  359. package/dist/agent-src/skills/unit-economics-modeling/SKILL.md +2 -2
  360. package/dist/agent-src/skills/upstream-contribute/SKILL.md +1 -1
  361. package/dist/agent-src/skills/validate-feature-fit/SKILL.md +2 -2
  362. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +2 -2
  363. package/dist/agent-src/skills/vision-articulation/SKILL.md +1 -1
  364. package/dist/agent-src/skills/voc-extract/SKILL.md +2 -2
  365. package/dist/agent-src/skills/voice-and-tone-design/SKILL.md +2 -2
  366. package/dist/agent-src/skills/wireframe/SKILL.md +1 -1
  367. package/dist/agent-src/skills/workspace-link/SKILL.md +1 -1
  368. package/dist/agent-src/templates/agent-settings.md +6 -6
  369. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  370. package/dist/agent-src/templates/contexts/knowledge-pages.md +2 -3
  371. package/dist/agent-src/templates/copilot-instructions.md +1 -1
  372. package/dist/agent-src/templates/roadmaps.md +1 -1
  373. package/dist/agent-src/templates/scripts/README.md +2 -2
  374. package/dist/agent-src/user-types/_template/user-type.md +1 -1
  375. package/dist/cli-delegate/check_update_banner.js +12 -1
  376. package/dist/cli-delegate/{chunk-NLP54U3J.js → chunk-2XY7ILBT.js} +13 -1
  377. package/dist/cli-delegate/{chunk-IAI3XL43.js → chunk-3FKR64R5.js} +22 -6
  378. package/dist/cli-delegate/{chunk-J3RFNKG4.js → chunk-55ACLDUE.js} +2 -2
  379. package/dist/cli-delegate/{chunk-PKOBQADP.js → chunk-57PN4GYJ.js} +1 -1
  380. package/dist/cli-delegate/{chunk-KCNTUSPI.js → chunk-AK6HODNQ.js} +4 -4
  381. package/dist/cli-delegate/{chunk-2FTMENJL.js → chunk-GYC3VRYN.js} +1 -1
  382. package/dist/cli-delegate/{chunk-LWTYTX6F.js → chunk-HZ52BRGG.js} +24 -18
  383. package/dist/cli-delegate/{chunk-6RJA2F5A.js → chunk-IVBPRCQO.js} +1 -1
  384. package/dist/cli-delegate/{chunk-I6RHFQEG.js → chunk-YUM5KURG.js} +17 -1
  385. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  386. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  387. package/dist/cli-delegate/cmd_conformance.js +3 -3
  388. package/dist/cli-delegate/cmd_doctor.js +3 -3
  389. package/dist/cli-delegate/cmd_explain.js +2 -2
  390. package/dist/cli-delegate/cmd_export.js +4 -4
  391. package/dist/cli-delegate/cmd_handoff.js +4 -4
  392. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  393. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  394. package/dist/cli-delegate/cmd_migrate.js +2 -2
  395. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  396. package/dist/cli-delegate/cmd_prune.js +4 -4
  397. package/dist/cli-delegate/cmd_route_audit.js +3 -3
  398. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  399. package/dist/cli-delegate/cmd_session_recycle.js +1 -1
  400. package/dist/cli-delegate/cmd_settings_get.js +15 -9
  401. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  402. package/dist/cli-delegate/cmd_settings_set.js +41 -11
  403. package/dist/cli-delegate/cmd_sync.js +4 -4
  404. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  405. package/dist/cli-delegate/cmd_update.js +1 -1
  406. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  407. package/dist/cli-delegate/cmd_validate.js +4 -4
  408. package/dist/cli-delegate/cmd_versions.js +1 -1
  409. package/dist/discovery/deprecation-report.md +1 -1
  410. package/dist/discovery/discovery-manifest.json +22 -75
  411. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  412. package/dist/discovery/discovery-manifest.summary.md +3 -3
  413. package/dist/discovery/dormancy-report.md +1 -1
  414. package/dist/discovery/orphan-report.md +1 -1
  415. package/dist/discovery/packs.json +5 -7
  416. package/dist/discovery/trust-report.md +2 -2
  417. package/dist/discovery/workspaces.json +4 -6
  418. package/dist/hooks/dispatch.js +8607 -8930
  419. package/dist/install/install.mjs +35 -8
  420. package/dist/mcp/registry-manifest.json +3 -3
  421. package/dist/mcp/server.json +2 -2
  422. package/dist/mcp/server.mjs +1433 -1342
  423. package/dist/router.json +1 -1
  424. package/dist/scripts/_lib/agent_settings.js +73 -0
  425. package/dist/scripts/_lib/agent_src.js +20 -0
  426. package/dist/scripts/_lib/guidelines_lane.js +158 -0
  427. package/dist/scripts/condense.js +2 -5
  428. package/dist/server/io/yamlIO.js +125 -17
  429. package/dist/server/schemas/settings.js +18 -4
  430. package/dist/shared/settingsSurface.js +6 -1
  431. package/docs/contracts/settings-classes.md +18 -9
  432. package/docs/guidelines/abstraction-thresholds.md +35 -0
  433. package/docs/guidelines/agent-infra/active-remediation-mechanics.md +3 -3
  434. package/docs/guidelines/agent-infra/false-green.md +2 -2
  435. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
  436. package/docs/guidelines/agent-infra/skill-quality-checklist.md +1 -1
  437. package/docs/guidelines/design-fidelity-mechanics.md +4 -0
  438. package/docs/guidelines/design-fidelity-routing.md +78 -0
  439. package/docs/guidelines/design-reconciliation.md +172 -0
  440. package/docs/guidelines/gtm-handoff.md +2 -2
  441. package/docs/guidelines/wing4-handoff.md +2 -2
  442. package/package.json +7 -3
  443. package/src/config/agent-settings.template.yml +55 -8
  444. package/src/config/ci-local-parity.yml +34 -0
  445. package/src/config/continuity-surface.json +4 -32
  446. package/src/config/gate-coverage.yml +31 -8
  447. package/src/config/gate-reachability-exemptions.json +1 -0
  448. package/src/config/gate-violation-baselines.json +3 -3
  449. package/src/config/hook-token-budget.json +1 -1
  450. package/src/config/host-injection-effect.json +2 -2
  451. package/src/config/pack-size-budget.json +55 -4
  452. package/src/config/platform-anchor.json +47 -0
  453. package/src/config/preamble-payload-budget.json +2 -3
  454. package/src/config/publish-surface.json +4 -2
  455. package/src/config/ratification-policy.json +7 -0
  456. package/src/config/rule-stub-ceilings.json +16 -9
  457. package/src/scripts/_cli/cmd_settings_get.ts +22 -7
  458. package/src/scripts/_lib/agent_settings.ts +85 -0
  459. package/src/scripts/_lib/agent_src.ts +21 -0
  460. package/src/scripts/_lib/artifact_maturity.ts +242 -0
  461. package/src/scripts/_lib/conformance_report.ts +403 -0
  462. package/src/scripts/_lib/continuity_writer.ts +72 -13
  463. package/src/scripts/_lib/design_system_import.ts +20 -0
  464. package/src/scripts/_lib/design_tolerance.ts +281 -0
  465. package/src/scripts/_lib/guidelines_lane.ts +164 -0
  466. package/src/scripts/_lib/obligation_frequency.ts +2 -3
  467. package/src/scripts/_lib/platform_anchor.ts +1053 -0
  468. package/src/scripts/_lib/ratification_artifact.ts +278 -0
  469. package/src/scripts/_lib/security_lint.ts +23 -2
  470. package/src/scripts/_lib/self_repair.ts +21 -0
  471. package/src/scripts/_lib/session_index_trust.ts +42 -1
  472. package/src/scripts/_lib/standing_bound_ratchet.ts +103 -37
  473. package/src/scripts/_lib/tolerance_shadow.ts +177 -0
  474. package/src/scripts/ai_council/redact_low_impact_entry.ts +8 -0
  475. package/src/scripts/autonomy_friction_traffic.ts +379 -0
  476. package/src/scripts/check_kernel_edit_ratified.ts +462 -0
  477. package/src/scripts/check_loss_class_declared.ts +793 -46
  478. package/src/scripts/check_platform_anchor.ts +390 -0
  479. package/src/scripts/check_preamble_payload_budget.ts +292 -4
  480. package/src/scripts/check_projected_rule_routes.ts +395 -0
  481. package/src/scripts/code_graph/cli.ts +6 -0
  482. package/src/scripts/condense.ts +2 -6
  483. package/src/scripts/generate_host_cost_table.ts +354 -0
  484. package/src/scripts/hook_manifest.json +1 -1
  485. package/src/scripts/hook_manifest.yaml +35 -27
  486. package/src/scripts/hooks/category_a.ts +1 -1
  487. package/src/scripts/hooks/chain_nudge_hook.ts +311 -25
  488. package/src/scripts/hooks/session_eol_hook.ts +68 -154
  489. package/src/scripts/hooks/source_first_gate_hook.ts +12 -0
  490. package/src/scripts/hooks/ui_route_nudge_hook.ts +52 -15
  491. package/src/scripts/hot_context_hook.ts +41 -401
  492. package/src/scripts/lint_evidence_artifacts.ts +1 -0
  493. package/src/scripts/lint_knowledge_scale.ts +0 -26
  494. package/src/scripts/memory_lookup.ts +9 -9
  495. package/src/scripts/report_standing_payload_by_host.ts +7 -7
  496. package/src/scripts/session_memory_index.ts +5 -0
  497. package/src/scripts/sync_agent_settings.ts +270 -10
  498. package/src/server/io/yamlIO.ts +127 -17
  499. package/src/server/schemas/settings.ts +25 -5
  500. package/src/shared/settingsSurface.ts +13 -2
  501. package/src/templates/compact-instructions.md +8 -3
  502. package/dist/agent-src/commands/chat-history/import.md +0 -170
  503. package/dist/agent-src/commands/chat-history.md +0 -63
@@ -19,17 +19,93 @@
19
19
  *
20
20
  * WHAT COUNTS AS MODEL-FACING
21
21
  * ---------------------------
22
- * A hook concern bound on a slot whose output can reach the model, whose script
23
- * BOTH emits a context payload AND applies a lossy operation to content. Both
24
- * halves are read from code with comments stripped — the first cut of this
25
- * detector matched `truncat` inside three docblocks that describe truncation
26
- * without performing any, and a gate that fires on prose about a defect rather
27
- * than the defect is how a real corpus of one becomes a pro-forma corpus of four.
22
+ * An EMITTER is a hook concern script whose code emits a context payload. A
23
+ * TARGET is any module that emitter reaches through relative imports —
24
+ * transitively, the emitter itself included — whose code applies a lossy
25
+ * operation to content. The emitter half and the lossy half are read from
26
+ * different files on purpose: a `_lib` module does not emit, the concern does,
27
+ * so requiring both halves of one file is what kept the corpus at one and then
28
+ * at zero.
28
29
  *
29
- * Measured at landing: **1** module qualifies — `hot_context_hook`, which is
30
- * exactly the `ephemeral-lossy` exemplar the contract classifies. A corpus of one
31
- * is the honest state of this tree, not a broken detector; the gate fires the day
32
- * a second lands.
30
+ * Both halves are read from code with comments stripped — ALL comments, a
31
+ * trailing `// truncate the rows` included, since one scan classifies every
32
+ * character rather than two regexes guessing at each other's boundaries (see
33
+ * `scanSource`). The first cut of this detector matched `truncat` inside three
34
+ * docblocks that describe truncation without performing any. The LOSSY half
35
+ * additionally blanks string, template and regex literals, and widening the
36
+ * walk is what made that necessary — measured over the reached set, the raw
37
+ * patterns matched a SQLite `PRAGMA wal_checkpoint(TRUNCATE)`, the settings key
38
+ * `'knowledge.global_sharing.redaction.enabled'`, and a destructive-command
39
+ * detector's own `/truncate\s+table/` regex. None of the three shortens
40
+ * anything. A gate that fires on data ABOUT an operation rather than on the
41
+ * operation is how a real corpus becomes a pro-forma one, each member carrying
42
+ * a declaration nobody meant.
43
+ *
44
+ * The EMIT half deliberately keeps reading literals: a concern that writes its
45
+ * payload key as `'additionalContext'` is emitting, and blanking that string
46
+ * would drop a real emitter — measured, it dropped six of seven.
47
+ *
48
+ * WHAT IT MEASURES, AND HOW THAT NUMBER MOVED
49
+ * -------------------------------------------
50
+ * MATCHED IS NOT LOSSY, and the headline used to say otherwise. The detector
51
+ * establishes a MATCH; the module's own DECLARATION says whether anything is
52
+ * lost, and `exact` says nothing is. Of the five modules below, four declare a
53
+ * lossy class and one declares `exact`, so "5 lossy modules" overstated the
54
+ * real lossy corpus by one. Every count here now reads "matched".
55
+ *
56
+ * Measured at landing (emitter-only): **1** module — `hot_context_hook`, the
57
+ * `ephemeral-lossy` exemplar the contract classifies. Measured 2026-09-09 after
58
+ * road-to-continuity-writer-activation step 3.1 retired that module's cache
59
+ * half: **0**. The scanned corpus was unchanged at 58 hook scripts; the matched
60
+ * subset was empty, so the gate passed over nothing — a hole, not a clean bill,
61
+ * tracked as blocker `loss-class-corpus-is-empty-after-hot-context`.
62
+ *
63
+ * Measured 2026-09-10 with the walk widened past concern scripts: **58 hook
64
+ * scripts scanned, 7 of them emitters, 73 modules reached, 5 of those
65
+ * matched** — four lossy plus one `exact`:
66
+ * - `_lib/session_index_trust.ts` — the surviving 30-row cap the blocker named
67
+ * - `session_memory_index.ts` — which applies it
68
+ * - `memory_lookup.ts` — the token-budget cut
69
+ * - `_lib/self_repair.ts` — evidence sanitization (but see below)
70
+ * - `ai_council/redact_low_impact_entry.ts` — `exact`: it refuses rather than
71
+ * rewrites, so no byte of its input is dropped or replaced
72
+ * The retired exemplar is not among them and does not come back.
73
+ *
74
+ * `exact` IS A SILENCER, AND IT IS UNVERIFIABLE HERE. `parseLossDeclaration`
75
+ * accepts it and `isProblem` is false, so declaring `exact` turns any future
76
+ * over-match green without the detector learning anything. Nothing in this gate
77
+ * checks that an `exact` module is byte-preserving — that is a reviewer's
78
+ * judgement, and this one was reviewed: the redactor returns a verdict, never a
79
+ * rewritten string. Treat a NEW `exact` declaration as a detector defect to fix
80
+ * rather than a declaration to accept.
81
+ *
82
+ * ONE CORPUS MEMBER IS MATCHED FOR THE WRONG REASON. `_lib/self_repair.ts` is
83
+ * declared `ephemeral-lossy` for `sanitizeEvidence`, which hard-cuts at
84
+ * `MAX_EVIDENCE` — and the cap pattern CANNOT SEE that constant: `MAX_EVIDENCE`
85
+ * fails `(?:WORD|CHAR|ROW|MAX)_(?:CAP|CHARS|LEN|WORDS)` because the unit word
86
+ * must be followed by a size noun. What actually matches is `\bredact`, against
87
+ * the `redact_low_impact_entry` identifier the module imports for its EGRESS
88
+ * gate. Delete or rename that import and a module that still cuts content
89
+ * leaves the corpus silently.
90
+ *
91
+ * Widening the pattern to any `MAX_<WORD>` was MEASURED before it was refused,
92
+ * 2026-09-10 over the same 73-module reach: it takes the corpus from 5 to
93
+ * **12**, admitting `MAX_RETRIES` (a retry budget), `DEFAULT_MAX_TOKENS` (an
94
+ * API parameter), `MAX_PATHS`, `MAX_ENTRIES` and three `*_MAX_BYTES` retention
95
+ * settings — seven constants, not one of them evidence that content is cut.
96
+ * Seven pro-forma declarations to catch one real cap is the trade this gate's
97
+ * own docstring argues against two paragraphs up, so the gap is disclosed
98
+ * rather than closed. The honest position: this member's declaration is right
99
+ * and the detector's reason for it is not.
100
+ *
101
+ * One pattern moved with the walk, and only one. `\b(?:WORD|CHAR|MAX)_…`
102
+ * required the cap constant to BEGIN with a unit word, so it saw `WORD_CAP` and
103
+ * could not see `SESSION_INDEX_ROW_CAP` — the exact constant the blocker names.
104
+ * The prefix is now optional and `ROW` joined the unit list; everything the old
105
+ * form matched, the new form still matches. Deliberately NOT generalized to any
106
+ * `_CAP` at all: that also matches `security_lint.PRAGMA_CAP`, a lint budget
107
+ * that shortens no content, which is the pro-forma member this paragraph exists
108
+ * to keep out.
33
109
  *
34
110
  * Exit codes: 0 clean (warnings allowed) · 1 an undeclared model-facing
35
111
  * transform · 2 misuse / unreadable manifest.
@@ -64,27 +140,265 @@ const MODEL_FACING_SLOTS = [
64
140
  /** Emitting a context payload — the "reaches the model" half. */
65
141
  const EMIT_PATTERNS = [/\bcontext\s*:/, /additionalContext/, /hookSpecificOutput/];
66
142
 
67
- /** Shortening, redacting or capping content — the "lossy" half. */
68
- const LOSSY_PATTERNS = [/\bredact/i, /\btruncat/i, /\b(?:WORD|CHAR|MAX)_(?:CAP|CHARS|LEN|WORDS)\b/];
143
+ /**
144
+ * Shortening, redacting or capping content — the "lossy" half.
145
+ *
146
+ * The third pattern names a CAP CONSTANT. Its unit vocabulary is the load-bearing
147
+ * part, not the leading position: an optional qualifying prefix lets it see
148
+ * `SESSION_INDEX_ROW_CAP` while `PRAGMA_CAP` — a count of lint pragmas, not of
149
+ * content — stays out.
150
+ */
151
+ const LOSSY_PATTERNS = [
152
+ /\bredact/i,
153
+ /\btruncat/i,
154
+ /\b(?:[A-Z0-9]+_)*(?:WORD|CHAR|ROW|MAX)_(?:CAP|CHARS|LEN|WORDS)\b/,
155
+ ];
156
+
157
+ /**
158
+ * Relative module specifiers, in every form this tree actually writes them.
159
+ *
160
+ * The extension is no longer part of the pattern — `resolveSpecifier` decides
161
+ * what a specifier resolves to, and it accepts only code files, so an
162
+ * extensionless import is a reach and `'./notes.md'` still is not.
163
+ */
164
+ // Anchored on an import or call position rather than matching any quoted
165
+ // `./`-shaped string anywhere. Unanchored, a path in a prose message could pull
166
+ // an unimported module into the gated population — over-match, so it costs a
167
+ // declaration rather than a silent green, but a corpus member nobody imports is
168
+ // the pro-forma shape this gate refuses. Measured: the corpus is the same 5
169
+ // either way, so the narrowing loses no real edge, `from`/`(`/`,` still covers
170
+ // static imports and every `createRequire` alias this tree writes.
171
+ const IMPORT_SPEC = /(?:from\s*|[(,]\s*)['"](\.\.?\/[^'"\n]*)['"]/g;
69
172
 
70
173
  export interface LossFinding {
71
174
  concern: string;
175
+ /** The concern's own script — the emitter the module was reached from. */
72
176
  script: string;
177
+ /** The reached module that applies the lossy operation. */
178
+ module: string;
73
179
  tier: 'fail' | 'warn';
74
180
  problem: DeclarationProblem;
75
181
  }
76
182
 
77
183
  export interface LossVerdict {
184
+ /** Concern scripts read from the manifest. */
78
185
  scanned: number;
186
+ /** Concern scripts that emit a context payload — the walk's entry points. */
187
+ emitters: number;
188
+ /** Distinct modules the detector MATCHED on a model-facing path. */
79
189
  modelFacing: number;
190
+ /**
191
+ * Of those, how many declare `exact` — matched, and losing nothing.
192
+ *
193
+ * Reported separately because the headline used to call every matched
194
+ * module lossy, and one of them is a redactor that refuses rather than
195
+ * rewrites. "Matched" is what the detector establishes; "lossy" is what
196
+ * the declaration says, and on `exact` it says the opposite.
197
+ */
198
+ declaredExact: number;
199
+ /**
200
+ * Those modules by repo-relative path, sorted.
201
+ *
202
+ * Exposed so a test can assert WHICH module the detector matched, not only
203
+ * how many — the blocker this walk closed was satisfiable by a count.
204
+ */
205
+ modules: string[];
80
206
  findings: LossFinding[];
81
207
  }
82
208
 
83
- /** Strip block and line comments so the detector reads CODE, never prose about code. */
209
+ /** The two views of a source file the detector reads. */
210
+ export interface SourceViews {
211
+ /** Comments removed, literals blanked — code, and nothing about code. */
212
+ code: string;
213
+ /** Comments removed, literals kept — code plus the data it writes. */
214
+ withLiterals: string;
215
+ }
216
+
217
+ /** Characters after which a `/` opens a regex rather than dividing. */
218
+ const REGEX_PREFIX = new Set(['(', ',', '=', ':', '[', '!', '&', '|', '?', '{', ';']);
219
+ /** Keywords after which the same is true. */
220
+ const REGEX_KEYWORDS = new Set(['return', 'typeof', 'case', 'in', 'of', 'new', 'delete', 'void', 'yield', 'await', 'throw']);
221
+
222
+ /** The standard division-vs-regex heuristic, read off what the scan has emitted. */
223
+ function regexAllowed(code: string): boolean {
224
+ let j = code.length - 1;
225
+ while (j >= 0 && /\s/.test(code[j] as string)) j -= 1;
226
+ if (j < 0) return true;
227
+ const ch = code[j] as string;
228
+ if (REGEX_PREFIX.has(ch)) return true;
229
+ if (!/[A-Za-z0-9_$]/.test(ch)) return false;
230
+ let k = j;
231
+ while (k >= 0 && /[A-Za-z0-9_$]/.test(code[k] as string)) k -= 1;
232
+ return REGEX_KEYWORDS.has(code.slice(k + 1, j + 1));
233
+ }
234
+
235
+ /**
236
+ * ONE left-to-right scan that classifies every character as code, literal or
237
+ * comment, and returns both views from that single pass.
238
+ *
239
+ * Independent regexes for comments and for literals cannot see each other, and
240
+ * a completion review found the resulting hole from both sides:
241
+ *
242
+ * - a trailing `// truncate the rows` survived comment stripping (which
243
+ * removed WHOLE-LINE `//` only) and read as a real truncation — an
244
+ * over-match that manufactures a declaration nobody meant;
245
+ * - a `/*` inside a string opened a phantom block comment that deleted every
246
+ * byte up to the next real `*` + `/` — an under-match, the silent-green
247
+ * direction.
248
+ *
249
+ * The second finding's ATTRIBUTION was corrected by reproduction before this
250
+ * fix was written: it blamed the composition order in `stripLiterals(
251
+ * stripComments(src))`, and the old comment pass ALONE already ate that input,
252
+ * so the order was never the cause. The phantom-comment half was pre-existing
253
+ * in comment stripping, not introduced by composing the two. Both halves are
254
+ * the same root cause seen from two sides, and both are gone here for the same
255
+ * reason: a scanner that knows which state it is in cannot open a comment
256
+ * inside a literal, cannot open a literal inside a comment, and does not care
257
+ * whether a `//` starts its line.
258
+ *
259
+ * Newlines inside a removed comment are preserved so the two views stay
260
+ * line-aligned with the source.
261
+ */
262
+ export function scanSource(src: string): SourceViews {
263
+ let code = '';
264
+ let withLiterals = '';
265
+ const n = src.length;
266
+ // `code` mode reads code; `template` mode reads template-literal text. A
267
+ // `${…}` pushes `code` back on, so a substitution's expression is code —
268
+ // which is what it is.
269
+ const modes: ('code' | 'template')[] = ['code'];
270
+ /** Brace depth outside each open `${…}`, innermost last. */
271
+ const subst: number[] = [];
272
+ let braces = 0;
273
+ let i = 0;
274
+
275
+ while (i < n) {
276
+ const ch = src[i] as string;
277
+
278
+ if (modes[modes.length - 1] === 'template') {
279
+ if (ch === '\\') {
280
+ withLiterals += src.slice(i, i + 2);
281
+ i += 2;
282
+ continue;
283
+ }
284
+ if (ch === '`') {
285
+ modes.pop();
286
+ code += '`';
287
+ withLiterals += '`';
288
+ i += 1;
289
+ continue;
290
+ }
291
+ if (ch === '$' && src[i + 1] === '{') {
292
+ modes.push('code');
293
+ subst.push(braces);
294
+ braces += 1;
295
+ code += '${';
296
+ withLiterals += '${';
297
+ i += 2;
298
+ continue;
299
+ }
300
+ if (ch === '\n') code += '\n';
301
+ withLiterals += ch;
302
+ i += 1;
303
+ continue;
304
+ }
305
+
306
+ const next = src[i + 1];
307
+ if (ch === '/' && next === '/') {
308
+ let j = i + 2;
309
+ while (j < n && src[j] !== '\n') j += 1;
310
+ i = j; // the newline itself is emitted on the next turn
311
+ continue;
312
+ }
313
+ if (ch === '/' && next === '*') {
314
+ let j = i + 2;
315
+ while (j < n && !(src[j] === '*' && src[j + 1] === '/')) j += 1;
316
+ const end = Math.min(j + 2, n);
317
+ const kept = src.slice(i, end).replace(/[^\n]/g, '');
318
+ code += kept;
319
+ withLiterals += kept;
320
+ i = end;
321
+ continue;
322
+ }
323
+ if (ch === "'" || ch === '"') {
324
+ let j = i + 1;
325
+ while (j < n && src[j] !== ch && src[j] !== '\n') j += src[j] === '\\' ? 2 : 1;
326
+ const end = src[j] === ch ? j + 1 : j;
327
+ code += ch + ch;
328
+ withLiterals += src.slice(i, end);
329
+ i = end;
330
+ continue;
331
+ }
332
+ if (ch === '`') {
333
+ modes.push('template');
334
+ code += '`';
335
+ withLiterals += '`';
336
+ i += 1;
337
+ continue;
338
+ }
339
+ if (ch === '/' && regexAllowed(code)) {
340
+ let j = i + 1;
341
+ let inClass = false;
342
+ let closed = false;
343
+ while (j < n && src[j] !== '\n') {
344
+ const c = src[j] as string;
345
+ if (c === '\\') {
346
+ j += 2;
347
+ continue;
348
+ }
349
+ if (c === '[') inClass = true;
350
+ else if (c === ']') inClass = false;
351
+ else if (c === '/' && !inClass) {
352
+ closed = true;
353
+ break;
354
+ }
355
+ j += 1;
356
+ }
357
+ if (closed) {
358
+ let k = j + 1;
359
+ while (k < n && /[dgimsuvy]/.test(src[k] as string)) k += 1;
360
+ code += '/RE/';
361
+ withLiterals += src.slice(i, k);
362
+ i = k;
363
+ continue;
364
+ }
365
+ }
366
+ if (ch === '{') {
367
+ braces += 1;
368
+ } else if (ch === '}') {
369
+ const open = subst[subst.length - 1];
370
+ if (open !== undefined && braces === open + 1) {
371
+ subst.pop();
372
+ braces = open;
373
+ modes.pop();
374
+ code += '}';
375
+ withLiterals += '}';
376
+ i += 1;
377
+ continue;
378
+ }
379
+ braces -= 1;
380
+ }
381
+ code += ch;
382
+ withLiterals += ch;
383
+ i += 1;
384
+ }
385
+ return { code, withLiterals };
386
+ }
387
+
388
+ /**
389
+ * Comments removed, literals KEPT — the emit half, and the marker-substitution
390
+ * half of the lossy test.
391
+ *
392
+ * Name and callers unchanged; the behaviour is the scan's, so a trailing `//`
393
+ * is now removed like any other comment.
394
+ */
84
395
  export function stripComments(src: string): string {
85
- return src
86
- .replace(/\/\*[\s\S]*?\*\//g, '')
87
- .replace(/^\s*\/\/.*$/gm, '');
396
+ return scanSource(src).withLiterals;
397
+ }
398
+
399
+ /** Comments removed and literals blanked — the pattern half of the lossy test. */
400
+ export function stripNonCode(src: string): string {
401
+ return scanSource(src).code;
88
402
  }
89
403
 
90
404
  export function emitsContext(codeOnly: string): boolean {
@@ -95,6 +409,169 @@ export function isLossy(codeOnly: string): boolean {
95
409
  return LOSSY_PATTERNS.some((p) => p.test(codeOnly));
96
410
  }
97
411
 
412
+ /**
413
+ * A substitution whose ARGUMENTS carry the lossy marker — `s.replace(RE,
414
+ * '[REDACTED]')`. Deliberately a proximity test and not a file-level one: a
415
+ * module that merely mentions `redaction` somewhere AND calls `.slice` somewhere
416
+ * else is not performing a redaction, and admitting it was measured to pull in
417
+ * three modules that lose nothing (settings KEY NAMES containing `redaction`, a
418
+ * SQLite `PRAGMA wal_checkpoint(TRUNCATE)`, and length constants used only to
419
+ * VALIDATE). Three pro-forma declarations is the corpus this gate's own
420
+ * docstring warns against, so the co-presence rule was measured and dropped.
421
+ */
422
+ // `(?:[^()]|\([^()]*\))*?` and not `[^)]*?`: the first form stopped at the
423
+ // FIRST `)`, so `s.replace(buildPattern(), '[REDACTED]')` — marker after a
424
+ // nested call — was invisible, which is a silent-green gap in the one mechanism
425
+ // this pattern exists to close. One level of nesting covers every shape in this
426
+ // tree; deeper nesting is a bounded under-match rather than an unbounded scan
427
+ // that would run into the next statement.
428
+ const MARKER_SUBSTITUTION = /\.(?:replace|replaceAll)\s*\((?:[^()]|\([^()]*\))*?(?:redact|truncat)/i;
429
+
430
+ /**
431
+ * Is this module lossy, judged on BOTH the blanked and the unblanked form?
432
+ *
433
+ * Blanking literals keeps the gate off data ABOUT operations — a SQLite
434
+ * `PRAGMA wal_checkpoint(TRUNCATE)`, a settings key containing `redaction`, a
435
+ * destructive-command detector's own `/truncate\s+table/`. It also deletes a
436
+ * real class of true positives, which a completion review caught and this
437
+ * function is the answer to: a redaction whose only code-level evidence is its
438
+ * replacement marker — `s.replace(RE, '[REDACTED]')` — is lossy, and blanking
439
+ * alone made it invisible.
440
+ *
441
+ * The discriminator is measured, not guessed, and the first version of it was
442
+ * wrong. Asking only whether the module ALSO contains a shortening call
443
+ * anywhere admitted three modules that lose nothing — settings key names
444
+ * containing `redaction`, a SQLite `PRAGMA wal_checkpoint(TRUNCATE)`, and
445
+ * length constants used to validate rather than cut — taking the corpus from 5
446
+ * to 8. Co-presence in a file is not evidence. The marker has to sit in the
447
+ * substitution's own arguments, which is what separates `s.replace(RE,
448
+ * '[REDACTED]')` from `db.exec(q)`, `read(k)` and `RE.test(cmd)`.
449
+ *
450
+ * Residual error, stated rather than implied: a substitution whose replacement
451
+ * merely NAMES redaction without performing one still counts. That direction is
452
+ * the affordable one — it costs a declaration, where the other direction is a
453
+ * module cutting content with nothing watching.
454
+ */
455
+ export function isLossyModule(source: string): boolean {
456
+ return isLossyViews(scanSource(source));
457
+ }
458
+
459
+ /** The same test over an already-scanned file, so the scan is paid once. */
460
+ export function isLossyViews(views: SourceViews): boolean {
461
+ if (isLossy(views.code)) return true;
462
+ return MARKER_SUBSTITUTION.test(views.withLiterals);
463
+ }
464
+
465
+ /** Resolve a relative specifier to a real file, preferring the `.ts` twin of a `.js` import. */
466
+ export function resolveSpecifier(spec: string, fromFile: string, root: string): string | null {
467
+ const base = path.resolve(path.dirname(fromFile), spec);
468
+ // A specifier that already names a code file keeps the original two
469
+ // candidates. One that does not gets the extensionless and directory forms
470
+ // in Node resolution order: the pattern used to REQUIRE an extension, so
471
+ // `require('./_lib/x')` and `from './helpers'` were not reaches at all — a
472
+ // static gap the contract's disclosure list did not carry. A specifier
473
+ // naming some other extension (`./x.md`) falls here too and resolves to
474
+ // nothing, which keeps non-code files out of the walk.
475
+ //
476
+ // Measured 2026-09-10, and the honest number is zero: the reached set is
477
+ // 73 modules with the extension required and 73 without, and the matched
478
+ // corpus is the same 5 either way — this tree writes no extensionless
479
+ // relative specifier on any reached path today. The gap is closed
480
+ // prospectively, not because it was leaking. A self-test case pins that
481
+ // the new candidates resolve at all, so the closure is not untested.
482
+ // `.mts` / `.cts` / `.tsx` / `.jsx` are in the list although this tree
483
+ // writes none of them on a reached path today. Leaving them out is a
484
+ // latent under-match that would surface as a silently smaller corpus the
485
+ // day one appears, which is the failure direction this whole gate exists
486
+ // to refuse; carrying them costs nothing.
487
+ const candidates = /\.(?:js|ts|mjs|cjs|mts|cts|tsx|jsx)$/.test(base)
488
+ ? [base.replace(/\.(?:js|mjs|cjs|jsx)$/, '.ts'), base]
489
+ : [`${base}.ts`, `${base}.js`, path.join(base, 'index.ts'), path.join(base, 'index.js')];
490
+ for (const candidate of candidates) {
491
+ if (!candidate.startsWith(root + path.sep)) continue;
492
+ try {
493
+ if (fs.statSync(candidate).isFile()) return candidate;
494
+ } catch {
495
+ /* not a file — try the next candidate */
496
+ }
497
+ }
498
+ return null;
499
+ }
500
+
501
+ /**
502
+ * Read each file once per run and keep both views plus its declaration.
503
+ *
504
+ * The walk, the lossy test and the declaration parse all want the same file,
505
+ * and `reachedModules` re-walked a heavily shared `_lib` graph once per
506
+ * emitter. With 7 emitters that was O(emitters × graph) reads for a result
507
+ * that is per-file and per-entry and does not change during a run.
508
+ */
509
+ export class SourceCache {
510
+ private readonly files = new Map<string, { raw: string; views: SourceViews } | null>();
511
+ private readonly reach = new Map<string, string[]>();
512
+
513
+ get(file: string): { raw: string; views: SourceViews } | null {
514
+ const hit = this.files.get(file);
515
+ if (hit !== undefined) return hit;
516
+ let entry: { raw: string; views: SourceViews } | null = null;
517
+ try {
518
+ const raw = fs.readFileSync(file, 'utf-8');
519
+ entry = { raw, views: scanSource(raw) };
520
+ } catch {
521
+ entry = null;
522
+ }
523
+ this.files.set(file, entry);
524
+ return entry;
525
+ }
526
+
527
+ /** Memoized per entry point, which is what the per-emitter loop asks for. */
528
+ reachedFrom(entry: string, root: string): string[] {
529
+ const hit = this.reach.get(entry);
530
+ if (hit !== undefined) return hit;
531
+ const out = reachedModules(entry, root, this);
532
+ this.reach.set(entry, out);
533
+ return out;
534
+ }
535
+ }
536
+
537
+ /**
538
+ * Every module an emitter reaches through relative imports, transitively, the
539
+ * emitter itself included.
540
+ *
541
+ * `createRequire` is how the lazy hook paths in this tree load their heavy
542
+ * dependencies, so the walk reads relative SPECIFIERS rather than `import`
543
+ * statements — a `req('./_lib/session_index_trust.js')` is a reach and a static
544
+ * import is a reach, and the detector must not be able to tell them apart.
545
+ *
546
+ * There is no depth cap, and that is a fix rather than an omission. The cap
547
+ * was tested AFTER `seen.add`, and the stack is LIFO, so `depth` was the depth
548
+ * of whichever path happened to push a node first — a module first popped deep
549
+ * was marked reached and its children were never explored, even when a shallow
550
+ * path to it existed and was popped later. That made the reach set depend on
551
+ * import order rather than on the graph. `seen` is what terminates a cycle, and
552
+ * it terminated every cycle already, so removing the cap removes the
553
+ * order-dependence and nothing else.
554
+ */
555
+ export function reachedModules(entry: string, root: string, cache: SourceCache = new SourceCache()): string[] {
556
+ const seen = new Set<string>();
557
+ const stack: string[] = [entry];
558
+ while (stack.length > 0) {
559
+ const file = stack.pop();
560
+ if (file === undefined) break;
561
+ if (seen.has(file)) continue;
562
+ seen.add(file);
563
+ const hit = cache.get(file);
564
+ if (hit === null) continue;
565
+ for (const m of hit.views.withLiterals.matchAll(IMPORT_SPEC)) {
566
+ const spec = m[1];
567
+ if (spec === undefined) continue;
568
+ const target = resolveSpecifier(spec, file, root);
569
+ if (target !== null && !seen.has(target)) stack.push(target);
570
+ }
571
+ }
572
+ return [...seen];
573
+ }
574
+
98
575
  function loadManifest(root: string): { concerns: Record<string, { script?: unknown }>; platforms: Record<string, Record<string, string[]>> } {
99
576
  const file = path.join(root, 'src', 'scripts', 'hook_manifest.yaml');
100
577
  const raw = fs.readFileSync(file, 'utf-8');
@@ -136,8 +613,11 @@ export function evaluate(root: string = REPO_ROOT, ledger?: GateLedger): LossVer
136
613
  }
137
614
 
138
615
  const findings: LossFinding[] = [];
616
+ const lossyModules = new Map<string, boolean>();
617
+ const exactModules = new Set<string>();
618
+ const cache = new SourceCache();
139
619
  let scanned = 0;
140
- let modelFacing = 0;
620
+ let emitters = 0;
141
621
 
142
622
  ledger?.plan(Object.keys(concerns));
143
623
 
@@ -148,34 +628,66 @@ export function evaluate(root: string = REPO_ROOT, ledger?: GateLedger): LossVer
148
628
  continue;
149
629
  }
150
630
  const abs = path.join(root, rel);
151
- if (!fs.existsSync(abs)) {
631
+ const entry = cache.get(abs);
632
+ if (entry === null) {
152
633
  ledger?.skip(name, 'no_applicable_files');
153
634
  continue;
154
635
  }
155
636
  scanned += 1;
156
637
 
157
- const source = fs.readFileSync(abs, 'utf-8');
158
- const code = stripComments(source);
159
- if (!isLossy(code) || !emitsContext(code)) {
160
- // Not a lossy model-facing transform: the check does not apply to it.
638
+ // The emitter half is a property of the CONCERN script. A `_lib` module
639
+ // never emits; requiring it to is what emptied this corpus.
640
+ if (!emitsContext(entry.views.withLiterals)) {
161
641
  ledger?.outOfScope(name, 'not_applicable_kind');
162
642
  continue;
163
643
  }
644
+ emitters += 1;
164
645
 
165
646
  // Unknown reachability fails closed: a bound concern this gate cannot
166
647
  // place on a known slot is treated as model-reaching.
167
648
  const reaches = facing.has(name) || !bound.has(name);
168
- if (reaches) modelFacing += 1;
169
649
 
170
- const decl = parseLossDeclaration(source);
171
- if (!isProblem(decl)) {
172
- ledger?.complete(name);
650
+ let anyLossy = false;
651
+ let anyProblem = false;
652
+ for (const file of cache.reachedFrom(abs, root)) {
653
+ const hit = cache.get(file);
654
+ if (hit === null) continue;
655
+ if (!isLossyViews(hit.views)) continue;
656
+ anyLossy = true;
657
+ const moduleRel = path.relative(root, file);
658
+ if (reaches) lossyModules.set(moduleRel, true);
659
+
660
+ // The declaration lives in a docblock, so it is read from the RAW
661
+ // source — the one place the comment-stripped views are wrong.
662
+ const decl = parseLossDeclaration(hit.raw);
663
+ if (!isProblem(decl)) {
664
+ if (reaches && decl.lossClass === 'exact') exactModules.add(moduleRel);
665
+ continue;
666
+ }
667
+ anyProblem = true;
668
+ findings.push({ concern: name, script: rel, module: moduleRel, tier: reaches ? 'fail' : 'warn', problem: decl });
669
+ }
670
+
671
+ if (!anyLossy) {
672
+ // Nothing this emitter reaches shortens content: the check does not
673
+ // apply to it.
674
+ ledger?.outOfScope(name, 'not_applicable_kind');
675
+ continue;
676
+ }
677
+ if (anyProblem) {
678
+ ledger?.fail(name, 'reaches a lossy module with no usable loss_class');
173
679
  continue;
174
680
  }
175
- findings.push({ concern: name, script: rel, tier: reaches ? 'fail' : 'warn', problem: decl });
176
- ledger?.fail(name, `undeclared or malformed loss_class (${decl.kind})`);
681
+ ledger?.complete(name);
177
682
  }
178
- return { scanned, modelFacing, findings };
683
+ return {
684
+ scanned,
685
+ emitters,
686
+ modelFacing: lossyModules.size,
687
+ declaredExact: exactModules.size,
688
+ modules: [...lossyModules.keys()].sort(),
689
+ findings,
690
+ };
179
691
  }
180
692
 
181
693
  // ---------------------------------------------------------------- self-test
@@ -188,12 +700,154 @@ export function run() {
188
700
  }
189
701
  `;
190
702
 
191
- function plant(dir: string, header: string, body: string, slot = 'session_start'): void {
192
- fs.mkdirSync(path.join(dir, 'src', 'scripts'), { recursive: true });
193
- fs.writeFileSync(path.join(dir, 'src', 'scripts', 'fixture_hook.ts'), header + body);
703
+ /**
704
+ * The specifier the self-test fixtures import, assembled rather than written.
705
+ *
706
+ * `prepack-check` scans every SHIPPED source for `from '<relative>'` and fails
707
+ * on one that does not resolve. These fixtures are template literals describing
708
+ * a file the self-test writes to a temp directory, so the path resolves there
709
+ * and nowhere here — and interpolating it keeps the literal form out of this
710
+ * file while leaving it intact in the fixture that gets written. The same shape
711
+ * this gate's own IMPORT_SPEC was narrowed for: a path in a string is not an
712
+ * import, and two different tools read it as one.
713
+ */
714
+ const FIXTURE_LIB = './_lib/fixture_lib.js';
715
+ const FIXTURE_LIB_BARE = './_lib/fixture_lib';
716
+
717
+ /** An emitter that is clean itself and reaches a lossy `_lib` module. */
718
+ const IMPORTING_EMITTER = `
719
+ import { capRows } from '${FIXTURE_LIB}';
720
+ export function run() {
721
+ return { context: capRows(rows).join('\\n') };
722
+ }
723
+ `;
724
+
725
+ /** The same emitter, importing a module that shortens nothing. */
726
+ const IMPORTING_CLEAN_EMITTER = `
727
+ import { passthrough } from '${FIXTURE_LIB}';
728
+ export function run() {
729
+ return { context: passthrough(rows).join('\\n') };
730
+ }
731
+ `;
732
+
733
+ const LOSSY_LIB = `
734
+ export const SESSION_INDEX_ROW_CAP = 30;
735
+ export function capRows(rows) { return rows.slice(0, SESSION_INDEX_ROW_CAP); }
736
+ `;
737
+
738
+ const CLEAN_LIB = `
739
+ export function passthrough(rows) { return rows; }
740
+ `;
741
+
742
+ /** The same emitter again, reaching its lib through an EXTENSIONLESS specifier. */
743
+ const EXTENSIONLESS_EMITTER = `
744
+ import { capRows } from '${FIXTURE_LIB_BARE}';
745
+ export function run() {
746
+ return { context: capRows(rows).join('\\n') };
747
+ }
748
+ `;
749
+
750
+ /**
751
+ * The cap-widening pair. These two differ in ONE token — the unit word — so
752
+ * either one failing localises the pattern change. The reject half used to be
753
+ * a byte-identical copy of the "reached module" case above and could not fail
754
+ * independently of it.
755
+ */
756
+ const PREFIXED_UNIT_CAP_LIB = `
757
+ export const SESSION_INDEX_ROW_CAP = 30;
758
+ export function rows(r) { return r.slice(0, SESSION_INDEX_ROW_CAP); }
759
+ `;
760
+ const PREFIXED_NON_UNIT_CAP_LIB = `
761
+ export const SESSION_INDEX_PRAGMA_CAP = 30;
762
+ export function rows(r) { return r; }
763
+ `;
764
+ const ROWS_EMITTER = IMPORTING_EMITTER.replaceAll('capRows', 'rows');
765
+
766
+ // ---- the four shapes that must stay OUT, one fixture each -----------------
767
+ //
768
+ // One per shape on purpose. They used to be two shapes in one fixture, so a
769
+ // regression in either was reported as one failure and neither was localised.
770
+
771
+ /** A SQLite pragma that names TRUNCATE and shortens nothing. */
772
+ const PRAGMA_LITERAL_EMITTER = `
773
+ const stmt = 'PRAGMA wal_checkpoint(TRUNCATE)';
774
+ export function run() { return { context: stmt }; }
775
+ `;
776
+
777
+ /** A destructive-command detector's OWN pattern — data about truncation. */
778
+ const DETECTOR_REGEX_EMITTER = `
779
+ const destructive = /\\btruncate\\s+table\\b/i;
780
+ export function run() { return { context: String(destructive) }; }
781
+ `;
782
+
783
+ /** A settings KEY NAME containing `redaction`. Reading a flag is not redacting. */
784
+ const SETTINGS_KEY_EMITTER = `
785
+ const KEY = 'knowledge.global_sharing.redaction.enabled';
786
+ export function run() { return { context: read(KEY) }; }
787
+ `;
788
+
789
+ /** A TRAILING comment — the half whole-line comment stripping could not see. */
790
+ const TRAILING_COMMENT_EMITTER = `
791
+ export function run() { return { context: rows.join('\\n') }; } // truncate the rows here
792
+ `;
793
+
794
+ // ---- the three shapes that must stay IN ------------------------------------
795
+
796
+ /** A redaction whose ONLY code-level evidence is its replacement literal. */
797
+ const MARKER_REPLACEMENT_LIB = `
798
+ export function scrub(s) { return s.replace(SECRET_RE, '[REDACTED]'); }
799
+ `;
800
+ // The same redaction with the marker AFTER a nested call. `[^)]*?` stopped at
801
+ // the first `)` and made this shape invisible — a silent-green gap in the one
802
+ // mechanism that pattern exists to close, and unpinned until this case.
803
+ const NESTED_MARKER_LIB = `
804
+ export function scrub(s) { return s.replace(buildPattern(kind), '[REDACTED]'); }
805
+ `;
806
+ const SCRUB_EMITTER = `
807
+ import { scrub } from '${FIXTURE_LIB}';
808
+ export function run() { return { context: scrub(body) }; }
809
+ `;
810
+
811
+ /** A contraction inside a template literal, with real truncating code after it. */
812
+ const TEMPLATE_APOSTROPHE_EMITTER = `
813
+ export function run(body) {
814
+ const msg = \`it's fine\`; const kept = body.truncate('x');
815
+ return { context: msg + kept };
816
+ }
817
+ `;
818
+
819
+ /**
820
+ * An UNBALANCED `/*` inside a string literal, followed by a real comment.
821
+ *
822
+ * The unbalanced form is the one that bites: whole-line comment stripping ran
823
+ * from the phantom opener to the next real closer and deleted the truncation
824
+ * between them, so the module read clean. A balanced `'src/` + `**` + `/*.ts'`
825
+ * glob only mangles the string and leaves the code, which is why this fixture
826
+ * does not use one.
827
+ */
828
+ const PHANTOM_COMMENT_EMITTER = `
829
+ const open = '/*';
830
+ export function run(body) { return { context: open + truncateBody(body) }; }
831
+ /* a real comment, after the phantom one */
832
+ `;
833
+
834
+ interface Plant {
835
+ header?: string;
836
+ body: string;
837
+ libHeader?: string;
838
+ lib?: string;
839
+ slot?: string;
840
+ }
841
+
842
+ function plant(dir: string, p: Plant): void {
843
+ fs.mkdirSync(path.join(dir, 'src', 'scripts', '_lib'), { recursive: true });
844
+ fs.writeFileSync(path.join(dir, 'src', 'scripts', 'fixture_hook.ts'), (p.header ?? '') + p.body);
845
+ if (p.lib !== undefined) {
846
+ fs.writeFileSync(path.join(dir, 'src', 'scripts', '_lib', 'fixture_lib.ts'), (p.libHeader ?? '') + p.lib);
847
+ }
194
848
  fs.writeFileSync(
195
849
  path.join(dir, 'src', 'scripts', 'hook_manifest.yaml'),
196
- `schema_version: 1\nconcerns:\n fixture:\n script: src/scripts/fixture_hook.ts\nplatforms:\n claude:\n ${slot}:\n - fixture\n`,
850
+ `schema_version: 1\nconcerns:\n fixture:\n script: src/scripts/fixture_hook.ts\nplatforms:\n claude:\n ${p.slot ?? 'session_start'}:\n - fixture\n`,
197
851
  );
198
852
  }
199
853
 
@@ -204,13 +858,13 @@ const DECL_EPHEMERAL = '/**\n * loss_class: ephemeral-lossy\n */\n';
204
858
  const DECL_TYPO = '/**\n * loss_class: recoverable_lossy\n */\n';
205
859
 
206
860
  function selfTestCases(): SelfTestCase[] {
207
- const mk = (name: string, expect: 'reject' | 'accept', header: string, body: string, slot?: string): SelfTestCase => ({
861
+ const mk = (name: string, expect: 'reject' | 'accept', p: Plant): SelfTestCase => ({
208
862
  name,
209
863
  expect,
210
864
  run: () => {
211
865
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'lcd-'));
212
866
  try {
213
- plant(dir, header, body, slot);
867
+ plant(dir, p);
214
868
  return runGateCli(REPO_ROOT, 'src/scripts/check_loss_class_declared.ts', ['--root', dir, '--quiet'], REPO_ROOT);
215
869
  } finally {
216
870
  fs.rmSync(dir, { recursive: true, force: true });
@@ -218,14 +872,72 @@ function selfTestCases(): SelfTestCase[] {
218
872
  },
219
873
  });
220
874
  return [
221
- mk('model-facing lossy transform with no declaration → reject', 'reject', NO_DECL, LOSSY_EMITTER),
222
- mk('same transform with recoverable-lossy + locator → accept', 'accept', DECL_RECOVERABLE, LOSSY_EMITTER),
223
- mk('recoverable-lossy WITHOUT a locator → reject', 'reject', DECL_RECOVERABLE_NO_LOCATOR, LOSSY_EMITTER),
224
- mk('ephemeral-lossy (owes no locator) → accept', 'accept', DECL_EPHEMERAL, LOSSY_EMITTER),
225
- mk('a misspelled class is not honoured → reject', 'reject', DECL_TYPO, LOSSY_EMITTER),
226
- mk('non-lossy emitter needs no declaration → accept', 'accept', NO_DECL, `export function run(){return {context:'hi'};}`),
227
- mk('lossy but emits no context → accept', 'accept', NO_DECL, `const WORD_CAP=5;export function run(){return body.slice(0,WORD_CAP);}`),
228
- mk('prose about truncation is not truncation → accept', 'accept', NO_DECL, `/* we deliberately never truncate or redact here */\nexport function run(){return {context:'hi'};}`),
875
+ mk('model-facing lossy transform with no declaration → reject', 'reject', { header: NO_DECL, body: LOSSY_EMITTER }),
876
+ mk('same transform with recoverable-lossy + locator → accept', 'accept', { header: DECL_RECOVERABLE, body: LOSSY_EMITTER }),
877
+ mk('recoverable-lossy WITHOUT a locator → reject', 'reject', { header: DECL_RECOVERABLE_NO_LOCATOR, body: LOSSY_EMITTER }),
878
+ mk('ephemeral-lossy (owes no locator) → accept', 'accept', { header: DECL_EPHEMERAL, body: LOSSY_EMITTER }),
879
+ mk('a misspelled class is not honoured → reject', 'reject', { header: DECL_TYPO, body: LOSSY_EMITTER }),
880
+ mk('non-lossy emitter needs no declaration → accept', 'accept', { header: NO_DECL, body: `export function run(){return {context:'hi'};}` }),
881
+ mk('lossy but emits no context → accept', 'accept', { header: NO_DECL, body: `const WORD_CAP=5;export function run(){return body.slice(0,WORD_CAP);}` }),
882
+ mk('prose about truncation is not truncation → accept', 'accept', { header: NO_DECL, body: `/* we deliberately never truncate or redact here */\nexport function run(){return {context:'hi'};}` }),
883
+ // The widening, in both directions.
884
+ mk('a lossy _lib module an emitter REACHES, undeclared → reject', 'reject', {
885
+ header: NO_DECL, body: IMPORTING_EMITTER, libHeader: NO_DECL, lib: LOSSY_LIB,
886
+ }),
887
+ mk('the same reached module, declared on the module itself → accept', 'accept', {
888
+ header: NO_DECL, body: IMPORTING_EMITTER, libHeader: DECL_EPHEMERAL, lib: LOSSY_LIB,
889
+ }),
890
+ mk('a declaration on the EMITTER does not cover the reached module → reject', 'reject', {
891
+ header: DECL_EPHEMERAL, body: IMPORTING_EMITTER, libHeader: NO_DECL, lib: LOSSY_LIB,
892
+ }),
893
+ mk('a reached module that shortens nothing → accept', 'accept', {
894
+ header: NO_DECL, body: IMPORTING_CLEAN_EMITTER, libHeader: NO_DECL, lib: CLEAN_LIB,
895
+ }),
896
+ // The cap widening, isolated: these two differ in ONE token.
897
+ mk('a prefixed UNIT cap is a cap — SESSION_INDEX_ROW_CAP → reject', 'reject', {
898
+ header: NO_DECL, body: ROWS_EMITTER, libHeader: NO_DECL, lib: PREFIXED_UNIT_CAP_LIB,
899
+ }),
900
+ mk('the same shape with a NON-unit word — SESSION_INDEX_PRAGMA_CAP → accept', 'accept', {
901
+ header: NO_DECL, body: ROWS_EMITTER, libHeader: NO_DECL, lib: PREFIXED_NON_UNIT_CAP_LIB,
902
+ }),
903
+ mk('a lint budget is not a content cap — PRAGMA_CAP → accept', 'accept', {
904
+ // `replaceAll`, not `replace`: the first-match form renamed the
905
+ // import and left the call site reading `capRows`, so the fixture
906
+ // no longer meant what its name said.
907
+ header: NO_DECL, body: IMPORTING_EMITTER.replaceAll('capRows', 'pragmas'), libHeader: NO_DECL,
908
+ lib: `export const PRAGMA_CAP = 20;\nexport function pragmas(rows) { return rows; }\n`,
909
+ }),
910
+ // The four DATA shapes, one case each, so a regression localises.
911
+ mk('a SQLite PRAGMA naming TRUNCATE shortens nothing → accept', 'accept', {
912
+ header: NO_DECL, body: PRAGMA_LITERAL_EMITTER,
913
+ }),
914
+ mk("a detector's own /truncate table/ pattern is data → accept", 'accept', {
915
+ header: NO_DECL, body: DETECTOR_REGEX_EMITTER,
916
+ }),
917
+ mk('a settings key containing `redaction` is not a redaction → accept', 'accept', {
918
+ header: NO_DECL, body: SETTINGS_KEY_EMITTER,
919
+ }),
920
+ mk('a TRAILING // comment about truncation is not truncation → accept', 'accept', {
921
+ header: NO_DECL, body: TRAILING_COMMENT_EMITTER,
922
+ }),
923
+ // The three shapes that must stay IN — the true positives blanking and
924
+ // regex-composed stripping each cost, one case per direction.
925
+ mk('a redaction whose only evidence is its replacement literal → reject', 'reject', {
926
+ header: NO_DECL, body: SCRUB_EMITTER, libHeader: NO_DECL, lib: MARKER_REPLACEMENT_LIB,
927
+ }),
928
+ mk('a marker after a nested call is still a redaction → reject', 'reject', {
929
+ header: NO_DECL, body: SCRUB_EMITTER, libHeader: NO_DECL, lib: NESTED_MARKER_LIB,
930
+ }),
931
+ mk('an apostrophe in a template literal does not blank the code after it → reject', 'reject', {
932
+ header: NO_DECL, body: TEMPLATE_APOSTROPHE_EMITTER,
933
+ }),
934
+ mk('a /* inside a string literal does not open a comment → reject', 'reject', {
935
+ header: NO_DECL, body: PHANTOM_COMMENT_EMITTER,
936
+ }),
937
+ // The reach widening: an extensionless specifier is a reach.
938
+ mk('an extensionless relative import is a reach → reject', 'reject', {
939
+ header: NO_DECL, body: EXTENSIONLESS_EMITTER, libHeader: NO_DECL, lib: LOSSY_LIB,
940
+ }),
229
941
  ];
230
942
  }
231
943
 
@@ -233,7 +945,12 @@ function selfTestCases(): SelfTestCase[] {
233
945
 
234
946
  export function main(argv: string[] = process.argv.slice(2)): number {
235
947
  if (argv.includes('--self-test')) {
236
- return runSelfTest({ gate: 'check_loss_class_declared', cases: selfTestCases(), minCases: 7, minRejectCases: 3 });
948
+ // Floors sit ON the actual counts, not below them. At 13/5 against
949
+ // 15/6 the reject floor tolerated deleting a rejecting case in
950
+ // silence, so it no longer defended the number `gate-coverage.yml`
951
+ // documents. Raising a floor is a deliberate edit; letting one rot
952
+ // below the corpus is not.
953
+ return runSelfTest({ gate: 'check_loss_class_declared', cases: selfTestCases(), minCases: 24, minRejectCases: 11 });
237
954
  }
238
955
  const quiet = argv.includes('--quiet');
239
956
  const json = argv.includes('--json');
@@ -278,9 +995,33 @@ export function main(argv: string[] = process.argv.slice(2)): number {
278
995
  }
279
996
  };
280
997
 
998
+ // One line per MODULE, not per (concern, module) pair. A shared `_lib`
999
+ // module reached from several emitters is one undeclared module and one
1000
+ // edit; printing it once per reaching emitter made the output disagree
1001
+ // with `modules` / `modelFacing`, which already deduplicate. The ledger
1002
+ // keeps its per-concern `fail` — that count answers a different question
1003
+ // (which concerns are affected), and the reaching set is named on the line
1004
+ // so nothing is lost by collapsing it.
1005
+ const byModule = new Map<string, { first: LossFinding; concerns: string[] }>();
281
1006
  for (const f of v.findings) {
1007
+ const hit = byModule.get(f.module);
1008
+ if (hit === undefined) byModule.set(f.module, { first: f, concerns: [f.concern] });
1009
+ else {
1010
+ hit.concerns.push(f.concern);
1011
+ if (f.tier === 'fail') hit.first = { ...hit.first, tier: 'fail' };
1012
+ }
1013
+ }
1014
+
1015
+ for (const { first: f, concerns } of byModule.values()) {
282
1016
  const stream = f.tier === 'fail' ? process.stderr : process.stdout;
283
- stream.write(`${f.tier === 'fail' ? '❌' : '⚠️ '} ${f.concern} (${f.script}) shortens content on a model-facing path and ${say(f)}\n`);
1017
+ const reached =
1018
+ concerns.length === 1
1019
+ ? `reached from concern ${f.concern} → ${f.script}`
1020
+ : `reached from ${String(concerns.length)} concerns: ${concerns.join(', ')}`;
1021
+ stream.write(
1022
+ `${f.tier === 'fail' ? '❌' : '⚠️ '} ${f.module} shortens content on a model-facing path ` +
1023
+ `(${reached}) and ${say(f)}\n`,
1024
+ );
284
1025
  }
285
1026
 
286
1027
  if (v.findings.some((f) => f.tier === 'fail')) {
@@ -293,9 +1034,15 @@ export function main(argv: string[] = process.argv.slice(2)): number {
293
1034
  return 1;
294
1035
  }
295
1036
  if (!quiet) {
1037
+ // "matched", not "lossy". The detector establishes a match; the
1038
+ // DECLARATION says whether anything is lost, and on `exact` it says
1039
+ // nothing is. Calling every matched module lossy overstated the real
1040
+ // lossy corpus by however many `exact` members it held.
1041
+ const exact = v.declaredExact > 0 ? `, ${String(v.declaredExact)} of them declaring exact` : '';
296
1042
  process.stdout.write(
297
1043
  `✅ every model-facing lossy transform declares its loss class ` +
298
- `(${String(v.modelFacing)} model-facing of ${String(v.scanned)} hook script(s)).\n`,
1044
+ `(${String(v.modelFacing)} matched module(s)${exact}, reached from ${String(v.emitters)} emitter(s) ` +
1045
+ `of ${String(v.scanned)} hook script(s)).\n`,
299
1046
  );
300
1047
  }
301
1048
  return 0;