Skip to content

SCALE-SEAM (99) — the content shelf, and a destination header that was wrong until now - #432

Merged
ibuilder merged 10 commits into
mainfrom
claude/codebase-audit-roadmap-45656d
Sep 4, 2026
Merged

SCALE-SEAM (99) — the content shelf, and a destination header that was wrong until now#432
ibuilder merged 10 commits into
mainfrom
claude/codebase-audit-roadmap-45656d

Conversation

@ibuilder

@ibuilder ibuilder commented Sep 4, 2026

Copy link
Copy Markdown
Owner

What & why

apps/web/src/api/client.ts goes 665 → 648 (62 methods above the STAYING banner, 4 below).
contentCatalog, placeContent and importContent move to the existing
apps/web/src/api/authoring.ts. What they answer: what pre-made content can I place, and place
it?
Continues SCALE-SEAM in docs/roadmap.md.

A role-for-role parallel, read off the signatures

The family shelf was already in that file. The content shelf is the same three roles, with matching
shapes and arities:

role family content
catalog reader familyCatalog(){count, categories: Record<…>} contentCatalog(){count, note, groups: Record<…>}
placer placeFamily(pid, family, position) placeContent(pid, category, point, name)
multipart importer async importFamilies(pid, file, …) async importContent(pid, file, opts)

A parallel between two method triples is structural. "Both are shelves" would have been a
shared word — the grouping (88) and (89) each had to reject — so the argument here is deliberately
the signatures, not the noun.

The destination's own first line was wrong until this commit

authoring.ts line 1 has read "the family/content shelf" while the file held zero content
methods; the word's only other appearances there are an HTTP header and a sentence about IFC type
content. The docstring stated an intended scope as though it were a fact.

This is recorded as corroboration that was false, not as evidence. A header agreeing with the
answer is worth nothing until someone checks whether it is true — and this one had been wrong for as
long as it had existed. It is the smallest possible instance of the drift this repo's instructions
keep warning about, and it is exactly the kind of agreement that is tempting to cite instead of
verify.

Note on this branch's history

#431 was squash-merged, so origin/main carries (98) as ae4bc0e7 while this branch still held the
pre-squash commits. The content diff between the remote branch and origin/main was verified empty
first, so the branch held no unmerged work; the histories were reconciled with a -s ours merge
rather than a force-push.

Checklist (mirrors CONTRIBUTING.md)

  • Backend: cd services/api && python -m ruff check ../.. clean
  • Backend: test_file_sizes.py, test_claude_md_gates.py, test_roadmap_status.py exit 0;
    full suite running as a cross-check
  • Web: npx tsc --noEmit 0, npm run lint 0, npm run build 0;
    npx vitest run src/api 27 files / 119 tests
  • No import cycles introduced
  • CHANGELOG.md entry added (newest at top); docs/roadmap.md updated
  • Version bump — n/a, not a release PR
  • No secrets, no competitor names in shipped docs

🤖 Generated with Claude Code

https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA


Generated by Claude Code

Summary by CodeRabbit

  • API Improvements

    • Content catalog browsing, content placement, and multipart content importing are now available through the authoring API.
    • Content placement supports location, category, naming, and publishing options.
    • Content importing supports category, scale, storey, naming, and related import options.
  • Documentation

    • Updated API and roadmap documentation to reflect the available content-authoring capabilities.

…trix loses

Moves `lodSummary`/`setLod` and `phasing`/`setPhase` out of `client.ts` into
`api/model.ts`. They answer one question — *what state are the model's elements
in, and set it?* — and `client.ts` goes 727 -> 711, 76 methods above the STAYING
banner.

The grouping is derived, not asserted:

- identical return shape `{ total, <x>ed, prop, counts: Record<...> }`;
- both writers are `(pid, guids, <enum>, publish) -> editIfc`;
- both readers are consumed by `viewer/tools/modelStatePanels.ts` (251, 316);
- both writers sit unwired and *adjacent* on `clientCallers.test.ts`'s UNCALLED
  allowlist;
- `model.ts` already owned `/model/lod/census`, `/lod/handover-readiness` and
  `/lod/assessment`, while the base distribution `/projects/{pid}/lod` was left
  behind in `client.ts` — `lodSummary` was a sibling separated from its family.

