[{"data":1,"prerenderedAt":1740},["ShallowReactive",2],{"docs-nav:en":3,"docs:\u002Fdocs\u002Finfra\u002Ftesting":186},[4],{"title":5,"path":6,"stem":7,"children":8},"Docs","\u002Fdocs","docs",[9,12,16,20,24,28,50,75,120,145],{"title":10,"path":6,"stem":11},"What is Fougere","docs\u002Findex",{"title":13,"path":14,"stem":15},"Getting started","\u002Fdocs\u002Fgetting-started","docs\u002F01.getting-started",{"title":17,"path":18,"stem":19},"An app you already have","\u002Fdocs\u002Fexisting-app","docs\u002F02.existing-app",{"title":21,"path":22,"stem":23},"Bring your schema","\u002Fdocs\u002Fbring-your-schema","docs\u002F03.bring-your-schema",{"title":25,"path":26,"stem":27},"The CLI","\u002Fdocs\u002Fcli","docs\u002F04.cli",{"title":29,"path":30,"stem":31,"children":32,"page":49},"Concepts","\u002Fdocs\u002Fconcepts","docs\u002F2.concepts",[33,37,41,45],{"title":34,"path":35,"stem":36},"Philosophy","\u002Fdocs\u002Fconcepts\u002Fphilosophy","docs\u002F2.concepts\u002F1.philosophy",{"title":38,"path":39,"stem":40},"The Frond","\u002Fdocs\u002Fconcepts\u002Ffrond","docs\u002F2.concepts\u002F2.frond",{"title":42,"path":43,"stem":44},"The shortest path","\u002Fdocs\u002Fconcepts\u002Fshortest-path","docs\u002F2.concepts\u002F3.shortest-path",{"title":46,"path":47,"stem":48},"The base","\u002Fdocs\u002Fconcepts\u002Fthe-base","docs\u002F2.concepts\u002F4.the-base",false,{"title":51,"path":52,"stem":53,"children":54,"page":49},"Schema","\u002Fdocs\u002Fschema","docs\u002F3.schema",[55,59,63,67,71],{"title":56,"path":57,"stem":58},"Entities","\u002Fdocs\u002Fschema\u002Fentities","docs\u002F3.schema\u002F1.entities",{"title":60,"path":61,"stem":62},"Views","\u002Fdocs\u002Fschema\u002Fviews","docs\u002F3.schema\u002F2.views",{"title":64,"path":65,"stem":66},"The identity card","\u002Fdocs\u002Fschema\u002Fcard","docs\u002F3.schema\u002F3.card",{"title":68,"path":69,"stem":70},"Standard Schema","\u002Fdocs\u002Fschema\u002Fstandard-schema","docs\u002F3.schema\u002F4.standard-schema",{"title":72,"path":73,"stem":74},"How a schema moves","\u002Fdocs\u002Fschema\u002Fevolution","docs\u002F3.schema\u002F5.evolution",{"title":76,"path":77,"stem":78,"children":79,"page":49},"Business","\u002Fdocs\u002Fbusiness","docs\u002F4.business",[80,84,88,92,96,100,104,108,112,116],{"title":81,"path":82,"stem":83},"Handlers","\u002Fdocs\u002Fbusiness\u002Fhandlers","docs\u002F4.business\u002F1.handlers",{"title":85,"path":86,"stem":87},"Ports","\u002Fdocs\u002Fbusiness\u002Fports","docs\u002F4.business\u002F10.ports",{"title":89,"path":90,"stem":91},"Presenters","\u002Fdocs\u002Fbusiness\u002Fpresenters","docs\u002F4.business\u002F2.presenters",{"title":93,"path":94,"stem":95},"Collectors","\u002Fdocs\u002Fbusiness\u002Fcollectors","docs\u002F4.business\u002F3.collectors",{"title":97,"path":98,"stem":99},"Errors","\u002Fdocs\u002Fbusiness\u002Ferrors","docs\u002F4.business\u002F4.errors",{"title":101,"path":102,"stem":103},"Seeds","\u002Fdocs\u002Fbusiness\u002Fseeds","docs\u002F4.business\u002F5.seeds",{"title":105,"path":106,"stem":107},"Facts","\u002Fdocs\u002Fbusiness\u002Ffacts","docs\u002F4.business\u002F6.facts",{"title":109,"path":110,"stem":111},"Storage","\u002Fdocs\u002Fbusiness\u002Fstorage","docs\u002F4.business\u002F7.storage",{"title":113,"path":114,"stem":115},"Writes that stand or fall as one","\u002Fdocs\u002Fbusiness\u002Ftogether","docs\u002F4.business\u002F8.together",{"title":117,"path":118,"stem":119},"Repositories","\u002Fdocs\u002Fbusiness\u002Frepositories","docs\u002F4.business\u002F9.repositories",{"title":121,"path":122,"stem":123,"children":124,"page":49},"Client","\u002Fdocs\u002Fclient","docs\u002F5.client",[125,129,133,137,141],{"title":126,"path":127,"stem":128},"Queries & commands","\u002Fdocs\u002Fclient\u002Fqueries-commands","docs\u002F5.client\u002F1.queries-commands",{"title":130,"path":131,"stem":132},"Forms","\u002Fdocs\u002Fclient\u002Fforms","docs\u002F5.client\u002F2.forms",{"title":134,"path":135,"stem":136},"Session","\u002Fdocs\u002Fclient\u002Fsession","docs\u002F5.client\u002F3.session",{"title":138,"path":139,"stem":140},"invoke","\u002Fdocs\u002Fclient\u002Finvoke","docs\u002F5.client\u002F4.invoke",{"title":142,"path":143,"stem":144},"Back-office","\u002Fdocs\u002Fclient\u002Fadmin","docs\u002F5.client\u002F5.admin",{"title":146,"path":147,"stem":148,"children":149,"page":49},"Infrastructure","\u002Fdocs\u002Finfra","docs\u002F6.infra",[150,154,158,162,166,170,174,178,182],{"title":151,"path":152,"stem":153},"The gradient","\u002Fdocs\u002Finfra\u002Fgradient","docs\u002F6.infra\u002F1.gradient",{"title":155,"path":156,"stem":157},"Surfaces — REST & GraphQL","\u002Fdocs\u002Finfra\u002Fsurfaces","docs\u002F6.infra\u002F2.surfaces",{"title":159,"path":160,"stem":161},"Deployment","\u002Fdocs\u002Finfra\u002Fdeployment","docs\u002F6.infra\u002F3.deployment",{"title":163,"path":164,"stem":165},"Hosts","\u002Fdocs\u002Finfra\u002Fhosts","docs\u002F6.infra\u002F4.hosts",{"title":167,"path":168,"stem":169},"Sources","\u002Fdocs\u002Finfra\u002Fsources","docs\u002F6.infra\u002F5.sources",{"title":171,"path":172,"stem":173},"Lifecycle","\u002Fdocs\u002Finfra\u002Flifecycle","docs\u002F6.infra\u002F6.lifecycle",{"title":175,"path":176,"stem":177},"Identity across Fronds","\u002Fdocs\u002Finfra\u002Fidentity","docs\u002F6.infra\u002F7.identity",{"title":179,"path":180,"stem":181},"Observability","\u002Fdocs\u002Finfra\u002Fobservability","docs\u002F6.infra\u002F8.observability",{"title":183,"path":184,"stem":185},"Testing","\u002Fdocs\u002Finfra\u002Ftesting","docs\u002F6.infra\u002F9.testing",{"id":187,"title":183,"body":188,"description":1735,"extension":1736,"meta":1737,"navigation":310,"path":184,"seo":1738,"stem":185,"__hash__":1739},"docs_en\u002Fdocs\u002F6.infra\u002F9.testing.md",{"type":189,"value":190,"toc":1721},"minimark",[191,195,203,214,229,258,329,334,342,357,452,470,488,492,549,555,561,584,591,601,643,648,673,677,754,792,803,806,810,817,971,982,989,996,1000,1023,1026,1029,1036,1062,1065,1151,1166,1173,1242,1246,1253,1321,1336,1339,1343,1346,1369,1451,1464,1481,1485,1505,1535,1541,1545,1681,1696,1707,1714,1717],[192,193,183],"h1",{"id":194},"testing",[196,197,198,202],"p",{},[199,200,201],"code",{},"@fougere\u002Ftesting"," rests on two ideas, and neither is a testing technique.",[196,204,205,209,210,213],{},[206,207,208],"strong",{},"Part of a test suite has no reason to be written."," ",[199,211,212],{},"Judge.row"," has a closed list of\nrefusals — unknown field, missing field, read-only, immutable, the shape, a named codec —\nand the four axes say who writes what and when. So the cases can be computed from the\nentity. A test typed by hand becomes false the day the entity moves and nobody hears\nabout it; a derived one moves with it.",[196,215,216,219,220,223,224,228],{},[206,217,218],{},"A test's level of reality does not belong to it."," That is the gesture Fougere already\napplies to topology: ",[199,221,222],{},"remotes:"," sits outside the frond, and the same code runs in-process\nor behind JSON-RPC. Here, what is real and where it runs are stated ",[225,226,227],"em",{},"outside"," the test\nfile, which says only what it expects.",[230,231,236],"pre",{"className":232,"code":233,"language":234,"meta":235,"style":235},"language-bash shiki shiki-themes material-theme-lighter github-light github-dark","pnpm add -D @fougere\u002Ftesting\n","bash","",[199,237,238],{"__ignoreMap":235},[239,240,243,247,251,255],"span",{"class":241,"line":242},"line",1,[239,244,246],{"class":245},"sbgvK","pnpm",[239,248,250],{"class":249},"s_sjI"," add",[239,252,254],{"class":253},"stzsN"," -D",[239,256,257],{"class":249}," @fougere\u002Ftesting\n",[230,259,263],{"className":260,"code":261,"language":262,"meta":235,"style":235},"language-ts shiki shiki-themes material-theme-lighter github-light github-dark","\u002F\u002F vitest.config.ts — the whole file\nimport { fougereTest } from '@fougere\u002Ftesting\u002Fvitest';\n\nexport default fougereTest();\n","ts",[199,264,265,271,305,312],{"__ignoreMap":235},[239,266,267],{"class":241,"line":242},[239,268,270],{"class":269},"sutJx","\u002F\u002F vitest.config.ts — the whole file\n",[239,272,274,278,282,286,289,292,296,299,302],{"class":241,"line":273},2,[239,275,277],{"class":276},"sVHd0","import",[239,279,281],{"class":280},"sP7_E"," {",[239,283,285],{"class":284},"su5hD"," fougereTest",[239,287,288],{"class":280}," }",[239,290,291],{"class":276}," from",[239,293,295],{"class":294},"sjJ54"," '",[239,297,298],{"class":249},"@fougere\u002Ftesting\u002Fvitest",[239,300,301],{"class":294},"'",[239,303,304],{"class":280},";\n",[239,306,308],{"class":241,"line":307},3,[239,309,311],{"emptyLinePlaceholder":310},true,"\n",[239,313,315,318,321,324,327],{"class":241,"line":314},4,[239,316,317],{"class":276},"export",[239,319,320],{"class":276}," default",[239,322,285],{"class":323},"sGLFI",[239,325,326],{"class":284},"()",[239,328,304],{"class":280},[330,331,333],"h2",{"id":332},"where-a-test-sits-says-what-it-is-about","Where a test sits says what it is about",[230,335,340],{"className":336,"code":338,"language":339},[337],"language-text","fronds\u002Fcatalog\u002Ftests\u002Fpricing.test.ts    catalog is real, its neighbours are not\ntests\u002Fcontracts.test.ts                 every frond is real, together\n","text",[199,341,338],{"__ignoreMap":235},[196,343,344,345,348,349,352,353,356],{},"A directory is a declaration — the reading the scan already performs on ",[199,346,347],{},"entities\u002F"," and\n",[199,350,351],{},"handlers\u002F",". So ",[199,354,355],{},"testApp()"," takes no argument at all:",[230,358,360],{"className":260,"code":359,"language":262,"meta":235,"style":235},"await using app = await testApp();\n\nexpect(app.fronds.map((frond) => frond.name)).toEqual(['catalog']);\n",[199,361,362,386,390],{"__ignoreMap":235},[239,363,364,368,372,376,379,382,384],{"class":241,"line":242},[239,365,367],{"class":366},"sbsja","await using",[239,369,371],{"class":370},"s_hVV"," app",[239,373,375],{"class":374},"smGrS"," =",[239,377,378],{"class":276}," await",[239,380,381],{"class":323}," testApp",[239,383,326],{"class":284},[239,385,304],{"class":280},[239,387,388],{"class":241,"line":273},[239,389,311],{"emptyLinePlaceholder":310},[239,391,392,395,398,401,404,406,409,412,414,418,421,424,427,429,432,434,437,440,442,445,447,450],{"class":241,"line":307},[239,393,394],{"class":323},"expect",[239,396,397],{"class":284},"(app",[239,399,400],{"class":280},".",[239,402,403],{"class":284},"fronds",[239,405,400],{"class":280},[239,407,408],{"class":323},"map",[239,410,411],{"class":284},"(",[239,413,411],{"class":280},[239,415,417],{"class":416},"s99_P","frond",[239,419,420],{"class":280},")",[239,422,423],{"class":366}," =>",[239,425,426],{"class":284}," frond",[239,428,400],{"class":280},[239,430,431],{"class":284},"name))",[239,433,400],{"class":280},[239,435,436],{"class":323},"toEqual",[239,438,439],{"class":284},"([",[239,441,301],{"class":294},[239,443,444],{"class":249},"catalog",[239,446,301],{"class":294},[239,448,449],{"class":284},"])",[239,451,304],{"class":280},[196,453,454,455,458,459,462,463,465,466,469],{},"The project starts at the first ancestor holding a ",[199,456,457],{},"fougere.config.ts"," or a ",[199,460,461],{},"fronds\u002F",", and\nthe subject is the segment after ",[199,464,461],{},". A sub-directory below ",[199,467,468],{},"tests\u002F"," carries\nnothing, and neither does a test's title: a name is prose, and prose deciding how an app\nis wired is the hidden runtime the doctrine refuses.",[196,471,472,473,476,477,480,481,484,485,487],{},"Rows live in SQLite ",[199,474,475],{},":memory:"," — a real engine, so the DDL runs, the ",[199,478,479],{},"CHECK"," constraints\nexist and transactions are real, and each call gets a database of its own. Stating ",[199,482,483],{},"root","\nor ",[199,486,403],{}," yourself wins over the position, the same precedence a config key has over a\nconvention everywhere else.",[330,489,491],{"id":490},"the-cases-come-from-the-entity","The cases come from the entity",[230,493,495],{"className":260,"code":494,"language":262,"meta":235,"style":235},"\u002F\u002F tests\u002Fcontracts.test.ts — the whole file\nimport { testApp, checkAll } from '@fougere\u002Ftesting';\n\ncheckAll(await testApp());\n",[199,496,497,502,528,532],{"__ignoreMap":235},[239,498,499],{"class":241,"line":242},[239,500,501],{"class":269},"\u002F\u002F tests\u002Fcontracts.test.ts — the whole file\n",[239,503,504,506,508,510,513,516,518,520,522,524,526],{"class":241,"line":273},[239,505,277],{"class":276},[239,507,281],{"class":280},[239,509,381],{"class":284},[239,511,512],{"class":280},",",[239,514,515],{"class":284}," checkAll",[239,517,288],{"class":280},[239,519,291],{"class":276},[239,521,295],{"class":294},[239,523,201],{"class":249},[239,525,301],{"class":294},[239,527,304],{"class":280},[239,529,530],{"class":241,"line":307},[239,531,311],{"emptyLinePlaceholder":310},[239,533,534,537,539,542,544,547],{"class":241,"line":314},[239,535,536],{"class":323},"checkAll",[239,538,411],{"class":284},[239,540,541],{"class":276},"await",[239,543,381],{"class":323},[239,545,546],{"class":284},"())",[239,548,304],{"class":280},[196,550,551,552,400],{},"It names no entity: the list comes from the scan. A list of imports is a second copy of\nwhat the project already declares, and it goes stale the day an entity is added — silently,\nbecause a missing test cannot fail. One entity at a time reads\n",[199,553,554],{},"checkContract(app, Product)",[230,556,559],{"className":557,"code":558,"language":339},[337],"✓ Product — the contract it declares\n  ✓ a valid body            ✓ sku shorter than min      ✓ cents below minimum\n  ✓ a key outside the contract  ✓ sku longer than max   ✓ cents above maximum\n  ✓ not an object at all    ✓ name absent               ✓ status outside the stated set\n  ✓ sku absent              ✓ name of the wrong type    ✓ createdAt supplied on an update\n",[199,560,558],{"__ignoreMap":235},[196,562,563,564,567,568,571,572,575,576,579,580,583],{},"Not one case is written. They are enumerated from five readings that already exist —\n",[199,565,566],{},"Visibility.input"," for what may come in, ",[199,569,570],{},"Judge.onAbsent"," for what is required,\n",[199,573,574],{},"Lifecycle.of(field).immutable"," for patch mode, the ",[199,577,578],{},"shape"," for the bounds, and the key\noutside the contract. Change a ",[199,581,582],{},"min:"," in the entity and the cases follow.",[196,585,586,587,590],{},"A case carries the ",[206,588,589],{},"path"," of the field at fault and never the message: we know which\nfield must be refused because we are the ones who broke it, and naming the message would\nmake the list a second judge to keep in step by hand.",[196,592,593,596,597,600],{},[199,594,595],{},"checkOutput"," costs nothing to state — ",[199,598,599],{},"Visibility.output"," already answers it, so a field the\nboundary closes has no business in any response:",[230,602,604],{"className":260,"code":603,"language":262,"meta":235,"style":235},"password: text({ boundary: 'writeOnly' })   \u002F\u002F in, never out\n",[199,605,606],{"__ignoreMap":235},[239,607,608,611,614,617,619,622,626,628,630,633,635,637,640],{"class":241,"line":242},[239,609,610],{"class":245},"password",[239,612,613],{"class":280},":",[239,615,616],{"class":323}," text",[239,618,411],{"class":284},[239,620,621],{"class":280},"{",[239,623,625],{"class":624},"skxfh"," boundary",[239,627,613],{"class":280},[239,629,295],{"class":294},[239,631,632],{"class":249},"writeOnly",[239,634,301],{"class":294},[239,636,288],{"class":280},[239,638,639],{"class":284},")   ",[239,641,642],{"class":269},"\u002F\u002F in, never out\n",[644,645,647],"h3",{"id":646},"what-it-does-not-prove","What it does not prove",[196,649,650,651,654,655,657,658,661,662,668,669,672],{},"A test derived from ",[199,652,653],{},"Post"," can never say that ",[199,656,653],{}," is wrong. It proves the\n",[206,659,660],{},"realizations agree",". What must break when the declaration moves is elsewhere and\nalready built: ",[663,664,665],"a",{"href":73},[199,666,667],{},"fougere migrate",". And business intent —\n",[225,670,671],{},"publishing sets the status to published"," — is always written by hand.",[330,674,676],{"id":675},"sampling-a-valid-body","Sampling a valid body",[230,678,680],{"className":260,"code":679,"language":262,"meta":235,"style":235},"sampleInput(Product)                          \u002F\u002F → { sku: 'gDXv8gBIU', name: '…', cents: 750868 }\nsampleInput(Product, { sku: 'ABC-01' })       \u002F\u002F what you impose, verbatim\nsampleInput(Product, {}, { seed: 7 })         \u002F\u002F pinned\n",[199,681,682,693,724],{"__ignoreMap":235},[239,683,684,687,690],{"class":241,"line":242},[239,685,686],{"class":323},"sampleInput",[239,688,689],{"class":284},"(Product)                          ",[239,691,692],{"class":269},"\u002F\u002F → { sku: 'gDXv8gBIU', name: '…', cents: 750868 }\n",[239,694,695,697,700,702,704,707,709,711,714,716,718,721],{"class":241,"line":273},[239,696,686],{"class":323},[239,698,699],{"class":284},"(Product",[239,701,512],{"class":280},[239,703,281],{"class":280},[239,705,706],{"class":624}," sku",[239,708,613],{"class":280},[239,710,295],{"class":294},[239,712,713],{"class":249},"ABC-01",[239,715,301],{"class":294},[239,717,288],{"class":280},[239,719,720],{"class":284},")       ",[239,722,723],{"class":269},"\u002F\u002F what you impose, verbatim\n",[239,725,726,728,730,732,735,737,740,742,746,748,751],{"class":241,"line":307},[239,727,686],{"class":323},[239,729,699],{"class":284},[239,731,512],{"class":280},[239,733,734],{"class":280}," {},",[239,736,281],{"class":280},[239,738,739],{"class":624}," seed",[239,741,613],{"class":280},[239,743,745],{"class":744},"srdBf"," 7",[239,747,288],{"class":280},[239,749,750],{"class":284},")         ",[239,752,753],{"class":269},"\u002F\u002F pinned\n",[196,755,756,757,209,759,762,763,766,767,770,771,770,774,770,777,780,781,784,785,788,789,791],{},"The value itself is not ours to invent: a ",[199,758,578],{},[206,760,761],{},"is"," JSON Schema, so\n",[199,764,765],{},"json-schema-faker"," honours ",[199,768,769],{},"minLength",", ",[199,772,773],{},"enum",[199,775,776],{},"format",[199,778,779],{},"pattern"," and ",[199,782,783],{},"items",". What is\nours is ",[225,786,787],{},"which"," fields belong in a body — ",[199,790,566],{},", the one reader of the boundary\nand lifecycle axes the façade and the form already stand on.",[196,793,794,795,798,799,802],{},"The seed defaults to a value derived from the entity name, so two runs agree and a failure\ncan be replayed. A ",[199,796,797],{},"ref()"," is ",[206,800,801],{},"refused by name"," rather than generated: it points at a row\nthat must exist, and a made-up id points at nothing.",[196,804,805],{},"The same function seeds a dev database and sets the scene for a browser scenario.",[330,807,809],{"id":808},"doubles","Doubles",[196,811,812,813,816],{},"Only what is already declared replaceable can be stood in for — a\n",[663,814,815],{"href":86},"port",". \"I need a mock here\" becomes a design signal rather than a\nreflex.",[230,818,820],{"className":260,"code":819,"language":262,"meta":235,"style":235},"await using app = await testApp({ stub: [Pricing] });\napp.stub(Pricing).total.mockReturnValue(4242);\n\nawait run({ entity: 'product', op: 'quote' }, invocation);\n\nexpect(app.stub(Pricing).total).toHaveBeenCalledWith(1000);\n",[199,821,822,853,885,889,934,939],{"__ignoreMap":235},[239,823,824,826,828,830,832,834,836,838,841,843,846,849,851],{"class":241,"line":242},[239,825,367],{"class":366},[239,827,371],{"class":370},[239,829,375],{"class":374},[239,831,378],{"class":276},[239,833,381],{"class":323},[239,835,411],{"class":284},[239,837,621],{"class":280},[239,839,840],{"class":624}," stub",[239,842,613],{"class":280},[239,844,845],{"class":284}," [Pricing] ",[239,847,848],{"class":280},"}",[239,850,420],{"class":284},[239,852,304],{"class":280},[239,854,855,858,860,863,866,868,871,873,876,878,881,883],{"class":241,"line":273},[239,856,857],{"class":284},"app",[239,859,400],{"class":280},[239,861,862],{"class":323},"stub",[239,864,865],{"class":284},"(Pricing)",[239,867,400],{"class":280},[239,869,870],{"class":284},"total",[239,872,400],{"class":280},[239,874,875],{"class":323},"mockReturnValue",[239,877,411],{"class":284},[239,879,880],{"class":744},"4242",[239,882,420],{"class":284},[239,884,304],{"class":280},[239,886,887],{"class":241,"line":307},[239,888,311],{"emptyLinePlaceholder":310},[239,890,891,893,896,898,900,903,905,907,910,912,914,917,919,921,924,926,929,932],{"class":241,"line":314},[239,892,541],{"class":276},[239,894,895],{"class":323}," run",[239,897,411],{"class":284},[239,899,621],{"class":280},[239,901,902],{"class":624}," entity",[239,904,613],{"class":280},[239,906,295],{"class":294},[239,908,909],{"class":249},"product",[239,911,301],{"class":294},[239,913,512],{"class":280},[239,915,916],{"class":624}," op",[239,918,613],{"class":280},[239,920,295],{"class":294},[239,922,923],{"class":249},"quote",[239,925,301],{"class":294},[239,927,928],{"class":280}," },",[239,930,931],{"class":284}," invocation)",[239,933,304],{"class":280},[239,935,937],{"class":241,"line":936},5,[239,938,311],{"emptyLinePlaceholder":310},[239,940,942,944,946,948,950,952,954,957,959,962,964,967,969],{"class":241,"line":941},6,[239,943,394],{"class":323},[239,945,397],{"class":284},[239,947,400],{"class":280},[239,949,862],{"class":323},[239,951,865],{"class":284},[239,953,400],{"class":280},[239,955,956],{"class":284},"total)",[239,958,400],{"class":280},[239,960,961],{"class":323},"toHaveBeenCalledWith",[239,963,411],{"class":284},[239,965,966],{"class":744},"1000",[239,968,420],{"class":284},[239,970,304],{"class":280},[196,972,973,974,977,978,981],{},"The double is ",[206,975,976],{},"proposed, not written",": its methods are read from the port's prototype, so\nit carries exactly what the port carries and gains a new one the day the port does. What\nit ",[225,979,980],{},"returns"," is yours to state — a service's return type is a bare TypeScript type, erased\nat runtime, with no declared fields to build from. That is the line the whole package sits\non: what Fougere's vocabulary declares can be derived, arbitrary code cannot.",[196,983,984,985,988],{},"A port nobody answers under is refused by name, for the reason ",[199,986,987],{},"ports:"," refuses a key that\nmatched nothing.",[196,990,991,992,995],{},"Nothing can stand in for what a handler fetches itself — a ",[199,993,994],{},"fetch",", a library imported at\nthe top of the file, the clock. Fougere resolves by the signature, so it replaces only\nwhat the signature declares, and the model's answer for an outside call is already to make\nit a port.",[330,997,999],{"id":998},"the-doors-agree","The doors agree",[230,1001,1003],{"className":260,"code":1002,"language":262,"meta":235,"style":235},"checkDoors(app, Product);   \u002F\u002F local · REST · GraphQL · RPC\n",[199,1004,1005],{"__ignoreMap":235},[239,1006,1007,1010,1012,1014,1017,1020],{"class":241,"line":242},[239,1008,1009],{"class":323},"checkDoors",[239,1011,397],{"class":284},[239,1013,512],{"class":280},[239,1015,1016],{"class":284}," Product)",[239,1018,1019],{"class":280},";",[239,1021,1022],{"class":269},"   \u002F\u002F local · REST · GraphQL · RPC\n",[196,1024,1025],{},"The five CRUD operations, reads and writes. A write creates a different row at each door,\nso what is compared is what the caller SENT — comparing values would compare clocks.",[196,1027,1028],{},"The repo claims everywhere that REST, GraphQL and RPC are three projections of one\ncontract. This is what checks it, on the rows and on the refusals both — a refusal is\nwhere doors diverge most, and where each is most tempted to answer in its own words.",[196,1030,1031,1032,1035],{},"Each door's envelope is taken off before comparing: they wrap differently by construction,\nand comparing wrappers would compare protocols. The GraphQL field is not recomputed but\n",[206,1033,1034],{},"asked of the schema"," — a list is the field whose type is the entity's list type, a find\nis the one of the entity's own type that takes an id.",[196,1037,1038,1039,1042,1043,1046,1047,770,1050,1053,1054,1057,1058,1061],{},"For a handler-specific invariant, ",[199,1040,1041],{},"checkDoorContract(app, Entity, cases)"," runs the same\nhand-written cases through all four doors. Fougere uses it to pin optional-input semantics:\nomitted ",[199,1044,1045],{},"foo?: T"," arrives as ",[199,1048,1049],{},"undefined",[199,1051,1052],{},"foo: T | null"," keeps explicit ",[199,1055,1056],{},"null",", and\n",[199,1059,1060],{},"foo?: T | null"," keeps those two states distinct.",[330,1063,105],{"id":1064},"facts",[230,1066,1068],{"className":260,"code":1067,"language":262,"meta":235,"style":235},"await run({ entity: 'post', op: 'publish' }, invocation);\n\nexpect(app.announced(PostPublished)).toMatchObject([{ id, title }]);\n",[199,1069,1070,1110,1114],{"__ignoreMap":235},[239,1071,1072,1074,1076,1078,1080,1082,1084,1086,1089,1091,1093,1095,1097,1099,1102,1104,1106,1108],{"class":241,"line":242},[239,1073,541],{"class":276},[239,1075,895],{"class":323},[239,1077,411],{"class":284},[239,1079,621],{"class":280},[239,1081,902],{"class":624},[239,1083,613],{"class":280},[239,1085,295],{"class":294},[239,1087,1088],{"class":249},"post",[239,1090,301],{"class":294},[239,1092,512],{"class":280},[239,1094,916],{"class":624},[239,1096,613],{"class":280},[239,1098,295],{"class":294},[239,1100,1101],{"class":249},"publish",[239,1103,301],{"class":294},[239,1105,928],{"class":280},[239,1107,931],{"class":284},[239,1109,304],{"class":280},[239,1111,1112],{"class":241,"line":273},[239,1113,311],{"emptyLinePlaceholder":310},[239,1115,1116,1118,1120,1122,1125,1128,1130,1133,1135,1137,1140,1142,1145,1147,1149],{"class":241,"line":307},[239,1117,394],{"class":323},[239,1119,397],{"class":284},[239,1121,400],{"class":280},[239,1123,1124],{"class":323},"announced",[239,1126,1127],{"class":284},"(PostPublished))",[239,1129,400],{"class":280},[239,1131,1132],{"class":323},"toMatchObject",[239,1134,439],{"class":284},[239,1136,621],{"class":280},[239,1138,1139],{"class":284}," id",[239,1141,512],{"class":280},[239,1143,1144],{"class":284}," title ",[239,1146,848],{"class":280},[239,1148,449],{"class":284},[239,1150,304],{"class":280},[196,1152,1153,1154,1157,1158,1161,1162,1165],{},"Watching costs nothing: ",[199,1155,1156],{},"Emissions.announce"," hands every fact to the carrier, so a test\nsits in the carrier's seat and adds no second dispatcher — the one thing ",[199,1159,1160],{},"Emissions","\nrefuses to become. A fact travels under its registration key (",[199,1163,1164],{},"postPublished","), and both\nspellings are accepted.",[196,1167,1168,1169,1172],{},"Its dual needed nothing at all — ",[199,1170,1171],{},"app.deliver"," is already the carrier's door, and it waits\nfor every subscriber and rejects if one refuses:",[230,1174,1176],{"className":260,"code":1175,"language":262,"meta":235,"style":235},"await app.deliver('postPublished', { id: 'p1', title: 'Delivered', at: … });\n",[199,1177,1178],{"__ignoreMap":235},[239,1179,1180,1182,1184,1186,1189,1191,1193,1195,1197,1199,1201,1203,1205,1207,1210,1212,1214,1217,1219,1221,1224,1226,1228,1231,1233,1236,1238,1240],{"class":241,"line":242},[239,1181,541],{"class":276},[239,1183,371],{"class":284},[239,1185,400],{"class":280},[239,1187,1188],{"class":323},"deliver",[239,1190,411],{"class":284},[239,1192,301],{"class":294},[239,1194,1164],{"class":249},[239,1196,301],{"class":294},[239,1198,512],{"class":280},[239,1200,281],{"class":280},[239,1202,1139],{"class":624},[239,1204,613],{"class":280},[239,1206,295],{"class":294},[239,1208,1209],{"class":249},"p1",[239,1211,301],{"class":294},[239,1213,512],{"class":280},[239,1215,1216],{"class":624}," title",[239,1218,613],{"class":280},[239,1220,295],{"class":294},[239,1222,1223],{"class":249},"Delivered",[239,1225,301],{"class":294},[239,1227,512],{"class":280},[239,1229,1230],{"class":624}," at",[239,1232,613],{"class":280},[239,1234,1235],{"class":284}," … ",[239,1237,848],{"class":280},[239,1239,420],{"class":284},[239,1241,304],{"class":280},[330,1243,1245],{"id":1244},"drift-between-fronds","Drift between fronds",[196,1247,1248,1249,1252],{},"The one place the gradient genuinely lies: the code is identical in-process and split, but\none side may have aged. ",[199,1250,1251],{},"fougere sync"," wrote the consumer's copy three weeks ago, the\nproducer moved on, and it still compiles.",[230,1254,1256],{"className":260,"code":1255,"language":262,"meta":235,"style":235},"const drift = driftOf(mine, theirs, 'blog');\n\nexplain(drift);\n\u002F\u002F blog.post — gone: publish\n\u002F\u002F blog.post — title: its bounds moved\n\u002F\u002F postPublished — + authorId (required) → re-sync and deploy the readers, THEN the sender\n",[199,1257,1258,1292,1296,1306,1311,1316],{"__ignoreMap":235},[239,1259,1260,1263,1266,1268,1271,1274,1276,1279,1281,1283,1286,1288,1290],{"class":241,"line":242},[239,1261,1262],{"class":366},"const",[239,1264,1265],{"class":370}," drift",[239,1267,375],{"class":374},[239,1269,1270],{"class":323}," driftOf",[239,1272,1273],{"class":284},"(mine",[239,1275,512],{"class":280},[239,1277,1278],{"class":284}," theirs",[239,1280,512],{"class":280},[239,1282,295],{"class":294},[239,1284,1285],{"class":249},"blog",[239,1287,301],{"class":294},[239,1289,420],{"class":284},[239,1291,304],{"class":280},[239,1293,1294],{"class":241,"line":273},[239,1295,311],{"emptyLinePlaceholder":310},[239,1297,1298,1301,1304],{"class":241,"line":307},[239,1299,1300],{"class":323},"explain",[239,1302,1303],{"class":284},"(drift)",[239,1305,304],{"class":280},[239,1307,1308],{"class":241,"line":314},[239,1309,1310],{"class":269},"\u002F\u002F blog.post — gone: publish\n",[239,1312,1313],{"class":241,"line":936},[239,1314,1315],{"class":269},"\u002F\u002F blog.post — title: its bounds moved\n",[239,1317,1318],{"class":241,"line":941},[239,1319,1320],{"class":269},"\u002F\u002F postPublished — + authorId (required) → re-sync and deploy the readers, THEN the sender\n",[196,1322,1323,1324,1327,1328,1331,1332,1335],{},"Both cards come from ",[199,1325,1326],{},"rpc.discover",", and ",[199,1329,1330],{},"diff"," names what separates them. Read in one\ndirection on purpose: a producer serving ",[225,1333,1334],{},"more"," than the consumer knows is not drift, it\nis a producer that moved forward without breaking anyone.",[196,1337,1338],{},"On facts the line says the order out loud, because a fact is judged strictly: a reader\nthat has not been re-synced refuses what the sender now announces.",[330,1340,1342],{"id":1341},"the-browser","The browser",[196,1344,1345],{},"The rung the others cannot reach: a form that states no rule of its own.",[230,1347,1349],{"className":260,"code":1348,"language":262,"meta":235,"style":235},"formFieldsOf(Product, 'product')\n",[199,1350,1351],{"__ignoreMap":235},[239,1352,1353,1356,1358,1360,1362,1364,1366],{"class":241,"line":242},[239,1354,1355],{"class":323},"formFieldsOf",[239,1357,699],{"class":284},[239,1359,512],{"class":280},[239,1361,295],{"class":294},[239,1363,909],{"class":249},[239,1365,301],{"class":294},[239,1367,1368],{"class":284},")\n",[230,1370,1374],{"className":1371,"code":1372,"language":1373,"meta":235,"style":235},"language-html shiki shiki-themes material-theme-lighter github-light github-dark","\u003Cinput id=\"sku\" name=\"sku\" type=\"text\" required minlength=\"3\" maxlength=\"12\">\n","html",[199,1375,1376],{"__ignoreMap":235},[239,1377,1378,1381,1385,1388,1391,1394,1397,1399,1402,1404,1406,1408,1410,1413,1415,1417,1419,1421,1424,1427,1429,1431,1434,1436,1439,1441,1443,1446,1448],{"class":241,"line":242},[239,1379,1380],{"class":280},"\u003C",[239,1382,1384],{"class":1383},"sQzsp","input",[239,1386,1139],{"class":1387},"s9AJx",[239,1389,1390],{"class":280},"=",[239,1392,1393],{"class":294},"\"",[239,1395,1396],{"class":249},"sku",[239,1398,1393],{"class":294},[239,1400,1401],{"class":1387}," name",[239,1403,1390],{"class":280},[239,1405,1393],{"class":294},[239,1407,1396],{"class":249},[239,1409,1393],{"class":294},[239,1411,1412],{"class":1387}," type",[239,1414,1390],{"class":280},[239,1416,1393],{"class":294},[239,1418,339],{"class":249},[239,1420,1393],{"class":294},[239,1422,1423],{"class":1387}," required",[239,1425,1426],{"class":1387}," minlength",[239,1428,1390],{"class":280},[239,1430,1393],{"class":294},[239,1432,1433],{"class":249},"3",[239,1435,1393],{"class":294},[239,1437,1438],{"class":1387}," maxlength",[239,1440,1390],{"class":280},[239,1442,1393],{"class":294},[239,1444,1445],{"class":249},"12",[239,1447,1393],{"class":294},[239,1449,1450],{"class":280},">\n",[196,1452,1453,1454,1456,1457,1459,1460,1463],{},"Membership comes from ",[199,1455,566],{},", requiredness from the lifecycle axis, and the bounds\nfrom the shape — under the names the browser already enforces. So the page contains no\nvalidation code at all: the browser refuses a two-character ",[199,1458,1396],{},", and the same body sent\nstraight to the door is refused by the judge. ",[225,1461,1462],{},"Local judge = remote judge"," is watched here\nrather than argued; every other rung can only simulate it inside one process.",[196,1465,1466,1467,1470,1471,1474,1475,770,1478,400],{},"Playwright drives it and starts the server itself (",[199,1468,1469],{},"webServer"," in ",[199,1472,1473],{},"playwright.config.ts",").\nA worked example: ",[199,1476,1477],{},"demos\u002Ftest-gradient",[199,1479,1480],{},"pnpm e2e",[330,1482,1484],{"id":1483},"a-frozen-clock","A frozen clock",[196,1486,1487,770,1490,780,1493,1496,1497,1500,1501,1504],{},[199,1488,1489],{},"created()",[199,1491,1492],{},"updated()",[199,1494,1495],{},"create: 'now'"," are realized in one place —\n",[199,1498,1499],{},"applyCreate","\u002F",[199,1502,1503],{},"applyUpdate"," — which is what makes the instant substitutable at all:",[230,1506,1508],{"className":260,"code":1507,"language":262,"meta":235,"style":235},"const release = freezeClock('2026-01-02T03:04:05Z');\n",[199,1509,1510],{"__ignoreMap":235},[239,1511,1512,1514,1517,1519,1522,1524,1526,1529,1531,1533],{"class":241,"line":242},[239,1513,1262],{"class":366},[239,1515,1516],{"class":370}," release",[239,1518,375],{"class":374},[239,1520,1521],{"class":323}," freezeClock",[239,1523,411],{"class":284},[239,1525,301],{"class":294},[239,1527,1528],{"class":249},"2026-01-02T03:04:05Z",[239,1530,301],{"class":294},[239,1532,420],{"class":284},[239,1534,304],{"class":280},[196,1536,1537,1538,400],{},"A value, not an interception: nothing there is a channel, and a stable instant is a fact\nabout the run rather than a hook on ",[199,1539,1540],{},"Date",[330,1542,1544],{"id":1543},"load","Load",[230,1546,1548],{"className":260,"code":1547,"language":262,"meta":235,"style":235},"\u002F\u002F scripts\u002Fload.ts\nimport { writeFileSync } from 'node:fs';\nimport { testApp, loadScript } from '@fougere\u002Ftesting';\n\nconst app = await testApp();\nwriteFileSync('load.js', loadScript(app, { door: 'http:\u002F\u002F127.0.0.1:3000\u002F_fougere\u002Fcall' }));\nawait app.dispose();\n",[199,1549,1550,1555,1577,1602,1606,1622,1665],{"__ignoreMap":235},[239,1551,1552],{"class":241,"line":242},[239,1553,1554],{"class":269},"\u002F\u002F scripts\u002Fload.ts\n",[239,1556,1557,1559,1561,1564,1566,1568,1570,1573,1575],{"class":241,"line":273},[239,1558,277],{"class":276},[239,1560,281],{"class":280},[239,1562,1563],{"class":284}," writeFileSync",[239,1565,288],{"class":280},[239,1567,291],{"class":276},[239,1569,295],{"class":294},[239,1571,1572],{"class":249},"node:fs",[239,1574,301],{"class":294},[239,1576,304],{"class":280},[239,1578,1579,1581,1583,1585,1587,1590,1592,1594,1596,1598,1600],{"class":241,"line":307},[239,1580,277],{"class":276},[239,1582,281],{"class":280},[239,1584,381],{"class":284},[239,1586,512],{"class":280},[239,1588,1589],{"class":284}," loadScript",[239,1591,288],{"class":280},[239,1593,291],{"class":276},[239,1595,295],{"class":294},[239,1597,201],{"class":249},[239,1599,301],{"class":294},[239,1601,304],{"class":280},[239,1603,1604],{"class":241,"line":314},[239,1605,311],{"emptyLinePlaceholder":310},[239,1607,1608,1610,1612,1614,1616,1618,1620],{"class":241,"line":936},[239,1609,1262],{"class":366},[239,1611,371],{"class":370},[239,1613,375],{"class":374},[239,1615,378],{"class":276},[239,1617,381],{"class":323},[239,1619,326],{"class":284},[239,1621,304],{"class":280},[239,1623,1624,1627,1629,1631,1634,1636,1638,1640,1642,1644,1646,1649,1651,1653,1656,1658,1660,1663],{"class":241,"line":941},[239,1625,1626],{"class":323},"writeFileSync",[239,1628,411],{"class":284},[239,1630,301],{"class":294},[239,1632,1633],{"class":249},"load.js",[239,1635,301],{"class":294},[239,1637,512],{"class":280},[239,1639,1589],{"class":323},[239,1641,397],{"class":284},[239,1643,512],{"class":280},[239,1645,281],{"class":280},[239,1647,1648],{"class":624}," door",[239,1650,613],{"class":280},[239,1652,295],{"class":294},[239,1654,1655],{"class":249},"http:\u002F\u002F127.0.0.1:3000\u002F_fougere\u002Fcall",[239,1657,301],{"class":294},[239,1659,288],{"class":280},[239,1661,1662],{"class":284},"))",[239,1664,304],{"class":280},[239,1666,1668,1670,1672,1674,1677,1679],{"class":241,"line":1667},7,[239,1669,541],{"class":276},[239,1671,371],{"class":284},[239,1673,400],{"class":280},[239,1675,1676],{"class":323},"dispose",[239,1678,326],{"class":284},[239,1680,304],{"class":280},[230,1682,1684],{"className":232,"code":1683,"language":234,"meta":235,"style":235},"k6 run load.js\n",[199,1685,1686],{"__ignoreMap":235},[239,1687,1688,1691,1693],{"class":241,"line":242},[239,1689,1690],{"class":245},"k6",[239,1692,895],{"class":249},[239,1694,1695],{"class":249}," load.js\n",[196,1697,1698,1699,1702,1703,1706],{},"The generated scenario covers ",[206,1700,1701],{},"every"," operation the app serves, with a valid body for\neach, and its JSON-RPC envelope comes from ",[199,1704,1705],{},"frameCall"," rather than a hand copy that will\ngo on claiming to be JSON-RPC the day the format moves.",[196,1708,1709,1710,1713],{},"What stays yours: the weights, the stages and the thresholds. The scan knows which\noperations exist; it knows nothing about the traffic they receive. An operation given\n",[199,1711,1712],{},"weight: 0"," becomes a decision visible in the file instead of an omission nobody can see.",[196,1715,1716],{},"Regenerate rather than patch.",[1718,1719,1720],"style",{},"html pre.shiki code .sbgvK, html code.shiki .sbgvK{--shiki-light:#E2931D;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s_sjI, html code.shiki .s_sjI{--shiki-light:#91B859;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .stzsN, html code.shiki .stzsN{--shiki-light:#91B859;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sutJx, html code.shiki .sutJx{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#6A737D;--shiki-default-font-style:inherit;--shiki-dark:#6A737D;--shiki-dark-font-style:inherit}html pre.shiki code .sVHd0, html code.shiki .sVHd0{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit}html pre.shiki code .sP7_E, html code.shiki .sP7_E{--shiki-light:#39ADB5;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .su5hD, html code.shiki .su5hD{--shiki-light:#90A4AE;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sjJ54, html code.shiki .sjJ54{--shiki-light:#39ADB5;--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sGLFI, html code.shiki .sGLFI{--shiki-light:#6182B8;--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sbsja, html code.shiki .sbsja{--shiki-light:#9C3EDA;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s_hVV, html code.shiki .s_hVV{--shiki-light:#90A4AE;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .smGrS, html code.shiki .smGrS{--shiki-light:#39ADB5;--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s99_P, html code.shiki .s99_P{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#E36209;--shiki-default-font-style:inherit;--shiki-dark:#FFAB70;--shiki-dark-font-style:inherit}html pre.shiki code .skxfh, html code.shiki .skxfh{--shiki-light:#E53935;--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .srdBf, html code.shiki .srdBf{--shiki-light:#F76D47;--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sQzsp, html code.shiki .sQzsp{--shiki-light:#E53935;--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .s9AJx, html code.shiki .s9AJx{--shiki-light:#9C3EDA;--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":235,"searchDepth":273,"depth":273,"links":1722},[1723,1724,1727,1728,1729,1730,1731,1732,1733,1734],{"id":332,"depth":273,"text":333},{"id":490,"depth":273,"text":491,"children":1725},[1726],{"id":646,"depth":307,"text":647},{"id":675,"depth":273,"text":676},{"id":808,"depth":273,"text":809},{"id":998,"depth":273,"text":999},{"id":1064,"depth":273,"text":105},{"id":1244,"depth":273,"text":1245},{"id":1341,"depth":273,"text":1342},{"id":1483,"depth":273,"text":1484},{"id":1543,"depth":273,"text":1544},"Cases derived from the declaration, doubles derived from the port, and a gradient the test file never states — where a test sits says what it is about.","md",{},{"title":183,"description":1735},"JNFXtD18zzLJ6sqogjimASX7TfTb59aqdc5l2DrJd6U",1788560231400]