@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,301 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* pod-export-ui.test.cjs — SMOKE HERMÉTICO da UI web de checkpoint (Story SF10.5, F3).
|
|
5
|
+
*
|
|
6
|
+
* NÃO exige VPS/nginx/rede externa. Sobe o servidor `node:http` em 127.0.0.1:0 (porta efêmera) e
|
|
7
|
+
* exercita o fluxo real (login → seletor → lista → checkpoint → approve/reject) com o backend de
|
|
8
|
+
* approve INJETADO (sem spawn do executor) OU o `approveRun` REAL de SF10.4 (que RECUSA antes de
|
|
9
|
+
* qualquer spawn) para o teste adversarial de bypass. Cobre os 11 ACs testáveis sem VPS + os 3
|
|
10
|
+
* pontos de segurança que o /security-review ataca:
|
|
11
|
+
* (S1) Preview XSS — as peças são renderizadas em <iframe sandbox> (sem allow-same-origin +
|
|
12
|
+
* allow-scripts) e servidas por /preview com CSP `sandbox; script-src 'none'`,
|
|
13
|
+
* sem Set-Cookie; traversal e acesso anônimo RECUSADOS.
|
|
14
|
+
* (S2) UI ≠ gate — POST /approve forjado (operador não-aprovador / sem segredo / segredo
|
|
15
|
+
* errado) é RECUSADO pelo backend REAL (approveRun de SF10.4), UI devolve
|
|
16
|
+
* erro GENÉRICO (sem oracle). A UI nunca é a única barreira.
|
|
17
|
+
* (S3) CSRF/sessão — cookie HMAC não-forjável (corpo adulterado/expirado/chave errada → null),
|
|
18
|
+
* SameSite=Strict+HttpOnly, verificação de Origin e token CSRF nas ações.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const http = require('node:http');
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
|
|
26
|
+
const ui = require('./pod-export-ui.cjs');
|
|
27
|
+
const svc = require('./pod-export-service.cjs');
|
|
28
|
+
|
|
29
|
+
let failures = 0;
|
|
30
|
+
function assert(ok, msg) { if (ok) console.log(`[OK ] ${msg}`); else { console.error(`[FALHA] ${msg}`); failures++; } }
|
|
31
|
+
function tmpDir(label) { return fs.mkdtempSync(path.join(os.tmpdir(), `sf10.5-${label}-`)); }
|
|
32
|
+
|
|
33
|
+
const SESSION_KEY = 'k'.repeat(48); // >=32 ch
|
|
34
|
+
const TEAM_SECRET = 'senha-da-equipe';
|
|
35
|
+
const APPROVER_SECRET = 'segredo-do-aprovador';
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Fixture: queue com um run em awaiting/ + runDir com peças HTML, + identities + hashes.
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
function makeFixture() {
|
|
41
|
+
const root = tmpDir('fix');
|
|
42
|
+
const queueDir = path.join(root, 'queue');
|
|
43
|
+
const runsRoot = path.join(root, 'runs');
|
|
44
|
+
const bundleDir = path.join(root, 'bundle');
|
|
45
|
+
fs.mkdirSync(bundleDir, { recursive: true });
|
|
46
|
+
// workflow.yaml com criteria do checkpoint (para checkpointCriterion).
|
|
47
|
+
fs.writeFileSync(path.join(bundleDir, 'workflow.yaml'),
|
|
48
|
+
'steps:\n - id: montagem\n checkpoint: { blocking: true, criteria: "7 peças: capa + 5 + CTA" }\n');
|
|
49
|
+
// fila: um run pausado em awaiting/.
|
|
50
|
+
const dirs = svc.ensureQueue(queueDir);
|
|
51
|
+
const runId = 'pex-hn-carousel-2026';
|
|
52
|
+
fs.writeFileSync(path.join(dirs.awaiting, `${runId}.json`), JSON.stringify({ pod: 'hn-carousel', started_by: 'bob' }));
|
|
53
|
+
fs.writeFileSync(path.join(dirs.awaiting, `${runId}.result.json`), JSON.stringify({ runId, kind: 'awaiting', started_by: 'bob', awaiting: 'montagem', at: '2026-07-31T10:00:00Z' }));
|
|
54
|
+
// runDir com state.json (para findStateByRunId) + uma peça HTML HOSTIL (script/onerror).
|
|
55
|
+
const runDir = path.join(runsRoot, 'hn-carousel', 'wf', runId);
|
|
56
|
+
fs.mkdirSync(path.join(runDir, 'pieces'), { recursive: true });
|
|
57
|
+
fs.writeFileSync(path.join(runDir, 'state.json'), JSON.stringify({ run_id: runId, steps: {} }));
|
|
58
|
+
fs.writeFileSync(path.join(runDir, 'pieces', '1-capa.html'), '<html><body><script>window.top.location="http://evil"</script><img src=x onerror="document.cookie">CAPA</body></html>');
|
|
59
|
+
fs.writeFileSync(path.join(runDir, 'pieces', '7-cta.html'), '<html><body>CTA</body></html>');
|
|
60
|
+
// identities: daniel = aprovador, alice = não.
|
|
61
|
+
const identitiesFile = path.join(root, 'identities.json');
|
|
62
|
+
fs.writeFileSync(identitiesFile, JSON.stringify({ identities: [{ id: 'daniel', label: 'Daniel', is_approver: true }, { id: 'alice', label: 'Alice' }] }));
|
|
63
|
+
|
|
64
|
+
const cfg = ui.loadUiConfig({
|
|
65
|
+
PODEXPORT_QUEUE_DIR: queueDir,
|
|
66
|
+
PODEXPORT_RUNS_DIR: runsRoot,
|
|
67
|
+
PODEXPORT_BUNDLE_DIR: bundleDir,
|
|
68
|
+
PODEXPORT_IDENTITIES: identitiesFile,
|
|
69
|
+
PODEXPORT_UI_TEAM_HASH: svc.hashApproverSecret(TEAM_SECRET),
|
|
70
|
+
PODEXPORT_UI_SESSION_KEY: SESSION_KEY,
|
|
71
|
+
PODEXPORT_APPROVER_SECRET_HASH: svc.hashApproverSecret(APPROVER_SECRET),
|
|
72
|
+
PODEXPORT_UI_INSECURE: '1', // sem Secure para o cliente de teste em http.
|
|
73
|
+
});
|
|
74
|
+
// O approveRun real lê PODEXPORT_IDENTITIES do env (loadIdentities), então exponha no process.env.
|
|
75
|
+
process.env.PODEXPORT_IDENTITIES = identitiesFile;
|
|
76
|
+
return { cfg, runId, root };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Cliente HTTP mínimo.
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
function request(server, { method = 'GET', pathUrl = '/', cookie, body, origin } = {}) {
|
|
83
|
+
const { port } = server.address();
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const headers = {};
|
|
86
|
+
if (cookie) headers.Cookie = cookie;
|
|
87
|
+
if (body != null) { headers['Content-Type'] = 'application/x-www-form-urlencoded'; headers['Content-Length'] = Buffer.byteLength(body); }
|
|
88
|
+
if (origin !== null) headers.Origin = origin || `http://127.0.0.1:${port}`;
|
|
89
|
+
const req = http.request({ host: '127.0.0.1', port, method, path: pathUrl, headers }, (res) => {
|
|
90
|
+
let data = ''; res.on('data', (c) => { data += c; });
|
|
91
|
+
res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: data }));
|
|
92
|
+
});
|
|
93
|
+
req.on('error', reject);
|
|
94
|
+
if (body != null) req.write(body);
|
|
95
|
+
req.end();
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cookieFrom(res) {
|
|
100
|
+
const sc = res.headers['set-cookie'];
|
|
101
|
+
if (!sc) return null;
|
|
102
|
+
const m = String(sc[0]).match(/pex_ui=([^;]*)/);
|
|
103
|
+
return m ? `pex_ui=${m[1]}` : null;
|
|
104
|
+
}
|
|
105
|
+
function sidFrom(cookie) {
|
|
106
|
+
const token = cookie.replace('pex_ui=', '');
|
|
107
|
+
const payload = ui.verifySession(token, SESSION_KEY);
|
|
108
|
+
return payload && payload.sid;
|
|
109
|
+
}
|
|
110
|
+
function form(obj) { return new URLSearchParams(obj).toString(); }
|
|
111
|
+
|
|
112
|
+
// ===========================================================================
|
|
113
|
+
// UNIT — cripto de sessão/CSRF (S3), confinamento (S1), fila.
|
|
114
|
+
// ===========================================================================
|
|
115
|
+
function unitTests() {
|
|
116
|
+
console.log('\n--- UNIT: sessão/CSRF/confinamento/fila ---');
|
|
117
|
+
const t = ui.signSession({ sid: 'abc', operator: 'daniel', exp: Date.now() + 1000 }, SESSION_KEY);
|
|
118
|
+
assert(ui.verifySession(t, SESSION_KEY).operator === 'daniel', 'sessão válida decodifica o operador');
|
|
119
|
+
assert(ui.verifySession(t, 'chave-errada-de-32-chars-1234567890') === null, 'sessão com CHAVE errada → null (não-forjável)');
|
|
120
|
+
const idx = t.lastIndexOf('.');
|
|
121
|
+
const tampered = `${t.slice(0, idx - 4)}AAAA${t.slice(idx)}`;
|
|
122
|
+
assert(ui.verifySession(tampered, SESSION_KEY) === null, 'corpo ADULTERADO (mesma assinatura) → null');
|
|
123
|
+
const expired = ui.signSession({ sid: 'x', operator: null, exp: Date.now() - 1 }, SESSION_KEY);
|
|
124
|
+
assert(ui.verifySession(expired, SESSION_KEY) === null, 'sessão EXPIRADA → null');
|
|
125
|
+
assert(ui.verifyCsrf(ui.csrfToken('sid1', SESSION_KEY), 'sid1', SESSION_KEY), 'CSRF token válido casa');
|
|
126
|
+
assert(!ui.verifyCsrf(ui.csrfToken('sid1', SESSION_KEY), 'sid2', SESSION_KEY), 'CSRF de outro sid NÃO casa');
|
|
127
|
+
assert(!ui.verifyCsrf('', 'sid1', SESSION_KEY), 'CSRF vazio → false');
|
|
128
|
+
|
|
129
|
+
// Confinamento de path (anti-traversal no preview).
|
|
130
|
+
const rd = tmpDir('rd');
|
|
131
|
+
assert(ui.confineToRunDir(rd, 'pieces/1.html') !== null, 'path relativo legítimo confina OK');
|
|
132
|
+
assert(ui.confineToRunDir(rd, '../../etc/passwd') === null, 'traversal ../../ → RECUSADO');
|
|
133
|
+
const absTry = ui.confineToRunDir(rd, '/etc/passwd');
|
|
134
|
+
assert(absTry === null || absTry.startsWith(path.resolve(rd) + path.sep), 'path absoluto NUNCA escapa o runDir (confinado, não /etc/passwd real)');
|
|
135
|
+
assert(ui.confineToRunDir(rd, 'a/../../b') === null, 'traversal disfarçada a/../../b → RECUSADO');
|
|
136
|
+
assert(!ui.safeRunId('../x'), 'runId com .. → inválido');
|
|
137
|
+
assert(ui.safeRunId('pex-abc-123'), 'runId legítimo → válido');
|
|
138
|
+
|
|
139
|
+
// Rate limiter.
|
|
140
|
+
const rl = ui.createRateLimiter(2, 1000);
|
|
141
|
+
assert(rl.check('a', 0).allowed && rl.check('a', 1).allowed && !rl.check('a', 2).allowed, 'rate-limit barra a 3ª tentativa na janela');
|
|
142
|
+
|
|
143
|
+
// escapeHtml (anti-XSS na própria UI).
|
|
144
|
+
assert(ui.escapeHtml('<script>&"') === '<script>&"', 'escapeHtml neutraliza < > & "');
|
|
145
|
+
|
|
146
|
+
// listCheckpoints lê awaiting/ do disco.
|
|
147
|
+
const { cfg } = makeFixture();
|
|
148
|
+
const list = ui.listCheckpoints(cfg);
|
|
149
|
+
assert(list.length === 1 && list[0].pod === 'hn-carousel' && list[0].startedBy === 'bob', 'listCheckpoints lê awaiting/ (pod+started_by do disco)');
|
|
150
|
+
|
|
151
|
+
// rejectRun move awaiting→blocked com motivo.
|
|
152
|
+
const rej = ui.rejectRun(cfg, list[0].runId, { reason: 'fora do brand', rejectedBy: 'alice' });
|
|
153
|
+
assert(rej.ok && rej.kind === 'blocked', 'rejectRun move o run para blocked/');
|
|
154
|
+
const blockedRes = JSON.parse(fs.readFileSync(path.join(cfg.queueDir, 'blocked', `${list[0].runId}.result.json`), 'utf8'));
|
|
155
|
+
assert(blockedRes.rejected === true && blockedRes.reason === 'fora do brand' && blockedRes.rejected_by === 'alice', 'reject grava motivo+quem rejeitou');
|
|
156
|
+
assert(ui.listCheckpoints(cfg).length === 0, 'run rejeitado some de awaiting/');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ===========================================================================
|
|
160
|
+
// HTTP — fluxo real + adversarial. approveRun REAL de SF10.4 (recusa pré-spawn).
|
|
161
|
+
// ===========================================================================
|
|
162
|
+
async function httpTests() {
|
|
163
|
+
console.log('\n--- HTTP: login/seletor/lista/checkpoint/preview/approve/reject ---');
|
|
164
|
+
const { cfg, runId } = makeFixture();
|
|
165
|
+
const approveSpy = [];
|
|
166
|
+
// approveFn stub SÓ para o caminho de SUCESSO (evita spawn); denials usam o REAL abaixo.
|
|
167
|
+
const server = ui.createServer(cfg, {
|
|
168
|
+
approveRun: async (c, id, deps) => {
|
|
169
|
+
approveSpy.push({ id, asId: deps.asId, secret: deps.approverSecret });
|
|
170
|
+
// Delega ao REAL para HONRAR o gate (recusa não-aprovador/sem-segredo/segredo-errado).
|
|
171
|
+
return svc.approveRun(c, id, { ...deps, spawn: () => { const e = require('events').EventEmitter; const ch = new e(); process.nextTick(() => { ch.emit('exit', 0); }); ch.stdout = new e(); return ch; } });
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
175
|
+
|
|
176
|
+
// AC1 — acesso exige login: GET / sem cookie → redireciona /login.
|
|
177
|
+
let res = await request(server, { pathUrl: '/' });
|
|
178
|
+
assert(res.status === 303 && res.headers.location === '/login', 'GET / sem sessão → redirect /login (AC1)');
|
|
179
|
+
|
|
180
|
+
// Login errado → 401 genérico (sem distinguir).
|
|
181
|
+
res = await request(server, { method: 'POST', pathUrl: '/login', body: form({ password: 'errada' }) });
|
|
182
|
+
assert(res.status === 401 && /Senha incorreta/.test(res.body) && !res.headers['set-cookie'], 'senha errada → 401 genérico, sem cookie');
|
|
183
|
+
|
|
184
|
+
// Login certo → cookie HttpOnly + SameSite=Strict.
|
|
185
|
+
res = await request(server, { method: 'POST', pathUrl: '/login', body: form({ password: TEAM_SECRET }) });
|
|
186
|
+
const cookie = cookieFrom(res);
|
|
187
|
+
const sc = String(res.headers['set-cookie'][0]);
|
|
188
|
+
assert(res.status === 303 && cookie, 'login correto → 303 + cookie de sessão (AC1)');
|
|
189
|
+
assert(/HttpOnly/.test(sc) && /SameSite=Strict/.test(sc), 'cookie é HttpOnly + SameSite=Strict (S3/CSRF)');
|
|
190
|
+
const sid = sidFrom(cookie);
|
|
191
|
+
|
|
192
|
+
// Logado mas sem operador → GET / mostra o SELETOR (AC2).
|
|
193
|
+
res = await request(server, { pathUrl: '/', cookie });
|
|
194
|
+
assert(res.status === 200 && /Quem está usando/.test(res.body) && /Daniel/.test(res.body) && /Alice/.test(res.body), 'sem operador → seletor lista identidades de SF10.4 (AC2)');
|
|
195
|
+
|
|
196
|
+
// Seletor sem CSRF → 403 (S3).
|
|
197
|
+
res = await request(server, { method: 'POST', pathUrl: '/operator', cookie, body: form({ operator: 'alice' }) });
|
|
198
|
+
assert(res.status === 403, 'POST /operator sem token CSRF → 403 (S3)');
|
|
199
|
+
|
|
200
|
+
// Seletor com CSRF válido → grava operador.
|
|
201
|
+
const csrf = ui.csrfToken(sid, SESSION_KEY);
|
|
202
|
+
res = await request(server, { method: 'POST', pathUrl: '/operator', cookie, body: form({ operator: 'alice', _csrf: csrf }) });
|
|
203
|
+
const cookieAlice = cookieFrom(res) || cookie;
|
|
204
|
+
assert(res.status === 303, 'POST /operator com CSRF → 303 (operador atribuído, AC2)');
|
|
205
|
+
|
|
206
|
+
// Lista de checkpoints mostra o run em awaiting/ (AC3).
|
|
207
|
+
res = await request(server, { pathUrl: '/', cookie: cookieAlice });
|
|
208
|
+
assert(res.status === 200 && /hn-carousel/.test(res.body) && /Operando como/.test(res.body), 'lista mostra run awaiting/ + operador (AC3)');
|
|
209
|
+
|
|
210
|
+
// Tela de checkpoint: iframe SANDBOX + critério em destaque (AC3/S1).
|
|
211
|
+
res = await request(server, { pathUrl: `/checkpoint?run=${runId}`, cookie: cookieAlice });
|
|
212
|
+
assert(res.status === 200 && /7 peças: capa/.test(res.body), 'checkpoint mostra o CRITÉRIO em destaque (AC3)');
|
|
213
|
+
const iframeTags = res.body.match(/<iframe[^>]*>/g) || [];
|
|
214
|
+
assert(iframeTags.length >= 1 && iframeTags.every((t) => /sandbox/.test(t) && !/allow-same-origin/.test(t) && !/allow-scripts/.test(t)),
|
|
215
|
+
'preview em <iframe sandbox> SEM allow-same-origin/allow-scripts (S1 preview XSS)');
|
|
216
|
+
// Operador alice (não-aprovador): SEM form de aprovar, COM aviso honesto (DF3-3).
|
|
217
|
+
assert(/Aprovação restrita/.test(res.body) && !/name="secret"/.test(res.body), 'não-aprovador: aviso honesto, sem campo de segredo (AC4/DF3-3)');
|
|
218
|
+
|
|
219
|
+
// /preview: sandbox-CSP, nosniff, SEM Set-Cookie (S1).
|
|
220
|
+
const sidAlice = sidFrom(cookieAlice);
|
|
221
|
+
res = await request(server, { pathUrl: `/preview/${runId}/pieces/1-capa.html`, cookie: cookieAlice });
|
|
222
|
+
assert(res.status === 200 && /sandbox/.test(res.headers['content-security-policy']) && /script-src 'none'/.test(res.headers['content-security-policy']),
|
|
223
|
+
'/preview serve a peça com CSP sandbox + script-src none (S1)');
|
|
224
|
+
assert(res.headers['x-content-type-options'] === 'nosniff' && !res.headers['set-cookie'], '/preview: nosniff e NUNCA Set-Cookie (S1)');
|
|
225
|
+
assert(/CAPA/.test(res.body), '/preview devolve o HTML da peça (conteúdo neutralizado pelo sandbox, não executa)');
|
|
226
|
+
|
|
227
|
+
// /preview traversal → recusado (S1).
|
|
228
|
+
res = await request(server, { pathUrl: `/preview/${runId}/..%2f..%2f..%2fstate.json`, cookie: cookieAlice });
|
|
229
|
+
assert(res.status === 400, '/preview com traversal → RECUSADO (S1)');
|
|
230
|
+
// /preview sem sessão → 403 (não vaza conteúdo do run).
|
|
231
|
+
res = await request(server, { pathUrl: `/preview/${runId}/pieces/1-capa.html` });
|
|
232
|
+
assert(res.status === 403, '/preview sem sessão → 403 (S1)');
|
|
233
|
+
|
|
234
|
+
// ---- ADVERSARIAL (S2): backend é o gate, não a UI ----
|
|
235
|
+
// (a) operador NÃO-aprovador (alice) forja POST /approve com CSRF válido → backend RECUSA.
|
|
236
|
+
res = await request(server, { method: 'POST', pathUrl: '/approve', cookie: cookieAlice, body: form({ run: runId, secret: APPROVER_SECRET, _csrf: ui.csrfToken(sidAlice, SESSION_KEY) }) });
|
|
237
|
+
assert(res.status === 403 && /Não foi possível aprovar/.test(res.body), 'alice (não-aprovador) com segredo CERTO → RECUSADO pelo backend, erro genérico (S2/AC4)');
|
|
238
|
+
assert(ui.listCheckpoints(cfg).length === 1, 'run continua em awaiting/ após approve recusado (nada aprovado)');
|
|
239
|
+
|
|
240
|
+
// Troca para daniel (aprovador).
|
|
241
|
+
res = await request(server, { method: 'POST', pathUrl: '/operator', cookie: cookieAlice, body: form({ operator: 'daniel', _csrf: ui.csrfToken(sidAlice, SESSION_KEY) }) });
|
|
242
|
+
const cookieDaniel = cookieFrom(res) || cookieAlice;
|
|
243
|
+
const sidDaniel = sidFrom(cookieDaniel);
|
|
244
|
+
const csrfDaniel = ui.csrfToken(sidDaniel, SESSION_KEY);
|
|
245
|
+
|
|
246
|
+
// (b) daniel SEM segredo → RECUSADO.
|
|
247
|
+
res = await request(server, { method: 'POST', pathUrl: '/approve', cookie: cookieDaniel, body: form({ run: runId, secret: '', _csrf: csrfDaniel }) });
|
|
248
|
+
assert(res.status === 403 && /Não foi possível aprovar/.test(res.body), 'daniel SEM segredo → RECUSADO (S2/AC4)');
|
|
249
|
+
// (c) daniel com segredo ERRADO → RECUSADO.
|
|
250
|
+
res = await request(server, { method: 'POST', pathUrl: '/approve', cookie: cookieDaniel, body: form({ run: runId, secret: 'chute-errado', _csrf: csrfDaniel }) });
|
|
251
|
+
assert(res.status === 403 && /Não foi possível aprovar/.test(res.body), 'daniel com segredo ERRADO → RECUSADO (S2/AC4)');
|
|
252
|
+
|
|
253
|
+
// Tela do aprovador: mostra o form de segredo com a frase obrigatória (DF3-4).
|
|
254
|
+
res = await request(server, { pathUrl: `/checkpoint?run=${runId}`, cookie: cookieDaniel });
|
|
255
|
+
assert(/name="secret"/.test(res.body) && /não autoriza sozinho/.test(res.body), 'aprovador: form de segredo + "não autoriza sozinho" (DF3-4)');
|
|
256
|
+
|
|
257
|
+
// (d) daniel com segredo CERTO → APROVADO (via stub spawn; backend autoriza).
|
|
258
|
+
res = await request(server, { method: 'POST', pathUrl: '/approve', cookie: cookieDaniel, body: form({ run: runId, secret: APPROVER_SECRET, _csrf: csrfDaniel }) });
|
|
259
|
+
assert(res.status === 303 && /flash=approved/.test(res.headers.location), 'daniel com segredo CERTO → aprovado (backend autoriza; UI só repassa)');
|
|
260
|
+
assert(approveSpy.some((c) => c.asId === 'daniel' && c.secret === APPROVER_SECRET), 'UI repassou asId=operador + segredo ao approveRun (não decide sozinha, S2)');
|
|
261
|
+
|
|
262
|
+
// Origin de outra origem numa ação de estado → recusado (defesa em profundidade além do SameSite).
|
|
263
|
+
res = await request(server, { method: 'POST', pathUrl: '/reject', cookie: cookieDaniel, origin: 'http://evil.example', body: form({ run: runId, reason: 'x', _csrf: csrfDaniel }) });
|
|
264
|
+
assert(res.status === 403, 'POST com Origin cross-site → 403 (CSRF defesa em profundidade, S3)');
|
|
265
|
+
|
|
266
|
+
// Reject legítimo (com CSRF+Origin) devolve o run com motivo (AC).
|
|
267
|
+
res = await request(server, { method: 'POST', pathUrl: '/reject', cookie: cookieDaniel, body: form({ run: runId, reason: 'capa é cópia', _csrf: csrfDaniel }) });
|
|
268
|
+
assert(res.status === 303 && /flash=rejected/.test(res.headers.location), 'reject com motivo → 303 (run devolvido)');
|
|
269
|
+
|
|
270
|
+
// Health sem auth.
|
|
271
|
+
res = await request(server, { pathUrl: '/health' });
|
|
272
|
+
assert(res.status === 200 && res.body === 'ok', '/health responde sem auth');
|
|
273
|
+
|
|
274
|
+
await new Promise((r) => server.close(r));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ===========================================================================
|
|
278
|
+
// DIRETO no backend (SF10.4): o gate independe da UI (recusa pré-spawn, sem rede).
|
|
279
|
+
// ===========================================================================
|
|
280
|
+
async function backendGateTests() {
|
|
281
|
+
console.log('\n--- BACKEND: approveRun de SF10.4 é o gate real (independe da UI) ---');
|
|
282
|
+
const { cfg } = makeFixture();
|
|
283
|
+
const hash = svc.hashApproverSecret(APPROVER_SECRET);
|
|
284
|
+
const base = { env: process.env, approverSecretHash: hash, identitiesFile: cfg.identitiesFile };
|
|
285
|
+
let r = await svc.approveRun(cfg, 'qualquer', { ...base, asId: 'alice', approverSecret: APPROVER_SECRET });
|
|
286
|
+
assert(r.denied && r.code === 3, 'chamada DIRETA: alice (não-aprovador) + segredo certo → RECUSADO (pré-spawn)');
|
|
287
|
+
r = await svc.approveRun(cfg, 'qualquer', { ...base, asId: 'daniel', approverSecret: '' });
|
|
288
|
+
assert(r.denied, 'chamada DIRETA: daniel sem segredo → RECUSADO');
|
|
289
|
+
r = await svc.approveRun(cfg, 'qualquer', { ...base, asId: 'daniel', approverSecret: 'errado' });
|
|
290
|
+
assert(r.denied, 'chamada DIRETA: daniel segredo errado → RECUSADO');
|
|
291
|
+
assert(!/segredo/i.test(JSON.stringify(r).replace(/segredo do aprovador (inválido|ausente)/gi, '')), 'razão de recusa nunca contém o valor do segredo');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
(async () => {
|
|
296
|
+
unitTests();
|
|
297
|
+
await httpTests();
|
|
298
|
+
await backendGateTests();
|
|
299
|
+
console.log(`\n${failures === 0 ? '[SUCESSO]' : '[FALHOU]'} pod-export-ui: ${failures} falha(s).`);
|
|
300
|
+
process.exit(failures === 0 ? 0 : 1);
|
|
301
|
+
})().catch((e) => { console.error('[ERRO NO TESTE]', e); process.exit(1); });
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
/**
|
|
4
|
+
* pod-memory-budget.test.cjs — Story SF11.4 / ADR-022 (DF-SF11.4-1, -2, -3).
|
|
5
|
+
*
|
|
6
|
+
* O QUE ESTA SUÍTE PROVA (por execução, nunca por leitura de código):
|
|
7
|
+
* AC4 A poda por orçamento FALA: `consolidate()` devolve `evicted` não-vazio,
|
|
8
|
+
* `needsCuration === true`, e o RUNNER emite a linha "[memory] AVISO: ..." em
|
|
9
|
+
* STDERR nomeando o que saiu. Kill-test junto: com o `evicted` sabotado (código
|
|
10
|
+
* pré-SF11.4), a poda volta a ser silenciosa e as asserções ficariam VERMELHAS —
|
|
11
|
+
* prova de que a fixture exercita o mecanismo, e não o relato dele.
|
|
12
|
+
* AC5 CONTRAPROVA da tabela de 8 promoções do ADR-022 §C4: promovendo heurísticas
|
|
13
|
+
* uma a uma sob o código NOVO, `ranked` NÃO caminha para zero e nenhum passo
|
|
14
|
+
* descarta em silêncio (sob o código antigo, 8 promoções zeravam a memória ativa
|
|
15
|
+
* com `needsCuration:false` em todos os passos).
|
|
16
|
+
* AC10 A trilha auditável é append-only DE VERDADE: linha já escrita sobrevive mesmo
|
|
17
|
+
* quando o registro-fonte some do log cru.
|
|
18
|
+
*
|
|
19
|
+
* Node >= 18, sem dependências externas.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const fs = require('node:fs');
|
|
23
|
+
const os = require('node:os');
|
|
24
|
+
const path = require('node:path');
|
|
25
|
+
const { spawnSync } = require('node:child_process');
|
|
26
|
+
|
|
27
|
+
const mem = require('./pod-memory.cjs');
|
|
28
|
+
const { consolidate, memoryPaths, TOKEN_BUDGET, TOP_N } = mem;
|
|
29
|
+
|
|
30
|
+
const failures = [];
|
|
31
|
+
let passed = 0;
|
|
32
|
+
function ok(label, cond, extra) {
|
|
33
|
+
if (cond) { passed += 1; console.log(`[OK ] ${label}`); return; }
|
|
34
|
+
failures.push(label);
|
|
35
|
+
console.log(`[FALHA] ${label}${extra ? ` — ${String(extra).slice(0, 400)}` : ''}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function tmpPod(label) {
|
|
39
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `sf11.4-${label}-`));
|
|
40
|
+
const podDir = path.join(tmp, 'p');
|
|
41
|
+
fs.mkdirSync(path.join(podDir, 'memory'), { recursive: true });
|
|
42
|
+
return { tmp, podDir };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Heurística longa e determinística — o pool estoura o teto com ~30 delas. */
|
|
46
|
+
function bulky(i, ts) {
|
|
47
|
+
return {
|
|
48
|
+
id: `F${i}`,
|
|
49
|
+
ts,
|
|
50
|
+
trigger: `heuristica de campo numero ${i} da rodada de producao`,
|
|
51
|
+
recommendation: `recomendacao verificavel numero ${i} — ${'texto com tamanho realista de heuristica de campo '.repeat(3)}`,
|
|
52
|
+
evidence: { count: 1 },
|
|
53
|
+
tier: 'normal',
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Log cru com N heurísticas (ts crescente ⇒ as de índice BAIXO são a cauda podada). */
|
|
58
|
+
function seedPool(podDir, n) {
|
|
59
|
+
const lines = [];
|
|
60
|
+
for (let i = 0; i < n; i++) lines.push(JSON.stringify(bulky(i, 1_700_000_000_000 + i * 1000)));
|
|
61
|
+
fs.writeFileSync(memoryPaths('p', { podDir }).raw, `${lines.join('\n')}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
65
|
+
// AC4 (parte 1) — o CONTRATO: `evicted` + `needsCuration` na biblioteca
|
|
66
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
console.log('--- AC4/DF-SF11.4-2: a poda por orçamento devolve o que descartou ---');
|
|
68
|
+
{
|
|
69
|
+
const { tmp, podDir } = tmpPod('contrato');
|
|
70
|
+
seedPool(podDir, 34);
|
|
71
|
+
const r = consolidate('p', { podDir });
|
|
72
|
+
|
|
73
|
+
ok(`poda ocorreu e é NOMEADA: evicted=${r.evicted.length} (antes deste conserto: nenhum canal)`,
|
|
74
|
+
Array.isArray(r.evicted) && r.evicted.length > 0, JSON.stringify(r).slice(0, 300));
|
|
75
|
+
ok('needsCuration LIGA por poda de orçamento (antes era calculado ANTES do while e ficava false)',
|
|
76
|
+
r.needsCuration === true);
|
|
77
|
+
ok('o teto continua respeitado (DF-SF11.4-3: poda não foi removida)',
|
|
78
|
+
r.tokens <= TOKEN_BUDGET, `tokens=${r.tokens}`);
|
|
79
|
+
ok('cada item de evicted traz trigger/recommendation/score (curadoria sabe O QUE recuperar)',
|
|
80
|
+
r.evicted.every((e) => typeof e.trigger === 'string' && typeof e.recommendation === 'string' &&
|
|
81
|
+
typeof e.score === 'number'), JSON.stringify(r.evicted[0]));
|
|
82
|
+
|
|
83
|
+
// a poda corta a CAUDA por score — as descartadas são as de menor score, nunca o topo
|
|
84
|
+
const md = fs.readFileSync(memoryPaths('p', { podDir }).consolidated, 'utf8');
|
|
85
|
+
ok('o descartado NÃO está no consolidado e o mantido está (evicted descreve o artefato real)',
|
|
86
|
+
r.evicted.every((e) => !md.includes(`**[${e.trigger}]**`)) &&
|
|
87
|
+
md.includes(`**[${bulky(33, 0).trigger}]**`));
|
|
88
|
+
// SF11.4b/ADR-023 (DF-SF11.4b-1): a conservação passou a ser contra o POOL, não contra
|
|
89
|
+
// `min(TOP_N, pool)`. Sob a admissão única não existe mais "excedente do TOP_N" fora da
|
|
90
|
+
// contabilidade: o que o teto de atenção corta entra em `evicted` com `reason:'top_n'`.
|
|
91
|
+
// Esta linha, no estado SF11.4, provava o defeito de C2 sem perceber — ela fechava a
|
|
92
|
+
// conta em 30 ignorando exatamente as heurísticas que sumiam caladas.
|
|
93
|
+
ok('conservação: ranked + evicted = POOL ATIVO — nada some fora da contabilidade',
|
|
94
|
+
r.topNCount + r.evicted.length === 34,
|
|
95
|
+
`${r.topNCount} + ${r.evicted.length} (pool 34)`);
|
|
96
|
+
ok('toda não-admissão tem CAUSA registrada (reason top_n|budget), nenhuma sem classificação',
|
|
97
|
+
r.evicted.every((e) => e.reason === 'top_n' || e.reason === 'budget'),
|
|
98
|
+
JSON.stringify(r.evicted.map((e) => e.reason)));
|
|
99
|
+
|
|
100
|
+
// o log cru NUNCA perde a heurística descartada (a substância fica, a injeção sai)
|
|
101
|
+
const raw = fs.readFileSync(memoryPaths('p', { podDir }).raw, 'utf8');
|
|
102
|
+
ok('heurística descartada do consolidado PERMANECE no log cru (poda ≠ deleção)',
|
|
103
|
+
r.evicted.every((e) => raw.includes(e.trigger)));
|
|
104
|
+
|
|
105
|
+
// idempotência: consolidar 2x devolve o mesmo descarte (determinístico)
|
|
106
|
+
const r2 = consolidate('p', { podDir });
|
|
107
|
+
ok('determinismo: 2ª consolidação descarta exatamente o mesmo conjunto',
|
|
108
|
+
JSON.stringify(r2.evicted) === JSON.stringify(r.evicted));
|
|
109
|
+
|
|
110
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
+
// AC4 (parte 2) — o CANAL: o RUNNER emite o aviso em STDERR (+ kill-test)
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
+
console.log('\n--- AC4: pc-run-pod NÃO descarta mais o retorno de consolidate() ---');
|
|
117
|
+
{
|
|
118
|
+
const { tmp, podDir } = tmpPod('runner');
|
|
119
|
+
seedPool(podDir, 34);
|
|
120
|
+
|
|
121
|
+
// Child process: usa o RUNNER REAL (recordLearning), captura stderr de verdade.
|
|
122
|
+
const child = path.join(tmp, 'run-record.cjs');
|
|
123
|
+
fs.writeFileSync(child, `
|
|
124
|
+
const runner = require(${JSON.stringify(path.join(__dirname, 'pc-run-pod.cjs'))});
|
|
125
|
+
const podDir = ${JSON.stringify(podDir)};
|
|
126
|
+
const ctx = { state: { pod: 'p' }, podRoot: podDir, memoryPodDir: podDir };
|
|
127
|
+
runner.recordLearning(ctx, {
|
|
128
|
+
id: 'NOVA', ts: 1_700_000_900_000,
|
|
129
|
+
trigger: 'aprendizado novo da rodada de producao',
|
|
130
|
+
recommendation: 'esta heuristica nova entra no consolidado e empurra a cauda para fora do orcamento',
|
|
131
|
+
evidence: { count: 1 }, tier: 'normal',
|
|
132
|
+
}, { forceConsolidate: true });
|
|
133
|
+
`);
|
|
134
|
+
const res = spawnSync(process.execPath, [child], { encoding: 'utf8' });
|
|
135
|
+
ok('runner: recordLearning completa (memória é auxiliar; o aviso não derruba o run)',
|
|
136
|
+
res.status === 0, res.stderr);
|
|
137
|
+
// SF11.4b/DF-SF11.4b-1: a linha virou LEDGER — total primeiro, quebra por causa depois.
|
|
138
|
+
ok('runner: STDERR traz "[memory] AVISO: N heurística(s) NÃO entraram no consolidado"',
|
|
139
|
+
/\[memory\] AVISO: \d+ heurística\(s\) NÃO entraram no consolidado do pod "p"/.test(res.stderr),
|
|
140
|
+
res.stderr);
|
|
141
|
+
ok('runner: a linha traz a QUEBRA por causa (teto de atenção · custo)',
|
|
142
|
+
/por teto de atenção TOP_N=30/.test(res.stderr) && /de custo \d+ tok/.test(res.stderr), res.stderr);
|
|
143
|
+
ok('runner: o aviso NOMEIA a heurística que saiu (não é aviso genérico)',
|
|
144
|
+
/"heuristica de campo numero \d+ da rodada de producao"/.test(res.stderr), res.stderr);
|
|
145
|
+
ok('runner: o aviso aponta a AÇÃO de curadoria (harden-learning / obsolete)',
|
|
146
|
+
/cure o consolidado/.test(res.stderr) && /harden-learning/.test(res.stderr), res.stderr);
|
|
147
|
+
|
|
148
|
+
// ── KILL-TEST: com o `evicted` sabotado (código pré-SF11.4), o canal emudece ──
|
|
149
|
+
// SF11.4b/DF-SF11.4b-1: a âncora migrou para o ponto ÚNICO de registro de não-admissão
|
|
150
|
+
// (`drop` em `admit`) — sabotá-lo emudece as DUAS vias de uma vez, que é exatamente o
|
|
151
|
+
// estado pré-SF11.4 (poda por orçamento calada) somado ao pré-SF11.4b (TOP_N mudo).
|
|
152
|
+
const memSrc = fs.readFileSync(path.join(__dirname, 'pod-memory.cjs'), 'utf8');
|
|
153
|
+
const anchor = ` const drop = (entry, reason) => {
|
|
154
|
+
evicted.push({`;
|
|
155
|
+
ok('kill-test: âncora do registro de não-admissão encontrada em admit()',
|
|
156
|
+
memSrc.includes(anchor));
|
|
157
|
+
const sabotaged = memSrc.replace(anchor, ` const drop = (entry, reason) => {
|
|
158
|
+
void entry; void reason; if (false) evicted.push({`);
|
|
159
|
+
const sabPath = path.join(tmp, 'pod-memory-sabotada.cjs');
|
|
160
|
+
fs.writeFileSync(sabPath, sabotaged);
|
|
161
|
+
const childKill = path.join(tmp, 'run-record-kill.cjs');
|
|
162
|
+
fs.writeFileSync(childKill, `
|
|
163
|
+
const Module = require('node:module');
|
|
164
|
+
const memPath = require.resolve(${JSON.stringify(path.join(__dirname, 'pod-memory.cjs'))});
|
|
165
|
+
const sab = require(${JSON.stringify(sabPath)});
|
|
166
|
+
require.cache[memPath] = { id: memPath, filename: memPath, loaded: true, exports: sab };
|
|
167
|
+
const runner = require(${JSON.stringify(path.join(__dirname, 'pc-run-pod.cjs'))});
|
|
168
|
+
void Module;
|
|
169
|
+
const podDir = ${JSON.stringify(podDir)};
|
|
170
|
+
const ctx = { state: { pod: 'p' }, podRoot: podDir, memoryPodDir: podDir };
|
|
171
|
+
runner.recordLearning(ctx, {
|
|
172
|
+
id: 'NOVA2', ts: 1_700_000_950_000,
|
|
173
|
+
trigger: 'outro aprendizado novo da rodada',
|
|
174
|
+
recommendation: 'com o evicted sabotado a poda volta a ser silenciosa — nenhum canal acende',
|
|
175
|
+
evidence: { count: 1 }, tier: 'normal',
|
|
176
|
+
}, { forceConsolidate: true });
|
|
177
|
+
`);
|
|
178
|
+
const resKill = spawnSync(process.execPath, [childKill], { encoding: 'utf8' });
|
|
179
|
+
ok('kill-test: SEM o registro de eviction, o runner volta a NÃO avisar nada (defeito reproduzido)',
|
|
180
|
+
resKill.status === 0 && !/\[memory\] AVISO/.test(resKill.stderr), resKill.stderr);
|
|
181
|
+
const sabMod = require(sabPath);
|
|
182
|
+
const sabRes = sabMod.consolidate('p', { podDir });
|
|
183
|
+
ok('kill-test: e o motor sabotado poda com needsCuration=false — exatamente o defeito de ADR-022 §C4',
|
|
184
|
+
sabRes.needsCuration === false && sabRes.evicted.length === 0 && sabRes.topNCount < 34,
|
|
185
|
+
JSON.stringify({ needsCuration: sabRes.needsCuration, evicted: sabRes.evicted.length, topN: sabRes.topNCount }));
|
|
186
|
+
|
|
187
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
191
|
+
// AC5 — CONTRAPROVA da tabela de 8 promoções (ADR-022 §C4) sob o código novo
|
|
192
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
193
|
+
console.log('\n--- AC5: promover 8 vezes NÃO caminha para memória ativa zerada ---');
|
|
194
|
+
{
|
|
195
|
+
const { tmp, podDir } = tmpPod('promocoes');
|
|
196
|
+
seedPool(podDir, 34);
|
|
197
|
+
const base = consolidate('p', { podDir });
|
|
198
|
+
const trail = [{ step: 0, ranked: base.topNCount, hardened: 0, tokens: base.tokens, evicted: base.evicted.length }];
|
|
199
|
+
|
|
200
|
+
let silent = 0;
|
|
201
|
+
for (let n = 1; n <= 8; n++) {
|
|
202
|
+
// promove a heurística de MAIOR score ainda ativa (o caminho de SUCESSO do pod:
|
|
203
|
+
// o dono aprovando aprendizado), exatamente como a tabela do ADR-022 §C4.
|
|
204
|
+
const c = mem.computeConsolidation('p', { podDir });
|
|
205
|
+
const target = c.ranked[0];
|
|
206
|
+
mem.appendHardening('p', {
|
|
207
|
+
ref: mem.dedupKey(target),
|
|
208
|
+
target: { kind: 'knowledge', file: 'x.md', sha_before: 'a'.repeat(64), sha_after: 'b'.repeat(64) },
|
|
209
|
+
approved_by: 'daniel',
|
|
210
|
+
timestamp: `2026-08-0${n}T00:00:00.000Z`,
|
|
211
|
+
}, { podDir });
|
|
212
|
+
const r = consolidate('p', { podDir });
|
|
213
|
+
if (r.evicted.length > 0 && r.needsCuration !== true) silent += 1;
|
|
214
|
+
trail.push({ step: n, ranked: r.topNCount, hardened: r.hardenedCount, tokens: r.tokens, evicted: r.evicted.length });
|
|
215
|
+
}
|
|
216
|
+
const last = trail[trail.length - 1];
|
|
217
|
+
console.log(` trilha: ${trail.map((t) => `${t.ranked}`).join(' → ')} (ranked por promoção)`);
|
|
218
|
+
ok(`AC5 após 8 promoções a memória ativa NÃO foi zerada (ranked=${last.ranked}, era 0 no código antigo)`,
|
|
219
|
+
last.ranked > 0, JSON.stringify(trail));
|
|
220
|
+
ok('AC5 ranked NÃO decresce a cada promoção (promover LIBERA orçamento em vez de consumi-lo)',
|
|
221
|
+
last.ranked >= trail[0].ranked - 8, JSON.stringify(trail.map((t) => t.ranked)));
|
|
222
|
+
ok('AC5 nenhum passo podou em SILÊNCIO (evicted>0 sem needsCuration é impossível agora)',
|
|
223
|
+
silent === 0);
|
|
224
|
+
ok('AC5 o teto seguiu respeitado em todos os 8 passos',
|
|
225
|
+
trail.every((t) => t.tokens <= TOKEN_BUDGET), JSON.stringify(trail.map((t) => t.tokens)));
|
|
226
|
+
|
|
227
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
231
|
+
// SF11.4b/AC11 — forge-ingest-memory PARA de descartar o retorno de consolidate()
|
|
232
|
+
// (DEBT-SF11.4b-B, autorizado pelo dono). Execução REAL do script, stderr de verdade:
|
|
233
|
+
// era o 4º e último chamador cego — ingerir conhecimento externo é justamente o ato
|
|
234
|
+
// que empurra a cauda para fora, e ele não via nada sair.
|
|
235
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
236
|
+
console.log('\n--- SF11.4b/AC11: o ingest externo vê o que a consolidação deixou de fora ---');
|
|
237
|
+
{
|
|
238
|
+
const { tmp, podDir } = tmpPod('ingest');
|
|
239
|
+
seedPool(podDir, 40);
|
|
240
|
+
const res = spawnSync(process.execPath, [
|
|
241
|
+
path.join(__dirname, 'forge-ingest-memory.cjs'), podDir,
|
|
242
|
+
'--source', 'https://exemplo.invalid/artigo',
|
|
243
|
+
'--trigger', 'gatilho ingerido de fonte externa citada',
|
|
244
|
+
'--recommendation', 'recomendacao ingerida que passa a competir por slot com as de campo',
|
|
245
|
+
], { encoding: 'utf8' });
|
|
246
|
+
ok('ingest: o comando completa (o aviso não derruba a ingestão, que já foi gravada)',
|
|
247
|
+
res.status === 0, `${res.status} ${res.stderr}`);
|
|
248
|
+
ok('ingest: STDOUT confirma a ingestão (contrato do comando intocado)',
|
|
249
|
+
/^(appended|merged): chave M1 /m.test(res.stdout), res.stdout);
|
|
250
|
+
ok('ingest: STDERR traz o LEDGER com total + quebra por causa (antes: silêncio absoluto)',
|
|
251
|
+
/\[memory\] AVISO: \d+ heurística\(s\) NÃO entraram no consolidado/.test(res.stderr) &&
|
|
252
|
+
/por teto de atenção TOP_N=30/.test(res.stderr), res.stderr);
|
|
253
|
+
const total = Number((res.stderr.match(/AVISO: (\d+) heurística/) || [])[1]);
|
|
254
|
+
const md = fs.readFileSync(memoryPaths('_', { podDir }).consolidated, 'utf8');
|
|
255
|
+
const injetadas = (md.match(/^- \*\*\[/gm) || []).length;
|
|
256
|
+
ok(`ingest: o número anunciado (${total}) == pool (41) − injetadas (${injetadas}) — contagem independente`,
|
|
257
|
+
total === 41 - injetadas, `${total} vs ${41 - injetadas}`);
|
|
258
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
262
|
+
// AC10 — a trilha auditável é append-only por construção
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
264
|
+
console.log('\n--- AC10/DF-SF11.4-1: learnings.hardened.md nunca perde uma linha ---');
|
|
265
|
+
{
|
|
266
|
+
const { tmp, podDir } = tmpPod('trilha');
|
|
267
|
+
const src = {
|
|
268
|
+
id: 'H1', ts: 1_700_000_000_000, trigger: 'trigger endurecido',
|
|
269
|
+
recommendation: 'recomendacao endurecida', evidence: { count: 1 }, tier: 'normal',
|
|
270
|
+
};
|
|
271
|
+
mem.append('p', src, { podDir });
|
|
272
|
+
mem.appendHardening('p', {
|
|
273
|
+
ref: mem.dedupKey(src),
|
|
274
|
+
target: { kind: 'knowledge', file: 'slides-html.md', sha_before: 'a'.repeat(64), sha_after: 'b'.repeat(64) },
|
|
275
|
+
approved_by: 'daniel', timestamp: '2026-08-01T00:00:00.000Z',
|
|
276
|
+
}, { podDir });
|
|
277
|
+
consolidate('p', { podDir });
|
|
278
|
+
const paths = memoryPaths('p', { podDir });
|
|
279
|
+
const first = fs.readFileSync(paths.hardenedDetail, 'utf8');
|
|
280
|
+
ok('trilha criada com a linha íntegra (asserção, aprovador, momento)',
|
|
281
|
+
first.includes('🔒 **[trigger endurecido]** recomendacao endurecida — endurecida → knowledge/slides-html.md _(por daniel, 2026-08-01T00:00:00.000Z)_'),
|
|
282
|
+
first);
|
|
283
|
+
ok('consolidado NÃO repete o texto da endurecida (só a referência agregada)',
|
|
284
|
+
!fs.readFileSync(paths.consolidated, 'utf8').includes('recomendacao endurecida'));
|
|
285
|
+
|
|
286
|
+
consolidate('p', { podDir });
|
|
287
|
+
ok('idempotência: 2ª consolidação produz a trilha BYTE-IDÊNTICA',
|
|
288
|
+
fs.readFileSync(paths.hardenedDetail, 'utf8') === first);
|
|
289
|
+
|
|
290
|
+
// pior caso: o log cru perde o registro-fonte (rotação manual/edição) ⇒ a linha FICA
|
|
291
|
+
fs.writeFileSync(paths.raw, '');
|
|
292
|
+
consolidate('p', { podDir });
|
|
293
|
+
const after = fs.readFileSync(paths.hardenedDetail, 'utf8');
|
|
294
|
+
ok('append-only real: registro-fonte fora do log cru e a linha da trilha PERMANECE',
|
|
295
|
+
after.includes('🔒 **[trigger endurecido]**') && after.includes('Preservadas'), after);
|
|
296
|
+
consolidate('p', { podDir });
|
|
297
|
+
ok('append-only real: preservação é estável (rodar de novo não duplica nem remove)',
|
|
298
|
+
fs.readFileSync(paths.hardenedDetail, 'utf8') === after);
|
|
299
|
+
|
|
300
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
console.log(`\n${passed}/${passed + failures.length} verificações passaram.`);
|
|
304
|
+
if (failures.length) {
|
|
305
|
+
console.error('\nSUÍTE SF11.4 (orçamento da memória do pod) FALHOU:');
|
|
306
|
+
for (const f of failures) console.error(` - ${f}`);
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
console.log('SF11.4 verde: poda que fala, endurecidas por referência, trilha append-only.');
|