Repository navigation
Baby steps from 11 onwards (learner builds, my_11 …) - #6
Merged
Merged
Conversation
…ps did not hold The demo said step 20 stops the duplicate INV-1008 payment; it is a new request with a new key, and in-flight exclusivity, §25.1, is what would stop it. A pipeline test's comment put the claim after 6 and listed the tenant and the delegation as missing. Migration 018 said a claim is released "because nothing ran", which only the walk can know, and that the grants keep an answer, which the second policy does. Start-up's comment now says where rewriting is refused. Comments only. Migration 018 is edited in place: no database that is kept has applied it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README tells the step in plain words: every command carries an idempotency key beside its arguments, DSoR claims it with one INSERT, the same request sent twice gets the same answer and runs once, a different request under the key is refused, and fifty at once make one payment. Its five breaks were measured twice, in a copy, on the whole suite, and the two runs agreed: 2, 6, 15, 13, 3. The database tier, 52 tests, passed on a local PostgreSQL 17 set up like Neon with eighteen migrations, and the race passed eight more times on its own. Decision 132 records the review's three broken guarantees and the rest, what was done about each, and six breaks measured once each, all as predicted. docs/status.md lists my_20 at 702 tests and 132 decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The job runs pnpm check in every step folder, one after another, and was cancelled at its 30-minute limit on the last two runs: on Ubuntu it reached the middle of my_17, so steps 18 to 20 were never checked in CI. No step had failed before the cut-off. About three minutes a step; 60 leaves room for ten more, after which the job wants splitting by track. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copied without node_modules, .env or a local database, so this step's migrations can never run against step 20's Neon database. Only the package's name and description change. Everything else is byte for byte step 20's committed code, which passed 702 under pnpm check; the README is rewritten with this step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments in src and test only. Migration 018 keeps its NEW IN STEP 20: an applied migration is never edited. pnpm check: 702 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Migration 019 adds a version to invoices and payments: a whole number from 1, never null, raised by one in the same UPDATE as every change. The application may raise it and nothing else. A read says it, the resource_version DSOR-FRS-01a names, and it is labelled internal, so the agent that decides on a record can read the version it decided on. The tests that pin a record's fields exactly gain the version. Red first: no read said a version, and issuing left none to raise. pnpm check: 705 passed. DSOR-CON-01b, DSOR-FRS-01a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ded on, and a record that moved on is STALE_STATE invoice.issue and payment.cancel declare the optimistic strategy; payment.create, which makes a record and has nothing that could have moved on, says none. An optimistic command needs expected_version, the version the decision was made on, a whole number from 1, as an ordinary argument inside the payload hash; without one, or with anything else, it is VALIDATION_FAILED. The check is part of the change: one UPDATE with the status and the version in its WHERE, which raises the version. When it changes nothing, DSoR looks, the version first: a record that moved on is STALE_STATE, retry after_state_refresh, whatever its state now; on the same version, the business rule says CONFLICT, as before. Two changes on one version sent together: one is made, the other finds it stale. Every existing call to the two commands, in the tests, the contracts' example requests and the demo, says the version it decided on. Tests that retry a changed record say the version they would have read again. The demo reads INV-1009 before the agent issues it, and cancels the mistaken payment on the version its receipt says, so a second run is still CONFLICT and not STALE_STATE. Red first: five tests. pnpm check: 710 passed. DSOR-CON-01a, DSOR-CON-01b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tic command, and within the column's range An expected_version above 2147483647, PostgreSQL's largest INTEGER, failed inside the statement: a raw driver error, a log saying ALLOWED for something that never ran, and an idempotency claim left with no answer. And the handlers asked for a version by hand, after §21.11 had recorded ALLOW, so a missing one was refused VALIDATION_FAILED with ALLOW on the record; and nothing read the contract's declared strategy. Now §21.6 asks for expected_version, 1 to 2147483647, of every command whose contract says optimistic, so the refusal is recorded as the DENY it is; and the registry refuses a strategy no code carries out, pessimistic and connector_managed. Red first: 2**31 and 1e300 threw, the missing version was recorded ALLOW, and the registry took pessimistic. DSOR-CON-01a, DSOR-CON-01b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n a real server, and a refusal that hands over no number With the version taken out of the UPDATE, every step-21 test still passed, because the status alone refused each case (measured on a mutant). Now a draft at version 1 decided on as 7, and a draft an owner reopened at version 3 decided on as 1, are both stale and untouched; cancelling a payment raises its version, and a new payment starts at 1. test/versions.db.test.ts races twenty cancels of one payment on one version over a pool of twenty connections to a real PostgreSQL: one is made, nineteen are stale (database tier, local PostgreSQL 17: 53 passed, and the race three more times on its own). And STALE_STATE no longer says the record's new version, so a caller reads the record again instead of retrying on a number it was handed. Red first: the message named version 2. pnpm check: 716 passed. DSOR-CON-01b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…never asked, and comments say what is true Start-up refuses an application that may insert a payment's version, which is the table's to give, and one that may change an invoice's vendor or amount, which it has never asked since step 10. Its messages say the application changes a status and its version. Migration 019 said the application may only raise the version; the grant allows any value from 1, and only the code raises it by one, which the comment now says, with what an owner's hand edit and a reset do. freshness.ts and a step-15 test said no invoice has a version until step 21; the row has one now, and the label does not repeat it, so DSOR-FRS-01a is met in part. The new helpers sat between idFrom and its doc comment; they sit above it. Red first: start-up passed the four new grants. pnpm check: 716 passed. DSOR-TEN-01a. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd the second told it is stale Decision 133 said the demo shows a stale version; it did not. A section after the keys': the agent makes a payment, user_123 cancels it on the version it was made at, and the agent, deciding on that same version, is refused STALE_STATE, retry after_state_refresh. The demo's tests pin it, and the log's counts grow by its three decisions. Red first: four demo tests. pnpm check: 717 passed. DSOR-CON-01b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e, and the refusal is checked word for word Breaking the step on purpose found two gaps. A cancel with the version taken out of its UPDATE passed every test, because the status alone refused each case, the 20-way race included. And STALE_STATE could name the new version in other words than "version 2". Now a draft payment decided on as version 7 is STALE_STATE and stays a draft, the message is compared whole, and the race test says it proves one change, not the version. DSOR-CON-01b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tatus row The README says what a version is, where a caller gets one, why STALE_STATE holds no number, the demo's two decisions on one payment, the database tier, three breaks measured twice on the final code (2, 6 and 1 failing, both runs the same), and what the step leaves. The note records the build, the review and what breaking it found. docs/status.md lists my_21 at 718 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copied without node_modules, .env or a local database, so this step's migrations can never run against step 21's Neon database. Only the package's name and description change. Everything else is byte for byte step 21's committed code, which passed 718 under pnpm check; the README is rewritten with this step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments in src and test only. Migration 019 keeps its NEW IN STEP 21: an applied migration is never edited. pnpm check: 718 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…26.2 kept by the database Migration 020 adds dsor.proposals, written once, and dsor.proposal_moves, its history, where a proposal's state is its last move. The map of §26.2 is a table, and every move must be on it; because the machine has no loops, a state is entered and left at most once, and a move out of a state needs the move into it. So a move off the map, out of a state the proposal is not in, or out of a finished state is refused by the database, and two moves out of READY cannot both be written. The application may add rows and nothing else; start-up knows eight tenant tables and refuses an application that may change a proposal, a move or the map. Nothing calls this yet. DSOR-APR-01a, DSOR-APR-01b, DSOR-APR-01c, DSOR-IDM-04. pnpm check: 735 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the door moves it A new stage, after the claim, writes the proposal: the payload as the text §21.6 wrote down, its hash, who asked as the decision record says it, and the key. §21.11 refuses a command with none. The door moves it: DENIED when a later stage refuses, READY and EXECUTING just before the handler, then COMMITTED on a receipt, FAILED on a refusal, and OUTCOME_UNKNOWN when the handler throws, answers OUTCOME_UNKNOWN, or answers with a row. A move before the handler that cannot be written means nothing runs, and the claim is released. The receipt names the real proposal; success() no longer counts its own, and refuses one from another company. The orderings test walks all 3,628,800 orderings one at a time. DSOR-APR-01a, DSOR-APR-01b, DSOR-IDM-04. pnpm check: 747 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…finished proposal refusing to move After the two decisions on one payment, the demo lists this run's proposals with their states, found by the run's keys because the store keeps every run's; prints the stale cancel move by move, with who made each move and why; and asks, with the application's own rights, to move a COMMITTED proposal back to EXECUTING, which the database refuses. A store read, proposalsOf, lists a company's proposals in number order. DSOR-APR-01a, DSOR-APR-01c. pnpm check: 748 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…al server; and a state is left once The database tier races twenty moves out of READY over a pool of twenty connections: exactly one is written. Twenty moves out of a COMMITTED proposal at once: none is. 55 passed on a local PostgreSQL 17 set up like Neon. And a unit test for the rule nothing else reached: two different moves out of one state, approved and rejected at once, of which one is written; with "left once" taken out of migration 020, it fails. DSOR-IDM-04, DSOR-APR-01a, DSOR-APR-01c. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ost reply is read back A review measured a proposal left READY for good when the move to EXECUTING failed, and one left EXECUTING with its handler never run when a reply was lost; each time the retry made a second proposal. Now both moves are one INSERT, all or none, so a failure leaves PROPOSED or EXECUTING. When the reply is lost, the door reads the proposal back: EXECUTING is its own move, and the handler runs; PROPOSED is DENIED and the claim released. And a test for the branch nothing tested: a door that finds its proposal already finished never calls the handler. DSOR-APR-01a, DSOR-IDM-04. pnpm check: 752 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…AILED only when it says nothing was done A review measured a careless handler that made the payment and then answered TRANSACTION_FAILED leaving its proposal FAILED, finished. The door already distrusted that answer for the claim. Now FAILED only for CONFLICT, STALE_STATE, RESOURCE_NOT_FOUND and VALIDATION_FAILED, the four this program's handlers give; any other code is OUTCOME_UNKNOWN. DSOR-UNK-01b, DSOR-APR-01a. pnpm check: 753 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…status row The README says what a proposal is, how its state is its last move, the two rules the database keeps, the demo's ten proposals and the stale cancel move by move, the database tier, three breaks measured twice (4, 1 and 9 failing, both runs the same), and what the step leaves. The note records the build, the review and what breaking it found. docs/status.md lists my_22 at 753 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copied without node_modules, .env or a local database, so this step's migrations can never run against step 22's Neon database. Only the package's name and description change. Everything else is byte for byte step 22's committed code, which passed 753 under pnpm check; the README is rewritten with this step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments in src and test only. Migration 020 keeps its NEW IN STEP 22: an applied migration is never edited. pnpm check: 753 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uments, and the claim keeps it The door reads a mode beside the key, once, its own field only; §21.6 checks it: none is execute, anything but execute, propose_only or validate_only is VALIDATION_FAILED and recorded, and a query has only execute. Migration 021 keeps the mode on the claim, so the same key in another mode is IDEMPOTENCY_CONFLICT, and start-up refuses an application that may change it. Nothing behaves differently by mode yet. DSOR-OPR-05, DSOR-IDM-01d. pnpm check: 761 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and nothing else A dry run skips the claim at §21.7 and the proposal at §21.8, as §21 says, so it needs no key and leaves no claim and no proposal. The limit is still asked and the decision still recorded, with the result VALIDATED, so the log never shows a dry run as something allowed to run. The door answers VALIDATED with the decision, ALLOW, and carries nothing out: no change, no payment, no payment number taken. A refusal is the one execute would get. The result envelope type now has room for READY and VALIDATED, which name no data. DSOR-OPR-06, DSOR-OPR-05. pnpm check: 766 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…EADY A command called propose_only walks the same checklist, its key claimed, its proposal made with mode propose_only, the limit asked, the decision recorded, and then its proposal moves to READY, "waiting to be released", and nothing runs. The answer is READY with the proposal, the fingerprint and the semantics, and no data. A refusal is DENIED, as for execute; a move to READY that cannot be written is read back as decision 137 does, and refused with the claim released. Releasing a prepared proposal is proposal.execute, step 31's. DSOR-OPR-05. pnpm check: 771 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y the third makes it A dry run is VALIDATED, "the decision would be ALLOW; nothing was done"; a prepared one is READY, with its proposal waiting to be released; the third is COMMITTED. The line under them counts the payments: none by the first two, one by the third. The demo's log has three more decisions, the dry run's among them, and the counts in main.test.ts say so. DSOR-OPR-05. pnpm check: 772 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…back is OUTCOME_UNKNOWN, and the claim is kept A review measured one propose_only request leaving two READY proposals: the move to READY committed, its reply was lost, the read-back failed too, and the caller was told "safe to send again" with the claim released. Now, when the read-back fails, the answer is OUTCOME_UNKNOWN, the claim keeps it, and the proposal waits for reconciliation; the same before a handler, where a proposal EXECUTING that never ran sat beside a second that did. Tests for a READY reply lost and read back, and both unknown cases; and prepare, like the door, leaves a proposal that was not PROPOSED alone. DSOR-IDM-01c, DSOR-IDM-04, DSOR-UNK-01b. pnpm check: 775 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at leaves (decision 157) The problem. The READMEs of steps 22 to 25 described a log of decisions and reads only, pasted demo output without the moves' records, and in steps 24 and 25 said that requests at once in one company mostly lose the race for its log and are refused: no longer true once a record that loses its place tries the next one. The change. Step 22: the new idea in plain words (each move written with its record by one statement, the creation's record found through its move, two requests at the end of the log), the problem it solves with its measured numbers, the demo's move-by-move block with each move's record and a printed-log excerpt, the What changed table, the database tier's four racing tests and the migration edited in place, Breaks 4 and 5, three more Check yourself questions, DSOR-AUD-01 among the rules, and the limits it leaves. Step 23: the log excerpt of the three calls, with the moves' records. Step 24: why the demo pays one after another, Check yourself 5 and its answer (the limit refuses forty-nine of fifty, measured), and the limit's last paragraph. Step 25: each cancellation's record, the revocation's "still what was read", the brake's two locks writing no record, the revocation's CONFLICT and its snapshot moment, and the agent's traffic against the brake (measured on PostgreSQL 17: the person's control.suspend landed on its first try in twenty runs, with one and with five agents sending without a pause). Test totals and the other Break-it counts are left for the lead; they are listed in the report. How it was proven. Every pasted line is from a demo run of this code. Breaks 4 and 5 were measured on step 22's full suite in a copy outside the repository, twice each, and the runs agreed: 16 failed | 811 passed | 1 skipped (828), and 9 failed | 818 passed | 1 skipped (828). pnpm guard passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and what was chosen 145 is the review and the learner's four choices; 146 to 156 are each step's findings, what was fixed, what was rejected, what is written down, and how each fix was proven by breaking it; 157 is the audit records of proposal moves, with the six questions it asked and the answers taken on the learner's standing instruction. Open question 81 asks where a transition record should say what moved, since the audit record has no field for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The my_ table gives the totals measured on the final code (829, 868, 910 and 1007), says that from step 22 every proposal move is written with its own audit record (decision 157), that only a payment books against the day (decision 155), and that a slip's signer, supervisor or approver, may revoke it whether it is active or suspended (decision 156). The notes hold 157 decisions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments in steps 20 to 25, and step 20's README, sent a dead key to reconciliation at step 37. The map makes step 37 outcome_unknown and step 38 reconciliation; decision 153 corrected the same number in steps 22 to 25's other words. Words only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt measured again After decisions 153 to 157, all fourteen breaks of steps 22 to 25 were run again on the final code, on the whole suite, twice, in a copy outside the repository; the passes agreed, and each target was found once. Each pasted Tests line is the measured one, and each break's words name the tests that fail now. The totals: 829, 868, 910 and 1007; the database tiers, run on a local PostgreSQL 17 set up like Neon: 57, 57, 59 and 72. The change tables of steps 22, 24 and 25 list the start-up test file, and steps 24 and 25 the modes test file, which each changed. Step 24's two races measured before decision 157 now say when they were measured. The step notes' headers give the same counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion 157 On PostgreSQL 17, with step 24's booking broken on purpose to read the total and then write it, fifty reservations at once still booked all fifty, three runs of three; but fifty payments through the door made one, where on 2026-10-11 they made three. Since decision 157 the fifty take turns at the company's log, and the turns keep the broken bookings apart, so only the store's test sees that break now. Step 24's README and note say so, and the note no longer says that fifty payments mostly lose the log's race. Step 24's change table lists the slip test that now asks the daily limit too. Step 25's note says a suspended slip can be revoked since decision 156. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de (decision 157) A review of decision 157 found that "a request that loses ten places is refused, with nothing done" is false for a proposal's last move: there the handler has run, the caller is told the payment stands, and the proposal stays EXECUTING in its history and in the log, for reconciliation, which step 22's own test pins. And "the log cannot be emptied with TRUNCATE" holds only for a TRUNCATE of the log by itself: the log and the moves together, or CASCADE, empty both. Words only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…his machine (decision 157) The problem. test/transitions.db.test.ts asked that at least 15 of twenty payments sent at once be carried out, on every server. The READMEs tell the learner to run the database tier on Neon, where every statement holds its place at the end of the log for several round trips, and more requests lose ten places in a row. Measured on PostgreSQL 17 with a pool of twenty, on this machine: 20 of 20, every run. With up to 30 ms before and after each statement, 12 to 15. With up to 10 ms on each round trip inside a statement's transaction, 8 to 11; up to 100 ms, 4 to 8. Nothing was wrong, and the test failed. A crowd also took 22 to 25 seconds at 100 ms a round trip, and 43 to 57 at 200 ms, against the tier's 30-second limit. The change, in each of the four folders: - "most of twenty are carried out" is asked only when DSOR_DB_URL's host is 127.0.0.1, localhost or ::1 (it.skipIf), and its comment says why, with the numbers. Elsewhere the report shows it skipped. - the test that runs on every server keeps every move with its record, every record with its move, the chain verifying, each proposal's records one request's, and now also asks that every refusal left its proposal DENIED or made none. It has two minutes instead of thirty seconds. The proof. On my own PostgreSQL 17: the database tier passes in all four folders (22: 57, 23: 57, 24: 59, 25: 72 passed), and with the host given as "localhost." it reports 1 passed | 1 skipped for this file. Over a simulated 30 ms link, unbroken: the every-server test passes, the bar fails with 8 of 20. With the door's move to DENIED removed (a mutant): the every-server test fails, "expected [ 'DENIED', undefined ] to include 'PROPOSED'". On this machine with no delay that mutant passes, because nothing is refused there. pnpm check: 829, 868, 910 and 1007 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pared with their request's decision (decision 157)
The problem. Decision 157's Q5: a move's record carries the identity of
the request that made it, the same as that request's decision record,
its slip included, and never dsor's. Only the four moves of a payment
carried out were compared with their decision. A review wrote the record
of the move to DENIED in dsor's name (the door, src/operations.ts) and
the whole suite passed; the same done to step 23's prepare, the record
of READY, passed too.
The change, in test/transitions.test.ts of each folder: a helper finds
the one decision in the log and the record the move names, and compares
the request, the login, the slip and the identity. A person's authority
is as of each record's own time, so that one field is the move's own; an
agent's is the directory's, the same in both. Two tests for DENIED: the
agent under del_100, refused LIMIT_EXCEEDED (unattended, subject
user_123, actor chain the agent), and user_123, whose move to READY and
EXECUTING the server refused (direct). In steps 23 to 25, two more for
prepare's READY: the agent's payment and user_123's invoice.issue, each
called propose_only.
The proof. Red first was not possible: the code is right. Broken on
purpose, the whole suite each time, then put back:
- DENIED in dsor's name ({ ...by, subject: "dsor" } in the door):
22: 2 failed | 829 passed (831); 23: 2 failed | 870 passed (872);
24: 2 failed | 912 passed (914); 25: 2 failed | 1009 passed (1011).
The two DENIED tests, and nothing else.
- DENIED without its slip ({ ...by, underSlip: undefined }), step 22:
1 failed | 830 passed (831), the agent's.
- prepare's READY in dsor's name: 23: 2 failed | 870 passed (872);
24: 2 failed | 912 passed (914); 25: 2 failed | 1009 passed (1011).
The two READY tests, and nothing else.
pnpm check afterwards: 831, 872, 914 and 1011 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a revocation, tested (decision 157)
The problem. When the statement that revokes a slip commits and its reply
is lost, recordTogether finds the first record of a cancel written and
hands back no rows, and revokeSlip answers "revoked", with what it read as
waiting (src/delegation.ts, `if (rows === undefined || row?.revoked ===
1)`). No test reached that half of the condition. A review changed it to
`if (row?.revoked === 1)` and the whole suite passed, while the revocation
that had committed answered AUTHORIZATION_DENIED ("only the person who
signed del_100 may revoke it here, and user_123 did not"), its own
proposal was recorded FAILED, and 40,000.00 USD stayed held against the
day.
The change, in test/revoke.test.ts: a test with one proposal waiting for
approval and one approved under del_100, 40,000.00 USD reserved for the
approved one. The store runs the statement that revokes and then throws
ECONNRESET, as a lost reply. The test expects the true answer, revoked
with 2 cancelled; the slip revoked; both proposals CANCELLED; nothing
held against the day; and the revocation's own proposal COMMITTED.
The proof. Red first was not possible: the code is right. With the
review's mutant, the whole suite: 1 failed | 1011 passed (1012), the new
test, at the answer. Measured on a copy with the mutant: the answer
AUTHORIZATION_DENIED, the proposal FAILED, usedToday 40000.00, the slip
revoked and both proposals CANCELLED. Put back: pnpm check, 1012 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The problem. Two comments in src/audit.ts said more than the code does. - recordTogether's said it returns "the records". In step 25 a record can wait on a condition (recordInsertIf): a brake that stops an agent's moves, or a revocation that revokes nothing, leaves a statement that succeeds and writes none of the records it was handed. The records returned were sealed, not written. - isItMine's said it asks "on the connection that just failed". Through a pool, each statement takes whichever connection is free, and one that is gone is dropped (database.ts), so the look-up runs on another, a new one if it must. Only PGlite's one connection is the same. The change. Step 25's recordTogether comment says the records are as they were sealed, and that only the rows, `moved` or `revoked`, say whether they were written; steps 22 to 24 write no record on a condition, and their comment stays. isItMine's comment, in all four folders, says what the store does, and keeps why a failed look-up is OUTCOME_UNKNOWN. Comments only; no code changed. The proof. pnpm check: 831, 872, 914 and 1012 passed. oxfmt --check passes on the four files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… every receipt (decision 157) A hostile review of 2f85d2c found three things in test/transitions.db.test.ts. The problem. - "Every refusal leaves its proposal DENIED or made none" asks a little more than the door promises: it gives DENIED two tries of ten places, and a proposal it could not deny stays PROPOSED (decision 153). The comment said nothing of that. - A refusal whose request matched no record passed as "made none", and nothing showed the matching worked. - "Up to 100 ms on each round trip" did not say the pause came before and after each one, and nobody had measured Neon. The change, in each of the four folders: the comment says what the door promises and that no run measured has come to it; every receipt's request must be found among the proposals, so a refusal found by none really made none; the latency comments say "before and after each round trip", and that Neon itself was not measured. The assertion stays DENIED or none, as the task asked; accepting PROPOSED would let a door that never writes DENIED pass. The proof. My own PostgreSQL 17: the database tier, 57, 57, 59 and 72 passed. On a copy, with up to 30 ms and 100 ms before and after each round trip: the every-server test passed, in 7.9 and 25.9 seconds. The unit tier does not run this file, and pnpm check's typecheck does: it passed in all four folders, with 833, 874, 916 and 1014 tests passed once the two commits after this one were in place too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…th its request's decision too (decision 157)
A hostile review of dcc3c06 found a third place that writes a move with
the request's words: the door's catch around a handler that throws, which
moves the proposal to OUTCOME_UNKNOWN. Its record in dsor's name left
every test green. Measured, step 22's whole suite with that mutant:
831 passed (831). The review also found the helpers trusting which record
a move names: a move that named its decision's record would have the
decision compared with itself.
The change, in test/transitions.test.ts of each folder: two tests, the
agent's payment under del_100 and user_123's invoice.issue, each through a
door whose handler throws, comparing the record of OUTCOME_UNKNOWN with
the request's decision. The helper that finds a move's record now asks
that it is a proposal_transition into that state, and the comparison
asks for the same operation and payload hash too.
The proof. With the review's mutant ({ ...by, subject: "dsor" } in the
door's catch), the whole suite: 22: 2 failed | 831 passed (833);
23: 2 failed | 872 passed (874); 24: 2 failed | 914 passed (916);
25: 2 failed | 1012 passed (1014). The two new tests, and nothing else.
Put back: pnpm check 833, 874, 916 and 1014 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…157) A hostile review of 0257c3c found two wordings in test/revoke.test.ts that said something else. "With that test deleted" meant half of an `if` in revokeSlip, and read like a test case. "The log says it was done" sat over an assertion that reads the proposal's state, not the log. The change: "with that half of its condition deleted", and "COMMITTED, not FAILED: it was done". Comments only. The proof. pnpm check in step 25: 1014 passed. oxfmt --check passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the build The crowd test asks "most of twenty" only of a server on this machine now, so step 22's README says why: over a link with delays added on purpose, as Neon's would add them, 8 to 18 of twenty got through. Step 23's change table lists the transitions test that compares a prepared READY's record with its decision. Decision 157 says its race was measured on this machine, that a last move which loses its places leaves the proposal EXECUTING after the payment stands, and writes down what the review found and left for later: the revocation's parameter limit for step 29, a move that can name another company's record, step 24's broken booking now hidden through the door, and two comments in older steps. The status page gives the totals after the review's tests: 833, 874, 916 and 1014. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…view of decision 157 The review's tests moved every total: 833, 874, 916 and 1014. All fourteen breaks of steps 22 to 25 were run again on the final code, twice, in a copy outside the repository, and the passes agreed. Three breaks now fail more tests, and say which: step 22's Break 3 fails forty, three of them the records of a refused or unknown move; step 23's Break 3 nineteen, two of them a prepared READY's record; and step 25's Break 3 eight, one of them the revocation whose reply was lost. The step notes give the same counts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch added six open questions numbered from 76 while staging, through pull request #7, numbered its own from 76 to 123. Ids are never reused, so this branch's six follow staging's: 124 to 129, and the seven references to them in the decisions log follow too. No question's words change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… again Staging gained pull request #7, the mj_ track's steps through 30, and pull request #6 could no longer be merged into it, so GitHub ran no CI for its last push. One file conflicted: research/open-questions.md, where both branches had added questions after 75. Staging's 76 to 123 come first, and this branch's, renumbered in the commit before this one, follow as 124 to 129. docs/status.md merged by itself: staging's mj_ rows, and this branch's my_ rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…for its twin, and a company's log takes one writer at a time The database tiers of steps 16 to 25 ran against Neon for the first time, 127 ms from this machine. Steps 20 to 25 failed their tests of twenty and fifty requests at once: two settings had been measured on a PostgreSQL on this machine. Decision 158 records the problem, the two changes, what two hostile reviews found and what was chosen, and notes on decisions 131 and 157, whose numbers it changes. Open questions 130 (a person's brake waits behind the agent's turns, beside DSOR-OPS-01b and DSOR-DEL-04b) and 131 (a waiting retry whose own refusal cannot be recorded, §28) are left for the specification. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ing carried out (decision 158) On Neon, 127 ms away, fifty requests with one key made one payment, but the forty-nine others stopped waiting after two seconds, and 47 or 48 of their refusals then lost their place in the log. A request now waits up to thirty seconds, and the wait ends as soon as the answer is there. A look that fails after the first request was seen running, or after its key was found taken, is told OUTCOME_UNKNOWN, never that nothing was done. Tests: the thirty seconds shown by moving the clock at the first look; a failed second look and a failed first one; the file of the key waits two seconds, so no test leaves a request running into the next; the database test of fifty gets three minutes. Breaks 1, 3 and 4 measured again: more tests notice them now, and the README says why. DSOR-IDM-01b, DSOR-IDM-01c, DSOR-UNK-01b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n 158) The wait for the same request still being carried out, the answers to a look that fails, their tests, and the database test's three minutes, as step 20 has them. Break lines measured again on the new total; none moved. DSOR-IDM-01c, DSOR-UNK-01b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Neon, 127 ms away, fifty reads sent at once by one company lost their place at the end of the log so often that 36 to 39 were refused after ten tries. Now a writer waits for its company's turn, reads the end, writes after it and commits. The turn is an advisory lock taken with the company in the turn's first statement, at READ COMMITTED, and ended by PostgreSQL after ten seconds idle; inside the program each log has a line that waits without a connection and lets the next request go once a turn is taken. The ten places stay, for a writer that takes no turns. A statement is built before its records are said to be sent. Tests: the pool's turn pinned on a stub pool (READ COMMITTED, ROLLBACK, the line); on a real server, a writer waits for the turn and reads the end only then, another company is not kept waiting, and a quiet turn ends; "most of twenty, on this machine" becomes "all twenty, on any server"; two race tests race again through a database that takes no turns. Carried from step 20: the wait. DSOR-AUD-01, DSOR-TEN-02a, DSOR-IDM-01c, DSOR-UNK-01b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ard (decision 158) As steps 20 and 22 have them: the company's turn at the log, the program's line, the isolation level and the idle limit, their tests, and the wait for the same request. Break lines measured again on the new total; none moved. DSOR-AUD-01, DSOR-TEN-02a, DSOR-IDM-01c, DSOR-UNK-01b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ard (decision 158) As steps 20 and 22 have them, and the limits' tests of fifty at once get three minutes for a database far away. A request that books and cannot write its decision is said to have failed to write it, not to have lost ten places. Break lines measured again on the new total; none moved. DSOR-AUD-01, DSOR-TEN-02a, DSOR-DEL-06a, DSOR-IDM-01c, DSOR-UNK-01b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ard (decision 158) As steps 20, 22 and 24 have them. The agent's traffic can delay a person's brake now, not make it lose its place: the README says so, beside DSOR-OPS-01b and DSOR-DEL-04b (open question 130), and the revocation's comment says the turns close the moment a move into waiting could slip past it. Break lines measured again on the new total; none moved. DSOR-AUD-01, DSOR-TEN-02a, DSOR-OPS-01b, DSOR-IDM-01c, DSOR-UNK-01b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n 158 The my_ track's counts for steps 20 to 25, a word on each of the two changes in the rows of steps 20 and 22, a note on the first run against Neon, 158 decisions, and the date. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Asked in plain words with the trade-offs: a company's log takes one writer at a time, and a repeat of a request waits up to thirty seconds for the first. Both were chosen, and both are what is built. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Wania-Kazmi
marked this pull request as ready for review
October 14, 2026 22:43
mjunaidca
added a commit
that referenced
this pull request
Oct 16, 2026
… 158 Staging gained pull request #6, whose open questions 124 to 131 took the numbers step 31 had used. Step 31's three questions become 132 to 134, and its README, the status row, and the notes now cite those numbers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Decision 158: on a database far away
Problem. The database tiers of steps 16 to 25 ran against Neon for the first time on 2026-10-14, 127 ms from this machine. Until then they had run on a PostgreSQL on the same machine. Steps 20 to 25 each failed one to three tests, every one a test of twenty or fifty requests at once. Fifty requests with one key made one payment, but the forty-nine others stopped waiting after two seconds, and 47 or 48 of their refusals then lost their place in the audit log. From step 22, fifty requests from one company raced for the end of its log, and 36 to 39 of fifty reads were refused after ten tries; in steps 24 and 25 a raw
audit_pkeyerror came out ofproposewith no envelope. Both settings had been measured where a round trip takes a fraction of a millisecond.Solution. From step 20, a request waits up to thirty seconds, not two, for the same request still being carried out; the wait ends as soon as the answer is there. A look that fails after the first request was seen running, or after its key was found taken, is
OUTCOME_UNKNOWN, never "nothing was done". From step 22, each company's log takes one writer at a time: a PostgreSQL advisory lock, taken with the company in the turn's first statement, at READ COMMITTED, ended by PostgreSQL after ten seconds idle, behind a line in the program that waits without a connection and lets the next request go once a turn is taken. Steps 20 and 21 keep step 09's honest refusal of a lost place. Two hostile reviews of the fix found problems, all fixed and each broken on purpose to show its test catches it, except three written down: open question 130 (a busy agent can delay a person's brake past §44), 131 (a waiting retry whose refusal cannot be recorded), and how refusals arrive during an outage.Behaviour. Every database tier from step 16 to 25 passes on Neon, none skipped: 39, 51, 51, 51, 52, 53, 60, 60, 62 and 75 tests. "Most of twenty, on this machine" became "all twenty, on any server". New tests pin the turn on a stub pool and on a real server, show another company is not kept waiting, and that a quiet turn ends. Unit tests: 735, 754, 840, 881, 923 and 1021. All twenty-two Break it exercises of steps 20 to 25 were measured again, twice; only step 20's Breaks 1, 3 and 4 moved, each explained.
The second review of steps 15 to 25
Problem. On 2026-10-12 the learner asked for a review of steps 15 to 25. Every step passed its own
pnpm checkin CI and its database tier on PostgreSQL, but a hostile review, one reviewer for each step, three more across the steps, a skeptic for every serious finding and a critic, found 119 problems that held: 5 high, 40 medium and 74 low. Twelve were the code doing the wrong thing. The worst: in step 20, a retry that decision 132 had added let one idempotency key make two payments when a release's reply was lost (measured, 9 runs of 12); and from step 22, no proposal move was written to the audit log, thoughDSOR-AUD-01asks for it and step 08 had handed that job to step 22. Most of the rest were tests that still passed with their guarantee removed, and sentences that said more than the code.Solution. The learner chose to fix everything, with four recommendations: build audit records for proposal moves; let an approver revoke a slip they signed; write down four start-up gaps that need an administrator's mistake, for step 26; and reword step 25's revocation sentence rather than add a second slip check (decision 145). Each finding was fixed in the step where it began and carried into every later folder, red first or broken on purpose (decisions 146 to 156). Decision 157 writes every proposal move with its own
proposal_transitionrecord in one statement; the database refuses a move without one; and a record that loses its place in its company's log tries the next place after a short random pause, up to ten. An independent review of decision 157 found the crowd test likely to fail on Neon, two overclaiming sentences and two untested guarantees, all fixed, and a revocation limit that step 29 must handle, written down. The specification's questions are open questions 124 to 129.Behavior. Every step passes
pnpm check, and its database tier on a local PostgreSQL 17 set up like Neon, with every demo running against it. All 65 README breaks were measured again on the final code, twice, and the passes agreed.On a PostgreSQL 17 on the same machine, twenty payments sent at once now all go through with every move recorded, where 3 of 20 did before. Each step's own section below gives its counts as it was built.
CI: one job per step folder
Problem. CI checked the baby steps in one job per system, one folder after another. From
my_09on, eachmy_folder runs its test files one at a time, and each step takes longer than the one before, so the job grew with every step. With 40 to 45 folders it took from 35 minutes to more than 60, and from 41 folders it was cut off at 60 on Windows, so the newest steps went unchecked: Windows had never reachedmy_23,my_24ormy_25.Solution. A first job lists every folder with a
package.json. Every folder then gets a job of its own on Ubuntu and on Windows, at most 12 at once, because the organization's Free plan has 20 runners for all of its repositories. A job named "Baby steps" passes only when every one of them did. A new step is checked without editingci.yml, up to 128 folders. A hostile review of the change, five reviewers with a skeptic for each finding, found what the folders would meet on Windows for the first time. The ten-stage ordering walk inmy_22tomy_24would run past its 30-second limit, so it now has two minutes of its own, with the proof unchanged (decision 144). And a fresh Windows machine once took 33 seconds over its first TypeScript run, so each Windows job now runsnodeon TypeScript once, timed, before the tests. The review also corrected the comment's numbers andmy_01's README, which said CI does not run its tests.Behavior. The first run passed: 95 jobs, 19.5 minutes from the first job to the last (run 38256321861). A folder took 15 seconds to 7.2 minutes on Ubuntu, and 51 seconds to 8.9 minutes on Windows, where
my_25ran for the first time. The ordering walk took 43.2, 33.8 and 33.6 seconds on Windows inmy_22,my_23andmy_24: without its own limit all three would have failed. The warm-up took 0.1 to 2.7 seconds on all 45 Windows machines, so there was no stall this time. The test closest to its limit ismy_21's start-up check, at 22.5 of 30 seconds on Windows. Not changed: every job still runs the repository's own install, and every push checks every folder; and every job checks its lockfiles against the npm registry again, about 180 checks a push where the old job made 12.Step 25: revocation and the emergency brake
Problem. When the agent misbehaved at 3 a.m. there was no switch: user_123 could not tear up del_100, nothing stopped one agent or every agent at once, and asking the agent to stop is asking the thing that misbehaves to behave.
Solution. The brake is a record in DSoR's own store (migration 024; decision 142):
control.suspendstops one agent in a company, or every agent in it, and releases it. A new stage, §21.4, refuses a stopped agent's commandsAGENT_SUSPENDEDand lets its reads through; the move to EXECUTING, and a prepared command's to READY, ask the brake again in the same statement.delegation.revokemarks a slip revoked with who and why, and in one statement cancels what waited under it for approval, releasing its reservations (migration 025). The review found a refusal recorded as ALLOW, the brake optional on the way into EXECUTING, and a prepared command not stopped, all fixed: who may revoke and who may release are decided at a new §21.9 stage and recorded (decision 143). The stage-order test became a proof by its parts, since walking all orderings of eleven stages took 65 seconds. Written down: a question for the specification aboutDSOR-SRC-02band a revocation's target; the tenant administrator and the expiry half ofDSOR-DEL-04c, not built.Behavior.
pnpm check: 876 passed. Database tier: 69 passed on a local PostgreSQL 17 set up like Neon. Three README breaks measured twice, agreeing (5, 1, 2 failing); decision 143's six broken once each, all as predicted. Stage 3 of the learning path is complete.Step 24: limits with reservations
Problem. del_100 limited each payment to 50,000.00 and nothing else, so the agent could make any number of them in a day; and a daily limit is a running total with a race, where fifty payments at once each read the total and find room.
Solution. The slip gets a daily limit, 200,000.00 USD for del_100 (migrations 022, 023; decision 140). §21.10 books each payment against the slip's total for the UTC day in one
INSERT ... ON CONFLICT DO UPDATE ... WHERE used + amount <= limit, writing a reservation keyed by the proposal from the same statement. The door settles it as the proposal ends: committed, released, or held while unknown or prepared, and only when the history holds the move. A dry run books nothing. The review found the settle running without the move, a race test that could not fail, andreserve()taking any currency, all fixed; and that the application's own code could still make room, written down with why: a payment is not tied to its reservation (decision 141).Behavior.
pnpm check: 806 passed. Database tier: 57 passed on a local PostgreSQL 17 set up like Neon, five runs in a row; fifty reservations at once book exactly forty, and with room for one, fifty payments through the door make at most one, while a read-then-write booking made three. Three README breaks measured twice, agreeing (1, 4, 4 failing).Step 23: three ways to call
Problem. The agent could only find out whether a payment would be allowed by sending it, and every allowed one was made at once; nothing could be prepared now and released later.
Solution. A command is called
execute,propose_onlyorvalidate_only, said beside its arguments as the key is, read once, its own field only (decision 138). A dry run skips the claim and the proposal, as §21 says, still asks the limit, is recorded asVALIDATED, and answers VALIDATED, ALLOW, with nothing changed. A prepared command walks the same checklist and stops at READY. The claim keeps the mode (migration 021), so the same key in another mode isIDEMPOTENCY_CONFLICT. The review found one request able to leave two READY proposals, and a mode on the request's prototype executing, both fixed (decision 139). A dry run skips the handlers' preconditions until step 32, written down and pinned by a test.Behavior.
pnpm check: 779 passed. Database tier: 55 passed on a local PostgreSQL 17 set up like Neon. Three README breaks measured twice, agreeing (8, 1, 6 failing); decision 139's three broken once each, all as predicted.Step 22: proposals and their states
Problem. In the morning nobody could say what became of the agent's 2 a.m. request: the answer was spread across the audit log, an idempotency claim and a payment row, if there was one, and nothing kept a request's state from moving backwards.
Solution. Every command past its key's claim becomes a proposal at §21.8 (migration 020, decision 136): its payload as the hashed text, who asked, its key, and a history of moves, each with an actor and a cause; its state is its last move. The §26.2 map is a table and every move must be on it; the history is one line, a state entered once and left once, so a finished proposal cannot move and one cannot run twice, kept by the database and asked about at start-up. The door moves it: DENIED, READY and EXECUTING together, then COMMITTED, FAILED or OUTCOME_UNKNOWN; the receipt names the real proposal. The review found a proposal that could be left READY for good and a careless handler's failure believed, both fixed (decision 137).
Behavior.
pnpm check: 753 passed. Database tier: 55 passed on a local PostgreSQL 17 set up like Neon, including twenty moves out of READY at once over a pool, one written. Three README breaks measured twice, agreeing (4, 1, 9 failing); decision 137's three broken once each, all as predicted.Step 21: optimistic concurrency
Problem. The agent reads a payment, decides to cancel it, and meanwhile a person changes it. The agent's command ran anyway, on a record that was no longer the one it decided about, and nothing in the request said which version that was.
Solution. Invoices and payments carry a version, raised by one in the same UPDATE as every change (migration 019). Every command's contract declares its strategy:
invoice.issueandpayment.cancelareoptimistic,payment.createisnone, and the registry refuses any other (decision 133). An optimistic command must sayexpected_version, a whole number from 1 to 2147483647, asked at §21.6 so that a missing one is recorded as a DENY. The check is part of the change,... AND version = $3, and a record that moved on isSTALE_STATE, retryafter_state_refresh, before any business rule, with no new version in the message. The review found a version too large for the column that made the door throw, a declared strategy nothing read, and tests the status alone could pass, all fixed (decision 134). Breaking the fixes on purpose found two more weak tests, both fixed (decision 135).Behavior.
pnpm check: 718 passed. Database tier: 53 passed on a local PostgreSQL 17 set up like Neon, including twenty cancels of one payment on one version over a pool, one made. Three README breaks measured twice, agreeing (2, 6, 1 failing).Step 20: idempotency keys
Problem. A payment whose answer is lost is sent again by the agent, and DSoR made a second payment: nothing in a request said DSoR had seen it before.
Solution. Every command carries an idempotency key beside its arguments, never inside them, and DSoR claims it at §21.7 with one INSERT into
dsor.idempotency_keys, under a primary key on the company, the caller who logged in, the operation and the key (decision 131). The same key and request get the recorded answer word for word, with nothing run and no second decision recorded; the same key with a different request isIDEMPOTENCY_CONFLICT; the same request still running is waited for. The review found three broken guarantees, all fixed (decision 132): only a refusal from the walk releases a claim, so a handler's "safe to send again" after a payment no longer reopens it; a restrictive policy keeps an answer once written, start-up asks for it, and a replay is checked against its caller; and the claim's messages say only what is known.Behavior.
pnpm check: 702 passed. Database tier: 52 passed on a local PostgreSQL 17 set up like Neon, including fifty requests with one key over a pool of twenty connections, one payment, run eight more times. Five README breaks measured twice, agreeing; decision 132's six broken once each, all as predicted.Step 19: unattended mode and the role source
Problem. At 2 a.m. the agent's payment under
del_100was recorded as the agent acting for itself,direct, with no slip, and whatuser_123holds came from a list that never changes while the program runs: whenuser_123changes jobs, the agent keeps paying under their name.Solution. An agent's command under a slip is
unattended: the slip must allow that mode, the record's subject is the slip's signer, taken from the slip and never from the request, the agent is in the actor chain, and the slip is named on the record, inside the hash (decision 129). What the signer holds is asked of the company's directory, a fake one per company, at every decision; a directory that does not answer, or none at all, refuses withDEPENDENCY_TIMEOUT. The review found three broken guarantees, all fixed (decision 130): a future-dated or zone-less directory answer counted as fresh, so now an answer counts only with its zone and at most 24 hours old, and a stale or unusable one isFRESHNESS_UNSATISFIABLE; refusals after DSoR found the slip were recorded as the agent acting for itself, and are recorded under the slip now; and a column-level UPDATE on the log passed start-up. A slip allowsunattendedonly when it says so, and none is signed by its own agent.Behavior.
pnpm check: 673 passed. Database tier: 51 passed on a local PostgreSQL 17 set up like Neon, seventeen migrations. Six README breaks measured twice, agreeing; decision 130's nine broken once each, all as predicted.Step 18: delegations
Problem. The agent held power of its own: its role granted it the commands outright, nobody had signed for what it did, and when
user_123lost a permission the agent kept it.Solution. The permission slip,
dsor.delegations, in DSoR's own store under the same lock. Every command the agent sends runs under the one active slip DSoR finds for it; its power is computed at each decision as the slip's permissions cut down to what the signer holds right then and to the login's scopes, up to the slip's limit per payment (decision 127). The review found four ways past an expiry or a limit, all fixed (decision 128): an expiry the program cannot read is not in force, a slip or a signer that cannot be read is a refusal that is recorded, DSoR never chooses between two slips, and a helper function's owner is asked what the application is asked. A payment over the limit is nowLIMIT_EXCEEDED, the specification's code.Behavior.
pnpm check: 645 passed. Database tier: 51 passed on a local PostgreSQL 17 set up like Neon. Eight README breaks measured twice, agreeing; decision 128's eleven broken once each, all as predicted.Step 17: vendors and payments
Problem. No command said whether its effect could be undone, and there was nothing to undo: the running example's PAY-901 had nowhere to live.
Solution. Vendors and payments, under the same two locks as the invoices.
payment.createmakes a draft and iscompensatable, undone bypayment.cancel, which isatomic; every command's contract declares its semantics, the door writes them on every receipt from the contract, and the registry refuses an undo that is not a command the program has (decision 125). The review's eight findings were all fixed (decision 126): a payment's vendor is its invoice's by the database too, the payment numbers are nobody's to choose, a command that answers like a query is refused, a payment is in its invoice's currency, and the handler reads exactly what the payload hash describes.Behavior.
pnpm check: 608 passed. Database tier: 51 passed on a local PostgreSQL 17 set up like Neon. Ten README breaks measured twice, agreeing; decision 126's seven fixes broken once each, six as predicted and one that failed three tests instead of one because the break changed two things.Step 16: the control-plane store
Problem. DSoR's log, its only paperwork in the database, was
public.audit, filed beside the business's invoices in the schema the business's own tools treat as theirs. A cleanup ofpublicwould have taken DSoR's evidence with it.Solution. A second schema,
dsor, in the same database, and the log moved into it as it is: records, hash chain, grants and row-level security together (decision 122). The application may usedsorand create nothing there. Two hostile reviews followed (decisions 123 and 124), and start-up now also refuses an application that owns the database or either schema, or may create in either, by itself, oneSET ROLEaway, or through a helper function's owner.Behavior.
pnpm check: 548 passed. Database tier: 39 passed on a local PostgreSQL 17 set up like Neon; on Neon,dsor_step16waits for the folder's.env. Nine README breaks measured twice, agreeing.Step 15: freshness labels
my_15_freshness_labels. Every answer says how old its data is: the mode, when it was read, and from where (DSOR-FRS-01a), and the door refuses acurrentlabel stamped before the request began (DSOR-FRS-01b). The problem first, on step 14's demo, run on the local database so step 14's Neon database was not touched: the agent's INV-1008 saysissued, with no time and no source, so an agent's hour-old memory and a fresh read look the same. Four decisions taken by the learner before any code (decision 120): the modes in lowercase, as the schemas spell them, against §27's capitals (open question 51); the code that reads writes the label, and the door insists on one; an old value labelledcurrentis refused, not relabelled; noresource_versionuntil step 21. Four pieces, each red first and committed alone: the label, insrc/freshness.ts, with the two reads handing back their rows with it; the door insisting on a well-formed label; never an oldcurrent; and the demo printing the label under every read.The hostile review, told this time to assume a careless handler as decision 119 says, found no way past the door and no wrong label on a real read, and three fixes the learner chose (decision 121): a query that answers with a receipt is refused, since a receipt carries no label; the label's time is taken just before the query, so data is at least as fresh as it says, and only an exact ISO time not in the future is accepted; and four guards got a test, with
leaveTheDoorkeeping only a label's three parts. Written down and not fixed: the door checks when a label was stamped, not whether the code that stamped it read the database, so the cache a later step adds must label its own answers; and a clock moved backwards refuses an honest read with retrynever.The step's own database,
dsor_step15, was made on Neon with oneCREATE DATABASEthrough the owner login in the copied.env. The settings refuse Claude any command that names.env, so the learner changed the database name in it.531 tests, 39 in the database tier on Neon, and ten breaks measured twice with the runs agreeing. Learning-path stage 2 is complete.
Step 14: classification and masking
my_14_classification_and_masking. Every field has a label, the agent has a clearance, and a field above it is taken out of the answer before the answer leaves, and listed (DSOR-CLS-01, 02a, 02b, 03, 05). The problem first, on step 13's running demo: the supervisor's and the agent's reads of INV-1008 print the same line,31400.00 USD, and the agent's line goes to a model provider. Three decisions taken by the learner before any code (decision 105): one file for all labels, with a field missing from it confidential; a field above the clearance left out and listed, not masked with a placeholder; agents only filtered, the agent cleared forinternal. Four pieces, each red first and committed alone: the labels and the clearance; the filter at the door, for one invoice, a page and a command's receipt, with the answer labelled by the highest label among what is left; the record of a confidential read — a second audit record after the decision, with every address returned and the row count, written before the answer leaves and refusing the rows if it cannot be written; and the demo's two lines that are no longer the same.What the build found (decision 106): the record of a read has to be a second record, because the decision is written before the handler runs and the log is never amended; and a column-level GRANT does not grow with the table — migration 006's first version added two columns and every INSERT into the log was refused.
The hostile review, six agents and three verifiers. Five reviewers found ten guarantees no test pinned, each now a test that fails under the review's own mutant: a page filtered row by row, a row with no address staying locked, the human's answer whole, the fourth label reaching the door, the ceiling measuring what leaves, the rebuilt receipt validated again, a page's cursor going with its addresses, a row with no address refused as the program's own error, a store down for the look-back too, and an empty page's label. The critic then found two things that changed the design (decision 107): a label describes a value it can see the whole of, so a value with parts inside is confidential whatever its field is called — a reviewer got the amount out inside a
vendorthat was an object — and an answer the door cannot filter is an envelope with a code and a retry class ofnever, where it had thrown a raw TypeError after the side effect. A receipt with nodata, legal in the schema and what step 17's first PENDING_APPROVAL receipt will be, leaves as it came. The break measurement then caught the first of those fixes weakening the suite: with every object treated as unseeable, the table's ownamount: confidentialstopped mattering, so money is the one compound value the rule accepts and that break fails thirty-three tests again instead of two.One finding is recorded rather than fixed (decision 108): five endings leave one
ALLOWrecord and nothing else — an agent's answered read, a bad limit, an answer over step 13's ceiling, a row with no address, and the evidence store failing — so the log cannot tell a read that was answered from one refused after its decision. Measured against the rules nothing is missing, becauseDSOR-AUD-01asks for command decisions, proposal transitions andDSOR-CLS-05reads and all of those are written; "and then what happened" is a proposal's final outcome, two stages away; and the ending where the evidence store fails cannot be recorded by anything.After the step, decision 109, the learner's choice: the record of a read kept its row count under
extensions, because the build believedaudit-record.schema.jsonhad no field for it. It has one,row_count. Migration 007 adds the column with its column-levelGRANT, and the count moved there, red first. The tests changed and did not grow. Broken on purpose: no count written fails 3, a read-back that drops it fails 7, and 007 without itsGRANTfails 185 (187 once decisions 110 and 111 added two tests), because every INSERT names the column. Also corrected after the step: two citations of §4 that should say §6, a comment inboundary.tsthat described a rule the filter now enforces, and the counts indocs/status.mdand the step note.After the step, decisions 110 and 111, both the learner's choice after a run showed the leak. Money,
{ value, currency }, kept its field's label wherever it appeared, so a handler that put the amount invendorsent it to the agent labelledinternal, with nothing in the list. It now keeps its field's label only in a field declared to hold money, todayinvoice.amount, as the specification's entity schema declares each field's type; anywhere else it is a value with parts inside, and at least confidential (decision 110). AndlabelOfwalked the prototype chain:labelOf("invoice", "toString")returned a function, which is never above a clearance, so fields namedtoStringandvalueOfleft for the agent with the amount in them. It asks for the table's own names now, the fix decision 36 made for roles, in a file written after it (decision 111). Each red first, in commits of its own, and each guard broken alone. Break 13 had been measuring another guard: the function it breaks was also the door's "is this a row?" check, so all 65 of its failures were the door refusing every row. Those are two functions now, and Break 13 fails 4, all about the label. A hostile review of the two found no new way past the filter through a row. It found two parts of an answer the filter never looks at,askedByand a page'snext— a gap from the step's first build — two guards no test pins, and sentences that said more than was measured. The sentences are corrected; the rest is decisions 112 to 114.After the step, decisions 112 to 119, the learner's choices on what three more hostile reviews found. The door writes
askedByitself on every way out, and refuses a page whosenextis not the address of its last row. Before, a handler that put the row in either sent the amount to the agent, withamountlisted as withheld beside it (112). The money rule is a function a test can reach, and the door's row check is tested with money, as the agent too (113). The door copies a handler's answer once, and the check, the filter, the ceiling and the record of the read all work from that copy: a cursor that answered the check and the filter differently got the row out, and a record of a read named INV-1009 while INV-1008 was returned (116). A receipt is built from the parts its schema closes and is always checked; a row underrequiresorextensionshad left beside a filtereddata(117). Three guards the reviews found removable got a test (118). Written down and not fixed: two checks that fail open where nothing in this step reaches (114); an error's envelope, whose open parts take anything while its message is free text (115); and four ways past the copy that each need code written on purpose to trick the door, which could write the amount as text into a field anyway (119). The reviews had been told to assume the handler was trying to get the amount out, a stronger threat than this step is about, and decision 119 says so. Each fix red first, in commits of its own, with each guard broken alone.512 tests, 39 in the database tier on Neon with migrations 006 and 007, and the fifteen breaks measured twice after decision 118, the two runs agreeing. The limits are in the README: no egress policy, no tokens, no budgets, humans unfiltered, error answers unfiltered and unlabelled, an unknown entity blank for an agent.
Step 13: bounded queries
my_13_bounded_queries.invoice.list, the first operation that returns many rows, with the size of the answer the server's (DSOR-QRY-01). Measured first on a copy of step 12 with a list written the obvious way and fifty thousand invoices seeded:{ limit: 1,000,000 }returned all 50,002 rows, three megabytes, in a tenth of a second, and nothing said no. Three decisions taken by the learner before any code: the next page comes after the last invoice on this one, as a cursor; the cursor is an address, so the §21.6 scan refuses one from another company and step 12's suite tests the list with no edit; two maxima and a default in one file, with the handler's SQL carryingLIMITand the door measuring every answer against both maxima and refusing an oversize one as the program's own error. Three pieces, each red first and committed alone. The test that matters is a connection that counts what every statement returned: a list that fetched the whole table and cut the page afterwards passes every other test.A hostile review (four reviewers, a mutation pass of thirty-two one-line changes, a critic, three verifiers) found eight holes, each closed with a test that failed first: the counting connection was blind to an extra unordered read; error answers were exempt from the ceiling; the byte ceiling was proven with ASCII only, so a copy that counted characters passed; the list's own
WHEREcould be deleted with every test green because RLS filtered anyway; the walk trusted JavaScript's sort over the database's order; the demo pin carried a rule id it could not prove and a label that did not come from its request;NaNwas refused as "null". One design suggestion declined and recorded (decision 104): the page is not cut by bytes; a row wider than the ceiling refuses, and the README says so. 468 tests, 36 in the database tier on Neon, eleven breaks measured twice with the runs agreeing.Step 12: cross-tenant test suite
my_12_cross_tenant_test_suite. One generated test that calls every operation in the registry with another company's address: each contract carries an example request underextensions["com.panaversity.tutorial"], the suite moves every address in it toorg_789and asks six questions of every operation — refused withTENANT_MISMATCH; the same refusal, whole, for a real and a fake company and nothing about yours; the other company holds every number the example names, in the same state for a command; its rows untouched; exactly one record for the request in the caller's log, theDENY; the example works for its own company and carries nothing of the other. An operation without an example, or with an example that names no address, fails by name. Built explain → ask → build: the problem measured first (a careless third operation leakedorg_789's invoice while step 11's hand-written suite stayed green), two decisions taken by the learner, three pieces red first.A hostile review (four reviewers, a mutation pass, a critic) found the first version could be lied to — a refusal-shaped envelope carrying the row with a cosmetic
DENYappended, a success answer carrying the other company's row, assertions deletable without a failure, a seed row that made a question hollow — and every finding is closed with a test: the six questions are plain functions andcross-tenant-suite-itself.test.tsfeeds each the lie it exists to catch. 444 tests, 30 in the database tier on Neon, sixteen breaks measured twice with the runs agreeing. The map's "fresh Neon branch" is recorded as a divergence (decision 99).Problem
Step 10 keeps the two companies apart with a
WHERE tenant_id = $1in every query. That is one lock, held by the program alone. Measured on step 10's own database asdsor_runtime, a query that forgot the company returned both companies'INV-1008— and all 376 tests were green, because they only check the queries that exist today. The leak is the query somebody writes next year.Solution
my_11_row_level_security, a learner build of step 11, copied frommy_10_tenantsand built explain → ask → build: the problem shown from the running program first, three decisions put to the learner one at a time before any code (both tables get the lock; the company is said before each statement, in its own transaction; the demo runs the forgotten query), then five pieces, each red first, committed on its own, and broken on purpose.public.invoicesandpublic.audit, with one policy each: a row is visible, and may be written, only when its company is the one the transaction said. ACHECKties a record's chain to its company.theDatabase(tenant), which refuses an empty company. Two adapters run each statement inside its own transaction afterset_config('dsor.tenant_id', $1, true), for PGlite and for apgpool — where a per-connection setting would be met again by a stranger's statement. The stores' SQL is unchanged.BYPASSRLS; membership of a role that holds it (Neon'sneon_superuser, refused by name); ownership of a tenant table or membership of its owner; whether the lock is on at all, and is exactly the one policy the migration wrote; aSECURITY DEFINERhelper whose owner skips the lock; and any relation beside the two tenant tables the account may read.dsor_runtime. PGlite's connection is a superuser, and a superuser skips every policy: with a store that forgets the company, the tests as the superuser fail 28 of 400 and none is a store's own test; as the application, 131. Sixteen tests that ran raw SQL as the application without a company went red when the lock arrived and now say it, or run as the owner.Rules: DSOR-TEN-01b, DSOR-RP-01a, DSOR-RP-01b, DSOR-RP-01c, DSOR-RP-01d, each with its honest scope in the README. 412 tests; runs from a clean copy outside the repository.
Behaviour
pnpm startprints the forgotten query run through the program's own connection: one row fororg_456, no rows with no company said. Everything step 10 printed still prints.Two measurements corrected the build.
BYPASSRLSis a role attribute, and PostgreSQL never passes attributes through membership, so a Console-made Neon user is oneSET ROLE neon_superuseraway from skipping the lock rather than skipping it outright; the map's wording says "altogether", recorded as a divergence with the map untouched. And the audit policy binds a record'stenant, not itschain, which the migration's comment had claimed — aCHECKnow says it.The hostile review measured three windows past the lock that the first start-up check could not see — a second wide-open policy (policies are OR'd), a
SECURITY DEFINERhelper owned by aBYPASSRLSrole, a view the owner made — each as both companies coming back while the check said the lock was on. All closed, each red first, each guard removed again to see its test fail. Thirteen breaks in the README, every count measured twice at 400 with both runs agreeing.Tested again, on request. An independent evaluation — four reviewers, a mutation pass on a copy, a critic — graded the folder C and found: three more ways round the helper question (an aggregate, an
INHERIT FALSEmembership, a trigger on the invoices fired by the application's ownUPDATE), the demo's two lines pinned by no test, the pool adapter invisible topnpm test, two step 09 test files running the stores as the superuser, andsingleFork— an option Vitest 4 does not have, ignored since step 09, so the test files had run in parallel all along and the configs were never typechecked. All fixed, each guard removed again to see its test fail; the configs are typechecked now. Two faults were injected for real (aCOMMITthat dies on a real server; PGlite's one connection) and the cost of a transaction per statement measured. 412 tests, 16 in the database tier, seventeen breaks measured twice with the runs agreeing. Steps 09 and 10 carry the samesingleForklines and are reported here, not edited. Also fixed on this PR: step 08's demo test, which built its path in a way Windows could not open.And Neon itself. A project, a database and
dsor_runtimemade with SQL; the five migrations, the sixteen database-tier tests twice, and the demo through the real login all pass on Neon. Measured there: Neon's owner and every Console- or API-made role holdBYPASSRLSdirectly and Neon forbidsSET ROLE neon_superuser, so the map's "ignores row-level security altogether" is right for Neon, and the folder's earlier "one SET ROLE away" was PostgreSQL's rule, not Neon's — corrected. Over the network a statement with a company is four round trips (117 ms plain, 484 ms through the adapter), the measured price of "before each statement" that step 36's one transaction per request removes.The four files pull request #4 changed in
my_10(comments only, the drift markers) are carried intomy_11so the new guard holds.docs/status.mdandmy_notes(decisions 94 to 96, lessons 36 to 38, the step note) are updated in the same change.🤖 Generated with Claude Code