@cassiomc1/forgeloop 1.13.0 → 2.0.0-rc.2
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.
- package/AGENT_COMPATIBILITY.md +8 -0
- package/CONTRIBUTING.md +11 -7
- package/DOCS_INDEX.md +37 -4
- package/ENG/c-development-eng.md +2 -2
- package/ENG/cpp-development-eng.md +2 -2
- package/ENG/dotnet-aspnetcore-development-eng.md +7 -1
- package/ENG/flutter-development-eng.md +4 -2
- package/ENG/go-development-eng.md +2 -2
- package/ENG/java-development-eng.md +3 -3
- package/ENG/nodejs-backend-development-eng.md +10 -6
- package/ENG/php-development-eng.md +3 -3
- package/ENG/python-development-eng.md +205 -0
- package/ENG/rust-development-eng.md +42 -1
- package/ENG/sec-code-eng.md +7 -7
- package/ENG/sql-development-eng.md +4 -4
- package/ENG/swift-development-eng.md +5 -4
- package/ENG/test-code-eng.md +1 -1
- package/ENG/typescript-development-eng.md +4 -4
- package/EXECUTION_STATE.md +12 -0
- package/GUIDE_ROUTER.md +6 -0
- package/LICENSE +133 -21
- package/LOOP_ENGINEERING.md +28 -2
- package/LOOP_SYSTEM_DESIGN.md +10 -10
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/README.md +63 -59
- package/TERMINOLOGY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +3 -4
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +29 -3
- package/completions/forgeloop.bash +55 -3
- package/completions/forgeloop.fish +183 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +74 -39
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +171 -35
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +512 -8
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +34 -11
- package/docs/GETTING_STARTED.md +3 -3
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +46 -18
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +9 -0
- package/docs/RELEASE_CHECKLIST.md +116 -12
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SELF_HOSTED_TESTS.md +121 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +198 -1
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +774 -16
- package/docs/protocol-requirements.json +24 -0
- package/package.json +43 -11
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/task-bundle.schema.json +7 -2
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +6 -6
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +6 -3
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/scripts/run-mcp-clean-tests.mjs +30 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +316 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +289 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/temporary-paths.js +18 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +218 -174
- package/src/commands/action-show.js +5 -2
- package/src/commands/advance.js +5 -1
- package/src/commands/baseline.js +13 -11
- package/src/commands/checkpoint-revalidate.js +192 -0
- package/src/commands/complete.js +19 -3
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +266 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +51 -13
- package/src/commands/gate-record.js +194 -0
- package/src/commands/gate-revalidate.js +135 -0
- package/src/commands/handoff-list.js +6 -1
- package/src/commands/handoff-show.js +6 -1
- package/src/commands/migrate-protocol.js +2 -2
- package/src/commands/model-route.js +40 -0
- package/src/commands/next.js +7 -1
- package/src/commands/policy-diff.js +25 -4
- package/src/commands/progress.js +17 -11
- package/src/commands/record-diagnosis.js +3 -0
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +41 -0
- package/src/commands/status.js +6 -1
- package/src/commands/storage-backup.js +15 -0
- package/src/commands/storage-migrate.js +5 -0
- package/src/commands/storage-migration-resume.js +5 -0
- package/src/commands/storage-migration-status.js +44 -0
- package/src/commands/storage-restore-resume.js +7 -0
- package/src/commands/storage-restore.js +8 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-list.js +8 -43
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-migrate.js +3 -3
- package/src/commands/task-recover.js +10 -2
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/task-scope.js +18 -2
- package/src/commands/task-show.js +16 -2
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +39 -15
- package/src/commands/validate-receipt.js +8 -1
- package/src/config/guides.json +4 -0
- package/src/core/action-ledger-projection.js +10 -3
- package/src/core/actions.js +38 -75
- package/src/core/activation.js +11 -3
- package/src/core/approvals.js +17 -51
- package/src/core/artifact-registry.js +83 -2
- package/src/core/artifacts.js +81 -27
- package/src/core/attachment-paths.js +45 -0
- package/src/core/attestation-coverage.js +6 -1
- package/src/core/attestation-verifier.js +21 -2
- package/src/core/attestation.js +17 -6
- package/src/core/audit-ux.js +514 -0
- package/src/core/audit.js +6 -1
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/bundles.js +160 -66
- package/src/core/checkpoint-revalidation.js +321 -0
- package/src/core/cli-command-definitions.js +350 -7
- package/src/core/code-manifest.js +9 -3
- package/src/core/command-executors.js +274 -108
- package/src/core/command-input.js +136 -44
- package/src/core/completion-artifacts.js +85 -34
- package/src/core/completion-ownership.js +19 -8
- package/src/core/completion-recovery-rebind.js +11 -1
- package/src/core/completion.js +64 -15
- package/src/core/config.js +3 -0
- package/src/core/conformance.js +2 -1
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/continuity-lint.js +13 -2
- package/src/core/continuity-reconciliation.js +16 -3
- package/src/core/continuity.js +16 -8
- package/src/core/contract-bootstrap-recovery.js +656 -0
- package/src/core/contract-revision.js +209 -0
- package/src/core/decision/artifact.js +70 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +104 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +138 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +181 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +35 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/diagnosis-model.js +11 -0
- package/src/core/diagnosis.js +10 -1
- package/src/core/diagnostic-projection.js +29 -12
- package/src/core/diagnostic-record.js +28 -3
- package/src/core/efficiency.js +6 -1
- package/src/core/error-codes.js +278 -0
- package/src/core/events.js +535 -147
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +36 -13
- package/src/core/execution-profile.js +63 -38
- package/src/core/execution.js +26 -40
- package/src/core/filesystem.js +11 -0
- package/src/core/gate-artifact.js +105 -19
- package/src/core/gate-provenance.js +125 -0
- package/src/core/handoff.js +26 -14
- package/src/core/history.js +39 -35
- package/src/core/hypothesis-projection.js +32 -0
- package/src/core/information-gain-projection.js +104 -100
- package/src/core/inspect.js +12 -3
- package/src/core/integration-invocation-policy.js +37 -4
- package/src/core/integration-resources.js +119 -87
- package/src/core/ledger-event-collection.js +189 -0
- package/src/core/ledger-relations.js +23 -0
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/native-storage.js +12 -0
- package/src/core/next-action-continuity.js +31 -9
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +131 -43
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +9 -4
- package/src/core/next-action-verification-phase.js +2 -1
- package/src/core/next-action.js +56 -9
- package/src/core/phase.js +266 -103
- package/src/core/policy-engine.js +12 -13
- package/src/core/preflight-consistency.js +89 -62
- package/src/core/preflight-loaders.js +48 -9
- package/src/core/preflight-model.js +6 -1
- package/src/core/preflight.js +29 -12
- package/src/core/progress.js +21 -10
- package/src/core/protocol-info.js +68 -0
- package/src/core/protocol-migration.js +7 -6
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +281 -67
- package/src/core/recovery-history.js +67 -47
- package/src/core/reflection.js +72 -77
- package/src/core/responsibility.js +25 -13
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +28 -4
- package/src/core/router.js +67 -1
- package/src/core/runtime-context.js +123 -61
- package/src/core/schema-validation.js +11 -3
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/settlement.js +12 -1
- package/src/core/signing/bundle-file.js +53 -0
- package/src/core/signing/bundle-reference.js +15 -0
- package/src/core/signing/sigstore.js +60 -33
- package/src/core/signing/statement-file.js +27 -0
- package/src/core/sources.js +37 -0
- package/src/core/storage-runtime-registry.js +41 -0
- package/src/core/structural-quality/artifacts.js +20 -14
- package/src/core/structural-quality/service.js +81 -56
- package/src/core/target-layout.js +1 -1
- package/src/core/task-claim-state.js +249 -15
- package/src/core/task-command.js +21 -1
- package/src/core/task-conflict-inspection.js +133 -102
- package/src/core/task-descriptor.js +29 -22
- package/src/core/task-discovery.js +288 -90
- package/src/core/task-lock.js +66 -306
- package/src/core/task-migration-validation.js +5 -5
- package/src/core/task-migration.js +17 -287
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/task-snapshot.js +51 -8
- package/src/core/task-storage-migration.js +18 -0
- package/src/core/templates.js +23 -8
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +94 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +164 -105
- package/src/core/trajectory-evaluation.js +15 -3
- package/src/core/trajectory-metrics.js +47 -32
- package/src/core/transaction.js +16 -287
- package/src/core/verification-scope.js +20 -12
- package/src/core/work-state.js +100 -59
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- package/src/providers/registry.js +152 -0
- package/src/repository-index/binary-manager.js +7 -5
- package/src/storage/artifact-bytes.js +22 -0
- package/src/storage/attachment-binding.js +11 -0
- package/src/storage/attachment-files.js +82 -0
- package/src/storage/attachment-references.js +36 -0
- package/src/storage/backup.js +53 -0
- package/src/storage/capabilities.js +31 -0
- package/src/storage/connection-owner.js +119 -0
- package/src/storage/connection.js +376 -0
- package/src/storage/existing-project-scope.js +45 -0
- package/src/storage/export-destination.js +44 -0
- package/src/storage/exporter.js +284 -0
- package/src/storage/file-durability.js +46 -0
- package/src/storage/importer.js +461 -0
- package/src/storage/index.js +74 -0
- package/src/storage/leases.js +63 -0
- package/src/storage/ledger-event-snapshot.js +150 -0
- package/src/storage/ledger-relations.js +220 -0
- package/src/storage/legacy-maintenance-boundary.js +18 -0
- package/src/storage/legacy-signature.js +115 -0
- package/src/storage/maintenance-handoff.js +129 -0
- package/src/storage/maintenance-owner.js +42 -0
- package/src/storage/maintenance.js +310 -0
- package/src/storage/metadata-json.js +35 -0
- package/src/storage/migration-activate.js +59 -0
- package/src/storage/migration-archive.js +57 -0
- package/src/storage/migration-attachment-root.js +61 -0
- package/src/storage/migration-candidate.js +398 -0
- package/src/storage/migration-publication.js +143 -0
- package/src/storage/migration-rollback-admission.js +54 -0
- package/src/storage/migration-rollback-preparation.js +132 -0
- package/src/storage/migration-rollback-publication.js +183 -0
- package/src/storage/migration-rollback-release.js +66 -0
- package/src/storage/migration-rollback-source-stage.js +103 -0
- package/src/storage/migration-rollback.js +43 -0
- package/src/storage/migration-source-partition.js +56 -0
- package/src/storage/migration-source.js +255 -0
- package/src/storage/migration-terminal.js +39 -0
- package/src/storage/migration-validation.js +75 -0
- package/src/storage/migration.js +43 -0
- package/src/storage/operational-attachments.js +49 -0
- package/src/storage/operational-context.js +50 -0
- package/src/storage/project-backup.js +98 -0
- package/src/storage/project-bootstrap.js +24 -0
- package/src/storage/project-boundary.js +79 -0
- package/src/storage/project-read-snapshot.js +18 -0
- package/src/storage/project-replacement.js +137 -0
- package/src/storage/project-restore.js +203 -0
- package/src/storage/record-definitions.js +45 -0
- package/src/storage/repository.js +507 -0
- package/src/storage/restore-journal.js +62 -0
- package/src/storage/restore-layout.js +30 -0
- package/src/storage/restore-preparation.js +84 -0
- package/src/storage/restore-replacement-archive.js +162 -0
- package/src/storage/restore-replacement-owner.js +31 -0
- package/src/storage/restore-replacement-preparation.js +128 -0
- package/src/storage/restore-replacement.js +142 -0
- package/src/storage/restore-terminal.js +32 -0
- package/src/storage/runtime.js +31 -0
- package/src/storage/schema.js +214 -0
- package/src/storage/singleton-import.js +84 -0
- package/src/storage/snapshot-continuity.js +16 -0
- package/src/storage/snapshot.js +36 -0
- package/src/storage/storage-marker.js +79 -0
- package/src/storage/task-guards.js +107 -0
- package/src/storage/topology.js +42 -0
- package/src/storage/transaction.js +179 -0
- package/src/storage/unit-of-work.js +663 -0
- package/src/storage/windows-process-incarnation.js +206 -0
- package/.forgeloop/forgeloop.gitignore +0 -4
- package/LICENSE-DOCS.md +0 -13
- package/src/core/transaction-maintenance.js +0 -70
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const TEST_DIRECTORIES = Object.freeze(["test", "tests", "spec", "specs"]);
|
|
6
|
+
const TEST_FILE_PATTERN = /\.(?:test|spec)\.(?:c|cpp|go|java|js|jsx|mjs|py|rb|rs|ts|tsx)$/i;
|
|
7
|
+
|
|
8
|
+
async function filesUnder(root, relative = "") {
|
|
9
|
+
const current = path.join(root, relative);
|
|
10
|
+
let entries = [];
|
|
11
|
+
try { entries = await readdir(current, { withFileTypes: true }); } catch { return []; }
|
|
12
|
+
const result = [];
|
|
13
|
+
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
14
|
+
const child = path.join(relative, entry.name);
|
|
15
|
+
if (entry.isDirectory()) result.push(...await filesUnder(root, child));
|
|
16
|
+
else if (entry.isFile() && TEST_FILE_PATTERN.test(entry.name)) result.push(child);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function stableId(framework, file, suite, name) {
|
|
22
|
+
return `test-${createHash("sha256").update(JSON.stringify({ framework, file, suite, name })).digest("hex").slice(0, 24)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function frameworkFor(source) {
|
|
26
|
+
if (/node:test/.test(source)) return "node:test";
|
|
27
|
+
if (/vitest/.test(source)) return "vitest";
|
|
28
|
+
if (/jest/.test(source)) return "jest";
|
|
29
|
+
if (/pytest|unittest/.test(source)) return "python";
|
|
30
|
+
return "unknown";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function collectUnits(source) {
|
|
34
|
+
const units = [];
|
|
35
|
+
const matcher = /\b(test|it)\s*\(\s*(["'`])([^"'`]+)\2/g;
|
|
36
|
+
let match;
|
|
37
|
+
while ((match = matcher.exec(source))) units.push({ name: match[3].trim(), line: source.slice(0, match.index).split("\n").length });
|
|
38
|
+
return units;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function signals(file, name) {
|
|
42
|
+
const text = `${file} ${name}`.toLowerCase();
|
|
43
|
+
return {
|
|
44
|
+
requiredBehavior: /contract|acceptance|required|lifecycle|protocol/.test(text),
|
|
45
|
+
security: /security|auth|secret|permission|credential|csp|injection/.test(text),
|
|
46
|
+
publicApi: /api|compat|public|cli|protocol/.test(text),
|
|
47
|
+
criticalPath: /critical|transaction|lock|concurr|race|recovery/.test(text),
|
|
48
|
+
integration: /integration|e2e|browser|provider|package|release/.test(text),
|
|
49
|
+
migration: /migration|schema|upgrade|downgrade/.test(text),
|
|
50
|
+
release: /package|release|publish|smoke/.test(text),
|
|
51
|
+
platformSpecific: /windows|win32|macos|darwin|linux|platform|native/.test(text),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function inventoryTests(projectRoot = process.cwd()) {
|
|
56
|
+
const files = [];
|
|
57
|
+
for (const directory of TEST_DIRECTORIES) files.push(...await filesUnder(projectRoot, directory));
|
|
58
|
+
const tests = [];
|
|
59
|
+
for (const relativeFile of [...new Set(files)].sort()) {
|
|
60
|
+
const source = await readFile(path.join(projectRoot, relativeFile), "utf8");
|
|
61
|
+
const framework = frameworkFor(source);
|
|
62
|
+
for (const unit of collectUnits(source)) {
|
|
63
|
+
const file = relativeFile.replaceAll(path.sep, "/");
|
|
64
|
+
tests.push({
|
|
65
|
+
testId: stableId(framework, file, "", unit.name), file, framework, suite: "", name: unit.name,
|
|
66
|
+
line: unit.line, sourceSummary: `${file}:${unit.line} ${unit.name}`.slice(0, 500),
|
|
67
|
+
targets: [file], runtimeMs: null, uniqueBranches: null, signals: signals(file, unit.name),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
tests.sort((a, b) => a.testId.localeCompare(b.testId));
|
|
72
|
+
return { schemaVersion: 1, source: "DETERMINISTIC", tests };
|
|
73
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readTestUtility } from "./service.js";
|
|
2
|
+
import { assertSafePath } from "../filesystem.js";
|
|
3
|
+
import { cp, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const PROTECTED_CLASSES = new Set([
|
|
11
|
+
"KEEP_REQUIRED", "KEEP_RISK_GUARD", "KEEP_UNIQUE", "KEEP_AUTHORITY_BOUNDARY",
|
|
12
|
+
"KEEP_RECOVERY_INVARIANT", "KEEP_RELEASE_SMOKE", "KEEP_MIGRATION_COMPATIBILITY",
|
|
13
|
+
"KEEP_PLATFORM_BEHAVIOR",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function planFor(test) {
|
|
17
|
+
if (test.protected || PROTECTED_CLASSES.has(test.classification)) return { action: "KEEP", reason: "PROTECTED_TEST" };
|
|
18
|
+
if (test.classification === "REDUNDANT_CANDIDATE" && test.recommendation === "PROBE_REMOVAL") return { action: "PROBE_REMOVAL", reason: "SEMANTIC_AND_DETERMINISTIC_EVIDENCE_REQUIRED" };
|
|
19
|
+
if (test.classification === "KEEP_DOCUMENTATION_VALUE") return { action: "KEEP", reason: "DOCUMENTATION_VALUE" };
|
|
20
|
+
return { action: "BLOCKED", reason: "INSUFFICIENT_PROVENANCE" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function buildPrunePlan({ target, packageRoot, taskId } = {}) {
|
|
24
|
+
const utility = await readTestUtility({ target, packageRoot, taskId });
|
|
25
|
+
const items = utility.value.tests.map((test) => ({ testId: test.testId, file: test.file, name: test.name, ...planFor(test) }));
|
|
26
|
+
return { schemaVersion: 1, taskId, semanticStatus: utility.value.semanticStatus, items, deletionAuthority: false };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function runPruneProbe({ target, packageRoot, taskId, testId } = {}) {
|
|
30
|
+
const plan = await buildPrunePlan({ target, packageRoot, taskId });
|
|
31
|
+
const item = plan.items.find((candidate) => candidate.testId === testId);
|
|
32
|
+
if (!item) {
|
|
33
|
+
const error = new Error(`Unknown test ID: ${testId}`); error.code = "E_TEST_ID_UNKNOWN"; throw error;
|
|
34
|
+
}
|
|
35
|
+
if (item.action !== "PROBE_REMOVAL") {
|
|
36
|
+
const error = new Error(`Test prune probe blocked for ${testId}: ${item.reason}`);
|
|
37
|
+
error.code = "E_TEST_PRUNE_BLOCKED";
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), "forgeloop-prune-probe-"));
|
|
41
|
+
try {
|
|
42
|
+
// Persisted utility records do not grant access outside the project or
|
|
43
|
+
// through symlinks copied into the disposable workspace.
|
|
44
|
+
await assertSafePath(target, item.file);
|
|
45
|
+
await cp(target, temporaryRoot, {
|
|
46
|
+
recursive: true,
|
|
47
|
+
dereference: false,
|
|
48
|
+
filter(source) {
|
|
49
|
+
const relative = path.relative(target, source);
|
|
50
|
+
return relative !== ".forgeloop" && relative !== ".git" && !relative.startsWith(`${path.sep}.forgeloop${path.sep}`) && !relative.startsWith(`${path.sep}.git${path.sep}`) && relative !== "node_modules";
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const dependencies = path.join(target, "node_modules");
|
|
54
|
+
try { await symlink(dependencies, path.join(temporaryRoot, "node_modules"), "junction"); } catch { /* dependencies are optional for static probes */ }
|
|
55
|
+
if (item.file.endsWith(".mjs") || item.file.endsWith(".js")) {
|
|
56
|
+
const candidatePath = await assertSafePath(temporaryRoot, item.file);
|
|
57
|
+
const source = await readFile(candidatePath, "utf8");
|
|
58
|
+
const escapedName = item.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
59
|
+
const pattern = new RegExp(`\\b(test|it)\\s*\\(\\s*([\\"'])${escapedName}\\2`);
|
|
60
|
+
if (!pattern.test(source)) {
|
|
61
|
+
return { status: "INCONCLUSIVE", reason: "TEST_UNIT_NOT_LOCATED", testId, liveWorktreeModified: false, temporaryWorkspaceCleaned: true };
|
|
62
|
+
}
|
|
63
|
+
const disabled = source.replace(pattern, (_, keyword, quote) => `${keyword}.skip(${quote}${item.name}${quote}`);
|
|
64
|
+
await writeFile(candidatePath, disabled, "utf8");
|
|
65
|
+
const result = await execFileAsync(process.execPath, ["--test", item.file], {
|
|
66
|
+
cwd: temporaryRoot,
|
|
67
|
+
timeout: 120_000,
|
|
68
|
+
maxBuffer: 2_000_000,
|
|
69
|
+
windowsHide: true,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
status: "INCONCLUSIVE",
|
|
73
|
+
reason: "COVERAGE_OR_MUTATION_EVIDENCE_UNAVAILABLE",
|
|
74
|
+
testId,
|
|
75
|
+
affectedTestExitCode: 0,
|
|
76
|
+
affectedTestOutput: `${result.stdout ?? ""}${result.stderr ?? ""}`.slice(-8_000),
|
|
77
|
+
liveWorktreeModified: false,
|
|
78
|
+
temporaryWorkspaceCleaned: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return { status: "INCONCLUSIVE", reason: "FRAMEWORK_PROBE_UNSUPPORTED", testId, liveWorktreeModified: false, temporaryWorkspaceCleaned: true };
|
|
82
|
+
} catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
status: "BLOCKED",
|
|
85
|
+
reason: error.code === "ETIMEDOUT" ? "AFFECTED_TEST_TIMEOUT" : "AFFECTED_TEST_FAILED",
|
|
86
|
+
testId,
|
|
87
|
+
error: error.message,
|
|
88
|
+
liveWorktreeModified: false,
|
|
89
|
+
temporaryWorkspaceCleaned: true,
|
|
90
|
+
};
|
|
91
|
+
} finally {
|
|
92
|
+
await rm(temporaryRoot, { recursive: true, force: true });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const TEST_UTILITY_BATCH_SIZE = 8;
|
|
2
|
+
|
|
3
|
+
export function batchTests(tests = [], size = TEST_UTILITY_BATCH_SIZE) {
|
|
4
|
+
const batches = [];
|
|
5
|
+
for (let index = 0; index < tests.length; index += size) batches.push(tests.slice(index, index + size));
|
|
6
|
+
return batches;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildTestSemanticState(tests = []) {
|
|
10
|
+
return {
|
|
11
|
+
tests: tests.map(({ testId, file, framework, suite, name, line, sourceSummary, targets, runtimeMs, uniqueBranches, signals }) => ({
|
|
12
|
+
testId, file, framework, suite, name, line, sourceSummary, targets, runtimeMs, uniqueBranches, signals,
|
|
13
|
+
})),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { taskArtifactPath } from "../task-paths.js";
|
|
2
|
+
import { writeJsonArtifact, readJsonArtifact } from "../artifacts.js";
|
|
3
|
+
import { withTaskMutation } from "../task-command.js";
|
|
4
|
+
import { inventoryTests } from "./inventory.js";
|
|
5
|
+
import { buildTestUtilityArtifact } from "./utility.js";
|
|
6
|
+
import { canonicalFingerprint } from "../artifacts.js";
|
|
7
|
+
import { readWorkState } from "../work-state.js";
|
|
8
|
+
import { buildTestUtilityQuestionSet } from "../decision/question-registry.js";
|
|
9
|
+
import { ensureSemanticDecision } from "../decision/service.js";
|
|
10
|
+
import { batchTests, buildTestSemanticState } from "./semantic-state.js";
|
|
11
|
+
|
|
12
|
+
export async function runTestInventory({ projectRoot = process.cwd() } = {}) { return inventoryTests(projectRoot); }
|
|
13
|
+
|
|
14
|
+
export async function runTestUtility({ target, packageRoot, taskId, provider = null } = {}) {
|
|
15
|
+
const inventory = await inventoryTests(target);
|
|
16
|
+
const semanticDecisions = [];
|
|
17
|
+
const state = await readWorkState(target, { packageRoot, taskId });
|
|
18
|
+
for (const [index, batch] of batchTests(inventory.tests).entries()) {
|
|
19
|
+
const questionSet = buildTestUtilityQuestionSet(batch);
|
|
20
|
+
semanticDecisions.push(await ensureSemanticDecision({
|
|
21
|
+
target, packageRoot, taskId, provider,
|
|
22
|
+
decisionId: `test-utility-${index}-${canonicalFingerprint(batch.map((test) => test.testId)).slice(0, 16)}`,
|
|
23
|
+
request: {
|
|
24
|
+
decisionKind: "TEST_UTILITY", questionSetId: questionSet.id, questionSet,
|
|
25
|
+
state: { lifecycle: state, ...buildTestSemanticState(batch) },
|
|
26
|
+
candidateIds: batch.map((test) => test.testId),
|
|
27
|
+
candidateSetFingerprint: canonicalFingerprint(batch.map((test) => test.testId)),
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
return withTaskMutation(target, { taskId, packageRoot }, "test-utility", async (ctx) => {
|
|
32
|
+
const artifact = buildTestUtilityArtifact({ taskId: ctx.taskId, inventory, semanticDecisions: semanticDecisions.map((decision) => decision.artifact) });
|
|
33
|
+
const written = await writeJsonArtifact(target, taskArtifactPath(ctx.taskId, "testUtility"), artifact, "test-utility", packageRoot, { taskId: ctx.taskId, operation: "test-utility" });
|
|
34
|
+
return { path: written.path, artifact, inventory };
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function readTestUtility({ target, packageRoot, taskId } = {}) {
|
|
39
|
+
return readJsonArtifact(target, taskArtifactPath(taskId, "testUtility"), "test-utility", packageRoot);
|
|
40
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "../artifacts.js";
|
|
2
|
+
|
|
3
|
+
function classify(test, judgment = null, confidence = 1) {
|
|
4
|
+
const signal = test.signals ?? {};
|
|
5
|
+
if (signal.requiredBehavior) return { classification: "KEEP_REQUIRED", recommendation: "KEEP", protected: true };
|
|
6
|
+
if (signal.security) return { classification: "KEEP_RISK_GUARD", recommendation: "KEEP", protected: true };
|
|
7
|
+
if (signal.criticalPath) return { classification: "KEEP_RECOVERY_INVARIANT", recommendation: "KEEP", protected: true };
|
|
8
|
+
if (signal.migration) return { classification: "KEEP_MIGRATION_COMPATIBILITY", recommendation: "KEEP", protected: true };
|
|
9
|
+
if (signal.publicApi) return { classification: "KEEP_UNIQUE", recommendation: "KEEP", protected: true };
|
|
10
|
+
if (signal.release) return { classification: "KEEP_RELEASE_SMOKE", recommendation: "KEEP", protected: true };
|
|
11
|
+
if (signal.platformSpecific) return { classification: "KEEP_PLATFORM_BEHAVIOR", recommendation: "KEEP", protected: true };
|
|
12
|
+
if (signal.integration) return { classification: "KEEP_INTEGRATION_GUARD", recommendation: "KEEP", protected: true };
|
|
13
|
+
if (!judgment || confidence < 0.75) return { classification: "UNKNOWN", recommendation: "BLOCKED", protected: false };
|
|
14
|
+
if (judgment.protocol_invariant) return { classification: "KEEP_RECOVERY_INVARIANT", recommendation: "KEEP", protected: true };
|
|
15
|
+
if (judgment.semantic_duplicate) {
|
|
16
|
+
return {
|
|
17
|
+
classification: "REDUNDANT_CANDIDATE",
|
|
18
|
+
recommendation: judgment.requires_system_two_review ? "BLOCKED" : "PROBE_REMOVAL",
|
|
19
|
+
protected: false,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
if (judgment.likely_obsolete) return { classification: "OBSOLETE_CANDIDATE", recommendation: "BLOCKED", protected: false };
|
|
23
|
+
if (judgment.likely_flaky_low_signal) return { classification: "FLAKY_LOW_SIGNAL", recommendation: "BLOCKED", protected: false };
|
|
24
|
+
if (judgment.expensive_relative_to_signal) return { classification: "EXPENSIVE_LOW_SIGNAL", recommendation: "BLOCKED", protected: false };
|
|
25
|
+
if (judgment.documentation_value) return { classification: "KEEP_DOCUMENTATION_VALUE", recommendation: "KEEP", protected: false };
|
|
26
|
+
if (judgment.integration_value) return { classification: "KEEP_INTEGRATION_GUARD", recommendation: "KEEP", protected: true };
|
|
27
|
+
if (judgment.has_unique_semantic_intent) return { classification: "KEEP_UNIQUE", recommendation: "KEEP", protected: false };
|
|
28
|
+
return { classification: "UNKNOWN", recommendation: "BLOCKED", protected: false };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function buildTestUtilityArtifact({ taskId, inventory, semanticStatus = "NOT_REQUESTED", semanticDecision = null, semanticDecisions = [] } = {}) {
|
|
32
|
+
const decisions = semanticDecisions.length > 0 ? semanticDecisions : (semanticDecision ? [semanticDecision] : []);
|
|
33
|
+
const byTest = new Map(decisions.flatMap((decision) => Object.entries(decision.decision?.tests ?? {}).map(([testId, judgment]) => [testId, {
|
|
34
|
+
judgment, confidence: Math.min(...Object.keys(judgment).map((key) => decision.confidence?.[`test_${(decision.decision.candidateIds ?? []).indexOf(testId)}_${key}`] ?? 0)),
|
|
35
|
+
}])));
|
|
36
|
+
return {
|
|
37
|
+
schemaVersion: 1,
|
|
38
|
+
protocolVersion: 1,
|
|
39
|
+
taskId,
|
|
40
|
+
generatedAt: new Date().toISOString(),
|
|
41
|
+
inventoryFingerprint: canonicalFingerprint(inventory),
|
|
42
|
+
semanticStatus: decisions.length > 0 ? "PROVIDER_REPORTED" : semanticStatus,
|
|
43
|
+
...(decisions.length > 0 ? {
|
|
44
|
+
...(decisions.length === 1 ? { decisionId: decisions[0].decisionId, semanticDecisionFingerprint: canonicalFingerprint(decisions[0]) } : {}),
|
|
45
|
+
decisionIds: decisions.map((decision) => decision.decisionId),
|
|
46
|
+
semanticDecisionFingerprints: decisions.map((decision) => canonicalFingerprint(decision)),
|
|
47
|
+
} : {}),
|
|
48
|
+
tests: (inventory?.tests ?? []).map((test) => ({ ...test, ...classify(test, byTest.get(test.testId)?.judgment, byTest.get(test.testId)?.confidence ?? 0) })),
|
|
49
|
+
};
|
|
50
|
+
}
|
package/src/core/trace.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { withTaskSnapshot } from "./task-snapshot.js";
|
|
2
|
+
import { ledgerEventsOfTypes } from "./ledger-event-collection.js";
|
|
3
3
|
import {
|
|
4
4
|
assertDiagnosticCaseDetails,
|
|
5
5
|
assertInterventionDetails,
|
|
@@ -51,6 +51,7 @@ const EVENT_CATEGORY_MAP = Object.freeze({
|
|
|
51
51
|
TASK_RECOVERY_RECORDED: "recovery",
|
|
52
52
|
TASK_RECOVERY_RESUMED: "recovery",
|
|
53
53
|
OPERATOR_RECOVERY_RECORDED: "recovery",
|
|
54
|
+
TASK_ABANDONED: "recovery",
|
|
54
55
|
LEGACY_RECOVERY_MIGRATION_RECORDED: "recovery",
|
|
55
56
|
GATE_SATISFIED: "policy",
|
|
56
57
|
COMPLETION_VALIDATED: "completion",
|
|
@@ -82,6 +83,13 @@ const LIFECYCLE_TRANSITIONS = Object.freeze([
|
|
|
82
83
|
"COMPLETION_REJECTED",
|
|
83
84
|
]);
|
|
84
85
|
|
|
86
|
+
const STATIC_EVENT_SUMMARIES = Object.freeze({
|
|
87
|
+
TASK_RECOVERY_RESUMED: "Task resumed after recovery",
|
|
88
|
+
OPERATOR_RECOVERY_RECORDED: "Operator recovery recorded",
|
|
89
|
+
TASK_ABANDONED: "Task explicitly abandoned",
|
|
90
|
+
LEGACY_RECOVERY_MIGRATION_RECORDED: "Legacy recovery migration recorded",
|
|
91
|
+
});
|
|
92
|
+
|
|
85
93
|
export function eventCategory(eventType) {
|
|
86
94
|
return EVENT_CATEGORY_MAP[eventType] ?? "integrity";
|
|
87
95
|
}
|
|
@@ -93,6 +101,8 @@ export function timestampQuality(event) {
|
|
|
93
101
|
|
|
94
102
|
function eventSummary(event) {
|
|
95
103
|
const d = event.details ?? {};
|
|
104
|
+
const staticSummary = STATIC_EVENT_SUMMARIES[event.event];
|
|
105
|
+
if (staticSummary) return staticSummary;
|
|
96
106
|
switch (event.event) {
|
|
97
107
|
case "TASK_RECEIVED":
|
|
98
108
|
return `Task received${d.taskId ? ` (${d.taskId})` : ""}`;
|
|
@@ -132,12 +142,6 @@ function eventSummary(event) {
|
|
|
132
142
|
return "Continuity checkpoint recorded";
|
|
133
143
|
case "TASK_RECOVERY_RECORDED":
|
|
134
144
|
return `Task recovery recorded (${d.classification ?? "unknown"})`;
|
|
135
|
-
case "TASK_RECOVERY_RESUMED":
|
|
136
|
-
return "Task resumed after recovery";
|
|
137
|
-
case "OPERATOR_RECOVERY_RECORDED":
|
|
138
|
-
return "Operator recovery recorded";
|
|
139
|
-
case "LEGACY_RECOVERY_MIGRATION_RECORDED":
|
|
140
|
-
return "Legacy recovery migration recorded";
|
|
141
145
|
case "COMPLETION_VALIDATED":
|
|
142
146
|
return "Completion validated";
|
|
143
147
|
case "COMPLETION_REJECTED":
|
|
@@ -197,23 +201,6 @@ export function normalizeProtocolEvent(event, context = {}) {
|
|
|
197
201
|
};
|
|
198
202
|
}
|
|
199
203
|
|
|
200
|
-
function lifecycleTransitions(events) {
|
|
201
|
-
const transitions = [];
|
|
202
|
-
let lastPhaseEvent = null;
|
|
203
|
-
for (const event of events) {
|
|
204
|
-
if (!LIFECYCLE_TRANSITIONS.includes(event.event)) continue;
|
|
205
|
-
transitions.push({
|
|
206
|
-
sequence: event.seq,
|
|
207
|
-
at: event.at,
|
|
208
|
-
type: event.event,
|
|
209
|
-
details: event.details ?? {},
|
|
210
|
-
previous: lastPhaseEvent?.type ?? null,
|
|
211
|
-
});
|
|
212
|
-
lastPhaseEvent = event;
|
|
213
|
-
}
|
|
214
|
-
return transitions;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
204
|
function projectChecks(snapshot) {
|
|
218
205
|
const state = snapshot.state;
|
|
219
206
|
const attemptsByCheck = new Map();
|
|
@@ -285,7 +272,8 @@ function projectChecks(snapshot) {
|
|
|
285
272
|
}
|
|
286
273
|
|
|
287
274
|
function legacyDiagnoses(events, taskId) {
|
|
288
|
-
return
|
|
275
|
+
return [...ledgerEventsOfTypes(events, ["DIAGNOSIS_RECORDED"])]
|
|
276
|
+
.filter(event => !taskId || event.taskId === taskId).map((event) => ({
|
|
289
277
|
sequence: event.seq,
|
|
290
278
|
at: event.at,
|
|
291
279
|
sourceModel: "LEGACY_DIAGNOSIS_V1",
|
|
@@ -353,35 +341,53 @@ const PHASE_DERIVATIONS = Object.freeze({
|
|
|
353
341
|
INTERVENTION_RECORDED: () => "CORRECTING",
|
|
354
342
|
});
|
|
355
343
|
|
|
356
|
-
function
|
|
344
|
+
function phaseStep(event, currentPhase) {
|
|
345
|
+
const milestone = PHASE_MILESTONES[event.event];
|
|
346
|
+
if (milestone) return { phase: milestone.phase, quality: milestone.quality };
|
|
347
|
+
const derive = PHASE_DERIVATIONS[event.event];
|
|
348
|
+
const derived = derive ? derive(event.details ?? {}) : null;
|
|
349
|
+
return { phase: derived ?? currentPhase, quality: derived || currentPhase ? "derived" : "unknown" };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function orderedSequences(events) {
|
|
353
|
+
let previous = -Infinity;
|
|
354
|
+
return events.every(event => {
|
|
355
|
+
if (!Number.isFinite(event.seq) || event.seq <= previous) return false;
|
|
356
|
+
previous = event.seq;
|
|
357
|
+
return true;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function* phasedEvents(events, validatedOrder = false) {
|
|
357
362
|
let currentPhase = null;
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
currentPhase = milestone.phase;
|
|
364
|
-
qualityBySequence.set(event.seq, milestone.quality);
|
|
365
|
-
} else {
|
|
366
|
-
const derive = PHASE_DERIVATIONS[event.event];
|
|
367
|
-
const derived = derive ? derive(event.details ?? {}) : null;
|
|
368
|
-
if (derived) {
|
|
369
|
-
currentPhase = derived;
|
|
370
|
-
qualityBySequence.set(event.seq, "derived");
|
|
371
|
-
} else {
|
|
372
|
-
qualityBySequence.set(event.seq, currentPhase ? "derived" : "unknown");
|
|
373
|
-
}
|
|
363
|
+
if (validatedOrder || orderedSequences(events)) {
|
|
364
|
+
for (const event of events) {
|
|
365
|
+
const step = phaseStep(event, currentPhase);
|
|
366
|
+
currentPhase = step.phase;
|
|
367
|
+
yield { event, ...step };
|
|
374
368
|
}
|
|
375
|
-
|
|
369
|
+
return;
|
|
376
370
|
}
|
|
377
|
-
|
|
371
|
+
// Legacy duplicate sequence keys retain the original last-write projection.
|
|
372
|
+
const phases = new Map();
|
|
373
|
+
for (const event of events) {
|
|
374
|
+
const step = phaseStep(event, currentPhase);
|
|
375
|
+
currentPhase = step.phase;
|
|
376
|
+
phases.set(event.seq, step);
|
|
377
|
+
}
|
|
378
|
+
for (const event of events) yield { event, ...phases.get(event.seq) };
|
|
378
379
|
}
|
|
379
380
|
|
|
380
381
|
export function historyQualityFor({ snapshot, normalizedEvents }) {
|
|
382
|
+
return historyQualityFromSummary(snapshot, normalizedEvents.length,
|
|
383
|
+
normalizedEvents.some((event) => event.timestampQuality !== "authoritative"));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function historyQualityFromSummary(snapshot, eventCount, hasLegacyTimestamp) {
|
|
381
387
|
const reasons = [];
|
|
382
|
-
const hasLedger =
|
|
388
|
+
const hasLedger = eventCount > 0;
|
|
383
389
|
if (!hasLedger) reasons.push("LEDGER_ABSENT");
|
|
384
|
-
if (
|
|
390
|
+
if (hasLegacyTimestamp) {
|
|
385
391
|
reasons.push("LEGACY_PHASE_TIMESTAMPS_UNAVAILABLE");
|
|
386
392
|
}
|
|
387
393
|
if (snapshot.state && Number.isInteger(snapshot.state.verificationCycle) && snapshot.state.verificationCycle > 1
|
|
@@ -392,54 +398,69 @@ export function historyQualityFor({ snapshot, normalizedEvents }) {
|
|
|
392
398
|
return { level, reasons };
|
|
393
399
|
}
|
|
394
400
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
401
|
+
// History supplies an output collector only. Complete snapshot auditing and
|
|
402
|
+
// every diagnostic/action projection below remain independent of its selection.
|
|
403
|
+
export async function buildTaskTrace({ target, packageRoot, taskId = null, eventsPath = null } = {}, eventProjection = null) {
|
|
404
|
+
return withTaskSnapshot({ target, packageRoot, taskId, eventsPath }, snapshot =>
|
|
405
|
+
buildTraceFromSnapshot({ target, packageRoot, taskId, eventsPath, snapshot, eventProjection }));
|
|
406
|
+
}
|
|
398
407
|
|
|
399
|
-
|
|
400
|
-
const { phaseBySequence, qualityBySequence } = reconstructPhaseChronology(taskEvents);
|
|
408
|
+
function projectTraceEvents(taskEvents, artifactPath, eventProjection, validatedOrder) {
|
|
401
409
|
const normalizedEvents = [];
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
phase: phaseBySequence.get(event.seq) ?? null,
|
|
405
|
-
artifactPath,
|
|
406
|
-
phaseQuality: qualityBySequence.get(event.seq) ?? "unknown",
|
|
407
|
-
}));
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
const integrity = {
|
|
411
|
-
valid: snapshot.integrity.valid,
|
|
412
|
-
errors: snapshot.integrity.errors,
|
|
413
|
-
};
|
|
414
|
-
const historyQuality = historyQualityFor({ snapshot, normalizedEvents });
|
|
415
|
-
const diagnostics = structuredDiagnostics(taskEvents);
|
|
416
|
-
|
|
417
|
-
const failureSignatures = projectFailureSignatures({ state: snapshot.state, events: taskEvents });
|
|
418
|
-
const failureSurfaces = projectFailureSurfaces({ state: snapshot.state, events: taskEvents });
|
|
419
|
-
|
|
410
|
+
const transitions = [];
|
|
411
|
+
let lastPhaseEvent = null;
|
|
420
412
|
const executions = [];
|
|
421
413
|
const seenExecutions = new Set();
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
414
|
+
const evidenceSources = new Map();
|
|
415
|
+
const recovery = [];
|
|
416
|
+
const completionAttempts = [];
|
|
417
|
+
const continuity = [];
|
|
418
|
+
let validatedAt = null;
|
|
419
|
+
let rejectedAt = null;
|
|
420
|
+
let actionEventCount = 0;
|
|
421
|
+
let reconciliationCount = 0;
|
|
422
|
+
let totalEventCount = 0;
|
|
423
|
+
let hasLegacyTimestamp = false;
|
|
424
|
+
for (const { event, phase, quality } of phasedEvents(taskEvents, validatedOrder)) {
|
|
425
|
+
const normalized = normalizeProtocolEvent(event, {
|
|
426
|
+
phase: phase ?? null,
|
|
427
|
+
artifactPath,
|
|
428
|
+
phaseQuality: quality ?? "unknown",
|
|
434
429
|
});
|
|
435
|
-
|
|
430
|
+
totalEventCount += 1;
|
|
431
|
+
hasLegacyTimestamp ||= normalized.timestampQuality !== "authoritative";
|
|
432
|
+
if (eventProjection) eventProjection.add(normalized);
|
|
433
|
+
else normalizedEvents.push(normalized);
|
|
434
|
+
|
|
435
|
+
if (LIFECYCLE_TRANSITIONS.includes(event.event)) {
|
|
436
|
+
transitions.push({
|
|
437
|
+
sequence: event.seq,
|
|
438
|
+
at: event.at,
|
|
439
|
+
type: event.event,
|
|
440
|
+
details: event.details ?? {},
|
|
441
|
+
previous: lastPhaseEvent?.type ?? null,
|
|
442
|
+
});
|
|
443
|
+
lastPhaseEvent = event;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const details = event.details ?? {};
|
|
447
|
+
const executionId = details.executionId ?? details.executionRef ?? null;
|
|
448
|
+
if (executionId && !seenExecutions.has(executionId)) {
|
|
449
|
+
seenExecutions.add(executionId);
|
|
450
|
+
executions.push({
|
|
451
|
+
executionId,
|
|
452
|
+
sequence: event.seq,
|
|
453
|
+
at: event.at,
|
|
454
|
+
status: details.status ?? null,
|
|
455
|
+
exitCode: details.exitCode ?? null,
|
|
456
|
+
resolution: details.resolution ?? null,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
436
459
|
|
|
437
|
-
const evidenceSources = new Map();
|
|
438
|
-
for (const event of taskEvents) {
|
|
439
460
|
const refs = [
|
|
440
|
-
...(Array.isArray(
|
|
441
|
-
...(Array.isArray(
|
|
442
|
-
?
|
|
461
|
+
...(Array.isArray(details.evidenceRefs) ? details.evidenceRefs : []),
|
|
462
|
+
...(Array.isArray(details.hypotheses)
|
|
463
|
+
? details.hypotheses.flatMap((hypothesis) => hypothesis.evidenceRefs ?? [])
|
|
443
464
|
: []),
|
|
444
465
|
];
|
|
445
466
|
for (const ref of refs) {
|
|
@@ -447,19 +468,59 @@ export async function buildTaskTrace({ target, packageRoot, taskId = null, event
|
|
|
447
468
|
if (!evidenceSources.has(ref)) evidenceSources.set(ref, { ref, sources: [] });
|
|
448
469
|
if (!evidenceSources.get(ref).sources.includes(event.event)) evidenceSources.get(ref).sources.push(event.event);
|
|
449
470
|
}
|
|
471
|
+
|
|
472
|
+
if (["TASK_RECOVERY_RECORDED", "TASK_RECOVERY_RESUMED", "OPERATOR_RECOVERY_RECORDED", "TASK_ABANDONED", "LEGACY_RECOVERY_MIGRATION_RECORDED"].includes(event.event)) {
|
|
473
|
+
recovery.push({ sequence: event.seq, at: event.at, type: event.event, details });
|
|
474
|
+
}
|
|
475
|
+
if (event.event === "COMPLETION_VALIDATED" || event.event === "COMPLETION_REJECTED") {
|
|
476
|
+
completionAttempts.push({ sequence: event.seq, at: event.at, type: event.event });
|
|
477
|
+
if (event.event === "COMPLETION_VALIDATED") validatedAt = event.at ?? null;
|
|
478
|
+
else rejectedAt = event.at ?? null;
|
|
479
|
+
}
|
|
480
|
+
if (event.event.startsWith("ACTION_") || event.event.startsWith("APPROVAL_")) actionEventCount += 1;
|
|
481
|
+
if (event.event === "ACTION_RECONCILED") reconciliationCount += 1;
|
|
482
|
+
if (event.event === "CONTINUITY_RECORDED" || event.event === "CHECKPOINT_RECONCILED") {
|
|
483
|
+
continuity.push({ sequence: event.seq, at: event.at, type: event.event });
|
|
484
|
+
}
|
|
450
485
|
}
|
|
451
|
-
const evidence = [...evidenceSources.values()].sort((a, b) => a.ref.localeCompare(b.ref));
|
|
452
486
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
487
|
+
return {
|
|
488
|
+
normalizedEvents,
|
|
489
|
+
totalEventCount,
|
|
490
|
+
hasLegacyTimestamp,
|
|
491
|
+
transitions,
|
|
492
|
+
executions,
|
|
493
|
+
evidence: [...evidenceSources.values()].sort((a, b) => a.ref.localeCompare(b.ref)),
|
|
494
|
+
recovery,
|
|
495
|
+
completion: { validatedAt, rejectedAt, attempts: completionAttempts },
|
|
496
|
+
continuity,
|
|
497
|
+
actionEventCount,
|
|
498
|
+
reconciliationCount,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function buildTraceFromSnapshot({ target, packageRoot, taskId, eventsPath, snapshot, eventProjection }) {
|
|
503
|
+
const artifactPath = eventsPath ?? ".forgeloop/task-state/<task-key>/events.ndjson";
|
|
456
504
|
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
505
|
+
const belongsToTask = event => !event.taskId || event.taskId === taskId;
|
|
506
|
+
const taskEvents = taskId && !snapshot.events.every(belongsToTask)
|
|
507
|
+
? snapshot.events.filter(belongsToTask) : snapshot.events;
|
|
508
|
+
// The private snapshot audit already proved seq === index + 1. Invalid
|
|
509
|
+
// ledgers retain the original duplicate/order compatibility projection.
|
|
510
|
+
const {
|
|
511
|
+
normalizedEvents, totalEventCount, hasLegacyTimestamp, transitions,
|
|
512
|
+
executions, evidence, recovery, completion, continuity, actionEventCount, reconciliationCount,
|
|
513
|
+
} = projectTraceEvents(taskEvents, artifactPath, eventProjection, snapshot.integrity.valid);
|
|
514
|
+
|
|
515
|
+
const integrity = {
|
|
516
|
+
valid: snapshot.integrity.valid,
|
|
517
|
+
errors: snapshot.integrity.errors,
|
|
462
518
|
};
|
|
519
|
+
const historyQuality = historyQualityFromSummary(snapshot, totalEventCount, hasLegacyTimestamp);
|
|
520
|
+
const diagnostics = structuredDiagnostics(taskEvents);
|
|
521
|
+
|
|
522
|
+
const failureSignatures = projectFailureSignatures({ state: snapshot.state, events: taskEvents });
|
|
523
|
+
const failureSurfaces = projectFailureSurfaces({ state: snapshot.state, events: taskEvents });
|
|
463
524
|
|
|
464
525
|
const actions = snapshot.taskId
|
|
465
526
|
? await listActions(target, { packageRoot, taskId: snapshot.taskId })
|
|
@@ -477,7 +538,6 @@ export async function buildTaskTrace({ target, packageRoot, taskId = null, event
|
|
|
477
538
|
artifact: action,
|
|
478
539
|
}));
|
|
479
540
|
}
|
|
480
|
-
const actionEvents = taskEvents.filter((event) => event.event.startsWith("ACTION_") || event.event.startsWith("APPROVAL_"));
|
|
481
541
|
const byState = Object.fromEntries([...new Set(actions.map((action) => action.state))].sort()
|
|
482
542
|
.map((state) => [state, actions.filter((action) => action.state === state).length]));
|
|
483
543
|
const byCapability = Object.fromEntries([...new Set(actions.map((action) => action.capability))].sort()
|
|
@@ -501,8 +561,8 @@ export async function buildTaskTrace({ target, packageRoot, taskId = null, event
|
|
|
501
561
|
|| (projection.state === "VERIFIED" && !(projection.authorization.valid && projection.verification.valid))
|
|
502
562
|
).length,
|
|
503
563
|
repeatedIdempotencyAttempts,
|
|
504
|
-
reconciliationCount
|
|
505
|
-
eventCount:
|
|
564
|
+
reconciliationCount,
|
|
565
|
+
eventCount: actionEventCount,
|
|
506
566
|
};
|
|
507
567
|
|
|
508
568
|
return {
|
|
@@ -526,8 +586,9 @@ export async function buildTaskTrace({ target, packageRoot, taskId = null, event
|
|
|
526
586
|
historyQuality,
|
|
527
587
|
integrity,
|
|
528
588
|
artifacts: {},
|
|
529
|
-
events: normalizedEvents,
|
|
530
|
-
|
|
589
|
+
events: eventProjection ? eventProjection.result() : normalizedEvents,
|
|
590
|
+
...(eventProjection ? { totalEventCount } : {}),
|
|
591
|
+
transitions,
|
|
531
592
|
executions,
|
|
532
593
|
checks: projectChecks(snapshot),
|
|
533
594
|
evidence,
|
|
@@ -537,9 +598,7 @@ export async function buildTaskTrace({ target, packageRoot, taskId = null, event
|
|
|
537
598
|
},
|
|
538
599
|
failureSignatures,
|
|
539
600
|
failureSurfaces,
|
|
540
|
-
continuity
|
|
541
|
-
.filter((event) => ["CONTINUITY_RECORDED", "CHECKPOINT_RECONCILED"].includes(event.event))
|
|
542
|
-
.map((event) => ({ sequence: event.seq, at: event.at, type: event.event })),
|
|
601
|
+
continuity,
|
|
543
602
|
recovery,
|
|
544
603
|
policy: {},
|
|
545
604
|
audit: {},
|