From 7921d92570c69d9685c9ac0e2af485edb3095b1a Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 12:55:05 +0200 Subject: [PATCH 01/11] :white_check_mark: test(examples): close REF-EX C5 quota-ceiling-from-fact explicitly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pack already wires facts.quota.max_partitions into bounded-change.yaml's partition-change-bounds rule (D-071 deferral note). This gives the quota-ceiling-from-fact pattern its own discoverable, both-polarity test directory plus a facts-omitted edge case (an unresolved quota fact must fail safe to REVIEW, never APPROVE — mirrors internal/adoptertest's own hermetic unresolved-fact testdata) and an examples/archetypes/quota-ceiling/ seed. --- .../base/topics/prod/analytics.clicks.v1.yaml | 14 ++++++++++++++ examples/archetypes/quota-ceiling/expected.yaml | 3 +++ examples/archetypes/quota-ceiling/facts.yaml | 5 +++++ .../head/topics/prod/analytics.clicks.v1.yaml | 11 +++++++++++ .../base/topics/prod/analytics.clicks.v1.yaml | 11 +++++++++++ .../quota-ceiling/negative/expected.yaml | 9 +++++++++ .../archetypes/quota-ceiling/negative/facts.yaml | 5 +++++ .../head/topics/prod/analytics.clicks.v1.yaml | 11 +++++++++++ .../base/topics/prod/analytics.clicks.v1.yaml | 16 ++++++++++++++++ .../tests/topics/quota-ceiling/expect.yaml | 8 ++++++++ .../base/topics/prod/analytics.clicks.v1.yaml | 16 ++++++++++++++++ .../quota-ceiling/facts-omitted/expect.yaml | 4 ++++ .../quota-ceiling/facts-omitted/facts.yaml | 14 ++++++++++++++ .../head/topics/prod/analytics.clicks.v1.yaml | 16 ++++++++++++++++ .../tests/topics/quota-ceiling/facts.yaml | 7 +++++++ .../head/topics/prod/analytics.clicks.v1.yaml | 16 ++++++++++++++++ .../base/topics/prod/analytics.clicks.v1.yaml | 16 ++++++++++++++++ .../topics/quota-ceiling/negative/expect.yaml | 6 ++++++ .../topics/quota-ceiling/negative/facts.yaml | 7 +++++++ .../head/topics/prod/analytics.clicks.v1.yaml | 16 ++++++++++++++++ 20 files changed, 211 insertions(+) create mode 100644 examples/archetypes/quota-ceiling/base/topics/prod/analytics.clicks.v1.yaml create mode 100644 examples/archetypes/quota-ceiling/expected.yaml create mode 100644 examples/archetypes/quota-ceiling/facts.yaml create mode 100644 examples/archetypes/quota-ceiling/head/topics/prod/analytics.clicks.v1.yaml create mode 100644 examples/archetypes/quota-ceiling/negative/base/topics/prod/analytics.clicks.v1.yaml create mode 100644 examples/archetypes/quota-ceiling/negative/expected.yaml create mode 100644 examples/archetypes/quota-ceiling/negative/facts.yaml create mode 100644 examples/archetypes/quota-ceiling/negative/head/topics/prod/analytics.clicks.v1.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/base/topics/prod/analytics.clicks.v1.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/expect.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/base/topics/prod/analytics.clicks.v1.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/expect.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/facts.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/head/topics/prod/analytics.clicks.v1.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/head/topics/prod/analytics.clicks.v1.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/base/topics/prod/analytics.clicks.v1.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/expect.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/facts.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/head/topics/prod/analytics.clicks.v1.yaml diff --git a/examples/archetypes/quota-ceiling/base/topics/prod/analytics.clicks.v1.yaml b/examples/archetypes/quota-ceiling/base/topics/prod/analytics.clicks.v1.yaml new file mode 100644 index 00000000..5e8a067a --- /dev/null +++ b/examples/archetypes/quota-ceiling/base/topics/prod/analytics.clicks.v1.yaml @@ -0,0 +1,14 @@ +# REF-EX C5 (quota-ceiling-from-fact) — a value change is bounded by a FACT the +# diff itself cannot see (an in-repo quota registry, stubbed here; see +# builtin/repo-file, REF-GAP-2). Governed entry: one topic per file. +name: analytics.clicks.v1 +owner: analytics-team +partitions: 10 +replication_factor: 3 +retention_hours: 72 +cleanup_policy: delete +schema: + format: avro + subject: analytics.clicks.v1-value +description: > + Clickstream events for the analytics domain. diff --git a/examples/archetypes/quota-ceiling/expected.yaml b/examples/archetypes/quota-ceiling/expected.yaml new file mode 100644 index 00000000..a83b84bf --- /dev/null +++ b/examples/archetypes/quota-ceiling/expected.yaml @@ -0,0 +1,3 @@ +# partitions 10 -> 18 (<= stubbed quota 20). Satisfied prove -> no finding. +decision: APPROVE +findings: [] diff --git a/examples/archetypes/quota-ceiling/facts.yaml b/examples/archetypes/quota-ceiling/facts.yaml new file mode 100644 index 00000000..2c567b52 --- /dev/null +++ b/examples/archetypes/quota-ceiling/facts.yaml @@ -0,0 +1,5 @@ +author: + login: alice + groups: [analytics-team] +quota: + max_partitions: 20 diff --git a/examples/archetypes/quota-ceiling/head/topics/prod/analytics.clicks.v1.yaml b/examples/archetypes/quota-ceiling/head/topics/prod/analytics.clicks.v1.yaml new file mode 100644 index 00000000..93881469 --- /dev/null +++ b/examples/archetypes/quota-ceiling/head/topics/prod/analytics.clicks.v1.yaml @@ -0,0 +1,11 @@ +name: analytics.clicks.v1 +owner: analytics-team +partitions: 18 +replication_factor: 3 +retention_hours: 72 +cleanup_policy: delete +schema: + format: avro + subject: analytics.clicks.v1-value +description: > + Clickstream events for the analytics domain. diff --git a/examples/archetypes/quota-ceiling/negative/base/topics/prod/analytics.clicks.v1.yaml b/examples/archetypes/quota-ceiling/negative/base/topics/prod/analytics.clicks.v1.yaml new file mode 100644 index 00000000..baf7b64e --- /dev/null +++ b/examples/archetypes/quota-ceiling/negative/base/topics/prod/analytics.clicks.v1.yaml @@ -0,0 +1,11 @@ +name: analytics.clicks.v1 +owner: analytics-team +partitions: 10 +replication_factor: 3 +retention_hours: 72 +cleanup_policy: delete +schema: + format: avro + subject: analytics.clicks.v1-value +description: > + Clickstream events for the analytics domain. diff --git a/examples/archetypes/quota-ceiling/negative/expected.yaml b/examples/archetypes/quota-ceiling/negative/expected.yaml new file mode 100644 index 00000000..faab47c0 --- /dev/null +++ b/examples/archetypes/quota-ceiling/negative/expected.yaml @@ -0,0 +1,9 @@ +# partitions 10 -> 34 exceeds the stubbed quota ceiling (20). +decision: REVIEW +findings: + - rule: partition-change-bounds + obligation: bounded-change + effect: challenge + subject: topic-registry:analytics.clicks.v1 + points: 1 + code: bounded-change.out-of-band diff --git a/examples/archetypes/quota-ceiling/negative/facts.yaml b/examples/archetypes/quota-ceiling/negative/facts.yaml new file mode 100644 index 00000000..2c567b52 --- /dev/null +++ b/examples/archetypes/quota-ceiling/negative/facts.yaml @@ -0,0 +1,5 @@ +author: + login: alice + groups: [analytics-team] +quota: + max_partitions: 20 diff --git a/examples/archetypes/quota-ceiling/negative/head/topics/prod/analytics.clicks.v1.yaml b/examples/archetypes/quota-ceiling/negative/head/topics/prod/analytics.clicks.v1.yaml new file mode 100644 index 00000000..53ab292a --- /dev/null +++ b/examples/archetypes/quota-ceiling/negative/head/topics/prod/analytics.clicks.v1.yaml @@ -0,0 +1,11 @@ +name: analytics.clicks.v1 +owner: analytics-team +partitions: 34 +replication_factor: 3 +retention_hours: 72 +cleanup_policy: delete +schema: + format: avro + subject: analytics.clicks.v1-value +description: > + Clickstream events for the analytics domain. diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/base/topics/prod/analytics.clicks.v1.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/base/topics/prod/analytics.clicks.v1.yaml new file mode 100644 index 00000000..7039516d --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/base/topics/prod/analytics.clicks.v1.yaml @@ -0,0 +1,16 @@ +analytics.clicks.v1: + owner: analytics-team + partitions: 10 + replication_factor: 3 + retention_hours: 72 + cleanup_policy: delete + schema: + format: avro + subject: analytics.clicks.v1-value + compatibility: BACKWARD + references: + analytics.common: 1 + compaction: + strategy: delete + description: > + Clickstream events for the analytics domain, quota-ceiling C5 demonstrator. diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/expect.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/expect.yaml new file mode 100644 index 00000000..01e91566 --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/expect.yaml @@ -0,0 +1,8 @@ +# REF-EX C5 (quota-ceiling-from-fact), explicit case: partitions 10 -> 18, within +# the stubbed facts.quota.max_partitions ceiling (20). Reuses the SAME +# partition-change-bounds rule bounded-change.yaml already wires to +# facts.quota.max_partitions.value (D-071 deferral note) — this case just gives +# the pattern its own discoverable name so C5 is not only implicit inside +# bounded-change/. Satisfied prove -> no finding. +decision: APPROVE +findings: [] diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/base/topics/prod/analytics.clicks.v1.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/base/topics/prod/analytics.clicks.v1.yaml new file mode 100644 index 00000000..7039516d --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/base/topics/prod/analytics.clicks.v1.yaml @@ -0,0 +1,16 @@ +analytics.clicks.v1: + owner: analytics-team + partitions: 10 + replication_factor: 3 + retention_hours: 72 + cleanup_policy: delete + schema: + format: avro + subject: analytics.clicks.v1-value + compatibility: BACKWARD + references: + analytics.common: 1 + compaction: + strategy: delete + description: > + Clickstream events for the analytics domain, quota-ceiling C5 demonstrator. diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/expect.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/expect.yaml new file mode 100644 index 00000000..3b3cca7b --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/expect.yaml @@ -0,0 +1,4 @@ +# quota fact omitted -> the ceiling predicate errors on an unresolved fact -> the +# engine fails safe to REVIEW, never APPROVE, even though the raw partition change +# (10 -> 18) would have been within band had the fact resolved. +decision: REVIEW diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/facts.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/facts.yaml new file mode 100644 index 00000000..7263506e --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/facts.yaml @@ -0,0 +1,14 @@ +# Deliberately omits `quota`: no provider resolved facts.quota.max_partitions. +# REQ-EX-S07-01 edge — an omitted controlling fact must not APPROVE. Reading +# facts.quota.max_partitions.value on the absent fact errors in cel-go +# (internal/adoptertest.MapFacts never fabricates a resolved value for an absent +# provider/name), and the engine fails the prove `when` safe -> onFailure +# (challenge) -> REVIEW. This is existing E2 fail-safe behaviour (mirrored by +# internal/adoptertest/testdata/repo/.assent/tests/capped/unresolved-fact/), pinned +# here against the SAME quota-ceiling rule so the pattern's fail-safe edge is +# demonstrated in-place rather than only in the harness's own hermetic testdata. +author: + login: alice + groups: [analytics-team] +schema: + valid: true diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/head/topics/prod/analytics.clicks.v1.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/head/topics/prod/analytics.clicks.v1.yaml new file mode 100644 index 00000000..fe2986de --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts-omitted/head/topics/prod/analytics.clicks.v1.yaml @@ -0,0 +1,16 @@ +analytics.clicks.v1: + owner: analytics-team + partitions: 18 + replication_factor: 3 + retention_hours: 72 + cleanup_policy: delete + schema: + format: avro + subject: analytics.clicks.v1-value + compatibility: BACKWARD + references: + analytics.common: 1 + compaction: + strategy: delete + description: > + Clickstream events for the analytics domain, quota-ceiling C5 demonstrator. diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts.yaml new file mode 100644 index 00000000..283d485f --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/facts.yaml @@ -0,0 +1,7 @@ +author: + login: alice + groups: [analytics-team] +quota: + max_partitions: 20 +schema: + valid: true diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/head/topics/prod/analytics.clicks.v1.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/head/topics/prod/analytics.clicks.v1.yaml new file mode 100644 index 00000000..fe2986de --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/head/topics/prod/analytics.clicks.v1.yaml @@ -0,0 +1,16 @@ +analytics.clicks.v1: + owner: analytics-team + partitions: 18 + replication_factor: 3 + retention_hours: 72 + cleanup_policy: delete + schema: + format: avro + subject: analytics.clicks.v1-value + compatibility: BACKWARD + references: + analytics.common: 1 + compaction: + strategy: delete + description: > + Clickstream events for the analytics domain, quota-ceiling C5 demonstrator. diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/base/topics/prod/analytics.clicks.v1.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/base/topics/prod/analytics.clicks.v1.yaml new file mode 100644 index 00000000..7039516d --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/base/topics/prod/analytics.clicks.v1.yaml @@ -0,0 +1,16 @@ +analytics.clicks.v1: + owner: analytics-team + partitions: 10 + replication_factor: 3 + retention_hours: 72 + cleanup_policy: delete + schema: + format: avro + subject: analytics.clicks.v1-value + compatibility: BACKWARD + references: + analytics.common: 1 + compaction: + strategy: delete + description: > + Clickstream events for the analytics domain, quota-ceiling C5 demonstrator. diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/expect.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/expect.yaml new file mode 100644 index 00000000..06758bd6 --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/expect.yaml @@ -0,0 +1,6 @@ +# partitions 10 -> 34 exceeds the stubbed quota ceiling (20) -> challenge -> REVIEW. +decision: REVIEW +findings: + - rule: partition-change-bounds + obligation: bounded-change + effect: challenge diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/facts.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/facts.yaml new file mode 100644 index 00000000..283d485f --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/facts.yaml @@ -0,0 +1,7 @@ +author: + login: alice + groups: [analytics-team] +quota: + max_partitions: 20 +schema: + valid: true diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/head/topics/prod/analytics.clicks.v1.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/head/topics/prod/analytics.clicks.v1.yaml new file mode 100644 index 00000000..83f7d022 --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/negative/head/topics/prod/analytics.clicks.v1.yaml @@ -0,0 +1,16 @@ +analytics.clicks.v1: + owner: analytics-team + partitions: 34 + replication_factor: 3 + retention_hours: 72 + cleanup_policy: delete + schema: + format: avro + subject: analytics.clicks.v1-value + compatibility: BACKWARD + references: + analytics.common: 1 + compaction: + strategy: delete + description: > + Clickstream events for the analytics domain, quota-ceiling C5 demonstrator. From 9f9ab82c12ac6529f040443b392b552b5b16ed8d Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 12:55:16 +0200 Subject: [PATCH 02/11] :sparkles: feat(examples): close REF-EX C6 placement allow-list (infra-vars) New instance-set-allow-list rule (obligation `placement`) checks the S04 `instance_set` workload field against a stubbed facts.placement.allowed.value list. Pointer-scoped to /workloads/*/instance_set so every existing S01/S04 goldens stay vacuous (untouched). Added to bindings.yaml require: (vacuously satisfied on all pre-existing cases). Both polarities pass and earn genuine --coverage credit; examples/archetypes/placement-allow-list/ seeds the pattern. --- .../base/workloads/prod/orders-api.yaml | 9 ++++ .../placement-allow-list/expected.yaml | 4 ++ .../placement-allow-list/facts.yaml | 5 ++ .../head/workloads/prod/orders-api.yaml | 5 ++ .../base/workloads/prod/orders-api.yaml | 5 ++ .../negative/expected.yaml | 9 ++++ .../placement-allow-list/negative/facts.yaml | 5 ++ .../head/workloads/prod/orders-api.yaml | 5 ++ .../packs/infra-vars/.assent/bindings.yaml | 4 +- .../vars/rules/placement-allow-list.yaml | 29 +++++++++++ .../placement/base/envs/prod/compute.tfvars | 48 +++++++++++++++++++ .../.assent/tests/vars/placement/expect.yaml | 4 ++ .../.assent/tests/vars/placement/facts.yaml | 5 ++ .../placement/head/envs/prod/compute.tfvars | 48 +++++++++++++++++++ .../negative/base/envs/prod/compute.tfvars | 48 +++++++++++++++++++ .../tests/vars/placement/negative/expect.yaml | 7 +++ .../tests/vars/placement/negative/facts.yaml | 5 ++ .../negative/head/envs/prod/compute.tfvars | 48 +++++++++++++++++++ 18 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 examples/archetypes/placement-allow-list/base/workloads/prod/orders-api.yaml create mode 100644 examples/archetypes/placement-allow-list/expected.yaml create mode 100644 examples/archetypes/placement-allow-list/facts.yaml create mode 100644 examples/archetypes/placement-allow-list/head/workloads/prod/orders-api.yaml create mode 100644 examples/archetypes/placement-allow-list/negative/base/workloads/prod/orders-api.yaml create mode 100644 examples/archetypes/placement-allow-list/negative/expected.yaml create mode 100644 examples/archetypes/placement-allow-list/negative/facts.yaml create mode 100644 examples/archetypes/placement-allow-list/negative/head/workloads/prod/orders-api.yaml create mode 100644 examples/packs/infra-vars/.assent/packs/vars/rules/placement-allow-list.yaml create mode 100644 examples/packs/infra-vars/.assent/tests/vars/placement/base/envs/prod/compute.tfvars create mode 100644 examples/packs/infra-vars/.assent/tests/vars/placement/expect.yaml create mode 100644 examples/packs/infra-vars/.assent/tests/vars/placement/facts.yaml create mode 100644 examples/packs/infra-vars/.assent/tests/vars/placement/head/envs/prod/compute.tfvars create mode 100644 examples/packs/infra-vars/.assent/tests/vars/placement/negative/base/envs/prod/compute.tfvars create mode 100644 examples/packs/infra-vars/.assent/tests/vars/placement/negative/expect.yaml create mode 100644 examples/packs/infra-vars/.assent/tests/vars/placement/negative/facts.yaml create mode 100644 examples/packs/infra-vars/.assent/tests/vars/placement/negative/head/envs/prod/compute.tfvars diff --git a/examples/archetypes/placement-allow-list/base/workloads/prod/orders-api.yaml b/examples/archetypes/placement-allow-list/base/workloads/prod/orders-api.yaml new file mode 100644 index 00000000..b46311b7 --- /dev/null +++ b/examples/archetypes/placement-allow-list/base/workloads/prod/orders-api.yaml @@ -0,0 +1,9 @@ +# REF-EX C6 (placement allow-list) — a value change is bounded by an allow-list +# FACT the diff itself cannot see (an in-repo placement/capacity registry, +# stubbed here; see builtin/repo-file, REF-GAP-2). Governed entry: one workload +# per file. +name: orders-api +owner: orders-team +instance_set: standard-4 +min_replicas: 3 +max_replicas: 12 diff --git a/examples/archetypes/placement-allow-list/expected.yaml b/examples/archetypes/placement-allow-list/expected.yaml new file mode 100644 index 00000000..efde3d7c --- /dev/null +++ b/examples/archetypes/placement-allow-list/expected.yaml @@ -0,0 +1,4 @@ +# instance_set standard-4 -> standard-8, both on the stubbed allow-list. +# Satisfied prove -> no finding. +decision: APPROVE +findings: [] diff --git a/examples/archetypes/placement-allow-list/facts.yaml b/examples/archetypes/placement-allow-list/facts.yaml new file mode 100644 index 00000000..39ece203 --- /dev/null +++ b/examples/archetypes/placement-allow-list/facts.yaml @@ -0,0 +1,5 @@ +author: + login: alice + groups: [orders-team] +placement: + allowed: [standard-2, standard-4, standard-8] diff --git a/examples/archetypes/placement-allow-list/head/workloads/prod/orders-api.yaml b/examples/archetypes/placement-allow-list/head/workloads/prod/orders-api.yaml new file mode 100644 index 00000000..8903020e --- /dev/null +++ b/examples/archetypes/placement-allow-list/head/workloads/prod/orders-api.yaml @@ -0,0 +1,5 @@ +name: orders-api +owner: orders-team +instance_set: standard-8 +min_replicas: 3 +max_replicas: 12 diff --git a/examples/archetypes/placement-allow-list/negative/base/workloads/prod/orders-api.yaml b/examples/archetypes/placement-allow-list/negative/base/workloads/prod/orders-api.yaml new file mode 100644 index 00000000..386769d5 --- /dev/null +++ b/examples/archetypes/placement-allow-list/negative/base/workloads/prod/orders-api.yaml @@ -0,0 +1,5 @@ +name: orders-api +owner: orders-team +instance_set: standard-4 +min_replicas: 3 +max_replicas: 12 diff --git a/examples/archetypes/placement-allow-list/negative/expected.yaml b/examples/archetypes/placement-allow-list/negative/expected.yaml new file mode 100644 index 00000000..5f202f56 --- /dev/null +++ b/examples/archetypes/placement-allow-list/negative/expected.yaml @@ -0,0 +1,9 @@ +# instance_set standard-4 -> gpu-16, not on the stubbed allow-list. +decision: REVIEW +findings: + - rule: instance-set-allow-list + obligation: placement + effect: require-review + subject: infra-vars:orders-api + points: 0 + code: placement.disallowed-instance-set diff --git a/examples/archetypes/placement-allow-list/negative/facts.yaml b/examples/archetypes/placement-allow-list/negative/facts.yaml new file mode 100644 index 00000000..39ece203 --- /dev/null +++ b/examples/archetypes/placement-allow-list/negative/facts.yaml @@ -0,0 +1,5 @@ +author: + login: alice + groups: [orders-team] +placement: + allowed: [standard-2, standard-4, standard-8] diff --git a/examples/archetypes/placement-allow-list/negative/head/workloads/prod/orders-api.yaml b/examples/archetypes/placement-allow-list/negative/head/workloads/prod/orders-api.yaml new file mode 100644 index 00000000..2473c9a2 --- /dev/null +++ b/examples/archetypes/placement-allow-list/negative/head/workloads/prod/orders-api.yaml @@ -0,0 +1,5 @@ +name: orders-api +owner: orders-team +instance_set: gpu-16 +min_replicas: 3 +max_replicas: 12 diff --git a/examples/packs/infra-vars/.assent/bindings.yaml b/examples/packs/infra-vars/.assent/bindings.yaml index 8a83804f..8d525cc5 100644 --- a/examples/packs/infra-vars/.assent/bindings.yaml +++ b/examples/packs/infra-vars/.assent/bindings.yaml @@ -5,9 +5,9 @@ bindings: environment: dev packs: [vars] risk: { threshold: 10 } - require: [ownership, bounded-change] + require: [ownership, bounded-change, placement] - class: infra-vars environment: prod packs: [vars] risk: { threshold: 4 } - require: [ownership, bounded-change] + require: [ownership, bounded-change, placement] diff --git a/examples/packs/infra-vars/.assent/packs/vars/rules/placement-allow-list.yaml b/examples/packs/infra-vars/.assent/packs/vars/rules/placement-allow-list.yaml new file mode 100644 index 00000000..06919502 --- /dev/null +++ b/examples/packs/infra-vars/.assent/packs/vars/rules/placement-allow-list.yaml @@ -0,0 +1,29 @@ +# Placement allow-list (REF-EX C6) — instance_set (a keyed workload scalar, EX-S04) +# must stay on a fact-sourced allow-list. facts.placement.allowed.value is a stub +# for a real placement/capacity registry (REF-GAP-2 shape, builtin/repo-file): +# an in-repo-state fact no live provider is called for under `assent test` +# (ADR-0014). Pointer-scoped to /workloads/*/instance_set so this rule is vacuous +# (no match at all) on every existing S01/S04 case, none of which touch +# instance_set — the shipped goldens stay green untouched. +apiVersion: assent.dev/v1alpha1 +kind: MergePolicy +metadata: + name: vars-placement-allow-list +spec: + entries: + infra-vars: + mode: map + root: /workloads + rules: + - name: instance-set-allow-list + phase: enforce + match: + valueChanges: + pointers: ["/workloads/*/instance_set"] + kinds: [modify] + prove: + obligation: placement + when: 'new in facts.placement.allowed.value' + onFailure: + effect: require-review + code: placement.disallowed-instance-set diff --git a/examples/packs/infra-vars/.assent/tests/vars/placement/base/envs/prod/compute.tfvars b/examples/packs/infra-vars/.assent/tests/vars/placement/base/envs/prod/compute.tfvars new file mode 100644 index 00000000..3bfa2ddb --- /dev/null +++ b/examples/packs/infra-vars/.assent/tests/vars/placement/base/envs/prod/compute.tfvars @@ -0,0 +1,48 @@ +# REF-EX C6 (placement allow-list) — instance_set is the governed pointer. +workloads = { + orders-api = { + owner = "orders-team" + instance_set = "standard-4" + min_replicas = 3 + max_replicas = 12 + memory_mb = 2048 + resources = { + cpu = 500 + memory_mb = 2048 + } + labels = { + team = "orders-team" + tier = "prod" + } + } + payments-gateway = { + owner = "payments-team" + instance_set = "standard-8" + min_replicas = 4 + max_replicas = 16 + memory_mb = 4096 + resources = { + cpu = 1000 + memory_mb = 4096 + } + labels = { + team = "payments-team" + tier = "prod" + } + } + inventory-projector = { + owner = "inventory-team" + instance_set = "standard-2" + min_replicas = 2 + max_replicas = 6 + memory_mb = 1024 + resources = { + cpu = 250 + memory_mb = 1024 + } + labels = { + team = "inventory-team" + tier = "prod" + } + } +} diff --git a/examples/packs/infra-vars/.assent/tests/vars/placement/expect.yaml b/examples/packs/infra-vars/.assent/tests/vars/placement/expect.yaml new file mode 100644 index 00000000..af70f2b7 --- /dev/null +++ b/examples/packs/infra-vars/.assent/tests/vars/placement/expect.yaml @@ -0,0 +1,4 @@ +# orders-api instance_set standard-4 -> standard-8, both on the stubbed +# facts.placement.allowed.value list -> instance-set-allow-list proves silently. +decision: APPROVE +findings: [] diff --git a/examples/packs/infra-vars/.assent/tests/vars/placement/facts.yaml b/examples/packs/infra-vars/.assent/tests/vars/placement/facts.yaml new file mode 100644 index 00000000..39ece203 --- /dev/null +++ b/examples/packs/infra-vars/.assent/tests/vars/placement/facts.yaml @@ -0,0 +1,5 @@ +author: + login: alice + groups: [orders-team] +placement: + allowed: [standard-2, standard-4, standard-8] diff --git a/examples/packs/infra-vars/.assent/tests/vars/placement/head/envs/prod/compute.tfvars b/examples/packs/infra-vars/.assent/tests/vars/placement/head/envs/prod/compute.tfvars new file mode 100644 index 00000000..78464a24 --- /dev/null +++ b/examples/packs/infra-vars/.assent/tests/vars/placement/head/envs/prod/compute.tfvars @@ -0,0 +1,48 @@ +# REF-EX C6 (placement allow-list) — instance_set is the governed pointer. +workloads = { + orders-api = { + owner = "orders-team" + instance_set = "standard-8" + min_replicas = 3 + max_replicas = 12 + memory_mb = 2048 + resources = { + cpu = 500 + memory_mb = 2048 + } + labels = { + team = "orders-team" + tier = "prod" + } + } + payments-gateway = { + owner = "payments-team" + instance_set = "standard-8" + min_replicas = 4 + max_replicas = 16 + memory_mb = 4096 + resources = { + cpu = 1000 + memory_mb = 4096 + } + labels = { + team = "payments-team" + tier = "prod" + } + } + inventory-projector = { + owner = "inventory-team" + instance_set = "standard-2" + min_replicas = 2 + max_replicas = 6 + memory_mb = 1024 + resources = { + cpu = 250 + memory_mb = 1024 + } + labels = { + team = "inventory-team" + tier = "prod" + } + } +} diff --git a/examples/packs/infra-vars/.assent/tests/vars/placement/negative/base/envs/prod/compute.tfvars b/examples/packs/infra-vars/.assent/tests/vars/placement/negative/base/envs/prod/compute.tfvars new file mode 100644 index 00000000..db339e98 --- /dev/null +++ b/examples/packs/infra-vars/.assent/tests/vars/placement/negative/base/envs/prod/compute.tfvars @@ -0,0 +1,48 @@ +# REF-EX C6 (placement allow-list) negative — instance_set is the governed pointer. +workloads = { + orders-api = { + owner = "orders-team" + instance_set = "standard-4" + min_replicas = 3 + max_replicas = 12 + memory_mb = 2048 + resources = { + cpu = 500 + memory_mb = 2048 + } + labels = { + team = "orders-team" + tier = "prod" + } + } + payments-gateway = { + owner = "payments-team" + instance_set = "standard-8" + min_replicas = 4 + max_replicas = 16 + memory_mb = 4096 + resources = { + cpu = 1000 + memory_mb = 4096 + } + labels = { + team = "payments-team" + tier = "prod" + } + } + inventory-projector = { + owner = "inventory-team" + instance_set = "standard-2" + min_replicas = 2 + max_replicas = 6 + memory_mb = 1024 + resources = { + cpu = 250 + memory_mb = 1024 + } + labels = { + team = "inventory-team" + tier = "prod" + } + } +} diff --git a/examples/packs/infra-vars/.assent/tests/vars/placement/negative/expect.yaml b/examples/packs/infra-vars/.assent/tests/vars/placement/negative/expect.yaml new file mode 100644 index 00000000..cb12bf54 --- /dev/null +++ b/examples/packs/infra-vars/.assent/tests/vars/placement/negative/expect.yaml @@ -0,0 +1,7 @@ +# orders-api instance_set standard-4 -> gpu-16, not on the stubbed +# facts.placement.allowed.value list -> instance-set-allow-list fails -> REVIEW. +decision: REVIEW +findings: + - rule: instance-set-allow-list + obligation: placement + effect: require-review diff --git a/examples/packs/infra-vars/.assent/tests/vars/placement/negative/facts.yaml b/examples/packs/infra-vars/.assent/tests/vars/placement/negative/facts.yaml new file mode 100644 index 00000000..39ece203 --- /dev/null +++ b/examples/packs/infra-vars/.assent/tests/vars/placement/negative/facts.yaml @@ -0,0 +1,5 @@ +author: + login: alice + groups: [orders-team] +placement: + allowed: [standard-2, standard-4, standard-8] diff --git a/examples/packs/infra-vars/.assent/tests/vars/placement/negative/head/envs/prod/compute.tfvars b/examples/packs/infra-vars/.assent/tests/vars/placement/negative/head/envs/prod/compute.tfvars new file mode 100644 index 00000000..5167d2e9 --- /dev/null +++ b/examples/packs/infra-vars/.assent/tests/vars/placement/negative/head/envs/prod/compute.tfvars @@ -0,0 +1,48 @@ +# REF-EX C6 (placement allow-list) negative — instance_set is the governed pointer. +workloads = { + orders-api = { + owner = "orders-team" + instance_set = "gpu-16" + min_replicas = 3 + max_replicas = 12 + memory_mb = 2048 + resources = { + cpu = 500 + memory_mb = 2048 + } + labels = { + team = "orders-team" + tier = "prod" + } + } + payments-gateway = { + owner = "payments-team" + instance_set = "standard-8" + min_replicas = 4 + max_replicas = 16 + memory_mb = 4096 + resources = { + cpu = 1000 + memory_mb = 4096 + } + labels = { + team = "payments-team" + tier = "prod" + } + } + inventory-projector = { + owner = "inventory-team" + instance_set = "standard-2" + min_replicas = 2 + max_replicas = 6 + memory_mb = 1024 + resources = { + cpu = 250 + memory_mb = 1024 + } + labels = { + team = "inventory-team" + tier = "prod" + } + } +} From e6367fddde66eb2ae814e4460d35edf12ca7e34f Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 12:55:32 +0200 Subject: [PATCH 03/11] :sparkles: feat(examples): close REF-EX C7 referenced-resource-ownership (gap demo) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New referenced-resource-ownership rule (obligation `resource-ownership`) checks a stubbed facts.resource_owner.owner.value (builtin/resource-owner shape, REF-GAP-1) against facts.author.groups.value whenever a topic's acl.resource names another team's resource. Pointer-scoped to /*/acl/resource so the rule is vacuous on every topic that never declares it — including S06's wildcard-grant goldens, which already populate acl.grants without acl.resource; a bare `!has(entry.acl)` guard (the spec's own shorthand) would NOT have been vacuous there, so pointer-scoping is used instead (documented in the rule file). Not a new builtin: facts.resource_owner.owner.value is a fixture literal, never resolved from the referenced resource's own file. acl.owner is carried alongside acl.resource as the entry's self-declared, untrusted claim — the predicate reads only the resolved fact. Both polarities pass and earn genuine --coverage credit; examples/archetypes/ referenced-resource-ownership/ seeds the pattern. --- .../topics/prod/connectors.status.v1.yaml | 21 +++++++++ .../expected.yaml | 4 ++ .../referenced-resource-ownership/facts.yaml | 5 ++ .../topics/prod/connectors.status.v1.yaml | 16 +++++++ .../topics/prod/connectors.status.v1.yaml | 14 ++++++ .../negative/expected.yaml | 10 ++++ .../negative/facts.yaml | 9 ++++ .../topics/prod/connectors.status.v1.yaml | 16 +++++++ .../topic-registry/.assent/bindings.yaml | 4 +- .../rules/referenced-resource-ownership.yaml | 46 +++++++++++++++++++ .../topics/prod/connectors.status.v1.yaml | 23 ++++++++++ .../topics/resource-ownership/expect.yaml | 5 ++ .../topics/resource-ownership/facts.yaml | 9 ++++ .../topics/prod/connectors.status.v1.yaml | 25 ++++++++++ .../topics/prod/connectors.status.v1.yaml | 23 ++++++++++ .../resource-ownership/negative/expect.yaml | 8 ++++ .../resource-ownership/negative/facts.yaml | 13 ++++++ .../topics/prod/connectors.status.v1.yaml | 25 ++++++++++ 18 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 examples/archetypes/referenced-resource-ownership/base/topics/prod/connectors.status.v1.yaml create mode 100644 examples/archetypes/referenced-resource-ownership/expected.yaml create mode 100644 examples/archetypes/referenced-resource-ownership/facts.yaml create mode 100644 examples/archetypes/referenced-resource-ownership/head/topics/prod/connectors.status.v1.yaml create mode 100644 examples/archetypes/referenced-resource-ownership/negative/base/topics/prod/connectors.status.v1.yaml create mode 100644 examples/archetypes/referenced-resource-ownership/negative/expected.yaml create mode 100644 examples/archetypes/referenced-resource-ownership/negative/facts.yaml create mode 100644 examples/archetypes/referenced-resource-ownership/negative/head/topics/prod/connectors.status.v1.yaml create mode 100644 examples/packs/topic-registry/.assent/packs/topics/rules/referenced-resource-ownership.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/resource-ownership/base/topics/prod/connectors.status.v1.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/resource-ownership/expect.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/resource-ownership/facts.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/resource-ownership/head/topics/prod/connectors.status.v1.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/base/topics/prod/connectors.status.v1.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/expect.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/facts.yaml create mode 100644 examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/head/topics/prod/connectors.status.v1.yaml diff --git a/examples/archetypes/referenced-resource-ownership/base/topics/prod/connectors.status.v1.yaml b/examples/archetypes/referenced-resource-ownership/base/topics/prod/connectors.status.v1.yaml new file mode 100644 index 00000000..762a82ad --- /dev/null +++ b/examples/archetypes/referenced-resource-ownership/base/topics/prod/connectors.status.v1.yaml @@ -0,0 +1,21 @@ +# REF-EX C7 (referenced-resource-ownership, judgment call (b)) — a GAP demo, not +# a new builtin: an ACL entry naming another team's resource cannot be +# authorized from the diff alone. facts.resource_owner.owner stubs the shipped +# builtin/resource-owner shape (REF-GAP-1) as the ground truth. Vacuous when the +# entry carries no acl.resource (this base has acl.grants but no acl.resource +# yet; head adds resource/owner as LEAF keys under the same existing acl +# mapping — adding acl as a whole new subtree would go opaque, E1-S05). +name: connectors.status.v1 +owner: connectors-team +partitions: 6 +replication_factor: 3 +retention_hours: 168 +cleanup_policy: delete +schema: + format: avro + subject: connectors.status.v1-value +description: > + Connector health/status events. +acl: + grants: + connectors-team: read diff --git a/examples/archetypes/referenced-resource-ownership/expected.yaml b/examples/archetypes/referenced-resource-ownership/expected.yaml new file mode 100644 index 00000000..4417f7eb --- /dev/null +++ b/examples/archetypes/referenced-resource-ownership/expected.yaml @@ -0,0 +1,4 @@ +# acl.resource ADD names platform.connector-pool; the resolved fact +# (platform-team) IS in facts.author.groups.value. Satisfied prove -> no finding. +decision: APPROVE +findings: [] diff --git a/examples/archetypes/referenced-resource-ownership/facts.yaml b/examples/archetypes/referenced-resource-ownership/facts.yaml new file mode 100644 index 00000000..3cf0c7c7 --- /dev/null +++ b/examples/archetypes/referenced-resource-ownership/facts.yaml @@ -0,0 +1,5 @@ +author: + login: alice + groups: [connectors-team, platform-team] +resource_owner: + owner: platform-team diff --git a/examples/archetypes/referenced-resource-ownership/head/topics/prod/connectors.status.v1.yaml b/examples/archetypes/referenced-resource-ownership/head/topics/prod/connectors.status.v1.yaml new file mode 100644 index 00000000..6d3a6465 --- /dev/null +++ b/examples/archetypes/referenced-resource-ownership/head/topics/prod/connectors.status.v1.yaml @@ -0,0 +1,16 @@ +name: connectors.status.v1 +owner: connectors-team +partitions: 6 +replication_factor: 3 +retention_hours: 168 +cleanup_policy: delete +schema: + format: avro + subject: connectors.status.v1-value +description: > + Connector health/status events. +acl: + grants: + connectors-team: read + resource: platform.connector-pool + owner: platform-team diff --git a/examples/archetypes/referenced-resource-ownership/negative/base/topics/prod/connectors.status.v1.yaml b/examples/archetypes/referenced-resource-ownership/negative/base/topics/prod/connectors.status.v1.yaml new file mode 100644 index 00000000..f405cef0 --- /dev/null +++ b/examples/archetypes/referenced-resource-ownership/negative/base/topics/prod/connectors.status.v1.yaml @@ -0,0 +1,14 @@ +name: connectors.status.v1 +owner: connectors-team +partitions: 6 +replication_factor: 3 +retention_hours: 168 +cleanup_policy: delete +schema: + format: avro + subject: connectors.status.v1-value +description: > + Connector health/status events. +acl: + grants: + connectors-team: read diff --git a/examples/archetypes/referenced-resource-ownership/negative/expected.yaml b/examples/archetypes/referenced-resource-ownership/negative/expected.yaml new file mode 100644 index 00000000..4d4db7f4 --- /dev/null +++ b/examples/archetypes/referenced-resource-ownership/negative/expected.yaml @@ -0,0 +1,10 @@ +# acl.resource ADD names billing.ledger-pool; the resolved fact (finance-team) +# is NOT in facts.author.groups.value -> require-review. +decision: REVIEW +findings: + - rule: referenced-resource-ownership + obligation: resource-ownership + effect: require-review + subject: topic-registry:connectors.status.v1 + points: 0 + code: acl.resource-ownership-mismatch diff --git a/examples/archetypes/referenced-resource-ownership/negative/facts.yaml b/examples/archetypes/referenced-resource-ownership/negative/facts.yaml new file mode 100644 index 00000000..a2fbb3c2 --- /dev/null +++ b/examples/archetypes/referenced-resource-ownership/negative/facts.yaml @@ -0,0 +1,9 @@ +author: + login: alice + groups: [connectors-team, platform-team] +# The resolved fact (finance-team) does NOT match the head's self-declared +# acl.owner (billing-team) either — the predicate never reads entry.acl.owner, +# only the resolved fact, precisely because a self-declared claim cannot be +# trusted (that is the gap C7 demonstrates). +resource_owner: + owner: finance-team diff --git a/examples/archetypes/referenced-resource-ownership/negative/head/topics/prod/connectors.status.v1.yaml b/examples/archetypes/referenced-resource-ownership/negative/head/topics/prod/connectors.status.v1.yaml new file mode 100644 index 00000000..42c455e1 --- /dev/null +++ b/examples/archetypes/referenced-resource-ownership/negative/head/topics/prod/connectors.status.v1.yaml @@ -0,0 +1,16 @@ +name: connectors.status.v1 +owner: connectors-team +partitions: 6 +replication_factor: 3 +retention_hours: 168 +cleanup_policy: delete +schema: + format: avro + subject: connectors.status.v1-value +description: > + Connector health/status events. +acl: + grants: + connectors-team: read + resource: billing.ledger-pool + owner: billing-team diff --git a/examples/packs/topic-registry/.assent/bindings.yaml b/examples/packs/topic-registry/.assent/bindings.yaml index 36bd58d3..b1d36106 100644 --- a/examples/packs/topic-registry/.assent/bindings.yaml +++ b/examples/packs/topic-registry/.assent/bindings.yaml @@ -5,9 +5,9 @@ bindings: environment: dev packs: [topics] risk: { threshold: 10 } - require: [ownership, bounded-change, non-destructive, schema-valid, wildcard-grant] + require: [ownership, bounded-change, non-destructive, schema-valid, wildcard-grant, resource-ownership] - class: kafka-topic environment: prod packs: [topics] risk: { threshold: 4 } - require: [ownership, bounded-change, non-destructive, schema-valid, wildcard-grant] + require: [ownership, bounded-change, non-destructive, schema-valid, wildcard-grant, resource-ownership] diff --git a/examples/packs/topic-registry/.assent/packs/topics/rules/referenced-resource-ownership.yaml b/examples/packs/topic-registry/.assent/packs/topics/rules/referenced-resource-ownership.yaml new file mode 100644 index 00000000..a93b3be7 --- /dev/null +++ b/examples/packs/topic-registry/.assent/packs/topics/rules/referenced-resource-ownership.yaml @@ -0,0 +1,46 @@ +# Referenced-resource-ownership (REF-EX C7, judgment call (b)) — an ACL entry +# that NAMES another team's resource (acl.resource) is a cross-team reference the +# changed file alone cannot authorize: the diff only shows what THIS topic +# claims, not who actually owns the resource being referenced. facts.resource_owner.owner +# stubs the shipped builtin/resource-owner shape (REF-GAP-1, OutputOwner="owner") +# as the ground truth; the author must belong to that owning group, mirroring +# ownership.yaml's own-entry check but for a REFERENCED resource rather than the +# entry itself. Gap DEMO, not a new builtin: no cross-file resolution happens — +# facts.resource_owner.owner.value is a fixture literal here, never resolved from +# the referenced resource's own file. `acl.owner` is carried alongside +# `acl.resource` as the entry's own self-declared claim (documentation only, +# not read by this predicate — the predicate trusts the resolved fact, not the +# unverified self-declaration, which is the whole point of the gap). +# +# Pointer-scoped to /*/acl/resource so this rule is vacuous (no match at all) on +# every topic that never declares acl.resource. This is a STRONGER vacuity guard +# than the spec's own summary ("vacuous when !has(entry.acl)"): S06's +# wildcard-grant fixtures already populate `entry.acl` (via `acl.grants`) without +# ever setting `acl.resource`, so a bare `!has(entry.acl)` guard would NOT be +# vacuous for those goldens — a CEL predicate would then try +# `facts.resource_owner.owner.value` on a facts.yaml that never declares +# `resource_owner`, which errors and would flip those goldens' decision. Pointer +# scoping avoids the whole class of has()-vs-acl-shape traps by construction: the +# rule structurally cannot fire unless a change touches acl.resource specifically. +apiVersion: assent.dev/v1alpha1 +kind: MergePolicy +metadata: + name: topics-referenced-resource-ownership +spec: + entries: + kafka-topic: + mode: map + root: "" + rules: + - name: referenced-resource-ownership + phase: enforce + match: + valueChanges: + pointers: ["/*/acl/resource"] + kinds: [add, modify] + prove: + obligation: resource-ownership + when: 'facts.resource_owner.owner.value in facts.author.groups.value' + onFailure: + effect: require-review + code: acl.resource-ownership-mismatch diff --git a/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/base/topics/prod/connectors.status.v1.yaml b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/base/topics/prod/connectors.status.v1.yaml new file mode 100644 index 00000000..5f2d3a24 --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/base/topics/prod/connectors.status.v1.yaml @@ -0,0 +1,23 @@ +connectors.status.v1: + owner: connectors-team + partitions: 6 + replication_factor: 3 + retention_hours: 168 + cleanup_policy: delete + schema: + format: avro + subject: connectors.status.v1-value + compatibility: BACKWARD + references: + connectors.common: 1 + compaction: + strategy: delete + description: > + Connector health/status events. REF-EX C7 (referenced-resource-ownership) + demonstrator: acl already carries S06's grants map; base has no acl.resource + yet (acl.resource/acl.owner are ADDED as leaf keys under the SAME existing + acl mapping, not as a whole new subtree — a one-sided ADD of a mapping would + go opaque, E1-S05). + acl: + grants: + connectors-team: read diff --git a/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/expect.yaml b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/expect.yaml new file mode 100644 index 00000000..e289c537 --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/expect.yaml @@ -0,0 +1,5 @@ +# acl.resource ADD names platform.connector-pool; facts.resource_owner.owner +# (platform-team) IS in facts.author.groups.value -> referenced-resource-ownership +# proves silently -> APPROVE. +decision: APPROVE +findings: [] diff --git a/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/facts.yaml b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/facts.yaml new file mode 100644 index 00000000..3f08033e --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/facts.yaml @@ -0,0 +1,9 @@ +author: + login: alice + groups: [connectors-team, platform-team] +quota: + max_partitions: 24 +schema: + valid: true +resource_owner: + owner: platform-team diff --git a/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/head/topics/prod/connectors.status.v1.yaml b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/head/topics/prod/connectors.status.v1.yaml new file mode 100644 index 00000000..38013ce7 --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/head/topics/prod/connectors.status.v1.yaml @@ -0,0 +1,25 @@ +connectors.status.v1: + owner: connectors-team + partitions: 6 + replication_factor: 3 + retention_hours: 168 + cleanup_policy: delete + schema: + format: avro + subject: connectors.status.v1-value + compatibility: BACKWARD + references: + connectors.common: 1 + compaction: + strategy: delete + description: > + Connector health/status events. REF-EX C7 (referenced-resource-ownership) + demonstrator: acl already carries S06's grants map; base has no acl.resource + yet (acl.resource/acl.owner are ADDED as leaf keys under the SAME existing + acl mapping, not as a whole new subtree — a one-sided ADD of a mapping would + go opaque, E1-S05). + acl: + grants: + connectors-team: read + resource: platform.connector-pool + owner: platform-team diff --git a/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/base/topics/prod/connectors.status.v1.yaml b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/base/topics/prod/connectors.status.v1.yaml new file mode 100644 index 00000000..5f2d3a24 --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/base/topics/prod/connectors.status.v1.yaml @@ -0,0 +1,23 @@ +connectors.status.v1: + owner: connectors-team + partitions: 6 + replication_factor: 3 + retention_hours: 168 + cleanup_policy: delete + schema: + format: avro + subject: connectors.status.v1-value + compatibility: BACKWARD + references: + connectors.common: 1 + compaction: + strategy: delete + description: > + Connector health/status events. REF-EX C7 (referenced-resource-ownership) + demonstrator: acl already carries S06's grants map; base has no acl.resource + yet (acl.resource/acl.owner are ADDED as leaf keys under the SAME existing + acl mapping, not as a whole new subtree — a one-sided ADD of a mapping would + go opaque, E1-S05). + acl: + grants: + connectors-team: read diff --git a/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/expect.yaml b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/expect.yaml new file mode 100644 index 00000000..f531c1ee --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/expect.yaml @@ -0,0 +1,8 @@ +# acl.resource ADD names billing.ledger-pool; facts.resource_owner.owner +# (finance-team) is NOT in facts.author.groups.value -> referenced-resource-ownership +# fails -> require-review -> REVIEW. +decision: REVIEW +findings: + - rule: referenced-resource-ownership + obligation: resource-ownership + effect: require-review diff --git a/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/facts.yaml b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/facts.yaml new file mode 100644 index 00000000..3c19c6da --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/facts.yaml @@ -0,0 +1,13 @@ +author: + login: alice + groups: [connectors-team, platform-team] +quota: + max_partitions: 24 +schema: + valid: true +# The resolved fact (finance-team) does NOT match the head's self-declared +# acl.owner (billing-team) either -- the predicate never reads entry.acl.owner, +# only the resolved fact, precisely because a self-declared claim cannot be +# trusted (that is the gap C7 demonstrates). +resource_owner: + owner: finance-team diff --git a/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/head/topics/prod/connectors.status.v1.yaml b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/head/topics/prod/connectors.status.v1.yaml new file mode 100644 index 00000000..294c0895 --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/resource-ownership/negative/head/topics/prod/connectors.status.v1.yaml @@ -0,0 +1,25 @@ +connectors.status.v1: + owner: connectors-team + partitions: 6 + replication_factor: 3 + retention_hours: 168 + cleanup_policy: delete + schema: + format: avro + subject: connectors.status.v1-value + compatibility: BACKWARD + references: + connectors.common: 1 + compaction: + strategy: delete + description: > + Connector health/status events. REF-EX C7 (referenced-resource-ownership) + demonstrator: acl already carries S06's grants map; base has no acl.resource + yet (acl.resource/acl.owner are ADDED as leaf keys under the SAME existing + acl mapping, not as a whole new subtree — a one-sided ADD of a mapping would + go opaque, E1-S05). + acl: + grants: + connectors-team: read + resource: billing.ledger-pool + owner: billing-team From 7ef6c8bdc6eff5de7facf7f09f9db060506b43be Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 12:55:42 +0200 Subject: [PATCH 04/11] :white_check_mark: test(examples): pin REF-EX C8 companion-file delete as REVIEW (known limitation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit envs/prod/NOTES.md is outside infra-vars' class match (*.tfvars/*.tf); its deletion in head is caught only by the class-agnostic D-063/D-064 unmatched whole-file-DELETE fail-safe escalation, never a class-specific rule. Ran against the real engine before pinning: decision is a MEASURED REVIEW with the synthetic aggregate.unmatchedDelete finding, no obligation attached. Documented as a known limitation (not a feature) in the pack's config.yaml and examples/README.md — v1 does not correlate "delete A and append B" across files (REF-GAP-3, out of v1 engine scope). No second class added. --- examples/README.md | 9 +++++ examples/packs/infra-vars/.assent/config.yaml | 11 ++++++ .../infra-vars/.assent/tests/vars/cases.yaml | 34 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 examples/packs/infra-vars/.assent/tests/vars/cases.yaml diff --git a/examples/README.md b/examples/README.md index de4f32fa..ce16ddcf 100644 --- a/examples/README.md +++ b/examples/README.md @@ -29,6 +29,15 @@ to the HCL parser, so a `.tf` file's content, blocks or bare literals alike, is opaque and falls back to REVIEW, never a partial parse (ADR-0003) — see the `infra-vars` pack's `tf-opaque` case. Only `.tfvars` gets structured diffing today. +**Known limitation (REF-EX C8, REF-GAP-3):** deleting a file the pack's class does not +match (e.g. a companion `envs/prod/NOTES.md` alongside `infra-vars`' `*.tfvars`) is caught +only by the class-agnostic unmatched-whole-file-DELETE fail-safe (D-063/D-064, +`internal/core/aggregate/coverage.go`), which raises the decision to REVIEW with no +obligation attached — see the `infra-vars` pack's `vars/companion-delete` case (a +**measured**, not wished, decision). This is not cross-file correlation: v1 does not know +"delete A and append B" are related, and would REVIEW an unrelated companion delete the +same way. Out of v1 engine scope. + The authored surfaces here are the **frozen** `assent.dev/v1alpha1` schemas under `schemas/`, not drafts; the compatibility promises attached to them are in [`API_STABILITY.md`](../API_STABILITY.md). diff --git a/examples/packs/infra-vars/.assent/config.yaml b/examples/packs/infra-vars/.assent/config.yaml index c215f406..08afc91f 100644 --- a/examples/packs/infra-vars/.assent/config.yaml +++ b/examples/packs/infra-vars/.assent/config.yaml @@ -26,6 +26,17 @@ classes: # the HCL producer (so a literal-only Terraform config file could structurally # diff, matching tfvars) is future engine work, not yet built. match: { paths: ["envs/**/*.tfvars", "envs/**/*.tf"] } + # KNOWN LIMITATION (REF-EX C8, REF-GAP-3, out of v1 engine scope): this class + # matches only *.tfvars and *.tf. A COMPANION file outside that match — e.g. + # envs/prod/NOTES.md — is unclassified (ADR-0008 §1), and its deletion is + # caught only by the class-agnostic D-063/D-064 unmatched-whole-file-DELETE + # fail-safe (internal/core/aggregate/coverage.go), which raises the decision + # to REVIEW with no obligation/rule attached (see vars/companion-delete in + # .assent/tests/vars/cases.yaml — a MEASURED, not wished, REVIEW). This is + # NOT cross-file correlation: the engine has no notion that NOTES.md is + # "related" to compute.tfvars, and would REVIEW an unrelated companion + # delete identically. v1 does not correlate "delete A and append B" across + # files — that remains REF-GAP-3, likely out of v1 scope. providers: author: type: builtin/gitlab-groups diff --git a/examples/packs/infra-vars/.assent/tests/vars/cases.yaml b/examples/packs/infra-vars/.assent/tests/vars/cases.yaml new file mode 100644 index 00000000..04239b41 --- /dev/null +++ b/examples/packs/infra-vars/.assent/tests/vars/cases.yaml @@ -0,0 +1,34 @@ +# REF-EX C8 (companion-file delete, known limitation) — a file OUTSIDE the +# infra-vars class match (`envs/**/*.tfvars`, `envs/**/*.tf`) is deleted in head. +# No class governs envs/prod/NOTES.md at all, so no fileEvents rule can ever +# select it (this pack declares none anyway); the D-063/D-064 unmatched +# whole-file-DELETE escalation (internal/core/aggregate/coverage.go) is the ONLY +# thing standing between this delete and a silent APPROVE, and it raises the +# decision to REVIEW (never higher — no obligation/rule fires, so `findings` +# carries only the synthetic aggregate.unmatchedDelete escalation, `code: +# fileEvent.unmatchedDelete`, no `obligation`). +# +# This is a MEASURED decision, not a wished one (REQ-EX-S07-04): the case was run +# against the real engine before this comment was written. It is also a KNOWN +# LIMITATION, not a feature — v1 does NOT correlate "delete A and append B" across +# files (REF-GAP-3, out of v1 engine scope); this fixture demonstrates only that +# the ungoverned companion delete fails safe to REVIEW rather than silently +# vanishing. It must NOT be read as cross-file correlation — the engine has no +# idea NOTES.md is "related" to compute.tfvars; it would REVIEW an unrelated +# companion delete identically. See examples/README.md and this pack's +# config.yaml for the same limitation documented in prose. +cases: + - name: companion-delete + file: envs/prod/NOTES.md + base: | + # Deployment notes + + Runbook links and the on-call rotation for the prod environment. This file + is NOT governed by the infra-vars class (only *.tfvars / *.tf are), so its + deletion is an ungoverned, unclassified companion change — REF-EX C8. + head: null + expect: + decision: REVIEW + findings: + - rule: aggregate.unmatchedDelete + effect: require-review From 167a3ba92160772157358274602132e75fc83e0e Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 12:55:51 +0200 Subject: [PATCH 05/11] :memo: docs(planning): mark REF-EX C1-C8 landed; archetype-goldens manifest v2 backlog.md's REF-EX row and REF-GAP-1..3 rows now point at the landed EX-S07 fixtures instead of the future-tense "not started". archetype-goldens.md bumps to manifest version 2 with the three new C5-C7 archetype rows (quota-ceiling, placement-allow-list, referenced-resource-ownership). --- docs/planning/archetype-goldens.md | 8 +++++++- openspec/specs/backlog.md | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/planning/archetype-goldens.md b/docs/planning/archetype-goldens.md index 5d2af8e7..b173d5a3 100644 --- a/docs/planning/archetype-goldens.md +++ b/docs/planning/archetype-goldens.md @@ -1,6 +1,6 @@ # Archetype golden corpus — `assent test` seed manifest -**Manifest version:** `1` +**Manifest version:** `2` **Owned by:** P3-E3-S04 **Corpus root:** [`examples/archetypes/`](https://github.com/PlatformRelay/assent/tree/main/examples/archetypes) **Inventory:** [`archetypes.md`](archetypes.md) @@ -32,6 +32,12 @@ quadruple (or a `negative/` / case-subdir variant of that shape). `decision` is | `examples/archetypes/no-destruction/delete/` | `BLOCK` | File delete; never author-thread authorization. | | `examples/archetypes/no-destruction/rename/` | `REVIEW` | Rename ≥ delete strictness; `require-review`. | | `examples/archetypes/no-destruction/near-similarity/` | `BLOCK` | Near-similarity must not downgrade below delete. | +| `examples/archetypes/quota-ceiling/` | `APPROVE` | REF-EX C5 — in-band partition bump against a stubbed quota fact; empty findings. | +| `examples/archetypes/quota-ceiling/negative/` | `REVIEW` | Over-quota → `challenge` / `bounded-change.out-of-band` (same mechanism as `bounded-change/negative`, named for C5 discoverability). | +| `examples/archetypes/placement-allow-list/` | `APPROVE` | REF-EX C6 — `instance_set` change onto an allow-listed value (stubbed `facts.placement.allowed.value`); empty findings. | +| `examples/archetypes/placement-allow-list/negative/` | `REVIEW` | `instance_set` off the allow-list → `require-review` / `placement.disallowed-instance-set`. | +| `examples/archetypes/referenced-resource-ownership/` | `APPROVE` | REF-EX C7 (gap demo, judgment call (b)) — `acl.resource` names a resource the stubbed `facts.resource_owner.owner.value` confirms the author's group owns; empty findings. | +| `examples/archetypes/referenced-resource-ownership/negative/` | `REVIEW` | Resolved owner does not match the author's groups → `require-review` / `acl.resource-ownership-mismatch`. | **Index (not a golden quadruple):** `examples/archetypes/no-destruction/expected.yaml` lists the three case directories above and reaffirms `onFailure.effect: require-review` diff --git a/openspec/specs/backlog.md b/openspec/specs/backlog.md index cee22355..2bd10182 100644 --- a/openspec/specs/backlog.md +++ b/openspec/specs/backlog.md @@ -126,16 +126,16 @@ providers, not core-model redesign**. All rows below are generalized (invented n | ID | Gap | Status | Notes | | --- | --- | --- | --- | -| **REF-GAP-1** | Referenced-resource authorization fact source (a list value / ACL names *another* team's resource → who owns it?) | **CLOSED (E5-S08)** | `builtin/resource-owner` shipped; hermetic L0 + run-path wiring in E5-S10. Demonstrator fixture = C7 (**P5-EX EX-S07**, D-143 — was deferred D-071) | -| **REF-GAP-2** | In-repo-state-as-a-fact (quota/placement/limits registries + in-repo reviewers files that today no provider reads) | **CLOSED (E5-S07)** | `builtin/repo-file` most-specific-first shipped; hermetic run path in E5-S10 (`TestE5ExitGateResolvedFacts`). C5/C6 fixtures = **P5-EX EX-S07** (D-143; was deferred D-071) | -| **REF-GAP-3** | Cross-class / companion-file correlation ("two-step delete": remove from file A *and* append to manifest B) | **OPEN — likely out of v1** | `changes` is class-slice-scoped by contract (ADR-0017 §5); ship C8 as a known-limitation fixture (expected REVIEW), decide scope via OQ | +| **REF-GAP-1** | Referenced-resource authorization fact source (a list value / ACL names *another* team's resource → who owns it?) | **CLOSED (E5-S08)** | `builtin/resource-owner` shipped; hermetic L0 + run-path wiring in E5-S10. Demonstrator fixture = C7, landed **P5-EX EX-S07** (`.assent/tests/topics/resource-ownership/`, D-143 — was deferred D-071) | +| **REF-GAP-2** | In-repo-state-as-a-fact (quota/placement/limits registries + in-repo reviewers files that today no provider reads) | **CLOSED (E5-S07)** | `builtin/repo-file` most-specific-first shipped; hermetic run path in E5-S10 (`TestE5ExitGateResolvedFacts`). C5/C6 fixtures landed **P5-EX EX-S07** (`.assent/tests/topics/quota-ceiling/`, `.assent/tests/vars/placement/`, D-143; was deferred D-071) | +| **REF-GAP-3** | Cross-class / companion-file correlation ("two-step delete": remove from file A *and* append to manifest B) | **OPEN — likely out of v1** | `changes` is class-slice-scoped by contract (ADR-0017 §5); C8 known-limitation fixture landed **P5-EX EX-S07** (`infra-vars` `vars/companion-delete`, measured REVIEW); the underlying correlation engine feature itself remains open, scope decided via OQ | | **REF-GAP-4** | Plan-level blast radius (weighting the expanded IaC plan, not the request diff) | **OUT of model** | assent gates the request diff; `points`/`threshold` bulk-guard on the diff is the in-scope approximation | **Generalized example/test candidates (a later sanitized authoring lane — passes `check-sanitization.sh`):** | ID | Item | Status | Closest existing archetype | | --- | --- | --- | --- | -| **REF-EX** | Author 8 domain-neutral archetype fixtures C1–C8 (list-no-shrink, privilege-tier allow-list, wildcard-grant block, soft-delete-as-field-add, quota-ceiling-from-fact, placement allow-list, referenced-resource-ownership [gap demo], companion-file delete [known-limitation]) | **SPECIFIED (P5-EX)** — C1–C4 = EX-S06; C5–C8 = EX-S07; not started | [p5-ex-complex-examples](p5-ex-complex-examples/spec.md). Extends no-destruction (C1/C4/C8), allowed-fields+ownership (C2/C3/C6/C7), bounded-change (C5). Engine+E5 facts already shipped; D-071 deferral is this epic. **Not** P5-DEM (D-143) | +| **REF-EX** | Author 8 domain-neutral archetype fixtures C1–C8 (list-no-shrink, privilege-tier allow-list, wildcard-grant block, soft-delete-as-field-add, quota-ceiling-from-fact, placement allow-list, referenced-resource-ownership [gap demo], companion-file delete [known-limitation]) | **C1–C8 landed** (C1–C4 EX-S06; C5–C8 EX-S07) | [p5-ex-complex-examples](p5-ex-complex-examples/spec.md). Extends no-destruction (C1/C4/C8), allowed-fields+ownership (C2/C3/C6/C7), bounded-change (C5, reused). Engine+E5 facts already shipped; D-071 deferral closed by this epic. **Not** P5-DEM (D-143) | ## Phase 5 — E1 canonical change model stories From 71864350c9d9865c9325197e438dcec65051d810 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 12:56:03 +0200 Subject: [PATCH 06/11] :memo: docs(changelog): regenerate CHANGELOG.md for the EX-S07 lane --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f96ebd72..06a9f2e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,7 @@ repository still gets a decision, never by following the link; no release carrie - :memo: docs(changelog): regenerate CHANGELOG.md for the EX-S05 lane - :memo: docs(openspec): correct test attribution in the REQ-EX-S05-05 amendment - :memo: docs(changelog): regenerate CHANGELOG.md after rebase onto origin/main +- :memo: docs(planning): mark REF-EX C1-C8 landed; archetype-goldens manifest v2 ### Features - :sparkles: feat(docs): gate example pack and format claims against dogfood @@ -125,6 +126,8 @@ repository still gets a decision, never by following the link; no release carrie - :sparkles: feat(examples): thicken infra-vars nested tfvars maps (EX-S04) - :sparkles: feat(examples): close REF-EX C1-C4 in topic-registry/service-catalog - :sparkles: feat(examples): EX-S05 HCL honesty — govern .tf, pin the measured opaque decision +- :sparkles: feat(examples): close REF-EX C6 placement allow-list (infra-vars) +- :sparkles: feat(examples): close REF-EX C7 referenced-resource-ownership (gap demo) ### Fixes - :bug: fix(ci): pin ci-audit-test in the AUD-S18 check-stage list @@ -140,6 +143,8 @@ repository still gets a decision, never by following the link; no release carrie - :white_check_mark: test(release): key the merge-skip proof on commit shape, not subject prefix - :white_check_mark: test(examples): close REQ-EX-S05-05 non-vacuity gap for infra-vars .tf governance - :white_check_mark: test(examples): close REQ-EX-S05-05's other disjunct (fixture deletion) +- :white_check_mark: test(examples): close REF-EX C5 quota-ceiling-from-fact explicitly +- :white_check_mark: test(examples): pin REF-EX C8 companion-file delete as REVIEW (known limitation) ## [0.2.0] - 2026-08-09 ### Chores From 767595213c06555b0152f9cd800e2edc20918ada Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 13:04:37 +0200 Subject: [PATCH 07/11] :memo: docs(examples): state C5's measured zero incremental --coverage credit Verified by temporarily moving quota-ceiling/ out of the tree and re-running --coverage: deleting it does not redden the gate, because bounded-change/ and bounded-change/negative/ already supply partition-change-bounds' both-polarity credit. Documented so a reader does not assume the directory is load-bearing for --coverage; facts-omitted/ remains load-bearing for the engine's fail-safe behaviour (dogfood-examples would catch a regression there). --- .../.assent/tests/topics/quota-ceiling/expect.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/expect.yaml b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/expect.yaml index 01e91566..bd86f0ca 100644 --- a/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/expect.yaml +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/expect.yaml @@ -4,5 +4,18 @@ # facts.quota.max_partitions.value (D-071 deferral note) — this case just gives # the pattern its own discoverable name so C5 is not only implicit inside # bounded-change/. Satisfied prove -> no finding. +# +# MEASURED, not assumed: deleting this whole directory does NOT redden +# `assent test --coverage examples/packs/topic-registry` — bounded-change/ and +# bounded-change/negative/ already supply partition-change-bounds' both-polarity +# --coverage credit, so this directory earns ZERO incremental --coverage +# credit (verified by temporarily moving it out and re-running --coverage). +# Its value is (a) discoverability — a reader grepping for "quota-ceiling" finds +# a case, not just a comment inside bounded-change.yaml — and (b) facts-omitted/, +# which DOES pin real, load-bearing engine behaviour (an unresolved quota fact +# must fail safe to REVIEW): if that fail-safe ever regressed to APPROVE, +# facts-omitted/ would FAIL and redden dogfood-examples, even though --coverage +# alone would stay blind to it (REQ-EX-S05-05's amendment already established +# --coverage is architecturally blind to fixture deletion/regression this way). decision: APPROVE findings: [] From 23116c67cd99bf7a1261b31d81658be7963bd0f9 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 13:04:43 +0200 Subject: [PATCH 08/11] :memo: docs(examples): note infra-vars has no fileEvents rule at all (C8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A governed *.tfvars delete hits the SAME class-agnostic D-063/D-064 unmatched-whole-file-DELETE escalation as the ungoverned companion, not a distinct file-lifecycle rule — this pack (unlike topic-registry/ service-catalog) declares no fileEvents rule in any of its rule files. Stated as a direct reading of fileDeleteGoverned rather than a measured case: assent test cannot express a whole-file delete of a .tfvars fixture either way (inline cases.yaml refuses a non-lossless .tfvars marshal; the directory form requires the file on both sides). --- examples/packs/infra-vars/.assent/config.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/packs/infra-vars/.assent/config.yaml b/examples/packs/infra-vars/.assent/config.yaml index 08afc91f..08a44d35 100644 --- a/examples/packs/infra-vars/.assent/config.yaml +++ b/examples/packs/infra-vars/.assent/config.yaml @@ -37,6 +37,16 @@ classes: # "related" to compute.tfvars, and would REVIEW an unrelated companion # delete identically. v1 does not correlate "delete A and append B" across # files — that remains REF-GAP-3, likely out of v1 scope. + # + # A GOVERNED *.tfvars delete is REVIEW too, by the SAME class-agnostic + # escalation, not a distinct file-lifecycle rule: this pack (unlike + # topic-registry/service-catalog) declares no fileEvents rule at all + # (`.assent/packs/vars/rules/*.yaml` has none), so `fileDeleteGoverned` + # (internal/core/aggregate/coverage.go) finds nothing governing ANY delete, + # governed path or not — read directly from that function rather than run, + # since `assent test` cannot express a whole-file delete of a `.tfvars` + # fixture either way (inline cases.yaml refuses non-lossless `.tfvars` + # marshal; the directory form requires the file present on both sides). providers: author: type: builtin/gitlab-groups From a94449f37a07aed3b5d14b9d93934bc325e64985 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 13:04:51 +0200 Subject: [PATCH 09/11] :memo: docs(openspec): amend REQ-EX-S07-03/04 to match the measured implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit REQ-EX-S07-03's Test: path said referenced-ownership/; the directory is resource-ownership/ because assent lint's tests-per-rule hard error requires the case name to be one of {rule.Name, obligation}. REQ-EX-S07-04's Test: path implied a directory-form companion-delete/ case; it is an inline cases.yaml case because the directory form cannot express a whole-file delete (readSingleFilePair errors when head/ is absent). Also corrects the REQ-EX-S07-04 Edge clause: infra-vars has no fileEvents rule at all, so a governed *.tfvars delete hits the same class-agnostic escalation as the ungoverned companion, not a distinct rule. Mirrors the REQ-EX-S05-05 amendment precedent (b08be34) — spec-first still means the spec must track what was actually measured, not what was assumed before implementation. --- openspec/specs/p5-ex-complex-examples/spec.md | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/openspec/specs/p5-ex-complex-examples/spec.md b/openspec/specs/p5-ex-complex-examples/spec.md index 53d9dc62..5d67254c 100644 --- a/openspec/specs/p5-ex-complex-examples/spec.md +++ b/openspec/specs/p5-ex-complex-examples/spec.md @@ -587,7 +587,17 @@ Requirements: - **REQ-EX-S07-03 (C7)** — Given `entry.acl` names another team's resource and `facts.resource_owner.owner.value` does not match the author groups, when the case runs, then `require-review`. Edge: topics **without** `acl` still APPROVE the vacuous branch. - - Test: `examples/packs/topic-registry/.assent/tests/topics/referenced-ownership/` + + Amendment (implementation): the case directory is named `resource-ownership/`, not + `referenced-ownership/` — `assent lint`'s tests-per-rule hard error requires the on-disk + case name to be one of `{rule.Name, obligation}` (`internal/lint/tests_per_rule.go`), and + the rule's obligation is `resource-ownership`. The vacuity guard is a pointer-scoped + `valueChanges` match on `/*/acl/resource` (add/modify), not a bare `!has(entry.acl)` CEL + guard — S06's `wildcard-grant` goldens already populate `entry.acl` (via `acl.grants`) + without `acl.resource`, so `!has(entry.acl)` alone would NOT be vacuous for those goldens + (a CEL read of `facts.resource_owner.owner.value` on a `facts.yaml` that never declares + `resource_owner` errors and would flip their decision). Pointer-scoping achieves the same + outcome — topics without `acl.resource` stay APPROVE — without a has()-vs-acl-shape trap. + - Test: `examples/packs/topic-registry/.assent/tests/topics/resource-ownership/` + `examples/archetypes/referenced-resource-ownership/` - Verify: `./bin/assent test examples/packs/topic-registry` - Level: L1 @@ -596,8 +606,20 @@ Requirements: wished). Docs (`examples/README.md` or pack config comment + walkthrough S09) call this a **known limitation**, not a feature. Edge: deleting a *governed* `.tfvars` file still hits existing non-destructive / unmatched-delete rules — C8 is the ungoverned companion only. - - Test: `examples/packs/infra-vars/.assent/tests/vars/companion-delete/` + - optional `examples/archetypes/` known-limitation seed + Amendment (implementation): the fixture is an **inline** `.assent/tests/vars/cases.yaml` + case (`head: null`), not a directory-form `companion-delete/` — the directory form's + `readSingleFilePair` errors when `head/` is absent ("new/deleted-file cases are + E6-S06"), so a whole-file delete cannot be expressed as a directory case at all. Second + correction to the Edge clause itself: infra-vars declares **no** `fileEvents` rule in any + of its rule files, so a *governed* `.tfvars` delete hits the SAME class-agnostic + D-063/D-064 escalation as the ungoverned companion, not a distinct "non-destructive / + unmatched-delete rule" — there is no file-lifecycle rule in this pack to hit. This could + not be run as a case either (inline `cases.yaml` refuses a non-lossless `.tfvars` marshal; + the directory form again can't express a delete), so the config.yaml comment states it as + a direct reading of `fileDeleteGoverned` (`internal/core/aggregate/coverage.go`) rather + than a measured case. + - Test: `examples/packs/infra-vars/.assent/tests/vars/cases.yaml` (`companion-delete`) + + pack config.yaml comment - Verify: `./bin/assent test examples/packs/infra-vars` - Level: L1 - **REQ-EX-S07-05** — Given `cmd/assent/test_provider_fence_test.go`, when C5–C7 fixtures are From b6e98dca55fd534dd074bc0b918ab681d696c584 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 13:05:03 +0200 Subject: [PATCH 10/11] :memo: docs(changelog): regenerate CHANGELOG.md for the EX-S07 lane follow-ups --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06a9f2e2..982535da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -118,6 +118,10 @@ repository still gets a decision, never by following the link; no release carrie - :memo: docs(openspec): correct test attribution in the REQ-EX-S05-05 amendment - :memo: docs(changelog): regenerate CHANGELOG.md after rebase onto origin/main - :memo: docs(planning): mark REF-EX C1-C8 landed; archetype-goldens manifest v2 +- :memo: docs(changelog): regenerate CHANGELOG.md for the EX-S07 lane +- :memo: docs(examples): state C5's measured zero incremental --coverage credit +- :memo: docs(examples): note infra-vars has no fileEvents rule at all (C8) +- :memo: docs(openspec): amend REQ-EX-S07-03/04 to match the measured implementation ### Features - :sparkles: feat(docs): gate example pack and format claims against dogfood From a13b6aabbe895cc99d450f683d9547ca3ab659f1 Mon Sep 17 00:00:00 2001 From: Konrad Heimel Date: Sun, 16 Aug 2026 13:17:52 +0200 Subject: [PATCH 11/11] :memo: docs(changelog): regenerate CHANGELOG.md for the EX-S07 lane --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 982535da..ca64324e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -122,6 +122,8 @@ repository still gets a decision, never by following the link; no release carrie - :memo: docs(examples): state C5's measured zero incremental --coverage credit - :memo: docs(examples): note infra-vars has no fileEvents rule at all (C8) - :memo: docs(openspec): amend REQ-EX-S07-03/04 to match the measured implementation +- :memo: docs(changelog): regenerate CHANGELOG.md for the EX-S07 lane follow-ups +- :memo: docs(changelog): regenerate CHANGELOG.md for the EX-S07 lane ### Features - :sparkles: feat(docs): gate example pack and format claims against dogfood