From d9fd902d082d6c975ff129ae80e3b0a4b4b22ed2 Mon Sep 17 00:00:00 2001 From: mptyl Date: Tue, 11 Aug 2026 15:22:57 +0200 Subject: [PATCH] test: enforce the P1.1 registry install docs --- scripts/test-verify-workspace-install-docs.sh | 171 +++++++++--------- scripts/verify-workspace-install-docs.sh | 144 +++++++++++---- 2 files changed, 203 insertions(+), 112 deletions(-) diff --git a/scripts/test-verify-workspace-install-docs.sh b/scripts/test-verify-workspace-install-docs.sh index b9353b90..e4555708 100755 --- a/scripts/test-verify-workspace-install-docs.sh +++ b/scripts/test-verify-workspace-install-docs.sh @@ -401,7 +401,9 @@ expect_evidence_fixture_rejected() { mkdir -p \ "$fixture_root/deploy/workspaces" \ "$fixture_root/docs/contracts" \ - "$fixture_root/docs/install/examples" + "$fixture_root/docs/install/examples" \ + "$fixture_root/docs/install" \ + "$fixture_root/docs/migrations" cp "$root/deploy/workspaces/example.yaml" "$fixture_root/deploy/workspaces/example.yaml" cp "$root/deploy/workspaces/psd.yaml.example" "$fixture_root/deploy/workspaces/psd.yaml.example" cp "$root/docs/contracts/workspace-evidence-v3.md" \ @@ -410,6 +412,9 @@ expect_evidence_fixture_rejected() { "$fixture_root/docs/install/local-workspace-registry.md" cp "$root/docs/install/server-workspace-registry.md" \ "$fixture_root/docs/install/server-workspace-registry.md" + cp "$root/README.md" "$fixture_root/README.md" + cp "$root/docs/migrations/p1-to-p1-1-registry-layout.md" \ + "$fixture_root/docs/migrations/p1-to-p1-1-registry-layout.md" cp "$root/docs/install/examples/workspace-bindings.env.example" \ "$fixture_root/docs/install/examples/workspace-bindings.env.example" @@ -420,24 +425,45 @@ mutation = sys.argv[2] original = path.read_text() changed = original if mutation == "layout-omitted": - changed = original.replace("│ ├── example/evidence/...\n", "", 1) + changed = original.replace("├── thoth-workspaces.yaml\n", "", 1) elif mutation == "same-commit-omitted": changed = original.replace( - "| Revision identity | The descriptor blob and filesystem Evidence root tree are checked at the same 40-hex Git commit. |\n", + "| Revision identity | The catalog blob, descriptor blob, and filesystem Evidence root tree are checked at the same 40-hex Git commit. |\n", "", 1, ) -elif mutation in {"absolute-filesystem", "cross-workspace"}: +elif mutation == "flat-descriptor-path": + changed = original.replace("/workspace.yaml", "workspaces/.yaml", 1) +elif mutation in {"absolute-filesystem", "cross-workspace", "old-filesystem-layout"}: document = yaml.safe_load(original) - document["evidence"]["source"]["uri"] = ( - "/srv/evidence" if mutation == "absolute-filesystem" - else "workspace-content/example/evidence" - ) + document["evidence"]["source"]["uri"] = { + "absolute-filesystem": "/srv/evidence", + "cross-workspace": "other-workspace/evidence", + "old-filesystem-layout": "workspace-content/example/evidence", + }[mutation] changed = yaml.safe_dump(document, sort_keys=False) elif mutation == "wrong-docs-directory": changed = original.replace( - "workspace-docs/\n ├── example/{contract.env.example,README.md}\n └── another/{contract.env.example,README.md}", - "workspaces/.env.example\nworkspaces/.md", + "workspace-docs//{contract.env.example,README.md}", + "example/README.md and example/contract.env.example", + 1, + ) +elif mutation == "catalog-authority-omitted": + changed = original.replace( + "authoritative for workspace ID,", + "descriptor metadata may override workspace ID,", + 1, + ) +elif mutation == "bootstrap-omitted": + changed = original.replace( + "5. The API may create `/workspace.yaml` only when the catalog slot already exists and no Git\n object exists at that path in the exact pulled base commit.\n", + "", + 1, + ) +elif mutation == "api-updates-existing": + changed = original.replace( + "6. After bootstrap, existing descriptors change only through curator Git commit/push and\n installation pull. The API never writes `thoth-workspaces.yaml` or `/evidence/**`.\n", + "6. After bootstrap, use the API to update or delete existing descriptors directly from ThothII.\n", 1, ) elif mutation == "public-http-mode-omitted": @@ -494,8 +520,8 @@ elif mutation == "unsafe-placeholder": ) elif mutation == "p1-scope-inversion": changed = original.replace( - "P1 performs no acquisition, extraction, preprocessing/indexing, embeddings, Qdrant writes, `ACTIVE` publication, retention, or GC.", - "P1 materializes, extracts, and indexes Evidence before publication.", + "P1.1 performs no acquisition, extraction, preprocessing/indexing, embeddings, Qdrant writes,\n`ACTIVE` publication, retention, or GC.", + "P1.1 materializes, extracts, and indexes Evidence before publication.", 1, ) elif mutation.startswith("p1-append-"): @@ -529,16 +555,16 @@ elif mutation.startswith("p1-append-"): raise SystemExit(f"unknown P1 append mutation: {mutation}") base, third_person, ownership = operations[operation] claims = { - "base": f"P1 does {base}.", - "third-person": f"P1 {third_person}.", - "can": f"P1 can {base}.", - "may": f"P1 may {base}.", - "must": f"P1 must {base}.", - "will": f"P1 will {base}.", - "should": f"P1 should {base}.", - "adverb-before-modal": f"P1 directly may {base}.", - "adverb-after-modal": f"P1 may directly {base}.", - "ownership": f"P1 owns {ownership}.", + "base": f"P1.1 does {base}.", + "third-person": f"P1.1 {third_person}.", + "can": f"P1.1 can {base}.", + "may": f"P1.1 may {base}.", + "must": f"P1.1 must {base}.", + "will": f"P1.1 will {base}.", + "should": f"P1.1 should {base}.", + "adverb-before-modal": f"P1.1 directly may {base}.", + "adverb-after-modal": f"P1.1 may directly {base}.", + "ownership": f"P1.1 owns {ownership}.", } changed = original + f"\n{claims[form]}\n" elif mutation == "config-ordering": @@ -548,9 +574,15 @@ elif mutation == "config-ordering": elif mutation == "acceptance-conflation": changed = original.replace("manual acceptance: PENDING\n", "", 1) elif mutation == "curator-order": - second = "2. Add source bytes below `workspace-content//evidence`, then commit and push." - third = "3. Validate and publish the descriptor against that base commit." + second = "2. Keep `thoth-workspaces.yaml` curator-owned. It uses the `schema_version` value `1` and the ordered\n `workspaces` list of `{id, name, description?}` entries; it is authoritative for workspace ID,\n name, description, and display order." + third = "3. For an existing workspace, edit `/workspace.yaml` and any embedded `/evidence/**`, then\n commit and push." changed = original.replace(second + "\n" + third, third + "\n" + second, 1) +elif mutation == "migration-commit-omitted": + changed = original.replace("git mv workspaces/.yaml /workspace.yaml\n", "", 1) +elif mutation == "migration-upgrade-omitted": + changed = original.replace("3. Upgrade ThothII only after that migration commit is pushed.\n", "", 1) +elif mutation == "migration-rollback-omitted": + changed = original.replace("Roll back the application revision and registry commit together.", "Roll back only the application revision.", 1) else: raise SystemExit(f"unknown Evidence mutation: {mutation}") if changed == original: @@ -581,7 +613,9 @@ expect_evidence_claim_accepted() { mkdir -p \ "$fixture_root/deploy/workspaces" \ "$fixture_root/docs/contracts" \ - "$fixture_root/docs/install/examples" + "$fixture_root/docs/install/examples" \ + "$fixture_root/docs/install" \ + "$fixture_root/docs/migrations" cp "$root/deploy/workspaces/example.yaml" "$fixture_root/deploy/workspaces/example.yaml" cp "$root/deploy/workspaces/psd.yaml.example" "$fixture_root/deploy/workspaces/psd.yaml.example" cp "$root/docs/contracts/workspace-evidence-v3.md" \ @@ -590,6 +624,9 @@ expect_evidence_claim_accepted() { "$fixture_root/docs/install/local-workspace-registry.md" cp "$root/docs/install/server-workspace-registry.md" \ "$fixture_root/docs/install/server-workspace-registry.md" + cp "$root/README.md" "$fixture_root/README.md" + cp "$root/docs/migrations/p1-to-p1-1-registry-layout.md" \ + "$fixture_root/docs/migrations/p1-to-p1-1-registry-layout.md" cp "$root/docs/install/examples/workspace-bindings.env.example" \ "$fixture_root/docs/install/examples/workspace-bindings.env.example" printf '\n%s\n' "$claim" >>"$fixture_root/docs/contracts/workspace-evidence-v3.md" @@ -959,60 +996,32 @@ expect_guide_rejected \ "$root/docs/install/windows-line-endings.md" docs/install/windows-line-endings.md powershell-crlf-failure \ "PowerShell CRLF repair lacks failure propagation: Assert-NativeSuccess 'index export'" -expect_evidence_fixture_rejected \ - "canonical Evidence layout omitted" docs/contracts/workspace-evidence-v3.md layout-omitted \ - "missing canonical Evidence layout" -expect_evidence_fixture_rejected \ - "same revision ownership omitted" docs/contracts/workspace-evidence-v3.md same-commit-omitted \ - "missing same-revision ownership" -expect_evidence_fixture_rejected \ - "absolute filesystem Evidence path" deploy/workspaces/example.yaml absolute-filesystem \ - "noncanonical filesystem Evidence URI" -expect_evidence_fixture_rejected \ - "cross-workspace Evidence path" deploy/workspaces/psd.yaml.example cross-workspace \ - "Evidence namespace mismatch" -expect_evidence_fixture_rejected \ - "generated docs in wrong directory" docs/contracts/workspace-evidence-v3.md wrong-docs-directory \ - "generated docs path invalid" -expect_evidence_fixture_rejected \ - "public HTTP mode omitted" docs/contracts/workspace-evidence-v3.md public-http-mode-omitted \ - "missing public HTTP mode" -expect_evidence_fixture_rejected \ - "ambient S3 mode omitted" docs/contracts/workspace-evidence-v3.md ambient-s3-mode-omitted \ - "missing ambient S3 mode" -expect_evidence_fixture_rejected \ - "strict Evidence numeric domains omitted" docs/contracts/workspace-evidence-v3.md numeric-domains-omitted \ - "missing strict Evidence numeric domains" -expect_evidence_fixture_rejected \ - "S3 endpoint policy without endpoint invariant omitted" docs/contracts/workspace-evidence-v3.md endpoint-without-url-invariant-omitted \ - "missing S3 endpoint policy without endpoint invariant" -expect_evidence_fixture_rejected \ - "signed HTTP file boundary omitted" docs/contracts/workspace-evidence-v3.md http-file-boundary-omitted \ - "missing signed HTTP file boundary" -expect_evidence_fixture_rejected \ - "static S3 pair boundary omitted" docs/contracts/workspace-evidence-v3.md s3-pair-boundary-omitted \ - "missing static S3 file boundary" -expect_evidence_fixture_rejected \ - "static S3 optional token boundary omitted" docs/contracts/workspace-evidence-v3.md s3-token-boundary-omitted \ - "missing static S3 session-token boundary" -expect_evidence_fixture_rejected \ - "credential literal in public bindings" docs/install/examples/workspace-bindings.env.example credential-literal \ - "credential literal forbidden" -expect_evidence_fixture_rejected \ - "credential literal in public prose" docs/contracts/workspace-evidence-v3.md credential-literal-public-prose \ - "credential literal forbidden" -expect_evidence_fixture_rejected \ - "credential literal in public YAML" deploy/workspaces/example.yaml credential-literal-public-yaml \ - "credential literal forbidden" -expect_evidence_fixture_rejected \ - "signed query in public bindings" docs/install/examples/workspace-bindings.env.example signed-query-example \ - "query-bearing public URI forbidden" -expect_evidence_fixture_rejected \ - "unsafe Evidence file placeholder" docs/install/examples/workspace-bindings.env.example unsafe-placeholder \ - "unsafe file placeholder/path" -expect_evidence_fixture_rejected \ - "P1 Evidence scope inversion" docs/contracts/workspace-evidence-v3.md p1-scope-inversion \ - "P1 scope violation" +expect_evidence_fixture_rejected "root catalog omitted" docs/contracts/workspace-evidence-v3.md layout-omitted "missing canonical Evidence layout" +expect_evidence_fixture_rejected "same revision ownership omitted" docs/contracts/workspace-evidence-v3.md same-commit-omitted "missing same-revision ownership" +expect_evidence_fixture_rejected "flat descriptor path" docs/contracts/workspace-evidence-v3.md flat-descriptor-path 'The descriptor at `/workspace.yaml` must match the' +expect_evidence_fixture_rejected "absolute filesystem Evidence path" deploy/workspaces/example.yaml absolute-filesystem "noncanonical filesystem Evidence URI" +expect_evidence_fixture_rejected "cross-workspace Evidence path" deploy/workspaces/psd.yaml.example cross-workspace "Evidence namespace mismatch" +expect_evidence_fixture_rejected "old filesystem Evidence layout" deploy/workspaces/example.yaml old-filesystem-layout "Evidence namespace mismatch" +expect_evidence_fixture_rejected "generated docs in workspace directory" docs/contracts/workspace-evidence-v3.md wrong-docs-directory "generated docs path invalid" +expect_evidence_fixture_rejected "catalog metadata not authoritative" docs/install/local-workspace-registry.md catalog-authority-omitted "missing catalog authority" +expect_evidence_fixture_rejected "bootstrap create-once rule omitted" docs/install/local-workspace-registry.md bootstrap-omitted "curator flow missing registry rule" +expect_evidence_fixture_rejected "API updates existing descriptors claim" docs/install/local-workspace-registry.md api-updates-existing "curator flow missing registry rule" +expect_evidence_fixture_rejected "public HTTP mode omitted" docs/contracts/workspace-evidence-v3.md public-http-mode-omitted "missing public HTTP mode" +expect_evidence_fixture_rejected "ambient S3 mode omitted" docs/contracts/workspace-evidence-v3.md ambient-s3-mode-omitted "missing ambient S3 mode" +expect_evidence_fixture_rejected "strict Evidence numeric domains omitted" docs/contracts/workspace-evidence-v3.md numeric-domains-omitted "missing strict Evidence numeric domains" +expect_evidence_fixture_rejected "S3 endpoint policy without endpoint invariant omitted" docs/contracts/workspace-evidence-v3.md endpoint-without-url-invariant-omitted "missing S3 endpoint policy without endpoint invariant" +expect_evidence_fixture_rejected "signed HTTP file boundary omitted" docs/contracts/workspace-evidence-v3.md http-file-boundary-omitted "missing signed HTTP file boundary" +expect_evidence_fixture_rejected "static S3 pair boundary omitted" docs/contracts/workspace-evidence-v3.md s3-pair-boundary-omitted "missing static S3 file boundary" +expect_evidence_fixture_rejected "static S3 optional token boundary omitted" docs/contracts/workspace-evidence-v3.md s3-token-boundary-omitted "missing static S3 session-token boundary" +expect_evidence_fixture_rejected "credential literal in public bindings" docs/install/examples/workspace-bindings.env.example credential-literal "credential literal forbidden" +expect_evidence_fixture_rejected "credential literal in public prose" docs/contracts/workspace-evidence-v3.md credential-literal-public-prose "credential literal forbidden" +expect_evidence_fixture_rejected "credential literal in public YAML" deploy/workspaces/example.yaml credential-literal-public-yaml "credential literal forbidden" +expect_evidence_fixture_rejected "signed query in public bindings" docs/install/examples/workspace-bindings.env.example signed-query-example "query-bearing public URI forbidden" +expect_evidence_fixture_rejected "unsafe Evidence file placeholder" docs/install/examples/workspace-bindings.env.example unsafe-placeholder "unsafe file placeholder/path" +expect_evidence_fixture_rejected "P1.1 Evidence scope inversion" docs/contracts/workspace-evidence-v3.md p1-scope-inversion "P1.1 scope violation" +expect_evidence_fixture_rejected "migration commit step omitted" docs/migrations/p1-to-p1-1-registry-layout.md migration-commit-omitted "migration guide missing commit step" +expect_evidence_fixture_rejected "migration upgrade ordering omitted" docs/migrations/p1-to-p1-1-registry-layout.md migration-upgrade-omitted "migration guide missing upgrade ordering" +expect_evidence_fixture_rejected "migration rollback rule omitted" docs/migrations/p1-to-p1-1-registry-layout.md migration-rollback-omitted "migration guide missing rollback rule" p1_operations=( acquisition materialization extraction preprocessing embeddings qdrant-writes indexing active retention gc @@ -1024,9 +1033,9 @@ p1_positive_forms=( for operation in "${p1_operations[@]}"; do for form in "${p1_positive_forms[@]}"; do expect_evidence_fixture_rejected \ - "appended P1 ${operation} ${form} claim" \ + "appended P1.1 ${operation} ${form} claim" \ docs/contracts/workspace-evidence-v3.md "p1-append-${operation}-${form}" \ - "P1 scope violation" + "P1.1 scope violation" done done p1_safe_bases=( diff --git a/scripts/verify-workspace-install-docs.sh b/scripts/verify-workspace-install-docs.sh index 4cfbe8a9..9947a0ff 100755 --- a/scripts/verify-workspace-install-docs.sh +++ b/scripts/verify-workspace-install-docs.sh @@ -254,17 +254,18 @@ base = pathlib.Path(sys.argv[1]) contract_path = base / "docs/contracts/workspace-evidence-v3.md" local_path = base / "docs/install/local-workspace-registry.md" server_path = base / "docs/install/server-workspace-registry.md" +readme_path = base / "README.md" +migration_path = base / "docs/migrations/p1-to-p1-1-registry-layout.md" bindings_path = base / "docs/install/examples/workspace-bindings.env.example" descriptor_paths = [ base / "deploy/workspaces/example.yaml", base / "deploy/workspaces/psd.yaml.example", ] -paths = [contract_path, local_path, server_path, bindings_path, *descriptor_paths] +paths = [contract_path, local_path, server_path, readme_path, migration_path, bindings_path, *descriptor_paths] for path in paths: if not path.is_file(): raise SystemExit(f"missing workspace Evidence contract input: {path.relative_to(base)}") -# Generic descriptors must publish an explicit, ID-derived canonical filesystem contract. for path in descriptor_paths: relative = path.relative_to(base).as_posix() document = yaml.safe_load(path.read_text()) @@ -273,7 +274,7 @@ for path in descriptor_paths: if not isinstance(evidence, dict) or not isinstance(evidence.get("source"), dict): raise SystemExit(f"{relative}: missing explicit filesystem Evidence contract") uri = evidence["source"].get("uri") - expected_uri = f"workspace-content/{workspace_id}/evidence" + expected_uri = f"{workspace_id}/evidence" if not isinstance(uri, str) or uri.startswith("/") or "\\" in uri or ".." in uri.split("/"): raise SystemExit(f"{relative}: noncanonical filesystem Evidence URI") if uri != expected_uri: @@ -291,6 +292,15 @@ for path in descriptor_paths: raise SystemExit(f"{relative}: explicit filesystem Evidence object mismatch") contract = contract_path.read_text() +readme = readme_path.read_text() +migration = migration_path.read_text() +all_public = "\n".join(path.read_text() for path in paths) +active_public = "\n".join(path.read_text() for path in [contract_path, local_path, server_path, readme_path, bindings_path, *descriptor_paths]) + + +def normalize_space(text: str) -> str: + return re.sub(r"\s+", " ", text.strip()) + def named_example(name): match = re.search( @@ -306,7 +316,7 @@ examples = { "filesystem": { "evidence": { "source": { - "type": "filesystem", "uri": "workspace-content/example/evidence", + "type": "filesystem", "uri": "example/evidence", "patterns": ["**/*.md"], "max_bytes": 10485760, }, "policy": {"max_chunk_chars": 4000, "retain_published_generations": 3}, @@ -355,9 +365,15 @@ required_contract_phrases = [ "Content-only revision", "read-only Evidence summary", "excludes Evidence bytes", + "`schema_version` value `1`", + "It is authoritative for workspace ID,\nname, description, and display order.", + "The descriptor at `/workspace.yaml` must match the\ncatalog metadata exactly.", + "Catalog-only entries without `/workspace.yaml` are valid bootstrap slots and surface as\n`configuration_required`.", + "The API never writes `thoth-workspaces.yaml` or `/evidence/**`.", ] +normalized_contract = normalize_space(contract) for phrase in required_contract_phrases: - if phrase not in contract: + if normalize_space(phrase) not in normalized_contract: raise SystemExit(f"workspace Evidence contract lacks required rule: {phrase}") mode_rules = { @@ -372,21 +388,36 @@ if "positive safe integers" not in contract or "nonnegative safe integer" not in if "Endpoint-policy flags cannot be enabled without `endpoint_url`." not in contract: raise SystemExit("missing S3 endpoint policy without endpoint invariant") -# The canonical one-repository tree is exact, including generated docs outside workspaces/. -legacy_docs = re.compile(r"workspaces/(?:<[^>]+>|[^\s`/]+)\.(?:env\.example|md)") -all_public = "\n".join(path.read_text() for path in paths) -if legacy_docs.search(all_public) or "workspaces/.env.example" in all_public: +for forbidden in ( + "workspace-content//evidence", + "workspaces/.yaml", + "workspace-content/example/evidence", + "Validate and publish the descriptor against that base commit", +): + if forbidden in active_public: + raise SystemExit("old registry layout text found") + +legacy_docs = re.compile(r"(?:^|\n)\s*(?:|[a-z0-9-]+)/(?:(?:contract\.env\.example|README\.md))") +if "workspace-docs//{contract.env.example,README.md}" not in all_public: raise SystemExit("generated docs path invalid") +for pattern in ( + r"(?/README\.md", + r"(?/contract\.env\.example", + r"(?/evidence`", "Git tree", "same commit", "does not recursively inspect nested symlinks", "out of scope for P1.1")): + raise SystemExit("missing P1.1 lexical/tree ownership") if not all(token in p6 for token in ("commit-addressed materialization", "realpath", "recursive containment", "nested-symlink", "race")): raise SystemExit("missing P6 materialization ownership") -no_scope = "P1 performs no acquisition, extraction, preprocessing/indexing, embeddings, Qdrant writes, `ACTIVE` publication, retention, or GC." +no_scope = "P1.1 performs no acquisition, extraction, preprocessing/indexing, embeddings, Qdrant writes, `ACTIVE` publication, retention, or GC." p1_adverbs = r"(?:\s+(?:also|then|now|directly|itself))*" p1_base_operation = r"""(?: acquire|materialize|extract|preprocess|index|retain| @@ -439,15 +472,15 @@ p1_ownership = r"""(?: garbage[ -]collection )""" positive_p1_operation = re.compile( - rf"""\bP1\b{p1_adverbs}\s+(?: + rf"""\bP1(?:\.1)?\b{p1_adverbs}\s+(?: (?:(?:can|may|must|will|should|does){p1_adverbs}\s+){p1_base_operation}| {p1_third_person_operation}| {p1_ownership} )\b""", re.IGNORECASE | re.VERBOSE, ) -if no_scope not in contract or positive_p1_operation.search(contract): - raise SystemExit("P1 scope violation") +if normalize_space(no_scope) not in normalized_contract or positive_p1_operation.search(contract): + raise SystemExit("P1.1 scope violation") installation_rows = {row[0]: row[1:] for row in table_for("Installation files")} http_row = " ".join(installation_rows.get("Signed HTTP", [])) @@ -478,11 +511,21 @@ if len(automated) != 1 or len(manual) != 1: raise SystemExit("separate automated/manual states missing") flow_tokens = [ - "Clone the one shared registry", "workspace-content//evidence", "commit and push", - "Validate and publish the descriptor against that base commit", - "workspace-docs//contract.env.example", "workspace-docs//README.md", - "Evidence `*_FILE` files outside Git", "THT_WORKSPACE_SECRET_ROOTS", "`*_SOURCE` paths", - "tht config check -c ", "P2/P6 later performs preprocessing and materialization", + "Clone the one shared registry", + "thoth-workspaces.yaml", + "/workspace.yaml", + "/evidence/**", + "configuration_required", + "The API may create `/workspace.yaml` only when the catalog slot already exists and no Git", + "After bootstrap, existing descriptors change only through curator Git commit/push and", + "The API never writes `thoth-workspaces.yaml` or `/evidence/**`.", + "workspace-docs//contract.env.example", + "workspace-docs//README.md", + "Evidence `*_FILE` files outside Git", + "THT_WORKSPACE_SECRET_ROOTS", + "`*_SOURCE` paths", + "tht config check -c ", + "P2/P6 later performs preprocessing and materialization", ] for guide in (local_path, server_path): text = guide.read_text() @@ -495,20 +538,58 @@ for guide in (local_path, server_path): raise SystemExit(f"{guide.name}: missing curator flow") section = match.group(1) positions = [section.find(token) for token in flow_tokens] - if any(position < 0 for position in positions) or positions != sorted(positions): + if any(position < 0 for position in positions): + raise SystemExit(f"{guide.name}: curator flow missing registry rule") + if positions != sorted(positions): raise SystemExit(f"{guide.name}: curator flow out of order") -# Public prose, YAML, and examples may name credential variables and describe forbidden shapes, -# but they must never contain a high-confidence access-key literal. Identifier-aware boundaries -# avoid treating a legitimate variable name as a credential value. +for guide in (local_path, server_path): + guide_text = guide.read_text() + if "authoritative for workspace ID, name, description, and\ndisplay order" not in guide_text: + raise SystemExit("missing catalog authority") + if "The API may create `/workspace.yaml` only when the catalog slot already exists" not in guide_text: + raise SystemExit("missing bootstrap create-once rule") + if "After bootstrap, existing descriptors change only through curator Git commit/push and\n" not in guide_text: + raise SystemExit("missing existing-descriptor curator ownership") + +readme_required = [ + "thoth-workspaces.yaml", + "/workspace.yaml", + "/evidence/**", + "workspace-docs//{contract.env.example,README.md}", + "authoritative for workspace ID, name, description, and\ndisplay order", + "configuration_required", + "existing descriptors remain curator-owned and change only through curator Git commit,\npush, and installation pull.", + "The API never writes `thoth-workspaces.yaml` or `/evidence/**`;", + "docs/migrations/p1-to-p1-1-registry-layout.md", +] +normalized_readme = normalize_space(readme) +for phrase in readme_required: + if normalize_space(phrase) not in normalized_readme: + raise SystemExit("README registry overview incomplete") + +migration_commit_phrases = [ + "git mv workspaces/.yaml /workspace.yaml", + "git mv workspace-content//evidence /evidence", + "create and review thoth-workspaces.yaml from descriptor metadata", +] +for phrase in migration_commit_phrases: + if phrase not in migration: + raise SystemExit("migration guide missing commit step") +if "Upgrade ThothII only after that migration commit is pushed." not in migration: + raise SystemExit("migration guide missing upgrade ordering") +if "Roll back the application revision and registry commit together." not in migration: + raise SystemExit("migration guide missing rollback rule") +if "reject the old flat layout and a\nrepository without `thoth-workspaces.yaml`" not in migration: + raise SystemExit("migration guide missing rejection rule") + aws_access_key = re.compile( r"(?