Skip to content

feat: the ops domain, the business model, the tracker, and the AI/audio and design branches folded in - #82

Merged
jeremie0342 merged 151 commits into
masterfrom
feat/code-orientations
Aug 21, 2026
Merged

feat: the ops domain, the business model, the tracker, and the AI/audio and design branches folded in#82
jeremie0342 merged 151 commits into
masterfrom
feat/code-orientations

Conversation

@jeremie0342

@jeremie0342 jeremie0342 commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Four backlogs, three parallel branches, one branch.

code, live-feed, business-model and ops were worked through in order.
The AI/audio and design domains were being built at the same time on their own
branches; both are merged here, and where two sessions had answered the same
question differently, the answer that keeps working when a sixth domain
arrives is the one that stayed.

What is in it

The ops domain. Eight trades, five review families, and proof that is not
a pull request: a reusable artefact, an objective held, an incident led, a
bill made smaller with the service still standing. Post-mortems are blameless
as a schema property — there is no column anywhere for who caused an incident
— because a post-mortem that names somebody is one nobody writes honestly the
next time, and it is the next time that matters.

The business model, as tables rather than a deck. Studios and engagements,
sponsorship and events, enterprise contests, the data line and the consent
everything in it rests on, the finance line, certifications, paid mentoring,
consultations, continuous products, and a small CRM over the top. Every
vocabulary is rows, not enums: the twelfth product type is an INSERT.

An applicant tracker. The product registry was already selling
subscription_pipeline — "monthly access to candidate tracking" — with
nothing behind it. It is replaced by the thing it named. Three rules run
through it, each a promise to somebody who is not the customer: every
candidate carries an erasure date swept daily, a refusal carries a reason, and
the rows belong to the company that entered them.

Missions that cannot be published without naming what they cost. A mission
including on-call states the window, the acknowledgement time and a monthly
retainer, or the insert fails. Unpaid availability is the most common way the
ops trade is exploited, and a position that lives only in a document is one
that gets negotiated away by the first client who pushes.

Bugs found by running things

Migrations had never been applied to a database. Running them found five that
could not have worked, none reachable by a test because they fail before any
test has a database — a RAISE with a % and no argument, a view reading a
table renamed out from under it, is_current as a stored generated column
over CURRENT_DATE, and two number collisions between branches.

The integration suite had never run either: all four CI shards were dying
while linking, so 1500 tests had never executed against this branch. 184 test
binaries statically linking the whole dependency graph with full DWARF is more
disk than a GitHub runner has — three shards reported ENOSPC and the fourth a
linker SIGBUS, which is the same full disk seen through an mmap. With
debug = "line-tables-only" they run, and what they found is below.

Found by reading: four platform_revenues inserts omitted a NOT NULL column,
and the Mobile Money provider returned Pending with a synthetic reference
while calling nothing — the one failure mode a payout path must not have. It
now refuses, names the missing variables, and the ledger debit is reversed.

Found by the merge: parse_ebur128 looked for a line starting with
Summary: and ffmpeg prefixes every line its filters emit, so every loudness
figure came back NULL. The audio branch's own test carried the real output
shape and was catching it.

Found by the tests, once they could run:

  • goals validated a capability by pattern-matching the text of a CHECK
    constraint that migration 0404 had dropped when it made capabilities rows.
    The lookup answered false for every capability that exists.
  • The public feed's cursor was <rfc3339>|<uuid>, and an RFC3339 offset
    carries a +, which a query string decodes as a space. The only thing
    anybody does with next_cursor is put it in a URL, so page two answered 400
    to a cursor the server had just issued.
  • credentials::declare read its own insert through a data-modifying CTE.
    Every part of a statement sees one snapshot, so the endpoint 500'd on a
    credential it had in fact written.
  • An incident could be opened with a start date in the future; resolving
    stamps NOW(), which then lands before the start and trips the constraint —
    a 500 at the end of an outage, on the step meant to close it.
  • A NUL byte in any JSON string reached PostgreSQL, which cannot hold one in a
    text column at all, and surfaced as a 500 blaming the server.
  • Seven tables held a domain as a free string with nothing checking it. A typo
    there inserts cleanly and is invisible to every listing that joins the
    catalogue.

What the merges settled

  • Domains, capabilities, attestation bases, contest formats, slice types,
    portfolio platforms and deliverable formats are tables. The ops migrations
    stop restating CHECKs and insert rows; the block that restated forty-one
    capabilities to add six is gone, because the catalogue derives reviewer
    capabilities from the orientations themselves.
  • Three CHECK-to-table conversions had silently dropped another branch's
    values — design's slice type, its two contest formats, and every AI, audio
    and design deliverable format. Each was found by a foreign key at test time,
    which is the point of making them tables.
  • JURIED_KINDS and COMMUNITY_VOTED_KINDS were the same bug one file along.
    Two columns on tournament_kinds, with a CHECK saying a format is not
    judged two ways.
  • One matcher for mentorship, taking a domain — and design was right about
    something the shared one had wrong: a mentor's families come from their
    verified deliverables, not from what they told the wizard interested them.
    That is not design-specific, so it applies to all five domains now.
  • One /api/guides?domain=…. One user_domain_profiles for wizard answers,
    now including design's own two questions, which its wizard was refusing.
    One craft-score loop. One published_artifact_url where four columns were
    about to say the same thing.
  • ui-sound-design was defined by design and again by audio: one skill, two
    rows, and a person tagged with one invisible to a search for the other.

The contract, which was describing a different API

utoipa names an operation after its handler function and a component after the
Rust type, and neither is unique in a codebase this size: 126 handlers
collapsed onto 56 operation ids and 51 structs onto 18 component names, each
collision keeping whichever registered last. POST /api/legal/consent
documented a body from two modules away, and a fuzzer found it by sending the
documented body and being refused.

Two admin paths were documented at addresses nothing routes. Two JSONB arrays
were declared as objects. Two closed vocabularies were declared as free
strings, and one endpoint declared seven maximum lengths and enforced none.

Two unit tests now walk the built document — every operation id unique, every
referenced component defined — and they read all 865 operations, so the walk
cannot pass by finding nothing.

What is deliberately not here

An authenticated import from Datadog, Instana or a private Grafana. Such a key
carries a client's service map, incident history and traffic volumes — the
list the reinforced NDA protects, obtained more quietly. Public status pages
are read with no credential and shown beside the declared figure, never
instead of it; everything behind a login stays declared, sourced and read by a
person. The position is in both ops charters and the legal note, so its
absence is a stated line rather than a gap.

Four endpoints that had never worked

GET /users/{username}/ops-profile answered 500 to every request it had ever
received. Twelve of its thirteen figures are count(*), which PostgreSQL
returns as bigint, and the struct reads all thirteen as i64; the thirteenth
went through date_part and was cast ::INT, making it the one int4 in the
row. sqlx does not widen an integer to fit, so it refused the whole row. Its
four sibling services all cast ::BIGINT — ops was the only one written
otherwise, which is what happens when a domain is built on its own branch.