`authoring_matrix.py` DISAGREES and is recorded as the losing vote rather than
elided. It files `set_lod` under `data` and `set_phase` under `lifecycle`,
because it categorises by the IFC output each recipe writes — an LOD stage tag
against `Massing_Phasing.Status`. Different property sets, same question, which
is (89)'s "storage is a HOW" trap. This is the first slice where the matrix has
been wrong after being right three running.

It also MEETS (94)'s objection rather than overriding it: that slice declined
`setPhase` because taking the writer alone would have stranded `phasing()`, the
reader/writer split (87) had to undo. Both halves move together here.

The four names are added to `surface.test.ts` because its floor is a slack
ratchet (788 actual vs 751 floor) — the count alone would not notice a loss —
and because the UNCALLED allowlist is about call sites, not the surface.

Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0,
`test_file_sizes.py` / `test_claude_md_gates.py` / `test_roadmap_status.py` /
ruff all 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
…ness that actually bounds

Moves `lod500`, `setManufacturerInfo` and `attachOmDocument` out of `client.ts`
into `api/model.ts`, rejoining `verifyAsbuilt` and `recordAsbuiltDimension`
which (94) moved. `client.ts` goes 711 -> 698, 73 methods above the STAYING
banner.

TWO BOUNDING WITNESSES, derived independently, agreeing:

- `openAsBuiltPanel` in `viewer/tools/modelStatePanels.ts` calls exactly five
  API methods — read off that function's brace closure rather than grepped for,
  so it is the closure of a scope and not a sample. Two of the five were
  already in `model.ts`.
- The reader's own response type names its writer set field by field:
  `verified`/`by_method` from `verify_asbuilt`;
  `with_dimensions`/`dimensions_out_of_tolerance` from
  `record_asbuilt_dimension`; `with_manufacturer`/`with_serial` from
  `set_manufacturer_info`; `with_om_docs`/`om_documents` from
  `attach_om_document`. The backend route says it in prose: "Stamp elements
  with the `verify_asbuilt` recipe."

A reader whose response type enumerates its writers is a DERIVED population.
After eight slices of sampling, that is the first grouping witness here that
bounds a set instead of illustrating one.

WHAT CORROBORATES IS NOT WHAT BOUNDS. `test_lod500.py` reaches exactly three
recipes — `attach_om_document`, `set_manufacturer_info`, `verify_asbuilt` — and
OMITS `record_asbuilt_dimension`, which is unambiguously in this family. It
agrees with the answer without being able to establish it. Three slices claimed
"and no others" off a test file and were wrong all three times.

NOT CLAIMED, and said in the header, the pin, the changelog and the roadmap
rather than only in review: `attachDocument` stays in `client.ts`, takes a
`purpose` parameter, and `asbuilt_summary` counts ANY purpose-tagged document
reference — so "every writer of `with_om_docs` moves here" is false. The field
map is of the recipes each field was designed around, not of everything that
can set it.

Two sources disagree and lose for the same reason: `attach_om_document` is a
purpose-tagged wrapper of `detailing.attach_document`, and
`authoring_matrix.py` files it and `set_manufacturer_info` under `data`. The
first is a shared HELPER, the second a STORAGE bucket — the groupings (89) and
(90) each had to reject. The matrix is the losing vote twice running after
being right three times.

Also fixes the extraction helper: it terminated its brace count on the method
signature line, so a signature wrapping across two lines with no opening brace
on the first read as a 3-line method. `attachOmDocument` is 6.

Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0,
ruff (the CI command, from `services/api`) 0, and `test_file_sizes.py`,
`test_claude_md_gates.py`, `test_roadmap_status.py`, `test_ruff_scope.py`,
`test_reachable.py` all 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
PR #412 was squash-merged, so origin/main carries (95) as b0e8281 while this
branch still pointed at the pre-squash 44a6ed4. The content diff between the
remote branch and origin/main was empty before this merge, so the branch held
no unmerged work; -s ours keeps this branch's tree (origin/main plus (96)) and
reconciles the histories without rewriting the remote branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
… and was not

Moves `editHistory`, `editUndo` and `editRedo` out of `client.ts` into
`api/authoring.ts`. `client.ts` goes 698 -> 687, 70 methods above the STAYING
banner.

