test: add schema v3 only absence gate

This commit is contained in:
2026-08-11 08:09:01 +02:00
parent 5310c6555b
commit 66f44b054f
17 changed files with 4209 additions and 56 deletions
@@ -0,0 +1,23 @@
workspace:
schema_version: 3
id: task13-smoke
name: Task 13 Smoke
language: en
dwh:
engine: postgres
database: warehouse
schema: analytics
supported_transports: [postgres_direct]
semantic_index:
vector_store:
engine: qdrant
collection: task13-smoke
dimensions: 1024
distance: cosine
embedding:
provider: ollama_internal
model: qwen3-embedding:0.6b
dimensions: 1024
llm_policy:
default: local-qwen/task13-smoke
allowed: [local-qwen/task13-smoke]
+1 -25
View File
@@ -41,31 +41,7 @@ TASK13_CURRENT_IMAGE_OVERRIDE="$fixture/current-image.yaml"
mkdir -p "$TASK13_REMOTE"
workspace="$fixture/task13-smoke.yaml"
cat >"$workspace" <<'EOF'
workspace:
schema_version: 3
id: task13-smoke
name: Task 13 Smoke
language: en
dwh:
engine: postgres
database: warehouse
schema: analytics
supported_transports: [postgres_direct]
semantic_index:
vector_store:
engine: qdrant
collection: task13-smoke
dimensions: 1024
distance: cosine
embedding:
provider: ollama_internal
model: qwen3-embedding:0.6b
dimensions: 1024
llm_policy:
default: local-qwen/task13-smoke
allowed: [local-qwen/task13-smoke]
EOF
cp "$root/scripts/fixtures/workspace-registry-task13.yaml" "$workspace"
if [[ "$profile" == local ]]; then
task13_write_fixture_files
+716
View File
@@ -0,0 +1,716 @@
#!/usr/bin/env bash
# Regression tests for the fail-closed schema-v3-only absence gate.
set -euo pipefail
project_root="$(cd "$(dirname "$0")/.." && pwd -P)"
gate="$project_root/scripts/verify-schema-v3-only.sh"
gate_bash="${BASH:-bash}"
sandbox="$(mktemp -d "${TMPDIR:-/tmp}/thoth-v3-gate-test.XXXXXX")"
fixture="$sandbox/fixture repository"
output="$sandbox/output"
real_git="$(command -v git)"
canonical_schema="$project_root/backend/dist/workspaces/schema.js"
canonical_server="$project_root/backend/dist/server.js"
canonical_schema_checksum="$(cksum <"$canonical_schema")"
canonical_server_checksum="$(cksum <"$canonical_server")"
cleanup() {
rm -rf "$sandbox"
}
trap cleanup EXIT HUP INT TERM
fail() {
echo "FAIL: $*" >&2
[[ ! -f "$output" ]] || cat "$output" >&2
exit 1
}
write_fixture_descriptor() {
local path="$1" workspace_key="${2:-workspace:}" schema_key="${3:- schema_version: 3}"
printf '%s\n' "$workspace_key" "$schema_key" >"$path"
cat >>"$path" <<'YAML'
id: fixture-workspace
name: Fixture Workspace
language: en
dwh:
engine: postgres
database: warehouse
schema: public
supported_transports: [postgres_direct]
semantic_index:
vector_store:
engine: qdrant
collection: fixture-workspace
dimensions: 1024
distance: cosine
embedding:
provider: ollama_internal
model: qwen3-embedding:0.6b
dimensions: 1024
llm_policy:
allowed: [fixture/model]
YAML
}
seed_fixture() {
rm -rf "$fixture"
mkdir -p \
"$fixture/backend/src/nested dir" \
"$fixture/backend/scripts" \
"$fixture/frontend/src/api" \
"$fixture/deploy/workspaces" \
"$fixture/scripts/fixtures"
"$real_git" -C "$fixture" init -q
"$real_git" -C "$fixture" config user.email fixture@example.invalid
"$real_git" -C "$fixture" config user.name Fixture
printf '%s\n' 'export const schemaVersion = 3;' >"$fixture/backend/src/server.ts"
printf '%s\n' 'export const spaced = true;' >"$fixture/backend/src/nested dir/file name.ts"
newline_path="$fixture/backend/src/line
break.ts"
printf '%s\n' 'export const newline = true;' >"$newline_path"
printf '%s\n' 'export const currentWorkspace = true;' >"$fixture/frontend/src/api/workspaces.ts"
printf '%s\n' 'export const productionCheck = true;' >"$fixture/backend/scripts/runtime-check.mjs"
write_fixture_descriptor "$fixture/deploy/workspaces/example.yaml"
write_fixture_descriptor "$fixture/deploy/workspaces/psd.yaml.example"
printf '%s\n' '#!/usr/bin/env bash' 'echo operator-smoke' >"$fixture/scripts/workspace-registry-smoke.sh"
write_fixture_descriptor "$fixture/scripts/fixtures/workspace-registry-smoke.yaml"
write_fixture_descriptor "$fixture/scripts/fixtures/workspace-registry-windows.yaml"
printf '%s\n' 'Write-Output "schema v3"' >"$fixture/scripts/test-windows-clone-contract.ps1"
"$real_git" -C "$fixture" add .
"$real_git" -C "$fixture" commit -qm seed
}
commit_fixture() {
"$real_git" -C "$fixture" add .
"$real_git" -C "$fixture" commit -qm "$1"
}
run_gate() {
set +e
"$gate_bash" "$gate" --root "$fixture" --runtime-only >"$output" 2>&1
gate_status=$?
set -e
}
expect_pass() {
local label="$1"
run_gate
[[ $gate_status -eq 0 ]] || fail "$label: expected pass, got status $gate_status"
}
expect_rejected() {
local label="$1" expected="$2"
run_gate
[[ $gate_status -eq 1 ]] || fail "$label: expected rejection status 1, got $gate_status"
grep -Fq -- "$expected" "$output" || fail "$label: rejection did not identify $expected"
}
# Clean tracked live paths, including spaces and an embedded newline, are NUL-safe.
seed_fixture
expect_pass "clean runtime fixture"
seed_fixture
mkfifo "$fixture/scripts/runtime-fifo"
expect_rejected "runtime FIFO" "scripts/runtime-fifo"
set +e
"$gate_bash" "$gate" --help >"$output" 2>&1
help_status=$?
set -e
[[ $help_status -eq 0 ]] || fail "gate help failed with status $help_status"
grep -Fq 'Node' "$output" || fail "gate help omits the runtime-only Node dependency"
grep -Fq 'backend/dist/workspaces/schema.js' "$output" \
|| fail "gate help omits the runtime-only compiled schema dependency"
grep -Fq 'scripts/verify-schema-v3-only-release.sh' "$output" \
|| fail "gate help omits the durable release entry point"
grep -Fq 'npm ci' "$output" || fail "gate help omits lockfile install order"
# Prescribed symbols and migration markers are case-insensitive substrings.
seed_fixture
printf '%s\n' 'export type WorkspaceV2Compat = unknown;' >"$fixture/backend/src/legacy.ts"
commit_fixture backend-symbol
expect_rejected "backend prescribed derivative symbol" "backend/src/legacy.ts"
seed_fixture
printf '%s\n' 'export type LegacyWorkspace = unknown;' >"$fixture/backend/src/legacy-workspace.ts"
commit_fixture legacy-workspace-symbol
expect_rejected "exact LegacyWorkspace symbol" "backend/src/legacy-workspace.ts"
seed_fixture
printf '%s\n' 'export const status = "MIGRATION_REQUIRED";' >"$fixture/backend/src/status.ts"
commit_fixture backend-case-insensitive-marker
expect_rejected "case-insensitive marker" "backend/src/status.ts"
# Every forbidden category is applied to every recursive policy root without extension filters.
policy_roots=(backend/src frontend/src backend/scripts scripts)
policy_extensions=(ts py js yaml.example)
policy_names=(WorkspaceV2 LegacyWorkspace migration_required 'revision.state')
for category_index in 0 1 2 3; do
for root_index in 0 1 2 3; do
seed_fixture
matrix_root="${policy_roots[$root_index]}"
matrix_extension="${policy_extensions[$root_index]}"
matrix_path="$matrix_root/matrix-$category_index.$matrix_extension"
mkdir -p "${fixture:?}/$matrix_root"
printf '%s\n' "${policy_names[$category_index]}" >"$fixture/$matrix_path"
commit_fixture "policy-matrix-$category_index-$root_index"
expect_rejected "policy category $category_index root $matrix_root" "$matrix_path"
done
done
seed_fixture
printf '%s\n' 'export const status = "migration_required";' >"$fixture/frontend/src/api/workspaces.ts"
commit_fixture frontend-marker
expect_rejected "frontend migration status" "frontend/src/api/workspaces.ts"
seed_fixture
printf '%s\n' 'export const blocked = record.revision.state !== "operational";' >"$fixture/frontend/src/api/workspaces.ts"
commit_fixture frontend-revision-state
expect_rejected "frontend revision state gate" "frontend/src/api/workspaces.ts"
seed_fixture
mkdir -p "$fixture/backend/scripts/nested/production"
printf '%s\n' 'const helper = "migrate-v2-qdrant.js";' >"$fixture/backend/scripts/nested/production/runtime.mjs"
commit_fixture nested-mjs
expect_rejected "nested backend production script" "backend/scripts/nested/production/runtime.mjs"
seed_fixture
printf '%s\n' 'const historical = entry.revision.state;' >"$fixture/backend/scripts/runtime-check.mjs"
commit_fixture backend-historical-state
expect_rejected "backend historical revision state" "backend/scripts/runtime-check.mjs"
seed_fixture
printf '%s\n' 'node backend/dist/workspaces/MIGRATE-LEGACY.js' >"$fixture/scripts/workspace-registry-smoke.sh"
commit_fixture operator-migrator
expect_rejected "operator smoke migrator" "scripts/workspace-registry-smoke.sh"
# Workspace YAML embedded in live non-test deployment scripts is validated structurally.
seed_fixture
mkdir -p "$fixture/scripts/operators"
cat >"$fixture/scripts/operators/heredoc-smoke.sh" <<'EOF'
#!/usr/bin/env bash
cat <<'YAML'
workspace:
schema_version: 2
YAML
EOF
commit_fixture script-heredoc-v2
expect_rejected "deployment-script workspace schema" "scripts/operators/heredoc-smoke.sh"
seed_fixture
mkdir -p "$fixture/scripts/operators"
cat >"$fixture/scripts/operators/quoted-heredoc-smoke.sh" <<'EOF'
#!/usr/bin/env bash
cat <<'YAML'
"workspace" :
'schema_version' : 0x2
YAML
EOF
commit_fixture script-quoted-heredoc
expect_rejected "quoted deployment-script workspace schema" "scripts/operators/quoted-heredoc-smoke.sh"
seed_fixture
mkdir -p "$fixture/scripts/operators"
{
printf '%s\n' '#!/usr/bin/env bash' "cat <<'---'"
printf '%s \n' '---'
printf '%s\n' 'workspace:' ' schema_version: 2' '---'
} >"$fixture/scripts/operators/exact-close-smoke.sh"
"$gate_bash" -n "$fixture/scripts/operators/exact-close-smoke.sh"
commit_fixture script-exact-heredoc-close
expect_rejected "heredoc false close with trailing blanks" "scripts/operators/exact-close-smoke.sh"
seed_fixture
mkdir -p "$fixture/scripts/operators"
cat >"$fixture/scripts/operators/double-quoted-backslash-smoke.sh" <<'EOF'
#!/usr/bin/env bash
cat <<"\---"
---
workspace:
schema_version: 2
\---
EOF
"$gate_bash" -n "$fixture/scripts/operators/double-quoted-backslash-smoke.sh"
reviewer_output="$("$gate_bash" "$fixture/scripts/operators/double-quoted-backslash-smoke.sh")"
printf '%s\n' "$reviewer_output" | grep -F 'schema_version: 2' >/dev/null || fail "double-quoted backslash reviewer fixture did not execute with the Bash delimiter"
commit_fixture script-double-quoted-backslash
expect_rejected "double-quoted non-special backslash delimiter" "scripts/operators/double-quoted-backslash-smoke.sh"
seed_fixture
mkdir -p "$fixture/scripts/operators"
cat >"$fixture/scripts/operators/split-operator-smoke.sh" <<'EOF'
#!/usr/bin/env bash
cat <\
<'YAML'
workspace:
schema_version: 2
YAML
EOF
"$gate_bash" -n "$fixture/scripts/operators/split-operator-smoke.sh"
reviewer_output="$("$gate_bash" "$fixture/scripts/operators/split-operator-smoke.sh")"
printf '%s\n' "$reviewer_output" | grep -F 'schema_version: 2' >/dev/null || fail "split-operator reviewer fixture did not execute as a Bash heredoc"
commit_fixture script-split-heredoc-operator
expect_rejected "split heredoc operator continuation" "scripts/operators/split-operator-smoke.sh"
seed_fixture
mkdir -p "$fixture/scripts/operators"
cat >"$fixture/scripts/operators/evidence-bundle-smoke.sh" <<'EOF'
#!/usr/bin/env bash
cat <<'YAML'
evidence:
source: bundle
schema_version: 2
YAML
EOF
"$gate_bash" -n "$fixture/scripts/operators/evidence-bundle-smoke.sh"
commit_fixture script-evidence-bundle
expect_pass "evidence bundle without workspace mapping"
seed_fixture
mkdir -p "$fixture/scripts/operators"
cat >"$fixture/scripts/operators/powershell-comment-smoke.ps1" <<'EOF'
# harmless PowerShell comment \
$workspace = @'
workspace:
schema_version: 2
'@
EOF
commit_fixture powershell-comment-v2
expect_rejected "PowerShell comment backslash before v2 here-string" "scripts/operators/powershell-comment-smoke.ps1"
seed_fixture
mkdir -p "$fixture/scripts/operators"
cat >"$fixture/scripts/operators/powershell-valid-smoke.ps1" <<'EOF'
$workspace = @'
EOF
cat "$fixture/deploy/workspaces/example.yaml" >>"$fixture/scripts/operators/powershell-valid-smoke.ps1"
cat >>"$fixture/scripts/operators/powershell-valid-smoke.ps1" <<'EOF'
'@
$bundle = @'
evidence:
source: bundle
schema_version: 2
'@
EOF
commit_fixture powershell-v3-and-bundle
expect_rejected "PowerShell embedded v3 descriptor" "scripts/operators/powershell-valid-smoke.ps1"
seed_fixture
mkdir -p "$fixture/scripts/operators"
cat >"$fixture/scripts/operators/powershell-bundle-smoke.ps1" <<'EOF'
$bundle = @'
evidence:
source: bundle
schema_version: 2
'@
EOF
commit_fixture powershell-bundle
expect_pass "PowerShell non-workspace bundle"
# Quoted/space/indented YAML keys are real mappings; v3 passes and non-v3 fails.
seed_fixture
write_fixture_descriptor \
"$fixture/deploy/workspaces/example.yaml" \
'"workspace" :' \
" 'schema_version' : 3"
commit_fixture quoted-yaml-v3
expect_pass "quoted and indented workspace v3"
seed_fixture
cat >"$fixture/deploy/workspaces/example.yaml" <<'EOF'
'workspace' :
"schema_version" : 02
EOF
commit_fixture quoted-yaml-noncanonical
expect_rejected "quoted workspace noncanonical schema" "deploy/workspaces/example.yaml"
seed_fixture
printf '%s\n' 'workspace: { schema_version: 3 }' >"$fixture/deploy/workspaces/example.yaml"
commit_fixture inline-workspace
expect_rejected "inline workspace mapping" "deploy/workspaces/example.yaml"
# Deleted migrator basenames are rejected case-insensitively at any live nesting depth.
seed_fixture
mkdir -p "$fixture/backend/src/deep/nested"
printf '%s\n' 'export const otherwiseClean = true;' >"$fixture/backend/src/deep/nested/Migrate-Legacy.ts"
commit_fixture deleted-case-path
expect_rejected "case-insensitive deleted basename" "backend/src/deep/nested/Migrate-Legacy.ts"
# Live filesystem/index trust is fail-closed, including ignored and newline-bearing files.
seed_fixture
printf '%s\n' 'export const changed = true;' >"$fixture/backend/src/server.ts"
expect_rejected "modified tracked source" "backend/src/server.ts"
seed_fixture
printf '%s\n' 'export const staged = true;' >"$fixture/backend/src/server.ts"
"$real_git" -C "$fixture" add backend/src/server.ts
expect_rejected "staged tracked source" "backend/src/server.ts"
seed_fixture
printf '%s\n' 'export const untracked = true;' >"$fixture/backend/src/untracked.ts"
expect_rejected "untracked production source" "backend/src/untracked.ts"
seed_fixture
printf '%s\n' 'backend/src/ignored.ts' >"$fixture/.gitignore"
commit_fixture ignore-rule
printf '%s\n' 'export const ignored = true;' >"$fixture/backend/src/ignored.ts"
expect_rejected "ignored production source" "backend/src/ignored.ts"
seed_fixture
untracked_newline="$fixture/backend/src/untracked
production.ts"
printf '%s\n' 'export const untrackedNewline = true;' >"$untracked_newline"
expect_rejected "newline-bearing untracked source" "backend/src/untracked\nproduction.ts"
seed_fixture
ln -s server.ts "$fixture/backend/src/tracked-link.ts"
commit_fixture tracked-symlink
expect_rejected "tracked live symlink" "backend/src/tracked-link.ts"
# Generic non-workspace state/version formats remain allowed on live paths.
seed_fixture
mkdir -p "$fixture/backend/scripts/nested" "$fixture/scripts/operators"
printf '%s\n' \
'const first = entry.state;' \
'const second = lease.state === "operational";' \
'const third = job.state;' >"$fixture/backend/scripts/nested/generic-state.mjs"
printf '%s\n' '#!/usr/bin/env bash' 'bundle_schema_version=1' >"$fixture/scripts/operators/bundle-smoke.sh"
commit_fixture unrelated-state-version
expect_pass "unrelated entry lease job state and bundle version"
seed_fixture
printf '%s\n' 'const stale = selectedWorkspace?.state;' >"$fixture/backend/scripts/runtime-check.mjs"
commit_fixture workspace-state-gate
expect_rejected "selected workspace revision state" "backend/scripts/runtime-check.mjs"
seed_fixture
printf '%s\n' 'const revision = { revision: { id: "x", state: "operational" } };' >"$fixture/backend/scripts/runtime-check.mjs"
commit_fixture revision-object-state
expect_rejected "bounded revision object state" "backend/scripts/runtime-check.mjs"
# A top-level workspace descriptor has one exact schema_version: 3 key.
for malformed in schema-v1 schema-v2 leading-zero hexadecimal multiline duplicate; do
seed_fixture
case "$malformed" in
schema-v1)
printf '%s\n' 'workspace:' ' schema_version: 1' >"$fixture/deploy/workspaces/example.yaml"
;;
schema-v2)
printf '%s\n' 'workspace:' ' schema_version: 2' >"$fixture/deploy/workspaces/example.yaml"
;;
leading-zero)
printf '%s\n' 'workspace:' ' schema_version: 02' >"$fixture/deploy/workspaces/example.yaml"
;;
hexadecimal)
printf '%s\n' 'workspace:' ' schema_version: 0x2' >"$fixture/deploy/workspaces/example.yaml"
;;
multiline)
printf '%s\n' 'workspace:' ' schema_version: >' ' 3' >"$fixture/deploy/workspaces/example.yaml"
;;
duplicate)
printf '%s\n' 'workspace:' ' schema_version: 3' ' schema_version: 3' >"$fixture/deploy/workspaces/example.yaml"
;;
esac
commit_fixture "yaml-$malformed"
expect_rejected "malformed workspace schema $malformed" "deploy/workspaces/example.yaml"
done
# YAML that is not a top-level workspace descriptor is not a generic schema-version target.
seed_fixture
printf '%s\n' 'bundle_schema_version: 1' >"$fixture/deploy/workspaces/preprocess-dwh.yaml"
commit_fixture unrelated-yaml-version
expect_pass "unrelated YAML schema version"
# Explicit deleted source paths and case-insensitive deleted basenames cannot hide as directories.
seed_fixture
mkdir -p "$fixture/backend/src/workspaces/migrate-legacy.ts"
expect_rejected "deleted source directory" "backend/src/workspaces/migrate-legacy.ts"
seed_fixture
mkdir -p "$fixture/backend/src/deep/Migrate-V2-Qdrant.ts"
expect_rejected "case-insensitive deleted directory" "backend/src/deep/Migrate-V2-Qdrant.ts"
# Test and fixture naming never bypasses trust or content policy.
seed_fixture
mkdir -p "$fixture/frontend/src/test"
ln -s ../api/workspaces.ts "$fixture/frontend/src/test/negative.test.ts"
commit_fixture tracked-test-symlink
expect_rejected "tracked test symlink" "frontend/src/test/negative.test.ts"
seed_fixture
mkdir -p "$fixture/frontend/src/test"
printf '%s\n' 'export const clean = true;' >"$fixture/frontend/src/test/changed.test.ts"
commit_fixture tracked-test-dirty
printf '%s\n' 'export const changed = true;' >"$fixture/frontend/src/test/changed.test.ts"
expect_rejected "dirty test file" "frontend/src/test/changed.test.ts"
seed_fixture
mkdir -p "$fixture/frontend/src/test"
printf '%s\n' 'migration_required' >"$fixture/frontend/src/test/negative.test.ts"
commit_fixture tracked-test-forbidden
expect_rejected "forbidden marker in test path" "frontend/src/test/negative.test.ts"
# Explicitly live test-named Windows and workspace fixtures are not excluded.
seed_fixture
printf '%s\n' 'Write-Output "MIGRATE-LEGACY"' >"$fixture/scripts/test-windows-clone-contract.ps1"
commit_fixture live-windows-exception
expect_rejected "live Windows fixture exception" "scripts/test-windows-clone-contract.ps1"
seed_fixture
printf '%s\n' 'workspace:' ' schema_version: 2' >"$fixture/scripts/fixtures/workspace-registry-smoke.yaml"
commit_fixture live-workspace-fixture
expect_rejected "live workspace fixture exception" "scripts/fixtures/workspace-registry-smoke.yaml"
seed_fixture
write_fixture_descriptor \
"$fixture/scripts/fixtures/workspace-registry-future.yaml" \
'workspace:' \
' schema_version: 2'
commit_fixture future-workspace-family
expect_rejected "future workspace fixture family" "scripts/fixtures/workspace-registry-future.yaml"
# Only exact path+category policy literals are allowed; paths outside policy roots remain out of scope.
seed_fixture
mkdir -p "$fixture/docs/superpowers/plans"
printf '%s\n' 'Historical schema_version: 2 and migration_required.' >"$fixture/docs/superpowers/plans/history.md"
printf '%s\n' 'migration_required migrate-legacy WorkspaceV2 revision.state' >"$fixture/scripts/test-verify-schema-v3-only.sh"
commit_fixture exact-policy-allowlist
expect_pass "exact self-test policy allowlist and historical docs"
# Diagnostic paths are shell-escaped so a newline cannot forge another log line.
seed_fixture
newline_spoof="$fixture/backend/src/spoof
forged.py"
printf '%s\n' harmless >"$newline_spoof"
run_gate
[[ $gate_status -eq 1 ]] || fail "newline untracked path was not rejected"
grep -Fq 'backend/src/spoof\nforged.py' "$output" \
|| fail "newline path diagnostic was not escaped on one line"
# Scanner operational errors are propagated, not converted into absence.
seed_fixture
fake_bin="$sandbox/fake-bin"
mkdir -p "$fake_bin"
cat >"$fake_bin/git" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
for argument in "$@"; do
if [[ "$argument" == grep ]]; then
echo "simulated git grep failure" >&2
exit 2
fi
done
exec "$REAL_GIT" "$@"
EOF
chmod +x "$fake_bin/git"
set +e
PATH="$fake_bin:$PATH" REAL_GIT="$real_git" "$gate_bash" "$gate" --root "$fixture" --runtime-only >"$output" 2>&1
gate_status=$?
set -e
[[ $gate_status -eq 2 ]] || fail "git grep status 2 was masked as $gate_status"
grep -Fq 'simulated git grep failure' "$output" || fail "git grep failure diagnostics were lost"
# Foreign roots are test-only and can never select fixture code for a full check.
set +e
"$gate_bash" "$gate" --root "$fixture" >"$output" 2>&1
gate_status=$?
set -e
[[ $gate_status -ne 0 ]] || fail "foreign-root full mode unexpectedly passed"
grep -Fq -- '--root is available only with --runtime-only or --bootstrap-trust-only' "$output" \
|| fail "foreign-root full rejection did not report the trust boundary"
# Prescribed symbols are forbidden as case-insensitive substrings, including derivatives.
derivative_names=(WorkspaceV2Compat wOrKsPaCeV2 deprecatedV2Descriptor WRITEMIGRATEDWORKSPACE LegacyWorkspaceAdapter migrateLegacyWorkspaceCompat)
for derivative in "${derivative_names[@]}"; do
for matrix_root in "${policy_roots[@]}"; do
seed_fixture
matrix_path="$matrix_root/derivative.ts"
printf '%s\n' "$derivative" >"$fixture/$matrix_path"
commit_fixture "derivative-$derivative-${matrix_root//\//-}"
expect_rejected "derivative $derivative in $matrix_root" "$matrix_path"
done
done
# Mixed/all-lower legacy spellings fail; the approved lower-camel identifier remains valid.
for spelling in legacyworkspace LeGaCyWoRkSpAcE; do
seed_fixture
printf '%s\n' "$spelling" >"$fixture/backend/src/legacy-variant.ts"
commit_fixture legacy-spelling
expect_rejected "legacy spelling $spelling" "backend/src/legacy-variant.ts"
done
seed_fixture
printf '%s\n' 'const legacyWorkspacePath = current;' >"$fixture/backend/src/legacy-allowed.ts"
commit_fixture lower-camel-legacy
expect_pass "approved lower-camel legacy identifier"
# Full-text revision scanning covers bracket access and newline-separated dot access.
for revision_source in \
'selectedWorkspace["state"]' \
$'workspaceRevision\n .state'; do
seed_fixture
printf '%s\n' "$revision_source" >"$fixture/frontend/src/revision-variant.ts"
commit_fixture revision-variant
expect_rejected "revision structural variant" "frontend/src/revision-variant.ts"
done
seed_fixture
mkdir -p "$fixture/backend/src/workspaces"
printf '%s\n' 'if (revision.state !== "operational") return;' >"$fixture/backend/src/workspaces/registry.ts"
commit_fixture historical-decoder
expect_pass "single exact historical decoder"
printf '%s\n' 'if (revision["state"] === "retired") return;' >>"$fixture/backend/src/workspaces/registry.ts"
commit_fixture extra-historical-branch
expect_rejected "extra registry revision branch" "backend/src/workspaces/registry.ts"
# Binary/NUL policy files are decoded and rejected rather than skipped by git grep -I.
seed_fixture
printf 'WorkspaceV2\0hidden\n' >"$fixture/backend/src/binary.ts"
commit_fixture nul-policy
expect_rejected "NUL policy file" "backend/src/binary.ts"
# Workspace fixture-family discovery is recursive by basename.
seed_fixture
mkdir -p "$fixture/scripts/fixtures/nested/deeper"
write_fixture_descriptor "$fixture/scripts/fixtures/nested/deeper/workspace-registry-nested.yaml" workspace: ' schema_version: 2'
commit_fixture nested-workspace-fixture
expect_rejected "nested workspace fixture" "scripts/fixtures/nested/deeper/workspace-registry-nested.yaml"
# Python bytecode is disabled before pre-gate docs, and release dry-run starts with bootstrap trust.
grep -Fq 'PYTHONDONTWRITEBYTECODE: "1"' "$project_root/.github/workflows/deployment.yml" \
|| fail "workflow does not disable Python bytecode"
grep -Fq 'export PYTHONDONTWRITEBYTECODE=1' "$project_root/scripts/verify-schema-v3-only-release.sh" \
|| fail "release wrapper does not disable Python bytecode"
release_plan="$($gate_bash "$project_root/scripts/verify-schema-v3-only-release.sh" --dry-run)"
first_command="$(printf '%s\n' "$release_plan" | sed -n '1p')"
bootstrap_command="$(printf '%s\n' "$release_plan" | sed -n '4p')"
[[ "$first_command" == 'export PYTHONDONTWRITEBYTECODE=1' ]] \
|| fail "release dry-run does not print the Python bytecode export"
[[ "$bootstrap_command" == '/bin/bash scripts/verify-schema-v3-only.sh --bootstrap-trust-only' ]] \
|| fail "release plan does not bootstrap trust before npm"
printf '%s\n' "$release_plan" | grep -Fq '(cd backend && npm ci --ignore-scripts)' \
|| fail "release plan does not disable npm lifecycle scripts"
py_fixture="$sandbox/python-bytecode"
mkdir -p "$py_fixture/scripts"
printf '%s\n' 'value = 3' >"$py_fixture/scripts/module.py"
PYTHONPATH="$py_fixture" PYTHONDONTWRITEBYTECODE=1 python3 -c 'import scripts.module'
[[ ! -e "$py_fixture/scripts/__pycache__" ]] || fail "pre-gate Python created ignored bytecode"
seed_bootstrap_fixture() {
seed_fixture
mkdir -p "$fixture/.github/workflows"
for required in \
backend/package.json backend/package-lock.json \
backend/scripts/verify-workspace-descriptor-files.mjs \
backend/scripts/verify-workspace-descriptor-files.test.mjs \
backend/scripts/revision-state-policy.mjs \
backend/scripts/revision-state-policy.test.mjs \
backend/scripts/bash-heredoc.mjs \
backend/scripts/revision_state_policy.py \
backend/scripts/test_revision_state_policy.py \
scripts/verify-schema-v3-only.sh scripts/test-verify-schema-v3-only.sh \
scripts/verify-schema-v3-only-release.sh scripts/workspace_descriptor_doc_contract.py \
.github/workflows/deployment.yml; do
mkdir -p "$fixture/${required%/*}"
cp "$project_root/$required" "$fixture/$required"
done
"$real_git" -C "$fixture" add .
"$real_git" -C "$fixture" commit -qm bootstrap-files
}
assert_release_stops_before_npm() {
local label="$1"
fake_lifecycle="$sandbox/fake-lifecycle"
mkdir -p "$fake_lifecycle"
cat >"$fake_lifecycle/npm" <<EOF
#!/usr/bin/env bash
echo invoked >>"$sandbox/npm-invoked"
exit 99
EOF
chmod +x "$fake_lifecycle/npm"
rm -f "$sandbox/npm-invoked"
set +e
PATH="$fake_lifecycle:$PATH" /bin/bash "$fixture/scripts/verify-schema-v3-only-release.sh" >"$output" 2>&1
release_status=$?
set -e
[[ $release_status -ne 0 ]] || fail "$label unexpectedly passed"
[[ ! -e "$sandbox/npm-invoked" ]] || fail "$label invoked npm before bootstrap trust"
}
seed_bootstrap_fixture
printf '%s\n' '# dirty' >>"$fixture/backend/package.json"
assert_release_stops_before_npm "dirty package bootstrap"
seed_bootstrap_fixture
printf '%s\n' '# dirty' >>"$fixture/backend/scripts/verify-workspace-descriptor-files.test.mjs"
assert_release_stops_before_npm "dirty checker test bootstrap"
seed_bootstrap_fixture
printf '%s\n' '// dirty' >>"$fixture/backend/scripts/revision-state-policy.mjs"
assert_release_stops_before_npm "dirty revision policy bootstrap"
seed_bootstrap_fixture
printf '%s\n' '# dirty' >>"$fixture/backend/scripts/revision_state_policy.py"
assert_release_stops_before_npm "dirty Python policy helper bootstrap"
seed_bootstrap_fixture
printf '%s\n' '# dirty' >>"$fixture/scripts/verify-schema-v3-only.sh"
assert_release_stops_before_npm "dirty gate bootstrap"
seed_bootstrap_fixture
rm "$fixture/backend/scripts/verify-workspace-descriptor-files.mjs"
ln -s /dev/null "$fixture/backend/scripts/verify-workspace-descriptor-files.mjs"
assert_release_stops_before_npm "symlink checker bootstrap"
seed_bootstrap_fixture
printf '%s\n' 'scripts/__pycache__/' >"$fixture/.gitignore"
"$real_git" -C "$fixture" add .gitignore
"$real_git" -C "$fixture" commit -qm ignore-rule
mkdir -p "$fixture/scripts/__pycache__"
printf x >"$fixture/scripts/__pycache__/ignored.pyc"
assert_release_stops_before_npm "ignored trusted artifact bootstrap"
seed_bootstrap_fixture
printf x >"$fixture/scripts/untracked-helper.sh"
assert_release_stops_before_npm "untracked helper bootstrap"
seed_bootstrap_fixture
mkfifo "$fixture/scripts/bootstrap-fifo"
assert_release_stops_before_npm "FIFO bootstrap"
seed_bootstrap_fixture
printf '%s\n' unsafe >"$fixture/backend/.npmrc"
assert_release_stops_before_npm "untracked backend npmrc bootstrap"
seed_bootstrap_fixture
global_ignore="$sandbox/global-ignore"
printf '%s\n' backend/.npmrc >"$global_ignore"
"$real_git" -C "$fixture" config core.excludesFile "$global_ignore"
printf '%s\n' unsafe >"$fixture/backend/.npmrc"
assert_release_stops_before_npm "globally ignored backend npmrc bootstrap"
# Dist failures are isolated to fixture roots; canonical backend/dist is never mutated.
run_gate_dist() {
set +e
"$gate_bash" "$gate" --root "$fixture" --runtime-only --check-dist >"$output" 2>&1
gate_status=$?
set -e
}
seed_fixture
mkdir -p "$fixture/backend/dist"
printf '%s\n' server >"$fixture/backend/dist/server.js"
run_gate_dist
[[ $gate_status -eq 1 ]] || fail "fixture missing schema module unexpectedly passed"
grep -Fq 'backend/dist/workspaces/schema.js' "$output" || fail "fixture missing schema path not reported"
seed_fixture
mkdir -p "$fixture/backend/dist/workspaces"
printf '%s\n' schema >"$fixture/backend/dist/workspaces/schema.js"
run_gate_dist
[[ $gate_status -eq 1 ]] || fail "fixture missing server unexpectedly passed"
grep -Fq 'backend/dist/server.js' "$output" || fail "fixture missing server path not reported"
seed_fixture
mkdir -p "$fixture/backend/dist/workspaces"
printf '%s\n' schema >"$fixture/backend/dist/workspaces/schema.js"
printf '%s\n' server >"$fixture/backend/dist/server.js"
printf '%s\n' stale >"$fixture/backend/dist/workspaces/Migrate-Legacy.js"
run_gate_dist
[[ $gate_status -eq 1 ]] || fail "fixture stale migrator unexpectedly passed"
grep -Fq 'backend/dist/workspaces/Migrate-Legacy.js' "$output" || fail "fixture stale migrator path not reported"
[[ "$(cksum <"$canonical_schema")" == "$canonical_schema_checksum" ]] \
|| fail "shell regression mutated canonical compiled schema"
[[ "$(cksum <"$canonical_server")" == "$canonical_server_checksum" ]] \
|| fail "shell regression mutated canonical compiled server"
echo "schema-v3-only absence gate regression tests passed"
+2 -25
View File
@@ -478,31 +478,8 @@ task13_seed_registry() {
task13_run_logged "initialize bare workspace registry" \
git init --bare --initial-branch=main "$TASK13_REMOTE"
task13_run_logged "initialize workspace seed" git -C "$TASK13_SEED" init --initial-branch=main
cat >"$TASK13_SEED/workspaces/task13-smoke.yaml" <<'EOF'
workspace:
schema_version: 3
id: task13-smoke
name: Task 13 Smoke
language: en
dwh:
engine: postgres
database: warehouse
schema: analytics
supported_transports: [postgres_direct]
semantic_index:
vector_store:
engine: qdrant
collection: task13-smoke
dimensions: 1024
distance: cosine
embedding:
provider: ollama_internal
model: qwen3-embedding:0.6b
dimensions: 1024
llm_policy:
default: local-qwen/task13-smoke
allowed: [local-qwen/task13-smoke]
EOF
cp "$TASK13_ROOT/scripts/fixtures/workspace-registry-task13.yaml" \
"$TASK13_SEED/workspaces/task13-smoke.yaml"
task13_run_logged "commit initial workspace" git -C "$TASK13_SEED" add workspaces/task13-smoke.yaml
task13_run_logged "commit initial workspace" git -C "$TASK13_SEED" \
-c user.name='Task 13 Smoke' -c user.email='task13-smoke@example.invalid' \
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Reproducible schema-v3-only release gate. The Git checkout is the trust root;
# dependencies come from backend/package-lock.json and dist comes from a clean build.
set -euo pipefail
export PYTHONDONTWRITEBYTECODE=1
export NPM_CONFIG_USERCONFIG=/dev/null
export NPM_CONFIG_GLOBALCONFIG=/dev/null
root="$(cd "$(dirname "$0")/.." && pwd -P)"
if [[ ${1:-} == --dry-run ]]; then
cat <<'EOF'
export PYTHONDONTWRITEBYTECODE=1
export NPM_CONFIG_USERCONFIG=/dev/null
export NPM_CONFIG_GLOBALCONFIG=/dev/null
/bin/bash scripts/verify-schema-v3-only.sh --bootstrap-trust-only
(cd backend && npm ci --ignore-scripts)
(cd backend && npm run build)
(cd backend && npm run test:schema-v3-verifier)
/bin/bash scripts/test-verify-schema-v3-only.sh
/bin/bash scripts/verify-schema-v3-only.sh
EOF
exit 0
fi
[[ $# -eq 0 ]] || { echo "usage: $0 [--dry-run]" >&2; exit 2; }
/bin/bash "$root/scripts/verify-schema-v3-only.sh" --bootstrap-trust-only
(cd "$root/backend" && npm ci --ignore-scripts)
(cd "$root/backend" && npm run build)
(cd "$root/backend" && npm run test:schema-v3-verifier)
/bin/bash "$root/scripts/test-verify-schema-v3-only.sh"
/bin/bash "$root/scripts/verify-schema-v3-only.sh"
+278
View File
@@ -0,0 +1,278 @@
#!/usr/bin/env bash
# Fail-closed absence gate for the supported schema-v3-only workspace runtime.
set -euo pipefail
shopt -s nocasematch
script_root="$(cd "$(dirname "$0")/.." && pwd -P)"
root_argument="$script_root"
root_was_selected=0
runtime_only=0
check_dist=0
bootstrap_trust_only=0
usage() {
cat >&2 <<EOF
usage: $0 [--bootstrap-trust-only [--root REPOSITORY]]
$0 [--runtime-only [--root REPOSITORY] [--check-dist]]
Default mode trusts the canonical Git checkout, runs a clean backend build, and
then validates generated output and documentation. Runtime-only uses the trusted
canonical Node installation, dependencies, verifier, and built
backend/dist/workspaces/schema.js; --root is only for isolated Git fixtures.
--check-dist makes an isolated runtime fixture provide backend/dist/schema.js and
server.js and checks it for stale migrators. Full mode never accepts overrides.
Expandable deployment blocks are trusted only by repository-relative path plus the
SHA-256 of their exact raw opener/body/closer bytes in the Node verifier. This is
an exact-content trust exception with rationale metadata, not semantic proof.
Release trust anchor and order (durable entry point):
Git index/filesystem trust is established by --bootstrap-trust-only before any
checkout-controlled helper or npm lifecycle can run. CI supplies a clean Git
checkout and performs an inline clean-checkout assertion before the wrapper.
scripts/verify-schema-v3-only-release.sh then runs npm ci --ignore-scripts from the trusted
backend/package-lock.json; clean build; npm Node verifier test; Bash regression;
and the full schema-v3-only gate, which repeats trust checks.
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
--root) [[ $# -ge 2 ]] || { usage; exit 2; }; root_argument="$2"; root_was_selected=1; shift 2 ;;
--runtime-only) runtime_only=1; shift ;;
--check-dist) check_dist=1; shift ;;
--bootstrap-trust-only) bootstrap_trust_only=1; shift ;;
-h|--help) usage; exit 0 ;;
*) printf 'unknown argument: %s\n' "$1" >&2; usage; exit 2 ;;
esac
done
[[ $root_was_selected -eq 0 || $runtime_only -eq 1 || $bootstrap_trust_only -eq 1 ]] || { echo "--root is available only with --runtime-only or --bootstrap-trust-only" >&2; exit 2; }
[[ $runtime_only -eq 0 || $bootstrap_trust_only -eq 0 ]] || { echo "--runtime-only and --bootstrap-trust-only are mutually exclusive" >&2; exit 2; }
[[ $check_dist -eq 0 || $runtime_only -eq 1 ]] || { echo "--check-dist is available only with --runtime-only" >&2; exit 2; }
if ! root="$(cd "$root_argument" 2>/dev/null && pwd -P)"; then
printf 'repository root is not accessible: %q\n' "$root_argument" >&2
exit 2
fi
[[ $runtime_only -eq 1 || $bootstrap_trust_only -eq 1 || "$root" == "$script_root" ]] || { echo "full mode is restricted to the canonical repository" >&2; exit 2; }
tmp="$(mktemp -d "${TMPDIR:-/tmp}/thoth-v3-gate.XXXXXX")"
trap 'rm -rf "$tmp"' EXIT HUP INT TERM
if git_top="$(git -C "$root" rev-parse --show-toplevel 2>"$tmp/rev-parse")"; then :; else
status=$?; printf 'Git repository discovery failed for %q\n' "$root" >&2; cat "$tmp/rev-parse" >&2; exit "$status"
fi
canonical_git_top="$(cd "$git_top" && pwd -P)"
[[ "$canonical_git_top" == "$root" ]] || { printf '%s\n' "--root must name the canonical Git root" >&2; exit 2; }
for npmrc in .npmrc backend/.npmrc; do
if [[ -e "$root/$npmrc" || -L "$root/$npmrc" ]]; then
printf 'npm configuration node forbidden: %q\n' "$npmrc" >&2
exit 1
fi
done
policy_roots=(backend/src frontend/src backend/scripts scripts)
trust_roots=(backend/src frontend/src backend/scripts scripts deploy/workspaces)
print_path() { printf '%q' "$1"; }
fail_path() { local message="$1" path="$2"; printf '%s: ' "$message" >&2; print_path "$path" >&2; printf '\n' >&2; return 1; }
forbidden_module_stems='deprecated-v2-descriptor|migrate-legacy|migrate-v2-qdrant'
is_deleted_basename() {
[[ "${1##*/}" =~ ^($forbidden_module_stems)(\..*)?$ ]]
}
# Exact path + category exceptions only. They remain fully subject to trust checks.
is_allowed_match() {
local category="$1" path="$2"
case "$category:$path" in
prescribed-symbol:scripts/verify-schema-v3-only.sh|prescribed-symbol:scripts/test-verify-schema-v3-only.sh|prescribed-symbol:backend/scripts/verify-workspace-descriptor-files.mjs|prescribed-symbol:backend/scripts/verify-workspace-descriptor-files.test.mjs|prescribed-symbol:backend/scripts/revision-state-policy.mjs|prescribed-symbol:backend/scripts/revision-state-policy.test.mjs|prescribed-symbol:backend/scripts/bash-heredoc.mjs|prescribed-symbol:backend/scripts/revision_state_policy.py|prescribed-symbol:backend/scripts/test_revision_state_policy.py) return 0 ;;
legacy-workspace:scripts/verify-schema-v3-only.sh|legacy-workspace:scripts/test-verify-schema-v3-only.sh|legacy-workspace:backend/scripts/verify-workspace-descriptor-files.mjs|legacy-workspace:backend/scripts/verify-workspace-descriptor-files.test.mjs|legacy-workspace:backend/scripts/revision-state-policy.mjs|legacy-workspace:backend/scripts/revision-state-policy.test.mjs|legacy-workspace:backend/scripts/bash-heredoc.mjs|legacy-workspace:backend/scripts/revision_state_policy.py|legacy-workspace:backend/scripts/test_revision_state_policy.py) return 0 ;;
migration-marker:scripts/verify-schema-v3-only.sh|migration-marker:scripts/test-verify-schema-v3-only.sh|migration-marker:backend/scripts/verify-workspace-descriptor-files.mjs|migration-marker:backend/scripts/verify-workspace-descriptor-files.test.mjs|migration-marker:backend/scripts/revision-state-policy.mjs|migration-marker:backend/scripts/revision-state-policy.test.mjs|migration-marker:backend/scripts/bash-heredoc.mjs|migration-marker:backend/scripts/revision_state_policy.py|migration-marker:backend/scripts/test_revision_state_policy.py) return 0 ;;
migration-marker:scripts/workspace_descriptor_doc_contract.py|migration-marker:scripts/test_workspace_descriptor_doc_contract.py) return 0 ;;
migration-marker:backend/scripts/clean-dist.test.mjs) return 0 ;;
*) return 1 ;;
esac
}
# Common policy is defined once and scanned over every policy root. Revision-state is the sole layer.
prescribed_symbol_forbidden='WorkspaceV1|WorkspaceV2|DeprecatedV2Descriptor|LegacyMigrationResult|LegacyMigrationOptions|WorkspaceV2MigrationInput|migrateLegacyWorkspace|writeMigratedWorkspace|migrateWorkspaceV1ToV2|migrateWorkspaceV2ToV3'
legacy_workspace_forbidden='LegacyWorkspace'
migration_marker_forbidden="migration_required|($forbidden_module_stems)"
require_category_absent() {
local category="$1" sensitivity="$2" pattern="$3" output="$tmp/grep-$1" status path
if [[ "$sensitivity" == insensitive ]]; then
if git -C "$root" grep -z -l -I -i -E -e "$pattern" -- "${policy_roots[@]}" >"$output" 2>"$output.err"; then status=0; else status=$?; fi
else
if git -C "$root" grep -z -l -I -E -e "$pattern" -- "${policy_roots[@]}" >"$output" 2>"$output.err"; then status=0; else status=$?; fi
fi
case "$status" in
0)
while IFS= read -r -d '' path; do
is_allowed_match "$category" "$path" && continue
printf 'forbidden %s match: ' "$category" >&2; print_path "$path" >&2; printf '\n' >&2
return 1
done <"$output"
;;
1) : ;;
*) printf 'scanner failure (%s): %s\n' "$status" "$category" >&2; cat "$output.err" >&2; return "$status" ;;
esac
}
# One batched index inventory validates modes and working-tree presence for all tracked paths.
index_entries="$tmp/index"
if git -C "$root" ls-files -s -z -- "${trust_roots[@]}" >"$index_entries" 2>"$tmp/index.err"; then :; else
status=$?; echo "tracked index scan failed ($status)" >&2; cat "$tmp/index.err" >&2; exit "$status"
fi
tracked_paths="$tmp/tracked"
: >"$tracked_paths"
while IFS= read -r -d '' record; do
metadata="${record%%$'\t'*}"; path="${record#*$'\t'}"; mode="${metadata%% *}"
case "$mode" in 100*) ;; 120000) fail_path "tracked symlink forbidden" "$path"; exit 1 ;; *) fail_path "non-regular tracked entry forbidden" "$path"; exit 1 ;; esac
printf '%s\0' "$path" >>"$tracked_paths"
[[ -f "$root/$path" && ! -L "$root/$path" ]] || { fail_path "tracked file missing or unsafe" "$path"; exit 1; }
is_deleted_basename "$path" && { fail_path "deleted source basename remains tracked" "$path"; exit 1; }
done <"$index_entries"
# Filesystem node trust has no test/fixture exclusions.
filesystem="$tmp/filesystem"
if find "${trust_roots[@]/#/$root/}" -mindepth 1 -print0 >"$filesystem" 2>"$tmp/find.err"; then :; else
status=$?; echo "filesystem trust scan failed ($status)" >&2; cat "$tmp/find.err" >&2; exit "$status"
fi
while IFS= read -r -d '' absolute; do
path="${absolute#"$root/"}"
[[ ! -L "$absolute" ]] || { fail_path "symlink forbidden in trusted root" "$path"; exit 1; }
[[ -d "$absolute" || -f "$absolute" ]] || { fail_path "non-directory/non-regular node forbidden in trusted root" "$path"; exit 1; }
is_deleted_basename "$path" && { fail_path "deleted source basename remains on filesystem" "$path"; exit 1; }
done <"$filesystem"
reject_name_list() {
local label="$1" file="$2" path
while IFS= read -r -d '' path; do fail_path "$label" "$path"; return 1; done <"$file"
}
for spec in "untracked:--others --exclude-standard" "ignored:--others --ignored --exclude-standard"; do
label="${spec%%:*}"; options="${spec#*:}"; output="$tmp/$label"
# shellcheck disable=SC2086
if git -C "$root" ls-files -z $options -- "${trust_roots[@]}" >"$output" 2>"$output.err"; then :; else
status=$?; echo "$label scan failed ($status)" >&2; cat "$output.err" >&2; exit "$status"
fi
reject_name_list "$label file in trusted root" "$output" || exit 1
done
for mode in worktree cached; do
output="$tmp/dirty-$mode"
if [[ "$mode" == cached ]]; then command=(git -C "$root" diff --cached --name-only -z --); else command=(git -C "$root" diff --name-only -z --); fi
if "${command[@]}" "${trust_roots[@]}" >"$output" 2>"$output.err"; then :; else
status=$?; echo "$mode dirty scan failed ($status)" >&2; cat "$output.err" >&2; exit "$status"
fi
reject_name_list "modified trusted file ($mode)" "$output" || exit 1
done
require_trusted_files_at() {
local repository="$1"; shift
local listing="$tmp/explicit-$RANDOM" record metadata path mode expected
if git -C "$repository" ls-files -s -z -- "$@" >"$listing" 2>"$listing.err"; then :; else
status=$?; echo "explicit trust index scan failed ($status)" >&2; cat "$listing.err" >&2; return "$status"
fi
: >"$listing.paths"
while IFS= read -r -d '' record; do
metadata="${record%%$'\t'*}"; path="${record#*$'\t'}"; mode="${metadata%% *}"
case "$mode" in 100*) ;; *) fail_path "required file is not regular in index" "$path"; return 1 ;; esac
printf '%s\n' "$path" >>"$listing.paths"
done <"$listing"
for expected in "$@"; do
[[ -f "$repository/$expected" && ! -L "$repository/$expected" ]] || { fail_path "required trusted file missing or unsafe" "$expected"; return 1; }
grep -Fqx -- "$expected" "$listing.paths" || { fail_path "required file is not tracked" "$expected"; return 1; }
done
git -C "$repository" diff --quiet -- "$@" || { echo "required trust files are dirty" >&2; return 1; }
git -C "$repository" diff --quiet --cached -- "$@" || { echo "required trust files are staged dirty" >&2; return 1; }
}
# Bootstrap uses only Git/filesystem primitives. It must precede every npm or
# checkout-controlled helper in the durable release wrapper.
bootstrap_files=(
backend/package.json backend/package-lock.json
backend/scripts/verify-workspace-descriptor-files.mjs
backend/scripts/verify-workspace-descriptor-files.test.mjs
backend/scripts/revision-state-policy.mjs
backend/scripts/revision-state-policy.test.mjs
backend/scripts/bash-heredoc.mjs
backend/scripts/revision_state_policy.py
backend/scripts/test_revision_state_policy.py
scripts/verify-schema-v3-only.sh scripts/test-verify-schema-v3-only.sh
scripts/verify-schema-v3-only-release.sh .github/workflows/deployment.yml
scripts/workspace_descriptor_doc_contract.py
)
if [[ $bootstrap_trust_only -eq 1 ]]; then
require_trusted_files_at "$root" "${bootstrap_files[@]}"
echo "schema-v3-only bootstrap trust passed"
exit 0
fi
# Runtime fixtures execute only canonical trusted verifier code and dependencies.
require_trusted_files_at "$script_root" \
backend/scripts/verify-workspace-descriptor-files.mjs \
backend/scripts/revision-state-policy.mjs \
backend/scripts/bash-heredoc.mjs \
backend/scripts/revision_state_policy.py \
backend/package.json backend/package-lock.json \
scripts/verify-schema-v3-only.sh
workspace_verifier="$script_root/backend/scripts/verify-workspace-descriptor-files.mjs"
workspace_schema="$script_root/backend/dist/workspaces/schema.js"
if [[ $runtime_only -eq 0 ]]; then
require_trusted_files_at "$root" \
scripts/verify-schema-v3-only-release.sh .github/workflows/deployment.yml
(cd "$root/backend" && npm run build)
fi
[[ -f "$workspace_schema" && ! -L "$workspace_schema" ]] || { echo "trusted compiled schema module is missing or unsafe: backend/dist/workspaces/schema.js" >&2; exit 1; }
workspace_manifest="$tmp/workspace-manifest"
: >"$workspace_manifest"
while IFS= read -r -d '' path; do
case "$path" in
backend/src/*|frontend/src/*|backend/scripts/*|scripts/*) printf '%s\0%s\0' policy_text "$path" >>"$workspace_manifest" ;;
esac
kind=""
case "$path" in
deploy/workspaces/preprocess-dwh.yaml|deploy/workspaces/preprocess-evidence.yaml|deploy/workspaces/server-sessions.yaml.example) ;;
deploy/workspaces/*.yaml|deploy/workspaces/*.yml|deploy/workspaces/*.yaml.example|deploy/workspaces/*.yml.example|scripts/fixtures/workspace-registry-*.yaml|scripts/fixtures/workspace-registry-*.yml|scripts/fixtures/*/workspace-registry-*.yaml|scripts/fixtures/*/workspace-registry-*.yml) kind=workspace_descriptor ;;
scripts/*.sh|scripts/*.ps1)
case "$path" in scripts/verify-schema-v3-only.sh|scripts/test-verify-schema-v3-only.sh) ;; *) kind=deployment_script ;; esac
;;
esac
[[ -z "$kind" ]] || printf '%s\0%s\0' "$kind" "$path" >>"$workspace_manifest"
done <"$tracked_paths"
node "$workspace_verifier" --root "$root" --manifest "$workspace_manifest"
require_category_absent prescribed-symbol insensitive "$prescribed_symbol_forbidden"
require_category_absent legacy-workspace sensitive "$legacy_workspace_forbidden"
require_category_absent migration-marker insensitive "$migration_marker_forbidden"
check_dist_tree() {
local dist_root="$1" entries="$tmp/dist" absolute path
[[ -f "$dist_root/backend/dist/workspaces/schema.js" && ! -L "$dist_root/backend/dist/workspaces/schema.js" ]] || { echo "expected compiled schema module is missing or unsafe: backend/dist/workspaces/schema.js" >&2; return 1; }
[[ -f "$dist_root/backend/dist/server.js" && ! -L "$dist_root/backend/dist/server.js" ]] || { echo "expected compiled backend output is missing or unsafe: backend/dist/server.js" >&2; return 1; }
find "$dist_root/backend/dist" -mindepth 1 -print0 >"$entries"
while IFS= read -r -d '' absolute; do
path="${absolute#"$dist_root/"}"
if is_deleted_basename "$path"; then
fail_path "stale compiled workspace migrator output exists" "$path"
return 1
fi
done <"$entries"
}
[[ $runtime_only -eq 1 && $check_dist -eq 0 ]] || check_dist_tree "$root"
if [[ $runtime_only -eq 0 ]]; then
require_trusted_files_at "$root" \
scripts/workspace_descriptor_doc_contract.py README.md PROJECT_STATE.md \
docs/install/local-workspace-registry.md docs/install/server-workspace-registry.md \
docs/workspace-diagnostic-protocol.md
"$root/scripts/workspace_descriptor_doc_contract.py" \
--document "$root/README.md" --project-state "$root/PROJECT_STATE.md" \
--document "$root/docs/install/local-workspace-registry.md" \
--document "$root/docs/install/server-workspace-registry.md" \
--document "$root/docs/workspace-diagnostic-protocol.md"
fi
echo "schema-v3-only absence gate passed"