The suite reported one failure, which read as one awkward test. Exactly one
test reaches that endpoint, and only in passing. So an audit asked how often
that is true, and the answer was 404 of 922 registered routes called by no
test at all.

A smoke test now calls the 118 that take no path parameter, as a stranger, a
member and an admin — three doors, because an endpoint can decode fine on the
path that refuses you and fail on the one that reads rows. It found three more
on its first run, each 500 to every call ever made of it:

  • /api/enterprises/me/agency-clients and /api/enterprises/me/type-config
    order by enterprise_members.created_at, which does not exist. The function
    is a second copy of resolve_active_enterprise that drifted; realigning it
    also restored the status = 'active' filter it had lost, without which a
    pending invitation outranked a membership somebody holds.
  • /api/admin/accounting/export selects enterprises.country, which does not
    exist either. The country belongs on the invoice anyway: VAT follows where
    the customer was when they were billed.

Verification

  • 442 migrations apply in order to an empty database, checked by
    scripts/check-migrations.sh in fifteen seconds against a local PostgreSQL
    18 — no Docker. It exists because a migration is checked by nothing else:
    not cargo check, not clippy, not a unit test. A bad row does not fail one
    test, it fails the chain, and the backend never starts. That happened once
    here and cost a whole CI run.
  • 628 unit tests pass; cargo clippy --all-targets --all-features is clean.
  • The integration suite is green across all eight shards. It went from 174
    failures to zero, and the last rounds were verified locally before pushing
    rather than by waiting on CI.
  • The contract fuzzer went from ten findings to none.

Sharding

Eight shards, not four, and a 45-minute budget rather than 35. At four, three
shards out of four were being killed by the timeout, so a run reported a
quarter of its failures and lost the rest — several rounds here were spent
fixing on a quarter of the evidence. A shard is ~12 min of compilation plus
~16 min of tests and only the second half divides, so eight is ~20 min rather
than half of 28. It finds no additional failures; what changes is that the
results survive to be read.

Numbering

Three sessions each picked "the next free number" and collided twice, the
second time badly enough to need splitting the block — domains_are_rows adds
foreign keys to the recruitment tables while the ops migrations insert into
its catalogues, so business sits below 0400 and ops above 0423. A reserved
band per domain would end this.

One path changes

/api/events moved to /api/tournaments/feed: both events.rs and
tournament.rs had registered it, so the router panicked at startup and the
binary never booted.

The public feed's next_cursor also changes format, though it could not
survive being put in a URL before, which is the only thing anybody does with
it.

… what

Two gaps the code backlog assumed away.

The catalogue holds one name and one description per orientation, in one
language. Twenty-four queries read those columns, so the text stays where
it is and serves the default locale; a translations table carries the
others. A CHECK forbids the default locale there, because holding French
in both places is how the two copies start disagreeing. Not YAML, unlike
notification copy: operators create orientations at runtime through the
admin panel, so the text has to live where they can write it.

is_archived already says an orientation can no longer be chosen while
staying on the profiles that hold it. It does not say where its people
went. Renaming dev-frontend to web-frontend-developer without that link
means a recruiter filtering on the new slug silently misses every profile
carrying the old one — a failure of the one thing this platform sells.
replaced_by records the lineage, and cannot point at a live orientation
or at itself.

Verified against a clone of the real schema: the migration applies, and
each of the three constraints refuses what it is there to refuse.
"Développeur Backend" is not a trade, it is a family of them. Someone who
writes a database engine and someone who wires a REST API both answered to
it, which made the orientation useless for the one thing it exists for:
saying what a person actually does.

Thirty-two new trades, each with a French name and description in the style
of the eight seeded in 0088, an English translation, secondary domains so
the cross-domain graph keeps working, and tags. English is written now
rather than left for later, because an orientation with no English name is
invisible to half the audience and nothing in the product would surface the
omission.

Eight old slugs are archived, each pointing at what it became.
dev-embarque-iot is among them: it arrived in migration 0105, after the
backlog that planned this was written, and firmware-embedded-developer
would otherwise have duplicated it. systems-programmer stays as it is, and
gains the English name it never had.

Verified on a clone of the real schema: 33 active, 8 archived, every
archived one resolving to its successor, no empty description, no active
orientation missing its English.
Migration 0173 named thirty-three trades. The catalogue behind them covered
web, Rust, Python and TypeScript, so roughly twenty had nothing to attach
to. An orientation with an empty skill map looks supported and is not:
someone picks "Développeur Noyau et Pilotes" and the platform has no idea
what that involves, so it recommends nothing and verifies nothing.

151 nodes across twenty-three families — Apple, Android, cross-platform,
desktop, low-code, kernel, robotics, safety-critical, blockchain,
compilers, formal methods, database and search internals, distributed
systems, streaming, scientific computing, GPU, quant, network protocols,
tooling, media, platform apps — plus three that complete enterprise
software next to the SSO and multi-tenancy nodes already seeded.

Each names concrete technologies, because a skill called "Advanced Kernel
Concepts" is a label nobody can tell whether they have. Regulated fields
name their standards rather than paraphrase them.

distributed-tracing is deliberately absent: it already exists under ops,
and the skill map crosses domains on purpose. Duplicating it would give
two answers to whether someone knows tracing.
242 relations across the thirty-three active code orientations. Core is
what the trade cannot exist without — remove it and the person is doing
something else; three or four per orientation, because a trade where
everything is core says nothing about what to learn first.

Rows point at the coarsest node still specific to the trade. A systems
programmer needs Rust, all of it, so the row says rust and the tree
underneath describes the contents. A GPU developer does not need "GPU
compute" — that is the trade's own name — so the rows name cuda-kernels
and gpu-memory-hierarchy instead.

Two rows leave the code domain on purpose: distributed-tracing lives under
ops and edge-ai under ai. The map crosses domains because trades do.

Verified on a clone: every active orientation has skills, every one has at
least one core skill, and none of the 151 nodes added in 0174 is orphaned.
Migration 0172 added translations and nothing read them, which is a table
that looks like a feature. The two public catalogue handlers now join it,
falling back to the base row when no translation matches — so asking for
French finds nothing and gets the base, which is the same answer reached
without a special case.

resolve_from_accept_language existed with no caller. It is the resolver now.

Nine tests covering what a migration gets wrong in silence: a JOIN dropping
rows on a mistyped slug, an orientation shipping blank, a translation
nobody reads, an archived trade whose people become unreachable, a live one
claiming a successor, and a language we do not have blanking the page
instead of falling back.
Thirty-three code orientations would mean thirty-three capabilities and an
operator granting them one at a time. Nobody reviews at that granularity
either: someone who can judge a React component can judge a Svelte one and
cannot judge a CUDA kernel. Eight families plus a wildcard is the shape of
the actual competence.

The family lives on the orientation row, not in a match in the binary,
because orientations are created at runtime through the admin panel — a
compiled mapping would leave every new one unreviewable until a deploy. The
capability is derived as {primary_domain}_reviewer:{group}, so the ops and
design backlogs get the same mechanism instead of inventing a second one.

