@daniellins/power-claude 0.16.3

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.

Potentially problematic release.


This version of @daniellins/power-claude might be problematic. Click here for more details.

Files changed (467) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/bin/cli.js +124 -0
  4. package/installer/__tests__/doctor-ready-oracles.test.cjs +140 -0
  5. package/installer/__tests__/merge-settings.test.cjs +330 -0
  6. package/installer/adopt.js +161 -0
  7. package/installer/copy.js +203 -0
  8. package/installer/doctor.js +266 -0
  9. package/installer/install.js +155 -0
  10. package/installer/manifest.js +102 -0
  11. package/installer/motor-scripts.js +50 -0
  12. package/installer/motor-scripts.json +39 -0
  13. package/installer/uninstall.js +99 -0
  14. package/installer/update.js +376 -0
  15. package/installer/util.js +113 -0
  16. package/installer/wizard.js +64 -0
  17. package/package.json +26 -0
  18. package/scripts/__fixtures__/anti-path-demo/agents/ap-writer.md +13 -0
  19. package/scripts/__fixtures__/anti-path-demo/templates/write-tmpl.md +7 -0
  20. package/scripts/__fixtures__/anti-path-demo/workflows/wf-ap.smoke.json +10 -0
  21. package/scripts/__fixtures__/anti-path-demo/workflows/wf-ap.yaml +25 -0
  22. package/scripts/__fixtures__/assess-tier/retrofit-leve/pod/agents/content-chief.md +1 -0
  23. package/scripts/__fixtures__/assess-tier/retrofit-leve/pod/workflows/wf-criacao-redteam.yaml +3 -0
  24. package/scripts/__fixtures__/assess-tier/retrofit-leve/pod-spec.json +13 -0
  25. package/scripts/__fixtures__/fidelity/fake-green-average-in-band.yaml +21 -0
  26. package/scripts/__fixtures__/fidelity/fake-green-average.yaml +18 -0
  27. package/scripts/__fixtures__/fidelity/fake-green-denominator.yaml +19 -0
  28. package/scripts/__fixtures__/fidelity/fake-green-missing-evidence.yaml +18 -0
  29. package/scripts/__fixtures__/fidelity/fake-green-threshold.yaml +18 -0
  30. package/scripts/__fixtures__/fidelity/fake-green-weight.yaml +18 -0
  31. package/scripts/__fixtures__/fidelity/golden.yaml +31 -0
  32. package/scripts/__fixtures__/fidelity/trindade-incoherent.yaml +15 -0
  33. package/scripts/__fixtures__/forge-canonical/pod-spec-example.json +37 -0
  34. package/scripts/__fixtures__/migrate-squad/authored/agents/offer-engineer.md +17 -0
  35. package/scripts/__fixtures__/migrate-squad/authored/knowledge/no-source.md +7 -0
  36. package/scripts/__fixtures__/migrate-squad/authored/knowledge/value-stack-method.md +8 -0
  37. package/scripts/__fixtures__/migrate-squad/authored/specs/wf-sample.smoke.json +24 -0
  38. package/scripts/__fixtures__/migrate-squad/authored/specs/wf-sample.spec.json +45 -0
  39. package/scripts/__fixtures__/migrate-squad/legacy-squad/HEADLINE.md +3 -0
  40. package/scripts/__fixtures__/migrate-squad/legacy-squad/README.md +4 -0
  41. package/scripts/__fixtures__/migrate-squad/legacy-squad/agents/hormozi-offer.md +14 -0
  42. package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/dead-redundant.md +4 -0
  43. package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/method-guide.md +6 -0
  44. package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/veto-screen.md +4 -0
  45. package/scripts/__fixtures__/migrate-squad/legacy-squad/config/config.yaml +2 -0
  46. package/scripts/__fixtures__/migrate-squad/legacy-squad/data/case-library.md +3 -0
  47. package/scripts/__fixtures__/migrate-squad/legacy-squad/scripts/validate-mvn.py +16 -0
  48. package/scripts/__fixtures__/migrate-squad/legacy-squad/sops/sop-sample.md +3 -0
  49. package/scripts/__fixtures__/migrate-squad/legacy-squad/squad-io.yaml +14 -0
  50. package/scripts/__fixtures__/migrate-squad/legacy-squad/tasks/gather-context.md +7 -0
  51. package/scripts/__fixtures__/migrate-squad/legacy-squad/templates/output-tmpl.md +5 -0
  52. package/scripts/__fixtures__/migrate-squad/legacy-squad/workflows/wf-sample.yaml +24 -0
  53. package/scripts/__fixtures__/pipeline/pipelines/pipeline-checkpoint.yaml +21 -0
  54. package/scripts/__fixtures__/pipeline/pipelines/pipeline-demo.yaml +22 -0
  55. package/scripts/__fixtures__/pipeline/pods/pipe-a/agents/maker.md +13 -0
  56. package/scripts/__fixtures__/pipeline/pods/pipe-a/memory/learnings.consolidated.md +8 -0
  57. package/scripts/__fixtures__/pipeline/pods/pipe-a/memory/learnings.jsonl +83 -0
  58. package/scripts/__fixtures__/pipeline/pods/pipe-a/templates/make-tmpl.md +8 -0
  59. package/scripts/__fixtures__/pipeline/pods/pipe-a/workflows/wf-a.yaml +16 -0
  60. package/scripts/__fixtures__/pipeline/pods/pipe-b/agents/user.md +13 -0
  61. package/scripts/__fixtures__/pipeline/pods/pipe-b/memory/learnings.consolidated.md +8 -0
  62. package/scripts/__fixtures__/pipeline/pods/pipe-b/memory/learnings.jsonl +14 -0
  63. package/scripts/__fixtures__/pipeline/pods/pipe-b/templates/use-tmpl.md +6 -0
  64. package/scripts/__fixtures__/pipeline/pods/pipe-b/workflows/wf-b.yaml +16 -0
  65. package/scripts/__fixtures__/pod-run/agents/forge-chief.md +13 -0
  66. package/scripts/__fixtures__/pod-run/agents/pod-critic.md +13 -0
  67. package/scripts/__fixtures__/pod-run/templates/build-tmpl.md +8 -0
  68. package/scripts/__fixtures__/pod-run/templates/gate-tmpl.md +6 -0
  69. package/scripts/__fixtures__/pod-run/workflows/wf-min.yaml +43 -0
  70. package/scripts/__fixtures__/pod-run/workflows/wf-skip-modes.yaml +50 -0
  71. package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-all-skippable.yaml +30 -0
  72. package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-criteria-escalar-de-bloco.yaml +40 -0
  73. package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-skip-mode-desconhecido.yaml +30 -0
  74. package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-unknown-key.yaml +29 -0
  75. package/scripts/__fixtures__/pod-workflow/invalid-cycle.yaml +26 -0
  76. package/scripts/__fixtures__/pod-workflow/invalid-fix-loop-backedge.yaml +29 -0
  77. package/scripts/__fixtures__/pod-workflow/invalid-fix-loop-forward.yaml +29 -0
  78. package/scripts/__fixtures__/pod-workflow/invalid-handoff-open.yaml +27 -0
  79. package/scripts/__fixtures__/pod-workflow/invalid-knowledge-shape.yaml +18 -0
  80. package/scripts/__fixtures__/pod-workflow/invalid-missing-agent.yaml +15 -0
  81. package/scripts/__fixtures__/pod-workflow/invalid-missing-outputs.yaml +15 -0
  82. package/scripts/__fixtures__/pod-workflow/invalid-missing-template.yaml +15 -0
  83. package/scripts/__fixtures__/pod-workflow/invalid-missing-veto.yaml +13 -0
  84. package/scripts/__fixtures__/pod-workflow/invalid-template-not-on-disk.yaml +16 -0
  85. package/scripts/__fixtures__/pod-workflow/invalid-veto-prose.yaml +17 -0
  86. package/scripts/__fixtures__/pod-workflow/knowledge/a.md +4 -0
  87. package/scripts/__fixtures__/pod-workflow/knowledge/b.md +4 -0
  88. package/scripts/__fixtures__/pod-workflow/knowledge/c.md +4 -0
  89. package/scripts/__fixtures__/pod-workflow/templates/step-tmpl.md +7 -0
  90. package/scripts/__fixtures__/pod-workflow/valid-checkpoint-skip-modes.yaml +29 -0
  91. package/scripts/__fixtures__/pod-workflow/valid-fix-loop.yaml +30 -0
  92. package/scripts/__fixtures__/pod-workflow/valid-knowledge.yaml +30 -0
  93. package/scripts/__fixtures__/pod-workflow/valid-veto-always-true.yaml +20 -0
  94. package/scripts/__fixtures__/pod-workflow/valid.yaml +27 -0
  95. package/scripts/__fixtures__/smoke-canary/agents/canary-reporter.md +13 -0
  96. package/scripts/__fixtures__/smoke-canary/agents/canary-researcher.md +13 -0
  97. package/scripts/__fixtures__/smoke-canary/agents/canary-synth.md +13 -0
  98. package/scripts/__fixtures__/smoke-canary/memory/learnings.consolidated.md +10 -0
  99. package/scripts/__fixtures__/smoke-canary/memory/learnings.jsonl +448 -0
  100. package/scripts/__fixtures__/smoke-canary/templates/report-tmpl.md +9 -0
  101. package/scripts/__fixtures__/smoke-canary/templates/research-tmpl.md +7 -0
  102. package/scripts/__fixtures__/smoke-canary/templates/synth-tmpl.md +9 -0
  103. package/scripts/__fixtures__/smoke-canary/workflows/wf-canary.smoke.json +11 -0
  104. package/scripts/__fixtures__/smoke-canary/workflows/wf-canary.yaml +53 -0
  105. package/scripts/__fixtures__/source-audit/fake-green-bronze-unsignaled.yaml +10 -0
  106. package/scripts/__fixtures__/source-audit/fake-green-counts.yaml +9 -0
  107. package/scripts/__fixtures__/source-audit/fake-green-missing-evidence.yaml +9 -0
  108. package/scripts/__fixtures__/source-audit/fake-green-unclassified.yaml +10 -0
  109. package/scripts/__fixtures__/source-audit/fake-green-verdict.yaml +10 -0
  110. package/scripts/__fixtures__/source-audit/golden-blocked.yaml +10 -0
  111. package/scripts/__fixtures__/source-audit/golden.yaml +11 -0
  112. package/scripts/__fixtures__/sweep-forms/agents/forms-count.md +13 -0
  113. package/scripts/__fixtures__/sweep-forms/agents/forms-field.md +13 -0
  114. package/scripts/__fixtures__/sweep-forms/agents/forms-fileref.md +13 -0
  115. package/scripts/__fixtures__/sweep-forms/agents/forms-json.md +13 -0
  116. package/scripts/__fixtures__/sweep-forms/agents/forms-section.md +13 -0
  117. package/scripts/__fixtures__/sweep-forms/specs/wf-forms.spec.json +5 -0
  118. package/scripts/__fixtures__/sweep-forms/templates/t_count.md +5 -0
  119. package/scripts/__fixtures__/sweep-forms/templates/t_field.md +3 -0
  120. package/scripts/__fixtures__/sweep-forms/templates/t_fileref.md +3 -0
  121. package/scripts/__fixtures__/sweep-forms/templates/t_json.md +0 -0
  122. package/scripts/__fixtures__/sweep-forms/templates/t_section.md +3 -0
  123. package/scripts/__fixtures__/sweep-forms/workflows/wf-forms.smoke.json +13 -0
  124. package/scripts/__fixtures__/sweep-forms/workflows/wf-forms.yaml +67 -0
  125. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-1-noop-state-json.fixture.md +18 -0
  126. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-2-crash-buildworkflowyaml.fixture.md +26 -0
  127. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-3-nasce-verde-skip-modes.fixture.md +29 -0
  128. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-4-guarda-sem-irmao.fixture.md +17 -0
  129. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-5a-diff-stat-sem-guarda.fixture.md +17 -0
  130. package/scripts/__fixtures__/validate-story-acs-corpus/corpus-5b-test-f-sobre-diretorio.fixture.md +20 -0
  131. package/scripts/__fixtures__/veto-checks/copy-clean.md +4 -0
  132. package/scripts/__fixtures__/veto-checks/copy-dashes.md +4 -0
  133. package/scripts/__fixtures__/veto-checks/n8n-bad.md +18 -0
  134. package/scripts/__fixtures__/veto-checks/n8n-ok.md +22 -0
  135. package/scripts/__fixtures__/veto-checks/page-bad.html +7 -0
  136. package/scripts/__fixtures__/veto-checks/phash/copy.png +0 -0
  137. package/scripts/__fixtures__/veto-checks/phash/distinct.png +0 -0
  138. package/scripts/__fixtures__/veto-checks/phash/distinct2.png +0 -0
  139. package/scripts/__fixtures__/veto-checks/phash/make-fixtures.cjs +203 -0
  140. package/scripts/__fixtures__/veto-checks/phash/ref.png +0 -0
  141. package/scripts/__fixtures__/veto-checks/slide-ok.html +15 -0
  142. package/scripts/__fixtures__/veto-checks/slide-overflow.html +14 -0
  143. package/scripts/__fixtures__/veto-checks/sweep-pod/agents/vc-checker.md +13 -0
  144. package/scripts/__fixtures__/veto-checks/sweep-pod/templates/check-tmpl.md +6 -0
  145. package/scripts/__fixtures__/veto-checks/sweep-pod/workflows/wf-vc.smoke.json +10 -0
  146. package/scripts/__fixtures__/veto-checks/sweep-pod/workflows/wf-vc.yaml +19 -0
  147. package/scripts/__fixtures__/voz-do-dono/README.md +23 -0
  148. package/scripts/__fixtures__/voz-do-dono/dono-escalar.yaml +9 -0
  149. package/scripts/__fixtures__/voz-do-dono/dono-guia.yaml +10 -0
  150. package/scripts/__fixtures__/voz-do-dono/peca-3a-pessoa.md +6 -0
  151. package/scripts/__fixtures__/voz-do-dono/peca-com-lexico-proibido.md +6 -0
  152. package/scripts/__fixtures__/voz-do-dono/peca-limpa-1a-pessoa.md +6 -0
  153. package/scripts/__tests__/derive-ready-transitions.test.cjs +269 -0
  154. package/scripts/__tests__/enforce-cycle-authority.test.cjs +869 -0
  155. package/scripts/__tests__/enforce-git-push-authority.test.cjs +701 -0
  156. package/scripts/__tests__/enforce-ready-oracles.test.cjs +745 -0
  157. package/scripts/__tests__/motor-scripts.test.cjs +339 -0
  158. package/scripts/__tests__/pc-run-pod-counter.test.cjs +58 -0
  159. package/scripts/__tests__/pc-run-pod.test.cjs +578 -0
  160. package/scripts/__tests__/validate-pod-workflow.test.cjs +90 -0
  161. package/scripts/__tests__/validate-story-acs.test.cjs +607 -0
  162. package/scripts/anti-path.test.cjs +163 -0
  163. package/scripts/arena.cjs +514 -0
  164. package/scripts/arena.test.cjs +213 -0
  165. package/scripts/brief-leitura-obrigatoria.test.cjs +364 -0
  166. package/scripts/consolidate-knowledge-runner.test.cjs +249 -0
  167. package/scripts/default-funcional.test.cjs +153 -0
  168. package/scripts/derive-ready-transitions.cjs +325 -0
  169. package/scripts/forge-additive.test.cjs +158 -0
  170. package/scripts/forge-assess-tier.cjs +205 -0
  171. package/scripts/forge-assess-tier.test.cjs +114 -0
  172. package/scripts/forge-canonical.test.cjs +624 -0
  173. package/scripts/forge-generate.cjs +1303 -0
  174. package/scripts/forge-harden-learning.cjs +921 -0
  175. package/scripts/forge-harden-learning.test.cjs +242 -0
  176. package/scripts/forge-ingest-memory.cjs +136 -0
  177. package/scripts/forge-upgrade-pod.cjs +368 -0
  178. package/scripts/forge.test.cjs +609 -0
  179. package/scripts/harden-knowledge.test.cjs +612 -0
  180. package/scripts/harness-g5-lite.cjs +243 -0
  181. package/scripts/harness-g5-lite.test.cjs +134 -0
  182. package/scripts/hn-carousel-contrato-entrada.test.cjs +96 -0
  183. package/scripts/migrate-squad-orchestration.test.cjs +146 -0
  184. package/scripts/migration-manifest-sf3.2.test.cjs +382 -0
  185. package/scripts/migration-manifest-sf3.3.test.cjs +170 -0
  186. package/scripts/migration-manifest-sf5.5.test.cjs +189 -0
  187. package/scripts/migration-manifest.cjs +1146 -0
  188. package/scripts/migration-manifest.test.cjs +186 -0
  189. package/scripts/npm-scope-invocation.test.cjs +194 -0
  190. package/scripts/owner-layer.cjs +442 -0
  191. package/scripts/owner-layer.test.cjs +358 -0
  192. package/scripts/pc-run-pipeline.cjs +289 -0
  193. package/scripts/pc-run-pipeline.test.cjs +256 -0
  194. package/scripts/pc-run-pod-countgte.test.cjs +66 -0
  195. package/scripts/pc-run-pod.cjs +1355 -0
  196. package/scripts/phash-gate.test.cjs +433 -0
  197. package/scripts/pod-export-compile.cjs +487 -0
  198. package/scripts/pod-export-compile.test.cjs +312 -0
  199. package/scripts/pod-export-executor.cjs +678 -0
  200. package/scripts/pod-export-executor.test.cjs +1341 -0
  201. package/scripts/pod-export-hardening.test.cjs +531 -0
  202. package/scripts/pod-export-postprocess.cjs +427 -0
  203. package/scripts/pod-export-postprocess.test.cjs +558 -0
  204. package/scripts/pod-export-queue-race.test.cjs +865 -0
  205. package/scripts/pod-export-release.cjs +473 -0
  206. package/scripts/pod-export-release.test.cjs +267 -0
  207. package/scripts/pod-export-service.cjs +1297 -0
  208. package/scripts/pod-export-service.test.cjs +489 -0
  209. package/scripts/pod-export-tools.cjs +470 -0
  210. package/scripts/pod-export-ui.cjs +805 -0
  211. package/scripts/pod-export-ui.test.cjs +301 -0
  212. package/scripts/pod-memory-budget.test.cjs +309 -0
  213. package/scripts/pod-memory-note.cjs +357 -0
  214. package/scripts/pod-memory-note.test.cjs +452 -0
  215. package/scripts/pod-memory.cjs +1252 -0
  216. package/scripts/pod-memory.test.cjs +951 -0
  217. package/scripts/render-png.test.cjs +307 -0
  218. package/scripts/smoke-isolation.test.cjs +100 -0
  219. package/scripts/smoke-pod-workflow.cjs +755 -0
  220. package/scripts/smoke-sweep.test.cjs +170 -0
  221. package/scripts/validate-adopt.js +235 -0
  222. package/scripts/validate-branding.js +84 -0
  223. package/scripts/validate-budgets.js +140 -0
  224. package/scripts/validate-fidelity-output.cjs +267 -0
  225. package/scripts/validate-fidelity-output.test.cjs +101 -0
  226. package/scripts/validate-install.js +230 -0
  227. package/scripts/validate-no-clone.cjs +220 -0
  228. package/scripts/validate-no-clone.test.cjs +77 -0
  229. package/scripts/validate-pipeline.cjs +145 -0
  230. package/scripts/validate-pod-workflow.cjs +878 -0
  231. package/scripts/validate-source-audit.cjs +170 -0
  232. package/scripts/validate-source-audit.test.cjs +90 -0
  233. package/scripts/validate-story-acs.cjs +862 -0
  234. package/scripts/validate-update-scope-motor.js +421 -0
  235. package/scripts/veto-checks/html-valid.cjs +92 -0
  236. package/scripts/veto-checks/lexical-check.cjs +62 -0
  237. package/scripts/veto-checks/phash-gate.cjs +589 -0
  238. package/scripts/veto-checks/render-gate.cjs +165 -0
  239. package/scripts/veto-checks/render-png.cjs +358 -0
  240. package/scripts/veto-checks.test.cjs +219 -0
  241. package/scripts/watcher-audit.cjs +178 -0
  242. package/template/.claude/CLAUDE.md +103 -0
  243. package/template/.claude/agents/pc-analyst.md +100 -0
  244. package/template/.claude/agents/pc-architect.md +32 -0
  245. package/template/.claude/agents/pc-dev.md +27 -0
  246. package/template/.claude/agents/pc-devops.md +45 -0
  247. package/template/.claude/agents/pc-planner.md +32 -0
  248. package/template/.claude/agents/pc-po.md +25 -0
  249. package/template/.claude/agents/pc-qa.md +25 -0
  250. package/template/.claude/commands/power/academy.md +9 -0
  251. package/template/.claude/commands/power/automation.md +9 -0
  252. package/template/.claude/commands/power/content-carousel.md +267 -0
  253. package/template/.claude/commands/power/content.md +9 -0
  254. package/template/.claude/commands/power/dev.md +9 -0
  255. package/template/.claude/commands/power/forge.md +224 -0
  256. package/template/.claude/commands/power/hormozi.md +221 -0
  257. package/template/.claude/commands/power/modo.md +57 -0
  258. package/template/.claude/hooks/README.md +114 -0
  259. package/template/.claude/hooks/context-budget.cjs +64 -0
  260. package/template/.claude/hooks/enforce-cycle-authority.cjs +457 -0
  261. package/template/.claude/hooks/enforce-git-push-authority.cjs +925 -0
  262. package/template/.claude/hooks/enforce-ready-oracles.cjs +502 -0
  263. package/template/.claude/rules/authority.md +76 -0
  264. package/template/.claude/rules/curadoria-de-conhecimento.md +30 -0
  265. package/template/.claude/rules/model-matrix.md +34 -0
  266. package/template/.claude/rules/orchestration.md +133 -0
  267. package/template/.claude/settings.json +50 -0
  268. package/template/.claude/skills/pc-apply-qa-fixes/SKILL.md +108 -0
  269. package/template/.claude/skills/pc-apply-qa-fixes/references/fix-loop.md +40 -0
  270. package/template/.claude/skills/pc-apply-qa-fixes/references/fix-request-template.md +70 -0
  271. package/template/.claude/skills/pc-arena/SKILL.md +96 -0
  272. package/template/.claude/skills/pc-close-story/SKILL.md +105 -0
  273. package/template/.claude/skills/pc-commit/SKILL.md +108 -0
  274. package/template/.claude/skills/pc-develop-story/SKILL.md +116 -0
  275. package/template/.claude/skills/pc-full-cycle/SKILL.md +150 -0
  276. package/template/.claude/skills/pc-pipeline/SKILL.md +63 -0
  277. package/template/.claude/skills/pc-pipeline/references/gate-templates.md +46 -0
  278. package/template/.claude/skills/pc-review-story/SKILL.md +129 -0
  279. package/template/.claude/skills/pc-review-story/references/false-positive-detection.md +50 -0
  280. package/template/.claude/skills/pc-review-story/references/nfr-assess.md +47 -0
  281. package/template/.claude/skills/pc-review-story/references/risk-profile.md +75 -0
  282. package/template/.claude/skills/pc-review-story/references/security-checklist.md +69 -0
  283. package/template/.claude/skills/pc-review-story/references/test-design.md +37 -0
  284. package/template/.claude/skills/pc-review-story/references/trace-requirements.md +46 -0
  285. package/template/.claude/skills/pc-run-pod/SKILL.md +123 -0
  286. package/template/.claude/skills/pc-validate-story/SKILL.md +114 -0
  287. package/template/.claude/skills/pc-wave/SKILL.md +133 -0
  288. package/template/.env.example +10 -0
  289. package/template/knowledge/analyst/brainstorming.md +40 -0
  290. package/template/knowledge/analyst/brief-skeleton.md +32 -0
  291. package/template/knowledge/analyst/competitor-skeleton.md +39 -0
  292. package/template/knowledge/analyst/elicitation-menu.md +49 -0
  293. package/template/knowledge/analyst/market-research-skeleton.md +36 -0
  294. package/template/knowledge/analyst/research-prompt-structure.md +48 -0
  295. package/template/knowledge/analyst/roi-skeleton.md +51 -0
  296. package/template/knowledge/voice-dna/README.md +99 -0
  297. package/template/knowledge/voice-dna/voice-dna.example.yaml +157 -0
  298. package/template/pods/README.md +64 -0
  299. package/template/pods/academy/README.md +37 -0
  300. package/template/pods/academy/agents/academy-chief.md +27 -0
  301. package/template/pods/academy/examples/palestra.example.md +142 -0
  302. package/template/pods/academy/pod-spec.json +44 -0
  303. package/template/pods/academy/templates/materiais-tmpl.md +17 -0
  304. package/template/pods/academy/templates/revisao-didatica-tmpl.md +20 -0
  305. package/template/pods/academy/workflows/wf-materiais-revisao.yaml +30 -0
  306. package/template/pods/authority-content/README.md +43 -0
  307. package/template/pods/authority-content/agents/content-chief.md +28 -0
  308. package/template/pods/authority-content/pod-spec.json +45 -0
  309. package/template/pods/authority-content/templates/criacao-na-voz-tmpl.md +17 -0
  310. package/template/pods/authority-content/templates/red-team-factual-tmpl.md +23 -0
  311. package/template/pods/authority-content/workflows/wf-criacao-redteam.yaml +30 -0
  312. package/template/pods/automation/README.md +37 -0
  313. package/template/pods/automation/agents/langgraph-architect.md +30 -0
  314. package/template/pods/automation/agents/n8n-architect.md +30 -0
  315. package/template/pods/automation/examples/README.md +46 -0
  316. package/template/pods/automation/examples/n8n-content-radar.example.json +380 -0
  317. package/template/pods/automation/knowledge/langgraph-patterns.md +126 -0
  318. package/template/pods/automation/knowledge/n8n-patterns.md +131 -0
  319. package/template/pods/automation/memory/README.md +16 -0
  320. package/template/pods/automation/memory/learnings.consolidated.md +8 -0
  321. package/template/pods/automation/memory/learnings.jsonl +0 -0
  322. package/template/pods/automation/pod-spec.json +109 -0
  323. package/template/pods/automation/specs/wf-langgraph-build.spec.json +107 -0
  324. package/template/pods/automation/squad-io.yaml +28 -0
  325. package/template/pods/automation/templates/lg-build-tmpl.md +17 -0
  326. package/template/pods/automation/templates/lg-design-tmpl.md +23 -0
  327. package/template/pods/automation/templates/lg-discover-tmpl.md +20 -0
  328. package/template/pods/automation/templates/lg-handoff-tmpl.md +20 -0
  329. package/template/pods/automation/templates/lg-test-tmpl.md +20 -0
  330. package/template/pods/automation/templates/n8n-build-tmpl.md +14 -0
  331. package/template/pods/automation/templates/n8n-design-tmpl.md +23 -0
  332. package/template/pods/automation/templates/n8n-discover-tmpl.md +20 -0
  333. package/template/pods/automation/templates/n8n-handoff-tmpl.md +20 -0
  334. package/template/pods/automation/templates/n8n-test-tmpl.md +20 -0
  335. package/template/pods/automation/workflows/wf-langgraph-build.smoke.json +11 -0
  336. package/template/pods/automation/workflows/wf-langgraph-build.yaml +64 -0
  337. package/template/pods/automation/workflows/wf-n8n-build.smoke.json +11 -0
  338. package/template/pods/automation/workflows/wf-n8n-build.yaml +65 -0
  339. package/template/pods/content-carousel/README.md +91 -0
  340. package/template/pods/content-carousel/agents/carousel-builder.md +28 -0
  341. package/template/pods/content-carousel/agents/carousel-visual.md +24 -0
  342. package/template/pods/content-carousel/agents/hn-curator.md +74 -0
  343. package/template/pods/content-carousel/config/heuristic-watcher-rubric.yaml +59 -0
  344. package/template/pods/content-carousel/knowledge/aquisicao-ancora.md +443 -0
  345. package/template/pods/content-carousel/knowledge/cta-brief.md +25 -0
  346. package/template/pods/content-carousel/knowledge/dono.example.yaml +45 -0
  347. package/template/pods/content-carousel/knowledge/identidade-visual-mascote.md +62 -0
  348. package/template/pods/content-carousel/knowledge/image-generation.md +62 -0
  349. package/template/pods/content-carousel/knowledge/selecao-de-ancora.md +60 -0
  350. package/template/pods/content-carousel/knowledge/slides-html.md +56 -0
  351. package/template/pods/content-carousel/memory/README.md +18 -0
  352. package/template/pods/content-carousel/memory/learnings.consolidated.md +6 -0
  353. package/template/pods/content-carousel/memory/learnings.jsonl +0 -0
  354. package/template/pods/content-carousel/pod-spec.json +100 -0
  355. package/template/pods/content-carousel/squad-io.yaml +10 -0
  356. package/template/pods/content-carousel/templates/assembly-tmpl.md +23 -0
  357. package/template/pods/content-carousel/templates/digest-tmpl.md +29 -0
  358. package/template/pods/content-carousel/templates/visuals-tmpl.md +20 -0
  359. package/template/pods/content-carousel/workflows/wf-content-carousel.smoke.json +23 -0
  360. package/template/pods/content-carousel/workflows/wf-content-carousel.yaml +80 -0
  361. package/template/pods/dev-ia/README.md +45 -0
  362. package/template/pods/dev-ia/agents/db-architect.md +33 -0
  363. package/template/pods/dev-ia/agents/ux-architect.md +29 -0
  364. package/template/pods/dev-ia/knowledge/db/migration-protocol.md +95 -0
  365. package/template/pods/dev-ia/knowledge/db/rls-patterns.md +101 -0
  366. package/template/pods/dev-ia/knowledge/db/schema-review-checklist.md +76 -0
  367. package/template/pods/dev-ia/memory/README.md +16 -0
  368. package/template/pods/dev-ia/memory/learnings.consolidated.md +8 -0
  369. package/template/pods/dev-ia/memory/learnings.jsonl +0 -0
  370. package/template/pods/dev-ia/pod-spec.json +34 -0
  371. package/template/pods/dev-ia/templates/schema-review-tmpl.md +23 -0
  372. package/template/pods/dev-ia/templates/ux-flow-review-tmpl.md +23 -0
  373. package/template/pods/dev-ia/tools/db-ops/backup-manager.sh +321 -0
  374. package/template/pods/dev-ia/tools/db-ops/best-practices-auditor.sh +534 -0
  375. package/template/pods/dev-ia/tools/db-ops/bootstrap-runner.sh +255 -0
  376. package/template/pods/dev-ia/tools/db-ops/data-loader.sh +616 -0
  377. package/template/pods/dev-ia/tools/db-ops/detect-database.sh +113 -0
  378. package/template/pods/dev-ia/tools/db-ops/explain-analyzer.sh +285 -0
  379. package/template/pods/dev-ia/tools/db-ops/health-checker.sh +323 -0
  380. package/template/pods/dev-ia/tools/db-ops/migration-runner.sh +308 -0
  381. package/template/pods/dev-ia/tools/db-ops/postgresql-schema-loader.sh +334 -0
  382. package/template/pods/dev-ia/tools/db-ops/query-runner.sh +171 -0
  383. package/template/pods/dev-ia/tools/db-ops/rls-policy-installer.sh +480 -0
  384. package/template/pods/dev-ia/tools/db-ops/rls-test-suite.sh +172 -0
  385. package/template/pods/dev-ia/tools/db-ops/schema-context-loader.sh +176 -0
  386. package/template/pods/hormozi-method/README.md +52 -0
  387. package/template/pods/hormozi-method/agents/acquisition-strategist.md +33 -0
  388. package/template/pods/hormozi-method/agents/conversion-copywriter.md +34 -0
  389. package/template/pods/hormozi-method/agents/monetization-architect.md +35 -0
  390. package/template/pods/hormozi-method/agents/offer-engineer.md +37 -0
  391. package/template/pods/hormozi-method/knowledge/core-four-leads.md +51 -0
  392. package/template/pods/hormozi-method/knowledge/grand-slam-offer.md +66 -0
  393. package/template/pods/hormozi-method/knowledge/growth-decision.md +38 -0
  394. package/template/pods/hormozi-method/knowledge/hooks-and-ads.md +56 -0
  395. package/template/pods/hormozi-method/knowledge/launch-email.md +31 -0
  396. package/template/pods/hormozi-method/knowledge/money-model.md +46 -0
  397. package/template/pods/hormozi-method/knowledge/opportunity-screening.md +44 -0
  398. package/template/pods/hormozi-method/knowledge/pages.md +25 -0
  399. package/template/pods/hormozi-method/knowledge/pos-lancamento-leads.md +56 -0
  400. package/template/pods/hormozi-method/knowledge/pricing.md +36 -0
  401. package/template/pods/hormozi-method/knowledge/retention-ltv.md +37 -0
  402. package/template/pods/hormozi-method/knowledge/scale-roadmap.md +35 -0
  403. package/template/pods/hormozi-method/knowledge/swipefiles-curados.md +44 -0
  404. package/template/pods/hormozi-method/knowledge/value-equation-diagnostic.md +77 -0
  405. package/template/pods/hormozi-method/memory/README.md +16 -0
  406. package/template/pods/hormozi-method/memory/learnings.consolidated.md +6 -0
  407. package/template/pods/hormozi-method/memory/learnings.jsonl +0 -0
  408. package/template/pods/hormozi-method/pod-spec.json +50 -0
  409. package/template/pods/hormozi-method/specs/wf-full-launch-sequence.spec.json +149 -0
  410. package/template/pods/hormozi-method/specs/wf-lead-magnet-pipeline.spec.json +125 -0
  411. package/template/pods/hormozi-method/specs/wf-money-model-design.spec.json +117 -0
  412. package/template/pods/hormozi-method/specs/wf-paid-ads-campaign.spec.json +132 -0
  413. package/template/pods/hormozi-method/specs/wf-scaling-diagnostic.spec.json +121 -0
  414. package/template/pods/hormozi-method/squad-io.yaml +43 -0
  415. package/template/pods/hormozi-method/templates/conversion-copywriter-tmpl.md +20 -0
  416. package/template/pods/hormozi-method/templates/launch-execucao-tmpl.md +17 -0
  417. package/template/pods/hormozi-method/templates/launch-fechamento-tmpl.md +17 -0
  418. package/template/pods/hormozi-method/templates/launch-pos-tmpl.md +17 -0
  419. package/template/pods/hormozi-method/templates/launch-preparacao-tmpl.md +17 -0
  420. package/template/pods/hormozi-method/templates/lm-copy-tmpl.md +17 -0
  421. package/template/pods/hormozi-method/templates/lm-distribuicao-tmpl.md +18 -0
  422. package/template/pods/hormozi-method/templates/lm-estrategia-tmpl.md +17 -0
  423. package/template/pods/hormozi-method/templates/mm-arquitetura-tmpl.md +18 -0
  424. package/template/pods/hormozi-method/templates/mm-copy-tmpl.md +17 -0
  425. package/template/pods/hormozi-method/templates/mm-diagnostico-tmpl.md +14 -0
  426. package/template/pods/hormozi-method/templates/monetization-architect-tmpl.md +17 -0
  427. package/template/pods/hormozi-method/templates/offer-engineer-tmpl.md +20 -0
  428. package/template/pods/hormozi-method/templates/paid-criativos-tmpl.md +19 -0
  429. package/template/pods/hormozi-method/templates/paid-fundacao-tmpl.md +17 -0
  430. package/template/pods/hormozi-method/templates/paid-teste-escala-tmpl.md +17 -0
  431. package/template/pods/hormozi-method/templates/scaling-diagnostico-tmpl.md +17 -0
  432. package/template/pods/hormozi-method/templates/scaling-priorizacao-tmpl.md +17 -0
  433. package/template/pods/hormozi-method/templates/scaling-roadmap-tmpl.md +17 -0
  434. package/template/pods/hormozi-method/workflows/wf-full-launch-sequence.smoke.json +12 -0
  435. package/template/pods/hormozi-method/workflows/wf-full-launch-sequence.yaml +56 -0
  436. package/template/pods/hormozi-method/workflows/wf-grand-slam-offer.smoke.json +11 -0
  437. package/template/pods/hormozi-method/workflows/wf-grand-slam-offer.yaml +45 -0
  438. package/template/pods/hormozi-method/workflows/wf-lead-magnet-pipeline.smoke.json +9 -0
  439. package/template/pods/hormozi-method/workflows/wf-lead-magnet-pipeline.yaml +45 -0
  440. package/template/pods/hormozi-method/workflows/wf-money-model-design.smoke.json +11 -0
  441. package/template/pods/hormozi-method/workflows/wf-money-model-design.yaml +45 -0
  442. package/template/pods/hormozi-method/workflows/wf-paid-ads-campaign.smoke.json +9 -0
  443. package/template/pods/hormozi-method/workflows/wf-paid-ads-campaign.yaml +46 -0
  444. package/template/pods/hormozi-method/workflows/wf-scaling-diagnostic.smoke.json +11 -0
  445. package/template/pods/hormozi-method/workflows/wf-scaling-diagnostic.yaml +44 -0
  446. package/template/pods/squad-forge/README.md +53 -0
  447. package/template/pods/squad-forge/agents/forge-chief.md +31 -0
  448. package/template/pods/squad-forge/agents/mind-extractor.md +29 -0
  449. package/template/pods/squad-forge/agents/pod-critic.md +26 -0
  450. package/template/pods/squad-forge/config/heuristic-watcher-rubric.yaml +74 -0
  451. package/template/pods/squad-forge/knowledge/fidelity-rubric.yaml +176 -0
  452. package/template/pods/squad-forge/knowledge/g6-memory-check.md +17 -0
  453. package/template/pods/squad-forge/knowledge/memory-ingest.md +16 -0
  454. package/template/pods/squad-forge/knowledge/source-audit-rubric.yaml +60 -0
  455. package/template/pods/squad-forge/memory/README.md +16 -0
  456. package/template/pods/squad-forge/memory/learnings.consolidated.md +8 -0
  457. package/template/pods/squad-forge/memory/learnings.jsonl +0 -0
  458. package/template/pods/squad-forge/pod-spec.json +166 -0
  459. package/template/pods/squad-forge/squad-io.yaml +13 -0
  460. package/template/pods/squad-forge/tasks/pipeline.md +158 -0
  461. package/template/pods/squad-forge/templates/discovery-tmpl.md +40 -0
  462. package/template/pods/squad-forge/templates/generation-tmpl.md +18 -0
  463. package/template/pods/squad-forge/templates/red-team-tmpl.md +17 -0
  464. package/template/pods/squad-forge/templates/register-tmpl.md +17 -0
  465. package/template/pods/squad-forge/templates/workflow-tmpl.yaml +101 -0
  466. package/template/pods/squad-forge/workflows/wf-forge.smoke.json +12 -0
  467. package/template/pods/squad-forge/workflows/wf-forge.yaml +60 -0
