@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
|
@@ -164,6 +164,15 @@ export function terminalRequirements(requirements = []) {
|
|
|
164
164
|
return normalizeRequirements(requirements).filter((requirement) => requirement.terminalOwned);
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
export function terminalRequirementsForContract(contractValue = {}, { additionalEvidence = [] } = {}) {
|
|
168
|
+
const pool = [
|
|
169
|
+
...(contractValue.verification ?? []),
|
|
170
|
+
...(contractValue.successCriteria ?? []),
|
|
171
|
+
...additionalEvidence,
|
|
172
|
+
];
|
|
173
|
+
return terminalRequirements(pool);
|
|
174
|
+
}
|
|
175
|
+
|
|
167
176
|
export function lifecycleRequirements(requirements = []) {
|
|
168
177
|
return normalizeRequirements(requirements).filter((requirement) => requirement.type === "LIFECYCLE");
|
|
169
178
|
}
|
|
@@ -6,6 +6,7 @@ import { readPersistedRoute } from "./route-artifact.js";
|
|
|
6
6
|
import { stateIdentityErrors } from "./completion-relationships.js";
|
|
7
7
|
import { classifyLoadedWorkState } from "./work-state.js";
|
|
8
8
|
import { taskArtifactPath } from "./task-paths.js";
|
|
9
|
+
import { hasCurrentGateEvidence } from "./gate-provenance.js";
|
|
9
10
|
|
|
10
11
|
const START_EXECUTION_EVENTS = Object.freeze([
|
|
11
12
|
"CONTRACT_VALIDATED",
|
|
@@ -59,6 +60,7 @@ async function load(loadArtifact, code, message, artifacts, errors) {
|
|
|
59
60
|
try {
|
|
60
61
|
return await loadArtifact();
|
|
61
62
|
} catch (error) {
|
|
63
|
+
if (error.code === "E_STATE_REVISION_CONFLICT") throw error;
|
|
62
64
|
errors.push(issue(code, `${message}: ${error.message}`, artifacts));
|
|
63
65
|
return null;
|
|
64
66
|
}
|
|
@@ -129,9 +131,26 @@ function prerequisiteLedgerErrors(ledger, taskId, preflight, route) {
|
|
|
129
131
|
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight, ARTIFACT_PATHS.route],
|
|
130
132
|
));
|
|
131
133
|
}
|
|
134
|
+
const latestContractRevisionSeq = currentEvents.findLast((event) => event.event === "CONTRACT_REVISED")?.seq ?? 0;
|
|
135
|
+
const currentEpochSatisfiedGates = new Set(preflight.requiredGates.filter((gate) => (
|
|
136
|
+
hasCurrentGateEvidence(currentEvents, taskId, gate, latestContractRevisionSeq)
|
|
137
|
+
)));
|
|
138
|
+
for (const gate of preflight.requiredGates) {
|
|
139
|
+
if (!currentEpochSatisfiedGates.has(gate)) {
|
|
140
|
+
errors.push(issue(
|
|
141
|
+
"E_PHASE_CHRONOLOGY_INVALID",
|
|
142
|
+
`execution started before current-contract gate satisfaction: ${gate}`,
|
|
143
|
+
[ARTIFACT_PATHS.events, ARTIFACT_PATHS.preflight],
|
|
144
|
+
));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
132
147
|
return errors;
|
|
133
148
|
}
|
|
134
149
|
|
|
150
|
+
function prerequisiteArtifactPath(explicitPath, taskId, kind) {
|
|
151
|
+
return explicitPath ?? (taskId ? taskArtifactPath(taskId, kind) : ARTIFACT_PATHS[kind]);
|
|
152
|
+
}
|
|
153
|
+
|
|
135
154
|
export async function evaluateStartExecutionPrerequisites({
|
|
136
155
|
target,
|
|
137
156
|
state,
|
|
@@ -142,13 +161,14 @@ export async function evaluateStartExecutionPrerequisites({
|
|
|
142
161
|
statePath = null,
|
|
143
162
|
preflightPath = null,
|
|
144
163
|
eventsPath = null,
|
|
164
|
+
readers = null,
|
|
145
165
|
} = {}) {
|
|
146
166
|
const effectiveTaskId = taskId ?? null;
|
|
147
|
-
const contractRel = contractPath
|
|
148
|
-
const routeRel = routePath
|
|
149
|
-
const stateRel = statePath
|
|
150
|
-
const preflightRel = preflightPath
|
|
151
|
-
const eventsRel = eventsPath
|
|
167
|
+
const contractRel = prerequisiteArtifactPath(contractPath, effectiveTaskId, "contract");
|
|
168
|
+
const routeRel = prerequisiteArtifactPath(routePath, effectiveTaskId, "route");
|
|
169
|
+
const stateRel = prerequisiteArtifactPath(statePath, effectiveTaskId, "state");
|
|
170
|
+
const preflightRel = prerequisiteArtifactPath(preflightPath, effectiveTaskId, "preflight");
|
|
171
|
+
const eventsRel = prerequisiteArtifactPath(eventsPath, effectiveTaskId, "events");
|
|
152
172
|
|
|
153
173
|
const errors = [];
|
|
154
174
|
const requiredArtifacts = [
|
|
@@ -166,14 +186,14 @@ export async function evaluateStartExecutionPrerequisites({
|
|
|
166
186
|
}
|
|
167
187
|
|
|
168
188
|
const contract = await load(
|
|
169
|
-
() => readContract(target, packageRoot, { taskId: effectiveTaskId, contractPath }),
|
|
189
|
+
() => (readers?.readContract ?? readContract)(target, packageRoot, { taskId: effectiveTaskId, contractPath }),
|
|
170
190
|
"E_PHASE_PREREQUISITE_MISSING",
|
|
171
191
|
`EXECUTING requires ${contractRel}`,
|
|
172
192
|
[contractRel],
|
|
173
193
|
errors,
|
|
174
194
|
);
|
|
175
195
|
const route = await load(
|
|
176
|
-
() => readPersistedRoute(target, packageRoot, { taskId: effectiveTaskId, routePath }),
|
|
196
|
+
() => (readers?.readRoute ?? readPersistedRoute)(target, packageRoot, { taskId: effectiveTaskId, routePath }),
|
|
177
197
|
"E_PHASE_PREREQUISITE_MISSING",
|
|
178
198
|
`EXECUTING requires ${routeRel}`,
|
|
179
199
|
[routeRel],
|
|
@@ -190,21 +210,24 @@ export async function evaluateStartExecutionPrerequisites({
|
|
|
190
210
|
}
|
|
191
211
|
errors.push(...stateIdentityErrors({ contract, route, state }));
|
|
192
212
|
|
|
193
|
-
const freshness = await classifyLoadedWorkState({
|
|
213
|
+
const freshness = await (readers?.classifyState ?? classifyLoadedWorkState)({
|
|
194
214
|
target,
|
|
195
215
|
state,
|
|
196
216
|
contractFile: contractRel,
|
|
197
217
|
});
|
|
198
218
|
errors.push(...freshnessErrors(state, freshness));
|
|
199
219
|
|
|
200
|
-
const preflight = await evaluatePreflight({ target, packageRoot, taskId: effectiveTaskId, contractPath, routePath, statePath });
|
|
220
|
+
const preflight = await evaluatePreflight({ target, packageRoot, taskId: effectiveTaskId, contractPath, routePath, statePath, readers });
|
|
201
221
|
let persistedPreflight = null;
|
|
222
|
+
let persistedPreflightReadError = null;
|
|
202
223
|
try {
|
|
203
|
-
persistedPreflight = await readJsonArtifact(target, preflightRel, "preflight", packageRoot);
|
|
204
|
-
} catch {
|
|
224
|
+
persistedPreflight = await (readers?.readPreflight ?? readJsonArtifact)(target, preflightRel, "preflight", packageRoot);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error.code === "E_STATE_REVISION_CONFLICT") throw error;
|
|
227
|
+
persistedPreflightReadError = error;
|
|
205
228
|
// validatePersistedPreflight reports the stable, actionable preflight reason.
|
|
206
229
|
}
|
|
207
|
-
const persistedPreflightErrors = validatePersistedPreflight(persistedPreflight?.value, preflight);
|
|
230
|
+
const persistedPreflightErrors = validatePersistedPreflight(persistedPreflight?.value, preflight, persistedPreflightReadError);
|
|
208
231
|
if (!sameStringSet(state.requiredGates, preflight.requiredGates)
|
|
209
232
|
|| !sameStringSet(state.satisfiedGates, preflight.satisfiedGates)) {
|
|
210
233
|
errors.push(issue(
|
|
@@ -214,7 +237,7 @@ export async function evaluateStartExecutionPrerequisites({
|
|
|
214
237
|
));
|
|
215
238
|
}
|
|
216
239
|
|
|
217
|
-
const ledger = await validateEventLedger(target, packageRoot, { taskId: effectiveTaskId, eventsPath });
|
|
240
|
+
const ledger = await (readers?.validateLedger ?? validateEventLedger)(target, packageRoot, { taskId: effectiveTaskId, eventsPath });
|
|
218
241
|
errors.push(...prerequisiteLedgerErrors(ledger, contract.value.taskId, preflight, route));
|
|
219
242
|
errors.push(...validateStateLedgerCoherence(state, ledger.events).map((error) => issue(
|
|
220
243
|
error.code,
|
|
@@ -162,12 +162,63 @@ export function projectExecutionProfile(route) {
|
|
|
162
162
|
return route.executionProfile?.resolved ?? LEGACY_EXECUTION_PROFILE;
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
+
function routeProfileSignals(normalizedRoute) {
|
|
166
|
+
const fullReasons = [];
|
|
167
|
+
const balancedReasons = [];
|
|
168
|
+
if (FULL_WORK_TYPES.has(normalizedRoute.workType)) fullReasons.push(`WORK_${normalizedRoute.workType.toUpperCase().replaceAll("-", "_")}`);
|
|
169
|
+
if (BALANCED_WORK_TYPES.has(normalizedRoute.workType)) balancedReasons.push(`WORK_${normalizedRoute.workType.toUpperCase().replaceAll("-", "_")}`);
|
|
170
|
+
for (const surface of normalizedRoute.surfaces) {
|
|
171
|
+
if (FULL_SURFACES.has(surface)) fullReasons.push(`SURFACE_${surface.toUpperCase().replaceAll("-", "_")}`);
|
|
172
|
+
else if (BALANCED_SURFACES.has(surface)) balancedReasons.push(`SURFACE_${surface.toUpperCase().replaceAll("-", "_")}`);
|
|
173
|
+
}
|
|
174
|
+
for (const risk of normalizedRoute.risks) {
|
|
175
|
+
if (FULL_RISKS.has(risk)) fullReasons.push(`RISK_${risk.toUpperCase().replaceAll("-", "_")}`);
|
|
176
|
+
else if (BALANCED_RISKS.has(risk)) balancedReasons.push(`RISK_${risk.toUpperCase().replaceAll("-", "_")}`);
|
|
177
|
+
}
|
|
178
|
+
if (normalizedRoute.behaviorChange) balancedReasons.push("WORK_BEHAVIOR_CHANGE");
|
|
179
|
+
if (normalizedRoute.executableChange) balancedReasons.push("WORK_EXECUTABLE_CHANGE");
|
|
180
|
+
return { fullReasons, balancedReasons };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function contractProfileSignals(textSignals, normalizedRoute) {
|
|
184
|
+
const fullReasons = [];
|
|
185
|
+
if (textSignals.secrets) fullReasons.push("RISK_SECRETS");
|
|
186
|
+
if (textSignals.personalData) fullReasons.push("RISK_PERSONAL_DATA");
|
|
187
|
+
if (textSignals.publication) fullReasons.push("RISK_PUBLICATION");
|
|
188
|
+
if (textSignals.destructive) fullReasons.push("RISK_DESTRUCTIVE");
|
|
189
|
+
if (textSignals.migration && normalizedRoute.surfaces.includes("database")) fullReasons.push("RISK_MIGRATION");
|
|
190
|
+
if (textSignals.payment) fullReasons.push("RISK_PAYMENT");
|
|
191
|
+
if (textSignals.externalMutation) fullReasons.push("RISK_EXTERNAL_MUTATION");
|
|
192
|
+
if (textSignals.authoritySensitiveExternalMutation) fullReasons.push("RISK_AUTHORITY_SENSITIVE_EXTERNAL_MUTATION");
|
|
193
|
+
if (textSignals.broadProductionValidation) fullReasons.push("RISK_PRODUCTION_VALIDATION");
|
|
194
|
+
return fullReasons;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function scopeProfileSignals(scope) {
|
|
198
|
+
const balancedReasons = [];
|
|
199
|
+
if (scope.multiDeliverable) balancedReasons.push("SCOPE_MULTI_DELIVERABLE");
|
|
200
|
+
if (scope.multiClaim) balancedReasons.push("SCOPE_MULTI_CLAIM");
|
|
201
|
+
if (scope.broad || scope.multiSurfaceExecutableChange) balancedReasons.push("SCOPE_BROAD");
|
|
202
|
+
return balancedReasons;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function profileSignals({ normalizedRoute, contract, taskDescriptor }) {
|
|
206
|
+
const route = routeProfileSignals(normalizedRoute);
|
|
207
|
+
const scope = scopeSignals({ routeInput: normalizedRoute, contract, taskDescriptor });
|
|
208
|
+
return {
|
|
209
|
+
fullReasons: [...route.fullReasons, ...contractProfileSignals(contractSignals(contract), normalizedRoute)],
|
|
210
|
+
balancedReasons: [...route.balancedReasons, ...scopeProfileSignals(scope)],
|
|
211
|
+
scope,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
165
215
|
export function resolveExecutionProfile({
|
|
166
216
|
routeInput = {},
|
|
167
217
|
contract = null,
|
|
168
218
|
taskDescriptor = null,
|
|
169
219
|
configuredProfile = "auto",
|
|
170
220
|
requestedProfile = null,
|
|
221
|
+
semanticRecommendation = null,
|
|
171
222
|
} = {}) {
|
|
172
223
|
const normalizedRoute = normalizedRouteInput(routeInput);
|
|
173
224
|
const configured = normalizeRequestedProfile(configuredProfile, "configuredProfile");
|
|
@@ -181,43 +232,7 @@ export function resolveExecutionProfile({
|
|
|
181
232
|
else reasons.push("PROFILE_DEFAULT_AUTO");
|
|
182
233
|
|
|
183
234
|
let floor = "light";
|
|
184
|
-
const fullReasons =
|
|
185
|
-
const balancedReasons = [];
|
|
186
|
-
const addFull = (reason) => fullReasons.push(reason);
|
|
187
|
-
const addBalanced = (reason) => balancedReasons.push(reason);
|
|
188
|
-
|
|
189
|
-
if (FULL_WORK_TYPES.has(normalizedRoute.workType)) {
|
|
190
|
-
addFull(`WORK_${normalizedRoute.workType.toUpperCase().replaceAll("-", "_")}`);
|
|
191
|
-
}
|
|
192
|
-
if (BALANCED_WORK_TYPES.has(normalizedRoute.workType)) {
|
|
193
|
-
addBalanced(`WORK_${normalizedRoute.workType.toUpperCase().replaceAll("-", "_")}`);
|
|
194
|
-
}
|
|
195
|
-
for (const surface of normalizedRoute.surfaces) {
|
|
196
|
-
if (FULL_SURFACES.has(surface)) addFull(`SURFACE_${surface.toUpperCase().replaceAll("-", "_")}`);
|
|
197
|
-
else if (BALANCED_SURFACES.has(surface)) addBalanced(`SURFACE_${surface.toUpperCase().replaceAll("-", "_")}`);
|
|
198
|
-
}
|
|
199
|
-
for (const risk of normalizedRoute.risks) {
|
|
200
|
-
if (FULL_RISKS.has(risk)) addFull(`RISK_${risk.toUpperCase().replaceAll("-", "_")}`);
|
|
201
|
-
else if (BALANCED_RISKS.has(risk)) addBalanced(`RISK_${risk.toUpperCase().replaceAll("-", "_")}`);
|
|
202
|
-
}
|
|
203
|
-
if (normalizedRoute.behaviorChange) addBalanced("WORK_BEHAVIOR_CHANGE");
|
|
204
|
-
if (normalizedRoute.executableChange) addBalanced("WORK_EXECUTABLE_CHANGE");
|
|
205
|
-
|
|
206
|
-
const textSignals = contractSignals(contract);
|
|
207
|
-
if (textSignals.secrets) addFull("RISK_SECRETS");
|
|
208
|
-
if (textSignals.personalData) addFull("RISK_PERSONAL_DATA");
|
|
209
|
-
if (textSignals.publication) addFull("RISK_PUBLICATION");
|
|
210
|
-
if (textSignals.destructive) addFull("RISK_DESTRUCTIVE");
|
|
211
|
-
if (textSignals.migration && normalizedRoute.surfaces.includes("database")) addFull("RISK_MIGRATION");
|
|
212
|
-
if (textSignals.payment) addFull("RISK_PAYMENT");
|
|
213
|
-
if (textSignals.externalMutation) addFull("RISK_EXTERNAL_MUTATION");
|
|
214
|
-
if (textSignals.authoritySensitiveExternalMutation) addFull("RISK_AUTHORITY_SENSITIVE_EXTERNAL_MUTATION");
|
|
215
|
-
if (textSignals.broadProductionValidation) addFull("RISK_PRODUCTION_VALIDATION");
|
|
216
|
-
|
|
217
|
-
const scope = scopeSignals({ routeInput: normalizedRoute, contract, taskDescriptor });
|
|
218
|
-
if (scope.multiDeliverable) addBalanced("SCOPE_MULTI_DELIVERABLE");
|
|
219
|
-
if (scope.multiClaim) addBalanced("SCOPE_MULTI_CLAIM");
|
|
220
|
-
if (scope.broad || scope.multiSurfaceExecutableChange) addBalanced("SCOPE_BROAD");
|
|
235
|
+
const { fullReasons, balancedReasons, scope } = profileSignals({ normalizedRoute, contract, taskDescriptor });
|
|
221
236
|
|
|
222
237
|
if (fullReasons.length > 0) {
|
|
223
238
|
floor = "full";
|
|
@@ -247,7 +262,17 @@ export function resolveExecutionProfile({
|
|
|
247
262
|
}
|
|
248
263
|
|
|
249
264
|
const requestedRank = requested === "auto" ? PROFILE_RANK[floor] : PROFILE_RANK[requested];
|
|
250
|
-
|
|
265
|
+
let resolved = EXECUTION_PROFILES.find((profile) => PROFILE_RANK[profile] === Math.max(requestedRank, PROFILE_RANK[floor]));
|
|
266
|
+
const semanticProfile = typeof semanticRecommendation === "string"
|
|
267
|
+
? semanticRecommendation
|
|
268
|
+
: semanticRecommendation?.executionProfile?.recommendedProfile
|
|
269
|
+
?? semanticRecommendation?.recommendedProfile
|
|
270
|
+
?? null;
|
|
271
|
+
if (["light", "balanced", "full"].includes(semanticProfile)
|
|
272
|
+
&& PROFILE_RANK[semanticProfile] > PROFILE_RANK[resolved]) {
|
|
273
|
+
resolved = semanticProfile;
|
|
274
|
+
reasons.push("PROFILE_ESCALATED_BY_JEV");
|
|
275
|
+
}
|
|
251
276
|
const escalated = requested !== "auto" && PROFILE_RANK[requested] < PROFILE_RANK[floor];
|
|
252
277
|
if (escalated) reasons.push("PROFILE_ESCALATED_BY_SAFETY");
|
|
253
278
|
|
package/src/core/execution.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import path from "node:path";
|
|
1
|
+
import { needsExistingProjectScope, withExistingProjectScope } from "../storage/existing-project-scope.js";
|
|
3
2
|
import { ensureWithin, fileExists } from "./filesystem.js";
|
|
4
3
|
import {
|
|
5
4
|
ARTIFACT_PATHS,
|
|
6
5
|
executionArtifactPath,
|
|
6
|
+
parseCapturedJsonArtifact,
|
|
7
7
|
readJsonArtifact,
|
|
8
8
|
} from "./artifacts.js";
|
|
9
9
|
import { taskArtifactPath, taskExecutionPath } from "./task-paths.js";
|
|
10
|
+
import { getOperationalStore, operationalArtifactExists } from "../storage/operational-context.js";
|
|
10
11
|
|
|
11
12
|
export { E_COMMAND_RESOLUTION_AMBIGUOUS } from "./verification-capability.js";
|
|
12
13
|
import {
|
|
@@ -26,18 +27,14 @@ function executionError(code, message, artifacts = []) {
|
|
|
26
27
|
return error;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
/**
|
|
30
|
-
* Resolves where a new execution artifact should be written. Task-scoped
|
|
31
|
-
* execution artifacts require a real modern task namespace (a task.json
|
|
32
|
-
* descriptor). A descriptor-less task is legacy: writing task-scoped here
|
|
33
|
-
* would create a phantom `.forgeloop/task-state/<key>/executions/` namespace
|
|
34
|
-
* that corrupts task discovery. Reads already fall back across both
|
|
35
|
-
* locations, so a legacy execution stays resolvable.
|
|
36
|
-
*/
|
|
30
|
+
/** Resolve an execution identity; persistence separately requires canonical SQLite authority. */
|
|
37
31
|
export async function resolveExecutionArtifactPath(target, taskId, executionId) {
|
|
32
|
+
if (await needsExistingProjectScope(target)) {
|
|
33
|
+
return withExistingProjectScope(target, () => resolveExecutionArtifactPath(target, taskId, executionId), { readOnly: true });
|
|
34
|
+
}
|
|
38
35
|
if (!taskId) return executionArtifactPath(executionId);
|
|
39
36
|
const descriptorRel = taskArtifactPath(taskId, "descriptor");
|
|
40
|
-
if (await fileExists(ensureWithin(target, descriptorRel))) {
|
|
37
|
+
if (operationalArtifactExists(target, descriptorRel) ?? await fileExists(ensureWithin(target, descriptorRel))) {
|
|
41
38
|
return taskExecutionPath(taskId, executionId);
|
|
42
39
|
}
|
|
43
40
|
return executionArtifactPath(executionId);
|
|
@@ -87,42 +84,31 @@ export async function runCommandExecution({
|
|
|
87
84
|
}
|
|
88
85
|
|
|
89
86
|
export async function readExecutionArtifact({ target, executionRef, packageRoot, taskId } = {}) {
|
|
87
|
+
if (await needsExistingProjectScope(target)) {
|
|
88
|
+
return withExistingProjectScope(target, () => readExecutionArtifact({ target, executionRef, packageRoot, taskId }), { readOnly: true });
|
|
89
|
+
}
|
|
90
90
|
let relativePath;
|
|
91
91
|
try {
|
|
92
|
+
const store = getOperationalStore(target);
|
|
93
|
+
if (store && !taskId) {
|
|
94
|
+
// Validate the public reference before using it as a canonical identity.
|
|
95
|
+
executionArtifactPath(executionRef);
|
|
96
|
+
taskId = store.executionTaskId(executionRef);
|
|
97
|
+
}
|
|
92
98
|
relativePath = taskId ? taskExecutionPath(taskId, executionRef) : executionArtifactPath(executionRef);
|
|
99
|
+
if (store?.recognizes(relativePath)) {
|
|
100
|
+
const execution = store.readExecution(taskId, executionRef);
|
|
101
|
+
return parseCapturedJsonArtifact(
|
|
102
|
+
execution === null ? null : `${JSON.stringify(execution, null, 2)}\n`,
|
|
103
|
+
relativePath,
|
|
104
|
+
"execution",
|
|
105
|
+
packageRoot,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
93
108
|
const artifact = await readJsonArtifact(target, relativePath, "execution", packageRoot);
|
|
94
109
|
return artifact;
|
|
95
110
|
} catch (error) {
|
|
96
111
|
if (error.code === "E_EXECUTION_REF_INVALID") throw error;
|
|
97
|
-
if (error.code === "ARTIFACT_MISSING") {
|
|
98
|
-
if (taskId) {
|
|
99
|
-
try {
|
|
100
|
-
const fallbackPath = executionArtifactPath(executionRef);
|
|
101
|
-
const artifact = await readJsonArtifact(target, fallbackPath, "execution", packageRoot);
|
|
102
|
-
return artifact;
|
|
103
|
-
} catch {
|
|
104
|
-
// ignore fallback failure
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
try {
|
|
108
|
-
const taskStateDir = path.join(target, ".forgeloop", "task-state");
|
|
109
|
-
if (await fileExists(taskStateDir)) {
|
|
110
|
-
const entries = await readdir(taskStateDir, { withFileTypes: true });
|
|
111
|
-
for (const entry of entries) {
|
|
112
|
-
if (entry.isDirectory()) {
|
|
113
|
-
const execFile = path.join(taskStateDir, entry.name, "executions", `${executionRef}.json`);
|
|
114
|
-
if (await fileExists(execFile)) {
|
|
115
|
-
const rel = path.relative(target, execFile).replaceAll("\\", "/");
|
|
116
|
-
return await readJsonArtifact(target, rel, "execution", packageRoot);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
} catch {
|
|
122
|
-
// ignore scan failure
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
112
|
throw executionError("E_EXECUTION_REF_INVALID", "Execution reference does not resolve to a valid ForgeLoop artifact", [relativePath ?? ARTIFACT_PATHS.executionDirectory]);
|
|
127
113
|
}
|
|
128
114
|
}
|
package/src/core/filesystem.js
CHANGED
|
@@ -105,6 +105,17 @@ export function isPathWithin(root, candidate, { platform = process.platform } =
|
|
|
105
105
|
&& !pathApi.isAbsolute(relative));
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** Recheck an opened regular file against its admitted project path before reading. */
|
|
109
|
+
export async function assertRegularProjectFileIdentity(file, filename, observed, root, invalid) {
|
|
110
|
+
const actual = await file.stat({ bigint: true });
|
|
111
|
+
const current = await lstat(filename, { bigint: true });
|
|
112
|
+
if (!actual.isFile() || !current.isFile() || current.isSymbolicLink()
|
|
113
|
+
|| actual.dev !== observed.dev || actual.ino !== observed.ino
|
|
114
|
+
|| current.dev !== observed.dev || current.ino !== observed.ino
|
|
115
|
+
|| !isPathWithin(root, await realpathWithTransientWindowsRetry(filename))) throw invalid();
|
|
116
|
+
return actual;
|
|
117
|
+
}
|
|
118
|
+
|
|
108
119
|
export async function assertSafePath(root, relativePath) {
|
|
109
120
|
const destination = ensureWithin(root, relativePath);
|
|
110
121
|
const absoluteRoot = path.resolve(root);
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { assertSafePath, ensureWithin, fileExists,
|
|
1
|
+
import { assertSafePath, ensureWithin, fileExists, isPathWithin, realpathWithTransientWindowsRetry } from "./filesystem.js";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open } from "node:fs/promises";
|
|
2
4
|
import { ARTIFACT_PATHS, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
|
|
3
5
|
import { assertSchema, readSchema } from "./schema-validation.js";
|
|
4
6
|
import { sha256 } from "./manifest.js";
|
|
5
7
|
import { taskGatePath } from "./task-paths.js";
|
|
8
|
+
import { getOperationalStore, operationalArtifactExists, readOperationalText } from "../storage/operational-context.js";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { withNativeReadScope } from "./native-storage.js";
|
|
6
11
|
|
|
7
12
|
function gateName(value) {
|
|
8
13
|
if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]*$/.test(value)) {
|
|
@@ -32,26 +37,107 @@ export async function readGate(target, gate, packageRoot, options = {}) {
|
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
export async function readGateIfPresent(target, gate, packageRoot, options = {}) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
return withNativeReadScope(target, async () => {
|
|
41
|
+
const relativePath = options?.gatePath ?? (options?.taskId ? taskGatePath(options.taskId, gate) : gatePath(gate, options));
|
|
42
|
+
await assertSafePath(target, relativePath);
|
|
43
|
+
const selected = operationalArtifactExists(target, relativePath);
|
|
44
|
+
if (selected !== null) return selected ? readJsonArtifact(target, relativePath, "gate", packageRoot) : null;
|
|
45
|
+
const filePath = ensureWithin(target, relativePath);
|
|
46
|
+
if (!(await fileExists(filePath))) return null;
|
|
47
|
+
return readJsonArtifact(target, relativePath, "gate", packageRoot);
|
|
48
|
+
});
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
/** Hash selected bytes through the operational read set, including missing rows. */
|
|
52
|
+
export function readCanonicalGateArtifactBinding(target, relativePath) {
|
|
53
|
+
const store = getOperationalStore(target);
|
|
54
|
+
if (store?.recognizes(relativePath) && relativePath.replaceAll("\\", "/").endsWith("/events.ndjson")) {
|
|
55
|
+
if (!operationalArtifactExists(target, relativePath)) return { path: relativePath, sha256: null, byteLength: 0 };
|
|
56
|
+
const digest = createHash("sha256");
|
|
57
|
+
let byteLength = 0;
|
|
58
|
+
for (const event of store.iterateEvents(relativePath)) {
|
|
59
|
+
const bytes = `${JSON.stringify(event)}\n`;
|
|
60
|
+
byteLength += Buffer.byteLength(bytes);
|
|
61
|
+
digest.update(bytes);
|
|
52
62
|
}
|
|
53
|
-
|
|
54
|
-
if (digest !== artifact.sha256) stale.push({ path: artifact.path, status: "changed" });
|
|
63
|
+
return { path: relativePath, sha256: digest.digest("hex"), byteLength };
|
|
55
64
|
}
|
|
56
|
-
|
|
65
|
+
const source = readOperationalText(target, relativePath);
|
|
66
|
+
if (!source.selected) return null;
|
|
67
|
+
return { path: relativePath, sha256: source.text === null ? null : sha256(source.text), byteLength: source.text === null ? 0 : Buffer.byteLength(source.text) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Admit one regular external evidence handle before reading any bytes. */
|
|
71
|
+
async function withGateFile(target, relativePath, maxBytes, callback) {
|
|
72
|
+
const filename = await assertSafePath(target, relativePath);
|
|
73
|
+
const root = await realpathWithTransientWindowsRetry(target);
|
|
74
|
+
const resolved = await realpathWithTransientWindowsRetry(filename);
|
|
75
|
+
if (!isPathWithin(root, resolved)) throw new Error("Gate artifact resolves outside the project");
|
|
76
|
+
const file = await open(resolved, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
77
|
+
try {
|
|
78
|
+
const info = await file.stat();
|
|
79
|
+
const current = await lstat(resolved);
|
|
80
|
+
if (!info.isFile() || !current.isFile() || current.isSymbolicLink()
|
|
81
|
+
|| info.dev !== current.dev || info.ino !== current.ino
|
|
82
|
+
|| !isPathWithin(root, await realpathWithTransientWindowsRetry(resolved))) throw new Error("Gate artifact changed during file admission");
|
|
83
|
+
if (info.size > maxBytes) throw new Error(`Gate artifact exceeds the maximum size of ${maxBytes} bytes`);
|
|
84
|
+
return await callback(file);
|
|
85
|
+
} finally { await file.close(); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function* gateFileChunks(file, maxBytes) {
|
|
89
|
+
let byteLength = 0;
|
|
90
|
+
for await (const bytes of file.createReadStream({ autoClose: false, highWaterMark: 64 * 1024 })) {
|
|
91
|
+
byteLength += bytes.length;
|
|
92
|
+
if (byteLength > maxBytes) throw new Error(`Gate artifact exceeds the maximum size of ${maxBytes} bytes`);
|
|
93
|
+
yield bytes;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Hash external evidence from one verified handle, with bounded streaming memory. */
|
|
98
|
+
export async function hashGateFile(target, relativePath, { maxBytes = Infinity } = {}) {
|
|
99
|
+
return withGateFile(target, relativePath, maxBytes, async file => {
|
|
100
|
+
const digest = createHash("sha256");
|
|
101
|
+
let byteLength = 0;
|
|
102
|
+
for await (const bytes of gateFileChunks(file, maxBytes)) {
|
|
103
|
+
byteLength += bytes.length;
|
|
104
|
+
digest.update(bytes);
|
|
105
|
+
}
|
|
106
|
+
return { path: relativePath, sha256: digest.digest("hex"), byteLength };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Explicit JSON gate input remains file-owned and allocation-bounded. */
|
|
111
|
+
export async function readGateFileJson(target, relativePath, { maxBytes } = {}) {
|
|
112
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new Error("Gate evidence requires a finite byte limit");
|
|
113
|
+
return withGateFile(target, relativePath, maxBytes, async file => {
|
|
114
|
+
const chunks = [];
|
|
115
|
+
for await (const bytes of gateFileChunks(file, maxBytes)) chunks.push(bytes);
|
|
116
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export async function validateGateArtifacts(target, gateValue, packageRoot) {
|
|
121
|
+
return withNativeReadScope(target, async () => {
|
|
122
|
+
const schema = await readSchema("gate", packageRoot);
|
|
123
|
+
assertSchema(gateValue, schema, "gate");
|
|
124
|
+
const stale = [];
|
|
125
|
+
for (const artifact of gateValue.artifacts ?? []) {
|
|
126
|
+
await assertSafePath(target, artifact.path);
|
|
127
|
+
const canonical = readCanonicalGateArtifactBinding(target, artifact.path);
|
|
128
|
+
if (canonical) {
|
|
129
|
+
if (canonical.sha256 === null) stale.push({ path: artifact.path, status: "missing" });
|
|
130
|
+
else if (canonical.sha256 !== artifact.sha256) stale.push({ path: artifact.path, status: "changed" });
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const artifactPath = ensureWithin(target, artifact.path);
|
|
134
|
+
if (!(await fileExists(artifactPath))) {
|
|
135
|
+
stale.push({ path: artifact.path, status: "missing" });
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const binding = await hashGateFile(target, artifact.path);
|
|
139
|
+
if (binding.sha256 !== artifact.sha256) stale.push({ path: artifact.path, status: "changed" });
|
|
140
|
+
}
|
|
141
|
+
return stale;
|
|
142
|
+
});
|
|
57
143
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { isLedgerEventCollection, ledgerEventAt, ledgerEntriesOfTypes } from "./ledger-event-collection.js";
|
|
2
|
+
import { canonicalFingerprint } from "./artifacts.js";
|
|
3
|
+
|
|
4
|
+
export const GATE_SATISFIED_EVENT = "GATE_SATISFIED";
|
|
5
|
+
export const GATE_REVALIDATED_EVENT = "GATE_REVALIDATED";
|
|
6
|
+
const CONTRACT_REVISED_EVENT = "CONTRACT_REVISED";
|
|
7
|
+
const TRANSACTION_COMMITTED_EVENT = "TRANSACTION_COMMITTED";
|
|
8
|
+
|
|
9
|
+
function eventHash(event) {
|
|
10
|
+
const { hash, ...body } = event ?? {};
|
|
11
|
+
return canonicalFingerprint(body);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function assertGateSatisfiedDetails(details) {
|
|
15
|
+
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
16
|
+
|| Object.keys(details).length !== 1
|
|
17
|
+
|| typeof details.gate !== "string"
|
|
18
|
+
|| details.gate.trim().length === 0) {
|
|
19
|
+
const error = new Error("GATE_SATISFIED requires exactly one non-empty gate detail");
|
|
20
|
+
error.code = "E_EVENT_INVALID";
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
return details;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function assertGateRevalidatedDetails(details) {
|
|
27
|
+
const keys = ["gate", "previousGateFingerprint", "gateFingerprint", "stalePaths"];
|
|
28
|
+
if (!details || typeof details !== "object" || Array.isArray(details)
|
|
29
|
+
|| Object.keys(details).length !== keys.length
|
|
30
|
+
|| keys.some((key) => !Object.prototype.hasOwnProperty.call(details, key))
|
|
31
|
+
|| typeof details.gate !== "string"
|
|
32
|
+
|| details.gate.trim().length === 0
|
|
33
|
+
|| !/^[a-z0-9][a-z0-9-]*$/.test(details.gate)
|
|
34
|
+
|| !/^[a-f0-9]{64}$/.test(details.previousGateFingerprint)
|
|
35
|
+
|| !/^[a-f0-9]{64}$/.test(details.gateFingerprint)
|
|
36
|
+
|| !Array.isArray(details.stalePaths)
|
|
37
|
+
|| details.stalePaths.length === 0
|
|
38
|
+
|| details.stalePaths.some((value) => typeof value !== "string" || value.trim().length === 0)
|
|
39
|
+
|| [...details.stalePaths].sort().join("\0") !== details.stalePaths.join("\0")
|
|
40
|
+
|| new Set(details.stalePaths).size !== details.stalePaths.length) {
|
|
41
|
+
const error = new Error("GATE_REVALIDATED requires a gate, two fingerprints, and sorted stale paths");
|
|
42
|
+
error.code = "E_GATE_REVALIDATION_INVALID";
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
return details;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function gateIndex(events, gateEvent, eventName) {
|
|
49
|
+
return events.findIndex((event) => event === gateEvent
|
|
50
|
+
|| (event?.seq === gateEvent?.seq
|
|
51
|
+
&& event?.taskId === gateEvent?.taskId
|
|
52
|
+
&& event?.event === eventName));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isValidGateCommit(gateEvent, commitEvent, operation) {
|
|
56
|
+
return commitEvent?.event === TRANSACTION_COMMITTED_EVENT
|
|
57
|
+
&& commitEvent.taskId === gateEvent.taskId
|
|
58
|
+
&& commitEvent.seq === gateEvent.seq + 1
|
|
59
|
+
&& commitEvent.previousHash === gateEvent.hash
|
|
60
|
+
&& gateEvent.hash === eventHash(gateEvent)
|
|
61
|
+
&& commitEvent.hash === eventHash(commitEvent)
|
|
62
|
+
&& commitEvent.details
|
|
63
|
+
&& typeof commitEvent.details === "object"
|
|
64
|
+
&& !Array.isArray(commitEvent.details)
|
|
65
|
+
&& Object.keys(commitEvent.details).length === 2
|
|
66
|
+
&& typeof commitEvent.details.transactionId === "string"
|
|
67
|
+
&& commitEvent.details.transactionId.length > 0
|
|
68
|
+
&& commitEvent.details.operation === operation;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function resolveGateRecordBoundary(events, gateEvent) {
|
|
72
|
+
if (!isLedgerEventCollection(events) || !gateEvent || gateEvent.event !== GATE_SATISFIED_EVENT) return null;
|
|
73
|
+
const index = gateIndex(events, gateEvent, GATE_SATISFIED_EVENT);
|
|
74
|
+
if (index < 0) return null;
|
|
75
|
+
const commitEvent = ledgerEventAt(events, index + 1);
|
|
76
|
+
return isValidGateCommit(gateEvent, commitEvent, "gate-record")
|
|
77
|
+
? { gateEvent, transactionCommitEvent: commitEvent }
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolveGateRevalidationBoundary(events, gateEvent) {
|
|
82
|
+
if (!isLedgerEventCollection(events) || !gateEvent || gateEvent.event !== GATE_REVALIDATED_EVENT) return null;
|
|
83
|
+
const index = gateIndex(events, gateEvent, GATE_REVALIDATED_EVENT);
|
|
84
|
+
if (index < 0) return null;
|
|
85
|
+
const commitEvent = ledgerEventAt(events, index + 1);
|
|
86
|
+
return isValidGateCommit(gateEvent, commitEvent, "gate-revalidate")
|
|
87
|
+
? { gateEvent, transactionCommitEvent: commitEvent }
|
|
88
|
+
: null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function hasCurrentGateEvidence(events, taskId, gate, latestContractRevisionSeq = 0) {
|
|
92
|
+
return events.some((event) => [GATE_SATISFIED_EVENT, GATE_REVALIDATED_EVENT].includes(event.event)
|
|
93
|
+
&& event.taskId === taskId
|
|
94
|
+
&& event.details?.gate === gate
|
|
95
|
+
&& event.seq > latestContractRevisionSeq);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function validateGateSatisfactionBindings(events = []) {
|
|
99
|
+
const errors = [];
|
|
100
|
+
for (const [index, event] of ledgerEntriesOfTypes(events, [GATE_SATISFIED_EVENT, GATE_REVALIDATED_EVENT])) {
|
|
101
|
+
if (![GATE_SATISFIED_EVENT, GATE_REVALIDATED_EVENT].includes(event.event)) continue;
|
|
102
|
+
const revalidated = event.event === GATE_REVALIDATED_EVENT;
|
|
103
|
+
try {
|
|
104
|
+
if (revalidated) assertGateRevalidatedDetails(event.details);
|
|
105
|
+
else assertGateSatisfiedDetails(event.details);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
errors.push({ code: error.code ?? "E_EVENT_INVALID", message: `event ${event.seq} (${event.event}): ${error.message}` });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const revisedBeforeGate = events.slice(0, index).findLast((candidate) => candidate.taskId === event.taskId
|
|
111
|
+
&& candidate.event === CONTRACT_REVISED_EVENT);
|
|
112
|
+
const boundary = revalidated
|
|
113
|
+
? resolveGateRevalidationBoundary(events, event)
|
|
114
|
+
: resolveGateRecordBoundary(events, event);
|
|
115
|
+
if ((revalidated || revisedBeforeGate) && !boundary) {
|
|
116
|
+
errors.push({
|
|
117
|
+
code: revalidated ? "E_GATE_REVALIDATION_INVALID" : "E_EVENT_INVALID",
|
|
118
|
+
message: revalidated
|
|
119
|
+
? `event ${event.seq} must be immediately followed by its gate-revalidate transaction commit`
|
|
120
|
+
: `event ${event.seq} must be immediately followed by its gate-record transaction commit after contract revision`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return errors;
|
|
125
|
+
}
|