WHY THAT MIXIN: `editIfc`, already there, is the PUSH they pop. `authoring.py`
records the pre-edit version on every `/edit` call "so this edit can be undone",
`_restore_version` pops that stack, and `edit_history.state()` reads its depths.
One stack, and the operation that fills it was already in this file.

The types agree: both writers return `{restored, state: {can_undo, can_redo}}`,
and `state` is `editHistory`'s own return type minus the depths — the writers
hand back the reader's answer. That is a type-level relation, not the shared
`/edit/` prefix. The prefix is real and is deliberately not the argument, since
a route prefix is exactly what the verification slice was caught grouping on.

A HYPOTHESIS TESTED AND WITHDRAWN, which is the part worth keeping. "Undo
restores the prior model version" makes `model.ts` the obvious home — it owns
`modelVersions`, `versionDiff`, `versionCostDelta`. It is the wrong home: those
read `/projects/{pid}/versions` out of `bim.py`, while undo pops a DIFFERENT
stack, the `edit_history` sidecar, which `recipe_log.py` describes as a list of
file paths with "No recipe, no parameters, no actor". Two stacks, one word — and
the word is what made the wrong answer look obvious. Second withdrawal of a
plausible destination after checking it, after (93) withdrew (92)'s `HttpCore`
forecast.

THE BOUND IS WEAKER THAN (96)'s AND IS STATED THAT WAY. `app.ts`'s S4 block
wires `refreshUndo` (calling only `editHistory`) and `doUndoRedo` (calling only
`editUndo`/`editRedo`), so the union is exactly these three — but that unit is a
block delimited by reading, not a closure the braces define. It corroborates; it
does not bound. Not every set has a witness as strong as the last one's, and
promoting a block to a closure would be this sequence's own recurring defect.

Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0,
ruff (the CI command, from `services/api`) 0, and `test_file_sizes.py`,
`test_claude_md_gates.py`, `test_roadmap_status.py` all 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
PR #413 was squash-merged, so origin/main carries (96) as ffb7b9f while this
branch still held the pre-squash commits. The content diff between the remote
branch and origin/main was empty before this merge, so the branch held no
unmerged work; -s ours keeps this branch's tree (origin/main plus (97)) and
reconciles the histories without rewriting the remote branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
…flagged one

CodeRabbit flagged `editUndo`'s docstring on #414: it says "restore the prior
model version + republish" unconditionally, but `publish` defaults to true and
can be false. Verified against the backend — `_restore_version` guards the
republish with `if publish:` — so the claim is wrong whenever a caller passes
`publish: false`.

Grepping every site of that wording found the same unconditional claim in THREE
live places, not one:

- `apps/web/src/api/authoring.ts` — `editUndo` (the flagged one)
- `services/api/src/aec_api/routers/authoring.py` — the `/edit/undo` route
- `services/api/src/aec_api/routers/authoring.py` — the `/edit/redo` route

All three now say republishing happens only when `publish`. The remaining hits
are UI button labels, where the call site never passes the flag so republish
really is unconditional, and historical CHANGELOG entries; both are correct as
written and are left alone.

Also fixes the same defect's other half in the same five lines: the declared
return type omitted `publish`, which the backend adds as `"running"` when it
republishes. Correcting the prose while leaving the type silent about the same
conditional would be the half-applied fix this sequence has twice been caught
making.

Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0,
ruff (the CI command, from `services/api`) 0, `test_file_sizes.py` 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
…module but not the codebase

Moves `elementDetailing`, `classify`, `applyDetailingRules`, `validateDetailing`
and `attachDocument` out of `client.ts` into a new `api/detailing.ts`.
`client.ts` goes 687 -> 665, 65 methods above the STAYING banner.

WHAT THEY ANSWER: what informational carriers are attached to this element,
write them, and which are missing?

THE WITNESS IS A 1:1 AND TOTAL FIELD-TO-WRITER MAP, the shape (96) established.
`element_detailing` walks `HasAssociations` and branches on exactly two
relationship types, and `detailing.py` holds exactly two writers, one per
response array:

  classifications[]  <-  classify        (IfcRelAssociatesClassification)
  documents[]        <-  attachDocument  (IfcRelAssociatesDocument)

The map is read out of the reader's own body, not matched on names. The other
two methods are those same writes automated and audited: `applyDetailingRules`
runs the condition-to-content rule set and writes both carrier kinds,
`validateDetailing` reports elements a rule applies to that lack the code.

