Skip to content

docs(cmf): settle the extensions bag contract - #59

Open
mkoushni wants to merge 8 commits into
praxis-proxy:mainfrom
mkoushni:fix/cmf-extensions-model
Open

docs(cmf): settle the extensions bag contract#59
mkoushni wants to merge 8 commits into
praxis-proxy:mainfrom
mkoushni:fix/cmf-extensions-model

Conversation

@mkoushni

@mkoushni mkoushni commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Document the CMF bag contract: per-type absent values, original vs flattened keys, and why subject.claims is not a bag key (docs/content/cmf-extensions.md).
  • Align capability_namespaces with the keys the extractors actually write; drop the unused public BAG_WORKLOAD_PREFIX.
  • ppe-pdp-diff moves present-empty sets and omitted claim scalars (== / order / membership) into the agreement subset. APL != / not in on a missing key are allowlisted splits. A flattened bool with no namespace, and a missing subject.id, stay on the allowlist.

Refs #18. Present-empty sets and omitted claim scalars agree on the verdict for presence, equality, membership, and order. Genuinely absent flattened-bool namespaces and a missing principal still diverge (CEL eval error / Cedar dispatch). Making those agree needs CEL root seeding, load-time validation via Program::references(), and a generated Cedar schema — follow-up to file.

Changes

Added

  • docs/content/cmf-extensions.md — normative bag contract:
    • per-type absent-value table (StringSet present-empty; optional String/Int/Float omitted; flattened member bools presence-only; non-option scalars always written)
    • original collections vs flattened role.* / perm.* / team.* / client.role.* / client.perm.* (the set is primary; the five flattened collections exist because that idiom predates the sets; nine other collections are set-only)
    • why there is no subject.claims map in the bag (would need a sixth AttributeValue variant and would duplicate flattened claim.*)
    • what APL, CEL, cedar-direct, and OPA do with a missing key, including APL != / not in and cedar-direct's User default vs lowercase bridged subject.type
    • catalog of all twelve extract_extensions slots, keys, and types
    • payloads that are not slots (args / args.<dotted>, result / result.<dotted>, data / data.<dotted>; raw_credentials and candidate_constraint stay out of the bag)
  • Linked from docs/content/extensions.md, docs/content/cmf.md, and docs/content/index.md.

ppe-apl-cmf

  • src/lib.rs — rustdoc points at docs/content/cmf-extensions.md.
  • src/extensions_bridge.rs — rustdoc on extract_extensions restates the per-type contract.
    • present_slots_follow_the_absent_value_contract — present empty slots emit empty sets, omit optionals, skip flattened bools, write non-option zeros/false, and never emit a subject.claims parent key.
    • original_set_and_flattened_bools_stay_paired — a member in the set also appears as the flattened bool, and both stay in lockstep.
  • src/security.rs — extractors write caller_workload.* / this_workload.* and security.labels; no workload.*.
  • src/payload.rs — top-level scalar and scalar-array payloads land on the bare keys args / result.
  • src/capability_namespaces.rsread_labels unlocks security.labels; read_workload unlocks caller_workload.* / this_workload.* (was empty / workload.* at 0.2.0).
  • src/constants.rs — removed public BAG_WORKLOAD_PREFIX.

ppe-apl-core

  • src/evaluator.rsmissing_key_matches_cmf_extensions_table pins APL missing-key semantics, including !=, !key, !(...), and not in.

ppe-pdp-diff

  • Cargo.toml / Cargo.lock — dev-deps on praxis-policy-apl-cmf and praxis-policy-core so cases can build bags through extract_extensions.
  • README.md — present-empty StringSet is in the semantic subset; omitted claim == / order is AgreeDeny; != / not in are allowlisted; pointer to docs/content/cmf-extensions.md.
  • src/cases.rs
    • Case.apl_rule + with_apl so agreement cases also run the native APL evaluator.
    • alice_via_bridge() — bag from extract_extensions, not a hand-built set.
    • empty-set / bridge-empty-teams / bridge-empty-rolesAgreeDeny.
    • missing-claim-string / missing-claim-int — omitted scalars, AgreeDeny.
    • missing-claim-not-eq / missing-not-in — APL Allows on a missing key; other engines as allowlisted.
  • src/allowlist.rsmissing-collection / missing-subject-id remain; missing-claim-not-eq / missing-not-in added; empty-set removed (now subset).
  • src/lib.rsassert_apl on AgreeAllow / AgreeDeny and on allowlist rows that name an APL verdict.

