@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,236 @@
|
|
|
1
|
+
import { createPresetContract } from "../core/contract-presets.js";
|
|
2
|
+
import {
|
|
3
|
+
contractFingerprint,
|
|
4
|
+
readContract,
|
|
5
|
+
validateContract,
|
|
6
|
+
writeContract,
|
|
7
|
+
} from "../core/contract.js";
|
|
8
|
+
import {
|
|
9
|
+
appendProtocolEvent,
|
|
10
|
+
validateEventLedger,
|
|
11
|
+
validateStateLedgerCoherence,
|
|
12
|
+
} from "../core/events.js";
|
|
13
|
+
import { canonicalFingerprint, readJsonArtifact } from "../core/artifacts.js";
|
|
14
|
+
import { resolveEffectiveContractBootstrapRepairAnchor } from "../core/contract-bootstrap-recovery.js";
|
|
15
|
+
import {
|
|
16
|
+
CONTRACT_REVISED_EVENT,
|
|
17
|
+
assertContractRevisedDetails,
|
|
18
|
+
} from "../core/contract-revision.js";
|
|
19
|
+
import { resolveTaskContext, TASK_SELECTION_MODES } from "../core/task-context.js";
|
|
20
|
+
import { assertTaskMutationAllowed, resolveTaskClaimState } from "../core/task-claim-state.js";
|
|
21
|
+
import { assertWorkspaceBinding } from "../core/workspace-binding.js";
|
|
22
|
+
import { withTaskTransaction } from "../core/transaction.js";
|
|
23
|
+
import { mutateWorkState, readWorkState } from "../core/work-state.js";
|
|
24
|
+
import { readPersistedRoute } from "../core/route-artifact.js";
|
|
25
|
+
import { assertStateIdentity } from "../core/completion-relationships.js";
|
|
26
|
+
|
|
27
|
+
const REVISION_PHASES = new Set(["CONTRACT_READY", "ROUTED", "PLANNED"]);
|
|
28
|
+
|
|
29
|
+
function revisionError(message, artifacts = []) {
|
|
30
|
+
const error = new Error(message);
|
|
31
|
+
error.code = "E_CONTRACT_REVISION_UNSAFE";
|
|
32
|
+
if (artifacts.length > 0) error.artifacts = artifacts;
|
|
33
|
+
return error;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function loadCandidate({ target, packageRoot, taskId, contractFile, preset, claims }) {
|
|
37
|
+
if (preset) return createPresetContract({ taskId, preset, claims });
|
|
38
|
+
let artifact;
|
|
39
|
+
try {
|
|
40
|
+
artifact = await readJsonArtifact(target, contractFile, "current-contract", packageRoot);
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw revisionError(`Unable to read replacement contract: ${error.message}`, error.artifacts ?? [contractFile]);
|
|
43
|
+
}
|
|
44
|
+
if (artifact.value.taskId !== taskId) {
|
|
45
|
+
throw revisionError(
|
|
46
|
+
`Replacement contract taskId "${artifact.value.taskId}" does not match requested taskId "${taskId}"`,
|
|
47
|
+
[contractFile],
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return artifact.value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function revisionStateProjection(state, contractFingerprintValue, phase) {
|
|
54
|
+
const next = {
|
|
55
|
+
...state,
|
|
56
|
+
contractFingerprint: contractFingerprintValue,
|
|
57
|
+
phase,
|
|
58
|
+
requiredGates: [],
|
|
59
|
+
satisfiedGates: [],
|
|
60
|
+
requiredArtifacts: [],
|
|
61
|
+
checks: [],
|
|
62
|
+
failures: [],
|
|
63
|
+
blockers: [],
|
|
64
|
+
verificationEvidence: [],
|
|
65
|
+
completedSteps: phase === "CONTRACT_READY" ? ["contract"] : ["contract", "route"],
|
|
66
|
+
pendingSteps: phase === "CONTRACT_READY"
|
|
67
|
+
? ["route", "planning", "implementation", "verification"]
|
|
68
|
+
: ["planning", "implementation", "verification"],
|
|
69
|
+
};
|
|
70
|
+
delete next.evidenceCoverage;
|
|
71
|
+
delete next.lastCompletionAttempt;
|
|
72
|
+
delete next.diagnosedHypothesis;
|
|
73
|
+
// The revision event carries the explicit PLANNED -> ROUTED rewind proof.
|
|
74
|
+
// Do not feed the old lifecycle transition marker back into createWorkState:
|
|
75
|
+
// the global transition table must remain forward-only.
|
|
76
|
+
delete next.previousPhase;
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function inspectRevisionEligibility({ target, packageRoot, taskId, context }) {
|
|
81
|
+
const state = await readWorkState(target, { packageRoot, taskId });
|
|
82
|
+
const contract = await readContract(target, packageRoot, { taskId });
|
|
83
|
+
if (!state) throw revisionError("Contract revision requires a current work-state checkpoint");
|
|
84
|
+
if (state.taskId !== taskId) throw revisionError("Work-state task identity does not match the selected task");
|
|
85
|
+
if (!REVISION_PHASES.has(state.phase)) {
|
|
86
|
+
throw revisionError(`Contract revision is unavailable in phase ${state.phase}`);
|
|
87
|
+
}
|
|
88
|
+
if (state.contractFingerprint !== contract.fingerprint) {
|
|
89
|
+
throw revisionError("Current work-state is not bound to the current contract artifact");
|
|
90
|
+
}
|
|
91
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId });
|
|
92
|
+
if (!ledger.valid) throw revisionError("Contract revision requires a valid event ledger", ledger.errors);
|
|
93
|
+
if (ledger.events.some((event) => event.event === "EXECUTION_STARTED")) {
|
|
94
|
+
throw revisionError("Contract revision is unavailable after execution has started");
|
|
95
|
+
}
|
|
96
|
+
const coherenceErrors = validateStateLedgerCoherence(state, ledger.events);
|
|
97
|
+
if (coherenceErrors.length > 0) {
|
|
98
|
+
throw revisionError("Contract revision cannot repair an inconsistent task", coherenceErrors);
|
|
99
|
+
}
|
|
100
|
+
const repairAnchor = resolveEffectiveContractBootstrapRepairAnchor(ledger.events);
|
|
101
|
+
if (repairAnchor) {
|
|
102
|
+
throw revisionError("Contract revision is unavailable for tasks with a contract-bootstrap repair or migration anchor");
|
|
103
|
+
}
|
|
104
|
+
const ownership = await resolveTaskClaimState(target, { taskId, packageRoot });
|
|
105
|
+
if (!ownership.ownershipValid || !ownership.mutationAllowed) {
|
|
106
|
+
throw revisionError("Contract revision cannot bypass invalid task ownership", ownership.ownershipErrors);
|
|
107
|
+
}
|
|
108
|
+
if (!context?.descriptor || context.descriptor.taskId !== taskId) {
|
|
109
|
+
throw revisionError("A valid task descriptor is required for contract revision");
|
|
110
|
+
}
|
|
111
|
+
return { state, contract, ledger, ownership };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function assertPreRevisionRouteIdentity({ target, packageRoot, taskId, state, contract }) {
|
|
115
|
+
if (state.phase === "CONTRACT_READY") return;
|
|
116
|
+
try {
|
|
117
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId });
|
|
118
|
+
assertStateIdentity({ contract, route, state });
|
|
119
|
+
} catch (error) {
|
|
120
|
+
throw revisionError(
|
|
121
|
+
`Contract revision requires a valid current route: ${error.message}`,
|
|
122
|
+
error.artifacts ?? [],
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function reviseLocked({ target, packageRoot, taskId, context, contractFile, preset, transaction }) {
|
|
128
|
+
await assertTaskMutationAllowed(target, { taskId, packageRoot });
|
|
129
|
+
await assertWorkspaceBinding(target, { taskId, packageRoot, operation: "contract-revise" });
|
|
130
|
+
const inspected = await inspectRevisionEligibility({ target, packageRoot, taskId, context });
|
|
131
|
+
const candidate = await loadCandidate({
|
|
132
|
+
target,
|
|
133
|
+
packageRoot,
|
|
134
|
+
taskId,
|
|
135
|
+
contractFile,
|
|
136
|
+
preset,
|
|
137
|
+
claims: context.descriptor.writeClaims,
|
|
138
|
+
});
|
|
139
|
+
await validateContract(candidate, packageRoot);
|
|
140
|
+
const nextFingerprint = contractFingerprint(candidate);
|
|
141
|
+
if (nextFingerprint === inspected.contract.fingerprint) {
|
|
142
|
+
return {
|
|
143
|
+
taskId,
|
|
144
|
+
revised: false,
|
|
145
|
+
alreadyCurrent: true,
|
|
146
|
+
phase: inspected.state.phase,
|
|
147
|
+
contractFingerprint: inspected.contract.fingerprint,
|
|
148
|
+
revision: inspected.state.revision,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await assertPreRevisionRouteIdentity({
|
|
153
|
+
target,
|
|
154
|
+
packageRoot,
|
|
155
|
+
taskId,
|
|
156
|
+
state: inspected.state,
|
|
157
|
+
contract: inspected.contract,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const before = inspected.state;
|
|
161
|
+
const nextPhase = before.phase === "PLANNED" ? "ROUTED" : before.phase;
|
|
162
|
+
const previousRouteFingerprint = before.routeFingerprint ?? null;
|
|
163
|
+
const projected = revisionStateProjection(before, nextFingerprint, nextPhase);
|
|
164
|
+
await writeContract(target, candidate, packageRoot, { taskId, operation: "contract-revise" });
|
|
165
|
+
const next = await mutateWorkState(target, {
|
|
166
|
+
expectedRevision: before.revision,
|
|
167
|
+
packageRoot,
|
|
168
|
+
taskId,
|
|
169
|
+
}, () => projected);
|
|
170
|
+
const details = {
|
|
171
|
+
previousContractFingerprint: inspected.contract.fingerprint,
|
|
172
|
+
contractFingerprint: nextFingerprint,
|
|
173
|
+
previousPhase: before.phase,
|
|
174
|
+
phase: next.phase,
|
|
175
|
+
previousStateRevision: before.revision,
|
|
176
|
+
revisedStateRevision: next.revision,
|
|
177
|
+
previousStateFingerprint: canonicalFingerprint(before),
|
|
178
|
+
revisedStateFingerprint: canonicalFingerprint(next),
|
|
179
|
+
previousRouteFingerprint,
|
|
180
|
+
};
|
|
181
|
+
assertContractRevisedDetails(details);
|
|
182
|
+
const event = await appendProtocolEvent(target, {
|
|
183
|
+
taskId,
|
|
184
|
+
event: CONTRACT_REVISED_EVENT,
|
|
185
|
+
details,
|
|
186
|
+
}, packageRoot, { taskId });
|
|
187
|
+
const commit = await appendProtocolEvent(target, {
|
|
188
|
+
taskId,
|
|
189
|
+
event: "TRANSACTION_COMMITTED",
|
|
190
|
+
details: { transactionId: transaction.transactionId, operation: "contract-revise" },
|
|
191
|
+
}, packageRoot, { taskId });
|
|
192
|
+
return {
|
|
193
|
+
taskId,
|
|
194
|
+
revised: true,
|
|
195
|
+
alreadyCurrent: false,
|
|
196
|
+
phase: next.phase,
|
|
197
|
+
previousContractFingerprint: inspected.contract.fingerprint,
|
|
198
|
+
contractFingerprint: nextFingerprint,
|
|
199
|
+
previousPhase: before.phase,
|
|
200
|
+
revision: next.revision,
|
|
201
|
+
eventSeq: event.seq,
|
|
202
|
+
transactionCommitSeq: commit.seq,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function runContractRevise({ target, packageRoot, taskId, contractFile = null, preset = null } = {}) {
|
|
207
|
+
if (!taskId) throw revisionError("contract-revise requires --task");
|
|
208
|
+
if ((!contractFile && !preset) || (contractFile && preset)) {
|
|
209
|
+
throw revisionError("contract-revise requires exactly one of --contract-file or --preset");
|
|
210
|
+
}
|
|
211
|
+
const context = await resolveTaskContext(target, {
|
|
212
|
+
taskId,
|
|
213
|
+
packageRoot,
|
|
214
|
+
explicitRequired: true,
|
|
215
|
+
selectionMode: TASK_SELECTION_MODES.MUTATION,
|
|
216
|
+
});
|
|
217
|
+
return withTaskTransaction({
|
|
218
|
+
target,
|
|
219
|
+
taskId: context.taskId,
|
|
220
|
+
packageRoot,
|
|
221
|
+
operation: "contract-revise",
|
|
222
|
+
recordCommitEvent: false,
|
|
223
|
+
}, async (transaction) => reviseLocked({
|
|
224
|
+
target,
|
|
225
|
+
packageRoot,
|
|
226
|
+
taskId: context.taskId,
|
|
227
|
+
context,
|
|
228
|
+
contractFile,
|
|
229
|
+
preset,
|
|
230
|
+
transaction,
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function formatContractReviseResult(result) {
|
|
235
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
236
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { readDecisionArtifact } from "../core/decision/artifact.js";
|
|
2
|
+
|
|
3
|
+
export async function runDecisionShow({ target, packageRoot, taskId, decisionId } = {}) {
|
|
4
|
+
if (typeof taskId !== "string" || !taskId || typeof decisionId !== "string" || !decisionId) {
|
|
5
|
+
const error = new Error("decision-show requires --task and --decision");
|
|
6
|
+
error.code = "E_DECISION_REQUEST_INVALID";
|
|
7
|
+
throw error;
|
|
8
|
+
}
|
|
9
|
+
return (await readDecisionArtifact(target, taskId, decisionId, packageRoot)).value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatDecisionShowResult(result) {
|
|
13
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readConfig } from "../core/config.js";
|
|
2
|
+
import { DECISION_DEFAULT_POLICY, DECISION_ENGINE_ID, PINNED_JEV_MODEL } from "../core/decision/constants.js";
|
|
3
|
+
import { normalizeDecisionPolicy } from "../core/decision/policy.js";
|
|
4
|
+
import { createTypesafeEngine } from "../adapters/typesafe/engine.js";
|
|
5
|
+
import { hasTypesafeCredentials } from "../adapters/typesafe/client.js";
|
|
6
|
+
|
|
7
|
+
export async function runDecisionStatus({ target, packageRoot, health = false } = {}) {
|
|
8
|
+
let policy = DECISION_DEFAULT_POLICY;
|
|
9
|
+
try {
|
|
10
|
+
const config = await readConfig(target, packageRoot);
|
|
11
|
+
policy = normalizeDecisionPolicy(config.decisionEngine ?? DECISION_DEFAULT_POLICY);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error.code !== "ARTIFACT_MISSING") throw error;
|
|
14
|
+
}
|
|
15
|
+
const result = { required: true, engine: DECISION_ENGINE_ID, model: PINNED_JEV_MODEL, credentials: hasTypesafeCredentials() ? "configured" : "missing", status: hasTypesafeCredentials() ? "configured" : "missing" };
|
|
16
|
+
if (health) result.health = await createTypesafeEngine({ policy }).health();
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function formatDecisionStatusResult(result) {
|
|
21
|
+
return `${JSON.stringify(result, null, 2)}\n`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { appendProtocolEvent, readEvents, validateEventLedger } from "../core/events.js";
|
|
2
|
+
import { withTaskMutation } from "../core/task-command.js";
|
|
3
|
+
|
|
4
|
+
function initialHistory(events, taskId) {
|
|
5
|
+
if (events.length < 2) return false;
|
|
6
|
+
return events[0]?.taskId === taskId
|
|
7
|
+
&& events[0]?.event === "TASK_RECEIVED"
|
|
8
|
+
&& events[1]?.taskId === taskId
|
|
9
|
+
&& events[1]?.event === "TRANSACTION_COMMITTED"
|
|
10
|
+
&& events[1]?.details?.operation === "task-create";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function runDiscover({ target, packageRoot, taskId, task } = {}) {
|
|
14
|
+
return withTaskMutation(target, { taskId: taskId ?? task, packageRoot }, "discover", async (ctx) => {
|
|
15
|
+
const events = await readEvents(target, packageRoot, { taskId: ctx.taskId });
|
|
16
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: ctx.taskId });
|
|
17
|
+
if (!ledger.valid) {
|
|
18
|
+
const error = new Error("Initial discovery requires a valid task event ledger");
|
|
19
|
+
error.code = "E_LEDGER_INVALID";
|
|
20
|
+
error.artifacts = ledger.errors.flatMap((entry) => entry.artifacts ?? []);
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (!initialHistory(events, ctx.taskId)) {
|
|
24
|
+
const error = new Error("Initial discovery requires the canonical task-create history");
|
|
25
|
+
error.code = "E_INITIAL_LIFECYCLE_INVALID";
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
const existing = events.find((event) => event.event === "DISCOVERY_STARTED");
|
|
29
|
+
if (existing) return { taskId: ctx.taskId, phase: "DISCOVERING", idempotent: true, eventSeq: existing.seq };
|
|
30
|
+
const event = await appendProtocolEvent(target, {
|
|
31
|
+
taskId: ctx.taskId,
|
|
32
|
+
event: "DISCOVERY_STARTED",
|
|
33
|
+
details: { source: "canonical-discover" },
|
|
34
|
+
}, packageRoot, { taskId: ctx.taskId });
|
|
35
|
+
return { taskId: ctx.taskId, phase: "DISCOVERING", idempotent: false, eventSeq: event.seq };
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatDiscoverResult(result) {
|
|
40
|
+
return `phase: ${result.phase}\n${result.idempotent ? "idempotent: true\n" : ""}`;
|
|
41
|
+
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -4,11 +4,25 @@ import { readTemplateEntries } from "../core/templates.js";
|
|
|
4
4
|
import { createEvidence } from "../core/evidence.js";
|
|
5
5
|
import { LAYOUT_VERSION } from "../core/target-layout.js";
|
|
6
6
|
import { inspectNativeAdapter, validateNativeAdapterTargets } from "../core/native-adapters.js";
|
|
7
|
-
import { findIncompleteTransactions
|
|
7
|
+
import { findIncompleteTransactions } from "../core/transaction.js";
|
|
8
8
|
import { isRepositoryCandidate } from "../repository-index/lifecycle.js";
|
|
9
9
|
import { getRepositoryIndexStatus, sanitizeRepositoryIndexStatus } from "../repository-index/status.js";
|
|
10
10
|
import { searchRepository } from "../repository-index/search.js";
|
|
11
11
|
import { getPersistentTransportStatus } from "../persistent-transport/client.js";
|
|
12
|
+
import { hasTypesafeCredentials } from "../adapters/typesafe/client.js";
|
|
13
|
+
import { PINNED_JEV_MODEL } from "../core/decision/constants.js";
|
|
14
|
+
import { getOperationalStore } from "../storage/operational-context.js";
|
|
15
|
+
|
|
16
|
+
function semanticDecisionPlaneFinding() {
|
|
17
|
+
const credentialState = hasTypesafeCredentials() ? "credentials configured" : "credentials missing";
|
|
18
|
+
return finding(
|
|
19
|
+
"semantic-decision-plane",
|
|
20
|
+
"info",
|
|
21
|
+
".forgeloop/config.json",
|
|
22
|
+
`Semantic decision plane: ${credentialState}; pinned model ${PINNED_JEV_MODEL}.`,
|
|
23
|
+
"Set TYPESAFE_API_KEY in the host environment before invoking a new semantic decision; recovery and inspection remain local.",
|
|
24
|
+
);
|
|
25
|
+
}
|
|
12
26
|
|
|
13
27
|
function finding(code, severity, relativePath, message, remediation = null, evidence = null) {
|
|
14
28
|
const evidenceRecord = evidence && typeof evidence === "object"
|
|
@@ -180,20 +194,39 @@ async function inspectRepositoryIndexForDoctor({ target, packageRoot, repository
|
|
|
180
194
|
return result;
|
|
181
195
|
}
|
|
182
196
|
|
|
197
|
+
async function inspectStorageForDoctor(target, findings) {
|
|
198
|
+
let storage = null;
|
|
199
|
+
const store = getOperationalStore(target);
|
|
200
|
+
if (store) {
|
|
201
|
+
const { checkStorageIntegrity, readStorageMeta } = await import("../storage/connection.js");
|
|
202
|
+
const integrity = checkStorageIntegrity(store.db);
|
|
203
|
+
const metadata = readStorageMeta(store.db);
|
|
204
|
+
storage = { backend: "sqlite", storageVersion: metadata.storage_version, schemaVersion: metadata.schema_version, integrity };
|
|
205
|
+
findings.push(finding(
|
|
206
|
+
integrity.ok ? "sqlite-storage-integrity" : "E_STORAGE_INTEGRITY_INVALID",
|
|
207
|
+
integrity.ok ? "info" : "error",
|
|
208
|
+
".forgeloop/state.sqlite",
|
|
209
|
+
integrity.ok ? "SQLite pages, foreign keys, ledger hashes and artifact bindings passed storage integrity checks." : "SQLite storage integrity checks failed; no automatic evidence repair was attempted.",
|
|
210
|
+
integrity.ok ? "Domain chronology and authority still require their existing validators." : "Preserve the database and diagnose the reported storage inconsistencies before writing or migrating.",
|
|
211
|
+
));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return storage ? { storage } : {};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function findDoctorTransactions(target) {
|
|
218
|
+
// SQLite recovery belongs to its native open/transaction boundary. Retained
|
|
219
|
+
// legacy manifests are import evidence, not operational recovery input.
|
|
220
|
+
return getOperationalStore(target) ? [] : findIncompleteTransactions(target);
|
|
221
|
+
}
|
|
222
|
+
|
|
183
223
|
export async function runDoctor({ target, packageRoot, adoptPaths = [], strict = false, fix = false, repositoryIndex, repositoryIndexOptions }) {
|
|
184
224
|
const findings = [];
|
|
185
|
-
|
|
186
|
-
if (fix && incompleteTransactions.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
transaction.status === "ROLLED_BACK" ? "TRANSACTION_RECOVERED" : "E_TRANSACTION_RECOVERY_FAILED",
|
|
191
|
-
transaction.status === "ROLLED_BACK" ? "info" : "error",
|
|
192
|
-
`.forgeloop/.txn/${transaction.transactionId}`,
|
|
193
|
-
`Transaction recovery result: ${transaction.status}.`,
|
|
194
|
-
));
|
|
195
|
-
}
|
|
196
|
-
incompleteTransactions = await findIncompleteTransactions(target);
|
|
225
|
+
const incompleteTransactions = await findDoctorTransactions(target);
|
|
226
|
+
if (fix && incompleteTransactions.length) {
|
|
227
|
+
findings.push(finding("E_STORAGE_OPERATION_UNSUPPORTED", "error", ".forgeloop/.txn",
|
|
228
|
+
"Filesystem transaction repair is retired; retained evidence was preserved.",
|
|
229
|
+
"Reconcile incomplete transactions with the compatible pre-migration release before importing into SQLite."));
|
|
197
230
|
}
|
|
198
231
|
for (const transaction of incompleteTransactions) {
|
|
199
232
|
findings.push(finding(
|
|
@@ -392,11 +425,16 @@ export async function runDoctor({ target, packageRoot, adoptPaths = [], strict =
|
|
|
392
425
|
findings,
|
|
393
426
|
});
|
|
394
427
|
|
|
428
|
+
findings.push(semanticDecisionPlaneFinding());
|
|
429
|
+
|
|
430
|
+
const storageFields = await inspectStorageForDoctor(target, findings);
|
|
431
|
+
|
|
395
432
|
const ok = findings.every((item) => item.severity !== "error")
|
|
396
433
|
&& (!strict || findings.every((item) => item.severity !== "warning"));
|
|
397
434
|
return {
|
|
398
435
|
ok,
|
|
399
436
|
findings,
|
|
437
|
+
...storageFields,
|
|
400
438
|
repositoryIndex: repositoryIndexResult,
|
|
401
439
|
evidence: [createEvidence({
|
|
402
440
|
kind: "OBSERVED",
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import { assertSafePath, ensureWithin } from "../core/filesystem.js";
|
|
5
|
+
import { readPersistedRoute } from "../core/route-artifact.js";
|
|
6
|
+
import { readContract } from "../core/contract.js";
|
|
7
|
+
import { readGuideMetadata, requiredGatesForGuides } from "../core/guide-metadata.js";
|
|
8
|
+
import { optionalConfig } from "../core/preflight-loaders.js";
|
|
9
|
+
import { persistGate, readCanonicalGateArtifactBinding, hashGateFile, readGateFileJson } from "../core/gate-artifact.js";
|
|
10
|
+
import { readWorkState } from "../core/work-state.js";
|
|
11
|
+
import { taskArtifactPath, taskGatePath } from "../core/task-paths.js";
|
|
12
|
+
import { withTaskMutation } from "../core/task-command.js";
|
|
13
|
+
import { PROTOCOL_VERSION } from "../core/protocol.js";
|
|
14
|
+
import { staleReasons } from "../core/next-action-artifacts.js";
|
|
15
|
+
import { ARTIFACT_PATHS } from "../core/artifacts.js";
|
|
16
|
+
import { appendProtocolEvent, validateEventLedger } from "../core/events.js";
|
|
17
|
+
|
|
18
|
+
const STATUSES = new Set(["satisfied", "unverified", "blocked"]);
|
|
19
|
+
const PRE_EXECUTION_PHASES = new Set(["ROUTED", "DESIGNING", "PLANNED"]);
|
|
20
|
+
const MAX_TEXT = 2000;
|
|
21
|
+
const MAX_REPEATABLE_ITEMS = 32;
|
|
22
|
+
const MAX_GATE_ARTIFACT_BYTES = 4 * 1024 * 1024;
|
|
23
|
+
const MAX_GATE_EVIDENCE_BYTES = 64 * 1024;
|
|
24
|
+
const CONFIG_REQUIRED_GATES_MARKER = "config.requiredGates";
|
|
25
|
+
|
|
26
|
+
const TASK_ARTIFACT_KEYS_BY_PATH = Object.freeze({
|
|
27
|
+
[ARTIFACT_PATHS.state]: "state",
|
|
28
|
+
[ARTIFACT_PATHS.contract]: "contract",
|
|
29
|
+
[ARTIFACT_PATHS.route]: "route",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function gateError(message, options = {}) {
|
|
33
|
+
const error = new Error(message);
|
|
34
|
+
error.code = options.code ?? "E_GATE_INVALID";
|
|
35
|
+
if (options.cause !== undefined) error.cause = options.cause;
|
|
36
|
+
if (options.artifacts !== undefined) error.artifacts = options.artifacts;
|
|
37
|
+
return error;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function taskScopedFreshnessPath(taskId, artifactPath) {
|
|
41
|
+
const key = TASK_ARTIFACT_KEYS_BY_PATH[artifactPath];
|
|
42
|
+
return key ? taskArtifactPath(taskId, key) : artifactPath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function bounded(values, name, options = {}) {
|
|
46
|
+
const maximum = options.maximum ?? MAX_REPEATABLE_ITEMS;
|
|
47
|
+
if (!Array.isArray(values)) {
|
|
48
|
+
throw gateError(`${name} must be an array`);
|
|
49
|
+
}
|
|
50
|
+
if (values.length > maximum) {
|
|
51
|
+
throw gateError(`${name} entries must not exceed ${maximum}`);
|
|
52
|
+
}
|
|
53
|
+
if (values.some((value) => typeof value !== "string" || value.trim().length === 0 || value.length > MAX_TEXT)) {
|
|
54
|
+
throw gateError(`${name} entries must be non-empty strings of at most ${MAX_TEXT} characters`);
|
|
55
|
+
}
|
|
56
|
+
return values.map((value) => value.trim());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function hashArtifact(target, artifactPath) {
|
|
60
|
+
if (typeof artifactPath !== "string" || !artifactPath || path.isAbsolute(artifactPath)) {
|
|
61
|
+
throw gateError(`Gate artifact must be a project-relative path: ${artifactPath}`);
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
ensureWithin(target, artifactPath);
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
throw gateError(`Gate artifact escapes the project: ${artifactPath}`, { cause });
|
|
67
|
+
}
|
|
68
|
+
const canonical = readCanonicalGateArtifactBinding(target, artifactPath);
|
|
69
|
+
if (canonical) {
|
|
70
|
+
await assertSafePath(target, artifactPath);
|
|
71
|
+
if (canonical.sha256 === null) throw gateError(`Gate artifact is unavailable: ${artifactPath}`);
|
|
72
|
+
if (canonical.byteLength > MAX_GATE_ARTIFACT_BYTES) throw gateError(`Gate artifact exceeds the maximum size of ${MAX_GATE_ARTIFACT_BYTES} bytes: ${artifactPath}`);
|
|
73
|
+
return { path: artifactPath, sha256: canonical.sha256 };
|
|
74
|
+
}
|
|
75
|
+
let info;
|
|
76
|
+
try { info = await lstat(ensureWithin(target, artifactPath)); }
|
|
77
|
+
catch (cause) { throw gateError(`Gate artifact is unavailable: ${artifactPath}`, { cause }); }
|
|
78
|
+
if (!info.isFile() || info.isSymbolicLink()) throw gateError(`Gate artifact must be a regular file: ${artifactPath}`);
|
|
79
|
+
if (info.size > MAX_GATE_ARTIFACT_BYTES) throw gateError(`Gate artifact exceeds the maximum size of ${MAX_GATE_ARTIFACT_BYTES} bytes: ${artifactPath}`);
|
|
80
|
+
try {
|
|
81
|
+
const binding = await hashGateFile(target, artifactPath, { maxBytes: MAX_GATE_ARTIFACT_BYTES });
|
|
82
|
+
return { path: artifactPath, sha256: binding.sha256 };
|
|
83
|
+
} catch (cause) {
|
|
84
|
+
throw gateError(`Gate artifact is unavailable or unsafe: ${artifactPath}`, { cause });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readEvidenceFile(target, evidencePath) {
|
|
89
|
+
if (typeof evidencePath !== "string" || !evidencePath || path.isAbsolute(evidencePath)) {
|
|
90
|
+
throw gateError(`Gate evidence must be a project-relative path: ${evidencePath}`);
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const value = await readGateFileJson(target, evidencePath, { maxBytes: MAX_GATE_EVIDENCE_BYTES });
|
|
94
|
+
return { path: evidencePath, value };
|
|
95
|
+
} catch (cause) {
|
|
96
|
+
throw gateError(`Gate evidence file is invalid: ${evidencePath}`, { cause });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function assertCurrentRoute(target, packageRoot, taskId, state, route) {
|
|
101
|
+
let contract;
|
|
102
|
+
try {
|
|
103
|
+
contract = await readContract(target, packageRoot, { taskId });
|
|
104
|
+
} catch (cause) {
|
|
105
|
+
throw gateError(`Cannot verify the active route against the current contract: ${cause.message}`, {
|
|
106
|
+
code: "E_ROUTE_STALE",
|
|
107
|
+
cause,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const freshness = staleReasons(state, contract, route);
|
|
111
|
+
if (freshness.length === 0) return;
|
|
112
|
+
const reason = freshness.find((entry) => entry.code === "E_ROUTE_STALE") ?? freshness[0];
|
|
113
|
+
throw gateError(reason.message, {
|
|
114
|
+
code: reason.code,
|
|
115
|
+
artifacts: reason.artifacts.map((artifactPath) => taskScopedFreshnessPath(taskId, artifactPath)),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function hasCurrentEpochGateEvent(events, taskId, gate) {
|
|
120
|
+
const latestRevisionSeq = events.findLast((event) => event.event === "CONTRACT_REVISED")?.seq ?? 0;
|
|
121
|
+
return events.some((event) => event.event === "GATE_SATISFIED"
|
|
122
|
+
&& event.taskId === taskId
|
|
123
|
+
&& event.details?.gate === gate
|
|
124
|
+
&& event.seq > latestRevisionSeq);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function runGateRecord({ target, packageRoot, taskId, gate, status, artifacts = [], decisions = [], unknowns = [], assumptions = [], evidenceFile = null } = {}) {
|
|
128
|
+
if (!STATUSES.has(status)) {
|
|
129
|
+
throw gateError(`Invalid gate status: ${status}`);
|
|
130
|
+
}
|
|
131
|
+
return withTaskMutation(target, { taskId, packageRoot }, "gate-record", async (ctx) => {
|
|
132
|
+
const state = await readWorkState(target, { packageRoot, taskId: ctx.taskId });
|
|
133
|
+
if (!state || !PRE_EXECUTION_PHASES.has(state.phase)) {
|
|
134
|
+
throw gateError(`gate-record is only available before execution; found ${state?.phase ?? "no phase"}`, { code: "E_PHASE_FREEZE" });
|
|
135
|
+
}
|
|
136
|
+
const route = await readPersistedRoute(target, packageRoot, { taskId: ctx.taskId });
|
|
137
|
+
await assertCurrentRoute(target, packageRoot, ctx.taskId, state, route);
|
|
138
|
+
const config = await optionalConfig(target, packageRoot, []);
|
|
139
|
+
const required = [...new Set([...(await requiredGatesForGuides(route.value.guides, packageRoot)), ...(config.requiredGates ?? [])])];
|
|
140
|
+
if (!required.includes(gate)) {
|
|
141
|
+
throw gateError(`Gate is not required by the active route: ${gate}`, { code: "E_GATE_NOT_REQUIRED" });
|
|
142
|
+
}
|
|
143
|
+
const artifactPaths = bounded(artifacts, "artifact");
|
|
144
|
+
const normalizedDecisions = bounded(decisions, "decision");
|
|
145
|
+
const normalizedUnknowns = bounded(unknowns, "unknown");
|
|
146
|
+
const approvedAssumptions = bounded(assumptions, "assumption");
|
|
147
|
+
const fileEvidence = evidenceFile ? await readEvidenceFile(target, evidenceFile) : null;
|
|
148
|
+
if (status === "satisfied" && (normalizedUnknowns.length > 0 || (normalizedDecisions.length === 0 && !fileEvidence))) {
|
|
149
|
+
throw gateError("A satisfied gate requires no unknowns and at least one decision");
|
|
150
|
+
}
|
|
151
|
+
const hashedArtifacts = [];
|
|
152
|
+
for (const artifactPath of artifactPaths) hashedArtifacts.push(await hashArtifact(target, artifactPath));
|
|
153
|
+
const guideMetadata = await readGuideMetadata(packageRoot);
|
|
154
|
+
const requiredBy = [
|
|
155
|
+
...route.value.guides.filter((guide) => (guideMetadata[guide]?.requiresGates ?? []).includes(gate)),
|
|
156
|
+
...((config.requiredGates ?? []).includes(gate) ? [CONFIG_REQUIRED_GATES_MARKER] : []),
|
|
157
|
+
];
|
|
158
|
+
const value = {
|
|
159
|
+
schemaVersion: 1,
|
|
160
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
161
|
+
taskId: ctx.taskId,
|
|
162
|
+
gate,
|
|
163
|
+
status,
|
|
164
|
+
requiredBy,
|
|
165
|
+
artifacts: hashedArtifacts,
|
|
166
|
+
decisions: normalizedDecisions,
|
|
167
|
+
unknowns: normalizedUnknowns,
|
|
168
|
+
approvedAssumptions,
|
|
169
|
+
evidence: [
|
|
170
|
+
...normalizedDecisions.map((decision) => ({ kind: "CALLER_RECORDED", authority: "CALLER", text: decision })),
|
|
171
|
+
...(fileEvidence ? [{ kind: "CALLER_RECORDED", authority: "CALLER", path: fileEvidence.path, details: fileEvidence.value }] : []),
|
|
172
|
+
],
|
|
173
|
+
};
|
|
174
|
+
const persisted = await persistGate(target, value, packageRoot, { taskId: ctx.taskId });
|
|
175
|
+
if (status === "satisfied") {
|
|
176
|
+
const ledger = await validateEventLedger(target, packageRoot, { taskId: ctx.taskId });
|
|
177
|
+
if (!ledger.valid) {
|
|
178
|
+
throw gateError("gate-record requires a valid event ledger", { code: "E_EVENT_INVALID", artifacts: ledger.errors });
|
|
179
|
+
}
|
|
180
|
+
if (!hasCurrentEpochGateEvent(ledger.events, ctx.taskId, gate)) {
|
|
181
|
+
await appendProtocolEvent(target, {
|
|
182
|
+
taskId: ctx.taskId,
|
|
183
|
+
event: "GATE_SATISFIED",
|
|
184
|
+
details: { gate },
|
|
185
|
+
}, packageRoot, { taskId: ctx.taskId });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return { taskId: ctx.taskId, gate, status, path: persisted.path ?? persisted.relativePath ?? taskGatePath(ctx.taskId, gate), artifacts: hashedArtifacts };
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function formatGateRecordResult(result) {
|
|
193
|
+
return `gate: ${result.gate}\nstatus: ${result.status}\n`;
|
|
194
|
+
}
|