From c24ffcd23851fa9c804c42365e34625a29d7c578 Mon Sep 17 00:00:00 2001 From: mptyl Date: Sun, 9 Aug 2026 21:21:40 +0200 Subject: [PATCH] fix: reject modal P1 scope claims --- scripts/test-verify-workspace-install-docs.sh | 161 +++++++++++++----- scripts/verify-workspace-install-docs.sh | 42 +++-- 2 files changed, 145 insertions(+), 58 deletions(-) diff --git a/scripts/test-verify-workspace-install-docs.sh b/scripts/test-verify-workspace-install-docs.sh index f53399f1..bd7106bf 100755 --- a/scripts/test-verify-workspace-install-docs.sh +++ b/scripts/test-verify-workspace-install-docs.sh @@ -545,22 +545,48 @@ elif mutation == "p1-scope-inversion": 1, ) elif mutation.startswith("p1-append-"): - claims = { - "p1-append-acquisition": "P1 owns Evidence acquisition.", - "p1-append-materialization": "P1 owns Evidence materialization.", - "p1-append-extraction": "P1 owns Evidence extraction.", - "p1-append-preprocessing": "P1 owns Evidence preprocessing.", - "p1-append-embeddings": "P1 owns Evidence embeddings.", - "p1-append-qdrant-writes": "P1 owns Evidence Qdrant writes.", - "p1-append-indexing": "P1 owns Evidence indexing.", - "p1-append-active": "P1 owns Evidence `ACTIVE` publication.", - "p1-append-retention": "P1 owns Evidence retention.", - "p1-append-gc": "P1 owns Evidence GC.", + operations = { + "acquisition": ("acquire Evidence", "acquires Evidence", "Evidence acquisition"), + "materialization": ( + "materialize Evidence", "materializes Evidence", "Evidence materialization" + ), + "extraction": ("extract Evidence", "extracts Evidence", "Evidence extraction"), + "preprocessing": ( + "preprocess Evidence", "preprocesses Evidence", "Evidence preprocessing" + ), + "embeddings": ("create embeddings", "creates embeddings", "Evidence embeddings"), + "qdrant-writes": ( + "write embeddings to Qdrant", "writes embeddings to Qdrant", + "Evidence Qdrant writes", + ), + "indexing": ("index Evidence", "indexes Evidence", "Evidence indexing"), + "active": ("publish `ACTIVE`", "publishes `ACTIVE`", "Evidence `ACTIVE` publication"), + "retention": ("retain Evidence", "retains Evidence", "Evidence retention"), + "gc": ("garbage-collect Evidence", "garbage-collects Evidence", "Evidence GC"), } - claim = claims.get(mutation) - if claim is None: + forms = ( + "base", "third-person", "can", "may", "must", "will", "should", + "adverb-before-modal", "adverb-after-modal", "ownership", + ) + suffix = mutation.removeprefix("p1-append-") + operation = next((name for name in operations if suffix.startswith(f"{name}-")), None) + form = suffix.removeprefix(f"{operation}-") if operation else "" + if operation is None or form not in forms: raise SystemExit(f"unknown P1 append mutation: {mutation}") - changed = original + f"\n{claim}\n" + 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}.", + } + changed = original + f"\n{claims[form]}\n" elif mutation == "config-ordering": changed = original.replace( "tht config check -c ", "tht -c config check", 1 @@ -593,6 +619,38 @@ PY fi } +expect_evidence_claim_accepted() { + local label="$1" claim="$2" + local fixture_root="$negative_root/evidence-safe-${label// /-}" + local fixture_output="$fixture_root/output" + + mkdir -p \ + "$fixture_root/deploy/workspaces" \ + "$fixture_root/docs/contracts" \ + "$fixture_root/docs/install/examples" + 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" \ + "$fixture_root/docs/contracts/workspace-evidence-v3.md" + cp "$root/docs/install/local-workspace-registry.md" \ + "$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/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" + + set +e + verify_workspace_evidence_contract "$fixture_root" >"$fixture_output" 2>&1 + local status=$? + set -e + if [[ $status -ne 0 ]]; then + echo "safe Evidence fixture rejected: $label" >&2 + cat "$fixture_output" >&2 + negative_failures=$((negative_failures + 1)) + fi +} + expect_guide_rejected() { local label="$1" validator="$2" source_guide="$3" relative_path="$4" local mutation="$5" expected_error="$6" @@ -1001,36 +1059,51 @@ expect_evidence_fixture_rejected \ expect_evidence_fixture_rejected \ "P1 Evidence scope inversion" docs/contracts/workspace-evidence-v3.md p1-scope-inversion \ "P1 scope violation" -expect_evidence_fixture_rejected \ - "appended P1 acquisition claim" docs/contracts/workspace-evidence-v3.md p1-append-acquisition \ - "P1 scope violation" -expect_evidence_fixture_rejected \ - "appended P1 materialization claim" docs/contracts/workspace-evidence-v3.md p1-append-materialization \ - "P1 scope violation" -expect_evidence_fixture_rejected \ - "appended P1 extraction claim" docs/contracts/workspace-evidence-v3.md p1-append-extraction \ - "P1 scope violation" -expect_evidence_fixture_rejected \ - "appended P1 preprocessing claim" docs/contracts/workspace-evidence-v3.md p1-append-preprocessing \ - "P1 scope violation" -expect_evidence_fixture_rejected \ - "appended P1 embeddings claim" docs/contracts/workspace-evidence-v3.md p1-append-embeddings \ - "P1 scope violation" -expect_evidence_fixture_rejected \ - "appended P1 Qdrant writes claim" docs/contracts/workspace-evidence-v3.md p1-append-qdrant-writes \ - "P1 scope violation" -expect_evidence_fixture_rejected \ - "appended P1 indexing claim" docs/contracts/workspace-evidence-v3.md p1-append-indexing \ - "P1 scope violation" -expect_evidence_fixture_rejected \ - "appended P1 ACTIVE publication claim" docs/contracts/workspace-evidence-v3.md p1-append-active \ - "P1 scope violation" -expect_evidence_fixture_rejected \ - "appended P1 retention claim" docs/contracts/workspace-evidence-v3.md p1-append-retention \ - "P1 scope violation" -expect_evidence_fixture_rejected \ - "appended P1 garbage collection claim" docs/contracts/workspace-evidence-v3.md p1-append-gc \ - "P1 scope violation" +p1_operations=( + acquisition materialization extraction preprocessing embeddings + qdrant-writes indexing active retention gc +) +p1_positive_forms=( + base third-person can may must will should + adverb-before-modal adverb-after-modal ownership +) +for operation in "${p1_operations[@]}"; do + for form in "${p1_positive_forms[@]}"; do + expect_evidence_fixture_rejected \ + "appended P1 ${operation} ${form} claim" \ + docs/contracts/workspace-evidence-v3.md "p1-append-${operation}-${form}" \ + "P1 scope violation" + done +done +p1_safe_bases=( + "acquire Evidence" "materialize Evidence" "extract Evidence" "preprocess Evidence" + "create embeddings" "write embeddings to Qdrant" "index Evidence" 'publish `ACTIVE`' + "retain Evidence" "garbage-collect Evidence" +) +p1_safe_third_person=( + "acquires Evidence" "materializes Evidence" "extracts Evidence" "preprocesses Evidence" + "creates embeddings" "writes embeddings to Qdrant" "indexes Evidence" 'publishes `ACTIVE`' + "retains Evidence" "garbage-collects Evidence" +) +p1_safe_ownership=( + "Evidence acquisition" "Evidence materialization" "Evidence extraction" + "Evidence preprocessing" "Evidence embeddings" "Evidence Qdrant writes" + "Evidence indexing" 'Evidence `ACTIVE` publication' "Evidence retention" "Evidence GC" +) +for index in "${!p1_operations[@]}"; do + operation="${p1_operations[$index]}" + base="${p1_safe_bases[$index]}" + third_person="${p1_safe_third_person[$index]}" + ownership="${p1_safe_ownership[$index]}" + expect_evidence_claim_accepted "negative P1 ${operation} cannot" "P1 cannot ${base}." + expect_evidence_claim_accepted "negative P1 ${operation} must not" "P1 must not ${base}." + expect_evidence_claim_accepted "negative P1 ${operation} does not" "P1 does not ${base}." + expect_evidence_claim_accepted "negative P1 ${operation} never" "P1 never ${third_person}." + expect_evidence_claim_accepted \ + "later plan ${operation}" "A later plan may assign ${ownership} to P1." + expect_evidence_claim_accepted "P2 ${operation}" "P2 may directly ${base}." + expect_evidence_claim_accepted "P6 ${operation}" "P6 ${third_person}." +done expect_evidence_fixture_rejected \ "config check option reordered" docs/contracts/workspace-evidence-v3.md config-ordering \ "exact config-check ordering missing" diff --git a/scripts/verify-workspace-install-docs.sh b/scripts/verify-workspace-install-docs.sh index 4c4d5a73..b7b41a11 100755 --- a/scripts/verify-workspace-install-docs.sh +++ b/scripts/verify-workspace-install-docs.sh @@ -406,21 +406,35 @@ if not all(token in p1 for token in ("lexical URI", "Git tree", "same commit", " 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." +p1_adverbs = r"(?:\s+(?:also|then|now|directly|itself))*" +p1_base_operation = r"""(?: + acquire|materialize|extract|preprocess|index|retain| + (?:create|generate)\s+embeddings?| + write\s+(?:embeddings?\s+)?to\s+Qdrant| + publish\s+`?ACTIVE\b`?| + garbage[- ]collect| + (?:run|perform)\s+(?:retention|GC|garbage[ -]collection) +)""" +p1_third_person_operation = r"""(?: + acquires|materializes|extracts|preprocesses|indexes|retains| + (?:creates|generates)\s+embeddings?| + writes\s+(?:embeddings?\s+)?to\s+Qdrant| + publishes\s+`?ACTIVE\b`?| + garbage[- ]collects| + (?:runs|performs)\s+(?:retention|GC|garbage[ -]collection) +)""" +p1_ownership = r"""(?: + (?:owns|handles|performs)|is\s+responsible\s+for +)\s+(?:Evidence\s+)?(?: + acquisition|materialization|extraction|preprocessing|embeddings?| + Qdrant\s+writes?|indexing|`?ACTIVE`?\s+publication|retention|GC| + garbage[ -]collection +)""" positive_p1_operation = re.compile( - r"""\bP1\b(?:\s+(?:also|then|now|directly|itself))*\s+(?: - (?:will\s+|must\s+|may\s+|can\s+)?(?: - acquires?|materializes?|extracts?|preprocesses?|indexes?|retains?| - (?:creates?|generates?)\s+embeddings?| - writes?\s+(?:embeddings?\s+)?to\s+Qdrant| - publishes?\s+`?ACTIVE\b`?| - garbage[- ]collects?| - (?:runs?|performs?)\s+(?:retention|GC|garbage[ -]collection) - )| - (?:owns?|handles?|performs?|is\s+responsible\s+for)\s+(?:Evidence\s+)?(?: - acquisition|materialization|extraction|preprocessing|embeddings?| - Qdrant\s+writes?|indexing|`?ACTIVE`?\s+publication|retention|GC| - garbage[ -]collection - ) + rf"""\bP1\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, )