翻訳待ち:The hold that could not release itself
AI サービスが一時的に利用できないため、復旧後に翻訳を補完します。ソース概要:← log $ the hold that could not release itself 2026-08-21 · 12 min read A true fact from yesterday had become a command that could govern forever. We stored a safety decision as held. Then the contract changed. The old…
AI サービスが一時的に利用できないため、復旧後に翻訳を補完します。
← log $ the hold that could not release itself 2026-08-21 · 12 min read A true fact from yesterday had become a command that could govern forever. We stored a safety decision as held. Then the contract changed. The old observation remained true, but it no longer had authority over the current decision. Our system could not tell the difference. The stale hold blocked the migration that would make it obsolete. The safety mechanism had trapped itself. The lesson: preserve historical facts with their contract and time. Derive current decisions from current values. Keep operator policy separate from both. The patch changed the order of two operations. The design problem was deeper: we had made the past block the future. what actually broke A production course-sync pipeline stopped applying updates. The poller was healthy. New revisions arrived. The migration code was deployed. Every safety check was doing exactly what it had been told to do. A contract-v3 safety hold had been written when the target violated contract v3. Later, contract v4 intentionally accepted the new target shape. The system already knew how to migrate the stored binding from v3 to v4. But on every poll it restored held first and returned before the migration path ran. The patch: run the exact known server-owned binding migration before returning the existing hold. Keep the hold. Release it separately after current validation. The patch was 53 added lines and two removed lines, including tests. PR #140 passed 57 focused tests, typecheck, lint, CI, Macroscope, and an independent review. we made the past block the future Four separate values became one sticky status Contract, observation, time, and operator policy start as separate lines and braid into one held status. The database had a status called held. That single word braided together four different things: A historical observation under contract v3. The current authority to stop work. The contract version that gave the observation meaning. An operator policy saying new source revisions cannot bypass a hold. The operator policy was correct. A new revision must not walk around an unresolved current safety violation. The model was wrong. It could not distinguish a current violation from a historical fact produced by an obsolete contract. rich hickey: state braided value and time In Simple Made Easy, Rich Hickey resurrects the word complect: to interleave, entwine, or braid. Having state in your program is never simple, because it has a fundamental complecting that goes on in its artifacts. It complects value and time. Watch Simple Made Easy, Rich Hickey's 2011 Strange Loop talk. Our held slot did exactly that. It told us a value without telling us when that value was true or which contract made it true. The Value of Values sharpens the point. Facts do not change. A new fact does not edit yesterday. It joins the record with a later time. You cannot update a fact. A fact is not a place. You cannot do that any more than you can change the past. The v3 violation remained a true historical fact. Contract v4 did not travel back in time and erase it. But the current decision had to be derived again from current values: decideSync({ contract: contractV4, target: currentTargetSnapshot, source: currentSourceRevision, unresolvedFacts, }) That is different from asking a mutable place what mood it is in: if (pollState.status === "held") return Are We There Yet? gives the more useful model: an identity is a series of causally related immutable values. The future is a function of the past. It does not change the past. The fact stays true. Its authority does not. t1 contract v3 target violates v3 hold observed t2 contract v4 intentional target now satisfies v4 derive again t3 current decision v3 hold remains in history ready or blocked by current facts ousterhout: define the error out of existence John Ousterhout's A Philosophy of Software Design has the most direct answer: The best way to eliminate exception handling complexity is to define your APIs so that there are no exceptions to handle: define errors out of existence. The better binding API is not “load whatever bytes the database currently contains.” It is: loadCurrentBinding(bindingId) Return the current valid server binding. Migrate an exact known predecessor internally. Reject unknown drift. Under that definition, “a known old server contract blocked its own migration” is not a recoverable error. It is not a state. It is not an operator task. Loading the binding already did the work. This is also Ousterhout's deep-module test. A module earns its interface by hiding more complexity than it introduces. One narrow interface, useful depth underneath prepareSync(bindingId, sourceRevision) decode stored contract migrate known predecessor load target snapshot validate current contract compare source revision resolve stale projections return one current decision The current system also shows what Ousterhout calls temporal decomposition. Code was organized around when work happened: detect, compare, stage, preview, apply. But contract knowledge was needed at several points in that timeline. Organizing by time scattered one piece of knowledge across the lifecycle. XState should show the order. Module boundaries should show who owns the knowledge. greg young: make the wrong model cheap to delete Jaga Santagostino sent us Greg Young's The Art of Destroying Software after reading this article. It names the replacement strategy better than I did. What if you were to optimize from the very beginning to be able to delete code? Watch The Art of Destroying Software by Greg Young. Young's rule of thumb is blunt: a component should take no more than about one week to rewrite. The number is not doctrine. It forces the boundary question. If a wrong model cannot be replaced in a week, which knowledge did we smear across the system? That sharpens this replacement: preserve behavior and receipts, then make the implementation disposable. Do not predict every future contract. Build a seam that lets us replace the model when the future proves it wrong. ddd: a projection is not the aggregate Domain-Driven Design gives the nouns teeth. A bounded context says where a model applies. A Dropbox revision belongs to the Authoring Source context. A target invariant belongs to the Target Course context. A Slack notice belongs to Operations. Calling every problem held collapsed those models into one status vocabulary. Authoring Sourceimmutable manifest → Sync Decisioncommand + facts → events → Sync Executionstage, apply, rollback Target Coursesnapshot + invariants → Operationsprojections + operator commands The likely aggregate root is the sync binding. It owns one invariant: One authoring source may mutate one target only under one current server contract. Eric Evans says the aggregate root is responsible for checking the invariants of its boundary. Scott Wlaschin's Domain Modeling Made Functional gives the implementation shape: a workflow accepts a command plus data and returns events. It does not publish them itself. type SyncDecision = | { type: "NoChange"; facts: SyncFact[] } | { type: "Ready"; planInput: PlanInput; facts: SyncFact[] } | { type: "NeedsReview"; reason: UnsafePlan; facts: SyncFact[] } | { type: "Blocked"; violation: CurrentViolation; facts: SyncFact[] } The operations dashboard can project those facts into a red badge. The badge does not become stronger than the aggregate that produced it. Our stale hold did. effect at the edges, xstate on time Effect and XState are both useful here. They are useful for different reasons. Effect owns database and provider services decoding untrusted values typed expected failures bounded transient retries transactions, tracing, receipts XState owns in-flight lifecycle modes legal events by mode retry waiting and cancellation resumability and child actors apply and rollback transitions Neither should own the pure question “does this historical v3 hold still block a v4 decision?” That is a function over values. XState's own guidance says guards should be pure and synchronous. Effect should not be sprayed over pure domain rules to make them look serious. The pure core decides. Effect touches the world. XState makes essential time visible. the prompt I turned this review lens into a public skill called uncomplect. It has the full Hickey, Ousterhout, Young, DDD, typed-effects, and lifecycle review pass. npx skills add joelhooks/skills --skill uncomplect --global The repo includes the source map, three behavior evals, twenty trigger cases, and an honest one-run benchmark. On the same model, the skill scored 24/24 against 11/24 without it. One run is a receipt, not a crown. The portable bit is still one paragraph: Treat the current system as a functional prototype, not an architecture to preserve. Preserve its proven behavior. Find what is complected. Separate values from time, policy from mechanism, and facts from projections. Define accidental errors out of existence. Draw the DDD boundaries and aggregate invariants. Keep Effect at effectful boundaries and XState on essential lifecycle. Optimize the seam for deletion instead of predicting future change. Propose the smallest deep replacement, show what it deletes, and name the remaining unknowns. A redesign that cannot name what disappears is probably just new nouns sitting on old complexity. patch now, replace deliberately The production patch did not auto-release the hold. That would have been tidy and wrong. It migrated the exact known contract value, wrote the migration receipt, and left the operator boundary intact. A separate release still validates the target under the current contract. New source revisions still cannot bypass a real hold. The replacement needs more care: Use the current pipeline as a functional prototype. List the proven behavior and safety invariants. Design two materially different interfaces, because Ousterhout says design it twice. Run both through the Hickey, Ousterhout, Young, DDD, Effect, and XState lens. Shadow both decisions against the same immutable inputs. Cut over with one authority at a time. Delete the old statuses, recovery branches, and operator folklore. More implementation inside one deep boundary. Less complexity everywhere else. The goal is not fewer lines. It is fewer concepts that every caller must understand. sources Rich Hickey, Simple Made Easy and its transcript Greg Young, The Art of Destroying Software Rich Hickey, The Value of Values Rich Hickey, Are We There Yet? Rich Hickey on contract evolution and compatibility John Ousterhout, A Philosophy of Software Design Eric Evans, Domain-Driven Design Reference Scott Wlaschin, Domain Modeling Made Functional Effect documentation XState guards and persistence The production patch, PR #140