diff --git a/CHANGELOG.md b/CHANGELOG.md index f96ebd72..ca64324e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -117,6 +117,13 @@ 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 +- :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 +- :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 @@ -125,6 +132,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 +149,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 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/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/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/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/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/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/config.yaml b/examples/packs/infra-vars/.assent/config.yaml index c215f406..08a44d35 100644 --- a/examples/packs/infra-vars/.assent/config.yaml +++ b/examples/packs/infra-vars/.assent/config.yaml @@ -26,6 +26,27 @@ 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. + # + # 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 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/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 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" + } + } +} 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/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..bd86f0ca --- /dev/null +++ b/examples/packs/topic-registry/.assent/tests/topics/quota-ceiling/expect.yaml @@ -0,0 +1,21 @@ +# 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. +# +# 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: [] 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. 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 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 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