Caught while writing this: the dev database sat at migration 115 of 176, so
the CHECK it held was the one from 0098. Rebuilding the constraint from
what was in front of me would have dropped verified_apprentice,
apprentice_verifier and the seven challenge_validator values that 0117 and
0120 add — silently disabling the validator workflow, since a capability
absent from the CHECK can never be granted and its guard then refuses
everyone. The constraint now restates every value, and a test asserts the
older ones are still grantable.

Verified by building the schema from all 176 migrations against an empty
database rather than cloning the stale dev one.
…m unreachable

Nine are derived from proofs — verified deliverables, slices merged
upstream, distinct languages. Six are judgements: shipping an audited
contract to mainnet is not a row count, and a rule invented to approximate
it would award it to the wrong people. Those are marked manual, the engine
never touches them, and a grant carries its author and its reason.

Three bugs found writing them, all in code that shipped:

The proof engine could award exactly one badge per user, ever. user_badges
was keyed on (user_id, badge_id) and every derived badge points at the same
sentinel row, so the second one collided with the first and the whole
recompute failed. Identity differs between the two systems — legacy badges
are their badge_id, derived ones their rule_id — so there are now two
partial uniques saying that.

Counting shared its query with the sample of source proofs, which was capped
at twenty-five. Any rule asking for more was unreachable: conditions met,
nothing fired. The thirty and hundred thresholds here would have been
decorative.

And the manual-grant constraint accepted the case it existed to refuse:
btrim(NULL) <> '' is NULL, and a CHECK that evaluates to NULL passes.

The grammar gains skill_domain, distinct_over and two proof types, so a code
badge is not awarded for design work and "three languages" is not satisfied
by thirty deliverables in one.

7 new tests. The 21 pre-existing badge tests still pass.
… on nothing

Two revocation paths disagreed. Moderation set verification_status to
'revoked'; fraud review set revoked_at and left the status at 'verified'.
Fifteen queries read the status alone — the rank computation, the
attestation generators, the timeline, the tracks — so a deliverable revoked
for fraud went on counting. Somebody caught cheating kept the rank the
cheating earned.

Fixed at the source rather than in fifteen readers: a trigger makes the two
columns agree, so verification_status = 'verified' is true whenever it is
written, which is what every one of those queries already assumed. Adding a
condition to each would fix today and not the sixteenth query.

The backlog asked for seven new attestation_type values. attestation_type
has three, and each carries an invariant — compagnonnage links a project,
gesture and skill link exactly one skill node. "PR merged upstream" is not
a kind of attestation, it is what one rests on, and an attestation of that
type would satisfy no branch and be refused outright. It is a second column,
orthogonal to the first, and one that names an artifact must link it: a
basis nobody can point at is an opinion.

Nullable, because every attestation issued before today rests on something
nobody recorded, and a backfilled guess would put a claim in the record that
no human made.

5 new tests. The 24 tests around moderation, fraud, ranks and attestations
still pass.
…blind

The backlog asked for five markdown grids. A grid in a document is read
once and then remembered wrongly; this one is read by two things.

A human opening a review of a given family gets the criteria for it, so two
reviewers apply one standard rather than two.

And the LLM verifier receives challenge_templates.evaluation_rubric, which
is NULL on every challenge nobody hand-wrote one for. It was asking the
model whether work was good with no statement of what good means — and
getting an answer. A challenge with no rubric now falls back to its
domain's grid.

Nine grids: one common to the domain, eight per family, each criterion
paired with what meeting it looks like. "Performance" alone is a word; what
a reviewer needs is what counts as satisfying it.

The constraints refuse an empty grid and a second default per domain: a
grid with no criteria is worse than none, because it looks like a standard
has been set.
The charter was implicit, scattered across general documents. A rule nobody
can cite is not a rule, so it is written down: what counts as a deliverable,
the three non-negotiables, contribution ethics, how AI assistance is
declared, and what revocation takes back.

The manifesto says why those rules and not others. It opens on the problem —
nobody knows what people can do — rather than on the technology, and it ends
on what the platform does not promise, which is the part that keeps the rest
honest.

The brief templates share one structure, because a badly written brief
produces deliverables that answer different questions and a reviewer who
arbitrates on instinct. Their families match the reviewer groups, so a brief
promises the grid that will actually be applied.

.gitignore listed docs/ as generated. Fourteen hand-written files were
already tracked there, so the rule only ever caught the fifteenth — every
new document needed -f, which is how documents stop being written.
…e disputed

slice_type had github_issue and eight siblings, all naming where the work
happens. A published library, an accepted RFC and a benchmark had nowhere to
go, so they were filed as 'other' and lost their shape. code_artifact plus a
subtype says what came out; slice_type keeps saying where it happened, so
the ingestion and the queues that switch on it gain no branches.

Languages are plural. A slice touching Rust and TypeScript is one slice, and
forcing a single language would either lose half the work or invent a second
artefact. Two endpoints read them — one person's languages, and the
community's — from verified work only: what somebody typed into a profile
answers a different question.

Benchmarks get a table because "twice as fast" is the most common claim in a
portfolio and the least checkable. A baseline, a method and the code are all
required by constraint rather than by documentation: without the baseline
there is no second term, without the method nobody can tell whether the
comparison was fair, and without the code it is a screenshot. Reproduction
carries its author or is not recorded.

9 tests.
A published library is a claim until somebody checks the registry. Ten
registries are recognised from the URL; three publish usage figures over a
public API and are read.

The split between what is tested and what is not is deliberate. Recognising
crates.io/crates/serde as the crate serde is pure code and fully covered —
including that an npm scope is part of the name, since @sveltejs/kit and kit
are different packages, and that an unknown URL is left alone rather than
guessed, since serde on crates.io is not serde on npm. Asking crates.io for
a download count is a call to somebody else's service; a test that made it
would fail whenever they deploy.

NULL is not zero. Go modules and Homebrew publish no download count, and
writing zero would claim nobody uses what we merely cannot measure. A failed
fetch keeps the previous figures and records the error: an old number with a
visible date beats no number, and beats a zero that reads as abandonment.

Figures are stored rather than fetched per page view — three registries per
profile view would be slow, would break when one is down, and would get us
rate-limited by all three. The worker runs daily over rows older than a
week, so the load spreads and a deployment that missed sync day is not a
week behind. Off unless asked for.

9 tests.
…acts

A slice could be rejected, reworked and resubmitted any number of times.
Each decision was a row and nothing tied them together, so neither side
could see where they were — the contributor got a rejection with no sense of
progress, and nobody could tell a second pass from a seventh.

Five rounds, not because the sixth attempt would be worthless but because by
then the problem is not the code: the slice is mis-scoped, mis-briefed, or
beyond the person who claimed it. The refusal says that, rather than reading
as one more rejection.

The round is derived by trigger from the decisions already recorded, so a
caller cannot restart the count and hide how many passes a slice has taken.

