Skip to content

docs(rfc-0005): alias events in the audit stream + v1 reader-side map derivation - #183

Merged
jensholdgaard merged 3 commits into
mainfrom
rfc0005-alias-audit-events
Jun 12, 2026
Merged

docs(rfc-0005): alias events in the audit stream + v1 reader-side map derivation#183
jensholdgaard merged 3 commits into
mainfrom
rfc0005-alias-audit-events

Conversation

@jensholdgaard

@jensholdgaard jensholdgaard commented Jun 12, 2026

Copy link
Copy Markdown
Owner

The gap (issue #148)

RFC 0001 §6.7 (amendment 2026-06-07) pins the alias model and write path: operator-driven, audited alias_asserted / alias_retracted events on the §6.4 audit stream, folded into a per-tenant alias map (ourios-core::alias::AliasMap, RFC0001.12–.16 green) that RFC 0002's resolves_to consumes (RFC0002.9 green). RFC 0001 §9 hands the storage half to "the RFC 0005 line" — but the RFC 0005 §3.7 audit schema only knows kinds 0–3, so alias events have no persistent home (AuditBatchError::AliasEventNotYetPersistable is the writer's interim rejection) and the querier cannot load the map from storage.

What this amendment pins (doc-only)

  1. RFC 0005 §3.7 — two new audit kinds, alias_asserted (4) and alias_retracted (5), matching the ordinals ourios-core::audit already pins. Payload lands in new OPTIONAL kind-prefixed columns per the compaction-amendment pattern: alias_representative_id (INT64), alias_member_ids (LIST<INT64>), alias_actor (STRING); the existing reason column is reused for the operator justification ("" ↔ NULL). The canonical-JSON Utf8 encoding for member_ids was considered and rejected on the same grounds the 2026-06-03 amendment rejected a structured reason: a LIST of ids is first-class queryable where a JSON blob is opaque, and LIST is already the §3.7 precedent for flat set-valued payload fields (positions_widened, compaction_input_files).
  2. Unknown-event_kind tolerance rule pinned. Today's AuditReader hard-errors on unknown ordinals, with a documented deferral "until a real new variant lands" — kinds 4–5 are that variant. The pinned rule: readers surface unknown ordinals as opaque envelope-only events rather than failing the file (the ParamType::Unknown / §3.9 discipline applied to the kind enum). Already-deployed readers are bounded by §3.8 rule 6's version-together argument.
  3. §3.7.1 — the v1 read path. The querier derives the requesting tenant's alias map at query-compile time by scanning the tenant's audit partition for kinds 4/5 (pruned by tenant partition key + event_kind/event_type dict/page-index columns; alias events are rare operator actions, so the scan is small) and folding in event-time order via the RFC 0001 §6.7 projection semantics — referenced, not duplicated. Consistency bound: the map reflects events durably written and flushed at scan time, the same eventual-consistency stance §6.7 already takes.
  4. The cached per-tenant alias-map artifact stays DEFERRED. Its file/format/cadence ride the RFC 0009 §3.4 atomic-publish manifest fork (Epic: Background compaction — small-file consolidation (RFC 0009) #94 / RFC 0005 amendment: queryable/pruneable attribute columns (service.name + attr predicates) #147). Because the audit stream remains the source of truth either way, adding the cache later changes no query-visible semantics — the same v1-full-replay-now / accelerate-later framing RFC 0001 §6.9 pinned for the miner snapshot. This is what ungates RFC 0001 §6.7 alias-index write path → resolves_to expansion (RFC0002.9) #148 from the manifest fork: v1 goes reader-side, no new publish point needed.
  5. Scenario RFC0005.14 — round-trip (full asserted set, actor, reason, per-kind null discipline), querier-side derivation (resolves_to reflects the folded assert/retract state), and tenant isolation. RFC 0001 §6.7 / §9 get matching dated amendment notes; no status: field changes. RFC 0002 needed no edit — it already defers to "the alias-set membership of RFC 0001 §6.7" without implying an operator-supplied-only map.

Invariants / hazards touched (CLAUDE.md §4)

  • §3.5 / H5 (schema changes need a migration plan): §3.8 rule 1 applies — every new column is OPTIONAL; old files lack them and read back as None; no data migration. New kinds follow the established "§3.8 additive amendment" path for the mapping table.
  • §3.1 (no silent merges): unchanged — aliasing stays operator-driven and audited; this amendment only gives the existing audit events a durable column home and a read path.
  • §3.7 (multi-tenancy): the derivation is scoped to the requesting tenant's partition subtree; cross-tenant isolation is pinned in RFC0005.14.
  • §3.6 (object storage is truth): strengthened — the audit stream on object storage becomes the alias source of truth; no local artifact is required.

Verification

  • mdbook build clean (no new diagrams added).
  • Doc-only diff: docs/rfcs/0001-template-miner.md, docs/rfcs/0005-parquet-storage.md. Cargo checks not run — no code changed.

Closes nothing yet — #148's implementation slice (writer columns, reader ordinals 4–5 + tolerance rule, querier derivation) lands against this spec.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation
    • Updated alias-map storage semantics to reflect finalized storage architecture
    • Extended audit-event schema to support alias lifecycle tracking (assertion and retraction events)
    • Clarified alias-map derivation strategy from audit streams with consistency guarantees
    • Updated reader semantics and testing plan for alias event handling

… derivation

RFC 0001 §6.7's alias_asserted / alias_retracted events had no home
in the RFC 0005 §3.7 audit schema, so nothing persisted and the
querier could not load the per-tenant alias map from storage
(issue #148). Pin kinds 4 / 5 with OPTIONAL alias_* columns
(§3.8 rule 1), the unknown-event_kind tolerance rule, and the v1
read path: the querier derives the tenant's map at query-compile
time by folding the audit stream (§3.7.1). The cached per-tenant
artifact stays deferred behind the RFC 0009 §3.4 manifest fork.
Adds scenario RFC0005.14; cross-refs recorded in RFC 0001 §6.7 / §9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jensholdgaard
jensholdgaard requested a review from Copilot June 12, 2026 05:51
@jensholdgaard

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@jensholdgaard, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 19 minutes and 4 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ee69fed-dd6c-40a2-8928-b2c344f5e8f9

📥 Commits

Reviewing files that changed from the base of the PR and between 05234bf and dc63a54.

📒 Files selected for processing (2)
  • docs/rfcs/0001-template-miner.md
  • docs/rfcs/0005-parquet-storage.md
📝 Walkthrough

Walkthrough

This PR amends RFC 0005 (Parquet storage) with alias audit event support (event_kind 4–5), specifying new schema columns, encoding policy, and v1 reader semantics for deriving per-tenant alias maps at query compile time. RFC 0001 is concurrently updated to reflect these finalized decisions.

Changes

Alias Audit Events and Derivation

Layer / File(s) Summary
Alias audit event kinds and schema
docs/rfcs/0005-parquet-storage.md
RFC 0005 audit-event stream updated with 2026-06-12 alias-event amendment; event_kind ordinals 4–5 added for alias_asserted and alias_retracted; new OPTIONAL alias_representative_id, alias_member_ids, alias_actor columns with LIST encoding for member IDs; reason reused for alias justification; unknown event_kind tolerance clarified.
Alias event encoding policy
docs/rfcs/0005-parquet-storage.md
Audit encoding policy extended with dictionary + page index for alias_representative_id, list encoding for alias_member_ids, and dictionary encoding for alias_actor; event_kind updated to include dictionary encoding.
Alias-map reader derivation semantics
docs/rfcs/0005-parquet-storage.md
RFC 0005 §3.7.1 new subsection defines v1 alias-map derivation: reader scans tenant alias audit events, folds them in timestamp order per RFC 0001 §6.7, feeds result to RFC 0002 resolves_to compilation with consistency bound tied to audit-flush visibility.
RFC 0001 alias-index integration
docs/rfcs/0001-template-miner.md
RFC 0001 amended to clarify v1 alias-map materialization at query compile time via audit stream folding; open-questions subsection updated to reflect RFC 0005 ownership of physical storage and snapshot cadence, per-tenant cached artifact deferred to RFC 0009.
Acceptance testing and cross-RFC linkage
docs/rfcs/0005-parquet-storage.md
RFC 0005 acceptance scenario RFC0005.14 added for alias-event round-trip, reason/NULL handling, and derivation validation; testing strategy extended with RFC0005.14 entry and unknown-kind tolerance pin; references updated for audit-event cross-RFC linkage.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

Possibly related PRs

  • jensholdgaard/ourios#153: Main PR implements operator/audited alias events (alias_asserted/alias_retracted as event_kind 4/5) and per-tenant alias-map derivation model that these RFC amendments document and specify.
  • jensholdgaard/ourios#151: Earlier PR introduced RFC 0001 alias-index write and per-tenant alias-map derivation language; this PR evolves that spec with RFC 0005 audit event kinds 4–5 and reader folding semantics.

Poem

🐰 Audit events now join the fold,
Kind four and five, with columns bold,
Aliases traced through tenant streams,
Derived at compile-time in querier dreams.
RFC tales align at last,
The storage spec, the query's past.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding alias events to RFC 0005's audit stream and specifying the v1 reader-side map derivation path.
Description check ✅ Passed The description is comprehensive and detailed, covering the gap, what is being pinned, invariants touched, and verification. However, it lacks explicit coverage of the checklist items (cargo fmt, clippy, tests, docs/CHANGELOG updates).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch rfc0005-alias-audit-events

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR amends the storage RFCs to give RFC 0001’s operator-driven alias events a durable home in the RFC 0005 audit Parquet stream, and to pin the v1 querier read-path for deriving a per-tenant alias map from those stored events.

Changes:

  • Extend RFC 0005 §3.7 audit schema with event_kind ordinals 4–5 (alias_asserted / alias_retracted) and new OPTIONAL alias_* columns.
  • Pin an “unknown event_kind tolerance” rule (readers must not fail files on unknown ordinals; surface envelope-only opaque events).
  • Specify v1 querier behavior: derive the requesting tenant’s alias map at query-compile time by scanning/folding audit rows of kinds 4–5; update RFC 0001 to reflect this v1 storage decision.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
docs/rfcs/0005-parquet-storage.md Adds alias audit kinds/columns, pins unknown-kind tolerance, and specifies the v1 reader-side alias-map derivation and a new scenario/test plan.
docs/rfcs/0001-template-miner.md Adds dated amendment notes linking the alias write-path to RFC 0005’s persisted audit stream and v1 querier derivation approach.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/rfcs/0005-parquet-storage.md Outdated
Comment thread docs/rfcs/0005-parquet-storage.md Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/rfcs/0001-template-miner.md (1)

1833-1857: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the stale “persisted alias map” wording.

This section still says the folded result is persisted as a per-tenant artifact and that the querier reads it at compile time, but the v1 amendment below says there is no persisted alias-map artifact yet and the audit stream is the source of truth. Please rewrite both paragraphs so they describe compile-time derivation from the audit log, with persistence deferred to the RFC 0009 cache.

🛠️ Suggested rewrite
- The folded result is persisted as a per-tenant artifact (one map per tenant, not per partition) that the querier reads at compile time.
+ The querier derives the requesting tenant's alias map at query-compile time by scanning the tenant's audit stream; any persisted alias-map artifact remains deferred to RFC 0009.

Also applies to: 1915-1924

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/rfcs/0001-template-miner.md` around lines 1833 - 1857, Update the
"Materialization and storage." paragraphs to remove references to a persisted
per-tenant alias-map artifact and instead describe that the querier derives
alias mappings at compile time directly from the audit event log (the
`alias_asserted` / `alias_retracted` streams) by folding per-tenant; keep the
behavior details (unioning `{representative_id} ∪ member_ids`, merging
overlapping classes, `min(members)` as canonical representative, single-member
collapse per RFC0001.16) but explicitly state that persistence of a cached alias
map is deferred to RFC0009 rather than being presently persisted; make the same
textual change in the later duplicated passage that currently repeats the
"persisted alias map" wording.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/rfcs/0005-parquet-storage.md`:
- Around line 785-797: The spec must pin a deterministic total order for folding
alias events so same-nanosecond ties across audit file flushes are reproducible:
update the text around the folding step that references `timestamp` order and
`alias_asserted`/`alias_retracted` semantics (and the
`ourios-core::alias::AliasMap` reference) to state events are ordered by
(timestamp, flush UUID, row ordinal) — i.e., primary key `timestamp`, tie-break
by the flush (file) UUID used when the audit file was written, and final
tie-break by the event's row ordinal within that flush — and note that this
total order is used when applying RFC 0001 §6.7 projection semantics so folding
is deterministic across files.

---

Outside diff comments:
In `@docs/rfcs/0001-template-miner.md`:
- Around line 1833-1857: Update the "Materialization and storage." paragraphs to
remove references to a persisted per-tenant alias-map artifact and instead
describe that the querier derives alias mappings at compile time directly from
the audit event log (the `alias_asserted` / `alias_retracted` streams) by
folding per-tenant; keep the behavior details (unioning `{representative_id} ∪
member_ids`, merging overlapping classes, `min(members)` as canonical
representative, single-member collapse per RFC0001.16) but explicitly state that
persistence of a cached alias map is deferred to RFC0009 rather than being
presently persisted; make the same textual change in the later duplicated
passage that currently repeats the "persisted alias map" wording.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9f4296bf-7bec-48de-9bc1-2206ed3b1115

📥 Commits

Reviewing files that changed from the base of the PR and between 42a7aa0 and 05234bf.

📒 Files selected for processing (2)
  • docs/rfcs/0001-template-miner.md
  • docs/rfcs/0005-parquet-storage.md

Comment thread docs/rfcs/0005-parquet-storage.md Outdated
The alias fold order is now (timestamp, file path lexicographic,
within-file row index) — deterministic across files, not just within
one; the member-id list uses the §3.7 unsigned INTEGER(64) notation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

Comment thread docs/rfcs/0005-parquet-storage.md Outdated
Comment thread docs/rfcs/0001-template-miner.md
Comment thread docs/rfcs/0005-parquet-storage.md
…e, v1 artifact note

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jensholdgaard
jensholdgaard merged commit fa66a6a into main Jun 12, 2026
8 of 10 checks passed
jensholdgaard added a commit that referenced this pull request Jun 12, 2026
…e envelope (RFC 0005 §3.7)

The reader rebuilds AliasAsserted / AliasRetracted payloads from the
alias_* columns (empty member list != NULL; NULL reason decodes to the
in-memory empty string; an empty actor is a writer-invariant error —
aliasing is never anonymous, RFC 0001 §6.7).

The UnknownEventKind hard error is replaced by the now-pinned §3.7
tolerance rule: an ordinal above the known range surfaces as the new
envelope-only AuditPayload::Unknown variant (the ParamType::Unknown
discipline applied to the kind enum), preserved verbatim on
read-then-write; folds over named kinds (AliasMap::apply, the drift
filter) ignore it by construction. The forged-ordinal reader test
flips from expect-error to expect-opaque-event per the RFC-gated
contract change (#183, CLAUDE.md §6.2), with a round-trip test
alongside.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jensholdgaard added a commit that referenced this pull request Jun 12, 2026
* feat(parquet): alias_* audit columns + writer mapping for kinds 4/5 (RFC 0005 §3.7, #148)

Adds the three OPTIONAL alias columns the 2026-06-12 amendment pins
(alias_representative_id, alias_member_ids LIST<UInt64> with the
empty-list-vs-NULL distinction, alias_actor), maps AliasAsserted /
AliasRetracted to event_kind 4 / 5 with member_ids stored verbatim and
the "" <-> NULL reason rule, applies the §3.7 encoding-policy rows for
the new columns, and retires the writer's interim
AliasEventNotYetPersistable rejection. Schema change is §3.8 rule 1
(additive OPTIONAL); the schema-pin fixture moves in lockstep. The
colocated expect-error test flips per the RFC-gated contract change
(CLAUDE.md §6.2), citing the amendment in its doc comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(parquet,core): decode alias kinds; unknown kinds become an opaque envelope (RFC 0005 §3.7)

The reader rebuilds AliasAsserted / AliasRetracted payloads from the
alias_* columns (empty member list != NULL; NULL reason decodes to the
in-memory empty string; an empty actor is a writer-invariant error —
aliasing is never anonymous, RFC 0001 §6.7).

The UnknownEventKind hard error is replaced by the now-pinned §3.7
tolerance rule: an ordinal above the known range surfaces as the new
envelope-only AuditPayload::Unknown variant (the ParamType::Unknown
discipline applied to the kind enum), preserved verbatim on
read-then-write; folds over named kinds (AliasMap::apply, the drift
filter) ignore it by construction. The forged-ordinal reader test
flips from expect-error to expect-opaque-event per the RFC-gated
contract change (#183, CLAUDE.md §6.2), with a round-trip test
alongside.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(querier): extract the shared audit partition walk into audit_scan

Pure relocation of drift.rs's tenant-rooted audit-tree walk (canonical
escape backstop, canonical-path de-dup, conservative day prune) into a
shared module, with the day window now optional and the resolved file
set sorted lexicographically — the file-path component of the RFC 0005
§3.7.1 total fold order the alias-map derivation needs next. The two
prune unit tests move with the helpers, unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(querier): derive the tenant alias map from the audit stream (RFC 0005 §3.7.1, #148)

run_query's alias parameter becomes Option<&AliasMap>: None — the
production default — folds the requesting tenant's map from its
audit/ partition at compile time (alias_store::derive_alias_map),
reading kinds 4/5 via AuditReader in the §3.7.1 total order
(timestamp, file path lexicographic, within-file row index — the
shared walk's sorted file set + in-file row order under a stable
sort by timestamp) and handing the events to
ourios-core::alias::AliasMap::from_events, whose RFC 0001 §6.7
semantics this does not restate. Some(map) stays the test/operator
override, bypassing storage. Queries with no resolves_to skip the
audit scan entirely. A row claiming another tenant under the
tenant's partition root is a loud Storage error, not a silent drop
(CLAUDE.md §3.7). Test updates are mechanical Option-wrapping at the
call sites; no assertion changes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: land RFC0005.14 + the storage-backed RFC0002.9 (#148 step 3)

(a) round-trip through the real audit writer/reader: member set
verbatim (order + duplicate preserved), empty-list retraction != NULL,
actor, the "" <-> NULL reason rule, and raw-column NULL discipline per
kind (§3.8 rule 6). (b) the §3.7.1 derived fold: assert-then-retract by
event time, plus both directions of the cross-file same-timestamp
tiebreak — one event per file, files renamed into a crafted
lexicographic order so the outcome is decided by the file-path
component of the total order and nothing else. (c) tenant isolation:
a second tenant's stored alias events never fold into the requesting
tenant's derived map (CLAUDE.md §3.7). (d) storage-backed RFC0002.9:
the assertion is written through ParquetAuditSink and run_query(None)
derives the map end-to-end — resolves_to(A) returns A ∪ {B} while
template_id == A stays exactly A; the injected-map RFC0002.9 test
stays alongside, unweakened.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(querier): bucket-root trust anchor for the audit scan; validate before alias derivation

Review round 1:
- a symlinked audit/tenant_id=... root is now rejected outright (the
  per-file starts_with backstop trusted the symlink's own resolution)
- run_query validates the query before paying the alias-derivation IO,
  so compile errors precede Storage errors deterministically
- tenant-mismatch error renders tenant IDs with Display, not Debug
- tests: symlinked-root rejection, error precedence, and the T2
  isolation test now proves its own map was derived (resolves_to == 2)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(querier,parquet): precise error-precedence comment; batch-local row index in messages

Copilot round 2, wording only: the run_query comment no longer implies
all invalid-query errors precede the audit scan (predicate compilation
needs the map), and the non-nullable-element conversion messages say
"batch row" — the index is batch-local, not file-global.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

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.

2 participants