Skip to content

An unplaced column and a doubled address - #140

Merged
HynLcc merged 4 commits into
engine/v1-reference-columnfrom
case/legacy-view-state
Sep 3, 2026
Merged

An unplaced column and a doubled address#140
HynLcc merged 4 commits into
engine/v1-reference-columnfrom
case/legacy-view-state

Conversation

@HynLcc

@HynLcc HynLcc commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Two cases, both of the same kind: something stored correctly, read back wrong.

case issue settles what it guards
view/a-column-the-view-does-not-place T6545 fd32044e4 a column whose stored notes give it no place still gets one
base-share/a-shared-forms-picture T6604 573e0b70e a shared form's picture has one address, not two

Comparison

Run 33742410017.
Columns oldest to newest.

case 66919acae 08-04 d961a7a03 08-07 develop
T6545 unplaced column
T6604 doubled address
T6597 both notes (already merged)

T6597 is in the run as a control, not as a new case: this PR changes the runner it
shares with T6545, and it still discriminates exactly where it did before.

The evidence

T6545, on 66919acae — an entry with no position fails the check on the way
out, and the request for the table's views fails with it:

500 {"message":"Invalid View projection","domainCode":"view.invalid_projection",
     "issues":[{"code":"invalid_union","errors":[[{"expected":"number", …}]]}]}

T6604, on d961a7a03 — one address with another on the front of it:

http://127.0.0.1:PORT/api/attachments/read/public/http:/127.0.0.1:PORT/api/
attachments/read/public/form/e2e-lab-cover-image

Two things worth reading before the next change here

T6604 first counted http:// and passed on both sides. Joining one address
onto another leaves the inner one with a single slash, so that count found one
address in a string plainly holding two. It counts the scheme now. The wrong
reading was invisible until the value itself was printed — the case was green on
a pre-fix commit and looked correct.

T6545's doc first said it "comes back looking fine and is wrong". The run said
otherwise: it fails the same way its sibling does, with the view list refusing.
The doc now says what was measured, and the shared checkpoint name lost its
reference to the other shape's keys.

Notes on the shapes

  • Both stored shapes go in with SQL, because nothing writes either any more —
    which is also why a base carrying one cannot get out of it from the interface.
    Each fixture reads the stored value back first and requires it to be the shape
    the case is about.
  • T6545 asserts the width as well as the position: filling the gap by
    replacing the entry would satisfy "it has a position" while throwing away the
    only thing the old notes actually said. The position is compared against the
    column's own index rather than a number written into the case, so nothing here
    encodes a particular default.
  • T6604 counts addresses rather than comparing against an expected string —
    what the storage prefix is depends on how the instance is deployed, and pinning
    it would make the case about configuration. The stored value must be a short
    path and the runner refuses an address, since an address is exactly what the fix
    passes through untouched. The control is the same form read from inside the
    product, where it goes through one layer and comes back right.

What the v1 column said

T6545 and T6597 are both red for v1 on every column, develop included. v1
does not fail the request — it answers 200 and hands the entry back exactly as
stored, unplaced or double-noted. So on the older engine this data never caused an
outage and was never settled either. The two engines fail these cases in opposite
directions.

T6604's v1 column is green throughout: the older share path does not double
the address.

Also in this PR

Four triage-ledger rows for candidates examined and put down: 7b969558a/T6890
and 1ea5d6c40/T6669 change cost rather than answers, and the two carried over
from the round before.

Six further candidates were checked and turned out to be already covered by
existing cases — T6942, T6905, T6874, T6856, T6867, T6941. Nothing to record for
those; the registry cross-check did its job.

🤖 Generated with Claude Code

HynLcc and others added 4 commits September 3, 2026 17:48
T6545: the notes a view keeps about each column say where it sits and how wide it
is, and views made long enough ago have entries with a width and no position at
all - a shape nothing writes any more. Nothing filled the gap in on the way out,
and what a view says about a column is checked there, so the request for the
table's views failed: every view at once, not one column in one of them.

Reproduced on 66919acae with Invalid View projection and expected "number".

Second shape on the runner the T6597 case introduced, differing only in the
`legacy` config value: same fixture, same observation, two shapes of old data. The
two were fixed three days apart, this one first, and what differs is which part of
the entry the check rejects - an unrecognised key there, a missing number here.

The doc first said this one "comes back looking fine and is wrong". The run said
otherwise and the doc now says what was measured. The checkpoint name lost its
reference to the other shape's keys for the same reason.

The width is asserted as well as the position: filling the gap by replacing the
entry would satisfy "it has a position" while throwing away the only thing the old
notes actually said. The position is compared against the column's own index
rather than a number written into the case, so nothing here encodes a particular
default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T6604: where a form's picture lives is stored as a short path, and the address a
browser can fetch is worked out from it when the form is read. A shared form is
read through two layers and both worked it out, the second over the first's
answer, so what came back was one address with another on the front of it. The
person who opens the link sees a broken picture while the same form inside the
product looks right - inside, it is read through one layer only.

Measured on d961a7a03:

  http://127.0.0.1:PORT/api/attachments/read/public/http:/127.0.0.1:PORT/api/
  attachments/read/public/form/e2e-lab-cover-image

The first version of this case counted "http://" and passed on both sides. Joining
one address onto another leaves the inner one with a SINGLE slash, so that count
found one address in a string plainly holding two. It counts the scheme now, and
the doc says why - the wrong reading was invisible until the value itself was
printed.

Addresses are counted rather than compared against an expected string: what the
storage prefix is depends on how the instance is deployed, and pinning it would
make this case about configuration. Ending at the stored path is what says the
address still points at the right thing.

The stored value has to be a short path and the runner refuses an address - an
address is exactly what the fix passes through untouched. Before the checkpoint
the form is read from inside the product as the control, where the same view is
read through one layer and comes back right.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
T6890 replaces a leading (expr IS NULL) sort key with Postgres's own NULLS
FIRST/LAST; the commit and the issue both say the order stays identical to v1's,
and what changes is the length of the sort list and whether an index can serve it.