@@ -0,0 +1,1252 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ /**
4
+ * pod-memory.cjs — motor determinístico do lifecycle de memória do pod (Story SF2.1).
5
+ *
6
+ * Implementa o modelo de DOIS NÍVEIS do ADR-003 / SPEC §G1, provado em código (não
7
+ * em prosa). É biblioteca standalone: SF2.2 (runner) e SF2.3 (fábrica) consomem via
8
+ * `require`; este motor NÃO integra com o runner nem com a fábrica.
9
+ *
10
+ * NÍVEL 1 — log cru: `pods/<pod>/memory/learnings.jsonl`
11
+ * append-only, uma linha JSON por evento. O AGENTE NUNCA LÊ ESTE ARQUIVO.
12
+ * Rotaciona para `memory/archive/learnings-{ts}.jsonl` além de M=500 entradas.
13
+ *
14
+ * NÍVEL 2 — consolidado curado: `pods/<pod>/memory/learnings.consolidated.md`
15
+ * top-N=30 heurísticas rankeadas por frequência × recência × impacto, dentro de
16
+ * um budget de ~1.500 tokens (bytes/4, mesma aproximação de validate-budgets.js).
17
+ * É o ÚNICO que o agente lê (via `getConsolidatedForBrief`).
18
+ *
19
+ * TRILHA AUDITÁVEL (SF11.4/ADR-022): `pods/<pod>/memory/learnings.hardened.md`
20
+ * detalhe íntegro das endurecidas (🔒), append-only, sem poda e sem teto. FORA do
21
+ * caminho de injeção — o agente NÃO lê este arquivo; o consolidado cita só uma
22
+ * referência AGREGADA (contagem por alvo). Não viaja no bundle do export.
23
+ *
24
+ * GARANTIAS CATEGÓRICAS (provadas na suíte pod-memory.test.cjs, não-fake-green):
25
+ * - O consolidado NUNCA excede top-N nem o budget de tokens.
26
+ * - ADMISSÃO ÚNICA (SF11.4b/ADR-023): nenhuma heurística ativa fica fora do
27
+ * consolidado sem entrar em `evicted` com `reason` — as DUAS vias de corte
28
+ * (teto de atenção TOP_N e alvo/teto de custo) passam pelo mesmo registro, e
29
+ * `evicted.length` é o TOTAL do que não foi injetado, nunca um subconjunto.
30
+ * - CRITICAL pinado NUNCA é despejado por poda do top-N nem por rotação do log cru.
31
+ * - Conflito é determinístico e reprodutível (mesmo input → mesmo vencedor).
32
+ * - Teto de pins ≤10 NUNCA é ultrapassado silenciosamente (sinaliza needsCuration).
33
+ * - `append` REJEITA `tier: "critical"` sem `attributed_by` válido.
34
+ *
35
+ * ── Anti-inflação do CRITICAL (v4, ADR-003 — as 3 travas, verificáveis em código) ──
36
+ * (i) CRITÉRIO ESTREITO — este motor NÃO decide se algo "é" critical. Critical =
37
+ * dano irreversível / segurança (operação destrutiva, perda de dados,
38
+ * vazamento, quebra de compliance), NUNCA "importante/útil/recorrente" (isso é
39
+ * peso no ranking normal). A decisão de conteúdo é de quem chama `append`
40
+ * (curadoria humana ou regra determinística upstream). O motor só valida a
41
+ * FORMA da atribuição — validar semântica em código exigiria LLM, e veto por
42
+ * LLM é proibido pela SPEC. Esta lista fechada é referência para o chamador.
43
+ * (ii) ATRIBUIÇÃO NÃO-DISCRICIONÁRIA — `attributed_by` é obrigatório e de enum
44
+ * fechado (`"human"` | `"deterministic_rule"`) para toda entrada critical.
45
+ * Nenhum caminho de código deixa o AGENTE em runtime marcar critical sem essa
46
+ * atribuição: `append` rejeita a entrada (não a rebaixa silenciosamente).
47
+ * (iii) TETO ≤10 — o conjunto pinado tem teto próprio; ao ultrapassar, `consolidate`
48
+ * devolve `{ needsCuration: true, pinnedCount, overflow }` e o excedente NÃO é
49
+ * escrito como pin permanente (fica retido no log cru + sinalizado, nunca
50
+ * descartado), até curadoria explícita rebaixar/fundir um pin existente.
51
+ *
52
+ * Node >= 18, sem dependências externas. Exporta a API para a suíte própria.
53
+ */
54
+
55
+ const fs = require('node:fs');
56
+ const path = require('node:path');
57
+
58
+ // ── Constantes do ADR-003 (decisão fechada; não reabrir nesta story) ─────────
59
+ // SF11.4b/ADR-023 (DF-SF11.4b-1, §C5) — PROPÓSITO DECLARADO de TOP_N: é **teto de
60
+ // ATENÇÃO**, não controle de custo. Custo se mede em BYTES e já é governado pelo
61
+ // alvo/teto abaixo; controlar custo duas vezes, em duas unidades, seria redundante — e
62
+ // foi essa ambiguidade que deixou o corte por TOP_N mudo por três ADRs. Medição que
63
+ // sustenta a leitura (ADR-023 §C5): com heurísticas curtas cabem 30 itens em 938 tok e
64
+ // caberiam >60 dentro do teto de 1.500 — uma lista que nenhum agente lê com proveito.
65
+ // O que TOP_N protege é a leitura do agente; o que o orçamento protege é o bolso do dono.
66
+ const TOP_N = 30; // teto de ATENÇÃO do conjunto ranqueado (exclui pinados)
67
+ const ROTATE_M = 500; // rotação do log cru além de M entradas
68
+ const CONSOLIDATE_K = 20; // cadência (disparo é do CALLER/SF2.2; exportado p/ ref)
69
+ const TOKEN_BUDGET = 1500; // TETO do framework p/ o consolidado (bytes/4) — ADR-003, intocado
70
+ // SF11.4b/ADR-023 (DF-SF11.4b-2) — ALVO de custo por pod, OPT-IN, declarado no manifesto
71
+ // que já existe (`pod-spec.json`). Não é limiar inventado: é a MEDIÇÃO do artefato no
72
+ // instante em que o dono aceitou a economia, com atribuição (`acceptedBy`/`acceptedAt`),
73
+ // versionada em git e reversível por ato explícito dele — o oráculo que DF4-5 (ADR-018)
74
+ // exige. Pod que não declara se comporta EXATAMENTE como antes (teto 1.500).
75
+ const POD_SPEC_FILE = 'pod-spec.json';
76
+ const MEMORY_COST_FIELD = 'memoryCost';
77
+ const PIN_CAP = 10; // teto do conjunto CRITICAL pinado (trava iii)
78
+ const DEFAULT_POD_ROOT = 'pods';
79
+
80
+ const TIER_NORMAL = 'normal';
81
+ const TIER_CRITICAL = 'critical';
82
+ const VALID_ATTRIBUTION = new Set(['human', 'deterministic_rule']);
83
+ // SF7.6/ADR-012 — registro de HARDENING: marca (append-only) que uma heurística foi
84
+ // promovida a enforcement físico (veto.assertion no wf). Nunca remove a heurística do
85
+ // log; `consolidate` a anota (🔒) e a EXCLUI do top-N ativo (orçamento de injeção é
86
+ // finito — não re-sugerir o que virou regra física).
87
+ const TYPE_HARDENING = 'hardening';
88
+ // SF10.10/ADR-012 Adendo (DF-SF10.10-1) — o ALVO do registro de hardening é uma UNIÃO
89
+ // DISCRIMINADA por `target.kind`:
90
+ // 'veto' → { pod, workflowId, step, assertion } (SF7.6, alvo original)
91
+ // 'knowledge' → { pod, file, sha_before, sha_after } (2º alvo: knowledge canônico)
92
+ // `kind` AUSENTE ⇒ 'veto': retrocompat dos registros JÁ EM DISCO, sem migração e sem
93
+ // reescrita do log (mesmo precedente do OK_PATTERN_LEGACY_RE — reconhecer o legado por
94
+ // convenção). `kind` DESCONHECIDO ⇒ fail-loud: nunca cai no ramo 'veto' por omissão.
95
+ // O registro NÃO embute o diff (tamanho fixo: dois shas + um nome) — registros de
96
+ // hardening nunca rotacionam e o consolidado alimenta um artefato orçado; o diff vive
97
+ // no git, que já versiona o arquivo de knowledge.
98
+ const HARDEN_KIND_VETO = 'veto';
99
+ const HARDEN_KIND_KNOWLEDGE = 'knowledge';
100
+ const HARDEN_KINDS = new Set([HARDEN_KIND_VETO, HARDEN_KIND_KNOWLEDGE]);
101
+ const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
102
+ // SF11.4/ADR-022 (DF-SF11.4-1) — o DETALHE de cada endurecida sai do artefato INJETADO
103
+ // e passa a viver aqui: append-only, sem poda, sem teto, fora do caminho de injeção
104
+ // (o agente lê SOMENTE learnings.consolidated.md — getConsolidatedForBrief continua
105
+ // lendo UM arquivo). No consolidado fica uma REFERÊNCIA AGREGADA nomeada e visível
106
+ // (ADR-021 DF-SF11.2-3: orçamento decide INLINE × REFERÊNCIA, nunca MANTER × DESCARTAR).
107
+ const HARDENED_DETAIL_FILE = 'learnings.hardened.md';
108
+ const HARDENED_DETAIL_REL = `memory/${HARDENED_DETAIL_FILE}`;
109
+ const HARDENED_DETAIL_BULLET = '- 🔒 ';
110
+
111
+ /**
112
+ * Discriminante do alvo de um registro de hardening. Ausente ⇒ 'veto' (legado).
113
+ * Desconhecido ⇒ THROW (fail-loud): tratar um kind não suportado como 'veto' leria
114
+ * `target.assertion` inexistente e derrubaria `consolidate` — o exato defeito que
115
+ * DF-SF10.10-1 fecha.
116
+ */
117
+ function hardeningKind(target) {
118
+ const kind = target && target.kind != null ? target.kind : HARDEN_KIND_VETO;
119
+ if (!HARDEN_KINDS.has(kind)) {
120
+ throw new Error(
121
+ `pod-memory: target.kind desconhecido ${JSON.stringify(kind)} — suportados: ${[...HARDEN_KINDS].join(', ')} ` +
122
+ '(ausente ⇒ "veto" por retrocompat; desconhecido NUNCA é tratado como "veto" — ADR-012 Adendo SF10.10/DF-SF10.10-1)',
123
+ );
124
+ }
125
+ return kind;
126
+ }
127
+ // SF8.3 — classe OK_PATTERN: auto-referência de fechamento limpo de workflow
128
+ // (buildOkLearning do runner). Fica no LOG CRU como trilha de auditoria de
129
+ // estabilidade, mas NUNCA compete por slot do top-N na consolidação — evidência
130
+ // de campo (triagem 2026-07-29): 26/122 entradas, slot #1 dos 3 consolidados
131
+ // com uso, crescendo a cada run limpo ("terminei e deu certo" não é heurística).
132
+ const TYPE_OK_PATTERN = 'ok_pattern';
133
+ // Reconhecimento LEGADO por assinatura estrutural: entradas de campo geradas antes
134
+ // da marcação por tipo não têm `type` — casam pelo template EXATO e único do
135
+ // produtor (pc-run-pod.cjs buildOkLearning). Complemento necessário para os
136
+ // consolidados de campo existentes auto-resolverem na próxima consolidação; o
137
+ // caminho canônico daqui em diante é o campo `type` (marcado na origem).
138
+ const OK_PATTERN_LEGACY_RE = /^workflow ".+" completou sem bloqueio$/;
139
+
140
+ /** Uma entrada é da classe ok_pattern? (tipo explícito OU assinatura legada.) */
141
+ function isOkPattern(r) {
142
+ if (!r || typeof r !== 'object') return false;
143
+ if (r.type === TYPE_OK_PATTERN) return true;
144
+ return r.type == null && OK_PATTERN_LEGACY_RE.test(String(r.trigger || ''));
145
+ }
146
+
147
+ // Tokens de negação p/ detecção determinística de conflito (heurística simples,
148
+ // documentada — NÃO NLP). Cobre PT-BR e EN. Ver detectConflict().
149
+ // Nota: "no" (PT = em+o) é deliberadamente OMITIDO para evitar falso-positivo;
150
+ // negação em EN usa "not"/"never"/"avoid".
151
+ const NEGATION_TOKENS = new Set([
152
+ 'não', 'nao', 'nunca', 'jamais', 'evite', 'evitar', 'sem',
153
+ 'never', 'avoid', 'not',
154
+ ]);
155
+
156
+ // ── Resolução de paths ───────────────────────────────────────────────────────
157
+ // `pod` pode ser o nome do pod (resolvido contra opts.root ?? 'pods') OU o caminho
158
+ // do diretório do pod (contém `memory/`). Isso mantém a assinatura da story
159
+ // (`append(pod, entry)`) e ao mesmo tempo é testável com um root temporário.
160
+ function podDirFor(pod, opts = {}) {
161
+ if (opts.podDir) return opts.podDir;
162
+ const root = opts.root || DEFAULT_POD_ROOT;
163
+ return path.join(root, pod);
164
+ }
165
+
166
+ function memoryPaths(pod, opts = {}) {
167
+ const base = path.join(podDirFor(pod, opts), 'memory');
168
+ const archiveDir = path.join(base, 'archive');
169
+ return {
170
+ dir: base,
171
+ raw: path.join(base, 'learnings.jsonl'),
172
+ consolidated: path.join(base, 'learnings.consolidated.md'),
173
+ // SF11.4/ADR-022 (DF-SF11.4-1): trilha auditável das endurecidas, FORA do caminho
174
+ // de injeção (o agente lê só o consolidado). Append-only, sem poda e sem teto.
175
+ hardenedDetail: path.join(base, HARDENED_DETAIL_FILE),
176
+ archiveDir,
177
+ conflicts: path.join(archiveDir, 'conflicts.jsonl'),
178
+ archivedEntries: path.join(archiveDir, 'archived-entries.jsonl'),
179
+ };
180
+ }
181
+
182
+ function ensureDir(dir) {
183
+ fs.mkdirSync(dir, { recursive: true });
184
+ }
185
+
186
+ // ── Alvo de custo declarado pelo dono (SF11.4b/ADR-023 DF-SF11.4b-2) ─────────
187
+ /**
188
+ * Lê `memoryCost` do `pod-spec.json` do pod. Retorna `null` quando o manifesto não
189
+ * existe ou não declara o campo — **a ausência não é defeito**: a trava é OPT-IN e
190
+ * inventar um número para quem não declarou é exatamente o que este desenho recusa.
191
+ *
192
+ * AUSÊNCIA TEM UMA SÓ FORMA: `ENOENT` (ADR-023 DF-SF11.4b-7, Emenda 1). Qualquer outro
193
+ * `errno` de leitura (EISDIR, EACCES, EIO, ELOOP…) significa "existe um manifesto e eu
194
+ * NÃO consegui lê-lo" — estado em que o alvo aceito pelo dono é DESCONHECIDO, não
195
+ * ausente — e LANÇA. Tratar falha de leitura como ausência devolvia o teto a
196
+ * `TOKEN_BUDGET` sem uma linha: o modo de falha que DF-SF11.4b-2 proíbe pelo nome.
197
+ *
198
+ * FAIL-LOUD DE FORMA: `memoryCost` PRESENTE e malformado LANÇA com mensagem acionável,
199
+ * nunca degrada em silêncio para `TOKEN_BUDGET`. Degradar seria reintroduzir "economia
200
+ * que evapora sem ninguém decidir" pela porta dos fundos (ADR-023 DF-SF11.4b-2).
201
+ * Mesmo padrão de `hardeningTargetLabel` (fail-loud de forma, DF-SF10.10-1).
202
+ *
203
+ * `acceptedBy` é EXIGIDO junto do número: o alvo só não é limiar arbitrário porque é
204
+ * uma medição ATRIBUÍDA a quem a aceitou (DF4-5/ADR-018 — "limiar sem oráculo é
205
+ * teatro"). Um `artifactTokens` sem dono é um número inventado com aparência de decisão.
206
+ */
207
+ function readMemoryCost(pod, opts = {}) {
208
+ const specPath = path.join(podDirFor(pod, opts), POD_SPEC_FILE);
209
+ let text;
210
+ // Um único `readFileSync` (sem o par `existsSync` + read): uma syscall a menos e sem a
211
+ // janela TOCTOU — a ausência vem do próprio erro, discriminada por `errno`.
212
+ try {
213
+ text = fs.readFileSync(specPath, 'utf8');
214
+ } catch (e) {
215
+ if (e && e.code === 'ENOENT') return null; // manifesto não existe ⇒ o pod não optou
216
+ throw new Error(
217
+ `pod-memory: ${specPath} EXISTE mas não pôde ser lido (errno ${(e && e.code) || 'desconhecido'}: ${e && e.message}) — ` +
218
+ `o motor lê "${MEMORY_COST_FIELD}" deste manifesto para aplicar o alvo de custo da memória, e com o alvo ` +
219
+ `DESCONHECIDO o teto NUNCA volta em silêncio para ${TOKEN_BUDGET} tok. Corrija o acesso ao arquivo ` +
220
+ '(permissão, tipo do inode, path) — ausência legítima é o arquivo NÃO existir (ADR-023 DF-SF11.4b-7).',
221
+ );
222
+ }
223
+ let spec;
224
+ try {
225
+ spec = JSON.parse(text);
226
+ } catch (e) {
227
+ throw new Error(
228
+ `pod-memory: ${specPath} não é JSON válido (${e.message}) — o motor lê "${MEMORY_COST_FIELD}" ` +
229
+ 'deste manifesto para aplicar o alvo de custo da memória; corrija o arquivo (ADR-023 DF-SF11.4b-2).',
230
+ );
231
+ }
232
+ const mc = spec && typeof spec === 'object' ? spec[MEMORY_COST_FIELD] : undefined;
233
+ if (mc == null) return null;
234
+ const where = `${specPath} → "${MEMORY_COST_FIELD}"`;
235
+ const bad = (why) => new Error(
236
+ `pod-memory: ${where} malformado — ${why}. O alvo de custo NUNCA degrada em silêncio para ` +
237
+ `${TOKEN_BUDGET} tok: corrija o manifesto ou remova o campo (a ausência é legítima e volta ao ` +
238
+ `teto do framework). Forma esperada: {"artifactTokens": <inteiro 1..${TOKEN_BUDGET}>, ` +
239
+ '"perRunTokens": <inteiro>, "steps": <inteiro>, "acceptedBy": "<quem aceitou>", ' +
240
+ '"acceptedAt": "<data>"} (ADR-023 DF-SF11.4b-2).',
241
+ );
242
+ if (typeof mc !== 'object' || Array.isArray(mc)) throw bad(`esperado objeto, recebido ${JSON.stringify(mc)}`);
243
+ const t = mc.artifactTokens;
244
+ if (typeof t !== 'number' || !Number.isInteger(t)) throw bad(`"artifactTokens" deve ser inteiro (recebido ${JSON.stringify(t)})`);
245
+ if (t <= 0) throw bad(`"artifactTokens" deve ser > 0 (recebido ${t})`);
246
+ if (t > TOKEN_BUDGET) throw bad(`"artifactTokens" (${t}) excede o teto do framework TOKEN_BUDGET=${TOKEN_BUDGET}`);
247
+ if (typeof mc.acceptedBy !== 'string' || mc.acceptedBy.trim() === '') {
248
+ throw bad('"acceptedBy" é obrigatório e não-vazio — o alvo é uma medição ACEITA e ATRIBUÍDA, ' +
249
+ 'não um número escolhido (DF4-5: limiar sem oráculo é teatro)');
250
+ }
251
+ if (mc.steps != null && (!Number.isInteger(mc.steps) || mc.steps <= 0)) {
252
+ throw bad(`"steps" (opcional) deve ser inteiro > 0 quando presente (recebido ${JSON.stringify(mc.steps)})`);
253
+ }
254
+ return {
255
+ artifactTokens: t,
256
+ steps: mc.steps == null ? null : mc.steps,
257
+ acceptedBy: mc.acceptedBy,
258
+ acceptedAt: mc.acceptedAt == null ? null : String(mc.acceptedAt),
259
+ specPath,
260
+ };
261
+ }
262
+
263
+ /**
264
+ * Teto EFETIVO da etapa de admissão: `min(alvo declarado, TOKEN_BUDGET)`. O `min` é
265
+ * explícito (e não só a validação de forma) porque o teto do framework é piso de
266
+ * segurança do desenho: nenhum manifesto pode ampliar o orçamento do consolidado.
267
+ * @returns {{ tokens: number, target: object|null }}
268
+ */
269
+ function effectiveTokenBudget(pod, opts = {}) {
270
+ const target = readMemoryCost(pod, opts);
271
+ if (!target) return { tokens: TOKEN_BUDGET, target: null };
272
+ return { tokens: Math.min(target.artifactTokens, TOKEN_BUDGET), target };
273
+ }
274
+
275
+ // ── Normalização (dedup, conflito) ───────────────────────────────────────────
276
+ function normalizeText(s) {
277
+ return String(s == null ? '' : s)
278
+ .toLowerCase()
279
+ .normalize('NFD')
280
+ .replace(/[\u0300-\u036f]/g, '') // remove acentos p/ casar 'nao'/'não'
281
+ .replace(/[^\w\s]/g, ' ') // pontuação → espaço
282
+ .replace(/\s+/g, ' ')
283
+ .trim();
284
+ }
285
+
286
+ function evidenceCount(entry) {
287
+ const ev = entry.evidence;
288
+ if (ev == null) return 1;
289
+ if (typeof ev === 'number') return ev;
290
+ if (typeof ev.count === 'number') return ev.count;
291
+ return 1;
292
+ }
293
+
294
+ function impactOf(entry) {
295
+ return typeof entry.impact === 'number' && entry.impact > 0 ? entry.impact : 1;
296
+ }
297
+
298
+ // chave de dedup: mesmo trigger + mesma recomendação (normalizados) → mesma heurística
299
+ function dedupKey(entry) {
300
+ return `${normalizeText(entry.trigger)}||${normalizeText(entry.recommendation)}`;
301
+ }
302
+
303
+ // chave de conflito: mesmo trigger, IGNORANDO polaridade da recomendação
304
+ function conflictKey(entry) {
305
+ return `${normalizeText(entry.trigger)}||${stripNegation(entry.recommendation)}`;
306
+ }
307
+
308
+ // remove tokens de negação iniciais/internos p/ obter o "núcleo" da recomendação
309
+ function stripNegation(rec) {
310
+ const words = normalizeText(rec).split(' ').filter(Boolean);
311
+ return words.filter((w) => !NEGATION_TOKENS.has(w)).join(' ');
312
+ }
313
+
314
+ function polarity(rec) {
315
+ // true = negativa (contém token de negação); false = afirmativa
316
+ return normalizeText(rec).split(' ').some((w) => NEGATION_TOKENS.has(w));
317
+ }
318
+
319
+ // ── (a) append — log cru append-only ─────────────────────────────────────────
320
+ /**
321
+ * Grava UMA linha JSON no learnings.jsonl. Append-only: nunca reescreve linhas
322
+ * anteriores (rotação é operação SEPARADA e explícita — ver rotate()).
323
+ * Rejeita `tier: "critical"` sem `attributed_by` válido (travas i/ii).
324
+ * @returns {{ ok: true, entry }} ou lança Error em entrada inválida.
325
+ */
326
+ function append(pod, entry, opts = {}) {
327
+ if (!entry || typeof entry !== 'object') {
328
+ throw new Error('pod-memory.append: entry deve ser um objeto');
329
+ }
330
+ if (!entry.trigger || !entry.recommendation) {
331
+ throw new Error('pod-memory.append: entry exige `trigger` e `recommendation`');
332
+ }
333
+ const tier = entry.tier || TIER_NORMAL;
334
+ if (tier !== TIER_NORMAL && tier !== TIER_CRITICAL) {
335
+ throw new Error(`pod-memory.append: tier inválido "${tier}" (use "normal"|"critical")`);
336
+ }
337
+ // Trava (ii): critical exige atribuição não-discricionária de enum fechado.
338
+ if (tier === TIER_CRITICAL && !VALID_ATTRIBUTION.has(entry.attributed_by)) {
339
+ throw new Error(
340
+ 'pod-memory.append: tier "critical" REJEITADO — `attributed_by` deve ser ' +
341
+ '"human" ou "deterministic_rule" (anti-inflação v4, trava ii). ' +
342
+ 'O agente em runtime não pode marcar critical sem atribuição.',
343
+ );
344
+ }
345
+ // SF8.3: `type` de classificação é PRESERVADO no registro (ex.: ok_pattern —
346
+ // marcado na origem pelo runner); "hardening" tem escritor próprio (appendHardening)
347
+ // e é rejeitado aqui para não fabricar marca de promoção sem ref/target/aprovação.
348
+ if (entry.type === TYPE_HARDENING) {
349
+ throw new Error('pod-memory.append: type "hardening" só via appendHardening (ADR-012)');
350
+ }
351
+
352
+ const record = {
353
+ id: entry.id || `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
354
+ ts: typeof entry.ts === 'number' ? entry.ts : Date.parse(entry.ts) || Date.now(),
355
+ trigger: entry.trigger,
356
+ recommendation: entry.recommendation,
357
+ evidence: entry.evidence != null ? entry.evidence : { count: 1 },
358
+ tier,
359
+ attributed_by: tier === TIER_CRITICAL ? entry.attributed_by : (entry.attributed_by || null),
360
+ source_run_id: entry.source_run_id || null,
361
+ };
362
+ if (entry.obsolete === true) record.obsolete = true;
363
+ if (entry.impact != null) record.impact = entry.impact;
364
+ if (entry.contradicts != null) record.contradicts = entry.contradicts;
365
+ if (entry.type != null) record.type = entry.type; // SF8.3 — classe preservada (ex.: ok_pattern)
366
+
367
+ const paths = memoryPaths(pod, opts);
368
+ ensureDir(paths.dir);
369
+ fs.appendFileSync(paths.raw, `${JSON.stringify(record)}\n`);
370
+ return { ok: true, entry: record };
371
+ }
372
+
373
+ // ── (SF7.6/ADR-012) appendHardening — marcação append-only de promoção ────────
374
+ /**
375
+ * Grava UMA linha JSON `{type:"hardening", ref, target, approved_by, timestamp}` no
376
+ * learnings.jsonl (append-only — a heurística original NUNCA é editada/removida).
377
+ * `ref` = dedupKey da heurística promovida; `target` é uma UNIÃO DISCRIMINADA por
378
+ * `kind` (DF-SF10.10-1):
379
+ * kind ausente/'veto' → { pod, workflowId, step, assertion:{type,...} } (ADR-012)
380
+ * kind 'knowledge' → { pod, file, sha_before, sha_after } — sem diff embutido
381
+ * Rejeita (fail-loud) registro sem ref/target/approved_by válidos, e `kind`
382
+ * desconhecido (nunca degrada para o ramo 'veto').
383
+ * @returns {{ ok: true, record }}
384
+ */
385
+ function appendHardening(pod, mark, opts = {}) {
386
+ if (!mark || typeof mark !== 'object') {
387
+ throw new Error('pod-memory.appendHardening: registro deve ser um objeto');
388
+ }
389
+ if (typeof mark.ref !== 'string' || mark.ref.trim() === '') {
390
+ throw new Error('pod-memory.appendHardening: `ref` (dedupKey da heurística) é obrigatório');
391
+ }
392
+ const t = mark.target;
393
+ if (!t || typeof t !== 'object' || Array.isArray(t)) {
394
+ throw new Error('pod-memory.appendHardening: `target` é obrigatório (objeto)');
395
+ }
396
+ const kind = hardeningKind(t); // fail-loud em kind desconhecido
397
+ let target;
398
+ if (kind === HARDEN_KIND_KNOWLEDGE) {
399
+ if (typeof t.file !== 'string' || t.file.trim() === '') {
400
+ throw new Error(
401
+ 'pod-memory.appendHardening: target kind "knowledge" exige `file` (nome do arquivo sob knowledge/)',
402
+ );
403
+ }
404
+ if (/[\\/]/.test(t.file) || t.file.includes('..')) {
405
+ throw new Error(
406
+ `pod-memory.appendHardening: target.file ${JSON.stringify(t.file)} deve ser nome SIMPLES sob knowledge/ ` +
407
+ '(sem separador de path nem "..") — o registro é marca de auditoria, não caminho arbitrário',
408
+ );
409
+ }
410
+ for (const k of ['sha_before', 'sha_after']) {
411
+ if (!SHA256_HEX_RE.test(String(t[k] == null ? '' : t[k]))) {
412
+ throw new Error(
413
+ `pod-memory.appendHardening: target kind "knowledge" exige \`${k}\` sha256 hex de 64 ` +
414
+ `(recebido: ${JSON.stringify(t[k])}) — DF-SF10.10-1`,
415
+ );
416
+ }
417
+ }
418
+ target = {
419
+ pod: t.pod || pod,
420
+ kind: HARDEN_KIND_KNOWLEDGE,
421
+ file: t.file,
422
+ sha_before: t.sha_before,
423
+ sha_after: t.sha_after,
424
+ };
425
+ } else {
426
+ // Ramo 'veto' — validação IDÊNTICA à de SF7.6, byte a byte (zero mudança de
427
+ // comportamento no alvo já em produção).
428
+ if (!t.workflowId || !t.step ||
429
+ !t.assertion || typeof t.assertion !== 'object' || typeof t.assertion.type !== 'string') {
430
+ throw new Error(
431
+ 'pod-memory.appendHardening: `target` exige { workflowId, step, assertion:{type,...} } (ADR-012)',
432
+ );
433
+ }
434
+ // `kind` só é MATERIALIZADO no registro quando veio explícito: o formato legado
435
+ // (sem `kind`) continua sendo o emitido por default — nada em disco muda de forma.
436
+ target = {
437
+ pod: t.pod || pod,
438
+ ...(t.kind != null ? { kind: HARDEN_KIND_VETO } : {}),
439
+ workflowId: t.workflowId,
440
+ step: t.step,
441
+ assertion: t.assertion,
442
+ };
443
+ }
444
+ if (typeof mark.approved_by !== 'string' || mark.approved_by.trim() === '') {
445
+ throw new Error(
446
+ 'pod-memory.appendHardening: `approved_by` é obrigatório — promoção sem aprovação registrada não marca (padrão --approved-by)',
447
+ );
448
+ }
449
+ const record = {
450
+ type: TYPE_HARDENING,
451
+ ref: mark.ref,
452
+ target,
453
+ approved_by: mark.approved_by,
454
+ timestamp: mark.timestamp || new Date().toISOString(),
455
+ };
456
+ const paths = memoryPaths(pod, opts);
457
+ ensureDir(paths.dir);
458
+ fs.appendFileSync(paths.raw, `${JSON.stringify(record)}\n`);
459
+ return { ok: true, record };
460
+ }
461
+
462
+ /** Registros de hardening do log cru (ordenados; o último por ref vence). */
463
+ function hardeningByRef(rawRecords) {
464
+ const map = new Map();
465
+ for (const r of rawRecords) {
466
+ if (r && r.type === TYPE_HARDENING && typeof r.ref === 'string') {
467
+ const prev = map.get(r.ref);
468
+ if (!prev || String(r.timestamp || '') >= String(prev.timestamp || '')) map.set(r.ref, r);
469
+ }
470
+ }
471
+ return map;
472
+ }
473
+
474
+ /**
475
+ * Separa registros de hardening E entradas ok_pattern das heurísticas (uso interno).
476
+ * SF8.3: ok_pattern sai do pool ANTES de merge/conflito/ranking — mesmo padrão dos
477
+ * hardenings; ambos permanecem no log cru (append-only, auditoria intacta).
478
+ */
479
+ function splitRaw(rawRecords) {
480
+ const hardenings = [];
481
+ const okPatterns = [];
482
+ const entries = [];
483
+ for (const r of rawRecords) {
484
+ if (r && r.type === TYPE_HARDENING) hardenings.push(r);
485
+ else if (isOkPattern(r)) okPatterns.push(r);
486
+ else entries.push(r);
487
+ }
488
+ return { hardenings, okPatterns, entries };
489
+ }
490
+
491
+ // ── Leitura do log cru (uso interno — nunca exposto ao agente) ────────────────
492
+ function readRawEntries(rawPath) {
493
+ if (!fs.existsSync(rawPath)) return [];
494
+ return fs.readFileSync(rawPath, 'utf8')
495
+ .split('\n')
496
+ .map((l) => l.trim())
497
+ .filter(Boolean)
498
+ .map((l) => {
499
+ try {
500
+ return JSON.parse(l);
501
+ } catch {
502
+ return null;
503
+ }
504
+ })
505
+ .filter(Boolean);
506
+ }
507
+
508
+ // ── merge idempotente de um .jsonl de auditoria (conflitos, arquivados) ────────
509
+ // Funde os registros já em disco com os novos, dedup por `keyFn`, ordena pela chave
510
+ // (determinístico) e reescreve. Novos sobrescrevem homônimos; antigos ausentes dos
511
+ // novos (ex.: fonte já rotacionada) são PRESERVADOS. Bytes idênticos em reexecução.
512
+ function mergeJsonlByKey(filePath, newRecords, keyFn) {
513
+ const existing = fs.existsSync(filePath)
514
+ ? fs.readFileSync(filePath, 'utf8').split('\n').map((l) => l.trim()).filter(Boolean)
515
+ .map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean)
516
+ : [];
517
+ const byKey = new Map();
518
+ for (const r of existing) byKey.set(keyFn(r), r);
519
+ for (const r of newRecords) byKey.set(keyFn(r), r); // novos vencem
520
+ const sorted = [...byKey.entries()]
521
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
522
+ .map(([, r]) => r);
523
+ fs.writeFileSync(filePath, sorted.map((r) => `${JSON.stringify(r)}\n`).join(''));
524
+ }
525
+
526
+ // ── dedup/merge — agrupa por dedupKey, soma ocorrências/evidência ─────────────
527
+ function mergeEntries(entries) {
528
+ const byKey = new Map();
529
+ for (const e of entries) {
530
+ const key = dedupKey(e);
531
+ const prev = byKey.get(key);
532
+ if (!prev) {
533
+ byKey.set(key, {
534
+ ...e,
535
+ occurrences: 1,
536
+ _evidence: evidenceCount(e),
537
+ _obsolete: e.obsolete === true,
538
+ _ids: [e.id],
539
+ });
540
+ } else {
541
+ prev.occurrences += 1;
542
+ prev._evidence += evidenceCount(e);
543
+ prev._ids.push(e.id);
544
+ // mantém a mais recente como representante (recência + estado obsolete atual)
545
+ if ((e.ts || 0) >= (prev.ts || 0)) {
546
+ prev.ts = e.ts;
547
+ prev.recommendation = e.recommendation;
548
+ prev.tier = e.tier || prev.tier;
549
+ prev.attributed_by = e.attributed_by || prev.attributed_by;
550
+ prev._obsolete = e.obsolete === true;
551
+ if (e.impact != null) prev.impact = e.impact;
552
+ if (e.contradicts != null) prev.contradicts = e.contradicts;
553
+ }
554
+ }
555
+ }
556
+ return [...byKey.values()];
557
+ }
558
+
559
+ // ── (c) detecção/resolução de conflito ────────────────────────────────────────
560
+ // Heurística determinística documentada (NÃO NLP): duas heurísticas conflitam se
561
+ // (1) campo estruturado explícito `contradicts: <id>` casa, OU
562
+ // (2) mesmo trigger normalizado + mesmo núcleo de recomendação (sem tokens de
563
+ // negação) + POLARIDADE oposta (uma afirma, a outra nega a mesma ação).
564
+ // Desempate ORDENADO (SPEC §G1): 1º `evidence.count` maior; 2º `ts` mais recente.
565
+ function detectConflicts(merged) {
566
+ const conflicts = [];
567
+ const loserKeys = new Set();
568
+ if (merged.length === 0) return { conflicts, loserKeys };
569
+
570
+ // índice de qualquer id (inclui os fundidos em _ids) → entrada representante
571
+ const idIndex = new Map();
572
+ for (const m of merged) {
573
+ for (const id of (m._ids || [m.id])) idIndex.set(id, m);
574
+ }
575
+
576
+ // grafo de conflito por índice em `merged`; arestas ligam pares em conflito
577
+ const idxOf = new Map(merged.map((m, i) => [m, i]));
578
+ const adj = merged.map(() => new Set());
579
+ const link = (a, b) => {
580
+ const i = idxOf.get(a);
581
+ const j = idxOf.get(b);
582
+ if (i !== undefined && j !== undefined && i !== j) {
583
+ adj[i].add(j);
584
+ adj[j].add(i);
585
+ }
586
+ };
587
+
588
+ // (1) contradição EXPLÍCITA via campo estruturado `contradicts: <id>` (cruza
589
+ // cores/triggers diferentes — não depende da heurística de negação).
590
+ for (const m of merged) {
591
+ if (m.contradicts != null && idIndex.has(m.contradicts)) {
592
+ link(m, idIndex.get(m.contradicts));
593
+ }
594
+ }
595
+
596
+ // (2) POLARIDADE oposta dentro do mesmo conflictKey (mesmo trigger + mesmo
597
+ // núcleo de recomendação, uma afirma e a outra nega).
598
+ const groups = new Map();
599
+ for (const m of merged) {
600
+ const k = conflictKey(m);
601
+ if (!groups.has(k)) groups.set(k, []);
602
+ groups.get(k).push(m);
603
+ }
604
+ for (const key of [...groups.keys()].sort()) {
605
+ const group = groups.get(key);
606
+ if (group.length < 2) continue;
607
+ const positives = group.filter((g) => !polarity(g.recommendation));
608
+ const negatives = group.filter((g) => polarity(g.recommendation));
609
+ for (const p of positives) for (const n of negatives) link(p, n);
610
+ }
611
+
612
+ const lastId = (m) => (m._ids ? m._ids[m._ids.length - 1] : m.id);
613
+ const record = (winner, loser, bothCritical, resolution) => {
614
+ conflicts.push({
615
+ // ts DETERMINÍSTICO derivado do vencedor (M1) — não do relógio; garante
616
+ // byte-idempotência de conflicts.jsonl (não Date.now()).
617
+ ts: winner.ts || 0,
618
+ trigger: winner.trigger,
619
+ winner_id: lastId(winner),
620
+ winner_recommendation: winner.recommendation,
621
+ loser_id: lastId(loser),
622
+ loser_recommendation: loser.recommendation,
623
+ criterion: bothCritical ? 'evidence.count desc, ts desc' : 'critical-protected (piso categórico)',
624
+ both_critical: bothCritical,
625
+ resolution,
626
+ });
627
+ };
628
+
629
+ // componentes conexos. Iteração em ordem de índice = reprodutível.
630
+ const seen = new Array(merged.length).fill(false);
631
+ let criticalConflictNeedsCuration = false;
632
+ for (let i = 0; i < merged.length; i++) {
633
+ if (seen[i] || adj[i].size === 0) continue;
634
+ const comp = [];
635
+ const stack = [i];
636
+ while (stack.length) {
637
+ const x = stack.pop();
638
+ if (seen[x]) continue;
639
+ seen[x] = true;
640
+ comp.push(merged[x]);
641
+ for (const y of adj[x]) if (!seen[y]) stack.push(y);
642
+ }
643
+
644
+ // ── H1 fix: CRITICAL é PISO CATEGÓRICO (SPEC §G1 item e) ──────────────────
645
+ // Um CRITICAL ativo SÓ pode perder conflito para OUTRO CRITICAL. Conflito
646
+ // CRITICAL×NORMAL NUNCA despeja o pin: o lado normal é rebaixado, o pin
647
+ // permanece, o conflito é registrado e sinaliza needsCuration (a resolução é
648
+ // curadoria humana, jamais automática).
649
+ const activeCriticals = comp.filter(isActiveCritical);
650
+ const nonCriticals = comp.filter((m) => !isActiveCritical(m));
651
+
652
+ if (activeCriticals.length === 0) {
653
+ // conflito puramente NORMAL — resolução por evidência (comportamento original)
654
+ const ranked = [...comp].sort(compareConflict);
655
+ const winner = ranked[0];
656
+ for (let k = 1; k < ranked.length; k++) {
657
+ loserKeys.add(dedupKey(ranked[k]));
658
+ record(winner, ranked[k], false, 'evidence');
659
+ }
660
+ continue;
661
+ }
662
+
663
+ // Há ≥1 CRITICAL ativo no componente.
664
+ // (a) crit-vs-crit: entre os CRITICAIS, resolução por evidência é PERMITIDA
665
+ // (SPEC: "resolução de conflito entre DOIS CRITICALs"). Top critical vence;
666
+ // os demais criticais perdem — mas só entre eles, nunca para um normal.
667
+ const rankedCriticals = [...activeCriticals].sort(compareConflict);
668
+ const survivingCritical = rankedCriticals[0];
669
+ for (let k = 1; k < rankedCriticals.length; k++) {
670
+ loserKeys.add(dedupKey(rankedCriticals[k]));
671
+ record(survivingCritical, rankedCriticals[k], true, 'evidence');
672
+ }
673
+ // (b) todo NORMAL em conflito com um pin perde; o pin PERMANECE. Sinaliza
674
+ // curadoria humana (needsCuration), nunca resolvido automaticamente.
675
+ for (const nc of nonCriticals) {
676
+ loserKeys.add(dedupKey(nc));
677
+ record(survivingCritical, nc, false, 'critical-protected');
678
+ criticalConflictNeedsCuration = true;
679
+ }
680
+ }
681
+ return { conflicts, loserKeys, needsCuration: criticalConflictNeedsCuration };
682
+ }
683
+
684
+ // CRITICAL ATIVO = tier critical + atribuição válida + não obsoleto (piso do pin)
685
+ function isActiveCritical(m) {
686
+ return m.tier === TIER_CRITICAL && VALID_ATTRIBUTION.has(m.attributed_by) && !m._obsolete;
687
+ }
688
+
689
+ // vencedor primeiro: maior evidência; empate → mais recente; empate → id estável
690
+ function compareConflict(a, b) {
691
+ if (b._evidence !== a._evidence) return b._evidence - a._evidence;
692
+ if ((b.ts || 0) !== (a.ts || 0)) return (b.ts || 0) - (a.ts || 0);
693
+ return String(a.id).localeCompare(String(b.id));
694
+ }
695
+
696
+ // ── rerank determinístico: frequência × recência × impacto ────────────────────
697
+ function scoreEntries(entries) {
698
+ const tsList = entries.map((e) => e.ts || 0);
699
+ const minTs = Math.min(...tsList, 0);
700
+ const maxTs = Math.max(...tsList, 1);
701
+ const span = maxTs - minTs || 1;
702
+ for (const e of entries) {
703
+ const freq = Math.max(1, e.occurrences || 1);
704
+ const recency = 1 + (((e.ts || 0) - minTs) / span); // [1,2], newer = maior
705
+ const impact = impactOf(e);
706
+ e._score = freq * recency * impact;
707
+ }
708
+ // ordena por score desc; desempate reprodutível: ts desc, depois id asc
709
+ entries.sort((a, b) => {
710
+ if (b._score !== a._score) return b._score - a._score;
711
+ if ((b.ts || 0) !== (a.ts || 0)) return (b.ts || 0) - (a.ts || 0);
712
+ return String(a.id).localeCompare(String(b.id));
713
+ });
714
+ return entries;
715
+ }
716
+
717
+ // ── render do consolidado (.md) ───────────────────────────────────────────────
718
+ function renderConsolidated(pod, pins, ranked, hardened = []) {
719
+ const lines = [];
720
+ lines.push(`# Memória consolidada — ${pod}`);
721
+ lines.push('');
722
+ lines.push('> Curado por pod-memory.cjs (SF2.1). Único artefato de memória lido pelo agente.');
723
+ lines.push('> Log cru (learnings.jsonl) NÃO é lido pelo agente — auditoria apenas.');
724
+ lines.push('');
725
+ if (pins.length > 0) {
726
+ lines.push('## CRITICAL (pinado)');
727
+ lines.push('');
728
+ lines.push('_Piso categórico — nunca despejado por poda/rotação. Sai só por curadoria._');
729
+ lines.push('');
730
+ for (const p of pins) {
731
+ lines.push(`- **[${p.trigger}]** ${p.recommendation} _(fonte: ${p.attributed_by})_`);
732
+ }
733
+ lines.push('');
734
+ }
735
+ lines.push('## Heurísticas (top-N)');
736
+ lines.push('');
737
+ if (ranked.length === 0) {
738
+ lines.push('_Nenhuma heurística consolidada ainda._');
739
+ } else {
740
+ for (const r of ranked) {
741
+ lines.push(`- **[${r.trigger}]** ${r.recommendation} _(x${r.occurrences})_`);
742
+ }
743
+ }
744
+ lines.push('');
745
+ // SF7.6/ADR-012 — endurecidas: FORA do top-N ativo (não competem por injeção),
746
+ // NUNCA removidas. SF11.4/ADR-022 (DF-SF11.4-1) — e agora também fora do INLINE:
747
+ // o detalhe (asserção, aprovador, momento) migrou para `memory/learnings.hardened.md`
748
+ // e aqui fica uma REFERÊNCIA AGREGADA. Motivo medido: a linha inline de cada
749
+ // endurecida é um PONTEIRO para um arquivo de knowledge que já é leitura obrigatória
750
+ // da etapa (ADR-021) — era 53,5% do artefato orçado e 40,3% do brief da run, gastos
751
+ // repetindo o que o agente já é obrigado a ler. ADR-021/DF-SF11.2-3 rege o caminho:
752
+ // rebaixar a REFERÊNCIA nomeada e visível, nunca DESCARTAR em silêncio.
753
+ if (hardened.length > 0) {
754
+ const counts = new Map();
755
+ for (const h of hardened) {
756
+ // hardeningTargetLabel valida a FORMA do registro (fail-loud, DF-SF10.10-1):
757
+ // um `knowledge/undefined` somado num agregado seria pior que inline (some no total).
758
+ const label = hardeningTargetLabel(h);
759
+ counts.set(label, (counts.get(label) || 0) + 1);
760
+ }
761
+ const summary = [...counts.entries()]
762
+ .sort((a, b) => String(a[0]).localeCompare(String(b[0])))
763
+ .map(([label, n]) => `${label} (${n})`)
764
+ .join(', ');
765
+ lines.push('## Endurecidas (🔒 → enforcement)');
766
+ lines.push('');
767
+ lines.push(
768
+ `_${hardened.length} heurística(s) já viraram forma canônica do pod — enforcement substitui injeção: ${summary}. ` +
769
+ 'O knowledge citado é LEITURA OBRIGATÓRIA da etapa e o veto citado bloqueia em disco; por isso o texto delas não é reinjetado aqui._',
770
+ );
771
+ lines.push(`_Detalhe auditável (asserção, aprovador, momento): ${HARDENED_DETAIL_REL} — append-only, nunca podado._`);
772
+ lines.push('');
773
+ }
774
+ return lines.join('\n');
775
+ }
776
+
777
+ /**
778
+ * Rótulo do ALVO de um registro de hardening — `knowledge/<file>` ou `veto <tipo> em
779
+ * <etapa>`. Fonte ÚNICA do rótulo para os dois consumidores (a referência agregada do
780
+ * consolidado e a trilha auditável), e ponto ÚNICO do fail-loud de forma: registro
781
+ * malformado LANÇA com mensagem acionável em vez de renderizar calado
782
+ * "knowledge/undefined" (SF10.10/DF-SF10.10-1; `appendHardening` já rejeita na origem —
783
+ * aqui é a simetria para log editado à mão).
784
+ */
785
+ function hardeningTargetLabel(h) {
786
+ const t = (h.mark && h.mark.target) || {};
787
+ const kind = hardeningKind(t); // ausente ⇒ 'veto'; desconhecido ⇒ throw
788
+ if (kind === HARDEN_KIND_KNOWLEDGE) {
789
+ if (typeof t.file !== 'string' || t.file.trim() === '' ||
790
+ /[\\/]/.test(t.file) || t.file.includes('..')) {
791
+ throw new Error(
792
+ `pod-memory: registro de hardening kind "knowledge" malformado (ref: ${h.mark.ref}) — ` +
793
+ `target.file deve ser nome simples sob knowledge/ (recebido: ${JSON.stringify(t.file)}). ` +
794
+ 'Registro corrompido no log cru; corrija a origem (appendHardening valida).',
795
+ );
796
+ }
797
+ return `knowledge/${t.file}`;
798
+ }
799
+ if (!t.assertion || typeof t.assertion.type !== 'string' || !t.step) {
800
+ throw new Error(
801
+ `pod-memory: registro de hardening kind "veto" malformado (ref: ${h.mark.ref}) — ` +
802
+ 'exige target.step e target.assertion.type. Registro corrompido no log cru; corrija a origem (appendHardening valida).',
803
+ );
804
+ }
805
+ return `veto ${t.assertion.type} em ${t.step}`;
806
+ }
807
+
808
+ /**
809
+ * (SF11.4/ADR-022 DF-SF11.4-1) Trilha auditável das endurecidas — o DETALHE que saiu do
810
+ * artefato injetado. Linha por endurecida, no MESMO formato que o consolidado emitia
811
+ * inline (nada de formato novo de dado — ADR-012:80: o que é de tamanho não-limitado
812
+ * pertence ao git, não ao artefato orçado).
813
+ */
814
+ function renderHardenedDetail(pod, hardened) {
815
+ const lines = [];
816
+ lines.push(`# Endurecidas (🔒) — trilha auditável — ${pod}`);
817
+ lines.push('');
818
+ lines.push('> Escrita por pod-memory.cjs (SF11.4/ADR-022). APPEND-ONLY: sem poda, sem teto.');
819
+ lines.push('> ARTEFATO DE AUDITORIA — fora do caminho de injeção: o agente NÃO lê este arquivo');
820
+ lines.push('> (lê só learnings.consolidated.md, que cita a referência agregada). Não viaja no bundle do export.');
821
+ lines.push('');
822
+ for (const h of hardened) {
823
+ lines.push(
824
+ `${HARDENED_DETAIL_BULLET}**[${h.entry.trigger}]** ${h.entry.recommendation} — endurecida → ${hardeningTargetLabel(h)} _(por ${h.mark.approved_by}, ${h.mark.timestamp})_`,
825
+ );
826
+ }
827
+ lines.push('');
828
+ return lines.join('\n');
829
+ }
830
+
831
+ function estTokens(str) {
832
+ return Math.ceil(Buffer.byteLength(str, 'utf8') / 4);
833
+ }
834
+
835
+ // ── computeConsolidation — o CÁLCULO da consolidação, SEM ESCRITA ─────────────
836
+ /**
837
+ * (SF10.10) Extraído de `consolidate` para que a listagem de candidatos (DF-SF10.10-3)
838
+ * use EXATAMENTE o mesmo conjunto curado, sem duplicar regra e sem escrever nada.
839
+ * READ-ONLY: lê o log cru e devolve o estado curado + o .md renderizado; quem escreve
840
+ * é `consolidate`. Determinística: mesmo log ⇒ mesmo resultado.
841
+ */
842
+ function computeConsolidation(pod, opts = {}) {
843
+ const paths = memoryPaths(pod, opts);
844
+ // SF7.6: registros de hardening são MARCA, não heurística — separados antes do
845
+ // merge (não entram em dedup/conflito/ranking). SF8.3: ok_pattern idem — trilha
846
+ // de auditoria de estabilidade no log cru, nunca compete por slot do top-N.
847
+ const { hardenings, okPatterns, entries: rawHeuristics } = splitRaw(readRawEntries(paths.raw));
848
+ const raw = rawHeuristics;
849
+ const hardenedRefs = hardeningByRef(hardenings);
850
+ const merged = mergeEntries(raw);
851
+
852
+ // conflito primeiro — perdedores são removidos do pool e arquivados. Um
853
+ // CRITICAL ativo NUNCA é perdedor de conflito com NORMAL (H1 fix): detectConflicts
854
+ // protege o pin e sinaliza needsCuration quando isso ocorre.
855
+ const { conflicts, loserKeys, needsCuration: conflictNeedsCuration } = detectConflicts(merged);
856
+ const survivors = merged.filter((m) => !loserKeys.has(dedupKey(m)));
857
+
858
+ // separa pinados (critical, atribuição válida, não obsoleto) do pool normal
859
+ const activeCritical = survivors.filter(isActiveCritical);
860
+ const normalPool = survivors.filter((m) => !isActiveCritical(m) && !m._obsolete);
861
+
862
+ // ── SF7.6/ADR-012: DEMOÇÃO das endurecidas do top-N ativo ──────────────────
863
+ // Heurística com registro de hardening sai do conjunto que compete por injeção
864
+ // (orçamento finito; enforcement substitui injeção) e vira anotação 🔒 —
865
+ // permanece no consolidado e no log cru (nunca removida).
866
+ const hardened = [];
867
+ const normals = [];
868
+ for (const m of normalPool) {
869
+ const mark = hardenedRefs.get(dedupKey(m));
870
+ if (mark) hardened.push({ entry: m, mark });
871
+ else normals.push(m);
872
+ }
873
+ // ordem determinística da seção 🔒: timestamp da promoção asc, depois ref asc
874
+ hardened.sort((a, b) =>
875
+ String(a.mark.timestamp).localeCompare(String(b.mark.timestamp)) ||
876
+ String(a.mark.ref).localeCompare(String(b.mark.ref)));
877
+
878
+ // ── trava (iii): teto ≤10 do conjunto pinado ──────────────────────────────
879
+ // ordem determinística dos pins: ts asc (lições mais antigas são fundacionais),
880
+ // depois id asc. Excedente vai p/ overflow (retido, sinalizado, NUNCA escrito
881
+ // como pin permanente nem descartado do log cru).
882
+ const pinsSorted = [...activeCritical].sort((a, b) =>
883
+ ((a.ts || 0) - (b.ts || 0)) || String(a.id).localeCompare(String(b.id)));
884
+ const pins = pinsSorted.slice(0, PIN_CAP);
885
+ const overflow = pinsSorted.slice(PIN_CAP).map((p) => ({
886
+ id: p._ids ? p._ids[p._ids.length - 1] : p.id,
887
+ trigger: p.trigger,
888
+ recommendation: p.recommendation,
889
+ }));
890
+ // needsCuration dispara por TRÊS vias: teto de pins estourado (trava iii), conflito
891
+ // CRITICAL×NORMAL (H1 — resolução é curadoria humana, não automática) OU heurística
892
+ // ativa NÃO ADMITIDA no consolidado (SF11.4b/DF-SF11.4b-1 — pelas DUAS vias de corte,
893
+ // calculadas abaixo em `admit`).
894
+
895
+ // ── ADMISSÃO ÚNICA: ranking + os DOIS cortes num só ponto ──────────────────
896
+ const scored = scoreEntries(normals);
897
+ const budget = effectiveTokenBudget(pod, opts);
898
+ const { ranked, evicted, md } = admit(scored, {
899
+ budgetTokens: budget.tokens,
900
+ render: (list) => renderConsolidated(pod, pins, list, hardened),
901
+ });
902
+
903
+ const needsCuration = pinsSorted.length > PIN_CAP || conflictNeedsCuration || evicted.length > 0;
904
+
905
+ return {
906
+ paths, merged, loserKeys, conflicts, pins, ranked, hardened, hardenedRefs,
907
+ md, needsCuration, overflow, evicted, okPatterns,
908
+ // SF11.4b/DF-SF11.4b-2 — orçamento EFETIVO desta consolidação e o alvo declarado
909
+ // (ou `null`). Campos ADITIVOS: o ledger precisa deles para nomear a causa do corte
910
+ // por custo sem que cada emissor releia o manifesto por conta própria.
911
+ tokenBudget: budget.tokens, memoryCost: budget.target,
912
+ };
913
+ }
914
+
915
+ /**
916
+ * (SF11.4b/ADR-023 DF-SF11.4b-1) **Admissão única** — um ponto de corte, um campo, um
917
+ * canal. Tudo que fica FORA de `ranked` entra em `evicted` com `reason`, seja qual for
918
+ * a via: `'top_n'` (teto de ATENÇÃO) ou `'budget'` (alvo/teto de CUSTO em bytes).
919
+ *
920
+ * POR QUE UM SÓ CAMPO, E NÃO UM `truncated` SEPARADO (Alternativa 1 do ADR-023,
921
+ * RECUSADA): a semântica dos dois eventos é IDÊNTICA — heurística ativa, que não existe
922
+ * em outro lugar do brief, que não foi injetada. Dois campos para um evento
923
+ * reproduziriam o sub-relato num terceiro lugar: quem lê `evicted.length` continuaria
924
+ * vendo "1" com 13 fora. `reason` é fato observável sobre QUAL caminho removeu a
925
+ * entrada — sem limiar, sem julgamento (DF4-5 satisfeito).
926
+ *
927
+ * O DEFEITO QUE ISTO FECHA (medido, ADR-023 §C1-C2): `scored.slice(0, TOP_N)` cortava
928
+ * ANTES de qualquer checagem de orçamento e não escrevia em `evicted`, não ligava
929
+ * `needsCuration`, não emitia linha nenhuma. Em pool 42 do `hn-carousel`, 13 heurísticas
930
+ * ficavam fora do brief e o runner anunciava **1** — sub-relato de 13×, classe PIOR que
931
+ * o silêncio (dá conforto e não sustenta a linha).
932
+ *
933
+ * ORDEM: `evicted` traz primeiro os cortes por atenção (score DESC — os que ficaram
934
+ * logo abaixo do teto vêm na frente) e depois os por custo (a cauda sai por último).
935
+ * Determinístico: mesmo pool ⇒ mesmo `evicted`, item a item.
936
+ *
937
+ * @param {Array} scored pool ativo já ranqueado (score DESC); pinados e 🔒 fora
938
+ * @param {{budgetTokens:number, render:(list:Array)=>string}} o
939
+ * @returns {{ranked:Array, evicted:Array, md:string}}
940
+ */
941
+ function admit(scored, { budgetTokens, render }) {
942
+ const evicted = [];
943
+ // ponto ÚNICO de registro de não-admissão: as duas vias passam por aqui, então
944
+ // nenhuma pode ficar muda sozinha. O corte por TOP_N ficou mudo por três ADRs
945
+ // justamente por ter um caminho próprio, sem passar por registro nenhum.
946
+ const drop = (entry, reason) => {
947
+ evicted.push({
948
+ trigger: entry.trigger,
949
+ recommendation: entry.recommendation,
950
+ score: entry._score,
951
+ reason,
952
+ });
953
+ };
954
+
955
+ // (1) teto de ATENÇÃO — TOP_N. Corta o excedente do pool ranqueado, COM VOZ.
956
+ let ranked = scored.slice(0, TOP_N);
957
+ for (const out of scored.slice(TOP_N)) drop(out, 'top_n');
958
+
959
+ // (2) alvo/teto de CUSTO — remove os de MENOR score (cauda) até o .md caber no
960
+ // orçamento efetivo. Pinados são piso categórico e nunca entram nesta poda;
961
+ // endurecidas (🔒) são referência agregada (nunca removidas — ADR-012), tampouco
962
+ // podadas. `evicted` é campo próprio, não sobrecarga de `overflow` (que tem semântica
963
+ // definida: excedente do teto de pins CRITICAL, RETIDO).
964
+ let md = render(ranked);
965
+ while (ranked.length > 0 && estTokens(md) > budgetTokens) {
966
+ drop(ranked[ranked.length - 1], 'budget');
967
+ ranked = ranked.slice(0, ranked.length - 1);
968
+ md = render(ranked);
969
+ }
970
+ return { ranked, evicted, md };
971
+ }
972
+
973
+ /**
974
+ * (SF11.4b/ADR-023 DF-SF11.4b-1) **Ledger de curadoria** — fonte ÚNICA da mensagem que
975
+ * os emissores imprimem. Existe como função, e não como string em cada emissor, por uma
976
+ * exigência do desenho: **nenhum emissor pode imprimir um número que não seja
977
+ * `evicted.length` total**. O total vem primeiro; a quebra por `reason` depois; o custo
978
+ * por último. Um emissor que conhecesse só uma das vias voltaria a dizer "1" com 13 fora.
979
+ *
980
+ * Unidade declarada DENTRO da mensagem: tokens = `Math.ceil(bytes UTF-8 / 4)` — o mesmo
981
+ * `estTokens` do mecanismo (a divergência UTF-16×UTF-8 já custou uma medição na SF11.2).
982
+ *
983
+ * @param {string} pod
984
+ * @param {object} result retorno de `consolidate()` (ou de `computeConsolidation`)
985
+ * @returns {string|null} `null` quando nada ficou de fora — nada a reportar
986
+ */
987
+ function formatEvictionLedger(pod, result) {
988
+ const evicted = Array.isArray(result && result.evicted) ? result.evicted : [];
989
+ if (evicted.length === 0) return null;
990
+ const byTopN = evicted.filter((e) => e.reason === 'top_n').length;
991
+ const byBudget = evicted.filter((e) => e.reason === 'budget').length;
992
+ const unclassified = evicted.length - byTopN - byBudget;
993
+ const budget = (result && result.tokenBudget) || TOKEN_BUDGET;
994
+ const parts = [];
995
+ if (byTopN > 0) parts.push(`${byTopN} por teto de atenção TOP_N=${TOP_N}`);
996
+ if (byBudget > 0) parts.push(`${byBudget} por ${result && result.memoryCost ? 'alvo' : 'teto'} de custo ${budget} tok`);
997
+ if (unclassified > 0) parts.push(`${unclassified} sem causa registrada`);
998
+ const names = evicted.map((e) => `"${e.trigger}"`).join(', ');
999
+ const tokens = result && typeof result.tokens === 'number' ? result.tokens : null;
1000
+ const steps = result && result.memoryCost ? result.memoryCost.steps : null;
1001
+ const lines = [
1002
+ `[memory] AVISO: ${evicted.length} heurística(s) NÃO entraram no consolidado do pod "${pod}"` +
1003
+ ` (${parts.join(' · ')}): ${names}`,
1004
+ ];
1005
+ if (tokens != null) {
1006
+ lines.push(steps
1007
+ ? ` Custo do consolidado: ${tokens} tok/etapa × ${steps} etapa(s) = ${tokens * steps} tok/rodada` +
1008
+ ' (tokens = ceil(bytes UTF-8 / 4); o consolidado entra INTEGRAL no brief de cada etapa).'
1009
+ : ` Custo do consolidado: ${tokens} tok/etapa (tokens = ceil(bytes UTF-8 / 4); o consolidado entra` +
1010
+ ' INTEGRAL no brief de cada etapa — declare "memoryCost.steps" no pod-spec.json para ver o custo/rodada).');
1011
+ }
1012
+ lines.push(' A substância continua no log cru — nada foi apagado. Para abrir espaço: cure o consolidado' +
1013
+ ' (marque redundância como obsolete) ou endureça (*harden-learning — promover LIBERA orçamento, ADR-022 DF-SF11.4-1).');
1014
+ return lines.join('\n');
1015
+ }
1016
+
1017
+ // ── (b)/(e)/(f) consolidate ───────────────────────────────────────────────────
1018
+ /**
1019
+ * Lê o log cru, deduz/mescla, resolve conflito, aplica ranking, poda ao top-N e ao
1020
+ * budget de tokens, pina os CRITICAL (fora do ranking, com teto ≤10), e escreve o
1021
+ * learnings.consolidated.md. Idempotente. Função PURA/chamável (o disparo por
1022
+ * cadência K é do CALLER/SF2.2).
1023
+ * @returns {{ pinnedCount, topNCount, tokens, needsCuration, overflow, evicted, conflicts }}
1024
+ */
1025
+ function consolidate(pod, opts = {}) {
1026
+ const c = computeConsolidation(pod, opts);
1027
+ const { paths, pins, ranked, hardened, md, merged, loserKeys, conflicts } = c;
1028
+ ensureDir(paths.dir);
1029
+ fs.writeFileSync(paths.consolidated, md);
1030
+ writeHardenedDetail(paths, pod, hardened);
1031
+
1032
+ // registra conflitos + arquiva as perdedoras (nunca deletadas do log cru) de
1033
+ // forma AUDITÁVEL, IDEMPOTENTE e com HISTÓRICO PRESERVADO (L3): funde os registros
1034
+ // existentes com os recomputados, dedup por chave estável, ordena determinístico.
1035
+ // - M1: cada registro carrega ts derivado do vencedor (não Date.now) ⇒ rodar 2x
1036
+ // produz bytes idênticos.
1037
+ // - L3: registros antigos cujas fontes já rotacionaram para archive/ NÃO são
1038
+ // perdidos — a fusão preserva-os (§G1 "conflito registrado").
1039
+ if (conflicts.length > 0) {
1040
+ ensureDir(paths.archiveDir);
1041
+ mergeJsonlByKey(paths.conflicts, conflicts, (x) => `${x.winner_id}||${x.loser_id}`);
1042
+ const losers = merged.filter((m) => loserKeys.has(dedupKey(m)))
1043
+ .map((m) => ({ id: m.id, trigger: m.trigger, recommendation: m.recommendation, reason: 'conflict-loser' }));
1044
+ mergeJsonlByKey(paths.archivedEntries, losers, (m) => String(m.id));
1045
+ }
1046
+
1047
+ return {
1048
+ pinnedCount: pins.length,
1049
+ topNCount: ranked.length,
1050
+ tokens: estTokens(md),
1051
+ needsCuration: c.needsCuration,
1052
+ overflow: c.overflow,
1053
+ // SF11.4/DF-SF11.4-2 — o que ficou FORA de `ranked` nesta consolidação. Campo
1054
+ // ADITIVO: nenhum caller quebra por não conhecê-lo; quem conhece (pc-run-pod,
1055
+ // forge-harden-learning, forge-ingest-memory) para de deixar a poda passar calada.
1056
+ // SF11.4b/DF-SF11.4b-1 — cada item agora traz `reason: 'top_n' | 'budget'` e a lista
1057
+ // é COMPLETA: as duas vias de não-injeção passam pelo mesmo campo (antes, o corte
1058
+ // por TOP_N não aparecia aqui e o total ficava sub-relatado — ADR-023 §C2).
1059
+ evicted: c.evicted,
1060
+ // SF11.4b/DF-SF11.4b-2 — orçamento efetivo aplicado e o alvo declarado (ou null).
1061
+ tokenBudget: c.tokenBudget,
1062
+ memoryCost: c.memoryCost,
1063
+ conflicts,
1064
+ hardenedCount: hardened.length,
1065
+ okPatternCount: c.okPatterns.length, // SF8.3 — fora do ranking, vivos no log cru
1066
+ };
1067
+ }
1068
+
1069
+ /**
1070
+ * (SF11.4/ADR-022 DF-SF11.4-1) Escreve a trilha auditável das endurecidas.
1071
+ *
1072
+ * APPEND-ONLY POR CONSTRUÇÃO, não por promessa: as linhas derivadas do log cru são
1073
+ * reescritas (determinístico ⇒ idempotente), e QUALQUER linha 🔒 já presente no arquivo
1074
+ * que não seja derivável do log atual é PRESERVADA numa seção própria. Assim uma linha
1075
+ * escrita aqui nunca some — nem se o registro-fonte for arquivado/perdido no log cru.
1076
+ * Não cria o arquivo quando não há endurecida nenhuma (pod sem promoção não ganha
1077
+ * artefato vazio).
1078
+ */
1079
+ function writeHardenedDetail(paths, pod, hardened) {
1080
+ const exists = fs.existsSync(paths.hardenedDetail);
1081
+ if (hardened.length === 0 && !exists) return;
1082
+ const derived = renderHardenedDetail(pod, hardened);
1083
+ const derivedBullets = new Set(
1084
+ derived.split('\n').filter((l) => l.startsWith(HARDENED_DETAIL_BULLET)),
1085
+ );
1086
+ let preserved = [];
1087
+ if (exists) {
1088
+ preserved = fs.readFileSync(paths.hardenedDetail, 'utf8')
1089
+ .split(/\r?\n/)
1090
+ .filter((l) => l.startsWith(HARDENED_DETAIL_BULLET) && !derivedBullets.has(l))
1091
+ .sort(); // ordem determinística: rodar 2x produz bytes idênticos
1092
+ }
1093
+ let out = derived;
1094
+ if (preserved.length > 0) {
1095
+ out += [
1096
+ '## Preservadas (registro-fonte não mais derivável do log cru)',
1097
+ '',
1098
+ '_Append-only: linha já escrita nesta trilha NUNCA é removida, mesmo que o registro que a originou saia do log cru (rotação/edição). Ver ADR-012 (endurecida nunca é removida)._',
1099
+ '',
1100
+ ...preserved,
1101
+ '',
1102
+ ].join('\n');
1103
+ }
1104
+ fs.writeFileSync(paths.hardenedDetail, out);
1105
+ }
1106
+
1107
+ // ── (SF10.10/DF-SF10.10-3) listCandidates — DIFERENÇA DE CONJUNTOS, read-only ──
1108
+ /**
1109
+ * Heurísticas que estão no CONSOLIDADO CURADO (pins + top-N já podado) e ainda NÃO
1110
+ * têm registro de hardening — nem `veto`, nem `knowledge`. É uma diferença de
1111
+ * conjuntos verificável, na MESMA ordem determinística em que aparecem no
1112
+ * consolidado (pins por ts asc; ranqueadas por score desc).
1113
+ *
1114
+ * FRONTEIRA (vinculante, DF-SF10.10-3): esta função NÃO classifica heurística como
1115
+ * "de conteúdo" vs "mecanizável por command_check" — não há oráculo para essa
1116
+ * distinção, e ADR-012 §Escopo OUT reserva a tradução heurística→forma ao humano.
1117
+ * Não escreve nada, não promove nada, não ordena por "importância" inventada.
1118
+ * @returns {Array<{ref, trigger, recommendation, occurrences, last_id, section}>}
1119
+ */
1120
+ function listCandidates(pod, opts = {}) {
1121
+ const { pins, ranked, hardenedRefs } = computeConsolidation(pod, opts);
1122
+ const pinSet = new Set(pins);
1123
+ const out = [];
1124
+ for (const m of [...pins, ...ranked]) {
1125
+ const ref = dedupKey(m);
1126
+ if (hardenedRefs.has(ref)) continue; // já endurecida (qualquer kind) ⇒ não é candidata
1127
+ out.push({
1128
+ ref,
1129
+ trigger: m.trigger,
1130
+ recommendation: m.recommendation,
1131
+ occurrences: m.occurrences || 1,
1132
+ last_id: m._ids && m._ids.length ? m._ids[m._ids.length - 1] : m.id,
1133
+ section: pinSet.has(m) ? 'critical' : 'top-n',
1134
+ });
1135
+ }
1136
+ return out;
1137
+ }
1138
+
1139
+ // ── (g) rotação do log cru ────────────────────────────────────────────────────
1140
+ /**
1141
+ * Operação SEPARADA e explícita (append permanece puramente append-only). Move as
1142
+ * entradas mais antigas (por ts) para archive/learnings-{ts}.jsonl quando o log cru
1143
+ * excede M=500, mantendo as M mais novas. NUNCA remove uma entrada-fonte de um
1144
+ * CRITICAL pinado ainda ATIVO (trava (e) — "nunca despejado por rotação"): tais
1145
+ * entradas são preservadas no log cru mesmo se antigas.
1146
+ * @returns {{ rotated: number, kept: number, archiveFile: string|null }}
1147
+ */
1148
+ function rotate(pod, opts = {}) {
1149
+ const paths = memoryPaths(pod, opts);
1150
+ const all = readRawEntries(paths.raw);
1151
+ if (all.length <= ROTATE_M) return { rotated: 0, kept: all.length, archiveFile: null };
1152
+
1153
+ // registros de hardening são MARCA de auditoria (ADR-012: nunca removida) — não
1154
+ // entram no merge e NUNCA rotacionam; suas heurísticas-fonte também ficam (a
1155
+ // anotação 🔒 do consolidado depende delas permanecerem no log cru).
1156
+ const { hardenings, entries: heuristicEntries } = splitRaw(all); // okPatterns rotacionam normalmente (SF8.3 — auditoria arquivável, nunca deletada)
1157
+ const hardenedRefKeys = new Set(hardeningByRef(hardenings).keys());
1158
+
1159
+ // chaves dedup dos CRITICAL ativos → suas entradas-fonte nunca rotacionam
1160
+ const activeCriticalKeys = new Set(
1161
+ mergeEntries(heuristicEntries)
1162
+ .filter((m) => m.tier === TIER_CRITICAL && VALID_ATTRIBUTION.has(m.attributed_by) && !m._obsolete)
1163
+ .map((m) => dedupKey(m)),
1164
+ );
1165
+ const isProtected = (e) =>
1166
+ e.type === TYPE_HARDENING ||
1167
+ (e.tier === TIER_CRITICAL && VALID_ATTRIBUTION.has(e.attributed_by)) ||
1168
+ activeCriticalKeys.has(dedupKey(e)) ||
1169
+ hardenedRefKeys.has(dedupKey(e));
1170
+
1171
+ // ordena por ts asc; candidatas a rotação são as mais antigas NÃO protegidas
1172
+ const indexed = all.map((e, i) => ({ e, i }));
1173
+ indexed.sort((a, b) => ((a.e.ts || 0) - (b.e.ts || 0)) || (a.i - b.i));
1174
+
1175
+ const toArchive = [];
1176
+ const keepKeys = new Set();
1177
+ let removable = all.length - ROTATE_M;
1178
+ for (const { e } of indexed) {
1179
+ if (removable > 0 && !isProtected(e)) {
1180
+ toArchive.push(e);
1181
+ removable -= 1;
1182
+ }
1183
+ }
1184
+ const archiveSet = new Set(toArchive);
1185
+ const kept = all.filter((e) => !archiveSet.has(e));
1186
+
1187
+ if (toArchive.length === 0) {
1188
+ return { rotated: 0, kept: all.length, archiveFile: null };
1189
+ }
1190
+
1191
+ ensureDir(paths.archiveDir);
1192
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
1193
+ const archiveFile = path.join(paths.archiveDir, `learnings-${stamp}.jsonl`);
1194
+ fs.writeFileSync(archiveFile, toArchive.map((e) => `${JSON.stringify(e)}\n`).join(''));
1195
+ // reescreve o log cru só com as retidas (rotação NÃO deleta — arquiva)
1196
+ fs.writeFileSync(paths.raw, kept.map((e) => `${JSON.stringify(e)}\n`).join(''));
1197
+ void keepKeys;
1198
+ return { rotated: toArchive.length, kept: kept.length, archiveFile };
1199
+ }
1200
+
1201
+ // ── (h.6 / AC10) contrato "agente nunca lê cru" ──────────────────────────────
1202
+ /**
1203
+ * ÚNICA fonte de conteúdo-para-brief que um consumidor (SF2.2) usa. Retorna
1204
+ * EXCLUSIVAMENTE o conteúdo do consolidado curado. NÃO abre, referencia nem recebe
1205
+ * o path do log cru — verificável por inspeção do corpo desta função na suíte.
1206
+ * @returns {string} conteúdo do learnings.consolidated.md (ou '' se ausente).
1207
+ */
1208
+ function getConsolidatedForBrief(pod, opts = {}) {
1209
+ const consolidatedPath = memoryPaths(pod, opts).consolidated;
1210
+ if (!fs.existsSync(consolidatedPath)) return '';
1211
+ return fs.readFileSync(consolidatedPath, 'utf8');
1212
+ }
1213
+
1214
+ module.exports = {
1215
+ append,
1216
+ appendHardening,
1217
+ consolidate,
1218
+ rotate,
1219
+ getConsolidatedForBrief,
1220
+ // SF10.10 — 2º alvo de promoção (knowledge) + detecção de candidatos
1221
+ listCandidates,
1222
+ computeConsolidation,
1223
+ hardeningKind,
1224
+ HARDEN_KIND_VETO,
1225
+ HARDEN_KIND_KNOWLEDGE,
1226
+ // SF11.4/ADR-022 — trilha auditável das endurecidas (fora do caminho de injeção)
1227
+ HARDENED_DETAIL_FILE,
1228
+ HARDENED_DETAIL_REL,
1229
+ renderHardenedDetail,
1230
+ // SF11.4b/ADR-023 — admissão única: ledger (fonte ÚNICA da mensagem dos emissores),
1231
+ // alvo de custo declarado pelo dono e orçamento efetivo.
1232
+ formatEvictionLedger,
1233
+ readMemoryCost,
1234
+ effectiveTokenBudget,
1235
+ POD_SPEC_FILE,
1236
+ MEMORY_COST_FIELD,
1237
+ // helpers exportados p/ a suíte (não fazem parte do contrato de consumo)
1238
+ memoryPaths,
1239
+ detectConflicts,
1240
+ mergeEntries,
1241
+ normalizeText,
1242
+ dedupKey,
1243
+ TYPE_HARDENING,
1244
+ TYPE_OK_PATTERN,
1245
+ isOkPattern,
1246
+ // constantes do ADR-003
1247
+ TOP_N,
1248
+ ROTATE_M,
1249
+ CONSOLIDATE_K,
1250
+ TOKEN_BUDGET,
1251
+ PIN_CAP,
1252
+ };