cedar-direct

  • Documents that a missing subject.type defaults to PascalCase User while the bridge writes lowercase.

Changelog

Test plan

  • make lint
  • cargo test -p praxis-policy-apl-cmf
  • cargo test -p praxis-policy-pdp-diff
  • make coverage (95.74% lines, floor 95%)
  • CI on this PR

The twelve CMF slots had an implicit per-type empty/absent rule and a
claims gap that only lived next to extractors. Document the contract,
assert it in the bridge, and move present-empty sets into the PDP
agreement subset so APL, CEL, cedar-direct, and OPA deny the same way.

Signed-off-by: mkoushni <mkoushni@redhat.com>
@araujof araujof added documentation Improvements or additions to documentation area/security labels Sep 1, 2026
@araujof araujof moved this from Backlog to In progress in Praxis Policy Engine (PPE) Sep 1, 2026
@araujof araujof added this to the 0.1.2 milestone Sep 1, 2026
Keep the praxis-proxy#18 changelog bullet and take the assertions entry from praxis-proxy#56.

Signed-off-by: mkoushni <mkoushni@redhat.com>

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review

Summary: Thorough documentation and differential testing of the CMF bag contract. The normative doc, bridge tests, and pdp-diff cases are consistent and well-structured. Two convention items in the new bridge tests.

Severity Count
Critical 0
Large 0
Medium 2

bag
}

/// The per-type contract in `docs/cmf-extensions.md`: inside a present

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Project conventions prohibit doc comments on #[test] functions ("Do not add doc comments (///) or regular comments (//) on test functions"). Remove the /// block — the function name already documents its intent.


let bag = bag_of(ext);

// StringSet: present and empty.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] Lines 177, 201, 228, 234, and 242 use inline comments as section headings inside this test body. Project conventions require assertion messages or tracing calls instead of inline comments in test function bodies. Convert each section heading to a tracing::debug! call (e.g. tracing::debug!("StringSet: present and empty");) or remove them — the assertion messages on each check already explain the intent.

@terylt

terylt commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

hi @mkoushni Overall, nice work... just a few small things and I think I'll create a couple of related issues.

Blocking: the APL row of the missing-key table

This is the only place the document misdescribes the code. The rest is accurate
but incomplete, which is normal for a first reference doc. A wrong normative
statement is worse, because the point of the document is that people trust it
instead of reading evaluator.rs.

1. != is backwards. Current text:

APL | false for presence, equality, membership, and order; != is true (an
absent key is not equal to a value)

Condition::Comparison hands off to eval_comparison
(crates/ppe-apl-core/src/evaluator.rs:106-109), which returns false for a
missing key before it looks at the operator (:127-131), so CompareOp::NotEq
at :139 is never reached. On the branch, require(subject.id != "alice")
denies on an empty bag and allows once subject.id is present.

2. The row should name negation. Condition::IsFalse returns true for an
absent key (evaluator.rs:98-101, deliberate per the comment at :90-93).
!authenticated fires on an empty bag. It is the one case where APL treats
absent as a positive result, and the one authors hit most, since
deny when not authenticated is the idiom.

Suggested replacement for the row:

APL | false for presence, equality, membership, order, and !=: a comparison
on an absent key is false before the operator is considered. Negation is the
exception, !key is true.

Worth a test, since the point of the document is that it is checkable.

Asks, not blockers

3. The authenticated row should name its cost. We are not asking for a
code change here; that belongs in the follow-up issue. But the row presents
omitted-when-unset as settled, and the PR's own test asserts
get_bool("delegated") == Some(false) and !contains("authenticated") on one
fixture. Two derived booleans, opposite treatment, and the stated reason does
not separate them, since delegated has no explicit undelegated marker either.

The cost is CEL-specific. On a bridge-built bag with a security slot and no
subject, authenticated and !authenticated both give
Undeclared reference to 'authenticated', and has(authenticated) is a
compile error, because has() rejects a bare name. CelResolver routes
compile errors through compile_error_decision, which always fails closed and
does not consult on_error (builtins/pdps/cel/src/resolver.rs:343-346). So
there is no guard an author can deploy, "allow anonymous reads" is not
expressible in CEL, and the deny reaches the operator as a key error.