And a rejection names its kind of blocker — red CI, missing tests, missing
documentation, review comments, wrong scope, out of depth. "Rejected" alone
leaves a contributor guessing between fixing a pipeline and renaming a
variable, and gives an operator no way to see that a project rejects
everything for the same reason.

5 tests. The 13 existing slice tests still pass.
Thirty-three trades with an empty catalogue are thirty-three trades the
platform claims to support and cannot. These are the starting points.

Drafts, not published. The title and the intent come from the backlog; the
constraints, the numbers and the out-of-scope need an author who knows the
trade, and offering an unreviewed brief to somebody learning is worse than
offering nothing. draft is the state the workflow already has for exactly
this — an operator edits and publishes.

Each inherits its family's review grid, so verification has criteria from
the first day rather than from whenever somebody remembers to write a
rubric. Each states what has to come out of it — a merged contribution, a
published package, something in service — read from what the backlog
described, so the instructions differ rather than being one paragraph
repeated 138 times.

Difficulty is set by family and is not a judgement about the people who work
in it: it says how much has to be true at once before the work is verifiable.
A kernel patch is not harder than a React component because kernels are
prestigious — it is harder because it cannot be half-done.

Language is left unset where a trade spans several. The polyglot badge counts
distinct languages, so a wrong one is worse than none.

Generated from the backlog rather than transcribed, and the generator caught
its own gap: the heading for systems-programmer carried a parenthetical, its
six challenges were being absorbed into the previous trade, and comparing the
parsed slugs against the live catalogue is what surfaced it.

6 tests.
A slice carries an orientation, and a label on a project says which
orientation its issues mean. The same "good first issue" is frontend
work on one repository and kernel work on another, which is why the
mapping is a table and not a constant.

`resolve_orientation` follows one rename, so catalogues written against
the old vocabulary keep working instead of being rewritten.

Twenty-eight repositories seeded across three catalogues: our own, the
twelve partners of Annexe F, and the large ecosystem projects. Labels
are per repository, and a repo with no label ingests nothing rather
than everything.

"Touche-a-tout" stops being a judgement and becomes a count of distinct
trades, which is only possible now that a slice has one.
…ests

`GET /code/first-issues` aggregates open, unclaimed issues across every
seeded repository, filtered by trade and language, capped at a first-issue
difficulty. An old trade slug still reaches its issues; an unknown one is a
404 rather than a silent "here is everything", because a typo that answers
the whole catalogue is how somebody claims kernel work believing it is
frontend. Cached an hour, namespaced by database so two deployments sharing
a Redis do not serve each other's feed.

`GET /code/ecosystems` says where each language community actually lives and
which of its events are reachable without a visa. Twelve curated rows, each
with a summary in the platform's own words — a link dump is what somebody
already failed to navigate before arriving here.

The label mapping is now read at ingestion, on both paths. The label that
triggered a webhook decides; otherwise the mapped labels must agree, and a
slice whose labels name two trades stays untyped. Silence beats a coin toss.
…egal

Sections C, M, A, P, G, L and O of the code backlog. Fourteen migrations,
all verified by rebuilding the schema from zero.

Contests. Code golf, TDD contests and code hackathons sit on the tournament
machinery rather than beside it: a code hackathon is a hackathon with a
domain, not a fourth kind. Submissions are revised in place and a revision
clears its judgement, because a score belongs to the artefact it was given
for. Code golf ranks ascending — a leaderboard that crowns the longest
solution is worse than none. Refusing an entry requires a reason.

Awards. Eight categories as rows, weighted 70/30 with each ballot normalised
to its own electorate first, so eight jurors carry the jury weight instead of
drowning in four thousand community votes. A juror keeps their community vote.

Marathon. Scored from upstream contributions nobody files twice, counted at
read time so the leaderboard is live and a revoked contribution stops
counting. The badge is granted by whoever concludes it, with the count in the
reason.

Marketplace. `missions`, not `code_missions`: the cyber and design backlogs
would otherwise inherit whichever mistakes shipped first. Twelve kinds of
work as rows. Invoices per instalment, because a retainer paid once at the
end is not a retainer. The commission is frozen at selection and copied onto
each invoice — what was charged in March must stay readable in November.

Craft score. The formula and the six tiers are rows, published by the API:
a score computed from a secret formula is one people game by guessing rather
than by doing the work. Every point returns the line explaining it. Review
grids finally carry scores, so the term that reads them has a source.

Portfolios. GitHub, GitLab, Codeberg, SourceHut and ten registries, with the
distinction that matters as a column: typing a handle proves nothing, only
OAuth does, and only the proved one counts.

Guides. Eight onboarding guides in two languages, a toolkit, twelve writeup
templates — rows rather than files, because they have to be translated and
edited by somebody who is not deploying.

Licences. Thirteen licences and what each permits a mission to promise. A GPL
upstream with full client ownership is refused at creation, which is the most
common legal accident in commercial open source work. AI disclosure is a
window and not a constraint: a merged pull request is verified by a webhook,
and a webhook has nobody to ask.

Onboarding. Seven questions, and a first month that says why. Mentor matching
reads family, language, distance, timezone and load, and returns the
reasoning — a mentee who can see why somebody was suggested can tell us it
was wrong.
… name

`ai` names a domain of work — ten orientations, review grids, artefacts that
get attested. It also named the LLM assistant that reads a submission, and
both answered under `/api/ai`. A reader of the API had no way to tell which
of the two a route belonged to.

The assistant moves to `/api/assistant`. Nothing about it changes but the
prefix; what changes is that `/api/ai` now means one thing.
"ML Engineer" answered for four jobs with four hiring markets. Six trades are
named here, two of the existing four are narrowed to stop overlapping them,
and all ten get an English name — the four seeded in 0088 never had one.

They also never had a skill map. Two years of looking supported while nothing
could be recommended and nothing verified: sixty-two nodes and a hundred and
ten mappings close that.

Review rights follow the mechanism 0176 built rather than a second one:
five families plus a wildcard, derived from the domain. And `ai` finally has
review grids — until now an AI challenge with no rubric reached the verifier
with its instructions alone, which is asking a model whether work is good
without saying what good means.
Eleven of the twelve are counted. Migration 0177 had to mark six code badges
manual because nothing could count them; half that gap closed when 0178 gave
attestations a basis, so "shipped a model" is a row with a value in it rather
than an operator's judgement. The rules read it.

Making that work needed the engine to admit the filter, and uncovered the
same bug 0177 fixed for deliverables: the attestation branch counted the
length of a page capped at twenty-five, so any rule above that threshold was
unreachable. It counts and samples separately now.

The seven AI bases go on `attestations.basis`, not on `attestation_type`.
The backlog asked for the second; 0178 already settled why it is the wrong
axis, and six of the seven must link a deliverable so the basis stays a claim
somebody can check.
A slice can produce a model, a dataset, an agent system or a paper. Six
subtypes say which, on the same axis `code_subtype` uses, and the address is
required for the four where a claim without one is a sentence.

