@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,138 @@
|
|
|
1
|
+
import { readdir } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { canonicalFingerprint, readJsonArtifact } from "../artifacts.js";
|
|
4
|
+
import { fileExists, ensureWithin } from "../filesystem.js";
|
|
5
|
+
import { validateEventLedger } from "../events.js";
|
|
6
|
+
import { taskDecisionDirectory } from "../task-paths.js";
|
|
7
|
+
import { getQuestionSet } from "./question-registry.js";
|
|
8
|
+
import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
|
|
9
|
+
import { DECISION_ENGINE_ID, DECISION_POLICY_VERSION, PINNED_JEV_MODEL, SEMANTIC_DECISION_RECORDED_EVENT, SEMANTIC_DECISION_SUPERSEDED_EVENT } from "./constants.js";
|
|
10
|
+
import { assertDecisionFresh } from "./freshness.js";
|
|
11
|
+
import { listOperationalArtifactNames } from "../../storage/operational-context.js";
|
|
12
|
+
import { withProjectReadSnapshot } from "../../storage/project-read-snapshot.js";
|
|
13
|
+
|
|
14
|
+
function invalid(message, code = DECISION_ERROR_CODES.BINDING_INVALID) {
|
|
15
|
+
return decisionError(code, message);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function assertArtifactIdentity(artifact, { expectedDecisionKind, expectedTaskId, expectedQuestionSetId } = {}) {
|
|
19
|
+
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) throw invalid("Semantic decision artifact is not an object.");
|
|
20
|
+
if (artifact.taskId !== expectedTaskId) throw invalid("Semantic decision task binding is invalid.");
|
|
21
|
+
if (expectedDecisionKind && artifact.decisionKind !== expectedDecisionKind) throw invalid("Semantic decision kind binding is invalid.");
|
|
22
|
+
if (expectedQuestionSetId && artifact.questionSetId !== expectedQuestionSetId) throw invalid("Semantic decision question-set binding is invalid.");
|
|
23
|
+
if (artifact.engine !== DECISION_ENGINE_ID || artifact.model !== PINNED_JEV_MODEL) throw decisionError(DECISION_ERROR_CODES.MODEL_UNSUPPORTED, "Semantic decision is not bound to the pinned Jev engine and model.");
|
|
24
|
+
if (artifact.policyVersion !== DECISION_POLICY_VERSION) throw invalid("Semantic decision policy version is invalid.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function assertArtifactShape(artifact) {
|
|
28
|
+
if (artifact.schemaVersion !== 1 || artifact.protocolVersion !== 1
|
|
29
|
+
|| !artifact.answers || typeof artifact.answers !== "object" || Array.isArray(artifact.answers)
|
|
30
|
+
|| !artifact.confidence || typeof artifact.confidence !== "object" || Array.isArray(artifact.confidence)
|
|
31
|
+
|| !artifact.decision || typeof artifact.decision !== "object" || Array.isArray(artifact.decision)
|
|
32
|
+
|| !artifact.usage || typeof artifact.usage !== "object" || Array.isArray(artifact.usage)) {
|
|
33
|
+
throw invalid("Semantic decision artifact schema is invalid.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function assertArtifactAuthority(artifact) {
|
|
38
|
+
if (artifact.authority !== "SEMANTIC_DECISION" || artifact.evidenceAuthority !== "NONE"
|
|
39
|
+
|| artifact.lifecycleAuthority !== false || artifact.completionAuthority !== false
|
|
40
|
+
|| artifact.ownershipAuthority !== false || artifact.installationAuthority !== false) {
|
|
41
|
+
throw invalid("Semantic decision authority boundary is invalid.");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function assertArtifactQuestionSet(artifact) {
|
|
46
|
+
const questionSet = artifact.questionSet ?? getQuestionSet(artifact.questionSetId);
|
|
47
|
+
if (questionSet.id !== artifact.questionSetId || questionSet.decisionKind === undefined) throw invalid("Semantic decision question-set identity is invalid.");
|
|
48
|
+
if (artifact.questionSetVersion !== questionSet.version || artifact.questionSetFingerprint !== questionSet.fingerprint) {
|
|
49
|
+
throw invalid("Semantic decision question-set fingerprint is stale.");
|
|
50
|
+
}
|
|
51
|
+
if (typeof artifact.decisionId !== "string" || artifact.decisionId.length === 0) {
|
|
52
|
+
throw invalid("Semantic decision ID is invalid.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function assertCanonicalArtifact(artifact, options = {}) {
|
|
57
|
+
assertArtifactIdentity(artifact, options);
|
|
58
|
+
assertArtifactShape(artifact);
|
|
59
|
+
assertArtifactAuthority(artifact);
|
|
60
|
+
assertArtifactQuestionSet(artifact);
|
|
61
|
+
return artifact;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function decisionRecordForArtifact(events, artifact) {
|
|
65
|
+
return events.find((event) => event.event === SEMANTIC_DECISION_RECORDED_EVENT
|
|
66
|
+
&& event.taskId === artifact.taskId
|
|
67
|
+
&& event.details?.decisionId === artifact.decisionId) ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isSuperseded(events, artifact) {
|
|
71
|
+
return events.some((event) => event.event === SEMANTIC_DECISION_SUPERSEDED_EVENT
|
|
72
|
+
&& event.taskId === artifact.taskId
|
|
73
|
+
&& event.details?.decisionId === artifact.decisionId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function assertRequiredFreshDecision({ artifact, currentBindings = {}, expectedDecisionKind, expectedTaskId, expectedQuestionSetId, ledger = [] } = {}) {
|
|
77
|
+
assertCanonicalArtifact(artifact, { expectedDecisionKind, expectedTaskId, expectedQuestionSetId });
|
|
78
|
+
if (!Array.isArray(ledger)) throw invalid("Semantic decision ledger is unavailable.", DECISION_ERROR_CODES.LEDGER_INVALID);
|
|
79
|
+
const record = decisionRecordForArtifact(ledger, artifact);
|
|
80
|
+
if (!record || record.details.artifactFingerprint !== canonicalFingerprint(artifact)) {
|
|
81
|
+
throw invalid("Semantic decision has no matching canonical ledger record.", DECISION_ERROR_CODES.LEDGER_INVALID);
|
|
82
|
+
}
|
|
83
|
+
if (isSuperseded(ledger, artifact)) throw decisionError(DECISION_ERROR_CODES.STALE, "Semantic decision has been superseded.");
|
|
84
|
+
assertDecisionFresh(artifact, {
|
|
85
|
+
...currentBindings,
|
|
86
|
+
model: PINNED_JEV_MODEL,
|
|
87
|
+
policyFingerprint: currentBindings.policyFingerprint ?? artifact.policyFingerprint,
|
|
88
|
+
});
|
|
89
|
+
return artifact;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function decisionArtifacts(target, taskId, packageRoot) {
|
|
93
|
+
const directory = taskDecisionDirectory(taskId);
|
|
94
|
+
let names = listOperationalArtifactNames(target, taskId, "decisions");
|
|
95
|
+
if (names === null) {
|
|
96
|
+
const absolute = ensureWithin(target, directory);
|
|
97
|
+
if (!(await fileExists(absolute))) return [];
|
|
98
|
+
names = (await readdir(absolute, { withFileTypes: true }))
|
|
99
|
+
.filter(entry => entry.isFile()).map(entry => entry.name);
|
|
100
|
+
}
|
|
101
|
+
const ids = names.filter(name => name.endsWith(".json")).map(name => name.slice(0, -5)).sort();
|
|
102
|
+
const artifacts = [];
|
|
103
|
+
for (const decisionId of ids) {
|
|
104
|
+
try {
|
|
105
|
+
artifacts.push((await readJsonArtifact(target, `${directory}/${decisionId}.json`, "semantic-decision", packageRoot)).value);
|
|
106
|
+
} catch {
|
|
107
|
+
// The canonical resolver reports a missing/invalid candidate below.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return artifacts;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function resolveRequiredSemanticDecision(options = {}) {
|
|
114
|
+
if (typeof options.taskId !== "string" || !options.taskId) throw decisionError(DECISION_ERROR_CODES.REQUIRED, "A task-bound semantic decision is required.");
|
|
115
|
+
return withProjectReadSnapshot(options.target, () => resolveSelectedSemanticDecision(options));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function resolveSelectedSemanticDecision({ target, packageRoot, taskId, decisionKind, decisionId = null, currentBindings = {} }) {
|
|
119
|
+
const ledgerResult = await validateEventLedger(target, packageRoot, { taskId });
|
|
120
|
+
if (!ledgerResult.valid) throw decisionError(DECISION_ERROR_CODES.LEDGER_INVALID, "The task ledger is invalid for semantic decision resolution.", ledgerResult.errors);
|
|
121
|
+
const events = ledgerResult.events;
|
|
122
|
+
const candidates = decisionId
|
|
123
|
+
? [(await readJsonArtifact(target, `${taskDecisionDirectory(taskId)}/${decisionId}.json`, "semantic-decision", packageRoot)).value]
|
|
124
|
+
: await decisionArtifacts(target, taskId, packageRoot);
|
|
125
|
+
const records = events
|
|
126
|
+
.filter((event) => event.event === SEMANTIC_DECISION_RECORDED_EVENT && event.taskId === taskId && (!decisionKind || event.details?.decisionKind === decisionKind))
|
|
127
|
+
.sort((left, right) => right.seq - left.seq);
|
|
128
|
+
for (const record of records) {
|
|
129
|
+
const artifact = candidates.find((candidate) => candidate.decisionId === record.details?.decisionId);
|
|
130
|
+
if (!artifact || isSuperseded(events, artifact)) continue;
|
|
131
|
+
try {
|
|
132
|
+
return assertRequiredFreshDecision({ artifact, currentBindings, expectedDecisionKind: decisionKind, expectedTaskId: taskId, ledger: events });
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (decisionId) throw error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
throw decisionError(DECISION_ERROR_CODES.REQUIRED, `No fresh canonical Jev decision is available for ${decisionKind ?? "the requested operation"}.`);
|
|
138
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { PINNED_JEV_MODEL } from "./constants.js";
|
|
2
|
+
import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
|
|
3
|
+
import { normalizeCanonicalDecision } from "./normalizers/index.js";
|
|
4
|
+
|
|
5
|
+
function finite(value) { return typeof value === "number" && Number.isFinite(value); }
|
|
6
|
+
|
|
7
|
+
function normalizeAnswer(value) {
|
|
8
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || finite(value)) return value;
|
|
9
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
10
|
+
const result = {};
|
|
11
|
+
for (const [key, child] of Object.entries(value)) result[key] = normalizeAnswer(child);
|
|
12
|
+
return result;
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value)) return value.slice(0, 64).map(normalizeAnswer);
|
|
15
|
+
throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, "Semantic decision answer has an unsupported shape.");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeAnswers(rawAnswers, questionSet) {
|
|
19
|
+
return Object.fromEntries(Object.keys(questionSet.questions).map((key) => {
|
|
20
|
+
if (!Object.prototype.hasOwnProperty.call(rawAnswers, key)) {
|
|
21
|
+
throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, `Semantic decision result is missing answer ${key}.`);
|
|
22
|
+
}
|
|
23
|
+
return [key, normalizeAnswer(rawAnswers[key])];
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function normalizeConfidence(confidence, questionSet) {
|
|
28
|
+
if (!confidence || typeof confidence !== "object" || Array.isArray(confidence)) {
|
|
29
|
+
throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, "Semantic decision result confidence is invalid.");
|
|
30
|
+
}
|
|
31
|
+
return Object.fromEntries(Object.keys(questionSet.questions).map((key) => [key, confidence[key] ?? null]));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function normalizeUsage(raw) {
|
|
35
|
+
const inputTokens = raw?.input_tokens ?? raw?.inputTokens;
|
|
36
|
+
const outputTokens = raw?.output_tokens ?? raw?.outputTokens;
|
|
37
|
+
if (!Number.isInteger(inputTokens) || inputTokens < 0 || !Number.isInteger(outputTokens) || outputTokens < 0) {
|
|
38
|
+
throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, "Semantic decision result usage is invalid.");
|
|
39
|
+
}
|
|
40
|
+
return { inputTokens, outputTokens, reportedBy: "PROVIDER" };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeDecisionResult(raw, { questionSet, input = {}, model = PINNED_JEV_MODEL } = {}) {
|
|
44
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, "Semantic decision result must be an object.");
|
|
45
|
+
if (raw.model !== model || raw.model !== PINNED_JEV_MODEL) throw decisionError(DECISION_ERROR_CODES.MODEL_UNSUPPORTED, "Semantic decision result used an unsupported model.");
|
|
46
|
+
if (!raw.answers || typeof raw.answers !== "object" || Array.isArray(raw.answers)) throw decisionError(DECISION_ERROR_CODES.RESULT_INVALID, "Semantic decision result is missing answers.");
|
|
47
|
+
const answers = normalizeAnswers(raw.answers, questionSet);
|
|
48
|
+
const confidence = normalizeConfidence(raw.confidence, questionSet);
|
|
49
|
+
const usage = normalizeUsage(raw.usage);
|
|
50
|
+
return {
|
|
51
|
+
model,
|
|
52
|
+
questionSetId: questionSet?.id ?? null,
|
|
53
|
+
answers,
|
|
54
|
+
confidence: Object.fromEntries(Object.keys(questionSet.questions).map((key) => [key, confidence[key] ?? null])),
|
|
55
|
+
decision: normalizeCanonicalDecision({ questionSet, answers, confidence, input }),
|
|
56
|
+
usage,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "../artifacts.js";
|
|
2
|
+
import { readConfig } from "../config.js";
|
|
3
|
+
import { appendProtocolEvent, iterateEvents } from "../events.js";
|
|
4
|
+
import { withTaskMutation } from "../task-command.js";
|
|
5
|
+
import { DECISION_DEFAULT_POLICY, PINNED_JEV_MODEL, SEMANTIC_DECISION_RECORDED_EVENT } from "./constants.js";
|
|
6
|
+
import { buildDecisionArtifact, decisionArtifactFingerprint, readDecisionArtifact, writeDecisionArtifact } from "./artifact.js";
|
|
7
|
+
import { decisionEventDetails, decisionSupersededEventDetails } from "./events.js";
|
|
8
|
+
import { createTypesafeEngine } from "../../adapters/typesafe/engine.js";
|
|
9
|
+
import { normalizeDecisionPolicy, decisionPolicyFingerprint } from "./policy.js";
|
|
10
|
+
import { validateDecisionRequest } from "./request.js";
|
|
11
|
+
import { normalizeDecisionResult } from "./result.js";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { readCurrentDecisionBindings } from "./task-bindings.js";
|
|
14
|
+
import { resolveRequiredSemanticDecision } from "./resolver.js";
|
|
15
|
+
import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
|
|
16
|
+
import { getTaskTransaction } from "../transaction.js";
|
|
17
|
+
import { getTestSemanticProvider } from "./test-provider.js";
|
|
18
|
+
import { needsExistingProjectScope, withExistingProjectScope } from "../../storage/existing-project-scope.js";
|
|
19
|
+
import { withProjectReadSnapshot } from "../../storage/project-read-snapshot.js";
|
|
20
|
+
|
|
21
|
+
async function withDecisionScope(options, run) {
|
|
22
|
+
if (options.target && options.taskId && await needsExistingProjectScope(options.target)) {
|
|
23
|
+
return withExistingProjectScope(options.target, () => run(options));
|
|
24
|
+
}
|
|
25
|
+
return run(options);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveProvider(provider) {
|
|
29
|
+
return provider ?? getTestSemanticProvider();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function loadPolicy(target, packageRoot) {
|
|
33
|
+
try {
|
|
34
|
+
const config = await readConfig(target, packageRoot);
|
|
35
|
+
return normalizeDecisionPolicy(config.decisionEngine ?? DECISION_DEFAULT_POLICY);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if (error.code === "ARTIFACT_MISSING") return normalizeDecisionPolicy(DECISION_DEFAULT_POLICY);
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function readCachedDecision({ target, packageRoot, taskId, decisionKind, currentBindings }) {
|
|
43
|
+
try {
|
|
44
|
+
return await resolveRequiredSemanticDecision({ target, packageRoot, taskId, decisionKind, currentBindings });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (["E_DECISION_REQUIRED", "E_DECISION_STALE", "E_DECISION_BINDING_INVALID"].includes(error.code)) return null;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildArtifact({ taskId, decisionId, policy, validated, result, request, taskBindings }) {
|
|
52
|
+
return buildDecisionArtifact({
|
|
53
|
+
taskId,
|
|
54
|
+
decisionId,
|
|
55
|
+
decisionKind: validated.decisionKind,
|
|
56
|
+
model: result.model ?? PINNED_JEV_MODEL,
|
|
57
|
+
questionSetId: validated.questionSet.id,
|
|
58
|
+
questionSetVersion: validated.questionSet.version,
|
|
59
|
+
questionSetFingerprint: validated.questionSet.fingerprint,
|
|
60
|
+
questionSet: validated.questionSet,
|
|
61
|
+
stateFingerprint: canonicalFingerprint(validated.state),
|
|
62
|
+
taskStateFingerprint: taskBindings.taskStateFingerprint ?? canonicalFingerprint(validated.state.lifecycle),
|
|
63
|
+
semanticStateFingerprint: canonicalFingerprint(validated.state.semantic),
|
|
64
|
+
policyVersion: policy.policyVersion,
|
|
65
|
+
policyFingerprint: decisionPolicyFingerprint(policy),
|
|
66
|
+
repositoryFingerprint: request.repositoryFingerprint ?? taskBindings.repositoryFingerprint,
|
|
67
|
+
contractFingerprint: request.contractFingerprint ?? taskBindings.contractFingerprint,
|
|
68
|
+
routeFingerprint: request.routeFingerprint ?? taskBindings.routeFingerprint,
|
|
69
|
+
verificationCycle: request.verificationCycle ?? taskBindings.verificationCycle,
|
|
70
|
+
candidateSetFingerprint: request.candidateSetFingerprint,
|
|
71
|
+
answers: result.answers,
|
|
72
|
+
confidence: result.confidence ?? {},
|
|
73
|
+
decision: result.decision,
|
|
74
|
+
usage: result.usage,
|
|
75
|
+
latencyMs: result.latencyMs ?? null,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function readCachedSemanticDecision({ policy, decisionId, target, taskId, packageRoot, validated, taskBindings, request }) {
|
|
80
|
+
if (!policy.cache || decisionId || !target || !taskId || await getTaskTransaction(target)) return null;
|
|
81
|
+
return readCachedDecision({
|
|
82
|
+
target,
|
|
83
|
+
packageRoot,
|
|
84
|
+
taskId,
|
|
85
|
+
decisionKind: validated.decisionKind,
|
|
86
|
+
currentBindings: {
|
|
87
|
+
...taskBindings,
|
|
88
|
+
stateFingerprint: canonicalFingerprint(validated.state),
|
|
89
|
+
taskStateFingerprint: taskBindings.taskStateFingerprint ?? canonicalFingerprint(validated.state.lifecycle),
|
|
90
|
+
semanticStateFingerprint: canonicalFingerprint(validated.state.semantic),
|
|
91
|
+
questionSetFingerprint: validated.questionSet.fingerprint,
|
|
92
|
+
policyFingerprint: decisionPolicyFingerprint(policy),
|
|
93
|
+
model: PINNED_JEV_MODEL,
|
|
94
|
+
...(request?.candidateSetFingerprint !== undefined ? { candidateSetFingerprint: request.candidateSetFingerprint } : {}),
|
|
95
|
+
...(request?.policyFingerprint !== undefined ? { policyFingerprint: request.policyFingerprint } : {}),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function persistDecision(target, packageRoot, resolvedDecisionId, artifact, ctx) {
|
|
101
|
+
await writeDecisionArtifact(target, ctx.taskId, resolvedDecisionId, artifact, packageRoot, { taskId: ctx.taskId, operation: "semantic-decision" });
|
|
102
|
+
const priorRecords = [];
|
|
103
|
+
const supersededIds = new Set();
|
|
104
|
+
for await (const candidate of iterateEvents(target, packageRoot, { taskId: ctx.taskId })) {
|
|
105
|
+
if (candidate.event === SEMANTIC_DECISION_RECORDED_EVENT && candidate.taskId === ctx.taskId
|
|
106
|
+
&& candidate.details?.decisionKind === artifact.decisionKind) priorRecords.push(candidate);
|
|
107
|
+
if (candidate.event === "SEMANTIC_DECISION_SUPERSEDED") supersededIds.add(candidate.details?.decisionId);
|
|
108
|
+
}
|
|
109
|
+
const event = await appendProtocolEvent(target, { taskId: ctx.taskId, event: SEMANTIC_DECISION_RECORDED_EVENT, details: decisionEventDetails(artifact) }, packageRoot, { taskId: ctx.taskId });
|
|
110
|
+
const previous = priorRecords.findLast(candidate => !supersededIds.has(candidate.details?.decisionId));
|
|
111
|
+
if (!previous) return { artifact, artifactFingerprint: decisionArtifactFingerprint(artifact), event, supersededEvent: null };
|
|
112
|
+
const previousArtifact = await readDecisionArtifact(target, ctx.taskId, previous.details.decisionId, packageRoot);
|
|
113
|
+
const supersededEvent = await appendProtocolEvent(target, {
|
|
114
|
+
taskId: ctx.taskId,
|
|
115
|
+
event: "SEMANTIC_DECISION_SUPERSEDED",
|
|
116
|
+
details: decisionSupersededEventDetails(previousArtifact.value, resolvedDecisionId),
|
|
117
|
+
}, packageRoot, { taskId: ctx.taskId });
|
|
118
|
+
return { artifact, artifactFingerprint: decisionArtifactFingerprint(artifact), event, supersededEvent };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function recordSemanticDecision(options = {}) {
|
|
122
|
+
return withDecisionScope(options, recordSelectedSemanticDecision);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function recordSelectedSemanticDecision({ target, packageRoot, taskId, decisionId, request, provider = null }) {
|
|
126
|
+
const resolvedDecisionId = decisionId ?? `${String(request?.decisionKind ?? "decision").toLowerCase()}-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
127
|
+
const { policy, taskBindings, validated, cached } = await withProjectReadSnapshot(target, async () => {
|
|
128
|
+
const policy = await loadPolicy(target, packageRoot);
|
|
129
|
+
const taskBindings = target && taskId ? await readCurrentDecisionBindings(target, packageRoot, taskId) : {};
|
|
130
|
+
const validated = validateDecisionRequest({
|
|
131
|
+
...request,
|
|
132
|
+
taskId,
|
|
133
|
+
state: { lifecycle: taskBindings.state ?? {}, semantic: request?.state ?? {} },
|
|
134
|
+
});
|
|
135
|
+
const cached = await readCachedSemanticDecision({ policy, decisionId, target, taskId, packageRoot, validated, taskBindings, request });
|
|
136
|
+
return { policy, taskBindings, validated, cached };
|
|
137
|
+
});
|
|
138
|
+
if (cached) return { artifact: cached, artifactFingerprint: decisionArtifactFingerprint(cached), event: null, supersededEvent: null, cached: true, policy };
|
|
139
|
+
const engine = resolveProvider(provider) ?? createTypesafeEngine({ policy });
|
|
140
|
+
if (provider && (provider.id !== "typesafe-jev" || provider.model !== PINNED_JEV_MODEL || typeof provider.evaluate !== "function")) {
|
|
141
|
+
throw decisionError(DECISION_ERROR_CODES.MODEL_UNSUPPORTED, "Semantic decision provider must be the pinned ForgeLoop Jev engine.");
|
|
142
|
+
}
|
|
143
|
+
const result = normalizeDecisionResult(await engine.evaluate(validated), { questionSet: validated.questionSet, input: validated });
|
|
144
|
+
const artifact = buildArtifact({ taskId, decisionId: resolvedDecisionId, policy, validated, result, request, taskBindings });
|
|
145
|
+
const persisted = await withTaskMutation(target, { taskId, packageRoot }, "semantic-decision", (ctx) => persistDecision(target, packageRoot, resolvedDecisionId, artifact, ctx), { explicitRequired: true });
|
|
146
|
+
return { ...persisted, policy };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function ensureSemanticDecision(options = {}) {
|
|
150
|
+
return withDecisionScope(options, ensureSelectedSemanticDecision);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function ensureSelectedSemanticDecision({ target, packageRoot, taskId, decisionId, request, provider = null, allowNetwork = true }) {
|
|
154
|
+
const { policy, validated, cached } = await withProjectReadSnapshot(target, async () => {
|
|
155
|
+
const policy = await loadPolicy(target, packageRoot);
|
|
156
|
+
const taskBindings = await readCurrentDecisionBindings(target, packageRoot, taskId);
|
|
157
|
+
const validated = validateDecisionRequest({
|
|
158
|
+
...request,
|
|
159
|
+
taskId,
|
|
160
|
+
state: { lifecycle: taskBindings.state ?? {}, semantic: request?.state ?? {} },
|
|
161
|
+
});
|
|
162
|
+
const currentBindings = {
|
|
163
|
+
...taskBindings,
|
|
164
|
+
stateFingerprint: canonicalFingerprint(validated.state),
|
|
165
|
+
taskStateFingerprint: taskBindings.taskStateFingerprint ?? canonicalFingerprint(validated.state.lifecycle),
|
|
166
|
+
semanticStateFingerprint: canonicalFingerprint(validated.state.semantic),
|
|
167
|
+
questionSetFingerprint: validated.questionSet.fingerprint,
|
|
168
|
+
policyFingerprint: decisionPolicyFingerprint(policy),
|
|
169
|
+
model: PINNED_JEV_MODEL,
|
|
170
|
+
...(request?.candidateSetFingerprint !== undefined ? { candidateSetFingerprint: request.candidateSetFingerprint } : {}),
|
|
171
|
+
};
|
|
172
|
+
const cached = await getTaskTransaction(target) ? null : await readCachedDecision({
|
|
173
|
+
target, packageRoot, taskId, decisionKind: validated.decisionKind,
|
|
174
|
+
currentBindings,
|
|
175
|
+
});
|
|
176
|
+
return { policy, validated, cached };
|
|
177
|
+
});
|
|
178
|
+
if (cached) return { artifact: cached, artifactFingerprint: decisionArtifactFingerprint(cached), cached: true, policy };
|
|
179
|
+
if (!allowNetwork && !provider) throw decisionError(DECISION_ERROR_CODES.REQUIRED, `A current ${validated.decisionKind} decision is required.`);
|
|
180
|
+
return recordSemanticDecision({ target, packageRoot, taskId, decisionId, request: { ...request, questionSet: validated.questionSet }, provider });
|
|
181
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { DECISION_LIMITS } from "./constants.js";
|
|
3
|
+
import { DECISION_ERROR_CODES, decisionError } from "./errors.js";
|
|
4
|
+
|
|
5
|
+
const SECRET_KEY = /(^|[_-])(api[_-]?key|password|passwd|secret|token|refresh|cookie|authorization|private[_-]?key|credential|database[_-]?url|connection[_-]?string|access[_-]?key|client[_-]?secret)([_-]|$)/i;
|
|
6
|
+
const SECRET_VALUE = /^(?:bearer\s+)?(?:sk-|pk-|ghp_|github_pat_|xox[baprs]-|AIza|AKIA)[A-Za-z0-9._-]{12,}$/i;
|
|
7
|
+
|
|
8
|
+
function isHomePath(value) {
|
|
9
|
+
return typeof value === "string" && (/^\/Users\/[^/]+\//.test(value) || /^\/home\/[^/]+\//.test(value));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sanitize(value, depth, counters) {
|
|
13
|
+
if (depth > DECISION_LIMITS.maxDepth) throw decisionError(DECISION_ERROR_CODES.STATE_LIMIT, "Decision state nesting exceeds the supported limit.");
|
|
14
|
+
if (value === null || typeof value === "boolean" || typeof value === "number") return value;
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
if (/[^\x09\x0A\x0D\x20-\uD7FF\uE000-\uFFFD]/u.test(value)) throw decisionError(DECISION_ERROR_CODES.STATE_UNSAFE, "Decision state contains unsafe control characters.");
|
|
17
|
+
if (SECRET_VALUE.test(value)) throw decisionError(DECISION_ERROR_CODES.STATE_UNSAFE, "Decision state contains a secret-like value.");
|
|
18
|
+
if (isHomePath(value)) return "<redacted-user-path>";
|
|
19
|
+
counters.bytes += Buffer.byteLength(value);
|
|
20
|
+
if (value.length > DECISION_LIMITS.maxStringChars) return `${value.slice(0, DECISION_LIMITS.maxStringChars)}…<truncated>`;
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
if (value.length > DECISION_LIMITS.maxItems) throw decisionError(DECISION_ERROR_CODES.STATE_LIMIT, "Decision state contains too many items.");
|
|
25
|
+
return value.map((item) => sanitize(item, depth + 1, counters));
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === "object") {
|
|
28
|
+
const entries = Object.entries(value);
|
|
29
|
+
if (entries.length > DECISION_LIMITS.maxItems) throw decisionError(DECISION_ERROR_CODES.STATE_LIMIT, "Decision state contains too many fields.");
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const [key, child] of entries) {
|
|
32
|
+
if (SECRET_KEY.test(key) || key.toLowerCase() === "env" || key.toLowerCase() === "environment") continue;
|
|
33
|
+
result[key] = sanitize(child, depth + 1, counters);
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
throw decisionError(DECISION_ERROR_CODES.STATE_UNSAFE, "Decision state contains an unsupported value.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function buildDecisionState(value) {
|
|
41
|
+
const counters = { bytes: 0 };
|
|
42
|
+
const sanitized = sanitize(value, 0, counters);
|
|
43
|
+
const bytes = Buffer.byteLength(JSON.stringify(sanitized));
|
|
44
|
+
if (bytes > DECISION_LIMITS.maxStateBytes) throw decisionError(DECISION_ERROR_CODES.STATE_LIMIT, "Decision state exceeds the supported byte limit.");
|
|
45
|
+
return sanitized;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildLifecycleDecisionState(value = {}) {
|
|
49
|
+
return buildDecisionState(value ?? {});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildSemanticDecisionState(value = {}) {
|
|
53
|
+
return buildDecisionState(value ?? {});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildCombinedDecisionState({ lifecycle = {}, semantic = {} } = {}) {
|
|
57
|
+
return {
|
|
58
|
+
lifecycle: buildLifecycleDecisionState(lifecycle),
|
|
59
|
+
semantic: buildSemanticDecisionState(semantic),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function decisionStateFingerprint(value, canonicalFingerprint) {
|
|
64
|
+
return canonicalFingerprint(buildDecisionState(value));
|
|
65
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { canonicalFingerprint } from "../artifacts.js";
|
|
2
|
+
import { readContract } from "../contract.js";
|
|
3
|
+
import { readPersistedRoute } from "../route-artifact.js";
|
|
4
|
+
import { readWorkState } from "../work-state.js";
|
|
5
|
+
import { withProjectReadSnapshot } from "../../storage/project-read-snapshot.js";
|
|
6
|
+
|
|
7
|
+
async function readOptionalArtifact(reader) {
|
|
8
|
+
try {
|
|
9
|
+
return await reader();
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (error.code === "ARTIFACT_MISSING") return null;
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function readCurrentDecisionBindings(target, packageRoot, taskId, options = {}) {
|
|
17
|
+
return withProjectReadSnapshot(target, () => readSnapshotBindings(target, packageRoot, taskId, options));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function readSnapshotBindings(target, packageRoot, taskId, { candidateSetFingerprint, policyFingerprint }) {
|
|
21
|
+
const state = await readWorkState(target, { packageRoot, taskId });
|
|
22
|
+
const contract = await readOptionalArtifact(() => readContract(target, packageRoot, { taskId }));
|
|
23
|
+
const route = await readOptionalArtifact(() => readPersistedRoute(target, packageRoot, { taskId }));
|
|
24
|
+
return {
|
|
25
|
+
state,
|
|
26
|
+
...(state ? { stateFingerprint: canonicalFingerprint(state) } : {}),
|
|
27
|
+
...(state ? { taskStateFingerprint: canonicalFingerprint(state) } : {}),
|
|
28
|
+
...(state?.repositoryFingerprint ? { repositoryFingerprint: state.repositoryFingerprint } : {}),
|
|
29
|
+
...((contract?.fingerprint ?? state?.contractFingerprint) ? { contractFingerprint: contract?.fingerprint ?? state.contractFingerprint } : {}),
|
|
30
|
+
...((route?.fingerprint ?? state?.routeFingerprint) ? { routeFingerprint: route?.fingerprint ?? state.routeFingerprint } : {}),
|
|
31
|
+
...(state?.verificationCycle !== undefined ? { verificationCycle: state.verificationCycle } : {}),
|
|
32
|
+
...(candidateSetFingerprint !== undefined ? { candidateSetFingerprint } : {}),
|
|
33
|
+
...(policyFingerprint !== undefined ? { policyFingerprint } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// This provider is deliberately reachable only through repository test injection.
|
|
2
|
+
// Production commands always use the pinned TypeSafe Jev engine or fail closed.
|
|
3
|
+
export const testSemanticProvider = Object.freeze({
|
|
4
|
+
id: "typesafe-jev",
|
|
5
|
+
model: "jev-1.13.0",
|
|
6
|
+
async evaluate(request) {
|
|
7
|
+
const answers = Object.fromEntries(Object.entries(request.questionSet.questions).map(([name, question]) => [
|
|
8
|
+
name,
|
|
9
|
+
question.type === "choice" ? Object.keys(question.criteria)[0] : { noul: true },
|
|
10
|
+
]));
|
|
11
|
+
return {
|
|
12
|
+
model: "jev-1.13.0",
|
|
13
|
+
answers,
|
|
14
|
+
confidence: Object.fromEntries(Object.keys(answers).map((name) => [name, 0.95])),
|
|
15
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
16
|
+
};
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
let activeTestSemanticProvider = null;
|
|
21
|
+
|
|
22
|
+
export function installTestSemanticProvider(provider = testSemanticProvider) {
|
|
23
|
+
activeTestSemanticProvider = provider;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function clearTestSemanticProvider() {
|
|
27
|
+
activeTestSemanticProvider = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getTestSemanticProvider() {
|
|
31
|
+
return activeTestSemanticProvider;
|
|
32
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const DECISION_RISK_TIERS = Object.freeze(["LOW_RISK", "NORMAL", "HIGH_RISK", "CRITICAL"]);
|
|
2
|
+
export const DECISION_THRESHOLDS = Object.freeze({
|
|
3
|
+
LOW_RISK: 0.65,
|
|
4
|
+
NORMAL: 0.75,
|
|
5
|
+
HIGH_RISK: 0.9,
|
|
6
|
+
CRITICAL: 0.97,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export function decisionConfidenceThreshold(risk = "NORMAL") {
|
|
10
|
+
return DECISION_THRESHOLDS[risk] ?? DECISION_THRESHOLDS.NORMAL;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function requiresEscalation(confidence, risk = "NORMAL") {
|
|
14
|
+
return typeof confidence !== "number" || confidence < decisionConfidenceThreshold(risk);
|
|
15
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ledgerEventsOfTypes } from "./ledger-event-collection.js";
|
|
2
|
+
|
|
1
3
|
import { canonicalFingerprint } from "./artifacts.js";
|
|
2
4
|
import { assertFailureClass } from "./protocol.js";
|
|
3
5
|
|
|
@@ -212,3 +214,12 @@ export function currentCycleDiagnosis(events, taskId, verificationCycle) {
|
|
|
212
214
|
}
|
|
213
215
|
return null;
|
|
214
216
|
}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
export function latestDiagnosisForTask(events, taskId) {
|
|
220
|
+
let latest = null;
|
|
221
|
+
for (const event of ledgerEventsOfTypes(events ?? [], ["DIAGNOSIS_RECORDED"])) {
|
|
222
|
+
if (!taskId || event.taskId === taskId) latest = event;
|
|
223
|
+
}
|
|
224
|
+
return latest;
|
|
225
|
+
}
|
package/src/core/diagnosis.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { appendProtocolEvent, validateEventLedger } from "./events.js";
|
|
2
2
|
import { readWorkState, mutateWorkState } from "./work-state.js";
|
|
3
|
+
import { getTaskTransaction, withTaskTransaction } from "./transaction.js";
|
|
3
4
|
import {
|
|
4
5
|
DIAGNOSIS_INFORMATION_GAIN,
|
|
5
6
|
assertDiagnosisDetails,
|
|
@@ -22,7 +23,15 @@ export {
|
|
|
22
23
|
normalizeDiagnosisText,
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
export async function recordDiagnosis({
|
|
26
|
+
export async function recordDiagnosis(options) {
|
|
27
|
+
if (await getTaskTransaction(options.target)) return recordDiagnosisInTransaction(options);
|
|
28
|
+
const state = await readWorkState(options.target, { packageRoot: options.packageRoot, taskId: options.taskId, statePath: options.statePath });
|
|
29
|
+
if (!state) return recordDiagnosisInTransaction(options);
|
|
30
|
+
return withTaskTransaction({ target: options.target, taskId: options.taskId ?? state.taskId,
|
|
31
|
+
packageRoot: options.packageRoot, operation: "record-diagnosis" }, () => recordDiagnosisInTransaction(options));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function recordDiagnosisInTransaction({
|
|
26
35
|
target,
|
|
27
36
|
packageRoot,
|
|
28
37
|
hypothesis,
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { ledgerEventsOfTypes } from "./ledger-event-collection.js";
|
|
2
|
+
|
|
1
3
|
export function diagnosticEventsForTask(events, taskId) {
|
|
2
4
|
return (events ?? []).filter((event) =>
|
|
3
5
|
(event.event === "DIAGNOSTIC_CASE_RECORDED" || event.event === "DIAGNOSIS_RECORDED")
|
|
4
6
|
&& (!taskId || event.taskId === taskId));
|
|
5
7
|
}
|
|
6
8
|
|
|
9
|
+
export function* iterateDiagnosticEvents(events, taskId) {
|
|
10
|
+
for (const event of ledgerEventsOfTypes(events ?? [], ["DIAGNOSTIC_CASE_RECORDED", "DIAGNOSIS_RECORDED"])) {
|
|
11
|
+
if (!taskId || event.taskId === taskId) yield event;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
function structuredProjection(event) {
|
|
8
16
|
const details = event.details ?? {};
|
|
9
17
|
return {
|
|
@@ -26,20 +34,29 @@ function legacyProjection(event) {
|
|
|
26
34
|
};
|
|
27
35
|
}
|
|
28
36
|
|
|
37
|
+
function resolveArrayCurrentCycleDiagnostic(events, taskId, verificationCycle) {
|
|
38
|
+
const candidates = diagnosticEventsForTask(events, taskId).filter(event => {
|
|
39
|
+
const cycle = event.details?.verificationCycle;
|
|
40
|
+
return verificationCycle === null || verificationCycle === undefined || cycle === null || cycle === undefined || cycle === verificationCycle;
|
|
41
|
+
}).sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
42
|
+
const structured = [...candidates].reverse().find(event => event.event === "DIAGNOSTIC_CASE_RECORDED");
|
|
43
|
+
const legacy = [...candidates].reverse().find(event => event.event === "DIAGNOSIS_RECORDED");
|
|
44
|
+
return structured ? structuredProjection(structured) : (legacy ? legacyProjection(legacy) : null);
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
export function resolveCurrentCycleDiagnostic(events, taskId, verificationCycle) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
if (Array.isArray(events)) return resolveArrayCurrentCycleDiagnostic(events, taskId, verificationCycle);
|
|
49
|
+
let structured = null;
|
|
50
|
+
let legacy = null;
|
|
51
|
+
for (const event of iterateDiagnosticEvents(events, taskId)) {
|
|
52
|
+
const cycle = event.details?.verificationCycle;
|
|
53
|
+
if (verificationCycle !== null && verificationCycle !== undefined
|
|
54
|
+
&& cycle !== null && cycle !== undefined && cycle !== verificationCycle) continue;
|
|
55
|
+
if (event.event === "DIAGNOSTIC_CASE_RECORDED") {
|
|
56
|
+
if (!structured || (event.seq ?? 0) >= (structured.seq ?? 0)) structured = event;
|
|
57
|
+
} else if (!legacy || (event.seq ?? 0) >= (legacy.seq ?? 0)) legacy = event;
|
|
58
|
+
}
|
|
40
59
|
if (structured) return structuredProjection(structured);
|
|
41
|
-
|
|
42
|
-
const legacy = [...candidates].reverse().find((event) => event.event === "DIAGNOSIS_RECORDED");
|
|
43
60
|
if (legacy) return legacyProjection(legacy);
|
|
44
61
|
|
|
45
62
|
return null;
|