Please record that in the row as a known sharp edge rather than as a rule whose
rationale reads as settled.

4. security.objects and security.data are missing from the catalog.
Both are on SecurityExtension
(crates/ppe-core/src/extensions/security.rs:353-358), neither is bridged, and
neither appears in the document. Populating both and running
extract_extensions emits 19 keys, none referencing either. Every other
unbridged field got a line, so these are the only silent omissions, and
DataPolicy carries apply_labels, allowed_actions, denied_actions and
retention, which is a field built to express data policy that the policy
language cannot read. We do not know whether praxis reads them off the typed
extension; if it does they are host-only by design and the doc should say so,
if not they are a gap and the doc should say that.

5. The attested_at reason contradicts the doc's own rows. It is excluded
because "APL has no datetime type", but request.timestamp and
completion.created_at are both listed as Strings. Suggested:

attested_at is not in the bag. request.timestamp and
completion.created_at are carried as plain strings, so the bag does not
refuse timestamps; unifying the three is out of scope here.

6. capability_namespaces.rs disagrees with the new doc. Both pre-existing
and not caused by this PR, but this is the PR that adds a third hand-maintained
key list to a crate whose two existing ones already disagree:

  • CAP_READ_LABELS maps to &[] with a comment saying labels are not extracted
    into bag keys. security.labels is written, and the doc lists it as always.
  • CAP_READ_WORKLOAD maps to BAG_WORKLOAD_PREFIX ("workload."), which
    nothing writes and which has exactly one use in the tree, that table entry.
    The bridge writes caller_workload.* and this_workload.*
    (security.rs:140-145).

7. Closes #18 is a stretch. Four of five acceptance criteria are met. The
fifth, that a policy behaves the same on all four decision points when the value
is absent, is not: present-empty sets agree and are tested, genuinely absent
values diverge and went on the allowlist. That is honest, it is just not the
criterion. Making them agree needs CEL root seeding, load-time validation via
Program::references(), and a decision on a generated Cedar schema, which we
will file as a follow-up. Either trim #18 with a pointer to that, or make this
Refs #18.

Follow-ups, fine to defer

8. Nothing binds the catalog to the bridge. The new test walks default
slots, which is right for the absent-value table, but nothing walks a fully
populated
Extensions and asserts the emitted key set, so a new field can land
unbridged and unnoticed. That is the failure mode that produced #18, and item 4
is an instance of it. constants.rs flags the same risk in its header. One test
that populates every slot, collects bag.keys(), and compares against a sorted
expected set would hold it.

9. The missing-claim-* allowlist reasons misstate APL. "APL would treat
the comparison as false" means APL denies, and so do the other three. All four
agree on the verdict, only the cause differs, which is what AgreeDeny models
elsewhere. Either restate it, or set apl_rule and let the harness prove it.

10. apl_rule is opt-in where the harness is otherwise opt-out. Three named
cases are checked; the rest, including ones trivially expressible in APL,
silently have none. Requiring either an apl_rule or an explicit
no_apl("reason") would match the convention used everywhere else here.

@terylt terylt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requesting some small changes above ^

Keep the CMF extensions-bag changelog under Unreleased; take 0.2.0 wording for assertions and token cache.

Signed-off-by: mkoushni <mkoushni@redhat.com>
@mkoushni
mkoushni requested a review from a team September 6, 2026 08:56
Name negation on the APL missing-key row, record the CEL authenticated
sharp edge, and catalog unbridged security.objects / security.data.
Omitted claim scalars agree on Deny, so they leave the allowlist.

Signed-off-by: mkoushni <mkoushni@redhat.com>
@mkoushni
mkoushni requested a review from terylt September 6, 2026 09:13
@shaneutt
shaneutt self-requested a review September 9, 2026 00:33
Keep the CMF extensions-bag changelog under Unreleased alongside the
PPE documentation entry from praxis-proxy#82.

Signed-off-by: mkoushni <mkoushni@redhat.com>
@shaneutt shaneutt removed their assignment Sep 9, 2026