T6669 stops producing a search hit index by re-running the whole v1 pipeline for
ids it then throws away; the index itself is the same on both sides.

Neither has anything for a case to tell apart.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same reading as the sibling case: v1 answers 200 and hands the entry back exactly
as stored, so on the older engine this data never caused an outage and was never
filled in either. Red on every column of the acceptance matrix, develop included.
Reported, not enforced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@HynLcc
HynLcc merged commit 0838494 into engine/v1-reference-column Sep 3, 2026
2 checks passed
@HynLcc
HynLcc deleted the case/legacy-view-state branch September 3, 2026 10:10
HynLcc added a commit that referenced this pull request Sep 3, 2026
* Count a day number from the day the week starts

T1972. The instruction that weeks start on Monday was ignored and every
day came back numbered from Sunday, so anything grouped or sorted by
the column is off by one day - plausible, self-consistent numbers.

* Answer how long ago in the unit it was asked for

T1970. The unit named in the formula was ignored and every answer came
back in seconds, so a person reads a six-figure number where they
expected a small one. Hours are checked against days, which holds
whatever today's date is.

* Put a deleted row in the trash

T1980. The trash is the promise that a delete is not final; rows were
not being written to it, so there is nothing to notice until someone
goes looking and the row is gone for good.

* Settle the deleted-row trash rejection

T1980. Written and run, green on both columns (run 32914315455). The
shape is gone; the runner is not kept.

* Compare the minute in a filter on a time of day

T1611. The time was thrown away and only the day compared, so
everything on that day landed on the same side of the line - and the
rows that come back look right, because they are all from the day that
was asked about.

* Prove the filter is asked the way the product expects it

Develop answered the real filter with no rows at all (run 32915607313)
while the parent answered with everything - an empty answer needs a
control before it can be read as the product's. The same filter with a
cutoff before every row now has to return every row.

* Set the filter in the toolbar and look at the table

Passing the filter alongside the read is answered with nothing on
develop and with everything on the parent (runs 32915607313,
32916759955) - neither is the product filtering. The case now saves the
filter on the view and reads the view, which is how a person uses one.

* Settle the time-of-day filter rejection

T1611. Two shapes, three runs, and the control never passed: the case
never established the product's own way of asking, so nothing about the
product was measured. The shape is gone; the runner is not kept.

* Ask every case of v1 too, and let v1 answer for nothing

v2 is what this lab guards: fixes land there, and a bug returning there is a
regression someone must act on. v1 is asked the same cases as a reference —
what does the engine our older customers are still on do with this — and
nothing it reports fails a run.

Reaching v1 takes more than the environment switch, which is the part that
looks done and is not. Routing asks FORCE_V2_ALL first and the base's own v2
flag second, and the product stamps every base it creates as v2, so turning the
switch off falls through to the second rule and still answers v2. Measured over
129 cases: with the switch off, not one observation differed from the v2
baseline, and the response header said why — reason `new_base` instead of
`env_force_v2_all`. So the base is unstamped before the runner touches it.

What that cannot do is make a base that was BORN on v1, which is what real v1
customers have. That gap is why v1 gates nothing, and why the column is worth
having anyway: it is evidence to follow up, not a verdict.

The engine is read live rather than captured at import — one process runs both
blocks, and a constant would pin the second to the first engine and report its
answers under the other's name. The v2 routing assertion is unchanged and still
absolute; the v1 branch is its mirror, not a relaxation, because a v1 run
answered by v2 is a fabricated column and throws just as hard.

* Declare the eleven cases v1 cannot be asked

Established by re-asking each v1 failure on a clean v1 base without the case's
own fixture — the only method that separates a real v1 defect from a fixture
v1 could never hold. Of 34 cases that answered differently on v1, these eleven
were not answering the question the case asks.

Two different things, which is why the field is a sentence and not a boolean.
Six are features v1 does not have: required links, undo capture columns, field
validation (an EMPTY table refuses the same column, so T5685 was never the
ordering bug). Five are fixtures v1 cannot hold: v1's own API normalizes a bare
user id into a full object, writes complete view column metadata, names the
field on an ordinary unique violation, clears a multi-select cell without
complaint, and clears its inbound link cells on a plain delete.

Declared, never inferred. Reading "v1 said it does not support that" out of an
error message fails open — a case that genuinely breaks, whose error happens to
read that way, would be skipped forever and nobody would learn.

* Give the v1 answers a table of their own

Under the guarded one, judged by nobody. Interleaving the engines would double
the width of the thing people actually read and put cells that fail the run
next to cells that cannot, which is the confusion this column has to avoid to
stay welcome.

The fail-closed contract still covers v2 exactly as before: every planned
(case x commit) cell, one payload, a hole is a failure. The same problems on
the v1 side collect in `referenceIssues`, which is printed and fails nothing —
a reference nobody can afford to leave red is a reference nobody keeps.

Two quieter things this had to fix. The engine goes in the payload FILE NAME,
not only the payload: two engines writing one stem would leave one silently
overwriting the other, and the report would then read a collision as a missing
cell. And the Regression Track keeps taking v2 only — its Run Key is (run,
attempt, case, commit) with no engine, so both engines would land on one row;
widening the key would change what every historical row means, and the v1
column does not want a queryable history.

Both engines run in one job. perf-lab gives each its own because overlapping
measurements pollute each other; bug observations do not, and a second job
would re-pay the whole bootstrap to save one app boot.

* Stop a background worker's leftovers from deciding a run

Import cases hand work to a queue, assert, and then tear down the space the
queue is still writing to. The worker's completion handler lands afterwards,
updates a table that is gone, and rejects with nobody to catch it — and vitest
fails the file on that.

Run 33055688034 is the whole argument: 247 tests passed, 11 skipped, none
failed, every payload written, the report job's acceptance gate green, and the
execute job red anyway on one such rejection from a v1 import case. The v1 pass
did not create the race, it doubled the chances of losing it.

