feat: the ops domain, the business model, the tracker, and the AI/audio and design branches folded in - #82
Merged
Merged
Conversation
… 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.
…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.
This was referenced Aug 21, 2026
`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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four backlogs, three parallel branches, one branch.
code,live-feed,business-modelandopswere 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" — withnothing 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
RAISEwith a%and no argument, a view reading atable renamed out from under it,
is_currentas a stored generated columnover
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_revenuesinserts omitted a NOT NULL column,and the Mobile Money provider returned
Pendingwith a synthetic referencewhile 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_ebur128looked for a line starting withSummary:and ffmpeg prefixes every line its filters emit, so every loudnessfigure 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:
goalsvalidated a capability by pattern-matching the text of a CHECKconstraint that migration 0404 had dropped when it made capabilities rows.
The lookup answered false for every capability that exists.
<rfc3339>|<uuid>, and an RFC3339 offsetcarries a
+, which a query string decodes as a space. The only thinganybody does with
next_cursoris put it in a URL, so page two answered 400to a cursor the server had just issued.
credentials::declareread 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.
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.
text column at all, and surfaced as a 500 blaming the server.
there inserts cleanly and is invisible to every listing that joins the
catalogue.
What the merges settled
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.
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_KINDSandCOMMUNITY_VOTED_KINDSwere the same bug one file along.Two columns on
tournament_kinds, with a CHECK saying a format is notjudged two ways.
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.
/api/guides?domain=…. Oneuser_domain_profilesfor wizard answers,now including design's own two questions, which its wizard was refusing.
One craft-score loop. One
published_artifact_urlwhere four columns wereabout to say the same thing.
ui-sound-designwas defined by design and again by audio: one skill, tworows, 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/consentdocumented 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-profileanswered 500 to every request it had everreceived. Twelve of its thirteen figures are
count(*), which PostgreSQLreturns as bigint, and the struct reads all thirteen as
i64; the thirteenthwent through
date_partand was cast::INT, making it the one int4 in therow. 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 writtenotherwise, 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-clientsand/api/enterprises/me/type-configorder by
enterprise_members.created_at, which does not exist. The functionis a second copy of
resolve_active_enterprisethat drifted; realigning italso restored the
status = 'active'filter it had lost, without which apending invitation outranked a membership somebody holds.
/api/admin/accounting/exportselectsenterprises.country, which does notexist either. The country belongs on the invoice anyway: VAT follows where
the customer was when they were billed.
Verification
scripts/check-migrations.shin fifteen seconds against a local PostgreSQL18 — 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 onetest, it fails the chain, and the backend never starts. That happened once
here and cost a whole CI run.
cargo clippy --all-targets --all-featuresis clean.failures to zero, and the last rounds were verified locally before pushing
rather than by waiting on CI.
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_rowsaddsforeign 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/eventsmoved to/api/tournaments/feed: bothevents.rsandtournament.rshad registered it, so the router panicked at startup and thebinary never booted.
The public feed's
next_cursoralso changes format, though it could notsurvive being put in a URL before, which is the only thing anybody does with
it.