TOTAL OVER THE MODULE, NOT THE CODEBASE, and the difference is the claim.
`attachOmDocument` — moved to `model.ts` in (96) — wraps the SAME
`detailing.attach_document` and also lands in `documents[]`, so "these are all
the writers of this reader's fields" is false. That overlap was recorded when it
moved, which is why it was on hand to qualify this slice rather than being found
in review.

ADJACENCY AGREED WITH THE ANSWER AND IS NOT EVIDENCE FOR IT. These five were
contiguous in `client.ts`; unlike (95), where non-contiguity was the whole
argument, a positional split would have found this set too. Stated precisely
because it looks like support.

ALSO RECORDED: `api.classify()` has no call site — `detailingSection.ts` drives
the recipe through the generic `authorAndReload` path, bypassing the typed
method, and `clientCallers.test.ts` counts it reached because it matches bare
string literals as well as calls. That looseness is deliberate per that file's
own docstring, so it is a limit of the gate rather than a defect in it.

The new mixin needs `editIfc`, so it declares `NeedsEditIfc` and composes
outside `withAuthoring`; `compositionOrder.test.ts` gains a fourth assertion.
MUTATION-CHECKED: relaxing the constraint to `Ctor<any>` produces "TS2578:
Unused '@ts-expect-error' directive" on exactly the new line, so it fails for
the reason claimed rather than merely passing.

The extraction removed 27 lines and the banner plus import added 5 back — caught
by the ratchet, not by me, the same way (94) found its own banner growth.

Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0,
ruff (the CI command, from `services/api`) 0, and `test_file_sizes.py`,
`test_claude_md_gates.py`, `test_roadmap_status.py` all 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
PR #414 was squash-merged, so origin/main carries (97) as 3faae36 while this
branch still held the pre-squash commits. The content diff between the remote
branch and origin/main was empty before this merge, so the branch held no
unmerged work; -s ours keeps this branch's tree (origin/main plus (98)) and
reconciles the histories without rewriting the remote branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
…s wrong until now

Moves `contentCatalog`, `placeContent` and `importContent` out of `client.ts`
into `api/authoring.ts`. `client.ts` goes 665 -> 648, 62 methods above the
STAYING banner.

WHAT THEY ANSWER: what pre-made content can I place, and place it?

THE WITNESS IS A ROLE-FOR-ROLE PARALLEL with the family shelf already in that
file, read off the signatures rather than the shared noun:

  catalog reader    familyCatalog()  {count, categories: Record<..>}
                    contentCatalog() {count, note, groups: Record<..>}
  placer            placeFamily(pid, family, position)
                    placeContent(pid, category, point, name)
  multipart import  async importFamilies(pid, file, ..)
                    async importContent(pid, file, opts)

Three roles, three methods each, matching shapes and arities. A parallel between
two method TRIPLES is structural; "both are shelves" would have been a shared
word, which is the grouping (88) and (89) each had to reject.

THE DESTINATION'S OWN FIRST LINE WAS WRONG UNTIL THIS COMMIT. `authoring.ts` has
described itself as holding "the family/content shelf" while containing ZERO
content methods — the word's only other appearances there are an HTTP header and
a sentence about IFC *type* content. The docstring stated an intended scope as
fact.

That is recorded as corroboration that was FALSE, not as evidence. A header
agreeing with the answer is worth nothing until someone checks whether it is
true, and this one had been wrong for as long as it had existed. It is the
smallest possible instance of the drift the project instructions keep warning
about.

Verified: tsc 0, lint 0, `vitest run src/api` 27 files / 119 tests, build 0,
ruff (the CI command, from `services/api`) 0, and `test_file_sizes.py`,
`test_claude_md_gates.py`, `test_roadmap_status.py` all 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
PR #431 was squash-merged, so origin/main carries (98) as ae4bc0e while this
branch still held the pre-squash commits. The content diff between the remote
branch and origin/main was empty before this merge, so the branch held no
unmerged work; -s ours keeps this branch's tree (origin/main plus (99)) and
reconciles the histories without rewriting the remote branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 7a1d3079-1967-483d-823e-41861159ab7d

📥 Commits

Reviewing files that changed from the base of the PR and between ae4bc0e and 17103db.