Ignoring them costs no signal. A case's evidence only ever arrives through
bugCheckpoint() and is written to its payload before anything may throw, and
the payloads are what the report job judges. Vitest still prints them — the
same bargain the v1 column takes: visible, gating nothing.

* Give each engine its own job, its own database

Sharing a job saved one bootstrap and was wrong in a way that only shows up on
reflection: two passes against ONE database means the second engine runs on
state the first one left, and the guarded column is what would have been
reading it. Run 33056650739 came out clean anyway, which is exactly why this
would have gone unnoticed.

Split, each engine gets its own containers and its own database built from the
commit's own migrations, and the two jobs run at the same time — the wall clock
is one engine's, not two. It is also what teable-perf-lab has done all along,
for the same reason plus its own: it restores a seed dump per engine so both
start from byte-identical state.

The execute matrix becomes commit x engine; the comparison keeps reading
columns, not jobs, so the plan now hands it `commitPlan` — each commit once.
Gating stays a property of the commit (which revision is the newest), and
whether a reproduction can fail anything stays a property of the engine.

While writing this down: perf-lab never needed the base unstamped because its
cases run on the base the prisma e2e seed writes straight into the database,
which never went through the product's create-base path and so carries
v2_enabled = false. Every case here builds its own base through the API — the
thing that keeps cases from disturbing each other — and the API stamps it.

* Stop asking v1 the eight questions this method cannot put to it

These cases have the product create a SECOND base mid-run — an import, a
duplicate, a share saved into a new one. That base is stamped v2 on creation
and cannot be unstamped before its tables are built, so half the case talked to
v1 and half to v2, and a mixture is not evidence. It showed: four of the eight
were red or 💥 on v1, and one flipped between two runs that agreed on
everything else.

Unstamping the whole space before each observation would reach them and would
be worse. That base's tables were built by v2; unstamping only makes v1 read a
structure v2 wrote, which is the exact false premise that got T6626 and T6595
thrown out this week — and it would be automatic and every run.

The reason string says "this method cannot ask v1", not "v1 lacks the feature",
because they are different claims and only the first is true here. The day a
base can be created on the engine that asked for it, these eight lines come
out.

v1 now answers for 110 of 129 cases. The card gained one line for it — the card
is what people read, so a reference column absent from it is a column nobody
sees — and that line says it decides nothing.