The address is the artefact. The backlog planned a MinIO bucket with
fifty-gigabyte multipart uploads for weights; that is money spent to make
work less findable than HuggingFace hosts it for free. So the hub is the
home, and the platform records, verifies and refreshes the link.

Which is why two tables lose their prefix rather than gaining a twin:

  * `code_benchmark_results` becomes `benchmark_results`. An MMLU score has
    a harness, a baseline and a reproduction event exactly like a latency
    measurement. It needed one column — which dataset and split — not eighty
    duplicated lines.
  * `code_package_stats` becomes `published_artifact_stats`, and the sweep
    that refreshes crates.io figures now refreshes HuggingFace ones. A model
    and a crate answer the same question with the same row.
…eople

The landing page carries a ticker labelled LIVE with fabricated names on it.
Skilluv's whole position is that nobody knows what people can do and we prove
it; invented social proof is the exact failure the product exists to correct,
and it is the one claim on that page a careful visitor can check.

This is the backend half. A projection table rather than a query over the
private ones: a landing page is the most exposed surface there is, and a
missing predicate on a join leaks to everybody at once. Visibility is decided
at write time and stored with the reason, so reading the feed touches nothing
that could expose somebody who opted out.

Only artefact-backed events are admitted, enforced by a CHECK and by every
emitter passing a URL. A merged pull request, a verified deliverable, an
issued attestation pointing at its verification page, a published package, a
delivered mission, a paid bounty. Never a self-declared event, never a points
counter — a feed of points proves nothing to anybody, which is what the
ticker it replaces was.

Consent follows the notification catalogue's shape and its reasoning. A
merged pull request is already public on GitHub, so relaying it is fair and
it defaults to visible; a payment is not public anywhere, so publishing what
somebody earns because they took a bounty is off unless asked for. A CHECK
enforces that: an event not already public elsewhere cannot default to
visible. Turning a kind off is retroactive, because somebody asking to be off
the page is not asking to be off it from now on.

The two kinds written from many code paths are emitted by triggers, because
finding all of them and remembering the next one is how coverage rots. The
four with a single writer are emitted from the code that already knows the
wording. A revoked artefact takes its own line down and the row stays, so
somebody investigating a complaint can see it was shown and when it stopped.

The read endpoint returns `live` from the actual density of the last week. A
pulsing dot over a feed whose first line is two days old proves the place is
empty, which is worse than no feed.
Rounds keep their cap of five. The backlog wanted six for AI on the grounds
that experimentation needs more attempts — it needs more attempts at the
experiment, not more trips through review, and past five the problem is the
scope or the brief. What was missing is the four reasons a reviewer actually
sends AI work back: a score measured on the training set, a run nobody can
repeat, unclear provenance, something unsafe to publish as it stands.

Red-team findings become rows because a disclosure process needs a state
machine and a markdown file cannot carry one. A target with its version, a
success rate over a stated number of attempts, a proposed mitigation — all
three required, because each absence is how a finding stops being one.

Forty-one challenges, one set per trade, as drafts. The toolkit and the
external competitions are rows rather than a page: a page cannot be filtered
by what somebody is learning, and nobody redeploys to fix a dead link. Every
resource carries what it costs to reach — free tier, GPU needed, course
auditable — which is the half no upstream list writes down.

The AI craft score is computed, never stored. A column keeps the points of a
revoked attestation until somebody remembers to recompute, and this platform
sells the opposite. Two terms the backlog listed are absent and say so: there
are no paid missions to count, and reviews record a verdict, not a grade.
…can dispute

Six generators, each re-checking its own precondition rather than trusting
its caller. They run from the proof orchestrator, not from the moment a slice
is verified, and deliberately: two of the six are earned by events that
happen afterwards — a reviewer reproducing a benchmark, a vendor agreeing a
date. Hooking verification alone would leave those permanently unissued,
which is the dormant-engine failure P19 was written to end.

An artefact attestation is one per artefact, so the index from 0068 narrows
to the level-up generator it was written for. Without that, a second model
shipped on the same skill collided with the first and vanished.

And the tables get doors. Benchmarks have had a schema since 0182 and no way
to write a row, which left the design inert — a measurement is worth
something because a second person ran it, and there was nowhere for that
second person to say so. Reproduction is refused to the author, whatever
capabilities they hold: confirming your own numbers is what it exists to rule
out.

Disclosure moves forward one step at a time. The schema can refuse an
inconsistent row; it cannot refuse an inconsistent move, and private straight
to published is how a working attack reaches the internet before the person
who could fix it has heard of it.
…domain

Thirteen AI repositories seeded. Their label mappings are empty and stay
empty: on a repository we do not own, "good first issue" means small, not
vision, and guessing would credit somebody with a speciality they never
worked in. On skilluv-ia, which is ours, the area labels say the trade — and
the generic marker stops being mapped, since it was filing every verifier fix
under backend development.

The two AI contests go on `kind`, not on `format`. Format is the pairing
scheme, and a prompt battle head-to-head is a bracket; putting it there would
have left every bracket tournament choosing between the two as if they were
alternatives.

Which surfaced a bug from 0114: individual registration allowed `individual`
and `hackathon` and refused the rest, so `marathon` and `defi_solitaire`
could be created and never joined — the endpoint answering "not open to
individual registration" about tournaments that take nothing else. Stated the
other way round now: guild wars take guilds, everything else takes people,
and a new kind is open by default rather than silently closed.

The onboarding answers live in one table keyed by domain, not in `users.ai_*`.
Six domains asking that favour is thirty columns on the table every query
touches. The trades somebody claims stay in `user_orientations`, which has
held them since 0089 — a second copy is how the two start disagreeing.

The wizard records a HuggingFace username and imports nothing from it.
Importing that account's models would put artefacts on a profile with no
verified deliverable behind them, which is a list somebody typed.
…d an artefact

Five documents. The charter names four non-negotiables instead of the code
domain's three, because the failure modes differ: an honest evaluation, a
reproducible run, a stated provenance, and limits written by the author. Data
leakage is called out as the most common and least visible error in the
domain, because it is.

The disclosure policy is the part a schema could not carry. Ninety days from
notification, no way back to private, and a written reason for withholding —
withholding with no stated ground is indistinguishable from burying a finding.

The IP document says plainly that it is not legal advice and marks its open
questions as open, rather than pretending to answers a lawyer has not given.
It is written now because no rule is itself a rule, and the worst one:
without a text, everybody improvises and finds out after publishing.
Writing the profile test surfaced it: revoke a deliverable for plagiarism and
the attestation resting on it stayed issued, kept feeding badges and counts,
and the record still said a stranger could go and check something that had
been withdrawn.

`revoke_attestations_depending_on_deliverable` was written for exactly that
and called from nowhere. A function nobody calls does not become a rule by
existing, so it is a trigger now — reached by both revocation paths, and
firing when the last live deliverable goes rather than the first, since a
compagnonnage attestation on five still stands on four. The backfill revokes
those already standing on nothing.

