@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.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/bin/cli.js +124 -0
- package/installer/__tests__/doctor-ready-oracles.test.cjs +140 -0
- package/installer/__tests__/merge-settings.test.cjs +330 -0
- package/installer/adopt.js +161 -0
- package/installer/copy.js +203 -0
- package/installer/doctor.js +266 -0
- package/installer/install.js +155 -0
- package/installer/manifest.js +102 -0
- package/installer/motor-scripts.js +50 -0
- package/installer/motor-scripts.json +39 -0
- package/installer/uninstall.js +99 -0
- package/installer/update.js +376 -0
- package/installer/util.js +113 -0
- package/installer/wizard.js +64 -0
- package/package.json +26 -0
- package/scripts/__fixtures__/anti-path-demo/agents/ap-writer.md +13 -0
- package/scripts/__fixtures__/anti-path-demo/templates/write-tmpl.md +7 -0
- package/scripts/__fixtures__/anti-path-demo/workflows/wf-ap.smoke.json +10 -0
- package/scripts/__fixtures__/anti-path-demo/workflows/wf-ap.yaml +25 -0
- package/scripts/__fixtures__/assess-tier/retrofit-leve/pod/agents/content-chief.md +1 -0
- package/scripts/__fixtures__/assess-tier/retrofit-leve/pod/workflows/wf-criacao-redteam.yaml +3 -0
- package/scripts/__fixtures__/assess-tier/retrofit-leve/pod-spec.json +13 -0
- package/scripts/__fixtures__/fidelity/fake-green-average-in-band.yaml +21 -0
- package/scripts/__fixtures__/fidelity/fake-green-average.yaml +18 -0
- package/scripts/__fixtures__/fidelity/fake-green-denominator.yaml +19 -0
- package/scripts/__fixtures__/fidelity/fake-green-missing-evidence.yaml +18 -0
- package/scripts/__fixtures__/fidelity/fake-green-threshold.yaml +18 -0
- package/scripts/__fixtures__/fidelity/fake-green-weight.yaml +18 -0
- package/scripts/__fixtures__/fidelity/golden.yaml +31 -0
- package/scripts/__fixtures__/fidelity/trindade-incoherent.yaml +15 -0
- package/scripts/__fixtures__/forge-canonical/pod-spec-example.json +37 -0
- package/scripts/__fixtures__/migrate-squad/authored/agents/offer-engineer.md +17 -0
- package/scripts/__fixtures__/migrate-squad/authored/knowledge/no-source.md +7 -0
- package/scripts/__fixtures__/migrate-squad/authored/knowledge/value-stack-method.md +8 -0
- package/scripts/__fixtures__/migrate-squad/authored/specs/wf-sample.smoke.json +24 -0
- package/scripts/__fixtures__/migrate-squad/authored/specs/wf-sample.spec.json +45 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/HEADLINE.md +3 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/README.md +4 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/agents/hormozi-offer.md +14 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/dead-redundant.md +4 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/method-guide.md +6 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/checklists/veto-screen.md +4 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/config/config.yaml +2 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/data/case-library.md +3 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/scripts/validate-mvn.py +16 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/sops/sop-sample.md +3 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/squad-io.yaml +14 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/tasks/gather-context.md +7 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/templates/output-tmpl.md +5 -0
- package/scripts/__fixtures__/migrate-squad/legacy-squad/workflows/wf-sample.yaml +24 -0
- package/scripts/__fixtures__/pipeline/pipelines/pipeline-checkpoint.yaml +21 -0
- package/scripts/__fixtures__/pipeline/pipelines/pipeline-demo.yaml +22 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-a/agents/maker.md +13 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-a/memory/learnings.consolidated.md +8 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-a/memory/learnings.jsonl +83 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-a/templates/make-tmpl.md +8 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-a/workflows/wf-a.yaml +16 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-b/agents/user.md +13 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-b/memory/learnings.consolidated.md +8 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-b/memory/learnings.jsonl +14 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-b/templates/use-tmpl.md +6 -0
- package/scripts/__fixtures__/pipeline/pods/pipe-b/workflows/wf-b.yaml +16 -0
- package/scripts/__fixtures__/pod-run/agents/forge-chief.md +13 -0
- package/scripts/__fixtures__/pod-run/agents/pod-critic.md +13 -0
- package/scripts/__fixtures__/pod-run/templates/build-tmpl.md +8 -0
- package/scripts/__fixtures__/pod-run/templates/gate-tmpl.md +6 -0
- package/scripts/__fixtures__/pod-run/workflows/wf-min.yaml +43 -0
- package/scripts/__fixtures__/pod-run/workflows/wf-skip-modes.yaml +50 -0
- package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-all-skippable.yaml +30 -0
- package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-criteria-escalar-de-bloco.yaml +40 -0
- package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-skip-mode-desconhecido.yaml +30 -0
- package/scripts/__fixtures__/pod-workflow/invalid-checkpoint-unknown-key.yaml +29 -0
- package/scripts/__fixtures__/pod-workflow/invalid-cycle.yaml +26 -0
- package/scripts/__fixtures__/pod-workflow/invalid-fix-loop-backedge.yaml +29 -0
- package/scripts/__fixtures__/pod-workflow/invalid-fix-loop-forward.yaml +29 -0
- package/scripts/__fixtures__/pod-workflow/invalid-handoff-open.yaml +27 -0
- package/scripts/__fixtures__/pod-workflow/invalid-knowledge-shape.yaml +18 -0
- package/scripts/__fixtures__/pod-workflow/invalid-missing-agent.yaml +15 -0
- package/scripts/__fixtures__/pod-workflow/invalid-missing-outputs.yaml +15 -0
- package/scripts/__fixtures__/pod-workflow/invalid-missing-template.yaml +15 -0
- package/scripts/__fixtures__/pod-workflow/invalid-missing-veto.yaml +13 -0
- package/scripts/__fixtures__/pod-workflow/invalid-template-not-on-disk.yaml +16 -0
- package/scripts/__fixtures__/pod-workflow/invalid-veto-prose.yaml +17 -0
- package/scripts/__fixtures__/pod-workflow/knowledge/a.md +4 -0
- package/scripts/__fixtures__/pod-workflow/knowledge/b.md +4 -0
- package/scripts/__fixtures__/pod-workflow/knowledge/c.md +4 -0
- package/scripts/__fixtures__/pod-workflow/templates/step-tmpl.md +7 -0
- package/scripts/__fixtures__/pod-workflow/valid-checkpoint-skip-modes.yaml +29 -0
- package/scripts/__fixtures__/pod-workflow/valid-fix-loop.yaml +30 -0
- package/scripts/__fixtures__/pod-workflow/valid-knowledge.yaml +30 -0
- package/scripts/__fixtures__/pod-workflow/valid-veto-always-true.yaml +20 -0
- package/scripts/__fixtures__/pod-workflow/valid.yaml +27 -0
- package/scripts/__fixtures__/smoke-canary/agents/canary-reporter.md +13 -0
- package/scripts/__fixtures__/smoke-canary/agents/canary-researcher.md +13 -0
- package/scripts/__fixtures__/smoke-canary/agents/canary-synth.md +13 -0
- package/scripts/__fixtures__/smoke-canary/memory/learnings.consolidated.md +10 -0
- package/scripts/__fixtures__/smoke-canary/memory/learnings.jsonl +448 -0
- package/scripts/__fixtures__/smoke-canary/templates/report-tmpl.md +9 -0
- package/scripts/__fixtures__/smoke-canary/templates/research-tmpl.md +7 -0
- package/scripts/__fixtures__/smoke-canary/templates/synth-tmpl.md +9 -0
- package/scripts/__fixtures__/smoke-canary/workflows/wf-canary.smoke.json +11 -0
- package/scripts/__fixtures__/smoke-canary/workflows/wf-canary.yaml +53 -0
- package/scripts/__fixtures__/source-audit/fake-green-bronze-unsignaled.yaml +10 -0
- package/scripts/__fixtures__/source-audit/fake-green-counts.yaml +9 -0
- package/scripts/__fixtures__/source-audit/fake-green-missing-evidence.yaml +9 -0
- package/scripts/__fixtures__/source-audit/fake-green-unclassified.yaml +10 -0
- package/scripts/__fixtures__/source-audit/fake-green-verdict.yaml +10 -0
- package/scripts/__fixtures__/source-audit/golden-blocked.yaml +10 -0
- package/scripts/__fixtures__/source-audit/golden.yaml +11 -0
- package/scripts/__fixtures__/sweep-forms/agents/forms-count.md +13 -0
- package/scripts/__fixtures__/sweep-forms/agents/forms-field.md +13 -0
- package/scripts/__fixtures__/sweep-forms/agents/forms-fileref.md +13 -0
- package/scripts/__fixtures__/sweep-forms/agents/forms-json.md +13 -0
- package/scripts/__fixtures__/sweep-forms/agents/forms-section.md +13 -0
- package/scripts/__fixtures__/sweep-forms/specs/wf-forms.spec.json +5 -0
- package/scripts/__fixtures__/sweep-forms/templates/t_count.md +5 -0
- package/scripts/__fixtures__/sweep-forms/templates/t_field.md +3 -0
- package/scripts/__fixtures__/sweep-forms/templates/t_fileref.md +3 -0
- package/scripts/__fixtures__/sweep-forms/templates/t_json.md +0 -0
- package/scripts/__fixtures__/sweep-forms/templates/t_section.md +3 -0
- package/scripts/__fixtures__/sweep-forms/workflows/wf-forms.smoke.json +13 -0
- package/scripts/__fixtures__/sweep-forms/workflows/wf-forms.yaml +67 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-1-noop-state-json.fixture.md +18 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-2-crash-buildworkflowyaml.fixture.md +26 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-3-nasce-verde-skip-modes.fixture.md +29 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-4-guarda-sem-irmao.fixture.md +17 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-5a-diff-stat-sem-guarda.fixture.md +17 -0
- package/scripts/__fixtures__/validate-story-acs-corpus/corpus-5b-test-f-sobre-diretorio.fixture.md +20 -0
- package/scripts/__fixtures__/veto-checks/copy-clean.md +4 -0
- package/scripts/__fixtures__/veto-checks/copy-dashes.md +4 -0
- package/scripts/__fixtures__/veto-checks/n8n-bad.md +18 -0
- package/scripts/__fixtures__/veto-checks/n8n-ok.md +22 -0
- package/scripts/__fixtures__/veto-checks/page-bad.html +7 -0
- package/scripts/__fixtures__/veto-checks/phash/copy.png +0 -0
- package/scripts/__fixtures__/veto-checks/phash/distinct.png +0 -0
- package/scripts/__fixtures__/veto-checks/phash/distinct2.png +0 -0
- package/scripts/__fixtures__/veto-checks/phash/make-fixtures.cjs +203 -0
- package/scripts/__fixtures__/veto-checks/phash/ref.png +0 -0
- package/scripts/__fixtures__/veto-checks/slide-ok.html +15 -0
- package/scripts/__fixtures__/veto-checks/slide-overflow.html +14 -0
- package/scripts/__fixtures__/veto-checks/sweep-pod/agents/vc-checker.md +13 -0
- package/scripts/__fixtures__/veto-checks/sweep-pod/templates/check-tmpl.md +6 -0
- package/scripts/__fixtures__/veto-checks/sweep-pod/workflows/wf-vc.smoke.json +10 -0
- package/scripts/__fixtures__/veto-checks/sweep-pod/workflows/wf-vc.yaml +19 -0
- package/scripts/__fixtures__/voz-do-dono/README.md +23 -0
- package/scripts/__fixtures__/voz-do-dono/dono-escalar.yaml +9 -0
- package/scripts/__fixtures__/voz-do-dono/dono-guia.yaml +10 -0
- package/scripts/__fixtures__/voz-do-dono/peca-3a-pessoa.md +6 -0
- package/scripts/__fixtures__/voz-do-dono/peca-com-lexico-proibido.md +6 -0
- package/scripts/__fixtures__/voz-do-dono/peca-limpa-1a-pessoa.md +6 -0
- package/scripts/__tests__/derive-ready-transitions.test.cjs +269 -0
- package/scripts/__tests__/enforce-cycle-authority.test.cjs +869 -0
- package/scripts/__tests__/enforce-git-push-authority.test.cjs +701 -0
- package/scripts/__tests__/enforce-ready-oracles.test.cjs +745 -0
- package/scripts/__tests__/motor-scripts.test.cjs +339 -0
- package/scripts/__tests__/pc-run-pod-counter.test.cjs +58 -0
- package/scripts/__tests__/pc-run-pod.test.cjs +578 -0
- package/scripts/__tests__/validate-pod-workflow.test.cjs +90 -0
- package/scripts/__tests__/validate-story-acs.test.cjs +607 -0
- package/scripts/anti-path.test.cjs +163 -0
- package/scripts/arena.cjs +514 -0
- package/scripts/arena.test.cjs +213 -0
- package/scripts/brief-leitura-obrigatoria.test.cjs +364 -0
- package/scripts/consolidate-knowledge-runner.test.cjs +249 -0
- package/scripts/default-funcional.test.cjs +153 -0
- package/scripts/derive-ready-transitions.cjs +325 -0
- package/scripts/forge-additive.test.cjs +158 -0
- package/scripts/forge-assess-tier.cjs +205 -0
- package/scripts/forge-assess-tier.test.cjs +114 -0
- package/scripts/forge-canonical.test.cjs +624 -0
- package/scripts/forge-generate.cjs +1303 -0
- package/scripts/forge-harden-learning.cjs +921 -0
- package/scripts/forge-harden-learning.test.cjs +242 -0
- package/scripts/forge-ingest-memory.cjs +136 -0
- package/scripts/forge-upgrade-pod.cjs +368 -0
- package/scripts/forge.test.cjs +609 -0
- package/scripts/harden-knowledge.test.cjs +612 -0
- package/scripts/harness-g5-lite.cjs +243 -0
- package/scripts/harness-g5-lite.test.cjs +134 -0
- package/scripts/hn-carousel-contrato-entrada.test.cjs +96 -0
- package/scripts/migrate-squad-orchestration.test.cjs +146 -0
- package/scripts/migration-manifest-sf3.2.test.cjs +382 -0
- package/scripts/migration-manifest-sf3.3.test.cjs +170 -0
- package/scripts/migration-manifest-sf5.5.test.cjs +189 -0
- package/scripts/migration-manifest.cjs +1146 -0
- package/scripts/migration-manifest.test.cjs +186 -0
- package/scripts/npm-scope-invocation.test.cjs +194 -0
- package/scripts/owner-layer.cjs +442 -0
- package/scripts/owner-layer.test.cjs +358 -0
- package/scripts/pc-run-pipeline.cjs +289 -0
- package/scripts/pc-run-pipeline.test.cjs +256 -0
- package/scripts/pc-run-pod-countgte.test.cjs +66 -0
- package/scripts/pc-run-pod.cjs +1355 -0
- package/scripts/phash-gate.test.cjs +433 -0
- package/scripts/pod-export-compile.cjs +487 -0
- package/scripts/pod-export-compile.test.cjs +312 -0
- package/scripts/pod-export-executor.cjs +678 -0
- package/scripts/pod-export-executor.test.cjs +1341 -0
- package/scripts/pod-export-hardening.test.cjs +531 -0
- package/scripts/pod-export-postprocess.cjs +427 -0
- package/scripts/pod-export-postprocess.test.cjs +558 -0
- package/scripts/pod-export-queue-race.test.cjs +865 -0
- package/scripts/pod-export-release.cjs +473 -0
- package/scripts/pod-export-release.test.cjs +267 -0
- package/scripts/pod-export-service.cjs +1297 -0
- package/scripts/pod-export-service.test.cjs +489 -0
- package/scripts/pod-export-tools.cjs +470 -0
- package/scripts/pod-export-ui.cjs +805 -0
- package/scripts/pod-export-ui.test.cjs +301 -0
- package/scripts/pod-memory-budget.test.cjs +309 -0
- package/scripts/pod-memory-note.cjs +357 -0
- package/scripts/pod-memory-note.test.cjs +452 -0
- package/scripts/pod-memory.cjs +1252 -0
- package/scripts/pod-memory.test.cjs +951 -0
- package/scripts/render-png.test.cjs +307 -0
- package/scripts/smoke-isolation.test.cjs +100 -0
- package/scripts/smoke-pod-workflow.cjs +755 -0
- package/scripts/smoke-sweep.test.cjs +170 -0
- package/scripts/validate-adopt.js +235 -0
- package/scripts/validate-branding.js +84 -0
- package/scripts/validate-budgets.js +140 -0
- package/scripts/validate-fidelity-output.cjs +267 -0
- package/scripts/validate-fidelity-output.test.cjs +101 -0
- package/scripts/validate-install.js +230 -0
- package/scripts/validate-no-clone.cjs +220 -0
- package/scripts/validate-no-clone.test.cjs +77 -0
- package/scripts/validate-pipeline.cjs +145 -0
- package/scripts/validate-pod-workflow.cjs +878 -0
- package/scripts/validate-source-audit.cjs +170 -0
- package/scripts/validate-source-audit.test.cjs +90 -0
- package/scripts/validate-story-acs.cjs +862 -0
- package/scripts/validate-update-scope-motor.js +421 -0
- package/scripts/veto-checks/html-valid.cjs +92 -0
- package/scripts/veto-checks/lexical-check.cjs +62 -0
- package/scripts/veto-checks/phash-gate.cjs +589 -0
- package/scripts/veto-checks/render-gate.cjs +165 -0
- package/scripts/veto-checks/render-png.cjs +358 -0
- package/scripts/veto-checks.test.cjs +219 -0
- package/scripts/watcher-audit.cjs +178 -0
- package/template/.claude/CLAUDE.md +103 -0
- package/template/.claude/agents/pc-analyst.md +100 -0
- package/template/.claude/agents/pc-architect.md +32 -0
- package/template/.claude/agents/pc-dev.md +27 -0
- package/template/.claude/agents/pc-devops.md +45 -0
- package/template/.claude/agents/pc-planner.md +32 -0
- package/template/.claude/agents/pc-po.md +25 -0
- package/template/.claude/agents/pc-qa.md +25 -0
- package/template/.claude/commands/power/academy.md +9 -0
- package/template/.claude/commands/power/automation.md +9 -0
- package/template/.claude/commands/power/content-carousel.md +267 -0
- package/template/.claude/commands/power/content.md +9 -0
- package/template/.claude/commands/power/dev.md +9 -0
- package/template/.claude/commands/power/forge.md +224 -0
- package/template/.claude/commands/power/hormozi.md +221 -0
- package/template/.claude/commands/power/modo.md +57 -0
- package/template/.claude/hooks/README.md +114 -0
- package/template/.claude/hooks/context-budget.cjs +64 -0
- package/template/.claude/hooks/enforce-cycle-authority.cjs +457 -0
- package/template/.claude/hooks/enforce-git-push-authority.cjs +925 -0
- package/template/.claude/hooks/enforce-ready-oracles.cjs +502 -0
- package/template/.claude/rules/authority.md +76 -0
- package/template/.claude/rules/curadoria-de-conhecimento.md +30 -0
- package/template/.claude/rules/model-matrix.md +34 -0
- package/template/.claude/rules/orchestration.md +133 -0
- package/template/.claude/settings.json +50 -0
- package/template/.claude/skills/pc-apply-qa-fixes/SKILL.md +108 -0
- package/template/.claude/skills/pc-apply-qa-fixes/references/fix-loop.md +40 -0
- package/template/.claude/skills/pc-apply-qa-fixes/references/fix-request-template.md +70 -0
- package/template/.claude/skills/pc-arena/SKILL.md +96 -0
- package/template/.claude/skills/pc-close-story/SKILL.md +105 -0
- package/template/.claude/skills/pc-commit/SKILL.md +108 -0
- package/template/.claude/skills/pc-develop-story/SKILL.md +116 -0
- package/template/.claude/skills/pc-full-cycle/SKILL.md +150 -0
- package/template/.claude/skills/pc-pipeline/SKILL.md +63 -0
- package/template/.claude/skills/pc-pipeline/references/gate-templates.md +46 -0
- package/template/.claude/skills/pc-review-story/SKILL.md +129 -0
- package/template/.claude/skills/pc-review-story/references/false-positive-detection.md +50 -0
- package/template/.claude/skills/pc-review-story/references/nfr-assess.md +47 -0
- package/template/.claude/skills/pc-review-story/references/risk-profile.md +75 -0
- package/template/.claude/skills/pc-review-story/references/security-checklist.md +69 -0
- package/template/.claude/skills/pc-review-story/references/test-design.md +37 -0
- package/template/.claude/skills/pc-review-story/references/trace-requirements.md +46 -0
- package/template/.claude/skills/pc-run-pod/SKILL.md +123 -0
- package/template/.claude/skills/pc-validate-story/SKILL.md +114 -0
- package/template/.claude/skills/pc-wave/SKILL.md +133 -0
- package/template/.env.example +10 -0
- package/template/knowledge/analyst/brainstorming.md +40 -0
- package/template/knowledge/analyst/brief-skeleton.md +32 -0
- package/template/knowledge/analyst/competitor-skeleton.md +39 -0
- package/template/knowledge/analyst/elicitation-menu.md +49 -0
- package/template/knowledge/analyst/market-research-skeleton.md +36 -0
- package/template/knowledge/analyst/research-prompt-structure.md +48 -0
- package/template/knowledge/analyst/roi-skeleton.md +51 -0
- package/template/knowledge/voice-dna/README.md +99 -0
- package/template/knowledge/voice-dna/voice-dna.example.yaml +157 -0
- package/template/pods/README.md +64 -0
- package/template/pods/academy/README.md +37 -0
- package/template/pods/academy/agents/academy-chief.md +27 -0
- package/template/pods/academy/examples/palestra.example.md +142 -0
- package/template/pods/academy/pod-spec.json +44 -0
- package/template/pods/academy/templates/materiais-tmpl.md +17 -0
- package/template/pods/academy/templates/revisao-didatica-tmpl.md +20 -0
- package/template/pods/academy/workflows/wf-materiais-revisao.yaml +30 -0
- package/template/pods/authority-content/README.md +43 -0
- package/template/pods/authority-content/agents/content-chief.md +28 -0
- package/template/pods/authority-content/pod-spec.json +45 -0
- package/template/pods/authority-content/templates/criacao-na-voz-tmpl.md +17 -0
- package/template/pods/authority-content/templates/red-team-factual-tmpl.md +23 -0
- package/template/pods/authority-content/workflows/wf-criacao-redteam.yaml +30 -0
- package/template/pods/automation/README.md +37 -0
- package/template/pods/automation/agents/langgraph-architect.md +30 -0
- package/template/pods/automation/agents/n8n-architect.md +30 -0
- package/template/pods/automation/examples/README.md +46 -0
- package/template/pods/automation/examples/n8n-content-radar.example.json +380 -0
- package/template/pods/automation/knowledge/langgraph-patterns.md +126 -0
- package/template/pods/automation/knowledge/n8n-patterns.md +131 -0
- package/template/pods/automation/memory/README.md +16 -0
- package/template/pods/automation/memory/learnings.consolidated.md +8 -0
- package/template/pods/automation/memory/learnings.jsonl +0 -0
- package/template/pods/automation/pod-spec.json +109 -0
- package/template/pods/automation/specs/wf-langgraph-build.spec.json +107 -0
- package/template/pods/automation/squad-io.yaml +28 -0
- package/template/pods/automation/templates/lg-build-tmpl.md +17 -0
- package/template/pods/automation/templates/lg-design-tmpl.md +23 -0
- package/template/pods/automation/templates/lg-discover-tmpl.md +20 -0
- package/template/pods/automation/templates/lg-handoff-tmpl.md +20 -0
- package/template/pods/automation/templates/lg-test-tmpl.md +20 -0
- package/template/pods/automation/templates/n8n-build-tmpl.md +14 -0
- package/template/pods/automation/templates/n8n-design-tmpl.md +23 -0
- package/template/pods/automation/templates/n8n-discover-tmpl.md +20 -0
- package/template/pods/automation/templates/n8n-handoff-tmpl.md +20 -0
- package/template/pods/automation/templates/n8n-test-tmpl.md +20 -0
- package/template/pods/automation/workflows/wf-langgraph-build.smoke.json +11 -0
- package/template/pods/automation/workflows/wf-langgraph-build.yaml +64 -0
- package/template/pods/automation/workflows/wf-n8n-build.smoke.json +11 -0
- package/template/pods/automation/workflows/wf-n8n-build.yaml +65 -0
- package/template/pods/content-carousel/README.md +91 -0
- package/template/pods/content-carousel/agents/carousel-builder.md +28 -0
- package/template/pods/content-carousel/agents/carousel-visual.md +24 -0
- package/template/pods/content-carousel/agents/hn-curator.md +74 -0
- package/template/pods/content-carousel/config/heuristic-watcher-rubric.yaml +59 -0
- package/template/pods/content-carousel/knowledge/aquisicao-ancora.md +443 -0
- package/template/pods/content-carousel/knowledge/cta-brief.md +25 -0
- package/template/pods/content-carousel/knowledge/dono.example.yaml +45 -0
- package/template/pods/content-carousel/knowledge/identidade-visual-mascote.md +62 -0
- package/template/pods/content-carousel/knowledge/image-generation.md +62 -0
- package/template/pods/content-carousel/knowledge/selecao-de-ancora.md +60 -0
- package/template/pods/content-carousel/knowledge/slides-html.md +56 -0
- package/template/pods/content-carousel/memory/README.md +18 -0
- package/template/pods/content-carousel/memory/learnings.consolidated.md +6 -0
- package/template/pods/content-carousel/memory/learnings.jsonl +0 -0
- package/template/pods/content-carousel/pod-spec.json +100 -0
- package/template/pods/content-carousel/squad-io.yaml +10 -0
- package/template/pods/content-carousel/templates/assembly-tmpl.md +23 -0
- package/template/pods/content-carousel/templates/digest-tmpl.md +29 -0
- package/template/pods/content-carousel/templates/visuals-tmpl.md +20 -0
- package/template/pods/content-carousel/workflows/wf-content-carousel.smoke.json +23 -0
- package/template/pods/content-carousel/workflows/wf-content-carousel.yaml +80 -0
- package/template/pods/dev-ia/README.md +45 -0
- package/template/pods/dev-ia/agents/db-architect.md +33 -0
- package/template/pods/dev-ia/agents/ux-architect.md +29 -0
- package/template/pods/dev-ia/knowledge/db/migration-protocol.md +95 -0
- package/template/pods/dev-ia/knowledge/db/rls-patterns.md +101 -0
- package/template/pods/dev-ia/knowledge/db/schema-review-checklist.md +76 -0
- package/template/pods/dev-ia/memory/README.md +16 -0
- package/template/pods/dev-ia/memory/learnings.consolidated.md +8 -0
- package/template/pods/dev-ia/memory/learnings.jsonl +0 -0
- package/template/pods/dev-ia/pod-spec.json +34 -0
- package/template/pods/dev-ia/templates/schema-review-tmpl.md +23 -0
- package/template/pods/dev-ia/templates/ux-flow-review-tmpl.md +23 -0
- package/template/pods/dev-ia/tools/db-ops/backup-manager.sh +321 -0
- package/template/pods/dev-ia/tools/db-ops/best-practices-auditor.sh +534 -0
- package/template/pods/dev-ia/tools/db-ops/bootstrap-runner.sh +255 -0
- package/template/pods/dev-ia/tools/db-ops/data-loader.sh +616 -0
- package/template/pods/dev-ia/tools/db-ops/detect-database.sh +113 -0
- package/template/pods/dev-ia/tools/db-ops/explain-analyzer.sh +285 -0
- package/template/pods/dev-ia/tools/db-ops/health-checker.sh +323 -0
- package/template/pods/dev-ia/tools/db-ops/migration-runner.sh +308 -0
- package/template/pods/dev-ia/tools/db-ops/postgresql-schema-loader.sh +334 -0
- package/template/pods/dev-ia/tools/db-ops/query-runner.sh +171 -0
- package/template/pods/dev-ia/tools/db-ops/rls-policy-installer.sh +480 -0
- package/template/pods/dev-ia/tools/db-ops/rls-test-suite.sh +172 -0
- package/template/pods/dev-ia/tools/db-ops/schema-context-loader.sh +176 -0
- package/template/pods/hormozi-method/README.md +52 -0
- package/template/pods/hormozi-method/agents/acquisition-strategist.md +33 -0
- package/template/pods/hormozi-method/agents/conversion-copywriter.md +34 -0
- package/template/pods/hormozi-method/agents/monetization-architect.md +35 -0
- package/template/pods/hormozi-method/agents/offer-engineer.md +37 -0
- package/template/pods/hormozi-method/knowledge/core-four-leads.md +51 -0
- package/template/pods/hormozi-method/knowledge/grand-slam-offer.md +66 -0
- package/template/pods/hormozi-method/knowledge/growth-decision.md +38 -0
- package/template/pods/hormozi-method/knowledge/hooks-and-ads.md +56 -0
- package/template/pods/hormozi-method/knowledge/launch-email.md +31 -0
- package/template/pods/hormozi-method/knowledge/money-model.md +46 -0
- package/template/pods/hormozi-method/knowledge/opportunity-screening.md +44 -0
- package/template/pods/hormozi-method/knowledge/pages.md +25 -0
- package/template/pods/hormozi-method/knowledge/pos-lancamento-leads.md +56 -0
- package/template/pods/hormozi-method/knowledge/pricing.md +36 -0
- package/template/pods/hormozi-method/knowledge/retention-ltv.md +37 -0
- package/template/pods/hormozi-method/knowledge/scale-roadmap.md +35 -0
- package/template/pods/hormozi-method/knowledge/swipefiles-curados.md +44 -0
- package/template/pods/hormozi-method/knowledge/value-equation-diagnostic.md +77 -0
- package/template/pods/hormozi-method/memory/README.md +16 -0
- package/template/pods/hormozi-method/memory/learnings.consolidated.md +6 -0
- package/template/pods/hormozi-method/memory/learnings.jsonl +0 -0
- package/template/pods/hormozi-method/pod-spec.json +50 -0
- package/template/pods/hormozi-method/specs/wf-full-launch-sequence.spec.json +149 -0
- package/template/pods/hormozi-method/specs/wf-lead-magnet-pipeline.spec.json +125 -0
- package/template/pods/hormozi-method/specs/wf-money-model-design.spec.json +117 -0
- package/template/pods/hormozi-method/specs/wf-paid-ads-campaign.spec.json +132 -0
- package/template/pods/hormozi-method/specs/wf-scaling-diagnostic.spec.json +121 -0
- package/template/pods/hormozi-method/squad-io.yaml +43 -0
- package/template/pods/hormozi-method/templates/conversion-copywriter-tmpl.md +20 -0
- package/template/pods/hormozi-method/templates/launch-execucao-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/launch-fechamento-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/launch-pos-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/launch-preparacao-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/lm-copy-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/lm-distribuicao-tmpl.md +18 -0
- package/template/pods/hormozi-method/templates/lm-estrategia-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/mm-arquitetura-tmpl.md +18 -0
- package/template/pods/hormozi-method/templates/mm-copy-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/mm-diagnostico-tmpl.md +14 -0
- package/template/pods/hormozi-method/templates/monetization-architect-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/offer-engineer-tmpl.md +20 -0
- package/template/pods/hormozi-method/templates/paid-criativos-tmpl.md +19 -0
- package/template/pods/hormozi-method/templates/paid-fundacao-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/paid-teste-escala-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/scaling-diagnostico-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/scaling-priorizacao-tmpl.md +17 -0
- package/template/pods/hormozi-method/templates/scaling-roadmap-tmpl.md +17 -0
- package/template/pods/hormozi-method/workflows/wf-full-launch-sequence.smoke.json +12 -0
- package/template/pods/hormozi-method/workflows/wf-full-launch-sequence.yaml +56 -0
- package/template/pods/hormozi-method/workflows/wf-grand-slam-offer.smoke.json +11 -0
- package/template/pods/hormozi-method/workflows/wf-grand-slam-offer.yaml +45 -0
- package/template/pods/hormozi-method/workflows/wf-lead-magnet-pipeline.smoke.json +9 -0
- package/template/pods/hormozi-method/workflows/wf-lead-magnet-pipeline.yaml +45 -0
- package/template/pods/hormozi-method/workflows/wf-money-model-design.smoke.json +11 -0
- package/template/pods/hormozi-method/workflows/wf-money-model-design.yaml +45 -0
- package/template/pods/hormozi-method/workflows/wf-paid-ads-campaign.smoke.json +9 -0
- package/template/pods/hormozi-method/workflows/wf-paid-ads-campaign.yaml +46 -0
- package/template/pods/hormozi-method/workflows/wf-scaling-diagnostic.smoke.json +11 -0
- package/template/pods/hormozi-method/workflows/wf-scaling-diagnostic.yaml +44 -0
- package/template/pods/squad-forge/README.md +53 -0
- package/template/pods/squad-forge/agents/forge-chief.md +31 -0
- package/template/pods/squad-forge/agents/mind-extractor.md +29 -0
- package/template/pods/squad-forge/agents/pod-critic.md +26 -0
- package/template/pods/squad-forge/config/heuristic-watcher-rubric.yaml +74 -0
- package/template/pods/squad-forge/knowledge/fidelity-rubric.yaml +176 -0
- package/template/pods/squad-forge/knowledge/g6-memory-check.md +17 -0
- package/template/pods/squad-forge/knowledge/memory-ingest.md +16 -0
- package/template/pods/squad-forge/knowledge/source-audit-rubric.yaml +60 -0
- package/template/pods/squad-forge/memory/README.md +16 -0
- package/template/pods/squad-forge/memory/learnings.consolidated.md +8 -0
- package/template/pods/squad-forge/memory/learnings.jsonl +0 -0
- package/template/pods/squad-forge/pod-spec.json +166 -0
- package/template/pods/squad-forge/squad-io.yaml +13 -0
- package/template/pods/squad-forge/tasks/pipeline.md +158 -0
- package/template/pods/squad-forge/templates/discovery-tmpl.md +40 -0
- package/template/pods/squad-forge/templates/generation-tmpl.md +18 -0
- package/template/pods/squad-forge/templates/red-team-tmpl.md +17 -0
- package/template/pods/squad-forge/templates/register-tmpl.md +17 -0
- package/template/pods/squad-forge/templates/workflow-tmpl.yaml +101 -0
- package/template/pods/squad-forge/workflows/wf-forge.smoke.json +12 -0
- 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
|
+
};
|