@araujof araujof left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestions for the absent-key contract, all verified together on 4216a6a: cargo fmt --check clean, praxis-policy-apl-core 352/352.

Two things in the APL row of the missing-key table:

  1. deny when not authenticated does not parse. not is reserved for the not in phrase (parser.rs returns NOT_IS_RESERVED in predicate position), and a when: clause hands its value straight to parse_predicate (parser.rs:1923), so when: not authenticated fails at config load. docs/content/apl/apl-grammar.md already states the rule, and line 36 of this file already uses !authenticated.

  2. The row omits two forms that are also true on an absent key: !(...) around any condition, and X not in Y (InSet resolves to false, then negates). That makes the "omitted claim scalar agrees on the verdict (all Deny)" claim false for != and not in. Measured on an empty bag:

Rule APL CEL / OPA / cedar-direct
require(claim.tenant != "acme") Allow Deny
require(subject.type not in blocked_types) Allow Deny

Denylists are the shape that hits it.

One note on the harness half: AllowlistEntry has cedar, cel and opa and no apl field, and assert_apl runs only on AgreeAllow / AgreeDeny, so this split cannot be recorded as a case today. Adding apl: Outcome would make it executable; fine to defer alongside #78 as long as the doc no longer claims agreement the harness does not check.

Comment thread docs/cmf-extensions.md Outdated

| Engine | Missing key | Empty `StringSet` |
|---|---|---|
| APL | false for presence, equality, membership, and order; `!=` is true (an absent key is not equal to a value, matching `!(x == y)`). Negation is the other exception: `!key` is true (`deny when not authenticated` is the idiom). | `contains` / `in` is false |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not authenticated does not parse, and !(...) / not in are missing from the true-on-absent list.

Suggested change
| APL | false for presence, equality, membership, and order; `!=` is true (an absent key is not equal to a value, matching `!(x == y)`). Negation is the other exception: `!key` is true (`deny when not authenticated` is the idiom). | `contains` / `in` is false |
| APL | false for presence, equality, membership, and order. Every negated form is true: `!=`, `!key`, `!(...)`, and `not in`. Negation is spelled `!`; `not` is reserved for the `not in` phrase, so the idiom is `!authenticated`. | `contains` / `in` is false |

Comment thread docs/cmf-extensions.md Outdated
| APL | false for presence, equality, membership, and order; `!=` is true (an absent key is not equal to a value, matching `!(x == y)`). Negation is the other exception: `!key` is true (`deny when not authenticated` is the idiom). | `contains` / `in` is false |
| CEL | evaluation error; default `OnError::Deny` turns it into a denial that reports a key error, not a policy false | `in` is false |
| cedar-direct | empty `roles` / `permissions` / `teams` / `claims` on the principal so those names exist; no `subject.id` is a dispatch error | `contains` is false |
| OPA | undefined; without `default allow := false` the query is a default deny | `in` is false |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds the verdict split after the table, so the fail-open direction is stated rather than implied.

Suggested change
| OPA | undefined; without `default allow := false` the query is a default deny | `in` is false |
| OPA | undefined; without `default allow := false` the query is a default deny | `in` is false |
The negated forms are the one place the four split on the **verdict**, not just
the cause. On an empty bag:
| Rule | APL | CEL / OPA / cedar-direct |
|---|---|---|
| `require(claim.tenant == "acme")` | Deny | Deny |
| `require(subject.roles contains "hr")` | Deny | Deny |
| `require(claim.tenant != "acme")` | **Allow** | Deny |
| `require(subject.type not in blocked_types)` | **Allow** | Deny |
A denylist is the shape that hits this. Guard the key:
`require(exists(claim.tenant) & claim.tenant != "acme")` denies when it is absent.

Comment thread docs/cmf-extensions.md Outdated
Comment on lines +135 to +137
bridge filled both from the same set. A policy written against an **omitted
claim scalar** agrees on the verdict (all Deny) and is an `AgreeDeny` in
`ppe-pdp-diff`; the cause still differs. A flattened bool whose namespace was

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scope the agreement claim to == / order / membership probes.