Ninety-odd cases across nine files: the catalogue, review rights per family,
the artefact constraints, benchmark reproduction refused to its own author,
the disclosure order, the generators and their idempotence, the score that
derives on read, the public feeds, the wizard vocabulary and the seeds.

Also fixed: a rename in the stats suite had turned the column
`code_package_registry_url` into a name no table has. It compiled, because
those queries are checked at runtime.
… a catalogue

Section 00 of the business-model backlog: the cleanup everything else rests
on.

**One search instead of three.** v1, v2 and v3 each added filters the previous
one lacked and each kept its own SQL, row shape and pagination. Three
endpoints answering the same question differently is three places for a filter
to be subtly wrong, and a caller who cannot tell which to use picks the
oldest. They are deleted, not deprecated.

v4 carries everything they did plus what the platform has learned to record
since: the trade, a capability, a craft-score tier, and whether an external
account was *proved* rather than merely typed. Keyset pagination on
`(score, id)`, so a recruiter paginating while a score moves does not silently
skip or repeat a row. Cached fifteen minutes, namespaced by database. The
answer says which filters it honoured — a silently dropped one reads as
"nobody matches".

An endorsement still cannot masquerade as verified work: `vouched_by_count` is
its own field and never enters the score.

**Craft scores per domain.** Migration 0195 put `craft_score_code` on `users`
while the formula and tiers it reads were already keyed by domain. The storage
has caught up: a row per person per domain, tier resolved in the same write so
a listing can filter on "Senior and above" without a join and a range
condition on every row. The twelfth domain is now a row rather than a column
and a twelve-branch match in the search.

**Revenue as a catalogue.** The ticket proposed widening a CHECK to
twenty-two values; the real column is `source VARCHAR(20)`, too short for
several of them. Twenty-seven streams as rows instead, each naming its pillar
and whether it recurs, with a foreign key from the ledger. `is_live` starts
false everywhere except the four that actually earn, and a trigger flips it
when something books revenue — a catalogue claiming twenty-seven live streams
when four are live is a lie told to oneself first.

The mission marketplace now books its own commission line, which was going to
the platform account and appearing nowhere an accountant reads.

`docs/monetization-strategy.md` (ticket 00-01) does not exist in this
repository; nothing to delete.
…ind it

Section 01 of the business-model backlog.

**Engagements.** Every product keeps its own table — credits, subscriptions,
bounties, missions — and none of them can answer "what does this company have
with us", which is the question asked before every renewal and the one an
upsell is decided from. One row per engagement, pointing back at whatever
holds the detail.

Eighteen product types as rows, each naming the revenue stream it feeds, so
"what do they have" joins to "what does it earn" without a mapping written
twice. A recurring product is refused without a renewal date: without one it
never appears on a renewal list and lapses because nobody was told to ask.
Lapsed and cancelled are separate statuses — one is a decision at the end, the
other in the middle, and a renewal report that conflates them is useless.

**Four documents.** The economic charter, the pricing grid, the data-licensing
policy, and the enterprise onboarding playbook.

The charter states the golden rule and the test for its two exceptions:
mentoring and the creator marketplace both have the talent on the selling
side, and either would have to go if it became a toll. It also states what
Skilluv refuses to sell, and the uncomfortable consequence of the positioning
— the platform cannot grow faster than the amount of genuinely proved work
passing through it.

The pricing grid says plainly that every figure is a hypothesis except the two
already implemented, and that none of them is charged to a talent for access
to an opportunity.

The data-licensing policy is in force before its tooling, deliberately: the
other order produces tooling that decides the policy. It lists what cannot be
sold, to whom, and names the four things that do not exist yet so nobody
mistakes the policy for a capability.

The onboarding playbook is written for a sales team that does not exist, and
says so.
… them

Section 02 of the business-model backlog: eight tickets, five tables.

**Four tickets, one table.** The backlog describes managed sourcing, volume
programmes and retained pools as three tables. They differ in three fields —
how many positions, whether there is a monthly fee, whether the shortlist is
refreshed on a cadence — and share the brief, the targeting, the shortlist,
the statuses and the fee structure. Three tables would have meant three
shortlist tables, three status machines and three places to fix the next bug.

Constraints keep the kinds honest: a retained pool is paid monthly and charges
no success fee, because the client is already paying to keep it warm and
charging on success too would be charging twice for one hire. A volume
campaign is five positions or more — one position at a reduced rate is a
discount, and should be recorded as the percentage it is.

**The rule the whole thing turns on.** Nobody reaches a client without having
agreed. Enforced by a trigger, not a service check, because the shortlist is
written from an admin endpoint, a curation job and eventually an import, and a
service check would hold for one of the three. The client's view filters to
people who answered; the invitation tells the talent who is asking and for
what, because asking somebody to agree to "an opportunity" is how people stop
answering.

A shortlist entry must argue for the person. A list of names with no argument
is a search result, and the client is paying precisely not to do that reading
themselves.

**Money.** The volume discount is applied from a scale rather than typed — a
discount somebody enters by hand eventually disagrees with the scale it came
from. The success fee books into the revenue ledger when it is charged, not
when it is collected.

The guarantee is its own table because it outlives the campaign: one that
closed in March still owes a refund in August. The refund is banded rather
than prorated, because the work was the same whether the person stayed one
month or five, and a straight line would refund almost everything at week two
— exactly when the client is angriest and least interested in the argument.
jeremie0342 and others added 13 commits August 20, 2026 14:25
…ht shards

Four endpoints that answered 500 to every request they had ever received, the
migration script and the read-endpoint smoke test that found three of them,
and the shard change that made the failures visible in the first place.
…r than broken

The smoke test covered the GET routes taking no path parameter. This adds the
74 that take one, called with an id nothing owns: a handler naming a column
that does not exist fails when the query runs, not when a row matches, so a
stranger's id still reaches the failure. 192 routes now, none of them reached
by any other test.

Only one of the 74 answered 5xx, and it was the same shape as the Stripe one:
Judge0 is not running here or in CI, and three call sites called that an
internal error. A sandbox we cannot reach is unavailable, not broken.

The distinction is drawn on the `reqwest` error rather than on the endpoint:
`is_connect`, `is_timeout` and `is_request` mean nothing answered, which is
503. A response we did receive and disliked stays a 500, because then the
request itself is the suspect and saying "unavailable" would send somebody to
look at the wrong machine.

Verified against PostgreSQL 18: three passes over 192 endpoints, green.
Two branches wrote the same onboarding migration at the same time and
neither could see the other. 0258 wins: it runs first, and it names the
questions `goal` and `main_tools`, which is what the generic wizard already
called them. 0306 stays as an empty file with the reason in it.

Resolving the wizard by hand lost six things that only the tests found:

- `main_tools` and `github_username`, two questions of the code wizard
- the GitHub side effect, which turns a typed username into an unverified
  portfolio row -- generalised onto the audio hook that already did exactly
  this, so the AI wizard's HuggingFace handle stops vanishing too
