Skip to content

Make error handling order-independent and fail closed (#267) - #268

Merged
tonyseale merged 2 commits into
EKGF:developfrom
matthiasautrata:issue/267-order-independent-errors
Oct 29, 2026
Merged

tonyseale merged 2 commits into
EKGF:developfrom
matthiasautrata:issue/267-order-independent-errors

Conversation

@matthiasautrata

@matthiasautrata matthiasautrata commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Resolves #267.

§6.1 evaluated And/Or left to right with short-circuit, and a resolution
error aborted evaluation. The outcome then depended on operand and rule
order: And(false, error) was false, And(error, false) was Failure, and
in §7.2 an error in a duty's activation condition (Step 3) aborted
before an active prohibition (Step 4) could deny. §6.2 made incomparable
operands false, so Not(x < 5) on a string was true and a prohibition
guarded by an ill-typed comparison failed open.

  • §6.1: conditions are three-valued (true, false, Err(E)); And, Or, Not
    are strong Kleene. Each connective depends only on the set of operand
    values, and replacing an error by a Boolean never changes a Boolean
    result. Errors still never become false.
  • §6.2: every operator requires operands of one type, and ordering
    operators require ordered values; otherwise ResolutionError
    (Incomparable). This covers neq and isNoneOf, which would
    otherwise fail open the same way.
  • §7.1: EvaluationError is the set of errors, not the first one found.
  • §7.2: an active prohibition denies whatever else is in error; an error
    in a prohibition or in a policy's applicability gives Failure only if
    no prohibition is active; an error in a permission matters only if no
    permission is active; duty states are still updated before the
    decision steps (Step 3), a duty whose activation is in error stays
    Pending, and that unknown activation gives Failure only on the Permit
    path. §8.2 matches.
  • §9.6 (order independence) and §9.7 (error soundness: a Success
    decision, and the duties reported with a Permit, are the same under
    every resolution of the errors; Failure may be conservative). §13.2's
    Dafny sketch uses Kleene connectives; S1, S5, S7 in §13.3.
  • README and overview: "Deterministic left-to-right" becomes
    "Order-independent; errors never fail open". Specification §4.8 no
    longer says "hard" errors.
  • Add tests/test_error_semantics.py: a reference check of the truth
    tables, and documentation checks that fail on the previous text.

One of five independent fixes to the formal semantics, each with its own issue and PR. The PRs overlap only in formal-semantics.md §7.2 (Steps 5-8, between this one and the error-handling and duty-collection PRs); rebase onto develop after whichever merges first.

@vercel

vercel Bot commented Oct 10, 2026

Copy link
Copy Markdown

@matthiasautrata is attempting to deploy a commit to the EKGF Team on Vercel.

A member of the Team first needs to authorize it.

matthiasautrata added a commit to matthiasautrata/dprod that referenced this pull request Oct 13, 2026
…EKGF#271)

Resolves EKGF#271.

§7.2 Step 1 collected a duty only if `matches(n, request)`, i.e. if the
duty's own action and asset matched the request. A duty states what the
bearer must do, not what is requested, so a request to display
ex:marketData never collected the duty to report on ex:usageStats, and
§12.3's example returned no duties. Step 2 then partitioned by
`d.subject = grantor` / `d.subject = grantee`, so a duty without
dprod:subjectOfDuty belonged to neither party, though specification
§3.2 and term-mapping describe consumer duties without a subject.

The ontology comment on dprod:subjectOfDuty contradicted those
documents: it defaulted the bearer to the data product owner, which is
the provider.

- §3.2: before evaluation, a duty without a subject takes the policy's
  grantee, or the requesting agent in a policy without one. `d.subject`
  is then the bearer everywhere, including `performed`. A duty borne by
  the requesting agent stands for one duty per agent, tracked
  separately in Σ, so one agent's missed duty is not held against
  another.
- §7.2: duties are collected from every policy with a permission in
  force for the request; grantor duties are those borne by the grantor,
  and all others are grantee duties. §12.2 matches.
- Collecting every duty of a granting policy makes a provider's missed
  delivery reachable from every consumer request. Violated is terminal,
  so denying on it would lock the consumer out permanently. Step 6 now
  denies only on violations by the grantee's side; the grantor's are
  reported in `violations` on the Permit.
- dprod:subjectOfDuty comment, specification §4.10, and term-mapping
  state the default.
- Add tests/test_duty_bearer.py, which fails on the previous text.

Rebased onto EKGF#268 (order-independent errors): a policy grants only if a
permission is active (= true), and a permission in error in a policy
that does not otherwise grant (E4) gives Failure on the Permit path,
since resolving it could engage that policy's duties. Section 9.7 now
also keeps the grantor violations reported with a Permit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@matthiasautrata
matthiasautrata marked this pull request as ready for review October 13, 2026 23:50
matthiasautrata and others added 2 commits October 21, 2026 17:35
Resolves EKGF#267.

§6.1 evaluated And/Or left to right with short-circuit, and a resolution
error aborted evaluation. The outcome then depended on operand and rule
order: And(false, error) was false, And(error, false) was Failure, and
in §7.2 an error in a duty's activation condition (Step 3) aborted
before an active prohibition (Step 4) could deny. §6.2 made incomparable
operands `false`, so Not(x < 5) on a string was true and a prohibition
guarded by an ill-typed comparison failed open.

- §6.1: conditions are three-valued (true, false, Err(E)); And, Or, Not
  are strong Kleene. Each connective depends only on the set of operand
  values, and replacing an error by a Boolean never changes a Boolean
  result. Errors still never become `false`.
- §6.2: every operator requires operands of one type, and ordering
  operators require ordered values; otherwise ResolutionError
  (Incomparable). This covers `neq` and `isNoneOf`, which would
  otherwise fail open the same way.
- §7.1: EvaluationError is the set of errors, not the first one found.
- §7.2: an active prohibition denies whatever else is in error; an error
  in a prohibition or in a policy's applicability gives Failure only if
  no prohibition is active; an error in a permission matters only if no
  permission is active; duty states are updated after the decision
  steps, and an unknown activation gives Failure only on the Permit
  path. §8.2 matches.
- §9.6 (order independence) and §9.7 (error soundness: a Success
  decision, and the duties reported with a Permit, are the same under
  every resolution of the errors; Failure may be conservative). §13.2's
  Dafny sketch uses Kleene connectives; S1, S5, S7 in §13.3.
- README and overview: "Deterministic left-to-right" becomes
  "Order-independent; errors never fail open". Specification §4.8 no
  longer says "hard" errors.
- Add tests/test_error_semantics.py: a reference check of the truth
  tables, and documentation checks that fail on the previous text.
Moving the duty-state update after the prohibition and permission checks
made a duty activate, and be found violated, only on a permitted request,
which shifted activatedAt and relative deadlines. Error soundness does not
need the move: a duty whose activation condition is in error stays
Pending, and its error still gives Failure only on the Permit path.

Resolves: EKGF#267

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jgeluk pushed a commit to matthiasautrata/dprod that referenced this pull request Oct 21, 2026
…EKGF#271)

Resolves EKGF#271.

§7.2 Step 1 collected a duty only if `matches(n, request)`, i.e. if the
duty's own action and asset matched the request. A duty states what the
bearer must do, not what is requested, so a request to display
ex:marketData never collected the duty to report on ex:usageStats, and
§12.3's example returned no duties. Step 2 then partitioned by
`d.subject = grantor` / `d.subject = grantee`, so a duty without
dprod:subjectOfDuty belonged to neither party, though specification
§3.2 and term-mapping describe consumer duties without a subject.

The ontology comment on dprod:subjectOfDuty contradicted those
documents: it defaulted the bearer to the data product owner, which is
the provider.

- §3.2: before evaluation, a duty without a subject takes the policy's
  grantee, or the requesting agent in a policy without one. `d.subject`
  is then the bearer everywhere, including `performed`. A duty borne by
  the requesting agent stands for one duty per agent, tracked
  separately in Σ, so one agent's missed duty is not held against
  another.
- §7.2: duties are collected from every policy with a permission in
  force for the request; grantor duties are those borne by the grantor,
  and all others are grantee duties. §12.2 matches.
- Collecting every duty of a granting policy makes a provider's missed
  delivery reachable from every consumer request. Violated is terminal,
  so denying on it would lock the consumer out permanently. Step 6 now
  denies only on violations by the grantee's side; the grantor's are
  reported in `violations` on the Permit.
- dprod:subjectOfDuty comment, specification §4.10, and term-mapping
  state the default.
- Add tests/test_duty_bearer.py, which fails on the previous text.

Rebased onto EKGF#268 (order-independent errors): a policy grants only if a
permission is active (= true), and a permission in error in a policy
that does not otherwise grant (E4) gives Failure on the Permit path,
since resolving it could engage that policy's duties. Section 9.7 now
also keeps the grantor violations reported with a Permit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jgeluk added a commit to matthiasautrata/dprod that referenced this pull request Oct 21, 2026
[Carried forward onto develop for EKGF#275 by Matthias Autrata: conflicts
resolved against EKGF#244/EKGF#245/EKGF#262/EKGF#268/EKGF#270/EKGF#272, the namespace changed from
https://www.omg.org/spec/DPROD/dprod/ to https://ekgf.org/dprod/spec/develop/,
and EKGF#244's duty-only shape and test migrated from dprod:recurrence to
dprod:schedule, which is not duty-only.]
jgeluk pushed a commit to matthiasautrata/dprod that referenced this pull request Oct 21, 2026
…EKGF#271)

Resolves EKGF#271.

§7.2 Step 1 collected a duty only if `matches(n, request)`, i.e. if the
duty's own action and asset matched the request. A duty states what the
bearer must do, not what is requested, so a request to display
ex:marketData never collected the duty to report on ex:usageStats, and
§12.3's example returned no duties. Step 2 then partitioned by
`d.subject = grantor` / `d.subject = grantee`, so a duty without
dprod:subjectOfDuty belonged to neither party, though specification
§3.2 and term-mapping describe consumer duties without a subject.

The ontology comment on dprod:subjectOfDuty contradicted those
documents: it defaulted the bearer to the data product owner, which is
the provider.

- §3.2: before evaluation, a duty without a subject takes the policy's
  grantee, or the requesting agent in a policy without one. `d.subject`
  is then the bearer everywhere, including `performed`. A duty borne by
  the requesting agent stands for one duty per agent, tracked
  separately in Σ, so one agent's missed duty is not held against
  another.
- §7.2: duties are collected from every policy with a permission in
  force for the request; grantor duties are those borne by the grantor,
  and all others are grantee duties. §12.2 matches.
- Collecting every duty of a granting policy makes a provider's missed
  delivery reachable from every consumer request. Violated is terminal,
  so denying on it would lock the consumer out permanently. Step 6 now
  denies only on violations by the grantee's side; the grantor's are
  reported in `violations` on the Permit.
- dprod:subjectOfDuty comment, specification §4.10, and term-mapping
  state the default.
- Add tests/test_duty_bearer.py, which fails on the previous text.

Rebased onto EKGF#268 (order-independent errors): a policy grants only if a
permission is active (= true), and a permission in error in a policy
that does not otherwise grant (E4) gives Failure on the Permit path,
since resolving it could engage that policy's duties. Section 9.7 now
also keeps the grantor violations reported with a Permit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jgeluk added a commit to matthiasautrata/dprod that referenced this pull request Oct 21, 2026
[Carried forward onto develop for EKGF#275 by Matthias Autrata: conflicts
resolved against EKGF#244/EKGF#245/EKGF#262/EKGF#268/EKGF#270/EKGF#272, the namespace changed from
https://www.omg.org/spec/DPROD/dprod/ to https://ekgf.org/dprod/spec/develop/,
and EKGF#244's duty-only shape and test migrated from dprod:recurrence to
dprod:schedule, which is not duty-only.]
jgeluk pushed a commit to matthiasautrata/dprod that referenced this pull request Oct 21, 2026
…EKGF#271)

Resolves EKGF#271.

§7.2 Step 1 collected a duty only if `matches(n, request)`, i.e. if the
duty's own action and asset matched the request. A duty states what the
bearer must do, not what is requested, so a request to display
ex:marketData never collected the duty to report on ex:usageStats, and
§12.3's example returned no duties. Step 2 then partitioned by
`d.subject = grantor` / `d.subject = grantee`, so a duty without
dprod:subjectOfDuty belonged to neither party, though specification
§3.2 and term-mapping describe consumer duties without a subject.

The ontology comment on dprod:subjectOfDuty contradicted those
documents: it defaulted the bearer to the data product owner, which is
the provider.

- §3.2: before evaluation, a duty without a subject takes the policy's
  grantee, or the requesting agent in a policy without one. `d.subject`
  is then the bearer everywhere, including `performed`. A duty borne by
  the requesting agent stands for one duty per agent, tracked
  separately in Σ, so one agent's missed duty is not held against
  another.
- §7.2: duties are collected from every policy with a permission in
  force for the request; grantor duties are those borne by the grantor,
  and all others are grantee duties. §12.2 matches.
- Collecting every duty of a granting policy makes a provider's missed
  delivery reachable from every consumer request. Violated is terminal,
  so denying on it would lock the consumer out permanently. Step 6 now
  denies only on violations by the grantee's side; the grantor's are
  reported in `violations` on the Permit.
- dprod:subjectOfDuty comment, specification §4.10, and term-mapping
  state the default.
- Add tests/test_duty_bearer.py, which fails on the previous text.

Rebased onto EKGF#268 (order-independent errors): a policy grants only if a
permission is active (= true), and a permission in error in a policy
that does not otherwise grant (E4) gives Failure on the Permit path,
since resolving it could engage that policy's duties. Section 9.7 now
also keeps the grantor violations reported with a Permit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jgeluk
jgeluk force-pushed the issue/267-order-independent-errors branch from 5c766bc to b6a67eb Compare October 21, 2026 16:37
@tonyseale
tonyseale merged commit 6e83436 into EKGF:develop Oct 29, 2026
2 of 3 checks passed
tonyseale pushed a commit to matthiasautrata/dprod that referenced this pull request Oct 29, 2026
…EKGF#271)

Resolves EKGF#271.

§7.2 Step 1 collected a duty only if `matches(n, request)`, i.e. if the
duty's own action and asset matched the request. A duty states what the
bearer must do, not what is requested, so a request to display
ex:marketData never collected the duty to report on ex:usageStats, and
§12.3's example returned no duties. Step 2 then partitioned by
`d.subject = grantor` / `d.subject = grantee`, so a duty without
dprod:subjectOfDuty belonged to neither party, though specification
§3.2 and term-mapping describe consumer duties without a subject.

The ontology comment on dprod:subjectOfDuty contradicted those
documents: it defaulted the bearer to the data product owner, which is
the provider.

- §3.2: before evaluation, a duty without a subject takes the policy's
  grantee, or the requesting agent in a policy without one. `d.subject`
  is then the bearer everywhere, including `performed`. A duty borne by
  the requesting agent stands for one duty per agent, tracked
  separately in Σ, so one agent's missed duty is not held against
  another.
- §7.2: duties are collected from every policy with a permission in
  force for the request; grantor duties are those borne by the grantor,
  and all others are grantee duties. §12.2 matches.
- Collecting every duty of a granting policy makes a provider's missed
  delivery reachable from every consumer request. Violated is terminal,
  so denying on it would lock the consumer out permanently. Step 6 now
  denies only on violations by the grantee's side; the grantor's are
  reported in `violations` on the Permit.
- dprod:subjectOfDuty comment, specification §4.10, and term-mapping
  state the default.
- Add tests/test_duty_bearer.py, which fails on the previous text.

Rebased onto EKGF#268 (order-independent errors): a policy grants only if a
permission is active (= true), and a permission in error in a policy
that does not otherwise grant (E4) gives Failure on the Permit path,
since resolving it could engage that policy's duties. Section 9.7 now
also keeps the grantor violations reported with a Permit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tonyseale pushed a commit to matthiasautrata/dprod that referenced this pull request Oct 29, 2026
[Carried forward onto develop for EKGF#275 by Matthias Autrata: conflicts
resolved against EKGF#244/EKGF#245/EKGF#262/EKGF#268/EKGF#270/EKGF#272, the namespace changed from
https://www.omg.org/spec/DPROD/dprod/ to https://ekgf.org/dprod/spec/develop/,
and EKGF#244's duty-only shape and test migrated from dprod:recurrence to
dprod:schedule, which is not duty-only.]
tonyseale pushed a commit that referenced this pull request Oct 29, 2026
…#271) (#272)

Resolves #271.

§7.2 Step 1 collected a duty only if `matches(n, request)`, i.e. if the
duty's own action and asset matched the request. A duty states what the
bearer must do, not what is requested, so a request to display
ex:marketData never collected the duty to report on ex:usageStats, and
§12.3's example returned no duties. Step 2 then partitioned by
`d.subject = grantor` / `d.subject = grantee`, so a duty without
dprod:subjectOfDuty belonged to neither party, though specification
§3.2 and term-mapping describe consumer duties without a subject.

The ontology comment on dprod:subjectOfDuty contradicted those
documents: it defaulted the bearer to the data product owner, which is
the provider.

- §3.2: before evaluation, a duty without a subject takes the policy's
  grantee, or the requesting agent in a policy without one. `d.subject`
  is then the bearer everywhere, including `performed`. A duty borne by
  the requesting agent stands for one duty per agent, tracked
  separately in Σ, so one agent's missed duty is not held against
  another.
- §7.2: duties are collected from every policy with a permission in
  force for the request; grantor duties are those borne by the grantor,
  and all others are grantee duties. §12.2 matches.
- Collecting every duty of a granting policy makes a provider's missed
  delivery reachable from every consumer request. Violated is terminal,
  so denying on it would lock the consumer out permanently. Step 6 now
  denies only on violations by the grantee's side; the grantor's are
  reported in `violations` on the Permit.
- dprod:subjectOfDuty comment, specification §4.10, and term-mapping
  state the default.
- Add tests/test_duty_bearer.py, which fails on the previous text.

- Rebased onto #268 (order-independent errors). A policy grants only
  if one of its permissions is active (`= true`). A permission in error
  in a policy that does not otherwise grant (E₄) gives Failure on the
  Permit path, because resolving it could engage that policy's duties.
  §9.7 also keeps the grantor violations reported with a Permit.

**Stacked on #268.** Until #268 merges, this PR also shows #268's two
commits; review only the last one. After #268 is squash-merged:
`git rebase --onto origin/develop 5c766bc
issue/271-duty-collection-and-bearer`
(or squash-merge directly: #268's changes are identical on both sides).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
tonyseale pushed a commit to matthiasautrata/dprod that referenced this pull request Oct 29, 2026
[Carried forward onto develop for EKGF#275 by Matthias Autrata: conflicts
resolved against EKGF#244/EKGF#245/EKGF#262/EKGF#268/EKGF#270/EKGF#272, the namespace changed from
https://www.omg.org/spec/DPROD/dprod/ to https://ekgf.org/dprod/spec/develop/,
and EKGF#244's duty-only shape and test migrated from dprod:recurrence to
dprod:schedule, which is not duty-only.]
tonyseale pushed a commit that referenced this pull request Oct 29, 2026
Resolves #184. Supersedes #234. Step 7 of #275.

Carries Jacobus Geluk's reusable schedule model (#234) forward onto
`develop`. His two commits are kept with their authorship (`047dcb4`,
`81777dd`); each has a note recording what changed while carrying it
forward. Following Tony's and Jacobus's decision on #184, the anchor
rule is: **the schedule's DTSTART/TZ sets the occurrence times, and
activation only limits the range.**

## Stacking

This branch is built on #268 → #272, plus a merge of #270 (`687ea77`),
because the schedule errors use #268's error model and §3.2 combines
with #270's `asset`. Until those three merge, this PR also shows their
commits. The schedule work is the last four commits:

- `047dcb4` Implement reusable schedule model (Jacobus)
- `81777dd` Address review findings on the schedule model (Jacobus)
- `7c67d39` Migrate the spec and examples from `dprod:recurrence` to
`dprod:schedule`
- `257e65c` Bump the contracts modification dates

**Merge after #270, #268 and #272.** Then squash-merge directly (the
shared changes are identical) or first rebase the four commits onto
`develop`.

## Changes compared with #234

- **No frozen-archive changes, and no duplicate of #244.** #244's
duty-only shape and test now cover `dprod:deadline`,
`dprod:subjectOfDuty` and `dprod:objectOfDuty`. `dprod:schedule` is not
duty-only. The obsolete `dprod:recurrence` is rejected by
`RejectObsoleteRecurrenceShape` on every rule type, and there are tests
for both.
- **Develop namespace.** `https://www.omg.org/spec/DPROD/dprod/` is
replaced throughout, including the TZID-conflict SPARQL. The namespace
move stays at step 10 of #275.
- **Schedule errors fit #267's fail-closed model.** There is no `errors`
field on the Result. A schedule that can't be processed adds its error
to a new set E₅ (`occurrences(d, Σ) = Error(e)`, computed in Step 3),
and Step 7 returns `Failure(E₃ ∪ E₄ ∪ E₅)`. D-FREEZE: a duty whose
schedule is in error makes no transition, so only the Permit path is
affected; Deny and NotApplicable are not. `EvaluationError =
Set<ResolutionError | ScheduleError>`, and the §9.7 completion argument
also replaces each schedule error with some set of occurrence times.
- **The workflow change is dropped.** #262 already runs `validate.py` in
CI.
- **`ScheduleIdentifierShape` is renamed
`RejectAnonymousScheduleShape`.** The spec generator renders every
NodeShape that has a `sh:targetClass` as a class except `Reject*`
shapes, so the old name appeared as a spurious "ScheduleIdentifier"
class.
- **The core spec and examples move to `dprod:schedule`.** The
data-offer-and-contract, lifecycle-and-versioning, provider-duties and
odcs-mapping examples, plus the `respec/template.html` contract example,
now use identified `Schedule` nodes with an explicit `DTSTART;TZID=…`
and an RRULE. The overview diagram is regenerated, and
`tests/test_spec_contract_sections.py` expects `Schedule` and
`ScheduleFormat` classes.
- **The modification dates are bumped** in the contracts docs and in
`dct:modified` on the profile, shapes and vocabulary.

All 9 review findings on #234 were checked and are addressed. They are:
crontab whitespace, the RFC 5545 regex (EXDATE/RDATE order, duplicate
FREQ), the TZ conflict and `carriesTimeZone`, how ScheduleError is
consumed and totality, the `expand` signature, bare-config tests,
undefined guide examples, the identifier message on its own shape, the
RRULE table, and the recurrence sweep of specification.md.

## Tests

- This branch: core 26 OK, contracts 98 OK, `validate.py` clean.
- Integrated with #264, #266, #270, #268 and #272: core 26 OK, contracts
102 OK, `validate.py` clean.

Every JSON-LD example that uses a schedule conforms to the contracts
shapes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Jacobus Geluk <jacobus.geluk@ekgf.org>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Formal semantics: evaluation order changes the outcome, and type errors fail open

2 participants