@b00dea/cockpit 0.0.0-stage → 0.1.1
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/CHANGELOG.md +474 -0
- package/LICENSE +21 -0
- package/README.md +25 -2
- package/bin/cockpit.mjs +33 -0
- package/cockpit.schema.json +2644 -0
- package/dist/autonomy.d.ts +45 -0
- package/dist/autonomy.js +173 -0
- package/dist/bounded-read.d.ts +14 -0
- package/dist/bounded-read.js +42 -0
- package/dist/browser/commands.d.ts +117 -0
- package/dist/browser/commands.js +453 -0
- package/dist/browser/package-paths.d.ts +1 -0
- package/dist/browser/package-paths.js +6 -0
- package/dist/browser/proxy.d.ts +22 -0
- package/dist/browser/proxy.js +37 -0
- package/dist/browser/runtime.d.ts +118 -0
- package/dist/browser/runtime.js +186 -0
- package/dist/browser/surfaces.d.ts +141 -0
- package/dist/browser/surfaces.js +561 -0
- package/dist/browser/ui-gate.d.ts +15 -0
- package/dist/browser/ui-gate.js +18 -0
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +5 -0
- package/dist/cli-guard.d.ts +5 -0
- package/dist/cli-guard.js +71 -0
- package/dist/cli-main.d.ts +176 -0
- package/dist/cli-main.js +1584 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +16 -0
- package/dist/command-table.d.ts +121 -0
- package/dist/command-table.js +664 -0
- package/dist/config/load.d.ts +183 -0
- package/dist/config/load.js +367 -0
- package/dist/config/sibling-toml.d.ts +48 -0
- package/dist/config/sibling-toml.js +160 -0
- package/dist/config/surface-tools-toml.d.ts +9 -0
- package/dist/config/surface-tools-toml.js +37 -0
- package/dist/config/unconfirmed-toml.d.ts +9 -0
- package/dist/config/unconfirmed-toml.js +29 -0
- package/dist/config/verified-write.d.ts +43 -0
- package/dist/config/verified-write.js +89 -0
- package/dist/diagnostics.d.ts +26 -0
- package/dist/diagnostics.js +344 -0
- package/dist/docs/check.d.ts +16 -0
- package/dist/docs/check.js +197 -0
- package/dist/docs/decision-record.d.ts +34 -0
- package/dist/docs/decision-record.js +135 -0
- package/dist/docs/decisions.d.ts +16 -0
- package/dist/docs/decisions.js +103 -0
- package/dist/docs/markdown.d.ts +36 -0
- package/dist/docs/markdown.js +88 -0
- package/dist/docs/references.d.ts +32 -0
- package/dist/docs/references.js +148 -0
- package/dist/emit.d.ts +22 -0
- package/dist/emit.js +43 -0
- package/dist/entrypoint-kinds.d.ts +3 -0
- package/dist/entrypoint-kinds.js +11 -0
- package/dist/environment/application/confirm.d.ts +35 -0
- package/dist/environment/application/confirm.js +122 -0
- package/dist/environment/application/destructive.d.ts +6 -0
- package/dist/environment/application/destructive.js +429 -0
- package/dist/environment/application/doctor.d.ts +94 -0
- package/dist/environment/application/doctor.js +247 -0
- package/dist/environment/application/guard.d.ts +49 -0
- package/dist/environment/application/guard.js +677 -0
- package/dist/environment/application/health.d.ts +143 -0
- package/dist/environment/application/health.js +385 -0
- package/dist/environment/application/lifecycle-context.d.ts +235 -0
- package/dist/environment/application/lifecycle-context.js +154 -0
- package/dist/environment/application/lifecycle-state.d.ts +25 -0
- package/dist/environment/application/lifecycle-state.js +202 -0
- package/dist/environment/application/lifecycle.d.ts +18 -0
- package/dist/environment/application/lifecycle.js +737 -0
- package/dist/environment/application/logs.d.ts +45 -0
- package/dist/environment/application/logs.js +127 -0
- package/dist/environment/application/services.d.ts +60 -0
- package/dist/environment/application/services.js +89 -0
- package/dist/environment/application/setup.d.ts +129 -0
- package/dist/environment/application/setup.js +741 -0
- package/dist/environment/application/shared.d.ts +11 -0
- package/dist/environment/application/shared.js +61 -0
- package/dist/environment/application/siblings.d.ts +43 -0
- package/dist/environment/application/siblings.js +269 -0
- package/dist/environment/claims.d.ts +389 -0
- package/dist/environment/claims.js +1701 -0
- package/dist/environment/codex-hook-trust.d.ts +11 -0
- package/dist/environment/codex-hook-trust.js +185 -0
- package/dist/environment/compose-environment.d.ts +1 -0
- package/dist/environment/compose-environment.js +6 -0
- package/dist/environment/compose-ownership.d.ts +21 -0
- package/dist/environment/compose-ownership.js +117 -0
- package/dist/environment/compose-topology.d.ts +125 -0
- package/dist/environment/compose-topology.js +490 -0
- package/dist/environment/contract.d.ts +522 -0
- package/dist/environment/contract.js +794 -0
- package/dist/environment/declaration.d.ts +14 -0
- package/dist/environment/declaration.js +39 -0
- package/dist/environment/destructive-composition.d.ts +29 -0
- package/dist/environment/destructive-composition.js +333 -0
- package/dist/environment/docker-daemon.d.ts +30 -0
- package/dist/environment/docker-daemon.js +78 -0
- package/dist/environment/docker-endpoint.d.ts +10 -0
- package/dist/environment/docker-endpoint.js +19 -0
- package/dist/environment/env-schema-observation.d.ts +46 -0
- package/dist/environment/env-schema-observation.js +241 -0
- package/dist/environment/evidence.d.ts +136 -0
- package/dist/environment/evidence.js +437 -0
- package/dist/environment/executable.d.ts +38 -0
- package/dist/environment/executable.js +190 -0
- package/dist/environment/generated-application/authorities.d.ts +5 -0
- package/dist/environment/generated-application/authorities.js +61 -0
- package/dist/environment/generated-application/bootstrap-skill.d.ts +16 -0
- package/dist/environment/generated-application/bootstrap-skill.js +328 -0
- package/dist/environment/generated-application/check.d.ts +13 -0
- package/dist/environment/generated-application/check.js +191 -0
- package/dist/environment/generated-application/custom-surface.d.ts +26 -0
- package/dist/environment/generated-application/custom-surface.js +289 -0
- package/dist/environment/generated-application/prepare.d.ts +29 -0
- package/dist/environment/generated-application/prepare.js +278 -0
- package/dist/environment/generated-application/publication.d.ts +33 -0
- package/dist/environment/generated-application/publication.js +547 -0
- package/dist/environment/generated-application/shared.d.ts +492 -0
- package/dist/environment/generated-application/shared.js +354 -0
- package/dist/environment/generated-application/surface-observation.d.ts +4 -0
- package/dist/environment/generated-application/surface-observation.js +69 -0
- package/dist/environment/generated-application/surface-outputs.d.ts +71 -0
- package/dist/environment/generated-application/surface-outputs.js +400 -0
- package/dist/environment/generated-application/surfaces.d.ts +3 -0
- package/dist/environment/generated-application/surfaces.js +704 -0
- package/dist/environment/generated-application/sync.d.ts +6 -0
- package/dist/environment/generated-application/sync.js +124 -0
- package/dist/environment/git-observation.d.ts +202 -0
- package/dist/environment/git-observation.js +553 -0
- package/dist/environment/health-composition.d.ts +12 -0
- package/dist/environment/health-composition.js +645 -0
- package/dist/environment/host-observation.d.ts +56 -0
- package/dist/environment/host-observation.js +52 -0
- package/dist/environment/identity.d.ts +48 -0
- package/dist/environment/identity.js +85 -0
- package/dist/environment/init-application.d.ts +333 -0
- package/dist/environment/init-application.js +726 -0
- package/dist/environment/install-policy-observation.d.ts +71 -0
- package/dist/environment/install-policy-observation.js +284 -0
- package/dist/environment/install-policy.d.ts +48 -0
- package/dist/environment/install-policy.js +96 -0
- package/dist/environment/instance.d.ts +33 -0
- package/dist/environment/instance.js +122 -0
- package/dist/environment/instruction-pointer.d.ts +19 -0
- package/dist/environment/instruction-pointer.js +144 -0
- package/dist/environment/lifecycle-composition.d.ts +31 -0
- package/dist/environment/lifecycle-composition.js +742 -0
- package/dist/environment/lifecycle-stage.d.ts +120 -0
- package/dist/environment/lifecycle-stage.js +408 -0
- package/dist/environment/lifecycle.d.ts +75 -0
- package/dist/environment/lifecycle.js +354 -0
- package/dist/environment/logs.d.ts +132 -0
- package/dist/environment/logs.js +519 -0
- package/dist/environment/map-limit.d.ts +2 -0
- package/dist/environment/map-limit.js +13 -0
- package/dist/environment/mcp-observation.d.ts +9 -0
- package/dist/environment/mcp-observation.js +46 -0
- package/dist/environment/observations.d.ts +46 -0
- package/dist/environment/observations.js +346 -0
- package/dist/environment/operator-application.d.ts +79 -0
- package/dist/environment/operator-application.js +197 -0
- package/dist/environment/ports.d.ts +182 -0
- package/dist/environment/ports.js +1205 -0
- package/dist/environment/posture-observation.d.ts +68 -0
- package/dist/environment/posture-observation.js +253 -0
- package/dist/environment/presence.d.ts +4 -0
- package/dist/environment/presence.js +26 -0
- package/dist/environment/read-composition.d.ts +32 -0
- package/dist/environment/read-composition.js +397 -0
- package/dist/environment/readiness-composition.d.ts +57 -0
- package/dist/environment/readiness-composition.js +151 -0
- package/dist/environment/reclaim.d.ts +370 -0
- package/dist/environment/reclaim.js +1940 -0
- package/dist/environment/repository-identity.d.ts +13 -0
- package/dist/environment/repository-identity.js +108 -0
- package/dist/environment/runtime-measurement-record.d.ts +38 -0
- package/dist/environment/runtime-measurement-record.js +111 -0
- package/dist/environment/runtime-measurement.d.ts +27 -0
- package/dist/environment/runtime-measurement.js +224 -0
- package/dist/environment/runtime-probe-residue.d.ts +43 -0
- package/dist/environment/runtime-probe-residue.js +148 -0
- package/dist/environment/runtime-probe.d.ts +72 -0
- package/dist/environment/runtime-probe.js +725 -0
- package/dist/environment/runtime.d.ts +551 -0
- package/dist/environment/runtime.js +2405 -0
- package/dist/environment/seed.d.ts +16 -0
- package/dist/environment/seed.js +43 -0
- package/dist/environment/selftest-application.d.ts +86 -0
- package/dist/environment/selftest-application.js +713 -0
- package/dist/environment/sensitive.d.ts +3 -0
- package/dist/environment/sensitive.js +48 -0
- package/dist/environment/served-checkout-grants.d.ts +53 -0
- package/dist/environment/served-checkout-grants.js +182 -0
- package/dist/environment/setup-observation.d.ts +62 -0
- package/dist/environment/setup-observation.js +1473 -0
- package/dist/environment/setup.d.ts +43 -0
- package/dist/environment/setup.js +176 -0
- package/dist/environment/sibling-checkouts.d.ts +119 -0
- package/dist/environment/sibling-checkouts.js +254 -0
- package/dist/environment/sibling-instructions.d.ts +33 -0
- package/dist/environment/sibling-instructions.js +224 -0
- package/dist/environment/sibling-observation.d.ts +27 -0
- package/dist/environment/sibling-observation.js +45 -0
- package/dist/environment/sibling-pull.d.ts +23 -0
- package/dist/environment/sibling-pull.js +95 -0
- package/dist/environment/sibling-registration.d.ts +45 -0
- package/dist/environment/sibling-registration.js +538 -0
- package/dist/environment/stack-inputs.d.ts +18 -0
- package/dist/environment/stack-inputs.js +210 -0
- package/dist/environment/stack-inventory-prune.d.ts +31 -0
- package/dist/environment/stack-inventory-prune.js +129 -0
- package/dist/environment/stack-inventory-report.d.ts +44 -0
- package/dist/environment/stack-inventory-report.js +78 -0
- package/dist/environment/stack-inventory.d.ts +94 -0
- package/dist/environment/stack-inventory.js +418 -0
- package/dist/environment/state.d.ts +410 -0
- package/dist/environment/state.js +2153 -0
- package/dist/environment/templates.d.ts +140 -0
- package/dist/environment/templates.js +685 -0
- package/dist/environment/tool-registration.d.ts +9 -0
- package/dist/environment/tool-registration.js +92 -0
- package/dist/environment/upgrade-application.d.ts +19 -0
- package/dist/environment/upgrade-application.js +175 -0
- package/dist/environment-exec/execute.d.ts +105 -0
- package/dist/environment-exec/execute.js +1179 -0
- package/dist/environment-exec/libexec-program.d.ts +6 -0
- package/dist/environment-exec/libexec-program.js +22 -0
- package/dist/environment-exec/verified-digest.d.ts +4 -0
- package/dist/environment-exec/verified-digest.js +16 -0
- package/dist/exact-keys.d.ts +7 -0
- package/dist/exact-keys.js +15 -0
- package/dist/fix-catalogue.d.ts +32 -0
- package/dist/fix-catalogue.js +60 -0
- package/dist/freshness.d.ts +29 -0
- package/dist/freshness.js +75 -0
- package/dist/generated-environment/adopt.d.ts +16 -0
- package/dist/generated-environment/adopt.js +271 -0
- package/dist/generated-environment/command.d.ts +47 -0
- package/dist/generated-environment/command.js +75 -0
- package/dist/generated-environment/transaction.d.ts +186 -0
- package/dist/generated-environment/transaction.js +1770 -0
- package/dist/grading/evaluate.d.ts +71 -0
- package/dist/grading/evaluate.js +403 -0
- package/dist/grading/grade.d.ts +33 -0
- package/dist/grading/grade.js +57 -0
- package/dist/guards/answer-confirmation.d.ts +41 -0
- package/dist/guards/answer-confirmation.js +109 -0
- package/dist/guards/evaluator.d.ts +130 -0
- package/dist/guards/evaluator.js +1258 -0
- package/dist/guards/harness-adapter.d.ts +17 -0
- package/dist/guards/harness-adapter.js +57 -0
- package/dist/guards/host.d.ts +8 -0
- package/dist/guards/host.js +10 -0
- package/dist/guards/invocation-parser.d.ts +66 -0
- package/dist/guards/invocation-parser.js +595 -0
- package/dist/guards/local-identity.d.ts +44 -0
- package/dist/guards/local-identity.js +129 -0
- package/dist/guards/policy-change.d.ts +43 -0
- package/dist/guards/policy-change.js +159 -0
- package/dist/guards/policy-pin.d.ts +13 -0
- package/dist/guards/policy-pin.js +177 -0
- package/dist/guards/protected-locations.d.ts +62 -0
- package/dist/guards/protected-locations.js +461 -0
- package/dist/guards/read-only.d.ts +37 -0
- package/dist/guards/read-only.js +315 -0
- package/dist/init/command.d.ts +41 -0
- package/dist/init/command.js +330 -0
- package/dist/init/derive.d.ts +53 -0
- package/dist/init/derive.js +819 -0
- package/dist/init/draft.d.ts +140 -0
- package/dist/init/draft.js +1228 -0
- package/dist/init/render.d.ts +47 -0
- package/dist/init/render.js +104 -0
- package/dist/init/scaffold.d.ts +53 -0
- package/dist/init/scaffold.js +121 -0
- package/dist/invocation.d.ts +13 -0
- package/dist/invocation.js +20 -0
- package/dist/package-metadata.d.ts +9 -0
- package/dist/package-metadata.js +19 -0
- package/dist/readiness.d.ts +53 -0
- package/dist/readiness.js +88 -0
- package/dist/references/named.d.ts +24 -0
- package/dist/references/named.js +27 -0
- package/dist/refusal.d.ts +15 -0
- package/dist/refusal.js +7 -0
- package/dist/registry-derivation.d.ts +31 -0
- package/dist/registry-derivation.js +32 -0
- package/dist/registry-resolve/command.d.ts +47 -0
- package/dist/registry-resolve/command.js +226 -0
- package/dist/registry-resolve/registry.d.ts +68 -0
- package/dist/registry-resolve/registry.js +292 -0
- package/dist/registry-resolve/render-shell.d.ts +18 -0
- package/dist/registry-resolve/render-shell.js +36 -0
- package/dist/repair.d.ts +29 -0
- package/dist/repair.js +8 -0
- package/dist/repository-path.d.ts +9 -0
- package/dist/repository-path.js +16 -0
- package/dist/schema-json.d.ts +1 -0
- package/dist/schema-json.js +177 -0
- package/dist/schema.d.ts +1289 -0
- package/dist/schema.js +985 -0
- package/dist/setup/command.d.ts +4 -0
- package/dist/setup/command.js +4 -0
- package/dist/sha256.d.ts +1 -0
- package/dist/sha256.js +4 -0
- package/dist/surfaces/adoption.d.ts +41 -0
- package/dist/surfaces/adoption.js +217 -0
- package/dist/surfaces/consumer-instructions.d.ts +20 -0
- package/dist/surfaces/consumer-instructions.js +43 -0
- package/dist/surfaces/contract.d.ts +42 -0
- package/dist/surfaces/contract.js +169 -0
- package/dist/surfaces/core-declarations.d.ts +24 -0
- package/dist/surfaces/core-declarations.js +79 -0
- package/dist/surfaces/development-markers.d.ts +1 -0
- package/dist/surfaces/development-markers.js +18 -0
- package/dist/surfaces/generator-adapter.d.ts +71 -0
- package/dist/surfaces/generator-adapter.js +809 -0
- package/dist/surfaces/harness-registry.d.ts +260 -0
- package/dist/surfaces/harness-registry.js +146 -0
- package/dist/surfaces/mcp.d.ts +145 -0
- package/dist/surfaces/mcp.js +349 -0
- package/dist/surfaces/merge.d.ts +30 -0
- package/dist/surfaces/merge.js +105 -0
- package/dist/surfaces/ownership.d.ts +190 -0
- package/dist/surfaces/ownership.js +795 -0
- package/dist/surfaces/projection.d.ts +136 -0
- package/dist/surfaces/projection.js +716 -0
- package/dist/surfaces/render.d.ts +79 -0
- package/dist/surfaces/render.js +636 -0
- package/dist/surfaces/restrictions.d.ts +130 -0
- package/dist/surfaces/restrictions.js +447 -0
- package/dist/surfaces/selftest.d.ts +162 -0
- package/dist/surfaces/selftest.js +393 -0
- package/dist/surfaces/skill-contract.d.ts +85 -0
- package/dist/surfaces/skill-contract.js +449 -0
- package/dist/surfaces/sync.d.ts +57 -0
- package/dist/surfaces/sync.js +164 -0
- package/dist/team-metadata.d.ts +41 -0
- package/dist/team-metadata.js +513 -0
- package/dist/testing/gates.d.ts +91 -0
- package/dist/testing/gates.js +182 -0
- package/dist/testing/hooks.d.ts +59 -0
- package/dist/testing/hooks.js +139 -0
- package/dist/testing/test-protection.d.ts +34 -0
- package/dist/testing/test-protection.js +137 -0
- package/dist/testing/verdict.d.ts +29 -0
- package/dist/testing/verdict.js +62 -0
- package/dist/testing/weakening.d.ts +15 -0
- package/dist/testing/weakening.js +30 -0
- package/dist/work-orders/application-closure.d.ts +7 -0
- package/dist/work-orders/application-closure.js +76 -0
- package/dist/work-orders/application-context.d.ts +162 -0
- package/dist/work-orders/application-context.js +137 -0
- package/dist/work-orders/application-decisions.d.ts +27 -0
- package/dist/work-orders/application-decisions.js +121 -0
- package/dist/work-orders/application-grading.d.ts +11 -0
- package/dist/work-orders/application-grading.js +212 -0
- package/dist/work-orders/application.d.ts +6 -0
- package/dist/work-orders/application.js +223 -0
- package/dist/work-orders/composition.d.ts +101 -0
- package/dist/work-orders/composition.js +1202 -0
- package/dist/work-orders/contract.d.ts +77 -0
- package/dist/work-orders/contract.js +140 -0
- package/dist/work-orders/decisions.d.ts +23 -0
- package/dist/work-orders/decisions.js +55 -0
- package/dist/work-orders/store.d.ts +207 -0
- package/dist/work-orders/store.js +291 -0
- package/docs/README.md +68 -0
- package/docs/architecture.md +136 -0
- package/docs/capability-matrix.md +58 -0
- package/docs/cli-reference.md +807 -0
- package/docs/command-list.md +62 -0
- package/docs/compose-patterns.md +99 -0
- package/docs/concepts.md +173 -0
- package/docs/configuration-essentials.md +177 -0
- package/docs/configuration-reference.md +636 -0
- package/docs/docs-check.md +76 -0
- package/docs/environment.md +948 -0
- package/docs/extending.md +43 -0
- package/docs/faq.md +113 -0
- package/docs/getting-started.md +692 -0
- package/docs/harness-surfaces.md +500 -0
- package/docs/joining-a-team.md +76 -0
- package/docs/release-policy.md +49 -0
- package/docs/safety-and-autonomy.md +463 -0
- package/docs/troubleshooting.md +382 -0
- package/docs/tutorial.md +171 -0
- package/docs/work-and-browser.md +420 -0
- package/libexec/browser-operation.cjs +270 -0
- package/libexec/fence-marker.cjs +101 -0
- package/libexec/fence-marker.d.cts +24 -0
- package/libexec/fenced-group-runner.cjs +77 -0
- package/libexec/fenced-supervisor.cjs +137 -0
- package/libexec/generated-filesystem.cjs +731 -0
- package/libexec/install-filesystem.cjs +225 -0
- package/libexec/rulesync-generator.NOTICE.md +1112 -0
- package/libexec/rulesync-generator.cjs +743 -0
- package/libexec/surface-generator-worker.cjs +266 -0
- package/libexec/surface-output-reader.cjs +99 -0
- package/libexec/surface-scratch-cleanup.cjs +14 -0
- package/libexec/validate-harness-evidence.d.mts +183 -0
- package/libexec/validate-harness-evidence.mjs +1312 -0
- package/package.json +74 -4
- package/surfaces/README.md +33 -0
- package/surfaces/agents/evaluator.agent.md +76 -0
- package/surfaces/guards/bash-guard.sh +48 -0
- package/surfaces/guards/claude.adapter.json +25 -0
- package/surfaces/guards/codex.adapter.json +24 -0
- package/surfaces/guards/opencode.adapter.json +25 -0
- package/surfaces/references/browser.md +63 -0
- package/surfaces/references/test-data-and-personas.md +84 -0
- package/surfaces/references/testing-tiers.md +11 -0
- package/surfaces/skills/cockpit-browser.skill.md +59 -0
- package/surfaces/skills/cockpit-explain.skill.md +32 -0
- package/surfaces/skills/cockpit-fix.skill.md +37 -0
- package/surfaces/skills/cockpit-setup.skill.md +39 -0
- package/surfaces/skills/cockpit-work.skill.md +34 -0
- package/surfaces/skills/collection.json +5 -0
- package/surfaces/skills/procedures/explain.md +66 -0
- package/surfaces/skills/procedures/fix.md +95 -0
- package/surfaces/skills/procedures/setup.md +452 -0
- package/surfaces/skills/procedures/work.md +134 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { compareEnvironmentSnapshot } from '../environment/evidence.js';
|
|
2
|
+
import { planSiblingProjection } from '../surfaces/projection.js';
|
|
3
|
+
import { GUARD_SURFACE_TARGETS } from '../surfaces/harness-registry.js';
|
|
4
|
+
import { TESTING_STAGE_TIMEOUT_MAX_SECONDS } from '../schema.js';
|
|
5
|
+
import { classifyRun } from './verdict.js';
|
|
6
|
+
import { RefusalError, errorCause } from '../environment/contract.js';
|
|
7
|
+
export const GATE_SELECTOR_OPTION = '--select';
|
|
8
|
+
const SELECTOR_KINDS = Object.freeze(['spec', 'file', 'test', 'route']);
|
|
9
|
+
const SELECTOR_VALUE = '{value}';
|
|
10
|
+
const tiers = Object.freeze([
|
|
11
|
+
Object.freeze({ id: 'none', title: 'None', changes: 'Comments or documentation only, when no behavior changes', gate: 'No gate required' }),
|
|
12
|
+
Object.freeze({ id: 'targeted', title: 'Targeted', changes: 'Isolated behavior', gate: 'Select the narrowest proving product gate' }),
|
|
13
|
+
Object.freeze({ id: 'full', title: 'Full', changes: 'Shared or cross-system behavior', gate: 'Run the complete declared product gate' }),
|
|
14
|
+
]);
|
|
15
|
+
const issuedRegistries = new WeakSet();
|
|
16
|
+
export const TESTING_GUIDE_REGISTRY = Object.freeze({ kind: 'unbound', tiers, stages: Object.freeze([]), gates: Object.freeze([]) });
|
|
17
|
+
issuedRegistries.add(TESTING_GUIDE_REGISTRY);
|
|
18
|
+
const NAME = /^[a-z][a-z0-9-]{0,63}$/;
|
|
19
|
+
const text = (value) => typeof value === 'string' && value.length > 0 && value.length <= 4096 && !/[\x00-\x1f\x7f]/.test(value);
|
|
20
|
+
export function createGateRegistry(input) {
|
|
21
|
+
if (!Array.isArray(input.stages) || !Array.isArray(input.gates) || !input.stages.length || !input.gates.length
|
|
22
|
+
|| input.stages.length > 128 || input.gates.length > 128)
|
|
23
|
+
throw new RefusalError('gate registry requires bounded nonempty stages and gates');
|
|
24
|
+
const stageIds = new Set(input.stages.map(stage => stage.id));
|
|
25
|
+
if (stageIds.size !== input.stages.length || new Set(input.gates.map(gate => gate.id)).size !== input.gates.length
|
|
26
|
+
|| input.stages.some(stage => !NAME.test(stage.id) || !Array.isArray(stage.command) || !stage.command.length
|
|
27
|
+
|| stage.command.length > 128 || !stage.command.every(text) || typeof stage.requiresStack !== 'boolean'
|
|
28
|
+
|| !Number.isSafeInteger(stage.timeoutSeconds) || stage.timeoutSeconds < 1 || stage.timeoutSeconds > TESTING_STAGE_TIMEOUT_MAX_SECONDS
|
|
29
|
+
|| stage.select !== undefined && (typeof stage.select !== 'object' || stage.select === null
|
|
30
|
+
|| Object.entries(stage.select).some(([kind, argv]) => !SELECTOR_KINDS.includes(kind) || !Array.isArray(argv)
|
|
31
|
+
|| !argv.length || argv.length > 32 || !argv.every(text) || !argv.some(value => value.includes(SELECTOR_VALUE)))))
|
|
32
|
+
|| input.gates.some(gate => !NAME.test(gate.id) || !['targeted', 'full'].includes(gate.tier) || !text(gate.changes)
|
|
33
|
+
|| !Array.isArray(gate.stages) || !gate.stages.length || new Set(gate.stages).size !== gate.stages.length
|
|
34
|
+
|| gate.stages.some((id) => !stageIds.has(id))))
|
|
35
|
+
throw new RefusalError('gate registry contains invalid or duplicate declarations');
|
|
36
|
+
const registry = { kind: 'bound', tiers,
|
|
37
|
+
stages: input.stages.map(stage => ({ ...stage, command: [...stage.command],
|
|
38
|
+
...(stage.select === undefined ? {} : { select: Object.fromEntries(Object.entries(stage.select)
|
|
39
|
+
.map(([kind, argv]) => [kind, [...argv]])) }) })),
|
|
40
|
+
gates: input.gates.map(gate => ({ ...gate, stages: [...gate.stages] })), };
|
|
41
|
+
issuedRegistries.add(registry);
|
|
42
|
+
return registry;
|
|
43
|
+
}
|
|
44
|
+
export async function runGate(registry, request, deps) {
|
|
45
|
+
if (!issuedRegistries.has(registry) || registry.kind !== 'bound')
|
|
46
|
+
throw new RefusalError('gate execution requires an issued product-bound registry');
|
|
47
|
+
const gate = registry.gates.find(gate => gate.id === request.gate);
|
|
48
|
+
const requested = request.stages ?? gate?.stages ?? [];
|
|
49
|
+
if (!gate || !requested.length || requested.some(id => !gate.stages.includes(id)))
|
|
50
|
+
throw new RefusalError('gate selection is empty or unknown');
|
|
51
|
+
const select = request.select === undefined ? null : { ...request.select };
|
|
52
|
+
if (select && (!SELECTOR_KINDS.includes(select.kind) || !text(select.value)))
|
|
53
|
+
throw new RefusalError('gate selection has an invalid selector');
|
|
54
|
+
const selected = gate.stages.filter(id => requested.includes(id));
|
|
55
|
+
const observations = [];
|
|
56
|
+
let before;
|
|
57
|
+
let stability = 'stable';
|
|
58
|
+
const stabilityReasons = [];
|
|
59
|
+
// Without an available stack every stage needing it is reported unavailable and never runs, so
|
|
60
|
+
// there is no runtime to hold stable; capturing one would only bury that reason under its own.
|
|
61
|
+
const needsRuntime = request.stack?.state === 'available' && selected.some(id => registry.stages.find(stage => stage.id === id).requiresStack);
|
|
62
|
+
let runtimeBefore;
|
|
63
|
+
let runtimeFresh = true;
|
|
64
|
+
const capture = async () => {
|
|
65
|
+
try {
|
|
66
|
+
if (!deps.runtime)
|
|
67
|
+
throw new RefusalError('runtime stability capture not supplied');
|
|
68
|
+
const startedAt = Date.now();
|
|
69
|
+
const evidence = await deps.runtime.capture();
|
|
70
|
+
if (evidence.state === 'available' && (Date.parse(evidence.captureStartedAt) < startedAt || Date.parse(evidence.observedAt) > Date.now()))
|
|
71
|
+
runtimeFresh = false;
|
|
72
|
+
return evidence;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
stabilityReasons.push(`runtime stability unavailable: ${errorCause(error)}`);
|
|
76
|
+
stability = 'unavailable';
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
try {
|
|
81
|
+
before = await deps.subject();
|
|
82
|
+
if (!/^[a-f0-9]{64}$/.test(before))
|
|
83
|
+
throw new RefusalError('subject digest must be SHA-256');
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
stability = 'unavailable';
|
|
87
|
+
stabilityReasons.push(`subject before: ${errorCause(error)}`);
|
|
88
|
+
}
|
|
89
|
+
if (needsRuntime)
|
|
90
|
+
runtimeBefore = await capture();
|
|
91
|
+
for (const id of selected) {
|
|
92
|
+
const stage = registry.stages.find(stage => stage.id === id);
|
|
93
|
+
let result;
|
|
94
|
+
const selection = select ? stage.select?.[select.kind] : undefined;
|
|
95
|
+
if (stage.requiresStack && request.stack?.state !== 'available') {
|
|
96
|
+
result = { kind: 'unavailable', reason: `stack unavailable: ${request.stack?.state === 'unavailable' ? request.stack.reason : 'no current stack observation'}` };
|
|
97
|
+
}
|
|
98
|
+
else if (select && !selection) {
|
|
99
|
+
// Running the whole stage would report a selection that never happened.
|
|
100
|
+
result = { kind: 'unavailable', reason: `stage ${id} declares no ${select.kind} selection; add testing.stages[].select.${select.kind} with the tool's own arguments` };
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const command = [...stage.command, ...(selection ?? []).map(value => value.replaceAll(SELECTOR_VALUE, select.value))];
|
|
104
|
+
try {
|
|
105
|
+
result = await deps.runProductTool({ gate: gate.id, stage: id, command, timeoutSeconds: stage.timeoutSeconds, select });
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
result = { kind: 'unavailable', reason: `product tooling could not run: ${errorCause(error)}` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
observations.push({ ...result, id });
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const after = await deps.subject();
|
|
115
|
+
if (!/^[a-f0-9]{64}$/.test(after))
|
|
116
|
+
throw new RefusalError('subject digest must be SHA-256');
|
|
117
|
+
if (before !== undefined && before !== after && stability !== 'unavailable')
|
|
118
|
+
stability = 'void';
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
stability = 'unavailable';
|
|
122
|
+
stabilityReasons.push(`subject after: ${errorCause(error)}`);
|
|
123
|
+
}
|
|
124
|
+
if (needsRuntime) {
|
|
125
|
+
const runtimeAfter = await capture();
|
|
126
|
+
if (deps.runtime && runtimeBefore && runtimeAfter) {
|
|
127
|
+
const comparison = compareEnvironmentSnapshot(deps.runtime.snapshot, runtimeBefore, runtimeAfter);
|
|
128
|
+
stabilityReasons.push(...comparison.diagnostics.map(row => row.message));
|
|
129
|
+
if (comparison.verdict === 'unavailable')
|
|
130
|
+
stability = 'unavailable';
|
|
131
|
+
else if (stability !== 'unavailable' && comparison.verdict !== 'stable')
|
|
132
|
+
stability = comparison.verdict;
|
|
133
|
+
if (!runtimeFresh && stability !== 'unavailable') {
|
|
134
|
+
stability = 'stale';
|
|
135
|
+
stabilityReasons.push('runtime capture predates the gate or is in the future');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else
|
|
139
|
+
stability = 'unavailable';
|
|
140
|
+
}
|
|
141
|
+
const result = classifyRun({ declared: gate.stages, selected, observations,
|
|
142
|
+
scope: request.staged ? 'staged' : select ? 'subset' : 'full', stability });
|
|
143
|
+
return { ...result, observations, reasons: [...result.reasons, ...stabilityReasons] };
|
|
144
|
+
}
|
|
145
|
+
// Without a declared gate there is nothing for test-run to run, and a tier table would send agents
|
|
146
|
+
// looking for a gate that does not exist; so the guide says what to do instead and never promises PASS.
|
|
147
|
+
const NO_GATE_GUIDE = '# Testing tiers\n\n'
|
|
148
|
+
+ 'This cockpit declares no product test gate, so no change can be graded PASS.\n\n'
|
|
149
|
+
+ 'Before changing behavior, add a characterization test at the nearest public boundary in the product\'s existing test runner and run it with that runner. If the product has no test runner, propose one to a person; do not add a test framework unasked. Comments or documentation changes that leave behavior unchanged need no test.\n\n'
|
|
150
|
+
+ 'Never report work as verified or passing: report which tests you ran and how they exited, and that no gate graded them.\n\n'
|
|
151
|
+
+ 'Ask a person to declare `[testing]` stages and gates in cockpit.toml, as the *Choose the proving test tier* section of `./node_modules/@b00dea/cockpit/docs/work-and-browser.md` describes.\n';
|
|
152
|
+
export function renderTierGuide(registry, projection) {
|
|
153
|
+
if (!issuedRegistries.has(registry))
|
|
154
|
+
throw new RefusalError('testing guide requires an issued registry');
|
|
155
|
+
const cell = (value) => value.replaceAll('|', '\\|').replaceAll('`', '');
|
|
156
|
+
const rows = registry.tiers.map(tier => {
|
|
157
|
+
const gates = registry.gates.filter(gate => gate.tier === tier.id);
|
|
158
|
+
return `| ${tier.title} | ${gates.map(gate => cell(gate.changes)).join('; ') || tier.changes} | ${gates.map(gate => `\`${gate.id}\` (${gate.stages.join(', ')})`).join('; ') || tier.gate} |`;
|
|
159
|
+
});
|
|
160
|
+
const bytes = registry.kind === 'unbound' ? NO_GATE_GUIDE : `# Testing tiers\n\nChoose the narrowest product-owned gate that proves the change.\n\n`
|
|
161
|
+
+ '| Tier | Change | Gate and ordered stages |\n| --- | --- | --- |\n' + rows.join('\n') + '\n\n'
|
|
162
|
+
+ 'Extend an existing behavior test at the nearest public boundary when it can prove the change; add a test only when no existing test owns that behavior. Each test must fail when that behavior regresses. Do not duplicate proof already supplied by a stronger test.\n\n'
|
|
163
|
+
+ `Run a gate with \`npx --no cockpit test-run --config cockpit.toml --gate <id>\`. Narrow it with \`npx --no cockpit test-run --config cockpit.toml --gate <id> ${GATE_SELECTOR_OPTION} <kind>:<value>\`, where kind is spec, file, test, or route. Each stage receives the value through its declared \`select\` arguments in its own tool's spelling; a stage that declares none for that kind is reported unavailable rather than run unselected.\n\n`
|
|
164
|
+
+ 'Use the targeted gate while iterating. When a stage runs the affected or changed-since selection of the product build or test tool, let that tool choose the tests instead of narrowing by hand. If the declared tier is Full, run the complete gate once after implementation and review stabilize, before handing back. A targeted tier ends with its declared targeted gate.\n\n'
|
|
165
|
+
+ 'Only the complete declared gate can PASS. Subsets and staged selftest are PARTIAL; unavailable checks are DEGRADED; changed subjects are VOID. Executed failures remain FAIL.\n\n'
|
|
166
|
+
+ 'All selected stages run. Stack-dependent stages name an unavailable stack. Generated hooks run no tests; call correctness gates explicitly or through CI.\n';
|
|
167
|
+
const lines = bytes.trimEnd().split('\n').length;
|
|
168
|
+
const guidePath = 'packages/cockpit/surfaces/references/testing-tiers.md';
|
|
169
|
+
const checked = planSiblingProjection({ ...projection, standingSources: [...projection.standingSources.filter(source => source.path !== guidePath),
|
|
170
|
+
...GUARD_SURFACE_TARGETS.filter(target => projection.targets?.includes(target) ?? true).map(target => ({ target,
|
|
171
|
+
path: guidePath, content: bytes, scope: 'always' }))] });
|
|
172
|
+
if (checked.kind === 'refusal')
|
|
173
|
+
return { kind: 'refusal', reason: checked.diagnostic.message };
|
|
174
|
+
const exceeded = checked.costs.find(cost => cost.status === 'exceeded');
|
|
175
|
+
if (exceeded?.status === 'exceeded')
|
|
176
|
+
return { kind: 'refusal',
|
|
177
|
+
reason: `the instructions ${exceeded.target} loads at start are ${exceeded.value} lines, over cockpit.guide_cap ${exceeded.cap}`,
|
|
178
|
+
exceeded: { target: exceeded.target, value: exceeded.value, cap: exceeded.cap } };
|
|
179
|
+
if (checked.costs.some(cost => cost.status !== 'within_cap'))
|
|
180
|
+
return { kind: 'refusal', reason: 'combined instruction cost is not characterized for every harness' };
|
|
181
|
+
return { kind: 'complete', bytes, lines, costs: checked.costs };
|
|
182
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ExecutionStep } from '../environment/contract.js';
|
|
2
|
+
import { type NamedPathInput } from '../references/named.js';
|
|
3
|
+
import { type SurfaceDesiredEntry } from '../surfaces/ownership.js';
|
|
4
|
+
export type NativeHookEntry = Extract<SurfaceDesiredEntry, {
|
|
5
|
+
kind: 'file';
|
|
6
|
+
}>;
|
|
7
|
+
export interface NativeHookPolicy extends NamedPathInput {
|
|
8
|
+
readonly bytes: string;
|
|
9
|
+
}
|
|
10
|
+
export type NativeHookMeasurement = ExecutionStep;
|
|
11
|
+
export interface NativeHookBudget {
|
|
12
|
+
readonly prepareMeasurement: (entry: NativeHookEntry) => Promise<NativeHookMeasurement>;
|
|
13
|
+
readonly policy: NativeHookPolicy;
|
|
14
|
+
}
|
|
15
|
+
export type NativeHookInput = Readonly<{
|
|
16
|
+
cockpitId: string;
|
|
17
|
+
harnesses: readonly string[];
|
|
18
|
+
}> & (Readonly<{
|
|
19
|
+
install: 'inspect';
|
|
20
|
+
budget?: NativeHookBudget;
|
|
21
|
+
}> | Readonly<{
|
|
22
|
+
install: 'explicit-native';
|
|
23
|
+
budget: NativeHookBudget;
|
|
24
|
+
}>);
|
|
25
|
+
type HookRefusal = Readonly<{
|
|
26
|
+
kind: 'refusal';
|
|
27
|
+
reason: string;
|
|
28
|
+
}>;
|
|
29
|
+
export type HookBudgetResult = Readonly<{
|
|
30
|
+
kind: 'within-budget';
|
|
31
|
+
scope: 'supplied-execution-environment';
|
|
32
|
+
executionSha256: string;
|
|
33
|
+
limitMs: number;
|
|
34
|
+
measuredMs: number;
|
|
35
|
+
}> | Readonly<{
|
|
36
|
+
kind: 'ci-required';
|
|
37
|
+
reason: string;
|
|
38
|
+
}> | HookRefusal;
|
|
39
|
+
export type NativeHookProjection = Readonly<{
|
|
40
|
+
kind: 'complete';
|
|
41
|
+
entries: readonly NativeHookEntry[];
|
|
42
|
+
cost: Extract<HookBudgetResult, {
|
|
43
|
+
kind: 'within-budget';
|
|
44
|
+
}>;
|
|
45
|
+
activation: Readonly<{
|
|
46
|
+
kind: 'explicit-native-install';
|
|
47
|
+
hooksPath: '.githooks';
|
|
48
|
+
mode: 493;
|
|
49
|
+
}>;
|
|
50
|
+
}> | Readonly<{
|
|
51
|
+
kind: 'not-installed';
|
|
52
|
+
reason: string;
|
|
53
|
+
}> | Exclude<HookBudgetResult, {
|
|
54
|
+
kind: 'within-budget';
|
|
55
|
+
}>;
|
|
56
|
+
export declare const NATIVE_HOOK_COST_SCRIPT = "{\n count=0\n while [ \"$count\" -lt 128 ]; do\n printf 'refs/heads/cost %s refs/heads/cost %s\\n' \"$2\" \"$3\"\n count=$((count + 1))\n done\n} | /bin/sh \"$1\"";
|
|
57
|
+
export declare function checkNativeHookBudget(entry: NativeHookEntry, measurement: NativeHookMeasurement, policy: NativeHookPolicy): Promise<HookBudgetResult>;
|
|
58
|
+
export declare function projectNativeHooks(input: NativeHookInput): Promise<NativeHookProjection>;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { sha256Hex } from '../sha256.js';
|
|
2
|
+
import { authenticatedExecutionOutcome, executeProbe } from '../environment-exec/execute.js';
|
|
3
|
+
import { resolveNamedPath } from '../references/named.js';
|
|
4
|
+
import { bindSurfaceOwnershipIdentity } from '../surfaces/ownership.js';
|
|
5
|
+
import { resolveExecutablePath } from '../environment/executable.js';
|
|
6
|
+
import { errorCause } from '../environment/contract.js';
|
|
7
|
+
export const NATIVE_HOOK_COST_SCRIPT = `{
|
|
8
|
+
count=0
|
|
9
|
+
while [ "$count" -lt 128 ]; do
|
|
10
|
+
printf 'refs/heads/cost %s refs/heads/cost %s\\n' "$2" "$3"
|
|
11
|
+
count=$((count + 1))
|
|
12
|
+
done
|
|
13
|
+
} | /bin/sh "$1"`;
|
|
14
|
+
const refusal = (reason) => ({ kind: 'refusal', reason });
|
|
15
|
+
function policyLimit(policy) {
|
|
16
|
+
const reference = resolveNamedPath(policy);
|
|
17
|
+
if (reference.kind !== 'resolved')
|
|
18
|
+
return refusal(`native hook budget policy is unavailable: ${policy.path}`);
|
|
19
|
+
const owner = policy.manifest.entries.find(entry => entry.path === policy.path);
|
|
20
|
+
if (!owner || owner.kind !== 'file' || typeof policy.bytes !== 'string' || Buffer.byteLength(policy.bytes) > 64
|
|
21
|
+
|| sha256Hex(policy.bytes) !== owner.sha256)
|
|
22
|
+
return refusal('native hook budget must bind exact policy bytes to the ownership manifest');
|
|
23
|
+
const marker = /^native-hook-ms=([1-9][0-9]*)\n$/.exec(policy.bytes);
|
|
24
|
+
const limitMs = marker ? Number(marker[1]) : Number.NaN;
|
|
25
|
+
return Number.isSafeInteger(limitMs) ? limitMs : refusal('native hook policy must declare one positive native-hook-ms cap');
|
|
26
|
+
}
|
|
27
|
+
export async function checkNativeHookBudget(entry, measurement, policy) {
|
|
28
|
+
const cap = policyLimit(policy);
|
|
29
|
+
if (typeof cap !== 'number')
|
|
30
|
+
return cap;
|
|
31
|
+
const resolvedShell = await resolveExecutablePath('/bin/sh');
|
|
32
|
+
if (resolvedShell.state !== 'available')
|
|
33
|
+
return refusal('native hook requires an authenticated system shell');
|
|
34
|
+
const file = measurement?.consumedFiles?.find(file => !file.executable && file.sha256 === entry.sha256
|
|
35
|
+
&& measurement.argv.length === 7 && measurement.argv[4] === file.path && file.argvIndexes.length === 1 && file.argvIndexes[0] === 4);
|
|
36
|
+
const argv = measurement?.argv;
|
|
37
|
+
const oid = (value) => /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(value) && /[1-9a-f]/.test(value);
|
|
38
|
+
const shell = measurement?.consumedFiles?.find(file => file.executable && file.path === resolvedShell.value.canonicalPath
|
|
39
|
+
&& file.sha256 === resolvedShell.value.sha256 && file.executionMode === resolvedShell.value.executionMode
|
|
40
|
+
&& file.argvIndexes.length === 1 && file.argvIndexes[0] === 0);
|
|
41
|
+
const workload = shell && argv && argv[0] === resolvedShell.value.canonicalPath && argv[1] === '-c' && argv[2] === NATIVE_HOOK_COST_SCRIPT
|
|
42
|
+
&& argv[3] === 'native-hook-cost' && oid(argv[5]) && oid(argv[6]) && argv[5].length === argv[6].length && argv[5] !== argv[6];
|
|
43
|
+
const marker = /^# cockpit:budget name=native-hook-ms limit-ms=([1-9][0-9]*)$/m.exec(entry.bytes);
|
|
44
|
+
const limitMs = marker ? Number(marker[1]) : Number.NaN;
|
|
45
|
+
if (!file || !workload || entry.writer !== 'cockpit' || sha256Hex(entry.bytes) !== entry.sha256 || limitMs !== cap) {
|
|
46
|
+
return refusal('native hook requires intact bytes, an authenticated policy cap and the exact 128-update ancestry workload');
|
|
47
|
+
}
|
|
48
|
+
const executionSha256 = sha256Hex(JSON.stringify(measurement));
|
|
49
|
+
const started = performance.now();
|
|
50
|
+
const outcome = authenticatedExecutionOutcome(await executeProbe(measurement), measurement);
|
|
51
|
+
const measuredMs = Math.ceil(performance.now() - started);
|
|
52
|
+
if (!outcome || outcome.kind !== 'complete' || outcome.execution.exitCode !== 0) {
|
|
53
|
+
return refusal(`native hook cost execution failed: ${outcome?.kind === 'refusal' ? outcome.code : 'missing authenticated successful receipt'}`);
|
|
54
|
+
}
|
|
55
|
+
return measuredMs > limitMs
|
|
56
|
+
? { kind: 'ci-required', reason: `native hook cost ${measuredMs}ms exceeds native-hook-ms=${limitMs}; move this check to CI` }
|
|
57
|
+
: { kind: 'within-budget', scope: 'supplied-execution-environment', executionSha256, limitMs, measuredMs };
|
|
58
|
+
}
|
|
59
|
+
function nativeScript(cockpitId, limitMs) {
|
|
60
|
+
return `#!/bin/sh
|
|
61
|
+
# cockpit:writer ${cockpitId}
|
|
62
|
+
# cockpit:budget name=native-hook-ms limit-ms=${limitMs}
|
|
63
|
+
count=0
|
|
64
|
+
while IFS=' ' read -r local_ref local_oid remote_ref remote_oid extra || [ -n "$local_ref$local_oid$remote_ref$remote_oid$extra" ]; do
|
|
65
|
+
count=$((count + 1))
|
|
66
|
+
if [ "$count" -gt 128 ]; then
|
|
67
|
+
echo 'native push guard: more than 128 ref updates; review and split the push' >&2
|
|
68
|
+
exit 2
|
|
69
|
+
fi
|
|
70
|
+
case "$local_ref:$remote_ref:$extra" in
|
|
71
|
+
refs/*:refs/*:) ;;
|
|
72
|
+
*) echo 'native push guard: malformed ref update' >&2; exit 2 ;;
|
|
73
|
+
esac
|
|
74
|
+
for oid in "$local_oid" "$remote_oid"; do
|
|
75
|
+
case "$oid" in
|
|
76
|
+
''|*[!0-9a-f]*) echo 'native push guard: malformed object identity' >&2; exit 2 ;;
|
|
77
|
+
esac
|
|
78
|
+
if [ "\${#oid}" -ne 40 ] && [ "\${#oid}" -ne 64 ]; then
|
|
79
|
+
echo 'native push guard: malformed object identity length' >&2
|
|
80
|
+
exit 2
|
|
81
|
+
fi
|
|
82
|
+
done
|
|
83
|
+
if [ "\${#local_oid}" -ne "\${#remote_oid}" ]; then
|
|
84
|
+
echo 'native push guard: object identity formats differ' >&2
|
|
85
|
+
exit 2
|
|
86
|
+
fi
|
|
87
|
+
case "$local_oid" in
|
|
88
|
+
*[!0]*) ;;
|
|
89
|
+
*) echo 'native push guard: branch or tag deletion requires explicit review' >&2; exit 2 ;;
|
|
90
|
+
esac
|
|
91
|
+
case "$remote_oid" in
|
|
92
|
+
*[!0]*) ;;
|
|
93
|
+
*) continue ;;
|
|
94
|
+
esac
|
|
95
|
+
git merge-base --is-ancestor "$remote_oid" "$local_oid"
|
|
96
|
+
status=$?
|
|
97
|
+
case "$status" in
|
|
98
|
+
0) ;;
|
|
99
|
+
1) echo 'native push guard: non-fast-forward update requires explicit review' >&2; exit 2 ;;
|
|
100
|
+
*) echo 'native push guard: ancestry check failed; inspect repository state' >&2; exit 2 ;;
|
|
101
|
+
esac
|
|
102
|
+
done
|
|
103
|
+
exit 0
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
106
|
+
export async function projectNativeHooks(input) {
|
|
107
|
+
if (input.install === 'inspect')
|
|
108
|
+
return { kind: 'not-installed', reason: 'native hooks require explicit installation' };
|
|
109
|
+
if (input.install !== 'explicit-native' || !/^cockpit:[a-z0-9][a-z0-9-]*:[a-f0-9]{64}$/.test(input.cockpitId)
|
|
110
|
+
|| input.budget.policy.authority.cockpitId !== input.cockpitId) {
|
|
111
|
+
return refusal('native hook requires explicit installation, cockpit identity and a named positive budget');
|
|
112
|
+
}
|
|
113
|
+
const limit = policyLimit(input.budget.policy);
|
|
114
|
+
if (typeof limit !== 'number')
|
|
115
|
+
return limit;
|
|
116
|
+
const bytes = nativeScript(input.cockpitId, limit);
|
|
117
|
+
const harnesses = [...new Set(input.harnesses)].sort();
|
|
118
|
+
if (!harnesses.length)
|
|
119
|
+
return refusal('native hook requires configured harness ownership');
|
|
120
|
+
const entry = { path: '.githooks/pre-push', kind: 'file', ownership: 'whole-file',
|
|
121
|
+
harnesses: harnesses, writer: 'cockpit', schemaVersion: 1, scope: 'scoped', capability: 'instructed', bytes, sha256: sha256Hex(bytes) };
|
|
122
|
+
if (typeof input.budget.prepareMeasurement !== 'function')
|
|
123
|
+
return refusal('native hook requires a bounded measurement plan callback');
|
|
124
|
+
let measurement;
|
|
125
|
+
try {
|
|
126
|
+
measurement = await input.budget.prepareMeasurement(entry);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
return refusal(`native hook measurement failed: ${errorCause(error)}`);
|
|
130
|
+
}
|
|
131
|
+
const cost = await checkNativeHookBudget(entry, measurement, input.budget.policy);
|
|
132
|
+
if (cost.kind !== 'within-budget')
|
|
133
|
+
return cost;
|
|
134
|
+
const owned = bindSurfaceOwnershipIdentity([entry], input.cockpitId);
|
|
135
|
+
if (owned.kind === 'refusal')
|
|
136
|
+
return refusal(owned.diagnostic.message);
|
|
137
|
+
return { kind: 'complete', entries: [entry], cost,
|
|
138
|
+
activation: { kind: 'explicit-native-install', hooksPath: '.githooks', mode: 0o755 } };
|
|
139
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface DiffFile {
|
|
2
|
+
old: string;
|
|
3
|
+
next: string;
|
|
4
|
+
hunk: boolean;
|
|
5
|
+
removed: boolean;
|
|
6
|
+
removedLines: string[];
|
|
7
|
+
added: string[];
|
|
8
|
+
renamed: boolean;
|
|
9
|
+
deleted: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface WeakeningFinding {
|
|
12
|
+
readonly path: string;
|
|
13
|
+
readonly kind: WeakeningKind;
|
|
14
|
+
readonly reason: string;
|
|
15
|
+
}
|
|
16
|
+
type WeakeningKind = 'test-file-removed' | 'test-skipped' | 'check-suppressed' | 'tests-removed' | 'assertions-removed' | 'assertions-loosened' | 'assertion-vacuous' | 'test-selection-narrowed';
|
|
17
|
+
/** Reads `git diff` output (taken with --unified=0 and core.quotePath=true) into one record per file. */
|
|
18
|
+
export declare function parseDiff(diff: string): DiffFile[];
|
|
19
|
+
/**
|
|
20
|
+
* Why added lines turn off or short-circuit what runs. Only explicit added-line forms are measured;
|
|
21
|
+
* this is not JavaScript dataflow analysis.
|
|
22
|
+
*/
|
|
23
|
+
export declare function additiveBypassReasons(added: readonly string[]): string[];
|
|
24
|
+
export interface TestRoles {
|
|
25
|
+
readonly isTest: (path: string) => boolean;
|
|
26
|
+
/** A runner's configuration, or a manifest that carries a test script. */
|
|
27
|
+
readonly isTestConfig: (path: string) => boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The findings a diff earns over the files `roles` calls tests. A diff cannot prove that a
|
|
31
|
+
* replacement keeps its force, so a removal is flagged and a person acknowledges the legitimate ones.
|
|
32
|
+
*/
|
|
33
|
+
export declare function classifyTestWeakening(files: readonly DiffFile[], roles: TestRoles): WeakeningFinding[];
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Detects a change that makes tests pass by weakening them. It has no imports on purpose: CI runs
|
|
2
|
+
// the base revision's copy of scripts/check-test-protection.mjs beside this file, so the detector
|
|
3
|
+
// that judges a change is never the one the change edited, and the same code judges a consumer's
|
|
4
|
+
// checkout when `work grade` runs.
|
|
5
|
+
function gitPath(value) {
|
|
6
|
+
if (!value.startsWith('"'))
|
|
7
|
+
return value;
|
|
8
|
+
// Git quotes paths as C strings, including octal UTF-8 bytes.
|
|
9
|
+
const bytes = [];
|
|
10
|
+
for (let i = 1; i < value.length - 1; i++) {
|
|
11
|
+
if (value[i] !== '\\') {
|
|
12
|
+
bytes.push(...Buffer.from(value[i]));
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const octal = /^[0-7]{1,3}/.exec(value.slice(i + 1))?.[0];
|
|
16
|
+
if (octal) {
|
|
17
|
+
bytes.push(Number.parseInt(octal, 8));
|
|
18
|
+
i += octal.length;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const escaped = value[++i];
|
|
22
|
+
const controls = { t: '\t', n: '\n', r: '\r', b: '\b', f: '\f', v: '\v', '\\': '\\', '"': '"' };
|
|
23
|
+
if (!(escaped in controls))
|
|
24
|
+
throw new Error('unsupported Git path escape');
|
|
25
|
+
bytes.push(...Buffer.from(controls[escaped]));
|
|
26
|
+
}
|
|
27
|
+
return Buffer.from(bytes).toString('utf8');
|
|
28
|
+
}
|
|
29
|
+
/** Reads `git diff` output (taken with --unified=0 and core.quotePath=true) into one record per file. */
|
|
30
|
+
export function parseDiff(diff) {
|
|
31
|
+
const files = [];
|
|
32
|
+
let file;
|
|
33
|
+
for (const line of diff.split('\n')) {
|
|
34
|
+
if (line.startsWith('diff --git ')) {
|
|
35
|
+
const names = /^diff --git ("(?:\\.|[^"])*"|a\/\S+) ("(?:\\.|[^"])*"|b\/\S+)$/.exec(line);
|
|
36
|
+
file = { old: names ? gitPath(names[1]).slice(2) : '', next: names ? gitPath(names[2]).slice(2) : '', hunk: false, removed: false, removedLines: [], renamed: false, deleted: false, added: [] };
|
|
37
|
+
files.push(file);
|
|
38
|
+
}
|
|
39
|
+
else if (file) {
|
|
40
|
+
if (line.startsWith('@@ '))
|
|
41
|
+
file.hunk = true;
|
|
42
|
+
else if (!file.hunk && line.startsWith('--- '))
|
|
43
|
+
file.old = gitPath(line.slice(4)).replace(/^a\//, '');
|
|
44
|
+
else if (!file.hunk && line.startsWith('+++ '))
|
|
45
|
+
file.next = gitPath(line.slice(4)).replace(/^b\//, '');
|
|
46
|
+
else if (line.startsWith('rename from ')) {
|
|
47
|
+
file.old = gitPath(line.slice(12));
|
|
48
|
+
file.renamed = true;
|
|
49
|
+
}
|
|
50
|
+
else if (line.startsWith('rename to '))
|
|
51
|
+
file.next = gitPath(line.slice(10));
|
|
52
|
+
else if (line.startsWith('deleted file mode '))
|
|
53
|
+
file.deleted = true;
|
|
54
|
+
else if (line.startsWith('-') && line.slice(1).trim()) {
|
|
55
|
+
file.removed = true;
|
|
56
|
+
file.removedLines.push(line.slice(1));
|
|
57
|
+
}
|
|
58
|
+
else if (line.startsWith('+'))
|
|
59
|
+
file.added.push(line.slice(1));
|
|
60
|
+
else if (line.startsWith('Binary files '))
|
|
61
|
+
file.removed = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return files;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Why added lines turn off or short-circuit what runs. Only explicit added-line forms are measured;
|
|
68
|
+
* this is not JavaScript dataflow analysis.
|
|
69
|
+
*/
|
|
70
|
+
export function additiveBypassReasons(added) {
|
|
71
|
+
const reasons = [];
|
|
72
|
+
if (/(?:\.\s*|\[\s*['"])(?:skip|todo|only|skipIf|runIf|fails)\b|\b(?:skip|todo|only)\s*:\s*true/.test(added.join('\n'))) {
|
|
73
|
+
reasons.push('test selection or expected-failure semantics added');
|
|
74
|
+
}
|
|
75
|
+
if (added.some(line => /^[ \t]*return[ \t]*;?[ \t]*(?:\/\/.*)?$/.test(line))) {
|
|
76
|
+
reasons.push('potential additive control-flow bypass: bare return added');
|
|
77
|
+
}
|
|
78
|
+
if (added.some(line => /^[ \t]*(?:if\s*\(\s*(?:true|1)\s*\)\s*(?:\{\s*)?return\s*;?(?:\s*\})?\s*(?:\/\/.*)?$|if\s*\(\s*(?:false|0|null|undefined)\s*\)\s*\{|(?:false\s*&&|true\s*\|\|)\s*(?:describe|test|it|suite)\s*\(|process\s*\.\s*exit\s*\(\s*(?:0\s*)?\))/.test(line))) {
|
|
79
|
+
reasons.push('potential additive control-flow bypass: explicit static bypass added');
|
|
80
|
+
}
|
|
81
|
+
return reasons;
|
|
82
|
+
}
|
|
83
|
+
// JS/TS first, then the Python forms that mean the same. A match in an added comment still counts:
|
|
84
|
+
// a directive is a comment.
|
|
85
|
+
const SKIPPED = /\b(?:it|test|describe|suite|context|bench)\s*\.\s*(?:skip|only|todo|skipIf|runIf|fails|failing|fixme)\b|\b(?:xit|xtest|xdescribe|xcontext|fit|fdescribe)\s*\(|\b(?:skip|only)\s*:\s*true|\bpytest\s*\.\s*(?:mark\s*\.\s*(?:skip|skipif|xfail)|skip)\b|\bunittest\s*\.\s*(?:skip|skipIf|skipUnless|expectedFailure)\b|^\s*@(?:skip|skipIf|skipUnless|expectedFailure)\b|\bself\s*\.\s*skipTest\s*\(/;
|
|
86
|
+
const SUPPRESSED = /@ts-ignore|@ts-nocheck|eslint-disable|\bnoqa\b|\btype:\s*ignore\b|\bpylint:\s*disable\b/;
|
|
87
|
+
const TEST_DECLARATION = /^\s*(?:(?:it|test)\s*(?:\.\s*\w+)*\s*(?:\.\s*each\s*\([^)]*\))?\s*\(|def\s+test_|async\s+def\s+test_)/;
|
|
88
|
+
const ASSERTION = /\bexpect\s*\(|\bassert\b|\bassert[A-Z]\w*\s*\(|\.should\b|\bt\.(?:is|true|deepEqual|equal|throws)\s*\(/;
|
|
89
|
+
const STRONG = /\.(?:toBe|toEqual|toStrictEqual|toMatchObject|toHaveBeenCalledWith|toHaveLength|toThrow\w*|toContain\w*|toMatch)\s*\(|\bassert(?:Equal|Equals|In|Raises|IsNone)\b|\bassert\s+\S+\s*(?:==|!=|in)\s/;
|
|
90
|
+
const WEAK = /\.(?:toBeDefined|toBeTruthy|toBeFalsy|toBeUndefined)\s*\(|\bexpect\.(?:anything|any)\s*\(|\.not\.toBeNull\s*\(|\bassert\s+\w+\s*$|\bassertTrue\s*\(\s*\w+\s*\)/;
|
|
91
|
+
const VACUOUS = /\bexpect\s*\(\s*(?:true|1|false|0|null|'[^']*'|"[^"]*")\s*\)\s*\.\s*(?:toBe|toEqual|toStrictEqual)\s*\(|\bassert\s+(?:True|1|not\s+False)\s*$|\bassert\s*\(?\s*true\s*\)?\s*;?\s*$|\bassertTrue\s*\(\s*True\s*\)/;
|
|
92
|
+
// What a test runner's own configuration says about which files it runs.
|
|
93
|
+
const NARROWING = /\b(?:exclude|testPathIgnorePatterns|testIgnore|ignore|deselect|norecursedirs|skip)\b|--ignore\b|--deselect\b|--exclude\b|--passWithNoTests|\|\|\s*true\b|\bexit\s+0\b/;
|
|
94
|
+
const WIDENING = /\b(?:include|testMatch|testRegex|testpaths|projects)\b/;
|
|
95
|
+
const TEST_SCRIPT = /^\s*"(?:pre|post)?test[^"]*"\s*:/;
|
|
96
|
+
const count = (lines, pattern) => lines.filter(line => pattern.test(line)).length;
|
|
97
|
+
/**
|
|
98
|
+
* The findings a diff earns over the files `roles` calls tests. A diff cannot prove that a
|
|
99
|
+
* replacement keeps its force, so a removal is flagged and a person acknowledges the legitimate ones.
|
|
100
|
+
*/
|
|
101
|
+
export function classifyTestWeakening(files, roles) {
|
|
102
|
+
const findings = [];
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
const path = file.deleted || file.next === '/dev/null' ? file.old : file.next;
|
|
105
|
+
const finding = (kind, reason) => findings.push({ path, kind, reason });
|
|
106
|
+
const wasTest = roles.isTest(file.old);
|
|
107
|
+
if (wasTest && (file.deleted || file.next === '/dev/null' || file.renamed && !roles.isTest(file.next))) {
|
|
108
|
+
finding('test-file-removed', 'a test file was deleted or moved out of the tests');
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (roles.isTest(path)) {
|
|
112
|
+
if (count(file.added, SKIPPED))
|
|
113
|
+
finding('test-skipped', 'a test was skipped, focused, marked todo or expected to fail');
|
|
114
|
+
if (count(file.added, SUPPRESSED))
|
|
115
|
+
finding('check-suppressed', 'a type or lint check was suppressed in a test');
|
|
116
|
+
if (count(file.removedLines, TEST_DECLARATION) > count(file.added, TEST_DECLARATION))
|
|
117
|
+
finding('tests-removed', 'more tests were removed than added');
|
|
118
|
+
if (count(file.removedLines, ASSERTION) > count(file.added, ASSERTION))
|
|
119
|
+
finding('assertions-removed', 'more assertions were removed than added');
|
|
120
|
+
else if (count(file.removedLines, STRONG) > count(file.added, STRONG) && count(file.added, WEAK) > count(file.removedLines, WEAK)) {
|
|
121
|
+
finding('assertions-loosened', 'specific assertions were replaced by weaker ones');
|
|
122
|
+
}
|
|
123
|
+
if (count(file.added, VACUOUS))
|
|
124
|
+
finding('assertion-vacuous', 'an assertion that cannot fail was added');
|
|
125
|
+
}
|
|
126
|
+
else if (roles.isTestConfig(path)) {
|
|
127
|
+
const manifest = /(?:^|\/)package\.json$/.test(path);
|
|
128
|
+
const scriptsAdded = manifest ? file.added.filter(line => TEST_SCRIPT.test(line)) : file.added;
|
|
129
|
+
if (count(scriptsAdded, NARROWING))
|
|
130
|
+
finding('test-selection-narrowed', 'test configuration now excludes, ignores or tolerates the absence of tests');
|
|
131
|
+
else if (manifest ? count(file.removedLines, TEST_SCRIPT) > count(scriptsAdded, TEST_SCRIPT)
|
|
132
|
+
: count(file.removedLines, WIDENING) > count(file.added, WIDENING))
|
|
133
|
+
finding('test-selection-narrowed', 'test configuration no longer selects what it selected');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return findings;
|
|
137
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare const RUN_VERDICTS: readonly ['PASS', 'FAIL', 'PARTIAL', 'DEGRADED', 'VOID'];
|
|
2
|
+
export type RunVerdict = typeof RUN_VERDICTS[number];
|
|
3
|
+
export type StageResult = Readonly<{
|
|
4
|
+
kind: 'executed';
|
|
5
|
+
exitCode: number;
|
|
6
|
+
signal: string | null;
|
|
7
|
+
outputBytes: number;
|
|
8
|
+
assertion: 'passed' | 'failed' | 'missing';
|
|
9
|
+
}> | Readonly<{
|
|
10
|
+
kind: 'unavailable';
|
|
11
|
+
reason: string;
|
|
12
|
+
}>;
|
|
13
|
+
export type StageObservation = StageResult & Readonly<{
|
|
14
|
+
id: string;
|
|
15
|
+
}>;
|
|
16
|
+
export interface RunInput {
|
|
17
|
+
readonly declared: readonly string[];
|
|
18
|
+
readonly selected: readonly string[];
|
|
19
|
+
readonly observations: readonly StageObservation[];
|
|
20
|
+
readonly scope: 'full' | 'subset' | 'staged';
|
|
21
|
+
readonly stability: 'stable' | 'void' | 'stale' | 'unavailable';
|
|
22
|
+
}
|
|
23
|
+
export interface RunClassification {
|
|
24
|
+
readonly verdict: RunVerdict;
|
|
25
|
+
readonly satisfiesGate: boolean;
|
|
26
|
+
readonly executed: number;
|
|
27
|
+
readonly reasons: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
export declare function classifyRun(input: RunInput): RunClassification;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export const RUN_VERDICTS = ['PASS', 'FAIL', 'PARTIAL', 'DEGRADED', 'VOID'];
|
|
2
|
+
export function classifyRun(input) {
|
|
3
|
+
const declared = new Set(input.declared);
|
|
4
|
+
const selected = new Set(input.selected);
|
|
5
|
+
const seen = new Set();
|
|
6
|
+
const reasons = [];
|
|
7
|
+
let failed = false;
|
|
8
|
+
let executed = 0;
|
|
9
|
+
let degraded = !declared.size || declared.size !== input.declared.length || !selected.size
|
|
10
|
+
|| selected.size !== input.selected.length || input.selected.some(id => !declared.has(id))
|
|
11
|
+
|| !['full', 'subset', 'staged'].includes(input.scope) || !['stable', 'void', 'stale', 'unavailable'].includes(input.stability);
|
|
12
|
+
if (degraded)
|
|
13
|
+
reasons.push('stage selection must be nonempty, unique and declared');
|
|
14
|
+
for (const row of input.observations) {
|
|
15
|
+
const first = !seen.has(row.id);
|
|
16
|
+
if (!selected.has(row.id) || seen.has(row.id)) {
|
|
17
|
+
degraded = true;
|
|
18
|
+
reasons.push(`${row.id}: unexpected or duplicate stage evidence`);
|
|
19
|
+
}
|
|
20
|
+
seen.add(row.id);
|
|
21
|
+
if (row.kind === 'executed' && (row.assertion === 'failed'
|
|
22
|
+
|| row.signal === null && Number.isInteger(row.exitCode) && row.exitCode > 0)) {
|
|
23
|
+
failed = true;
|
|
24
|
+
reasons.push(`${row.id}: executed check disagreed (exit ${row.exitCode})`);
|
|
25
|
+
}
|
|
26
|
+
if (row.kind === 'unavailable') {
|
|
27
|
+
degraded = true;
|
|
28
|
+
reasons.push(`${row.id}: ${row.reason}`);
|
|
29
|
+
}
|
|
30
|
+
else if (row.kind !== 'executed' || row.signal !== null || !Number.isInteger(row.exitCode) || row.exitCode < 0) {
|
|
31
|
+
degraded = true;
|
|
32
|
+
reasons.push(`${row.id}: execution interrupted or invalid (${row.signal ?? row.exitCode})`);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// The authenticated exit status of a process that ran to completion proves the stage ran;
|
|
36
|
+
// its output does not. Grading silence as unproven made tsc --noEmit, node --check and quiet
|
|
37
|
+
// linters unable to satisfy a gate. A runner that selects no tests reports that through its
|
|
38
|
+
// own exit status, and a narrowed run is PARTIAL whatever it prints.
|
|
39
|
+
if (first && selected.has(row.id))
|
|
40
|
+
executed++;
|
|
41
|
+
if (row.assertion !== 'passed' && row.assertion !== 'failed') {
|
|
42
|
+
degraded = true;
|
|
43
|
+
reasons.push(`${row.id}: no structured assertion result`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const id of selected)
|
|
48
|
+
if (!seen.has(id)) {
|
|
49
|
+
degraded = true;
|
|
50
|
+
reasons.push(`${id}: selected stage never ran`);
|
|
51
|
+
}
|
|
52
|
+
if (input.stability === 'unavailable') {
|
|
53
|
+
degraded = true;
|
|
54
|
+
reasons.push('subject stability evidence unavailable');
|
|
55
|
+
}
|
|
56
|
+
const voided = input.stability === 'void' || input.stability === 'stale';
|
|
57
|
+
if (voided)
|
|
58
|
+
reasons.push('subject changed or evidence authority is stale; evidence satisfies no gate');
|
|
59
|
+
const partial = input.scope !== 'full' || selected.size !== declared.size;
|
|
60
|
+
const verdict = failed ? 'FAIL' : degraded ? 'DEGRADED' : voided ? 'VOID' : partial ? 'PARTIAL' : 'PASS';
|
|
61
|
+
return { verdict, satisfiesGate: verdict === 'PASS', executed, reasons };
|
|
62
|
+
}
|