- `portfolio_url` and its `external_signals` row, same shape, design side
- the recommendation: `onboarding_recommendation` was declared, published in
  the OpenAPI, and called by nobody

And two divergences that predate the merge:

- `mission_arbiter` was added by 0260 and dropped again by 0305, 0400 and
  0404, all written before it. The capability could be granted to nobody, so
  the guard reading it refused everybody, silently. That is the eighth
  vocabulary a restated list has lost. `scripts/capabilities-named-in-code.py`
  plus a new invariant now refuse any capability the catalogue cannot grant,
  and a CI job runs the whole check in two minutes rather than the suite's
  forty-five. The eight shards depend on it.
- `preferred_families` was published by one query and validated by another,
  and for design the two disagreed: the endpoint offered reviewer groups
  where the validator wanted trades, so a form built from what the API
  published was refused by the API that published it. One `families_for` now.

`POST .../skip` answers 204. It used to answer 200 with a body it made up,
reporting no answers whatever the row held.

Verified against a local PostgreSQL 18: 441 migrations apply, the invariants
hold, and 638 unit plus 224 integration tests across fifteen files pass.
Build & Lint failed in thirty-one seconds on formatting alone, which skipped
the eight shards and turned the aggregate red without a single test having
run. Six of the eight files are the design branch's own: that branch never
had a green Build & Lint, so nothing had ever told it.
Two tickets, one principle: a record other people rely on is not the accused's
to destroy, and not the platform's either.

## L-02 — an accusation the accused can answer

`plagiarism_cases`, and it is deliberately not a `reports` row. A report has
nowhere for the accused to reply, and the reply is the substance of this
procedure: the outcome is a disqualification, a confiscated prize and a public
record, and deciding that without hearing the person is not a decision, it is
a verdict.

  POST /api/contests/submissions/{id}/flag
  GET  /api/contests/plagiarism/{id}
  POST /api/contests/plagiarism/{id}/respond
  GET  /api/admin/plagiarism
  POST /api/admin/plagiarism/{id}/decide

**Eighty characters, both ways.** "C'est copié" gives somebody three days to
work out what they are answering and nothing to answer it with. And dismissing
an accusation without a word leaves the accusation standing in everybody's
memory — so the floor on a decision is the floor on an accusation.

**An evidence link is required.** An accusation with nothing to compare against
cannot be checked by anybody, the reviewer included.

**Seventy-two hours is a floor on the decision, not a deadline on the person.**
A late answer is still recorded, and a reviewer who has not decided yet should
read it.

**Anybody may accuse; nobody may accuse themselves.** Plagiarism is spotted by
whoever recognises the original, which is rarely whoever is judging. Accusing
your own entry would give a losing entrant a way to withdraw while blaming the
process.

**An open case is not public.** The accused and the reviewers, nobody else:
publishing allegations before they are decided is how a dismissed case still
ruins somebody.

**Nobody is banned by this.** The backlog's "second strike, ban" reads well and
would, one Tuesday, ban somebody on an accusation a tired reviewer upheld in
four minutes. The count is surfaced; the ban stays a decision a human signs.

An upheld case **marks** the entry rather than deleting it — `disqualified`,
with the decision as its note. Deleting it would erase the fact that it was
entered at all, and the other entrants moved up: a ranking whose gaps are
unexplained is a ranking nobody can check.

## L-03 — erasure that leaves a tombstone

`DELETE FROM users` took the contest entries, the podium places, the validated
deliverables and the attestations with it. That is wrong in both directions at
once: it destroys more than the person asked for — a contest where the second
place vanished leaves first and third unexplained, and the winner's own
attestation cites a ranking that no longer adds up — and erasure does not
require it. What has to go is the personal data.

So the `users` row survives with nothing personal in it, and every row that is
wholly about one person is deleted outright: preferences, notifications,
wizard answers, declared portfolios, devices, and above all the tokens to
third-party tools — leaving one behind would leave Skilluv able to read
somebody's Figma after they left.

The username becomes `supprime-{eight hex}` and the e-mail lands on `.invalid`,
reserved by RFC 2606, so a stray mailer reaches nobody.

`title` is left alone: it is the rank, not an intitulé somebody typed.

**The export now carries the design half** — wizard answers, portfolios,
deliverables, attestations, contest entries and placings, missions taken,
rounds delivered, ratings received, plagiarism cases. An export that stopped at
challenges told somebody less than the platform knows about them, which is the
one thing an export may not do.

Connections are exported through their own projection, without the tokens: a
live credential in a zip that travels by e-mail is worse than not exporting it.

## Two bugs found on the way

**The export named a table that does not exist.** `user_email_preferences` has
never existed — the table is `notification_preferences` — so that line failed
every time an export ran.

**My own first draft of the erasure was broken in a way worth naming.** It
tried each DELETE and logged the failures. A statement that fails inside a
transaction aborts it, so every statement after the first miss was refused and
the tombstone was never written; the caller saw "current transaction is
aborted" rather than an error that meant anything. The table list is checked
against the schema first, and everything after that is fatal — half an erasure
is worse than none.
Three conflicts, all of them additive. `src/services/mod.rs` listed `erasure`,
`featured` and `mentorship_matching` where only the first was new -- the other
two were already declared, so the block collapses to one line in its
alphabetical place. The three locale files each gained a section on both
sides; both sections stay, and the keys still match across fr/en/ar.