Suggested change
bridge filled both from the same set. A policy written against an **omitted
claim scalar** agrees on the verdict (all Deny) and is an `AgreeDeny` in
`ppe-pdp-diff`; the cause still differs. A flattened bool whose namespace was
bridge filled both from the same set. A policy that probes an **omitted
claim scalar** with `==`, order, or membership agrees on the verdict (all Deny)
and is an `AgreeDeny` in `ppe-pdp-diff`; the cause still differs. A `!=` or
`not in` probe does not agree; see above. A flattened bool whose namespace was

Comment thread crates/ppe-apl-core/src/evaluator.rs Outdated
fn missing_key_matches_cmf_extensions_table() {
// `docs/cmf-extensions.md` "What each decision point does with a
// missing key" — APL row. Presence, equality, membership, and
// order are false; `!=` is true; `!key` is true.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// order are false; `!=` is true; `!key` is true.
// order are false; every negated form is true.

Comment on lines +2251 to +2257
);
let rule = crate::parser::parse_rule("require(authenticated)", "test")
.expect("require(authenticated) parses");
assert!(
matches!(evaluate_rules(&[rule], &bag), Decision::Deny { .. }),
"require(authenticated) fires on an empty bag"
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pins not in and !(...), plus the rule-level Allow so the fail-open direction is asserted rather than assumed.

Suggested change
);
let rule = crate::parser::parse_rule("require(authenticated)", "test")
.expect("require(authenticated) parses");
assert!(
matches!(evaluate_rules(&[rule], &bag), Decision::Deny { .. }),
"require(authenticated) fires on an empty bag"
);
);
assert!(
eval_pred("subject.type not in blocked_types", &bag),
"`not in` on an absent set key is true"
);
assert!(
eval_pred(r#"!(subject.id == "alice")"#, &bag),
"`!(...)` on an absent key is true"
);
let rule = crate::parser::parse_rule("require(authenticated)", "test")
.expect("require(authenticated) parses");
assert!(
matches!(evaluate_rules(&[rule], &bag), Decision::Deny { .. }),
"require(authenticated) fires on an empty bag"
);
let denylist =
crate::parser::parse_rule("require(subject.type not in blocked_types)", "test")
.expect("denylist require parses");
assert_eq!(
evaluate_rules(&[denylist], &bag),
Decision::Allow,
"an unguarded denylist require allows on an absent key"
);

@araujof araujof left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Just a few minor suggestions, otherwise good to go.

  • Consider moving docs/cmf-extensions.md to docs/content/cmf-extensions.md, and add a link to it from docs/content/extensions.md.
  • docs/cmf-extensions.md omits APL cases where a missing key makes !=, not in, or some negated conditions true. Narrow the “all four deny” claim and add differential tests for != and not in.
  • deny when not authenticated does not parse. See inlined patches and comments.
  • Do not recommend has(role.hr): when no role keys exist, CEL has no role namespace and returns an error. Use the always-present subject.roles set.
  • List the exact args and result keys emitted for top-level scalar and array payloads, not only args.* and result.*.
  • Document cedar-direct’s missing subject.type behavior. It defaults to User, while bridged values are lowercase, so type-scoped policies can miss the principal.
  • The claim that CEL cannot guard missing authentication is too broad. has(subject.id) works when the subject namespace exists; only a completely absent subject leaves no guard.
  • Update the changelog for the removed public BAG_WORKLOAD_PREFIX constant and the changed capability mappings.

@terylt

terylt commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Nice work! Here is my re-review:

Fix before merge

The changelog only has an Added bullet. The second commit removes pub const BAG_WORKLOAD_PREFIX from crates/ppe-apl-cmf/src/constants.rs, a public item of a crate released at 0.2.0 on 2026-09-03, and changes what capability_namespaces returns for read_labels and read_workload. That is a Fixed and a Removed entry, not documentation. I grepped praxis upstream/main for unlocked_bag_prefixes, capability_namespaces and the constant and found no uses, so nothing known breaks, but the crate's public surface changed and the changelog is where a host looks.

The PR body has the same gap: the Changes section lists docs, extensions_bridge.rs, lib.rs and ppe-pdp-diff, and does not mention capability_namespaces.rs, constants.rs, security.rs or evaluator.rs. Worth updating so a reader sizes the review correctly.

Still open, not blocking

The original-vs-flattened section states the five pairs but not the rule. Nine collections are set-only (client.teams, client.authorized_scopes, client.authorized_audiences, caller_workload.selectors, this_workload.selectors, security.labels, agent.conversation.topics, meta.tags, llm.capabilities), so require(role.hr) works and require(tag.pii) is false forever with nothing in the document saying that is by design. One paragraph saying the set is the primary form, the five flattened collections exist because that idiom predates the sets, and no more are added.

Move the catalog under docs/content, pin APL != / not in splits in
ppe-pdp-diff, and document cedar-direct's User default versus lowercase
bridged types.

Signed-off-by: mkoushni <mkoushni@redhat.com>
@mkoushni

mkoushni commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Docs. docs/cmf-extensions.md moved to docs/content/cmf-extensions.md, with links from extensions.md, cmf.md, and the architecture index.

APL missing-key contract. The “all four Deny” claim is limited to presence, equality, membership, and order. Negated forms (!=, !key, !(...), not in) are true. The idiom is !authenticated — deny when not authenticated does not parse. ppe-pdp-diff now has missing-claim-not-eq and missing-not-in.

On not in, OPA Allows with APL (not of undefined is true); CEL and cedar-direct Deny. That is recorded instead of claiming all three PDPs Deny.

CEL. has(role.hr) is no longer recommended when the role namespace is absent; use subject.roles. has(subject.id) is the guard when the subject namespace exists; only a completely absent subject leaves CEL with no authentication guard.

Payloads. Top-level scalars and scalar arrays land on the bare keys args / result, not only args.* / result.*. Tests pin that.

cedar-direct. A missing subject.type defaults to PascalCase User; the bridge writes lowercase (user), so type-scoped policies can miss that principal.

Changelog. Unreleased now records the BAG_WORKLOAD_PREFIX removal and the read_labels / read_workload mapping changes.

Hosts look at the changelog for public-surface changes; read_labels and
read_workload advertised prefixes the extractors never wrote. Also state
that only five collections flatten and the rest are set-only by design.

Signed-off-by: mkoushni <mkoushni@redhat.com>
@mkoushni

mkoushni commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author
  • Changelog category — teryl't asked for Fixed, not Changed. read_labels used to advertise nothing and read_workload advertised workload.*; both were wrong relative to the extractors. Recategorized locally.

  • PR body — updated on docs(cmf): settle the extensions bag contract #59 so Changes now names capability_namespaces.rs, constants.rs, security.rs, evaluator.rs, payload.rs, and cedar-direct.

  • Non-blocking paragraph — added under original-vs-flattened: the set is primary, the five flattened collections exist because that idiom predates the sets, and the nine others are set-only by design (require(tag.pii) stays false).

@mkoushni
mkoushni requested a review from araujof September 9, 2026 14:05

@terylt terylt left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One nit, not blocking

  • crates/ppe-apl-cmf/src/payload.rs, assert!(!bag.contains("args.")) is vacuous. AttributeBag::contains is an exact key lookup, so a key named args. can never exist. If the intent is no args.* children, use bag.iter() or len() == 1.

Approving. Fine to fold in on merge or leave to #77.

AttributeBag::contains is an exact lookup, so !contains("args.") never
caught children. len() == 1 is the real check.

Signed-off-by: mkoushni <mkoushni@redhat.com>
@mkoushni

mkoushni commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

test(cmf): assert scalar payloads occupy a single bag key

fix: len() == 1

@praxis-bot praxis-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Re-Review

Re-review after 6 new commits. One new convention finding not covered by the previous review.

Severity Count
Critical 0
Large 0
Medium 1

// `docs/content/cmf-extensions.md` "What each decision point does with a
// missing key" — APL row. Presence, equality, membership, and
// order are false. Every negated form is true: `!=`, `!key`,
// `!(...)`, and `not in`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Medium] This 4-line block comment inside the test body violates the convention: "Never use inline comments inside test function bodies. All explanatory text must be either an assertion message or a tracing::debug! call." The function name missing_key_matches_cmf_extensions_table already documents the intent. Move the doc reference into a tracing::debug! call, e.g.:

tracing::debug!("docs/content/cmf-extensions.md — APL missing-key row: presence/equality/membership/order are false; negated forms are true");

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/security documentation Improvements or additions to documentation

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

5 participants