diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index f8b26a9b..0a3e6041 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -35,8 +35,12 @@ cache in `embedding-models`, and `embedding-model-init` blocks `core` until `qwen3-embedding:0.6b` is present. - **Semantic contract.** Internal semantic indexing is fixed to `qwen3-embedding:0.6b`, - `1024` dimensions, and cosine distance. Schema-v3 descriptors are operational; schema-v1/v2 descriptors remain `migration_required` until an explicit reviewed migration writes schema version 3. One workspace owns one Qdrant collection, and schema, Evidence, and Memory records - coexist inside that collection with payload `kind` separation. + `1024` dimensions, and cosine distance. Schema v3 is the only accepted workspace descriptor + format. Schema v1 and v2 descriptors are rejected while a candidate snapshot is validated, so + activation or a pull fails atomically and leaves the prior valid snapshot active; there is no + in-product migrator or automatic conversion. One workspace owns one Qdrant collection, and + schema, Evidence, and Memory records coexist inside that collection with payload `kind` + separation. - **Final review runtime barriers.** Operational routes, retained session pins, and runtime rendering now require schema version 3 before resolving bindings, readiness, diagnostics, or Pi. Session admission verifies the exact internal Qdrant collection (dimensions, cosine @@ -53,11 +57,13 @@ archives exactly one labeled `_qdrant-data` volume and preserves the prior `qdrant` running state. `./scripts/vector-restore.sh --project-name --input --confirm-project ` requires the exact repeated project confirmation, validates manifest - and archive safety before stopping `qdrant`, stages rollback content, restores in place, and - restarts `qdrant` only if it was previously running. Restore does not migrate legacy workspace - descriptors, rename collections, or repair a semantic-index incompatibility. Backup and restore - share one atomic Docker-daemon lock per Compose project/Qdrant volume; contenders fail before - volume resolution, and cleanup removes the lock only when its ownership labels still match. + and archive safety before stopping `qdrant`, stages rollback content, restores semantic storage + in place, and restarts `qdrant` only if it was previously running. Recovery requires the registry + to already hold a reviewed v3 descriptor revision compatible with the restored collection; the + helper does not restore descriptors, rename collections, or repair a semantic-index + incompatibility. Backup and restore share one atomic Docker-daemon lock per Compose + project/Qdrant volume; contenders fail before volume resolution, and cleanup removes the lock + only when its ownership labels still match. - **Verification recorded for Task 13 final audit.** On Apple M4 Pro (`Darwin 25.5.0`, Docker Server `29.6.2 linux/arm64`), harness pytest passed **827 passed / 4 deselected**; backend Vitest passed **477/477** plus TypeScript and build; @@ -85,9 +91,8 @@ Windows Docker Desktop startup were not manually executed in this run. - **Task 13 known limitations.** Broad harness Ruff remains existing unrelated debt (**220 errors**); touched harness files were verified Ruff-clean. The final active-reference - audit remains non-empty only in categorized legacy parser/migration compatibility, legacy - descriptor/config fixtures, deterministic negative guards, retained off-repository migration - SQL, L2 legacy fixtures, gitignored task notes, and historical reference notes. No active + audit remains non-empty only in deterministic negative guards, retained off-repository migration + SQL, L2 compatibility fixtures, gitignored task notes, and historical reference notes. No active schema-v3 operator manual or supported runtime deployment path retains external vector or embedding endpoint coupling. - **Final review fix verification.** Backend Vitest passed **477/477** plus TypeScript and build; diff --git a/README.md b/README.md index a1e0d6d2..847a87d2 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ diagnostics are exposed by `tht doctor` and do not prevent the UI from starting. Workspace descriptors are shared through a validated Git repository while endpoint bindings and secret files remain installation-local. Use the [local Mac/PC installation manual](docs/install/local-workspace-registry.md) for Docker Desktop or a local engine, and the [server installation manual](docs/install/server-workspace-registry.md) -for the Gitea, reverse-proxy, backup, migration, and recovery workflow. The isolated deployment +for the Gitea, reverse-proxy, backup, upgrade, and recovery workflow. The isolated deployment exercise is `./scripts/workspace-registry-smoke.sh`; both manuals are checked with `./scripts/verify-workspace-install-docs.sh --profile local` or `--profile server`. @@ -70,10 +70,12 @@ every revision referenced by an open, closed, or failed unarchived session. It r single local installation list or from a server administrator's complete session list, never from a remote user's partial list. -Schema-v3 is the operational descriptor contract. Schema-v1/v2 descriptors remain -`migration_required` until an explicit reviewed migration writes schema version 3. One workspace -owns one Qdrant collection; schema, Evidence, and Memory records share that collection and stay -separated by indexed payload `kind`. +Schema v3 is the only accepted workspace descriptor format. Schema v1 and v2 descriptors are +rejected while a candidate snapshot is validated, so activation or a pull fails atomically and the +prior valid snapshot remains active. There is no in-product migrator or automatic conversion. A +repository must already contain reviewed v3 descriptors. One workspace owns one Qdrant collection; +schema, Evidence, and Memory records share that collection and stay separated by indexed payload +`kind`. Connector `ssh_tunnel` bindings are diagnostic-only in this release: their bounded probe always cleans up the loopback forward and returns `workspace_not_activatable`; session creation is rejected @@ -228,10 +230,11 @@ Compose project name by passing `--confirm-project`: The restore script stops `qdrant`, validates the exact labeled target, stages the current volume contents for rollback, extracts the requested archive into the volume, and then returns the -service to its prior running state. After restore, run the backend health checks and a known -retrieval query before reopening write traffic. Restore does not migrate schema-v1/v2 workspace -descriptors, does not rename collections, and does not reconcile an incompatible collection -contract; those remain explicit reviewed recovery steps outside the helper. +service to its prior running state. It restores semantic storage only. Before reopening write +traffic, the workspace registry must already be at a reviewed v3 descriptor revision compatible +with the restored collection; then run backend health checks and a known retrieval query. The +helper does not restore descriptors, rename collections, or reconcile an incompatible collection +contract. ## Production trust boundary and secrets diff --git a/docs/install/local-workspace-registry.md b/docs/install/local-workspace-registry.md index 65b41806..fc2511de 100644 --- a/docs/install/local-workspace-registry.md +++ b/docs/install/local-workspace-registry.md @@ -213,8 +213,11 @@ Use `POST /workspace-registry/pull` to fetch later revisions. Run workspace diag required DWH bindings are mounted. Schema-v3 diagnostics probe the internal Qdrant/Ollama services through backend config; ordinary diagnostics are read-only. -Schema-v3 is the only operational descriptor format. Schema-v1/v2 descriptors remain -`migration_required` until an explicit reviewed migration writes schema version 3. One workspace owns one Qdrant collection; schema, Evidence, and Memory records share that collection and remain +Schema v3 is the only accepted workspace descriptor format. Schema v1 and v2 descriptors are +rejected while the candidate snapshot is validated, so bootstrap activation or a pull fails +atomically and leaves the prior active snapshot unchanged. There is no in-product migrator or +automatic conversion. The repository must already contain reviewed v3 descriptors. One workspace +owns one Qdrant collection; schema, Evidence, and Memory records share that collection and remain isolated by payload `kind`. ## Semantic index ownership contract @@ -223,16 +226,9 @@ isolated by payload `kind`. | --- | --- | --- | | Workspace semantic index | Each workspace reserves a single Qdrant collection. | Schema, Evidence, and Memory stay in that one collection and remain isolated by payload `kind`. | -To migrate an existing legacy descriptor, create/clone an empty private remote, set the absolute -`THT_SOURCE_ROOT`, transform with absolute paths, review the schema-v1 result, explicitly produce -the reviewed schema-v3 contract, then commit/push. The transformer never imports `${ENV}` values -or secrets. - -```sh -THT_SOURCE_ROOT=/absolute/path/to/ThothII -npm --prefix "$THT_SOURCE_ROOT/backend" run build -node "$THT_SOURCE_ROOT/backend/dist/workspaces/migrate-legacy.js" --input /absolute/path/legacy.yaml --output /absolute/path/thoth-workspaces -``` +If source material needs conversion, perform it outside ThothII in a separate reviewed process. +Commit only the resulting reviewed v3 descriptors. That external process must not import `${ENV}` +values, secret values, certificates, keys, or secret files into the repository. ## Publish, update, backup, outage recovery, and rollback diff --git a/docs/install/server-workspace-registry.md b/docs/install/server-workspace-registry.md index 6b3bc043..2c72c9a8 100644 --- a/docs/install/server-workspace-registry.md +++ b/docs/install/server-workspace-registry.md @@ -60,9 +60,9 @@ use `ssh://git@git.example.invalid/platform/thoth-workspaces.git`. For HTTPS, cr machine credential in the secret manager and mount the Gitea/private CA separately. Never use a Gitea admin credential in the application. -Bootstrap an empty remote from a temporary review clone: migrate legacy descriptors, review their -schema-v3 identity and generated artifacts, commit, and push `main`. The running server is not an -authoring environment for migration. +Bootstrap an empty remote from a temporary review clone only after its canonical v3 descriptors +and generated public artifacts have been reviewed; commit and push `main`. The running server is +not a descriptor authoring or conversion environment. ## Curator flow for shared-registry Evidence @@ -258,14 +258,16 @@ For upgrades, record active status/head, finish active work, use the documented --check-only`, deploy the compatible image through `thothctl`, verify health/status, then resume proxy traffic. -For legacy descriptor migration, use a temporary review clone and the legacy transformer with absolute paths. -Its schema-v1 output is `migration_required`; explicitly supply collection identity, diagnostics, -and the reviewed v3 contract before commit. Never import `${ENV}` values or -copy secret files. +Schema v3 is the only accepted workspace descriptor format. Schema v1 and v2 descriptors are +rejected while the candidate snapshot is validated, so initial activation or a pull fails +atomically and leaves the prior active snapshot unchanged. There is no in-product migrator or +automatic conversion. The repository must already contain reviewed v3 descriptors. One workspace +owns one Qdrant collection; schema, Evidence, and Memory records share it and stay separated by +payload `kind`. -Schema-v3 is the only operational descriptor contract. Schema-v1/v2 descriptors remain -`migration_required` until an explicit reviewed migration writes version 3. One workspace owns one Qdrant collection; schema, Evidence, and Memory records share it and stay separated by payload -`kind`. +If source material needs conversion, perform it outside ThothII in a separate reviewed process. +Commit only the resulting reviewed v3 descriptors. That external process must not import `${ENV}` +values, secret values, certificates, keys, or secret files into the repository. ## Semantic index ownership contract @@ -312,9 +314,10 @@ Use the repository helpers for Qdrant backup/restore: Qdrant backup/restore targets exactly one labeled `qdrant-data` volume for the named Compose project. Restore requires the exact repeated project confirmation, validates the archive before -stopping `qdrant`, stages rollback content, and restores in place only for that project-scoped -volume. It does not migrate schema-v1/v2 workspaces, rename collections, or resolve semantic-index -incompatibilities. +stopping `qdrant`, stages rollback content, and restores semantic storage in place only for that +project-scoped volume. Before recovery, the registry must already contain a reviewed v3 descriptor +revision compatible with the restored collection. The helper does not restore descriptors, rename +collections, or resolve semantic-index incompatibilities. The Ollama model cache is a recoverable local cache, not the canonical semantic source of truth. You may back up `embedding-models` for faster offline recovery, but a cache loss is recoverable by diff --git a/docs/workspace-diagnostic-protocol.md b/docs/workspace-diagnostic-protocol.md index e057f934..9cba3bf1 100644 --- a/docs/workspace-diagnostic-protocol.md +++ b/docs/workspace-diagnostic-protocol.md @@ -7,9 +7,10 @@ response body belongs in the descriptor, generated `.env.example` files, or diag ## Scope and safety rules -- The operational descriptor is schema version 3. -- Schema-v1/v2 descriptors are readable only and remain `migration_required` until an explicit - reviewed migration writes schema version 3. +- Schema v3 is the only accepted workspace descriptor format. +- Schema v1 and v2 descriptors are rejected before diagnostics run. There is no in-product + migrator or automatic conversion; the Git repository must already contain reviewed v3 + descriptors. - One workspace owns one Qdrant collection. - Qdrant and Ollama are internal services. Operators do not bind external vector or embedding transports for active manuals or supported diagnostics. diff --git a/scripts/test-verify-workspace-install-docs.sh b/scripts/test-verify-workspace-install-docs.sh index bd7106bf..1a9073df 100755 --- a/scripts/test-verify-workspace-install-docs.sh +++ b/scripts/test-verify-workspace-install-docs.sh @@ -214,7 +214,7 @@ cat >"$project_state_positive" <<'EOF' ## Internal Qdrant + Ollama semantic infrastructure — LIVE 2026-08-08 -- Schema-v3 descriptors are operational and v1/v2 remain `migration_required`. +- Schema v3 is the only accepted workspace descriptor format. Schema v1 and v2 descriptors are rejected before activation or diagnostics. - One workspace owns one Qdrant collection. - Only DWH and LLM remain external runtime application endpoints. - The internal stack includes `qdrant`, `embedding`, and `embedding-model-init`. @@ -223,10 +223,146 @@ cat >"$project_state_positive" <<'EOF' ### Historical snapshot — previous deployment -- Older notes intentionally live only here. +- Schema-v2 descriptors were operational and returned `migration_required`. +- Operators used `migrate-legacy` in this superseded workflow. EOF verify_project_state_current_contract "$project_state_positive" positive-project-state >/dev/null +project_state_unrelated_migration="$negative_root/project-state-unrelated-migration.md" +python3 - "$project_state_positive" "$project_state_unrelated_migration" <<'PY' +import pathlib, sys +source = pathlib.Path(sys.argv[1]).read_text() +old = "rejected before activation or diagnostics.\n- One workspace" +new = ( + "rejected before activation or diagnostics. The unrelated session database\n" + " upgrade may report `migration_required`.\n- One workspace" +) +if source.count(old) != 1: + raise SystemExit("PROJECT_STATE positive fixture insertion point not found") +pathlib.Path(sys.argv[2]).write_text(source.replace(old, new, 1)) +PY +verify_project_state_current_contract "$project_state_unrelated_migration" \ + unrelated-current-project-state >/dev/null + +generic_unrelated_migration="$negative_root/generic-unrelated-migration.md" +python3 - "$root/docs/install/local-workspace-registry.md" "$generic_unrelated_migration" <<'PY' +import pathlib, sys +source = pathlib.Path(sys.argv[1]).read_text() +old = "isolated by payload `kind`.\n\n## Semantic index ownership contract" +new = ( + "isolated by payload `kind`. The unrelated session database\n" + "upgrade may report `migration_required`.\n\n## Semantic index ownership contract" +) +if source.count(old) != 1: + raise SystemExit("manual positive fixture insertion point not found") +pathlib.Path(sys.argv[2]).write_text(source.replace(old, new, 1)) +PY +verify_workspace_descriptor_doc_contract "$generic_unrelated_migration" \ + unrelated-current-manual >/dev/null + +assert_schema_contract_rejected() { + local source="$1" label="$2" expected="$3" output="$negative_root/schema-contract-output" + set +e + verify_workspace_descriptor_doc_contract "$source" "$label" >"$output" 2>&1 + local status=$? + set -e + if [[ $status -eq 0 ]] || ! grep -Fq "$expected" "$output"; then + echo "$label fixture was not rejected correctly" >&2 + cat "$output" >&2 + exit 1 + fi +} + +project_state_migration_required="$negative_root/project-state-migration-required.md" +python3 - "$project_state_positive" "$project_state_migration_required" <<'PY' +import pathlib, sys +source = pathlib.Path(sys.argv[1]).read_text() +marker = source.index("## Historical snapshots") +pathlib.Path(sys.argv[2]).write_text( + source[:marker] + + "- Schema v1 and v2 descriptors remain\n" + + " `migration_required`.\n\n" + + source[marker:] +) +PY +set +e +verify_project_state_current_contract "$project_state_migration_required" current-migration-required >"$project_state_output" 2>&1 +project_state_status=$? +set -e +if [[ $project_state_status -eq 0 ]] || ! grep -Fq "migration_required" "$project_state_output"; then + echo "current PROJECT_STATE migration_required fixture was not rejected correctly" >&2 + cat "$project_state_output" >&2 + exit 1 +fi + +project_state_readable="$negative_root/project-state-readable.md" +python3 - "$project_state_positive" "$project_state_readable" <<'PY' +import pathlib, sys +source = pathlib.Path(sys.argv[1]).read_text() +source = source.replace( + "Schema v1 and v2 descriptors are rejected before activation or diagnostics.", + "Schema v1 and v2 descriptors remain readable for operational use.", + 1, +) +pathlib.Path(sys.argv[2]).write_text(source) +PY +set +e +verify_project_state_current_contract "$project_state_readable" current-readable-v1-v2 >"$project_state_output" 2>&1 +project_state_status=$? +set -e +if [[ $project_state_status -eq 0 ]] || ! grep -Eq "rejected|readable|operational" "$project_state_output"; then + echo "current PROJECT_STATE v1/v2 readability fixture was not rejected correctly" >&2 + cat "$project_state_output" >&2 + exit 1 +fi + +manual_migration_required="$negative_root/manual-migration-required.md" +cp "$root/docs/install/local-workspace-registry.md" "$manual_migration_required" +printf '\nSchema v1 and v2 descriptors remain\n`migration_required`.\n' \ + >>"$manual_migration_required" +assert_schema_contract_rejected "$manual_migration_required" manual-migration-required "migration_required" + +manual_migrate_legacy="$negative_root/manual-migrate-legacy.md" +cp "$root/docs/install/local-workspace-registry.md" "$manual_migrate_legacy" +printf '\nRun `node backend/dist/workspaces/migrate-legacy.js` before activation.\n' >>"$manual_migrate_legacy" +assert_schema_contract_rejected "$manual_migrate_legacy" manual-migrate-legacy "migrate-legacy" + +manual_legacy_procedure="$negative_root/manual-legacy-procedure.md" +cp "$root/docs/install/server-workspace-registry.md" "$manual_legacy_procedure" +printf '\nMigrate legacy descriptors in a temporary review clone before activation.\n' >>"$manual_legacy_procedure" +assert_schema_contract_rejected "$manual_legacy_procedure" manual-legacy-procedure "legacy descriptor procedure" + +manual_missing_v3_only="$negative_root/manual-missing-v3-only.md" +python3 - "$root/docs/install/local-workspace-registry.md" "$manual_missing_v3_only" <<'PY' +import pathlib, re, sys +text = pathlib.Path(sys.argv[1]).read_text() +text = re.sub( + r"Schema v3 is the only accepted workspace descriptor format\.", + "Schema v3 is accepted as a workspace descriptor format.", + text, + count=1, + flags=re.IGNORECASE, +) +pathlib.Path(sys.argv[2]).write_text(text) +PY +assert_schema_contract_rejected "$manual_missing_v3_only" manual-missing-v3-only "v3-only acceptance" + +diagnostics_readable="$negative_root/diagnostics-readable-v1-v2.md" +python3 - "$root/docs/workspace-diagnostic-protocol.md" "$diagnostics_readable" <<'PY' +import pathlib, re, sys +text = pathlib.Path(sys.argv[1]).read_text() +text, count = re.subn( + r"Schema v1 and v2 descriptors are\s+rejected before diagnostics run\.", + "Schema v1 and v2 descriptors remain readable for operational diagnostics.", + text, + count=1, +) +if count != 1: + raise SystemExit("diagnostic rejection sentence not found") +pathlib.Path(sys.argv[2]).write_text(text) +PY +assert_schema_contract_rejected "$diagnostics_readable" diagnostics-readable-v1-v2 "v1/v2 rejection" + local_manual_paraphrase="$negative_root/local-manual-paraphrase.md" cp "$root/docs/install/local-workspace-registry.md" "$local_manual_paraphrase" python3 - "$local_manual_paraphrase" <<'PY' diff --git a/scripts/verify-workspace-install-docs.sh b/scripts/verify-workspace-install-docs.sh index b7b41a11..5b0a33e1 100755 --- a/scripts/verify-workspace-install-docs.sh +++ b/scripts/verify-workspace-install-docs.sh @@ -105,6 +105,109 @@ for token in tokens: PY } +verify_descriptor_migration_required_context() { + local source="$1" label="$2" scope="${3:-all}" + python3 - "$source" "$label" "$scope" <<'PY' +import pathlib, re, sys + +text = pathlib.Path(sys.argv[1]).read_text() +label = sys.argv[2] +scope = sys.argv[3] +if scope == "current": + marker = re.search(r"^## Historical snapshots\b", text, re.MULTILINE) + if not marker: + raise SystemExit(f"{label}: missing Historical snapshots boundary") + text = text[:marker.start()] +elif scope != "all": + raise SystemExit(f"{label}: invalid migration_required verifier scope: {scope}") + +blocks = [] +current = [] + +def flush(): + if current: + blocks.append(" ".join(current)) + current.clear() + +boundary = re.compile(r"^(?:#{1,6}\s+|[-*+]\s+|\d+[.)]\s+|>\s+|```|\|)") +for raw_line in text.splitlines(): + line = raw_line.strip() + if not line: + flush() + continue + if boundary.match(line): + flush() + line = re.sub(r"^(?:#{1,6}\s+|[-*+]\s+|\d+[.)]\s+|>\s+)", "", line) + current.append(line) +flush() + +migration_context = re.compile( + r"(?:\bworkspace(?:\s+[a-z0-9_-]+){0,3}\s+descriptors?\b|" + r"\bschema(?:[- ]?v?|\s+version\s*)[12]\b|" + r"\bv1\s*(?:/|and|or)\s*v2\b)", + re.IGNORECASE, +) +for block in blocks: + normalized = re.sub(r"\s+", " ", block).strip() + clauses = (part.strip() for part in re.split(r"(?<=[.!?;])\s+", normalized)) + for clause in clauses: + if "migration_required" in clause.lower() and migration_context.search(clause): + raise SystemExit(f"{label}: contains forbidden descriptor migration_required support") +PY +} + +verify_workspace_descriptor_doc_contract() { + local source="$1" label="$2" + verify_descriptor_migration_required_context "$source" "$label" || return 1 + python3 - "$source" "$label" <<'PY' +import pathlib, re, sys + +text = pathlib.Path(sys.argv[1]).read_text() +label = sys.argv[2] + +if not re.search( + r"schema[- ]v?3\s+is\s+the\s+only\s+accepted\s+workspace\s+descriptor", + text, + re.IGNORECASE, +): + raise SystemExit(f"{label}: missing explicit v3-only acceptance contract") + +legacy_subject = ( + r"(?:schema[- ]v?1\s*(?:/|and|or)\s*(?:schema[- ]?)?v?2|" + r"schema[- ]v?1/v2|v1/v2)\s+descriptors?" +) +if not re.search( + legacy_subject + r".{0,100}\brejected\b", + text, + re.IGNORECASE | re.DOTALL, +): + raise SystemExit(f"{label}: missing explicit v1/v2 rejection contract") + +forbidden_literals = { + "migrate-legacy": "migrate-legacy", + "legacy transformer": "deleted legacy transformer instruction", + "backend/dist/workspaces/migrate-legacy.js": "deleted transformer path", +} +for token, description in forbidden_literals.items(): + if token in text.lower(): + raise SystemExit(f"{label}: contains forbidden {description}") +if re.search( + r"(?:\bmigrat(?:e|ing)\s+legacy\s+descriptors?\b|\blegacy\s+descriptor\s+migration\b)", + text, + re.IGNORECASE, +): + raise SystemExit(f"{label}: contains forbidden legacy descriptor procedure") + +legacy_support = [ + legacy_subject + r".{0,100}\b(?:remain|are|stay)\b.{0,40}\b(?:readable|operational|supported|accepted)\b", + r"schema[- ]v?[12]\s+descriptors?.{0,100}\b(?:remain|are|stay)\b.{0,40}\b(?:readable|operational|supported|accepted)\b", +] +for pattern in legacy_support: + if re.search(pattern, text, re.IGNORECASE | re.DOTALL): + raise SystemExit(f"{label}: v1/v2 rejection contradicted by readable or operational support") +PY +} + verify_markdown_table_relationships() { local source="$1" label="$2" heading="$3" spec_json="$4" python3 - "$source" "$label" "$heading" "$spec_json" <<'PY' @@ -588,6 +691,7 @@ verify_vector_helper_interfaces() { verify_project_state_current_contract() { local source="${1:-$root/PROJECT_STATE.md}" local label="${2:-PROJECT_STATE.md}" + verify_descriptor_migration_required_context "$source" "$label current section" current || return 1 python3 - "$source" "$label" <<'PY' import pathlib, re, sys text = pathlib.Path(sys.argv[1]).read_text() @@ -599,10 +703,30 @@ current = text[:marker.start()] historical = text[marker.start():] if not re.search(r"Internal Qdrant \+ Ollama semantic infrastructure", current, re.MULTILINE): raise SystemExit(f"{label}: current section missing internal semantic snapshot heading") -if not re.search(r"Schema-v3 descriptors are operational", current, re.MULTILINE): - raise SystemExit(f"{label}: current section must say schema-v3 is operational") -if "migration_required" not in current: - raise SystemExit(f"{label}: current section must mention migration_required") +if "migrate-legacy" in current.lower(): + raise SystemExit(f"{label}: current section contains forbidden migrate-legacy instruction") +if not re.search( + r"schema[- ]v?3\s+is\s+the\s+only\s+accepted\s+workspace\s+descriptor", + current, + re.IGNORECASE, +): + raise SystemExit(f"{label}: current section lacks explicit v3-only acceptance contract") +legacy_subject = ( + r"(?:schema[- ]v?1\s*(?:/|and|or)\s*(?:schema[- ]?)?v?2|" + r"schema[- ]v?1/v2|v1/v2)\s+descriptors?" +) +if not re.search( + legacy_subject + r".{0,100}\brejected\b", + current, + re.IGNORECASE | re.DOTALL, +): + raise SystemExit(f"{label}: current section lacks explicit v1/v2 rejection contract") +for pattern in [ + legacy_subject + r".{0,100}\b(?:remain|are|stay)\b.{0,40}\b(?:readable|operational|supported|accepted)\b", + r"schema[- ]v?[12]\s+descriptors?.{0,100}\b(?:remain|are|stay)\b.{0,40}\b(?:readable|operational|supported|accepted)\b", +]: + if re.search(pattern, current, re.IGNORECASE | re.DOTALL): + raise SystemExit(f"{label}: current section contains contradictory active text: v1/v2 readable or operational support") if not re.search(r"\b(one|single)\b.*\bworkspace\b.*\b(one|single)\b.*\bQdrant\b.*\bcollection\b", current, re.IGNORECASE | re.DOTALL): raise SystemExit(f"{label}: current section must describe one-workspace/one-collection ownership") if not re.search(r"\bDWH\b", current) or not re.search(r"\bLLM\b", current): @@ -640,6 +764,10 @@ verify_internal_semantic_infrastructure_docs() { verify_workspace_descriptor_semantic_contract "$root/deploy/workspaces/psd.yaml.example" "psd workspace example" || return 1 verify_vector_helper_interfaces || return 1 verify_project_state_current_contract "$root/PROJECT_STATE.md" "PROJECT_STATE.md" || return 1 + verify_workspace_descriptor_doc_contract "$readme" "README" || return 1 + verify_workspace_descriptor_doc_contract "$local_manual" "local workspace manual" || return 1 + verify_workspace_descriptor_doc_contract "$server_manual" "server workspace manual" || return 1 + verify_workspace_descriptor_doc_contract "$diagnostics" "workspace diagnostic protocol" || return 1 local ownership_spec semantic_index_spec compact_spec ownership_spec='{"rows":[ @@ -670,13 +798,12 @@ verify_internal_semantic_infrastructure_docs() { require_pattern "$agents" "AGENTS.md" 'DWH and LLM remain external configuration endpoints' || return 1 for manual in "$local_manual" "$server_manual"; do require_pattern "$manual" "$(basename "$manual")" 'qwen3-embedding:0\.6b' || return 1 - require_pattern "$manual" "$(basename "$manual")" 'migration_required' || return 1 done require_pattern "$local_manual" "local workspace manual" 'CPU-first' || return 1 require_pattern "$local_manual" "local workspace manual" 'THOTH_ENABLE_EMBEDDING_GPU=1' || return 1 require_pattern "$server_manual" "server workspace manual" 'Qdrant backup/restore' || return 1 require_pattern "$compact_manual" "four-context install note" '1024 dimensioni' || return 1 - require_pattern "$diagnostics" "workspace diagnostic protocol" 'schema version 3' || return 1 + require_pattern "$diagnostics" "workspace diagnostic protocol" 'Schema v1 and v2 descriptors.+rejected before diagnostics' || return 1 require_pattern "$diagnostics" "workspace diagnostic protocol" 'semantic_index_incompatible' || return 1 require_absent "$diagnostics" "workspace diagnostic protocol" \ 'engine: pgvector' \