test: enforce the P1.1 registry install docs
This commit is contained in:
@@ -401,7 +401,9 @@ expect_evidence_fixture_rejected() {
|
|||||||
mkdir -p \
|
mkdir -p \
|
||||||
"$fixture_root/deploy/workspaces" \
|
"$fixture_root/deploy/workspaces" \
|
||||||
"$fixture_root/docs/contracts" \
|
"$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/example.yaml" "$fixture_root/deploy/workspaces/example.yaml"
|
||||||
cp "$root/deploy/workspaces/psd.yaml.example" "$fixture_root/deploy/workspaces/psd.yaml.example"
|
cp "$root/deploy/workspaces/psd.yaml.example" "$fixture_root/deploy/workspaces/psd.yaml.example"
|
||||||
cp "$root/docs/contracts/workspace-evidence-v3.md" \
|
cp "$root/docs/contracts/workspace-evidence-v3.md" \
|
||||||
@@ -410,6 +412,9 @@ expect_evidence_fixture_rejected() {
|
|||||||
"$fixture_root/docs/install/local-workspace-registry.md"
|
"$fixture_root/docs/install/local-workspace-registry.md"
|
||||||
cp "$root/docs/install/server-workspace-registry.md" \
|
cp "$root/docs/install/server-workspace-registry.md" \
|
||||||
"$fixture_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" \
|
cp "$root/docs/install/examples/workspace-bindings.env.example" \
|
||||||
"$fixture_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()
|
original = path.read_text()
|
||||||
changed = original
|
changed = original
|
||||||
if mutation == "layout-omitted":
|
if mutation == "layout-omitted":
|
||||||
changed = original.replace("│ ├── example/evidence/...\n", "", 1)
|
changed = original.replace("├── thoth-workspaces.yaml\n", "", 1)
|
||||||
elif mutation == "same-commit-omitted":
|
elif mutation == "same-commit-omitted":
|
||||||
changed = original.replace(
|
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,
|
1,
|
||||||
)
|
)
|
||||||
elif mutation in {"absolute-filesystem", "cross-workspace"}:
|
elif mutation == "flat-descriptor-path":
|
||||||
|
changed = original.replace("<id>/workspace.yaml", "workspaces/<id>.yaml", 1)
|
||||||
|
elif mutation in {"absolute-filesystem", "cross-workspace", "old-filesystem-layout"}:
|
||||||
document = yaml.safe_load(original)
|
document = yaml.safe_load(original)
|
||||||
document["evidence"]["source"]["uri"] = (
|
document["evidence"]["source"]["uri"] = {
|
||||||
"/srv/evidence" if mutation == "absolute-filesystem"
|
"absolute-filesystem": "/srv/evidence",
|
||||||
else "workspace-content/example/evidence"
|
"cross-workspace": "other-workspace/evidence",
|
||||||
)
|
"old-filesystem-layout": "workspace-content/example/evidence",
|
||||||
|
}[mutation]
|
||||||
changed = yaml.safe_dump(document, sort_keys=False)
|
changed = yaml.safe_dump(document, sort_keys=False)
|
||||||
elif mutation == "wrong-docs-directory":
|
elif mutation == "wrong-docs-directory":
|
||||||
changed = original.replace(
|
changed = original.replace(
|
||||||
"workspace-docs/\n ├── example/{contract.env.example,README.md}\n └── another/{contract.env.example,README.md}",
|
"workspace-docs/<id>/{contract.env.example,README.md}",
|
||||||
"workspaces/<id>.env.example\nworkspaces/<id>.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 `<id>/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 `<id>/evidence/**`.\n",
|
||||||
|
"6. After bootstrap, use the API to update or delete existing descriptors directly from ThothII.\n",
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
elif mutation == "public-http-mode-omitted":
|
elif mutation == "public-http-mode-omitted":
|
||||||
@@ -494,8 +520,8 @@ elif mutation == "unsafe-placeholder":
|
|||||||
)
|
)
|
||||||
elif mutation == "p1-scope-inversion":
|
elif mutation == "p1-scope-inversion":
|
||||||
changed = original.replace(
|
changed = original.replace(
|
||||||
"P1 performs no acquisition, extraction, preprocessing/indexing, embeddings, Qdrant writes, `ACTIVE` publication, retention, or GC.",
|
"P1.1 performs no acquisition, extraction, preprocessing/indexing, embeddings, Qdrant writes,\n`ACTIVE` publication, retention, or GC.",
|
||||||
"P1 materializes, extracts, and indexes Evidence before publication.",
|
"P1.1 materializes, extracts, and indexes Evidence before publication.",
|
||||||
1,
|
1,
|
||||||
)
|
)
|
||||||
elif mutation.startswith("p1-append-"):
|
elif mutation.startswith("p1-append-"):
|
||||||
@@ -529,16 +555,16 @@ elif mutation.startswith("p1-append-"):
|
|||||||
raise SystemExit(f"unknown P1 append mutation: {mutation}")
|
raise SystemExit(f"unknown P1 append mutation: {mutation}")
|
||||||
base, third_person, ownership = operations[operation]
|
base, third_person, ownership = operations[operation]
|
||||||
claims = {
|
claims = {
|
||||||
"base": f"P1 does {base}.",
|
"base": f"P1.1 does {base}.",
|
||||||
"third-person": f"P1 {third_person}.",
|
"third-person": f"P1.1 {third_person}.",
|
||||||
"can": f"P1 can {base}.",
|
"can": f"P1.1 can {base}.",
|
||||||
"may": f"P1 may {base}.",
|
"may": f"P1.1 may {base}.",
|
||||||
"must": f"P1 must {base}.",
|
"must": f"P1.1 must {base}.",
|
||||||
"will": f"P1 will {base}.",
|
"will": f"P1.1 will {base}.",
|
||||||
"should": f"P1 should {base}.",
|
"should": f"P1.1 should {base}.",
|
||||||
"adverb-before-modal": f"P1 directly may {base}.",
|
"adverb-before-modal": f"P1.1 directly may {base}.",
|
||||||
"adverb-after-modal": f"P1 may directly {base}.",
|
"adverb-after-modal": f"P1.1 may directly {base}.",
|
||||||
"ownership": f"P1 owns {ownership}.",
|
"ownership": f"P1.1 owns {ownership}.",
|
||||||
}
|
}
|
||||||
changed = original + f"\n{claims[form]}\n"
|
changed = original + f"\n{claims[form]}\n"
|
||||||
elif mutation == "config-ordering":
|
elif mutation == "config-ordering":
|
||||||
@@ -548,9 +574,15 @@ elif mutation == "config-ordering":
|
|||||||
elif mutation == "acceptance-conflation":
|
elif mutation == "acceptance-conflation":
|
||||||
changed = original.replace("manual acceptance: PENDING\n", "", 1)
|
changed = original.replace("manual acceptance: PENDING\n", "", 1)
|
||||||
elif mutation == "curator-order":
|
elif mutation == "curator-order":
|
||||||
second = "2. Add source bytes below `workspace-content/<id>/evidence`, then commit and push."
|
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. Validate and publish the descriptor against that base commit."
|
third = "3. For an existing workspace, edit `<id>/workspace.yaml` and any embedded `<id>/evidence/**`, then\n commit and push."
|
||||||
changed = original.replace(second + "\n" + third, third + "\n" + second, 1)
|
changed = original.replace(second + "\n" + third, third + "\n" + second, 1)
|
||||||
|
elif mutation == "migration-commit-omitted":
|
||||||
|
changed = original.replace("git mv workspaces/<id>.yaml <id>/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:
|
else:
|
||||||
raise SystemExit(f"unknown Evidence mutation: {mutation}")
|
raise SystemExit(f"unknown Evidence mutation: {mutation}")
|
||||||
if changed == original:
|
if changed == original:
|
||||||
@@ -581,7 +613,9 @@ expect_evidence_claim_accepted() {
|
|||||||
mkdir -p \
|
mkdir -p \
|
||||||
"$fixture_root/deploy/workspaces" \
|
"$fixture_root/deploy/workspaces" \
|
||||||
"$fixture_root/docs/contracts" \
|
"$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/example.yaml" "$fixture_root/deploy/workspaces/example.yaml"
|
||||||
cp "$root/deploy/workspaces/psd.yaml.example" "$fixture_root/deploy/workspaces/psd.yaml.example"
|
cp "$root/deploy/workspaces/psd.yaml.example" "$fixture_root/deploy/workspaces/psd.yaml.example"
|
||||||
cp "$root/docs/contracts/workspace-evidence-v3.md" \
|
cp "$root/docs/contracts/workspace-evidence-v3.md" \
|
||||||
@@ -590,6 +624,9 @@ expect_evidence_claim_accepted() {
|
|||||||
"$fixture_root/docs/install/local-workspace-registry.md"
|
"$fixture_root/docs/install/local-workspace-registry.md"
|
||||||
cp "$root/docs/install/server-workspace-registry.md" \
|
cp "$root/docs/install/server-workspace-registry.md" \
|
||||||
"$fixture_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" \
|
cp "$root/docs/install/examples/workspace-bindings.env.example" \
|
||||||
"$fixture_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"
|
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 \
|
"$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'"
|
"PowerShell CRLF repair lacks failure propagation: Assert-NativeSuccess 'index export'"
|
||||||
|
|
||||||
expect_evidence_fixture_rejected \
|
expect_evidence_fixture_rejected "root catalog omitted" docs/contracts/workspace-evidence-v3.md layout-omitted "missing canonical Evidence layout"
|
||||||
"canonical Evidence layout omitted" docs/contracts/workspace-evidence-v3.md layout-omitted \
|
expect_evidence_fixture_rejected "same revision ownership omitted" docs/contracts/workspace-evidence-v3.md same-commit-omitted "missing same-revision ownership"
|
||||||
"missing canonical Evidence layout"
|
expect_evidence_fixture_rejected "flat descriptor path" docs/contracts/workspace-evidence-v3.md flat-descriptor-path 'The descriptor at `<id>/workspace.yaml` must match the'
|
||||||
expect_evidence_fixture_rejected \
|
expect_evidence_fixture_rejected "absolute filesystem Evidence path" deploy/workspaces/example.yaml absolute-filesystem "noncanonical filesystem Evidence URI"
|
||||||
"same revision ownership omitted" docs/contracts/workspace-evidence-v3.md same-commit-omitted \
|
expect_evidence_fixture_rejected "cross-workspace Evidence path" deploy/workspaces/psd.yaml.example cross-workspace "Evidence namespace mismatch"
|
||||||
"missing same-revision ownership"
|
expect_evidence_fixture_rejected "old filesystem Evidence layout" deploy/workspaces/example.yaml old-filesystem-layout "Evidence namespace mismatch"
|
||||||
expect_evidence_fixture_rejected \
|
expect_evidence_fixture_rejected "generated docs in workspace directory" docs/contracts/workspace-evidence-v3.md wrong-docs-directory "generated docs path invalid"
|
||||||
"absolute filesystem Evidence path" deploy/workspaces/example.yaml absolute-filesystem \
|
expect_evidence_fixture_rejected "catalog metadata not authoritative" docs/install/local-workspace-registry.md catalog-authority-omitted "missing catalog authority"
|
||||||
"noncanonical filesystem Evidence URI"
|
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 \
|
expect_evidence_fixture_rejected "API updates existing descriptors claim" docs/install/local-workspace-registry.md api-updates-existing "curator flow missing registry rule"
|
||||||
"cross-workspace Evidence path" deploy/workspaces/psd.yaml.example cross-workspace \
|
expect_evidence_fixture_rejected "public HTTP mode omitted" docs/contracts/workspace-evidence-v3.md public-http-mode-omitted "missing public HTTP mode"
|
||||||
"Evidence namespace mismatch"
|
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 \
|
expect_evidence_fixture_rejected "strict Evidence numeric domains omitted" docs/contracts/workspace-evidence-v3.md numeric-domains-omitted "missing strict Evidence numeric domains"
|
||||||
"generated docs in wrong directory" docs/contracts/workspace-evidence-v3.md wrong-docs-directory \
|
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"
|
||||||
"generated docs path invalid"
|
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 \
|
expect_evidence_fixture_rejected "static S3 pair boundary omitted" docs/contracts/workspace-evidence-v3.md s3-pair-boundary-omitted "missing static S3 file boundary"
|
||||||
"public HTTP mode omitted" docs/contracts/workspace-evidence-v3.md public-http-mode-omitted \
|
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"
|
||||||
"missing public HTTP mode"
|
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 \
|
expect_evidence_fixture_rejected "credential literal in public prose" docs/contracts/workspace-evidence-v3.md credential-literal-public-prose "credential literal forbidden"
|
||||||
"ambient S3 mode omitted" docs/contracts/workspace-evidence-v3.md ambient-s3-mode-omitted \
|
expect_evidence_fixture_rejected "credential literal in public YAML" deploy/workspaces/example.yaml credential-literal-public-yaml "credential literal forbidden"
|
||||||
"missing ambient S3 mode"
|
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 \
|
expect_evidence_fixture_rejected "unsafe Evidence file placeholder" docs/install/examples/workspace-bindings.env.example unsafe-placeholder "unsafe file placeholder/path"
|
||||||
"strict Evidence numeric domains omitted" docs/contracts/workspace-evidence-v3.md numeric-domains-omitted \
|
expect_evidence_fixture_rejected "P1.1 Evidence scope inversion" docs/contracts/workspace-evidence-v3.md p1-scope-inversion "P1.1 scope violation"
|
||||||
"missing strict Evidence numeric domains"
|
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 \
|
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"
|
||||||
"S3 endpoint policy without endpoint invariant omitted" docs/contracts/workspace-evidence-v3.md endpoint-without-url-invariant-omitted \
|
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"
|
||||||
"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"
|
|
||||||
p1_operations=(
|
p1_operations=(
|
||||||
acquisition materialization extraction preprocessing embeddings
|
acquisition materialization extraction preprocessing embeddings
|
||||||
qdrant-writes indexing active retention gc
|
qdrant-writes indexing active retention gc
|
||||||
@@ -1024,9 +1033,9 @@ p1_positive_forms=(
|
|||||||
for operation in "${p1_operations[@]}"; do
|
for operation in "${p1_operations[@]}"; do
|
||||||
for form in "${p1_positive_forms[@]}"; do
|
for form in "${p1_positive_forms[@]}"; do
|
||||||
expect_evidence_fixture_rejected \
|
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}" \
|
docs/contracts/workspace-evidence-v3.md "p1-append-${operation}-${form}" \
|
||||||
"P1 scope violation"
|
"P1.1 scope violation"
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
p1_safe_bases=(
|
p1_safe_bases=(
|
||||||
|
|||||||
@@ -254,17 +254,18 @@ base = pathlib.Path(sys.argv[1])
|
|||||||
contract_path = base / "docs/contracts/workspace-evidence-v3.md"
|
contract_path = base / "docs/contracts/workspace-evidence-v3.md"
|
||||||
local_path = base / "docs/install/local-workspace-registry.md"
|
local_path = base / "docs/install/local-workspace-registry.md"
|
||||||
server_path = base / "docs/install/server-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"
|
bindings_path = base / "docs/install/examples/workspace-bindings.env.example"
|
||||||
descriptor_paths = [
|
descriptor_paths = [
|
||||||
base / "deploy/workspaces/example.yaml",
|
base / "deploy/workspaces/example.yaml",
|
||||||
base / "deploy/workspaces/psd.yaml.example",
|
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:
|
for path in paths:
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
raise SystemExit(f"missing workspace Evidence contract input: {path.relative_to(base)}")
|
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:
|
for path in descriptor_paths:
|
||||||
relative = path.relative_to(base).as_posix()
|
relative = path.relative_to(base).as_posix()
|
||||||
document = yaml.safe_load(path.read_text())
|
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):
|
if not isinstance(evidence, dict) or not isinstance(evidence.get("source"), dict):
|
||||||
raise SystemExit(f"{relative}: missing explicit filesystem Evidence contract")
|
raise SystemExit(f"{relative}: missing explicit filesystem Evidence contract")
|
||||||
uri = evidence["source"].get("uri")
|
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("/"):
|
if not isinstance(uri, str) or uri.startswith("/") or "\\" in uri or ".." in uri.split("/"):
|
||||||
raise SystemExit(f"{relative}: noncanonical filesystem Evidence URI")
|
raise SystemExit(f"{relative}: noncanonical filesystem Evidence URI")
|
||||||
if uri != expected_uri:
|
if uri != expected_uri:
|
||||||
@@ -291,6 +292,15 @@ for path in descriptor_paths:
|
|||||||
raise SystemExit(f"{relative}: explicit filesystem Evidence object mismatch")
|
raise SystemExit(f"{relative}: explicit filesystem Evidence object mismatch")
|
||||||
|
|
||||||
contract = contract_path.read_text()
|
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):
|
def named_example(name):
|
||||||
match = re.search(
|
match = re.search(
|
||||||
@@ -306,7 +316,7 @@ examples = {
|
|||||||
"filesystem": {
|
"filesystem": {
|
||||||
"evidence": {
|
"evidence": {
|
||||||
"source": {
|
"source": {
|
||||||
"type": "filesystem", "uri": "workspace-content/example/evidence",
|
"type": "filesystem", "uri": "example/evidence",
|
||||||
"patterns": ["**/*.md"], "max_bytes": 10485760,
|
"patterns": ["**/*.md"], "max_bytes": 10485760,
|
||||||
},
|
},
|
||||||
"policy": {"max_chunk_chars": 4000, "retain_published_generations": 3},
|
"policy": {"max_chunk_chars": 4000, "retain_published_generations": 3},
|
||||||
@@ -355,9 +365,15 @@ required_contract_phrases = [
|
|||||||
"Content-only revision",
|
"Content-only revision",
|
||||||
"read-only Evidence summary",
|
"read-only Evidence summary",
|
||||||
"excludes Evidence bytes",
|
"excludes Evidence bytes",
|
||||||
|
"`schema_version` value `1`",
|
||||||
|
"It is authoritative for workspace ID,\nname, description, and display order.",
|
||||||
|
"The descriptor at `<id>/workspace.yaml` must match the\ncatalog metadata exactly.",
|
||||||
|
"Catalog-only entries without `<id>/workspace.yaml` are valid bootstrap slots and surface as\n`configuration_required`.",
|
||||||
|
"The API never writes `thoth-workspaces.yaml` or `<id>/evidence/**`.",
|
||||||
]
|
]
|
||||||
|
normalized_contract = normalize_space(contract)
|
||||||
for phrase in required_contract_phrases:
|
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}")
|
raise SystemExit(f"workspace Evidence contract lacks required rule: {phrase}")
|
||||||
|
|
||||||
mode_rules = {
|
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:
|
if "Endpoint-policy flags cannot be enabled without `endpoint_url`." not in contract:
|
||||||
raise SystemExit("missing S3 endpoint policy without endpoint invariant")
|
raise SystemExit("missing S3 endpoint policy without endpoint invariant")
|
||||||
|
|
||||||
# The canonical one-repository tree is exact, including generated docs outside workspaces/.
|
for forbidden in (
|
||||||
legacy_docs = re.compile(r"workspaces/(?:<[^>]+>|[^\s`/]+)\.(?:env\.example|md)")
|
"workspace-content/<id>/evidence",
|
||||||
all_public = "\n".join(path.read_text() for path in paths)
|
"workspaces/<id>.yaml",
|
||||||
if legacy_docs.search(all_public) or "workspaces/<id>.env.example" in all_public:
|
"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*(?:<id>|[a-z0-9-]+)/(?:(?:contract\.env\.example|README\.md))")
|
||||||
|
if "workspace-docs/<id>/{contract.env.example,README.md}" not in all_public:
|
||||||
raise SystemExit("generated docs path invalid")
|
raise SystemExit("generated docs path invalid")
|
||||||
|
for pattern in (
|
||||||
|
r"(?<!workspace-docs/)<id>/README\.md",
|
||||||
|
r"(?<!workspace-docs/)<id>/contract\.env\.example",
|
||||||
|
r"(?<!workspace-docs/)example/README\.md",
|
||||||
|
r"(?<!workspace-docs/)example/contract\.env\.example",
|
||||||
|
):
|
||||||
|
if re.search(pattern, contract):
|
||||||
|
raise SystemExit("generated docs path invalid")
|
||||||
required_tree_lines = [
|
required_tree_lines = [
|
||||||
"registry.git/", "├── workspaces/", "│ ├── example.yaml", "│ └── another.yaml",
|
"registry.git/", "├── thoth-workspaces.yaml", "├── example/", "│ ├── workspace.yaml",
|
||||||
"├── workspace-content/", "│ ├── example/evidence/...",
|
"│ └── evidence/...", "├── another/", "│ └── workspace.yaml", "└── workspace-docs/",
|
||||||
"│ └── another/evidence/...", "└── workspace-docs/",
|
|
||||||
" ├── example/{contract.env.example,README.md}",
|
" ├── example/{contract.env.example,README.md}",
|
||||||
" └── another/{contract.env.example,README.md}",
|
" └── another/{contract.env.example,README.md}",
|
||||||
]
|
]
|
||||||
if any(line not in contract for line in required_tree_lines):
|
if any(line not in contract for line in required_tree_lines):
|
||||||
raise SystemExit("missing canonical Evidence layout")
|
raise SystemExit("missing canonical Evidence layout")
|
||||||
|
|
||||||
|
|
||||||
def table_for(heading):
|
def table_for(heading):
|
||||||
match = re.search(rf"^## {re.escape(heading)}\s*$", contract, re.MULTILINE)
|
match = re.search(rf"^## {re.escape(heading)}\s*$", contract, re.MULTILINE)
|
||||||
if not match:
|
if not match:
|
||||||
@@ -403,17 +434,19 @@ def table_for(heading):
|
|||||||
|
|
||||||
relationships = {row[0]: row[1] for row in table_for("Registry revision and phase ownership")}
|
relationships = {row[0]: row[1] for row in table_for("Registry revision and phase ownership")}
|
||||||
revision_text = relationships.get("Revision identity", "")
|
revision_text = relationships.get("Revision identity", "")
|
||||||
if "same 40-hex Git commit" not in revision_text or "descriptor blob" not in revision_text or "root tree" not in revision_text:
|
if not all(token in revision_text for token in ("same 40-hex Git commit", "catalog blob", "descriptor blob", "root tree")):
|
||||||
raise SystemExit("missing same-revision ownership")
|
raise SystemExit("missing same-revision ownership")
|
||||||
if "Evidence-only commit" not in relationships.get("Content-only revision", "") or "revision.commit" not in relationships.get("Content-only revision", ""):
|
if "Evidence-only commit" not in relationships.get("Content-only revision", "") or "revision.commit" not in relationships.get("Content-only revision", ""):
|
||||||
raise SystemExit("missing content-only revision identity")
|
raise SystemExit("missing content-only revision identity")
|
||||||
p1 = relationships.get("P1", "")
|
if "docs-only" not in relationships.get("Docs-only sync commit", "").lower() or "workspace-docs/**" not in relationships.get("Docs-only sync commit", ""):
|
||||||
|
raise SystemExit("missing docs-only sync rule")
|
||||||
|
p11 = relationships.get("P1.1", "")
|
||||||
p6 = relationships.get("P6", "")
|
p6 = relationships.get("P6", "")
|
||||||
if not all(token in p1 for token in ("lexical URI", "Git tree", "same commit", "does not recursively inspect nested symlinks")):
|
if not all(token in p11 for token in ("lexical URI `<id>/evidence`", "Git tree", "same commit", "does not recursively inspect nested symlinks", "out of scope for P1.1")):
|
||||||
raise SystemExit("missing P1 lexical/tree ownership")
|
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")):
|
if not all(token in p6 for token in ("commit-addressed materialization", "realpath", "recursive containment", "nested-symlink", "race")):
|
||||||
raise SystemExit("missing P6 materialization ownership")
|
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_adverbs = r"(?:\s+(?:also|then|now|directly|itself))*"
|
||||||
p1_base_operation = r"""(?:
|
p1_base_operation = r"""(?:
|
||||||
acquire|materialize|extract|preprocess|index|retain|
|
acquire|materialize|extract|preprocess|index|retain|
|
||||||
@@ -439,15 +472,15 @@ p1_ownership = r"""(?:
|
|||||||
garbage[ -]collection
|
garbage[ -]collection
|
||||||
)"""
|
)"""
|
||||||
positive_p1_operation = re.compile(
|
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}|
|
(?:(?:can|may|must|will|should|does){p1_adverbs}\s+){p1_base_operation}|
|
||||||
{p1_third_person_operation}|
|
{p1_third_person_operation}|
|
||||||
{p1_ownership}
|
{p1_ownership}
|
||||||
)\b""",
|
)\b""",
|
||||||
re.IGNORECASE | re.VERBOSE,
|
re.IGNORECASE | re.VERBOSE,
|
||||||
)
|
)
|
||||||
if no_scope not in contract or positive_p1_operation.search(contract):
|
if normalize_space(no_scope) not in normalized_contract or positive_p1_operation.search(contract):
|
||||||
raise SystemExit("P1 scope violation")
|
raise SystemExit("P1.1 scope violation")
|
||||||
|
|
||||||
installation_rows = {row[0]: row[1:] for row in table_for("Installation files")}
|
installation_rows = {row[0]: row[1:] for row in table_for("Installation files")}
|
||||||
http_row = " ".join(installation_rows.get("Signed HTTP", []))
|
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")
|
raise SystemExit("separate automated/manual states missing")
|
||||||
|
|
||||||
flow_tokens = [
|
flow_tokens = [
|
||||||
"Clone the one shared registry", "workspace-content/<id>/evidence", "commit and push",
|
"Clone the one shared registry",
|
||||||
"Validate and publish the descriptor against that base commit",
|
"thoth-workspaces.yaml",
|
||||||
"workspace-docs/<id>/contract.env.example", "workspace-docs/<id>/README.md",
|
"<id>/workspace.yaml",
|
||||||
"Evidence `*_FILE` files outside Git", "THT_WORKSPACE_SECRET_ROOTS", "`*_SOURCE` paths",
|
"<id>/evidence/**",
|
||||||
"tht config check -c <path>", "P2/P6 later performs preprocessing and materialization",
|
"configuration_required",
|
||||||
|
"The API may create `<id>/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 `<id>/evidence/**`.",
|
||||||
|
"workspace-docs/<id>/contract.env.example",
|
||||||
|
"workspace-docs/<id>/README.md",
|
||||||
|
"Evidence `*_FILE` files outside Git",
|
||||||
|
"THT_WORKSPACE_SECRET_ROOTS",
|
||||||
|
"`*_SOURCE` paths",
|
||||||
|
"tht config check -c <path>",
|
||||||
|
"P2/P6 later performs preprocessing and materialization",
|
||||||
]
|
]
|
||||||
for guide in (local_path, server_path):
|
for guide in (local_path, server_path):
|
||||||
text = guide.read_text()
|
text = guide.read_text()
|
||||||
@@ -495,20 +538,58 @@ for guide in (local_path, server_path):
|
|||||||
raise SystemExit(f"{guide.name}: missing curator flow")
|
raise SystemExit(f"{guide.name}: missing curator flow")
|
||||||
section = match.group(1)
|
section = match.group(1)
|
||||||
positions = [section.find(token) for token in flow_tokens]
|
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")
|
raise SystemExit(f"{guide.name}: curator flow out of order")
|
||||||
|
|
||||||
# Public prose, YAML, and examples may name credential variables and describe forbidden shapes,
|
for guide in (local_path, server_path):
|
||||||
# but they must never contain a high-confidence access-key literal. Identifier-aware boundaries
|
guide_text = guide.read_text()
|
||||||
# avoid treating a legitimate variable name as a credential value.
|
if "authoritative for workspace ID, name, description, and\ndisplay order" not in guide_text:
|
||||||
|
raise SystemExit("missing catalog authority")
|
||||||
|
if "The API may create `<id>/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",
|
||||||
|
"<id>/workspace.yaml",
|
||||||
|
"<id>/evidence/**",
|
||||||
|
"workspace-docs/<id>/{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 `<id>/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/<id>.yaml <id>/workspace.yaml",
|
||||||
|
"git mv workspace-content/<id>/evidence <id>/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(
|
aws_access_key = re.compile(
|
||||||
r"(?<![A-Za-z0-9_])(?:AKIA|ASIA)[A-Z0-9]{16}(?![A-Za-z0-9_])"
|
r"(?<![A-Za-z0-9_])(?:AKIA|ASIA)[A-Z0-9]{16}(?![A-Za-z0-9_])"
|
||||||
)
|
)
|
||||||
if aws_access_key.search(all_public):
|
if aws_access_key.search(all_public):
|
||||||
raise SystemExit("credential literal forbidden")
|
raise SystemExit("credential literal forbidden")
|
||||||
|
|
||||||
# Parse dotenv assignments in the core example and fenced public guide blocks. Public examples may
|
|
||||||
# contain paths and query-free identities, but never credential values or unsafe placeholders.
|
|
||||||
def dotenv_lines(path):
|
def dotenv_lines(path):
|
||||||
text = path.read_text()
|
text = path.read_text()
|
||||||
if path == bindings_path:
|
if path == bindings_path:
|
||||||
@@ -523,6 +604,7 @@ def dotenv_lines(path):
|
|||||||
assignments.append((match.group(1), match.group(2).strip().strip("\"'")))
|
assignments.append((match.group(1), match.group(2).strip().strip("\"'")))
|
||||||
return assignments
|
return assignments
|
||||||
|
|
||||||
|
|
||||||
def safe_absolute(value):
|
def safe_absolute(value):
|
||||||
if not value.startswith("/") or "//" in value:
|
if not value.startswith("/") or "//" in value:
|
||||||
return False
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user