Migrations 0263 and 0264 land in a gap nothing else claimed, so the chain
applies unchanged. `plagiarism_reviewer` was already a catalogue row, which
the capability check confirms rather than assumes.
Bumps [github/codeql-action/autobuild](https://github.com/github/codeql-action) from 3.37.3 to 4.37.7.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@4187e74...ff2f1c6)

---
updated-dependencies:
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.7
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [github/codeql-action/upload-sarif](https://github.com/github/codeql-action) from 3.37.3 to 4.37.7.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@4187e74...ff2f1c6)

---
updated-dependencies:
- dependency-name: github/codeql-action/upload-sarif
  dependency-version: 4.37.7
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata) from 2.5.0 to 3.1.0.
- [Release notes](https://github.com/dependabot/fetch-metadata/releases)
- [Commits](dependabot/fetch-metadata@21025c7...25dd0e3)

---
updated-dependencies:
- dependency-name: dependabot/fetch-metadata
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [rand_core](https://github.com/rust-random/rand_core) from 0.6.4 to 0.10.1.
- [Release notes](https://github.com/rust-random/rand_core/releases)
- [Changelog](https://github.com/rust-random/rand_core/blob/master/CHANGELOG.md)
- [Commits](rust-random/rand_core@v0.6.4...v0.10.1)

---
updated-dependencies:
- dependency-name: rand_core
  dependency-version: 0.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [jsonwebtoken](https://github.com/Keats/jsonwebtoken) from 10.4.0 to 11.0.0.
- [Changelog](https://github.com/Keats/jsonwebtoken/blob/master/CHANGELOG.md)
- [Commits](Keats/jsonwebtoken@v10.4.0...v11.0.0)

---
updated-dependencies:
- dependency-name: jsonwebtoken
  dependency-version: 11.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [zip](https://github.com/zip-rs/zip2) from 3.0.0 to 8.6.0.
- [Release notes](https://github.com/zip-rs/zip2/releases)
- [Changelog](https://github.com/zip-rs/zip2/blob/master/CHANGELOG.md)
- [Commits](zip-rs/zip2@v3.0.0...v8.6.0)

---
updated-dependencies:
- dependency-name: zip
  dependency-version: 8.6.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Three call sites used it. Two wanted OS entropy for a token, which is what
`getrandom::fill` already gives them everywhere else in this codebase --
`enterprise_sso`, `push_sender` and `scim` were all using it a few lines
away. The third was `scim::_rng_unused`, dead code whose only purpose was to
silence an unused-dependency warning back when `rand` was not a direct
dependency; it is, so the function goes with the import.

`getrandom::fill` is not a substitute for what `OsRng` did -- it is the same
OS entropy `OsRng` wrapped.
`missions::set_status` releases the escrow when a mission closes, with a
comment saying it lives there "so it happens whichever way the mission is
closed". Nothing did the same for cancellation. A mission cancelled from
`in_progress` with a paid invoice left the talent's share in `pending` for
ever: no code path could release it, no code path could return it, and
nothing counted it.

`POST /api/admin/missions/{slug}/arbitrate` had the same hole twice over,
and worse, because it documented the opposite. Its own doc-comment says
"`accepted` — the delivery stands and the money is released" and "`cancelled`
— the mission ends and the escrow goes back", and it wrote the mission status
with a raw UPDATE that bypassed the one function where both of those live. It
also stopped at `delivered`, which waits on the client accepting delivery —
the act arbitration exists because the client refused.

- `ledger::refund_from_pending`, the missing primitive. `refund_from_dispute`
  starts from `disputed`; routing a cancellation through `hold_dispute` first
  would write a dispute nobody raised into the queue an operator counts.
- `mission_billing::refund_all`, the mirror of `release_all`: provider first
  then the books, per `disputes::settle`, and the commission goes back with
  the rest so marketplace revenue stops counting a fee on a service nobody
  received.
- Only `paid` invoices. Released money is the talent's and may be withdrawn;
  clawing it back is the harder problem the release window exists to avoid,
  and a client who wants it has a dispute. On a milestone mission that draws
  exactly the right line.
- `refunded` is its own invoice status (0442). `cancelled` means nobody ever
  paid, and an accountant has to be able to tell those apart.
- Arbitration goes through `set_status`, so both outcomes move money, and
  `delivered -> cancelled` becomes a legal transition: the "different flow"
  the old comment said would be needed to dispute a delivery now exists.
- A repeat call finishes a settlement that died halfway instead of answering
  409, since the ledger's idempotency keys make the money safe to retry and
  the alternative is money stuck with no call that can free it.

No test had ever put an invoice on an arbitrated mission, which is why an
endpoint that moved no money passed. Four now do.
`contest_prizes::award` credits a winner's `pending` balance rather than
their `available` one, and its comment says why: "the release window is what
makes a contested result recoverable". Upholding a plagiarism case is the
only thing in the codebase that ever contests a result, and it marked the
entry `disqualified` and left the money — so a contest could hold, in one
person, a winner who was disqualified and a winner who was paid.

Two modules written to fit each other, never joined.

`contest_prizes::confiscate` reads what was actually awarded out of the
ledger rather than recomputing the split, refuses when the amount has already
been released — that is a debt to recover through people, not an entry to
reverse, and driving `pending` negative would make the books claim money that
is not there — and returns it to the contest escrow.

Back to the escrow, not to the sponsor and not to the runner-up. Refunding
the sponsor would decide, in a function nobody is reading, that a contest
with a cheating winner pays its second place nothing. Promoting the runner-up
would pay somebody weeks later on a decision they were never told about, and
re-running `award` against a changed podium is not something its idempotency
keys allow. In the escrow it is visible, balanced, and somebody's to decide
about.

The confiscation runs after the decision commits: the decision is the thing
that must not be lost, the accused can already read it, and a prize that
cannot be taken back must not turn a decided case back into an open one. A
failure is logged and counted rather than swallowed.

Also fixes `prize_currency` in my first draft of that read — the column is
`prize_cash_currency`, and the test is what said so.
The contract fuzzer's one failure out of 907, and the only test that had ever
typed a punctuation mark into a search box.

`forum::search_posts` joined the words with ` & ` and handed the result to
`to_tsquery`, which parses its argument as a query expression. Escaping
apostrophes is not enough: `&`, `|`, `!`, `(`, `)` and `:` are operators, so
they reached the parser as syntax and anything that did not happen to form a
valid expression raised `syntax error in tsquery` — a 500 at the client.
`admin_moderation::list_users` did the same with no escaping at all.

Searching the forum for `C++`, `R&D`, `(brouillon)` or `design:system` was
therefore a server error. Schemathesis found it with random bytes; a user
would have found it with a plus sign.

`websearch_to_tsquery` is the one built for a search box: it never raises,
whatever it is handed. It also reads quoted phrases and a leading `-` as
exclusion — what somebody typing into a search box already expects — and it
still ANDs bare words, so what used to work works the same. The second test
asserts that narrowing specifically, because "no 500" is otherwise buyable by
matching nothing.

Verified the test fails on the old code first: it reproduces the fuzzer's
error verbatim.

No other Postgres parser takes user input directly — checked for `to_tsquery`,
regex operators and jsonpath.
…prevent

Shard 7 failed on a unit test I broke and had not re-run:
`everything_open_can_still_be_cancelled` asserts that `delivered` cannot
become `cancelled`. I widened that edge so an arbiter could end a delivery it
had ruled against, and the assertion was the only thing that said the edge is
not merely a workflow rule.

It is a money rule. Cancelling is what returns the escrow, and it returns it
to the client — and `POST /api/missions/{slug}/status` lets the owning
enterprise make any transition the table allows. So for one commit a client
could accept the work, cancel the mission and take the payment back, with the
`refund_all` added in that same commit doing the taking.

The table goes back to `delivered -> closed`. The extra edge moves to a
`Decider` the caller passes: `set_status` is `Party` and refuses it,
`set_status_as(..., Decider::Arbiter)` allows it, and only the arbitrate
endpoint uses the second — after checking the capability.

The test now asserts both halves and the money on each side, and I verified
it fails on the broken version before keeping it.

The miss was mine and it is worth naming: I ran the integration suites that
touch missions after the change and not `cargo test --lib`, because the unit
tests live inside `src/` and I had already run them earlier in the session.
Earlier is not after.
@jeremie0342
jeremie0342 merged commit 0824535 into master Aug 21, 2026
28 checks passed
@jeremie0342
jeremie0342 deleted the feat/code-orientations branch August 21, 2026 19:11
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