📒 Files selected for processing (6)
  • CHANGELOG.md
  • apps/web/src/api/authoring.ts
  • apps/web/src/api/client.ts
  • apps/web/src/api/surface.test.ts
  • docs/roadmap.md
  • services/api/test_file_sizes.py

📝 Walkthrough

Walkthrough

This change moves three content authoring methods from ApiClient to withAuthoring, preserves their behavior, updates API surface checks, and records the extraction in project tracking and file-size limits.

Changes

Content shelf extraction

Layer / File(s) Summary
Authoring client methods
apps/web/src/api/authoring.ts
Adds contentCatalog, placeContent, and importContent with catalog retrieval, placement, multipart upload, query options, error handling, and typed responses.
API surface and tracking updates
apps/web/src/api/client.ts, apps/web/src/api/surface.test.ts, services/api/test_file_sizes.py, CHANGELOG.md, docs/roadmap.md
Removes the methods from ApiClient, adds them to the API surface checks, lowers the client file-size limit, and records SCALE-SEAM (99).

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: cursoragent

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/codebase-audit-roadmap-45656d

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.

ibuilder commented Sep 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review


Generated by Claude Code

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Pull request metadata =='
gh pr view 432 --repo ibuilder/massing --json number,title,baseRefName,headRefName,changedFiles,additions,deletions,files

echo '== Changed-file diff summary =='
git diff --stat origin/main...HEAD
git diff --find-renames --unified=80 origin/main...HEAD -- \
  apps/web/src/api/client.ts \
  apps/web/src/api/authoring.ts \
  apps/web/src/api \
  CHANGELOG.md \
  docs/roadmap.md

echo '== API module outlines =='
ast-grep outline apps/web/src/api/client.ts --items all
ast-grep outline apps/web/src/api/authoring.ts --items all

echo '== Content API definitions and references =='
rg -n -C 4 --glob '!node_modules/**' \
  '\b(contentCatalog|placeContent|importContent)\b' \
  apps/web/src

echo '== API module import graph =='
rg -n -C 3 --glob '!node_modules/**' \
  'from\s+[\"\x27].*(client|authoring)[\"\x27]|import\s*\(' \
  apps/web/src

Length of output: 236715


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '== Authoring mixin: content and family methods =='
sed -n '250,445p' apps/web/src/api/authoring.ts

echo '== API tests related to the authoring mixin =='
fd -a '^authoring.*\.test\.ts$|^surface\.test\.ts$|^clientCallers\.test\.ts$' apps/web/src/api \
  -x sh -c 'echo "---- $1"; sed -n "1,340p" "$1"' sh {}

echo '== Changed file-size guard =='
git diff --unified=30 origin/main...HEAD -- services/api/test_file_sizes.py
sed -n '1,260p' services/api/test_file_sizes.py

echo '== Diff integrity and current changed lines =='
git diff --check origin/main...HEAD
git diff --numstat origin/main...HEAD
git status --short

echo '== Pull request checks and review state =='
gh pr view 432 --repo ibuilder/massing --json statusCheckRollup,reviewDecision,isDraft,mergeStateStatus

Length of output: 183442


@ibuilder: Review complete. I found no actionable issues.

The three methods retain their signatures and request behavior. ApiClient still exposes them through withAuthoring(HttpCore). The existing content-library UI still calls all three methods. The surface test now protects the moved methods, and the client.ts size limit matches the new 648-line count.

git diff --check reported no whitespace errors. The required CI checks were still in progress when I inspected the pull request.

You are interacting with an AI system.

@ibuilder
ibuilder marked this pull request as ready for review September 4, 2026 07:18
@strix-security

strix-security Bot commented Sep 4, 2026

Copy link
Copy Markdown

Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here.

So far, Strix has reviewed 28 pull requests, surfaced 3 security issues (1 critical/high) and blocked 2 risky merges across this workspace.

@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@ibuilder
ibuilder merged commit a58f047 into main Sep 4, 2026
11 of 12 checks passed
ibuilder pushed a commit that referenced this pull request Sep 4, 2026
PR #432 was squash-merged, so origin/main carries (99) as a58f047 while this
branch still held the pre-squash commits. The content diff between the remote
branch and origin/main was empty before this merge, so the branch held no
unmerged work; -s ours keeps this branch's tree (origin/main plus (100)) and
reconciles the histories without rewriting the remote branch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tt2XKB83wwNt2nrMbK6eEA
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