devcontour-harness 0.0.0-stage → 0.1.0-alpha.6
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/LICENSE +21 -0
- package/README.md +183 -2
- package/docs/architecture.md +473 -0
- package/docs/compatibility-versions.md +56 -0
- package/docs/compatibility.md +192 -0
- package/docs/connectors.md +323 -0
- package/docs/contracts.md +603 -0
- package/docs/integrations/hindsight.md +148 -0
- package/docs/quickstart.md +182 -0
- package/docs/release-notes/0.1.0-alpha.6.md +238 -0
- package/docs/review-probes.md +120 -0
- package/docs/review-protocol.md +251 -0
- package/docs/toolchains.md +167 -0
- package/docs/turbo-starter.md +269 -0
- package/docs/upgrade.md +202 -0
- package/examples/README.md +27 -0
- package/examples/external-connector/README.md +19 -0
- package/examples/external-connector/connector.mjs +55 -0
- package/examples/files-only/README.md +28 -0
- package/examples/files-only/context/norms/rules.md +4 -0
- package/examples/files-only/graph.json +23 -0
- package/examples/files-only/policy.json +30 -0
- package/examples/one-vendor/README.md +19 -0
- package/examples/one-vendor/policy.json +27 -0
- package/examples/single-task/README.md +18 -0
- package/examples/single-task/graph.json +27 -0
- package/examples/single-task/policy-same-vendor.json +55 -0
- package/examples/single-task/policy.json +52 -0
- package/examples/single-task/src/.gitkeep +0 -0
- package/examples/single-task/test/run.js +19 -0
- package/examples/toolchains/README.md +32 -0
- package/examples/toolchains/go/bad/label/label.go +9 -0
- package/examples/toolchains/go/good/label/label.go +9 -0
- package/examples/toolchains/go/policy.json +79 -0
- package/examples/toolchains/go/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/go/repo/_gitignore +1 -0
- package/examples/toolchains/go/repo/go.mod +5 -0
- package/examples/toolchains/go/repo/go.sum +2 -0
- package/examples/toolchains/go/repo/label/label.go +9 -0
- package/examples/toolchains/go/repo/label/label_test.go +27 -0
- package/examples/toolchains/go/repo/label/version.go +9 -0
- package/examples/toolchains/go/task.json +25 -0
- package/examples/toolchains/mixed-go/bad/backend/label/label.go +9 -0
- package/examples/toolchains/mixed-go/bad/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-go/good/backend/label/label.go +9 -0
- package/examples/toolchains/mixed-go/good/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-go/policy.json +126 -0
- package/examples/toolchains/mixed-go/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/mixed-go/repo/_gitignore +6 -0
- package/examples/toolchains/mixed-go/repo/backend/go.mod +3 -0
- package/examples/toolchains/mixed-go/repo/backend/label/label.go +9 -0
- package/examples/toolchains/mixed-go/repo/backend/label/label_test.go +30 -0
- package/examples/toolchains/mixed-go/repo/contract/check.mjs +17 -0
- package/examples/toolchains/mixed-go/repo/contract/label.json +9 -0
- package/examples/toolchains/mixed-go/repo/frontend/package.json +15 -0
- package/examples/toolchains/mixed-go/repo/frontend/pnpm-lock.yaml +22 -0
- package/examples/toolchains/mixed-go/repo/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-go/repo/frontend/test/label.test.js +11 -0
- package/examples/toolchains/mixed-go/task.json +25 -0
- package/examples/toolchains/mixed-python/bad/backend/src/fx_backend/__init__.py +6 -0
- package/examples/toolchains/mixed-python/bad/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-python/good/backend/src/fx_backend/__init__.py +6 -0
- package/examples/toolchains/mixed-python/good/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-python/policy.json +121 -0
- package/examples/toolchains/mixed-python/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/mixed-python/repo/_gitignore +6 -0
- package/examples/toolchains/mixed-python/repo/backend/.python-version +1 -0
- package/examples/toolchains/mixed-python/repo/backend/pyproject.toml +19 -0
- package/examples/toolchains/mixed-python/repo/backend/src/fx_backend/__init__.py +6 -0
- package/examples/toolchains/mixed-python/repo/backend/tests/test_label.py +11 -0
- package/examples/toolchains/mixed-python/repo/backend/uv.lock +108 -0
- package/examples/toolchains/mixed-python/repo/contract/check.mjs +17 -0
- package/examples/toolchains/mixed-python/repo/contract/label.json +9 -0
- package/examples/toolchains/mixed-python/repo/frontend/package.json +15 -0
- package/examples/toolchains/mixed-python/repo/frontend/pnpm-lock.yaml +22 -0
- package/examples/toolchains/mixed-python/repo/frontend/src/label.js +4 -0
- package/examples/toolchains/mixed-python/repo/frontend/test/label.test.js +11 -0
- package/examples/toolchains/mixed-python/task.json +25 -0
- package/examples/toolchains/npm/bad/src/label.js +4 -0
- package/examples/toolchains/npm/good/src/label.js +4 -0
- package/examples/toolchains/npm/policy.json +82 -0
- package/examples/toolchains/npm/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/npm/repo/_gitignore +4 -0
- package/examples/toolchains/npm/repo/package-lock.json +22 -0
- package/examples/toolchains/npm/repo/package.json +13 -0
- package/examples/toolchains/npm/repo/src/label.js +4 -0
- package/examples/toolchains/npm/repo/test/label.test.js +13 -0
- package/examples/toolchains/npm/task.json +25 -0
- package/examples/toolchains/pnpm/bad/packages/label/src/label.js +4 -0
- package/examples/toolchains/pnpm/good/packages/label/src/label.js +4 -0
- package/examples/toolchains/pnpm/policy.json +85 -0
- package/examples/toolchains/pnpm/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/pnpm/repo/_gitignore +4 -0
- package/examples/toolchains/pnpm/repo/package.json +10 -0
- package/examples/toolchains/pnpm/repo/packages/checker/check.mjs +5 -0
- package/examples/toolchains/pnpm/repo/packages/checker/package.json +9 -0
- package/examples/toolchains/pnpm/repo/packages/label/package.json +14 -0
- package/examples/toolchains/pnpm/repo/packages/label/src/label.js +4 -0
- package/examples/toolchains/pnpm/repo/packages/label/test/label.test.js +13 -0
- package/examples/toolchains/pnpm/repo/pnpm-lock.yaml +29 -0
- package/examples/toolchains/pnpm/repo/pnpm-workspace.yaml +2 -0
- package/examples/toolchains/pnpm/task.json +25 -0
- package/examples/toolchains/python/bad/src/fx_label/__init__.py +6 -0
- package/examples/toolchains/python/good/src/fx_label/__init__.py +6 -0
- package/examples/toolchains/python/policy.json +80 -0
- package/examples/toolchains/python/repo/.python-version +1 -0
- package/examples/toolchains/python/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/python/repo/_gitignore +5 -0
- package/examples/toolchains/python/repo/pyproject.toml +19 -0
- package/examples/toolchains/python/repo/src/fx_label/__init__.py +6 -0
- package/examples/toolchains/python/repo/tests/test_label.py +13 -0
- package/examples/toolchains/python/repo/uv.lock +108 -0
- package/examples/toolchains/python/task.json +25 -0
- package/examples/toolchains/turbo/bad/packages/label/src/label.js +4 -0
- package/examples/toolchains/turbo/good/packages/label/src/label.js +4 -0
- package/examples/toolchains/turbo/policy.json +110 -0
- package/examples/toolchains/turbo/repo/REQUIREMENTS.md +9 -0
- package/examples/toolchains/turbo/repo/_gitignore +4 -0
- package/examples/toolchains/turbo/repo/package.json +13 -0
- package/examples/toolchains/turbo/repo/packages/checker/check.mjs +5 -0
- package/examples/toolchains/turbo/repo/packages/checker/package.json +9 -0
- package/examples/toolchains/turbo/repo/packages/label/package.json +14 -0
- package/examples/toolchains/turbo/repo/packages/label/src/label.js +4 -0
- package/examples/toolchains/turbo/repo/packages/label/test/label.test.js +13 -0
- package/examples/toolchains/turbo/repo/pnpm-lock.yaml +94 -0
- package/examples/toolchains/turbo/repo/pnpm-workspace.yaml +2 -0
- package/examples/toolchains/turbo/repo/turbo.json +7 -0
- package/examples/toolchains/turbo/task.json +25 -0
- package/examples/turbo-starter/README.md +16 -0
- package/examples/turbo-starter/changes/issued-at-consumer-only.patch +12 -0
- package/examples/turbo-starter/changes/issued-at.patch +130 -0
- package/examples/turbo-starter/cleanup.mjs +58 -0
- package/examples/turbo-starter/graph.json +32 -0
- package/examples/turbo-starter/harness/acceptance/db-isolation.test.mjs +97 -0
- package/examples/turbo-starter/harness/acceptance/env-probe.test.mjs +75 -0
- package/examples/turbo-starter/harness/acceptance/generation.test.mjs +54 -0
- package/examples/turbo-starter/harness/acceptance/issued-at.test.mjs +43 -0
- package/examples/turbo-starter/harness/acceptance/lifecycle-probe.test.mjs +47 -0
- package/examples/turbo-starter/harness/compose-bridge.mjs +593 -0
- package/examples/turbo-starter/harness/compose.acceptance.yml +23 -0
- package/examples/turbo-starter/harness/env/backend.env.template +7 -0
- package/examples/turbo-starter/harness/env/compose.env.template +6 -0
- package/examples/turbo-starter/policy.json +109 -0
- package/examples/turbo-starter/policy.lifecycle.json +114 -0
- package/examples/turbo-starter/setup.mjs +352 -0
- package/examples/two-vendors/policy.json +30 -0
- package/lib/agents/cli-adapter.d.ts +48 -0
- package/lib/agents/cli-adapter.js +386 -0
- package/lib/agents/inspect.d.ts +6 -0
- package/lib/agents/inspect.js +46 -0
- package/lib/agents/probe-server.d.ts +2 -0
- package/lib/agents/probe-server.js +13 -0
- package/lib/agents/review-probe.d.ts +295 -0
- package/lib/agents/review-probe.js +872 -0
- package/lib/agents/runtime-events.d.ts +11 -0
- package/lib/agents/runtime-events.js +21 -0
- package/lib/agents/schemas.d.ts +217 -0
- package/lib/agents/schemas.js +124 -0
- package/lib/agents/usage.d.ts +8 -0
- package/lib/agents/usage.js +83 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +491 -0
- package/lib/connectors/files.d.ts +108 -0
- package/lib/connectors/files.js +133 -0
- package/lib/contracts/checks.d.ts +307 -0
- package/lib/contracts/checks.js +181 -0
- package/lib/contracts/cli.d.ts +3895 -0
- package/lib/contracts/cli.js +664 -0
- package/lib/contracts/context.d.ts +135 -0
- package/lib/contracts/context.js +129 -0
- package/lib/contracts/digest.d.ts +9 -0
- package/lib/contracts/digest.js +22 -0
- package/lib/contracts/errors.d.ts +81 -0
- package/lib/contracts/errors.js +70 -0
- package/lib/contracts/events.d.ts +827 -0
- package/lib/contracts/events.js +258 -0
- package/lib/contracts/execution.d.ts +3216 -0
- package/lib/contracts/execution.js +872 -0
- package/lib/contracts/failure.d.ts +67 -0
- package/lib/contracts/failure.js +127 -0
- package/lib/contracts/index.d.ts +18 -0
- package/lib/contracts/index.js +18 -0
- package/lib/contracts/policy.d.ts +1080 -0
- package/lib/contracts/policy.js +544 -0
- package/lib/contracts/ports.d.ts +417 -0
- package/lib/contracts/ports.js +1 -0
- package/lib/contracts/primitives.d.ts +63 -0
- package/lib/contracts/primitives.js +73 -0
- package/lib/contracts/profiles.d.ts +336 -0
- package/lib/contracts/profiles.js +109 -0
- package/lib/contracts/published.d.ts +3237 -0
- package/lib/contracts/published.js +34 -0
- package/lib/contracts/result.d.ts +1280 -0
- package/lib/contracts/result.js +311 -0
- package/lib/contracts/review.d.ts +441 -0
- package/lib/contracts/review.js +477 -0
- package/lib/contracts/toolchain.d.ts +96 -0
- package/lib/contracts/toolchain.js +89 -0
- package/lib/contracts/version.d.ts +17 -0
- package/lib/contracts/version.js +17 -0
- package/lib/contracts/work.d.ts +173 -0
- package/lib/contracts/work.js +178 -0
- package/lib/control/base-move.d.ts +44 -0
- package/lib/control/base-move.js +90 -0
- package/lib/control/doctor.d.ts +50 -0
- package/lib/control/doctor.js +113 -0
- package/lib/control/harness.d.ts +1125 -0
- package/lib/control/harness.js +741 -0
- package/lib/control/provenance.d.ts +17 -0
- package/lib/control/provenance.js +25 -0
- package/lib/control/recovery.d.ts +237 -0
- package/lib/control/recovery.js +236 -0
- package/lib/control/review.d.ts +62 -0
- package/lib/control/review.js +178 -0
- package/lib/control/workflow.d.ts +247 -0
- package/lib/control/workflow.js +259 -0
- package/lib/engine/budget.d.ts +345 -0
- package/lib/engine/budget.js +278 -0
- package/lib/engine/classify.d.ts +8 -0
- package/lib/engine/classify.js +32 -0
- package/lib/engine/engine.d.ts +1527 -0
- package/lib/engine/engine.js +2259 -0
- package/lib/engine/failure.d.ts +1 -0
- package/lib/engine/failure.js +3 -0
- package/lib/engine/joint-runner.d.ts +257 -0
- package/lib/engine/joint-runner.js +254 -0
- package/lib/engine/measured.d.ts +15 -0
- package/lib/engine/measured.js +82 -0
- package/lib/engine/prompt.d.ts +66 -0
- package/lib/engine/prompt.js +131 -0
- package/lib/engine/property-recovery.d.ts +50 -0
- package/lib/engine/property-recovery.js +171 -0
- package/lib/engine/repair.d.ts +33 -0
- package/lib/engine/repair.js +82 -0
- package/lib/engine/result.d.ts +9 -0
- package/lib/engine/result.js +218 -0
- package/lib/engine/reuse.d.ts +80 -0
- package/lib/engine/reuse.js +102 -0
- package/lib/engine/runner.d.ts +360 -0
- package/lib/engine/runner.js +1836 -0
- package/lib/engine/standalone.d.ts +6 -0
- package/lib/engine/standalone.js +48 -0
- package/lib/engine/usage.d.ts +87 -0
- package/lib/engine/usage.js +165 -0
- package/lib/environment/isolation.d.ts +110 -0
- package/lib/environment/isolation.js +238 -0
- package/lib/environment/lifecycle.d.ts +40 -0
- package/lib/environment/lifecycle.js +197 -0
- package/lib/environment/live-calls.d.ts +14 -0
- package/lib/environment/live-calls.js +73 -0
- package/lib/environment/os-tools.d.ts +16 -0
- package/lib/environment/os-tools.js +35 -0
- package/lib/environment/process.d.ts +32 -0
- package/lib/environment/process.js +369 -0
- package/lib/environment/redaction.d.ts +22 -0
- package/lib/environment/redaction.js +102 -0
- package/lib/environment/resources.d.ts +31 -0
- package/lib/environment/resources.js +120 -0
- package/lib/index.d.ts +65 -0
- package/lib/index.js +67 -0
- package/lib/integrations/connector/commands.d.ts +358 -0
- package/lib/integrations/connector/commands.js +428 -0
- package/lib/integrations/connector/config.d.ts +215 -0
- package/lib/integrations/connector/config.js +214 -0
- package/lib/integrations/connector/delivery.d.ts +107 -0
- package/lib/integrations/connector/delivery.js +591 -0
- package/lib/integrations/connector/fake.d.ts +291 -0
- package/lib/integrations/connector/fake.js +786 -0
- package/lib/integrations/connector/history.d.ts +124 -0
- package/lib/integrations/connector/history.js +497 -0
- package/lib/integrations/connector/index.d.ts +16 -0
- package/lib/integrations/connector/index.js +16 -0
- package/lib/integrations/connector/redact.d.ts +19 -0
- package/lib/integrations/connector/redact.js +91 -0
- package/lib/integrations/connector/registry.d.ts +15 -0
- package/lib/integrations/connector/registry.js +43 -0
- package/lib/integrations/connector/snapshot.d.ts +131 -0
- package/lib/integrations/connector/snapshot.js +422 -0
- package/lib/integrations/connector/source.d.ts +37 -0
- package/lib/integrations/connector/source.js +73 -0
- package/lib/integrations/connector/steps.d.ts +71 -0
- package/lib/integrations/connector/steps.js +188 -0
- package/lib/integrations/connector/store.d.ts +140 -0
- package/lib/integrations/connector/store.js +277 -0
- package/lib/integrations/connector/summary.d.ts +38 -0
- package/lib/integrations/connector/summary.js +59 -0
- package/lib/integrations/connector/testing-worker.d.ts +1 -0
- package/lib/integrations/connector/testing-worker.js +81 -0
- package/lib/integrations/connector/testing.d.ts +205 -0
- package/lib/integrations/connector/testing.js +1615 -0
- package/lib/integrations/connector/transport.d.ts +240 -0
- package/lib/integrations/connector/transport.js +28 -0
- package/lib/integrations/memory/context.d.ts +120 -0
- package/lib/integrations/memory/context.js +239 -0
- package/lib/integrations/memory/index.d.ts +8 -0
- package/lib/integrations/memory/index.js +8 -0
- package/lib/integrations/memory/provider.d.ts +79 -0
- package/lib/integrations/memory/provider.js +22 -0
- package/lib/integrations/memory/sink.d.ts +56 -0
- package/lib/integrations/memory/sink.js +116 -0
- package/lib/integrations/memory/testing.d.ts +44 -0
- package/lib/integrations/memory/testing.js +61 -0
- package/lib/integrations/relay-v1/index.d.ts +122 -0
- package/lib/integrations/relay-v1/index.js +860 -0
- package/lib/integrations/relay-v2/index.d.ts +145 -0
- package/lib/integrations/relay-v2/index.js +752 -0
- package/lib/integrations/relay-v2/process.d.ts +32 -0
- package/lib/integrations/relay-v2/process.js +112 -0
- package/lib/integrations/relay-v2/protocol.d.ts +172 -0
- package/lib/integrations/relay-v2/protocol.js +142 -0
- package/lib/package.d.ts +2 -0
- package/lib/package.js +3 -0
- package/lib/profiles/resolve.d.ts +190 -0
- package/lib/profiles/resolve.js +273 -0
- package/lib/storage/document.d.ts +1234 -0
- package/lib/storage/document.js +80 -0
- package/lib/storage/sqlite.d.ts +35 -0
- package/lib/storage/sqlite.js +260 -0
- package/lib/testing/index.d.ts +5 -0
- package/lib/testing/index.js +5 -0
- package/lib/testing/store-contract.d.ts +8 -0
- package/lib/testing/store-contract.js +167 -0
- package/lib/toolchains/index.d.ts +9 -0
- package/lib/toolchains/index.js +9 -0
- package/lib/toolchains/inspect.d.ts +105 -0
- package/lib/toolchains/inspect.js +831 -0
- package/lib/toolchains/manifests.d.ts +67 -0
- package/lib/toolchains/manifests.js +238 -0
- package/lib/toolchains/probe.d.ts +28 -0
- package/lib/toolchains/probe.js +54 -0
- package/lib/toolchains/recipes.d.ts +33 -0
- package/lib/toolchains/recipes.js +51 -0
- package/lib/validate.d.ts +36 -0
- package/lib/validate.js +175 -0
- package/lib/verification/acceptance.d.ts +284 -0
- package/lib/verification/acceptance.js +670 -0
- package/lib/verification/checks.d.ts +72 -0
- package/lib/verification/checks.js +190 -0
- package/lib/verification/confirm.d.ts +125 -0
- package/lib/verification/confirm.js +411 -0
- package/lib/verification/corpus.d.ts +1303 -0
- package/lib/verification/corpus.js +744 -0
- package/lib/verification/dependencies.d.ts +66 -0
- package/lib/verification/dependencies.js +144 -0
- package/lib/verification/gates.d.ts +160 -0
- package/lib/verification/gates.js +600 -0
- package/lib/verification/joint.d.ts +63 -0
- package/lib/verification/joint.js +121 -0
- package/lib/verification/proof.d.ts +54 -0
- package/lib/verification/proof.js +96 -0
- package/lib/verification/property.d.ts +86 -0
- package/lib/verification/property.js +214 -0
- package/lib/verification/requirements.d.ts +25 -0
- package/lib/verification/requirements.js +69 -0
- package/lib/verification/self-check.d.ts +29 -0
- package/lib/verification/self-check.js +99 -0
- package/lib/verification/spike.d.ts +79 -0
- package/lib/verification/spike.js +124 -0
- package/package.json +100 -4
- package/profiles/expo-mobile.json +15 -0
- package/profiles/go-api.json +15 -0
- package/profiles/go-gotestsum.json +21 -0
- package/profiles/mobile-maestro.json +29 -0
- package/profiles/nest-api.json +26 -0
- package/profiles/next-product.json +72 -0
- package/profiles/node-checks.json +77 -0
- package/profiles/npm-ci.json +22 -0
- package/profiles/pnpm-frozen.json +24 -0
- package/profiles/python-api.json +32 -0
- package/profiles/python-uv.json +26 -0
- package/profiles/react-native.json +50 -0
- package/profiles/react-vite-admin.json +71 -0
- package/schemas/context-snapshot.schema.json +231 -0
- package/schemas/contract-document.schema.json +385 -0
- package/schemas/execution-document.schema.json +3695 -0
- package/schemas/execution-policy.schema.json +2196 -0
- package/schemas/joint-verification.schema.json +617 -0
- package/schemas/result-envelope.schema.json +1652 -0
- package/schemas/review-record.schema.json +177 -0
- package/schemas/run-event.schema.json +2458 -0
- package/schemas/run-result.schema.json +1570 -0
- package/schemas/stack-profile.schema.json +535 -0
- package/schemas/work-graph.schema.json +381 -0
- package/schemas/work-order.schema.json +256 -0
- package/skills/harness/SKILL.md +64 -0
- package/templates/AGENTS.md +13 -0
- package/templates/controller-mode/AGENTS.controller.md +58 -0
- package/templates/controller-mode/README.md +44 -0
- package/templates/graph.json +22 -0
- package/templates/policy.json +28 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, realpath, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { HarnessError, idSchema, } from "../contracts/index.js";
|
|
7
|
+
import { executionEnvironment, runSteps, withEnvironment } from "../environment/lifecycle.js";
|
|
8
|
+
import { git } from "../environment/process.js";
|
|
9
|
+
import { contractDocuments, obligationOf } from "./checks.js";
|
|
10
|
+
import { MAX_CASE_BYTES, canonicalJson, scrubLocalPaths, sha256, } from "./corpus.js";
|
|
11
|
+
import { runCheck } from "./gates.js";
|
|
12
|
+
/**
|
|
13
|
+
* Быстрый маршрут: подтверждённый пример становится обязательным случаем.
|
|
14
|
+
*
|
|
15
|
+
* Пример reviewer или ведущего — гипотеза. Controller сам запускает
|
|
16
|
+
* закреплённый эталон на этом входе (режим одного случая того же checker,
|
|
17
|
+
* что исполняет gate), повторяет проверку реализации на исходном дефектном
|
|
18
|
+
* SHA и требует воспроизводимого расхождения. Ответ эталона, а не слова
|
|
19
|
+
* reviewer, становится ожиданием. Команда из воспроизведения не исполняется
|
|
20
|
+
* никогда; данные входа не интерпретируются как shell.
|
|
21
|
+
*
|
|
22
|
+
* Маршрут только добавляет данные под тем же checker/oracle, той же областью
|
|
23
|
+
* и правилами ответа. Удаление случая, замена ожидания, допуск, покрытие,
|
|
24
|
+
* новый checker или команда — обычное изменение договора проверки.
|
|
25
|
+
*/
|
|
26
|
+
export const fastPathChanges = [
|
|
27
|
+
'add',
|
|
28
|
+
'remove',
|
|
29
|
+
'replace-expected',
|
|
30
|
+
'tolerance',
|
|
31
|
+
'coverage',
|
|
32
|
+
'checker',
|
|
33
|
+
'command',
|
|
34
|
+
];
|
|
35
|
+
export const caseInputSchema = z.strictObject({
|
|
36
|
+
workId: idSchema,
|
|
37
|
+
property: z.string().min(1).max(200),
|
|
38
|
+
/** Точный вход как JSON-текст. */
|
|
39
|
+
input: z.string().min(1).max(MAX_CASE_BYTES),
|
|
40
|
+
/** Утверждение источника; эталон его проверяет, но не наследует. */
|
|
41
|
+
expected: z.string().max(MAX_CASE_BYTES).nullable().optional(),
|
|
42
|
+
actual: z.string().max(MAX_CASE_BYTES).nullable().optional(),
|
|
43
|
+
reproductionId: z.string().max(80).optional(),
|
|
44
|
+
/** Дефектный SHA; по умолчанию — кандидат последней неуспешной попытки. */
|
|
45
|
+
sourceSha: z
|
|
46
|
+
.string()
|
|
47
|
+
.regex(/^[a-f0-9]{40,64}$/)
|
|
48
|
+
.optional(),
|
|
49
|
+
sourceRunId: z.string().max(200).optional(),
|
|
50
|
+
/** Какие незавершённые поручения принимают новую версию; по умолчанию — это. */
|
|
51
|
+
adopt: z.array(idSchema).max(30).optional(),
|
|
52
|
+
/** Ключ идемпотентности: повтор с тем же ключом возвращает прежний исход. */
|
|
53
|
+
key: z.string().regex(/^[A-Za-z0-9_.:-]{8,120}$/),
|
|
54
|
+
change: z.enum(fastPathChanges).default('add'),
|
|
55
|
+
origin: z.enum(['review', 'lead', 'agent', 'operator']).default('agent'),
|
|
56
|
+
runtime: z.string().max(40).optional(),
|
|
57
|
+
});
|
|
58
|
+
/** Точка после записи версии и до принятия: тест моделирует в ней сбой. */
|
|
59
|
+
export const caseHooks = { afterRecord: () => undefined };
|
|
60
|
+
const caseResult = z.object({
|
|
61
|
+
property: z.string(),
|
|
62
|
+
valid: z.boolean().optional(),
|
|
63
|
+
expected: z.unknown().optional(),
|
|
64
|
+
actual: z.unknown().optional(),
|
|
65
|
+
equal: z.boolean().optional(),
|
|
66
|
+
oracleError: z.string().optional(),
|
|
67
|
+
error: z.string().optional(),
|
|
68
|
+
});
|
|
69
|
+
/** Изменённые закреплённые файлы на SHA: checker должен быть утверждённой редакцией. */
|
|
70
|
+
export async function staleArtifacts(repoPath, sha, artifacts) {
|
|
71
|
+
const stale = [];
|
|
72
|
+
for (const a of artifacts) {
|
|
73
|
+
const blob = await git(repoPath, 'rev-parse', `${sha}:${a.path}`).catch(() => '');
|
|
74
|
+
if (blob !== a.blob)
|
|
75
|
+
stale.push(a.path);
|
|
76
|
+
}
|
|
77
|
+
return stale;
|
|
78
|
+
}
|
|
79
|
+
export async function submitCase(ctx, raw) {
|
|
80
|
+
const input = caseInputSchema.parse(raw);
|
|
81
|
+
if (input.change !== 'add')
|
|
82
|
+
throw new HarnessError('INVALID_INPUT', `Быстрый маршрут только добавляет подтверждённые случаи. Изменение «${input.change}» — обычное изменение договора проверки с независимым ревью контракта. Принятые версии корпуса не переписываются.`);
|
|
83
|
+
const corpus = ctx.corpus;
|
|
84
|
+
const store = corpus.engine.host.store;
|
|
85
|
+
const { work, ref, source } = store.read((v) => {
|
|
86
|
+
const work = v.work(input.workId);
|
|
87
|
+
if (!work)
|
|
88
|
+
throw new HarnessError('NOT_FOUND', 'Поручение не найдено: ' + input.workId);
|
|
89
|
+
const contracts = contractDocuments(work.order, v);
|
|
90
|
+
// Неизвестное свойство не подбирается по тексту: решение «отклонено»
|
|
91
|
+
// записывается в корпус первого договора проверки поручения.
|
|
92
|
+
const property = obligationOf(contracts, input.property)
|
|
93
|
+
? input.property
|
|
94
|
+
: contracts.flatMap((c) => c.checks?.obligations.map((o) => o.property) ?? [])[0];
|
|
95
|
+
if (!property)
|
|
96
|
+
throw new HarnessError('NOT_READY', 'У поручения нет договора проверки с корпусом');
|
|
97
|
+
return {
|
|
98
|
+
work: structuredClone(work),
|
|
99
|
+
ref: corpus.suiteFor(v, work, property),
|
|
100
|
+
source: sourceOf(v.attempts(work.id), work, input),
|
|
101
|
+
};
|
|
102
|
+
});
|
|
103
|
+
const origin = { kind: input.origin, ...(input.runtime ? { runtime: input.runtime } : {}) };
|
|
104
|
+
const started = corpus.begin(ref.suite, input.key);
|
|
105
|
+
if (started.done) {
|
|
106
|
+
const done = started.done;
|
|
107
|
+
const suite = corpus.read(ref.suite.id);
|
|
108
|
+
for (const p of suite.pending.filter((x) => x.status === 'waiting' && !x.awaitingOperator && x.version === done.version))
|
|
109
|
+
corpus.adopt(suite.id, p.id);
|
|
110
|
+
return view(corpus.read(suite.id), done.id, true);
|
|
111
|
+
}
|
|
112
|
+
const token = started.token;
|
|
113
|
+
if (!source) {
|
|
114
|
+
corpus.end(ref.suite.id, input.key, token);
|
|
115
|
+
throw new HarnessError('NOT_READY', 'Не указан дефектный SHA и у поручения нет завершённого кандидата');
|
|
116
|
+
}
|
|
117
|
+
const heartbeat = setInterval(() => {
|
|
118
|
+
try {
|
|
119
|
+
corpus.heartbeat(ref.suite.id, input.key, token);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
/* Утраченное владение увидит запись решения. */
|
|
123
|
+
}
|
|
124
|
+
}, Math.max(1000, ctx.policy.leaseMs / 2));
|
|
125
|
+
heartbeat.unref();
|
|
126
|
+
try {
|
|
127
|
+
const decided = await decide(ctx, work, ref, input, source);
|
|
128
|
+
// Диагностика решения переносима: локальные каталоги из исключений
|
|
129
|
+
// реализации заменяются меткой до записи.
|
|
130
|
+
const verdict = {
|
|
131
|
+
...decided,
|
|
132
|
+
reason: scrubLocalPaths(decided.reason, corpus.roots),
|
|
133
|
+
...(decided.observed
|
|
134
|
+
? {
|
|
135
|
+
observed: {
|
|
136
|
+
...decided.observed,
|
|
137
|
+
...(decided.observed.actual !== undefined
|
|
138
|
+
? { actual: scrubLocalPaths(decided.observed.actual, corpus.roots) }
|
|
139
|
+
: {}),
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
: {}),
|
|
143
|
+
};
|
|
144
|
+
const repo = ctx.policy.repositories.find((r) => r.id === work.order.repositoryId);
|
|
145
|
+
const recorded = corpus.record(corpus.read(ref.suite.id) ?? ref.suite, input.key, token, {
|
|
146
|
+
key: input.key,
|
|
147
|
+
property: input.property,
|
|
148
|
+
input: verdict.input,
|
|
149
|
+
inputHash: sha256(verdict.input),
|
|
150
|
+
outcome: verdict.outcome,
|
|
151
|
+
reason: verdict.reason,
|
|
152
|
+
claimed: {
|
|
153
|
+
expected: input.expected ?? null,
|
|
154
|
+
actual: input.actual ?? null,
|
|
155
|
+
...(input.reproductionId ? { reproduction: input.reproductionId } : {}),
|
|
156
|
+
},
|
|
157
|
+
source,
|
|
158
|
+
...(verdict.observed ? { observed: verdict.observed } : {}),
|
|
159
|
+
...(verdict.method ? { method: verdict.method } : {}),
|
|
160
|
+
origin,
|
|
161
|
+
}, (content) => execFileSync('git', ['hash-object', '-w', '--stdin'], {
|
|
162
|
+
cwd: ctx.repoPath(repo),
|
|
163
|
+
input: content,
|
|
164
|
+
})
|
|
165
|
+
.toString()
|
|
166
|
+
.trim(), {
|
|
167
|
+
works: input.adopt ?? [work.id],
|
|
168
|
+
actor: `${origin.kind}${origin.runtime ? ':' + origin.runtime : ''}`,
|
|
169
|
+
// operator mode: данные корпуса записываются, принятие поручением ждёт
|
|
170
|
+
// ручного технического подтверждения.
|
|
171
|
+
manual: ctx.policy.approvalMode === 'operator',
|
|
172
|
+
});
|
|
173
|
+
caseHooks.afterRecord();
|
|
174
|
+
if (recorded.pending && !recorded.pending.awaitingOperator)
|
|
175
|
+
corpus.adopt(ref.suite.id, recorded.pending.id);
|
|
176
|
+
return view(corpus.read(ref.suite.id), recorded.decision.id, false);
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
corpus.end(ref.suite.id, input.key, token);
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
clearInterval(heartbeat);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function sourceOf(attempts, work, input) {
|
|
187
|
+
if (input.sourceSha)
|
|
188
|
+
return {
|
|
189
|
+
sha: input.sourceSha,
|
|
190
|
+
...(input.sourceRunId ? { runId: input.sourceRunId } : {}),
|
|
191
|
+
taskId: work.id,
|
|
192
|
+
};
|
|
193
|
+
const run = input.sourceRunId
|
|
194
|
+
? attempts.find((a) => a.id === input.sourceRunId)
|
|
195
|
+
: attempts.findLast((a) => a.candidateSha && a.status !== 'active');
|
|
196
|
+
if (!run?.candidateSha)
|
|
197
|
+
return undefined;
|
|
198
|
+
return { sha: run.candidateSha, runId: run.id, taskId: work.id };
|
|
199
|
+
}
|
|
200
|
+
function view(suite, decisionId, repeated) {
|
|
201
|
+
const decision = suite.decisions.find((d) => d.id === decisionId);
|
|
202
|
+
const head = suite.versions.at(-1);
|
|
203
|
+
return {
|
|
204
|
+
suite: suite.id,
|
|
205
|
+
repeated,
|
|
206
|
+
decision,
|
|
207
|
+
version: head ? { number: head.number, digest: head.digest, cases: head.cases.length } : null,
|
|
208
|
+
adoption: suite.pending
|
|
209
|
+
.filter((p) => p.version === decision.version)
|
|
210
|
+
.map(({ id, status, results, awaitingOperator }) => ({
|
|
211
|
+
id,
|
|
212
|
+
status,
|
|
213
|
+
results,
|
|
214
|
+
...(awaitingOperator ? { awaitingOperator } : {}),
|
|
215
|
+
})),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
async function decide(ctx, work, ref, input, source) {
|
|
219
|
+
const reject = (reason, text = input.input) => ({
|
|
220
|
+
input: text,
|
|
221
|
+
outcome: 'rejected',
|
|
222
|
+
reason,
|
|
223
|
+
});
|
|
224
|
+
let text;
|
|
225
|
+
try {
|
|
226
|
+
text = canonicalJson(JSON.parse(input.input));
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
return reject('вход не JSON');
|
|
230
|
+
}
|
|
231
|
+
// Замаскированный вход непригоден: настоящего значения уже нет.
|
|
232
|
+
if (text.includes('[REDACTED]'))
|
|
233
|
+
return reject('вход замаскирован redaction и непригоден', text);
|
|
234
|
+
if (Buffer.byteLength(text) + Buffer.byteLength(input.expected ?? '') > MAX_CASE_BYTES)
|
|
235
|
+
return reject(`случай больше ${MAX_CASE_BYTES} байт`, text);
|
|
236
|
+
const store = ctx.corpus.engine.host.store;
|
|
237
|
+
const found = store.read((v) => obligationOf(contractDocuments(work.order, v), input.property));
|
|
238
|
+
if (!found)
|
|
239
|
+
return reject(`свойство ${input.property} не объявлено в договорах поручения`, text);
|
|
240
|
+
const declaration = found.contract.checks;
|
|
241
|
+
if (!declaration.corpus?.extensible || !found.obligation.coverage.corpus)
|
|
242
|
+
return reject('договор проверки не разрешает пополнять корпус этого свойства', text);
|
|
243
|
+
if (!work.order.properties?.includes(input.property))
|
|
244
|
+
return reject('механизм договора проверки для поручения не включён или свойство вне его области: включение проходит ревью плана', text);
|
|
245
|
+
const repo = ctx.policy.repositories.find((r) => r.id === (found.contract.repositoryId ?? work.order.repositoryId));
|
|
246
|
+
const gate = repo?.gates.find((g) => g.id === declaration.gate);
|
|
247
|
+
if (!repo || !gate?.property)
|
|
248
|
+
return reject(`gate ${declaration.gate} не настроен как генеративный`, text);
|
|
249
|
+
const artifacts = found.contract.artifacts ?? [];
|
|
250
|
+
const method = {
|
|
251
|
+
gate: gate.id,
|
|
252
|
+
command: gate.command,
|
|
253
|
+
artifacts: artifacts.map((a) => ({ path: a.path, blob: a.blob })),
|
|
254
|
+
contract: found.contract.id,
|
|
255
|
+
basis: ref.suite.basis,
|
|
256
|
+
};
|
|
257
|
+
const repoPath = ctx.repoPath(repo);
|
|
258
|
+
try {
|
|
259
|
+
await git(repoPath, 'cat-file', '-e', `${source.sha}^{commit}`);
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
return {
|
|
263
|
+
input: text,
|
|
264
|
+
outcome: 'inconclusive',
|
|
265
|
+
reason: `исходный SHA ${source.sha} недоступен`,
|
|
266
|
+
method,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
// Checker на исходном SHA — закреплённая редакция: иначе ответ дал бы
|
|
270
|
+
// другой эталон, чем утверждён договором.
|
|
271
|
+
const stale = await staleArtifacts(repoPath, source.sha, artifacts);
|
|
272
|
+
if (stale.length)
|
|
273
|
+
return {
|
|
274
|
+
input: text,
|
|
275
|
+
outcome: 'inconclusive',
|
|
276
|
+
reason: `на исходном SHA checker не совпадает с закреплённым: ${stale.join(', ')}`,
|
|
277
|
+
method,
|
|
278
|
+
};
|
|
279
|
+
const runs = [];
|
|
280
|
+
const durations = [];
|
|
281
|
+
await mkdir(join(ctx.stateDir, 'case-checks'), { recursive: true });
|
|
282
|
+
const dir = await realpath(await mkdtemp(join(ctx.stateDir, 'case-checks', 'case-')));
|
|
283
|
+
await rm(dir, { recursive: true, force: true });
|
|
284
|
+
await git(repoPath, 'worktree', 'add', '--detach', dir, source.sha);
|
|
285
|
+
const io = await realpath(await mkdtemp(join(tmpdir(), 'harness-case-')));
|
|
286
|
+
try {
|
|
287
|
+
const execution = executionEnvironment([ctx.policy.environment, repo.environment], {
|
|
288
|
+
DEVCONTOUR_REPOSITORY_ID: repo.id,
|
|
289
|
+
DEVCONTOUR_TASK_ID: work.id,
|
|
290
|
+
DEVCONTOUR_PHASE: 'case',
|
|
291
|
+
}, ctx.environment ?? process.env);
|
|
292
|
+
const signal = AbortSignal.timeout(gate.timeoutMs * 3 + 120000);
|
|
293
|
+
await withEnvironment(repo.lifecycle ?? ctx.policy.lifecycle, dir, join(io, 'environment'), execution, signal, async () => {
|
|
294
|
+
await runSteps(repo.prepare, dir, join(io, 'prepare'), execution, signal);
|
|
295
|
+
const request = join(io, 'case.json');
|
|
296
|
+
await writeFile(request, JSON.stringify({ property: input.property, input: JSON.parse(text) }));
|
|
297
|
+
const boundary = ctx.boundary();
|
|
298
|
+
for (let i = 0; i < 2; i++) {
|
|
299
|
+
const out = join(io, `result-${i}.json`);
|
|
300
|
+
const started = Date.now();
|
|
301
|
+
const env = { DEVCONTOUR_CASE: request, DEVCONTOUR_CASE_RESULT: out };
|
|
302
|
+
const result = await runCheck({ config: ctx.policy }, {
|
|
303
|
+
argv: gate.command,
|
|
304
|
+
cwd: gate.cwd ? join(dir, gate.cwd) : dir,
|
|
305
|
+
env: {
|
|
306
|
+
...execution.env,
|
|
307
|
+
...env,
|
|
308
|
+
HARNESS_CASE: request,
|
|
309
|
+
HARNESS_CASE_RESULT: out,
|
|
310
|
+
},
|
|
311
|
+
redact: execution.redact,
|
|
312
|
+
timeoutMs: gate.timeoutMs,
|
|
313
|
+
signal,
|
|
314
|
+
write: [dir, io],
|
|
315
|
+
readable: boundary.readable,
|
|
316
|
+
controller: boundary.hidden,
|
|
317
|
+
settingsDir: join(io, `settings-${i}`),
|
|
318
|
+
});
|
|
319
|
+
durations.push(Date.now() - started);
|
|
320
|
+
if (result.timedOut) {
|
|
321
|
+
runs.push({ property: input.property, error: 'timeout' });
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
const parsed = caseResult.safeParse(JSON.parse(await readFile(out, 'utf8').catch(() => 'null')));
|
|
325
|
+
runs.push(parsed.success ? parsed.data : { property: input.property, error: 'нет результата' });
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
return {
|
|
331
|
+
input: text,
|
|
332
|
+
outcome: 'inconclusive',
|
|
333
|
+
reason: 'проверка не выполнилась: ' +
|
|
334
|
+
(error instanceof Error ? error.message : String(error)).slice(0, 300),
|
|
335
|
+
method,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
finally {
|
|
339
|
+
await git(repoPath, 'worktree', 'remove', '--force', dir).catch(() => undefined);
|
|
340
|
+
await rm(io, { recursive: true, force: true }).catch(() => undefined);
|
|
341
|
+
}
|
|
342
|
+
const observed = (r) => ({
|
|
343
|
+
...(r.expected !== undefined ? { expected: canonicalJson(r.expected) } : {}),
|
|
344
|
+
...(r.actual !== undefined ? { actual: canonicalJson(r.actual) } : {}),
|
|
345
|
+
...(r.equal !== undefined ? { equal: r.equal } : {}),
|
|
346
|
+
repeats: runs.length,
|
|
347
|
+
durationsMs: durations,
|
|
348
|
+
});
|
|
349
|
+
const first = runs[0];
|
|
350
|
+
if (!first || first.error)
|
|
351
|
+
return {
|
|
352
|
+
input: text,
|
|
353
|
+
outcome: 'inconclusive',
|
|
354
|
+
reason: first?.error === 'timeout'
|
|
355
|
+
? 'эталон или реализация не уложились в предел gate'
|
|
356
|
+
: `проверка не дала ответа: ${first?.error ?? 'нет результата'}`,
|
|
357
|
+
method,
|
|
358
|
+
observed: { repeats: runs.length, durationsMs: durations },
|
|
359
|
+
};
|
|
360
|
+
if (first.valid === false)
|
|
361
|
+
return { ...reject('вход вне объявленной области свойства', text), method };
|
|
362
|
+
if (first.oracleError)
|
|
363
|
+
return {
|
|
364
|
+
input: text,
|
|
365
|
+
outcome: 'inconclusive',
|
|
366
|
+
reason: `ошибка эталона: ${first.oracleError.slice(0, 300)}`,
|
|
367
|
+
method,
|
|
368
|
+
observed: observed(first),
|
|
369
|
+
};
|
|
370
|
+
if (runs.length < 2 || runs[1].error || canonicalJson(runs[1]) !== canonicalJson(first))
|
|
371
|
+
return {
|
|
372
|
+
input: text,
|
|
373
|
+
outcome: 'inconclusive',
|
|
374
|
+
reason: 'результат не воспроизвёлся при повторе',
|
|
375
|
+
method,
|
|
376
|
+
observed: observed(first),
|
|
377
|
+
};
|
|
378
|
+
const expected = canonicalJson(first.expected);
|
|
379
|
+
if (input.expected != null) {
|
|
380
|
+
let claimed;
|
|
381
|
+
try {
|
|
382
|
+
claimed = canonicalJson(JSON.parse(input.expected));
|
|
383
|
+
}
|
|
384
|
+
catch {
|
|
385
|
+
claimed = undefined;
|
|
386
|
+
}
|
|
387
|
+
if (claimed !== expected)
|
|
388
|
+
return {
|
|
389
|
+
input: text,
|
|
390
|
+
outcome: 'refuted',
|
|
391
|
+
reason: `ожидание источника ${input.expected.slice(0, 200)} опровергнуто эталоном: ${expected.slice(0, 200)}`,
|
|
392
|
+
method,
|
|
393
|
+
observed: observed(first),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (first.equal)
|
|
397
|
+
return {
|
|
398
|
+
input: text,
|
|
399
|
+
outcome: 'refuted',
|
|
400
|
+
reason: 'на исходном SHA реализация совпадает с эталоном: расхождения нет',
|
|
401
|
+
method,
|
|
402
|
+
observed: observed(first),
|
|
403
|
+
};
|
|
404
|
+
return {
|
|
405
|
+
input: text,
|
|
406
|
+
outcome: 'confirmed',
|
|
407
|
+
reason: `эталон дал ${expected.slice(0, 200)}, реализация на ${source.sha.slice(0, 12)} — ${canonicalJson(first.actual).slice(0, 200)}; расхождение воспроизведено дважды`,
|
|
408
|
+
method,
|
|
409
|
+
observed: observed(first),
|
|
410
|
+
};
|
|
411
|
+
}
|