* Ask the field endpoint what the field editor already asks (#133)

* Ask the field endpoint what the field editor already asks

Four cases from the last week of teable-ee fixes, all reachable through the
public API and all reproduced on their own fix's parent.

Two of them are the same missing check on two column types: a totalling column
created through the API asking for something its source cannot give - the sum
of a tickbox, a count over a button - was accepted and written, leaving a
column that read 0.00 on every row and whose settings opened with no source and
nothing selectable. The editor never offers those combinations; the endpoint
took them anyway. They stay two cases because they were fixed two days apart in
two places, and each is green on the other's parent.

The third: a filter box produces text, and the row-number column was the one
numeric column whose comparison refused it, answering 500 to a view a person
had just built and saved.

The fourth: a conditional column reading a table in another base dropped which
base, so reopening its settings drew an accessible table as one the person has
no permission to see.

Also three rows in the triage ledger for what was read and not taken: an
unshipped share-view scope bypass, a connection timeout no request can provoke,
and a fix whose pre-fix state is "v1 answered", which this harness reads as the
case being unable to run rather than as the bug.

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

* Say in the doc that v1 never had the row-number filter problem

The reference column answered correctly on every commit in the matrix, which
is worth recording: customers still on the older engine never saw this one.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Copy a table, copy a base, and total what you are linked to (#134)

* Do not publish a table because someone copied one

T6790: duplicating a table carried each view's sharing into the copy - the
switch, the rules, and the password - and only minted a new address. The copy
was a live public page from the moment it existed, openable by anyone who had
ever been given the source's password, with nothing in the interface saying so.

On the existing duplicate-shared-view runner, behind a new `assert` value,
because it is the same setup and the same request as T6573 and differs only in
what it reads off the copy - which is the point: this answer replaced that one.

The checkpoint reads three things off every copied view, since the switch, the
address and the rules are three separate ways a copy can be reachable, and then
checks the source kept its own link, so "not shared" cannot have been reached by
unsharing everything. A password is set on the source first; an inherited
address is a page nobody opened, an inherited password is a page other people
can already open.

Reproduced on the fix's parent 9c97d777c: the copy came back
enableShare true, with the source's password verbatim.

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

* Keep an any-of-these total inside the row it belongs to

T7004: a total over linked rows narrowed to "status is todo OR status is
doing". Written with OR, the condition escaped the link - the query stopped
asking "and linked to this row" and totalled every matching row in the other
table.

The number it produced was a real sum of real rows, in the right units and the
right order of magnitude, so nothing looked wrong. What the report leads with
is the other half: a project joined to nothing at all that already shows a
figure.

The checkpoint reads both rows. The unlinked one must total nothing, which is
what a person notices; the linked one must total exactly its own selected work,
which is what says the condition is still applied - a build that fixed the
scope by dropping the condition would pass the first test alone.

The other table carries three kinds of row and the runner refuses a fixture
missing any of them: linked and selected, linked and excluded, and selected but
somebody else's. Without the third, a total that ignored the link entirely
would still read correctly.

Reproduced on 9c97d777c, which is before this fix: the project joined to
nothing totalled 520, every todo and doing row in the table.

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

* Copy a base whose tables were named before the naming changed

T6990: Postgres constraint names are unique per table, not per schema, and old
bases carry a self-referencing key called fk___id on every table. Duplicating a
base drops those keys, copies the rows and puts them back - but the step that
listed them matched on the name and the schema and not on the owning table, so
each table's list came back holding the other's rows. The drop ran twice for one
table, the second found nothing, and the copy died there.

Reported from production as an unhandled rejection in the browser, with the base
half-made and nothing a person could change to get past it.

The keys go in with SQL because nothing produces that name any more - it is what
an old base has been carrying since before the convention changed, which is the
same reason nobody hitting this could get out of it from the interface. The
fixture counts how many tables carry the name before the checkpoint: with only
one there is nothing to collide.

The duplicate goes through raw axios with the status open. The generated client
throws a bare "Internal Server Error" and drops the body, and the body is the
part worth reading - a 500 that turned out to be something else would make this
case red for the wrong reason. On 168f206df it reads:

  Raw query failed. Code: `42704`. Message: `ERROR: constraint "fk___id" of
  relation "tblivzjntazg6wE9b1W" does not exist`

which is the Sentry error the issue was filed on.

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

* Record that v1 carried the copied share state too

The acceptance matrix answers red on both pre-fix columns for the v1 reference,
which matches the issue's reading of the legacy duplicate path and contradicts
what a local run of the same commit said. Both are written down; which of the
two the harness should be trusted on is a separate question.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Three summaries over linked rows that gave the wrong answer (#135)

* Ask for the largest of a borrowed list and get a number

T7099: a column only becomes a list by borrowing across a one-to-many, and a
conditional total over such a column had been taught to look inside the list for
sum and average but not for largest, smallest, all-of and any-of. Those four went
straight at the stored list, which the database refuses - there is no largest of
a list - so the column could not be made at all. Sum over the same source builds
fine, which makes it look like something about the field rather than about the
function.

Asking for the total sits inside the checkpoint, because asking is what fails.
Kept outside, the same refusal scores as "this case could not run here" rather
than as the bug - which is what the first version of this runner did.

Two things the case learned the hard way, both now written into it:

The expected answers come from the list the product actually built, read back
off the middle row, not from the rows this case seeded. Asserting against the
seed is asserting against the case's own model of the product.

Only the number half is covered. An unticked box does not reach a borrowed list
at all - a pair of leaves, one ticked and one not, produces [true] - so all-of
and any-of answer the same whether they work or not, and a case built on them
would be green on every column. The runner takes only max and min.

Reproduced on 46b7ae3cb: "Failed to backfill computed fields
[max_of_the_borrowed_list]: function max(jsonb) does not exist", which is the
commit message's own words.

Also two triage-ledger rows: T7019, an incident about contention between
replicas that one application against one database cannot reproduce, and T6988,
whose fix's own e2e needs a stubbed snapshot loader and a hand-set doc version
that this harness's over-the-wire subscription does not have.

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

* Count what the bracket in the condition says

T7080: "orders for this customer that are either unpaid or flagged" is one
condition with a bracket in it, and the interface builds it as a group inside a
group. The fast path answering this kind of column read the outer conditions and
dropped the bracket, so it counted every row matching the customer.

The count is wrong upwards and looks ordinary - real rows, right customer, wrong
ones. Nothing marks the column, and reopening it still shows the condition in
full, bracket included, so there is nothing to see.

A second column runs beside it with the same reference match and no bracket. It
goes through the same fast path and is correct on both sides of the fix, so the
checkpoint reads it FIRST: if the flat one is wrong too, the reference match
itself is broken and the failure says so instead of blaming the bracket. The
settle loop waits on that control rather than on the column under test - waiting
on the latter would be waiting for the bug to go away.

The runner refuses three fixtures, each for a stated reason: no row the bracket
excludes (a bracket that excludes nothing counts the same either way), no host
counting anything (a column stuck on zero passes for the wrong reason), and no
host whose reference matches nothing (that row is what says the reference match
still applies).

Reproduced on c3d0fb6ac, with the control staying correct throughout:

  host-a withBracket 2, expected 1, rowsMatchingTheReferenceAlone 2
  host-b withBracket 2, expected 1, rowsMatchingTheReferenceAlone 2

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

* Give back the distinct choices in the order the rows are in

T7044: two wrong answers from one summary of a choice column. "Todo" then
"Done" came back as "Done, Todo" - sorted, not in the order of the rows - and
when both children said "Todo", the count of distinct values answered 2, which
is the number of rows.

Neither reads as a fault. A reordered pair of words looks like a choice the
product made, and 2 is a number somebody would act on. What makes it findable is
the other summaries on the same row: join and compact are correct, so the row
shows "Todo, Done" and "Done, Todo" side by side. Those two ride along as the
control, and the checkpoint reads them first - if they disagree with the rows,
the whole summary is broken and this is not the distinct-values bug.

Both phases run even when the first found something, and the failure carries
everything at once. On a pre-fix commit the order is already wrong in phase one,
so a checkpoint that stopped there would never reach the count - which only
becomes wrong once two children agree - and half the report would be asserted
and never demonstrated. Measured on 1c2da4254, both faults in one red:

  at first, the distinct choices come back as ["Done","Todo"], expected
  ["Todo","Done"]; after the edit, the count of distinct choices reads 2,
  expected 1 - which is the number of linked rows, so it is counting rows

with joined "Todo, Done" and compacted ["Todo","Done"] correct throughout.

The runner refuses children whose choices are already in alphabetical order: a
summary that sorted them instead of keeping row order would look correct.

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

* Record that v1 still gives back the sorted answer

The acceptance matrix answers red for the v1 reference on every column,
develop included: the fix is v2-only, so anyone on the older engine sees both
faults today rather than having seen them once. The v1 column is a reference
and never gates a run, so this is reported, not enforced.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* A share link, a joined formula, and two records that share a name (#136)

* Treat two linked records sharing a name as two records

T7082: two records are the same record when they have the same id, not when
they happen to be called the same thing - and nothing stops two rows sharing a
name, because a name is a value someone typed. A summary of the distinct linked
records compared what it displayed rather than what it held, so two different
records both called "Same" collapsed into one and a real linked record left the
answer.

It leaves quietly. The column is not marked, the result is a plausible list of
names, and the only way to notice is to count it against the summary beside it
that keeps everything. Whatever reads the column next is short by one.

The checkpoint asserts no list of names. It asserts that the distinct summary
equals the keep-everything summary, because when every linked record is a
different record those two ARE the same answer. That invariant holds whatever
shape the cells come back in, which is not a hypothetical here: the issue was
fixed twice and the second commit changed exactly that shape.

Measured across all three states:

  d09c75728  keeps three {id,title}  distinct ["Same","Other"]
  692c2b4b5  keeps three {id,title}  distinct ["Same","Same","Other"]
  develop    keeps three titles      distinct the same three titles

The middle row is why both commits are named in sourceCommits: after the first,
the identity is right and the two answers still disagree. A written-in list of
names would have been "correct" there and rewritten by the second commit - a
case following the product rather than holding it in place.

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

* Write down why T7070 cannot be asked here

The fixture builds and the write is refused, but in the wrong phase: the insert
raises it, while the fix repairs the deferred propagate. The commit's own e2e
reaches that second call site by draining an outbox inside the v2 test
container; this harness runs the Nest application, where a small fixture
computes inline and never gets there. Both link directions tried.

The attempt also turned up something that is not T7070 and is not fixed: on
develop, a base with a missing link key column refuses inserts into the table on
the other side outright, before any propagation. Recorded under the table so the
next pass does not rediscover it.

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

* Write down two more that cannot be asked here

T7066 was written and run twice against the fix's parent, green both times. Its
precondition is that the summaries exist before the row does, so the answer is
worked out during the write - built first as one call writing the row and its
links, then as the two writes the report's own steps describe. Neither
reproduced, and the T7044 sibling case is green on that parent too, measured, so
it does not cover this one either. The shape stays behind the runner's
whenTheRowIsWritten and alsoCheckAfterAnEdit config values so a third attempt
does not start from nothing.

T7047 reads through the v2 contract's own list endpoint rather than the public
record API, and most of what it changes is performance-shaped - skipping
count(*), paging by cursor instead of OFFSET - so much of it introduces the path
it repairs.

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

* Let a table whose formula joins people columns take a row

T7024: "everyone involved, listed once, separated by commas" over seven people
columns, written four functions deep. Each layer re-stated the whole of the
layer inside it, so the statement the database was asked to plan grew with every
one and reached megabytes. The row is recomputed inside the write, so nothing
came back: the page spun and the gateway gave up. The table could not accept a
row - not slowly, at all - and all a person could see was a timeout.

Making the formula column is inside the checkpoint. What grew is the statement,
and planning it is what fails, so it fails when the column is made as readily as
when a row is added: on the fix's parent 42c9ba98a the case never reaches the
write, because creating the column already answers

  Unexpected unit of work error: Error: Client has encountered a connection
  error and is not queryable

which is the message from the customer's own backend log. Built with the column
in setup - as this runner first was - that failure scores as "this case could
not run here", the one verdict that hides the bug.

The write carries its own time limit rather than being allowed to hang, for the
same reason: a request that never answers would run out the whole case. The
limit is generous on purpose. This is not a measurement of speed and does not
belong in the performance lab - the difference asserted is between an answer and
no answer.

Seven people columns because that is where the report was filed and because the
statement grew with the count; fewer may plan something large that still
completes, which would be green on both sides.

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

* Tell a share link that its database is away, not that we broke

T6926: a space can be bound to a customer's own database, and that binding can be
switched off - revoked credentials, a retired connection, a migration part way.
The share link, the view and the permission are all still correct; there is
simply nowhere to read from.

What came back was an unhandled 500. Whoever holds a share link is usually
outside the company, with no account, nothing else to look at and nobody to ask.
A 500 tells them the product is broken and there is nothing to do about it. A 503
naming an unavailable database tells them - and anything watching the endpoint -
that the same page will work later.

The checkpoint asserts the status AND the code: a 503 that does not say why is
indistinguishable from any other outage, and being distinguishable is the whole
of the fix. A 200 is called out separately, because a share link answering
normally while its database is away would be worse than the bug.

The binding goes in with SQL - binding a space to another database is not part of
this observation, and a switched-off connection is not something a request can
ask for - and the fixture opens the link and requires a 200 first, or a 503
afterwards could just as well mean the share was never set up.

Reproduced on aa1f9f883: 500, code internal_server_error, carrying the raw
DataDbBindingNotReadyError from resolveSpaceDataDb, which is where the commit
message says it came from.

The separator in the T7024 case is now a plain comma. The customer's was an
ideographic one and this repository is English-only; what grows the statement is
the nesting, not the character, and the case was re-run on the fix's parent to
confirm it still reproduces.

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

* Declare what the v1 column can and cannot say about these two

T6926 is skipped on v1, for a reason about this harness rather than the product:
the case makes its own space and base, and case-base.ts unstamps only the base it
manages, so a base born inside a runner is born on v2. The v1 run answered
"requested of v1 but v2 answered (reason=new_base)" - the harness refusing to
fabricate a reference column. Any runner that creates its own base inherits this.

T7082 is red for v1 on every column, develop included: both fixes are v2-only, so
a summary on the older engine still merges two records sharing a name and still
loses one. Reported, not enforced - the second case here to say that about v1.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* The authority-matrix fixture, and three cases that needed digging (#137)

* Make a column that picks by case and ends in linked records

T6980: "cost depends on where the cost comes from" - a manually entered figure
for some rows, a different figure for others, and otherwise whatever is linked.
The first two answers are numbers; the last is a list of linked records, stored
as a document rather than as a number.

The step merging the branches compared only the ones with a case attached. Those
agreed - both numbers - so it never looked at what the otherwise branch held. The
database was then asked to choose between numbers and a document in one
expression and refused, killing the column and the schema change it was part of.
Nothing in the interface says the last branch is a different kind of thing.

Two number branches, and the runner refuses fewer: their agreeing with each other
is exactly what stopped the merge looking further, and with one branch there is
nothing to agree with. The linked column is checked to hold a list before the
checkpoint - holding one value it would be the same kind of thing as the numbers
and there would be nothing to reconcile. The link is many-to-many because every
row needs the same list, which one-to-many will not allow.

Making the column is inside the checkpoint, since reconciling the branches
happens while it is built. Reproduced on f44a82cf8:

  Failed to backfill computed fields [Cost]: CASE types jsonb and double
  precision cannot be matched

The rows falling to the otherwise branch are read but not pinned to a value: what
a list of linked records renders as has changed before, and pinning it would tie
this case to something it is not about.

Also a ledger row for T7105, which is cost rather than behaviour - a request for
27 columns selected all 235, and answered with the same 27 either way.

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

* Write down two more dead-letter fixes this harness cannot reach

T6711 and T6904 both put their observation inside the background schema-operation
runner: whether a leftover table.import is marked dead or repaired, and whether a
computed task planned against a still-provisioning table is dead-lettered or
retried. Neither difference reaches an HTTP response.

The T7070 attempt already established by measurement that a small fixture here
computes inline and the deferred path is not reached, so these are recorded
against that finding rather than re-derived.

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

* Make a second undo retry the step that failed, not the one before

T7038: undo moved the place it had walked back to BEFORE carrying out the step,
and never moved it back when the step failed. A failed undo therefore still
counted as done, so the next press started from behind it and reversed the step
before - one the person had not asked to reverse. Here that step is the row's
creation, so pressing undo twice deletes a row nobody asked to delete.

The failed undo is not the problem. It is honest and visible. The second press is
the one that quietly takes something else away, and that is what the checkpoint
reads: the row still there, and still holding the value the failed step could not
put back.

Measured, first press then second:

  f44a82cf8   failed "must have a unique value"   fulfilled   one row left
  develop     failed "must have a unique value"   failed, same message   both rows

The first press is identical on both sides. The whole difference is the second.

A step fails to reverse for an ordinary reason: the column does not allow
duplicates, the value was changed away, and another row has taken the old value
since. That row is written on a DIFFERENT window id - the stack is keyed by it,
and writing it on the same one would put it on the history this case walks back
through.

This case covers one of the six risks the report lists. The other five are
concurrency and crash shapes that a single client against one process cannot
show, and the doc says so rather than letting the issue id imply coverage.

Written down in the doc because it cost a run: every write has to carry the
window id, the generated client takes no per-call headers, and an undo with
nothing on the stack answers "empty" - which the first version of the fixture
check read as "not fulfilled" and let through, green on a pre-fix commit against
an empty stack. The writes now go through raw axios and "empty" is rejected by
name.

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

* Write down T7057, and where the permission family actually stands

T7057 changes which fields a substring index covers, not what a search returns:
an all-field search over an uncovered field falls back to the unindexed path and
answers the same. Same reading as T6821.

The other half of this is a note rather than a rejection. Four uncovered fixes -
T6997, T7025, T7027 and T6944 - all wait behind one piece of setup that does not
exist here yet: the authority matrix on, a role that makes a field unreadable,
and a second user holding it. None of them is blocked by the harness; the matrix
is driven through public endpoints and a second signed-in user comes from the
same test utils runners already import. The note records the endpoints and the
worked example so the next attempt does not go looking for them, and says which
of the four is the best first customer.

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

* Build the authority-matrix fixture, and show a grid its reader cannot open

T6944: under the authority matrix a role can withhold one column, and the rest of
the table stays readable - that is the point of withholding a column rather than
the table. But a view remembers what it is grouped by and the page sends that
grouping with every request for rows, so asked to group by a column the reader
may not see, the server refused the request outright. The person got not a view
without its grouping but a view with NO ROWS, and a message naming neither the
column nor the grouping. An administrator opening the same view sees everything,
which is the worst shape a support conversation can have.

Reproduced on 12407c409 with the customer's own words:

  400 {"message":"Group references a field that is not readable",
       "code":"validation_error"}

Most of this commit is framework/authority-matrix.ts, which stands up the three
things together - the matrix enabled on a base, a role that withholds something,
a second signed-in person holding that role. Three more reported bugs need
exactly that, so it lives beside fixture-db rather than inside this runner, and
is setup-only for the same reason: asking for it inside a checkpoint throws. The
URLs are literals, not imports from the enterprise client, because a case runs
against revisions weeks apart and a moved export would break it everywhere
instead of failing honestly on the commit that moved it.

sourceCommits names a4c8c3396 alone. Two commits carry this issue id and the case
is red on BOTH sides of the earlier one - measured on 12407c409 and on 7bc91231d,
which is after it - so f70f0d508 is recorded in the ledger as the half this case
does not settle rather than claimed here.

The control is the same request without the grouping, which must return every
row: it says the person can read this table, so the refusal is about the grouping
and not about them. The fixture also requires the withheld column to be absent
from what comes back, or grouping by it would be an ordinary request.

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

* Attribute the grouped-grid fix to the commit that actually made it

The acceptance matrix caught a wrong claim. The case was written naming
a4c8c3396, one of the two commits carrying T6944's id, and the matrix answered
red on f44a82cf8 - which is after it. Bisected one commit at a time: red on
12407c409, 7bc91231d, f44a82cf8 and 8fd1e28b9; green at 2ae77481c, which carries
T6997's id, "evaluate v2 reads over masked values".

So sourceCommits names 2ae77481c while bug.issue stays T6944, because T6944 is
what a person reported and this is that person's symptom. Both T6944 commits are
now ledger rows for the half this case does not settle, with the measurements
that say so.

The doc records the mistake as well as the correction: anything a case claims
about which commit fixed what has to come from a column, not from an issue id.

Also noted that T6980 is red for v1 on every column including develop - the third
case here to find the older engine still carrying a fix that only landed on v2.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* Record eleven candidates examined and put down (#138)

No cases in this change - only what was learned by failing to write them. Each row
carries the shapes tried and what they measured, so the next pass spends its time
somewhere else.

Structural, and unlikely to change: T6893 and T6694 move handlers onto v2, so
their pre-fix state is "v1 answered", which assertServedByV2 reads as the case
being unable to run; T6895 replaces a timing-out POST with a stream, so no request
both sides answer; T7105 is cost rather than behaviour; T7057 changes index
coverage, not results; T7035 and T7059 are browser fixes.

Inside the background runner, where nothing this harness reads can see the
difference: T6711, T6904 and T6982. T6982 was written and run twice, and turned up
that a table carrying provision_state = 'pending' is not out of reach here - the
assumption both shapes rested on.

Written and run, and still not reproduced: T7061 (twice - whether a computed plan
splits into stages is the planner's decision and a case cannot ask for a split),
T6925 (a third shape, which found that a formula mixing a date branch and a text
branch is accepted, unflagged, and computes nothing on develop), and T6500 (twice
- the field-conversion path T6925 named as untried, now tried).

Two rows note fixes that are ready to write the day they ship: T7027, whose
permission-filtered response still carries ids of what was filtered away, and the
server-side half of 38d0e067e, which closes two comment permission paths that are
currently open. The fixture both need already exists.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* A view that says both things, and two roles that were not listened to (#139)

* Let a role that grants archiving actually grant it

T7025: giving somebody an authority-matrix role also puts them in the base, and
puts them in as a Viewer. A Viewer, by their base role alone, may not archive
anything; the role says they may. Two gates read those two answers and the base
role was checked first, so the answer was always the Viewer's and the role's
grant never got a hearing.

The refusal named neither the role that grants the action nor the thing that
withholds it, while the settings screen showed everything correctly configured -
because it was. Reproduced on 8fd1e28b9:

  403 {"message":"not allowed to operate record|archive on tbl…",
       "code":"restricted_resource"}

The person arrives THROUGH THE ROLE ALONE, with no invitation to the space first.
That is the whole shape - it is what makes them a Viewer. Invited as an editor,
their base role would permit archiving on its own, the gate that reads it first
would answer correctly by accident, and the case would be green on both sides.
That is a new `join` option on framework/authority-matrix.ts: how somebody gets
into a space is not a detail here, it is the bug.

The checkpoint also asks to archive a row the role does NOT reach, and requires a
refusal. That half never goes red - being refused everything is also being
refused this - and it is not there to catch the reported bug. It is there because
the cheapest wrong fix is to stop checking, and that fix passes the first half.

The report describes a grouped and sorted view; the fix says that was incidental
and the fixture leaves it out.

Also a ledger row for T7027: its fix is unshipped and its symptom is a
permission-filtered response still carrying ids of what was filtered away. Same
call as T7065 - the fixture it needs now exists, so it is ready to write the day
it ships.

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

* Write down that T6982's repair is not visible from outside

Written and run twice, green on the fix's parent both times. A real settings
change was made and the job it recorded was rewritten by fixture-db into the
interrupted shape the commit describes - pending, metadata_pending, no
last_error, old enough for the keeper to claim - with table_meta.provision_state
set to pending alongside it.

The first shape asked that the table go out of reach and then come back. The
second dropped that and asked only that it read within two minutes. On both
28a55d9ac and develop the table read immediately either way, so neither shape
tells the two sides apart.

The first shape was also wrong in kind, and the ledger row says so: requiring the
symptom before observing it lets a fixture confirm only itself. The second
verifies what it wrote instead.

Worth knowing on its own: a table carrying provision_state = 'pending' was not
out of reach in this environment, which is the assumption both shapes rested on.
What the fix changes - whether the keeper repairs the job or marks it dead -
lives in the job's own row, and the commit's e2e reads it through Prisma while
driving the runner in process. Same seam as T6711 and T6904.

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

* Write down four more that cannot be asked here, and T7061

T6893 and T6694 move handlers onto v2, so their pre-fix state is "v1 answered",
which assertServedByV2 reads as the case being unable to run rather than as the
bug - same as T7067. T6895 replaces a POST that timed out at the gateway with a
stream, so there is no request both sides answer.

T7061 was written and run twice, green on the fix's parent both times: first with
a conditional lookup and one formula, then with three more formula steps after
it, because the commit says the fault needs a stage that runs the lookup edge
while the plan still has formula steps left over. Whether a plan splits into
stages at all is the planner's decision - d74a81ab1 explicitly keeps small chains
in one stage - and a case cannot ask for a split from outside.

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

* Let a role that permits commenting actually permit it

T7034: the same fault as its sibling, one action over. Giving somebody an
authority-matrix role puts them in the base as a Viewer, and a Viewer by their
base role alone may not comment; the role says they may. Commenting was gated on
the base role alone, so the grant never reached the write.

The person can see the record, open it and read the thread, and cannot add to it
- told only that the resource is restricted, which names nothing they could
change, because from the settings screen nothing is wrong. Reproduced on
855da66cf:

  403 {"message":"not allowed to operate record|comment on tbl…",
       "code":"restricted_resource"}

The checkpoint reads the thread back as well as the status: a write that answered
and left nothing behind is the same silence with a friendlier status. It then
comments on a row the role does NOT reach and requires a refusal - that half
never goes red, and it is there because the same change also had to bound
commenting by the role's row conditions, which the base-role path never applied
at all, so a fix that stopped checking would pass the first half.

Second customer for framework/authority-matrix.ts, and the shape it shares with
record/archive-a-row-your-role-says-you-may is now recorded in both docs: a
matrix grant that never reaches the write because a base role answered first has
been found twice, in archiving and in commenting.

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

* Add a third failed shape to T6925, and what it turned up

The ledger already carried T6925 with two shapes tried and green, and named the
untried path: the computed backfill a field conversion runs, not the pass that
fills a newly created column. This third shape went at the cause the commit
names - an IF() whose branches are a date and a word, so the column is typed as
text and every branch is trimmed - and it does not express the behaviour either.

On develop that column is created without error and computes nothing at all: no
date on the row whose checkbox is ticked, and not even the word on the row whose
checkbox is not. With no correct answer on the fixed side, there is nothing for a
pre-fix column to differ from. The conversion path is still untried.

That last observation is written under the table as its own note. It is not
T6925 and is not claimed as a fault - mixing a date branch with a text branch may
simply not be supported - but a formula that is accepted, is not flagged, and
answers nothing is worth somebody looking at, and the next person to try this
shape will hit it in the first minute.

Two things this cost, both worth remembering: check:source-commits caught that
T6925 was already a ledger row before the case could ship, and an edit to the
runner silently did not apply because prettier had rewrapped the line it matched
on - the run afterwards was against the previous file.

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

* Try the conversion path T6925 named, and write down that it is still not it

T6500 is the field-conversion backfill the T6925 ledger row records as untried,
so this tries it: a number column with rows and a formula reading it, converted to
text inside the checkpoint, then in a second shape converted back again - which is
what the report describes people doing. Green on the fix's parent both times.

Neither direction reproduces "operator does not exist: double precision = text".
Something narrower decides whether the stored column and the freshly computed
value end up different kinds, and a formula that simply echoes the column is not
it. The production reports name six computed fields across two tables in one
base, so the shape may need a chain rather than one formula.

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

* Read a view that says both things about a column

T6597: which columns a view shows has been recorded two ways over this product's
life - an older note saying whether a column is SHOWN, and the current one saying
whether it is HIDDEN. Views made long enough ago carry both, and no request writes
that shape any more; it is simply what is in the table.

Read back, the two were passed through side by side, and what a view says about a
column is checked on the way out. So the request for the table's views failed -
every view at once, not one column in one of them. Reproduced on c9aef116b:

  500 {"message":"Invalid View projection","domainCode":"view.invalid_projection",
       "issues":[{"code":"unrecognized_keys","keys":["visible"]}]}

The checkpoint asks for the views at all, and then that the entry has been settled
into one answer - older note gone, current one kept, order and width intact. The
entry carries those two so a settled entry can be told from an emptied one:
dropping it whole would satisfy "the older note is gone" while losing what the
view needs.

The stored notes go in with SQL because nothing writes that shape any more, which
is also why a base carrying it cannot get out from the interface, and the fixture
reads them back first to require the older key is really there.

The feature name in the engine assertion was a guess and the assertion refused it:
GET /table/{tableId}/view answers as getViews, not createTable. That is the check
doing what it is for - a case pinned to the wrong feature watches code its bug
does not live in.

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

* Record that v1 answers this one the other way round

v1 is red on every column including develop, and not for v2's reason: it does not
fail the request, it answers 200 and hands back the entry exactly as stored, both
notes included. So on the older engine this data never caused an outage and was
never settled either. The two engines fail this case in opposite directions,
which is worth saying before anyone reads the v1 column as "v1 was affected too".

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* An unplaced column and a doubled address (#140)

* Give a column its place when the stored notes give it none

T6545: the notes a view keeps about each column say where it sits and how wide it
is, and views made long enough ago have entries with a width and no position at
all - a shape nothing writes any more. Nothing filled the gap in on the way out,
and what a view says about a column is checked there, so the request for the
table's views failed: every view at once, not one column in one of them.

Reproduced on 66919acae with Invalid View projection and expected "number".

Second shape on the runner the T6597 case introduced, differing only in the
`legacy` config value: same fixture, same observation, two shapes of old data. The
two were fixed three days apart, this one first, and what differs is which part of
the entry the check rejects - an unrecognised key there, a missing number here.

The doc first said this one "comes back looking fine and is wrong". The run said
otherwise and the doc now says what was measured. The checkpoint name lost its
reference to the other shape's keys for the same reason.

The width is asserted as well as the position: filling the gap by replacing the
entry would satisfy "it has a position" while throwing away the only thing the old
notes actually said. The position is compared against the column's own index
rather than a number written into the case, so nothing here encodes a particular
default.

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

* Give a shared form's picture one address instead of two

T6604: where a form's picture lives is stored as a short path, and the address a
browser can fetch is worked out from it when the form is read. A shared form is
read through two layers and both worked it out, the second over the first's
answer, so what came back was one address with another on the front of it. The
person who opens the link sees a broken picture while the same form inside the
product looks right - inside, it is read through one layer only.

Measured on d961a7a03:

  http://127.0.0.1:PORT/api/attachments/read/public/http:/127.0.0.1:PORT/api/
  attachments/read/public/form/e2e-lab-cover-image

The first version of this case counted "http://" and passed on both sides. Joining
one address onto another leaves the inner one with a SINGLE slash, so that count
found one address in a string plainly holding two. It counts the scheme now, and
the doc says why - the wrong reading was invisible until the value itself was
printed.

Addresses are counted rather than compared against an expected string: what the
storage prefix is depends on how the instance is deployed, and pinning it would
make this case about configuration. Ending at the stored path is what says the
address still points at the right thing.

The stored value has to be a short path and the runner refuses an address - an
address is exactly what the fix passes through untouched. Before the checkpoint
the form is read from inside the product as the control, where the same view is
read through one layer and comes back right.

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

* Write down two more that change cost, not answers

T6890 replaces a leading (expr IS NULL) sort key with Postgres's own NULLS
FIRST/LAST; the commit and the issue both say the order stays identical to v1's,
and what changes is the length of the sort list and whether an index can serve it.

T6669 stops producing a search hit index by re-running the whole v1 pipeline for
ids it then throws away; the index itself is the same on both sides.

Neither has anything for a case to tell apart.

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

* Record that v1 leaves this entry unplaced too

Same reading as the sibling case: v1 answers 200 and hands the entry back exactly
as stored, so on the older engine this data never caused an outage and was never
filled in either. Red on every column of the acceptance matrix, develop included.
Reported, not enforced.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 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.

1 participant