[{"data":1,"prerenderedAt":2804},["ShallowReactive",2],{"docs-sections":3,"docs-page-\u002Fdocs\u002Fpatterns":123},[4,9,13,17,22,26,30,34,38,42,46,50,55,59,63,67,72,76,80,85,89,93,97,102,106,110,114,119],{"path":5,"title":6,"description":7,"group":8},"\u002Fdocs","Introduction","What tunables is, the problem it solves, and the principles it holds to.","Get started",{"path":10,"title":11,"description":12,"group":8},"\u002Fdocs\u002Fquickstart","Quickstart","Take one group of settings from constants to a screen with a history, in a Nuxt app on Postgres.",{"path":14,"title":15,"description":16,"group":8},"\u002Fdocs\u002Fpatterns","What it is for","The kinds of value tunables keeps — live limits, plans, per-workspace and per-person values, model choices, values that bound each other, costly switches — and the feature that does each.",{"path":18,"title":19,"description":20,"group":21},"\u002Fdocs\u002Fsettings-and-groups","Settings and groups","Declare each setting once, in a group edited beside what it affects, and read it with types.","Concepts",{"path":23,"title":24,"description":25,"group":21},"\u002Fdocs\u002Fkinds-and-units","Kinds and units","The eight kinds of setting, the built-in units, your own units, and how values are checked and read.",{"path":27,"title":28,"description":29,"group":21},"\u002Fdocs\u002Fchecks-and-guards","Checks and guards","Rules across settings, guards for what your app keeps, within for presets, confirmations for costly changes, and notes.",{"path":31,"title":32,"description":33,"group":21},"\u002Fdocs\u002Fhistory-and-restore","History and restore","Every change is a revision with who, when, why and each value before and after; restoring one runs the same checks as any save.",{"path":35,"title":36,"description":37,"group":21},"\u002Fdocs\u002Fpresets","Presets","Named, reusable configurations with one default per slot, assigned by reference and bounded by the settings.",{"path":39,"title":40,"description":41,"group":21},"\u002Fdocs\u002Fscopes","Scopes","Values per workspace and per person, through layers — the system, a plan, the workspace, the person — each bounded by what it inherits.",{"path":43,"title":44,"description":45,"group":21},"\u002Fdocs\u002Fcatalogs","Catalogs","Lists of allowed choices, such as permitted models, whose entries are retired rather than deleted and cannot be retired while in use.",{"path":47,"title":48,"description":49,"group":21},"\u002Fdocs\u002Fdeployment-values","Deployment values","Values a deployment sets in its environment and nobody tunes while the app runs, declared once and listed in .env.example.",{"path":51,"title":52,"description":53,"group":54},"\u002Fdocs\u002Fstorage","Storage","Postgres through your own client, the four tables and how to create them, joining your transactions, and the storage contract behind it.","Integrate",{"path":56,"title":57,"description":58,"group":54},"\u002Fdocs\u002Fserver","Server and access","Mount tunables' API as a fetch handler in Nitro, Hono, Bun or Deno, as Express middleware or as an oRPC router, with access decided by your app.",{"path":60,"title":61,"description":62,"group":54},"\u002Fdocs\u002Faccess","Who can do what","End users bound by values they never see, staff who tune groups, owners and members who change only their own scope, and the terminal.",{"path":64,"title":65,"description":66,"group":54},"\u002Fdocs\u002Ftesting","Testing","Your app's tunables in memory for its own tests, with values set where a test needs them, and how to test rules, guards and confirmations.",{"path":68,"title":69,"description":70,"group":71},"\u002Fdocs\u002Fnuxt-layer","The Nuxt layer","Ready screens for a Nuxt UI app — a group's form, All settings, history, presets, assignments and catalogs — placed beside what they affect.","Screens",{"path":73,"title":74,"description":75,"group":71},"\u002Fdocs\u002Fvue-composables","Vue composables","Headless composables for building tunables' screens in any Vue app, on any design system.",{"path":77,"title":78,"description":79,"group":71},"\u002Fdocs\u002Ftheming","Theming","The screens bring no colors, fonts or CSS of their own; they take your Nuxt UI theme.",{"path":81,"title":82,"description":83,"group":84},"\u002Fdocs\u002Fcli","The CLI","The tunables command, tunables.config.ts, and which commands need a database.","CLI",{"path":86,"title":87,"description":88,"group":84},"\u002Fdocs\u002Fcli-check","Check and generate","The commands that need no database — checking definitions, writing .env.example, and keeping explanations and defaults in step with a pdd-kit Product Model.",{"path":90,"title":91,"description":92,"group":84},"\u002Fdocs\u002Fcli-values","Read and change values","The commands that read and change stored values, through the same checks and history as the screens.",{"path":94,"title":95,"description":96,"group":84},"\u002Fdocs\u002Fcli-skills","Install skills","Install tunables' agent skills for Claude Code, Codex CLI, Cursor, Gemini CLI and GitHub Copilot, and keep them current.",{"path":98,"title":99,"description":100,"group":101},"\u002Fdocs\u002Fskills","Agent skills","Three skills that let a coding agent adopt tunables, define settings the right way and verify that every value has one home.","Skills",{"path":103,"title":104,"description":105,"group":101},"\u002Fdocs\u002Fskill-adopt","tunables-adopt","Bring tunables into an app that has none yet, from an inventory of every value to a wired, synced app with the old homes removed.",{"path":107,"title":108,"description":109,"group":101},"\u002Fdocs\u002Fskill-define","tunables-define","Add, change, rename or remove one value in an app that uses tunables, with its home, its checks, its reads and what happens to stored values.",{"path":111,"title":112,"description":113,"group":101},"\u002Fdocs\u002Fskill-verify","tunables-verify","Audit an app's tunables against their rules, report every finding with where it is and the rule it breaks, and fix only the findings you choose.",{"path":115,"title":116,"description":117,"group":118},"\u002Fdocs\u002Fapi","API reference","Every export of the tunables package, by the subpath it is imported from.","Reference",{"path":120,"title":121,"description":122,"group":118},"\u002Fdocs\u002Ftasklane-example","Tasklane, the example app","A todo app with workspaces and people as tunables — two groups, plans as a layer, a catalog of models, scopes, a rule, a confirmation, and the playground that runs it.",{"id":124,"title":15,"body":125,"description":16,"extension":2799,"group":8,"meta":2800,"navigation":751,"order":215,"path":14,"seo":2801,"stem":2802,"__hash__":2803},"docs\u002Fdocs\u002Fpatterns.md",{"type":126,"value":127,"toc":2786},"minimark",[128,132,142,147,154,413,433,437,443,821,845,849,855,998,1142,1162,1166,1172,1436,1457,1461,1467,1543,1552,1556,1562,1701,1714,1718,1724,1791,1803,1807,1813,2099,2113,2117,2123,2464,2493,2497,2503,2644,2654,2657,2663,2770,2782],[129,130,15],"h1",{"id":131},"what-it-is-for",[133,134,135,136,141],"p",{},"tunables keeps the values someone decides while the app runs. Most apps have\nthe same handful of kinds of them. This page names each, with a short example\nfrom ",[137,138,140],"a",{"href":139},".\u002Ftasklane-example","Tasklane",", a todo app with workspaces and the\npeople in them, and the feature that does it.",[143,144,146],"h2",{"id":145},"limits-staff-tune-live","Limits staff tune live",[133,148,149,153],{},[150,151,152],"em",{},"How many projects a workspace may have. How large an attachment may be.","\nValues that start as constants, get moved to environment variables, and then\nneed changing on a Friday afternoon without a deploy.",[155,156,161],"pre",{"className":157,"code":158,"language":159,"meta":160,"style":160},"language-ts shiki shiki-themes tunables-panel-light tunables-panel-light tunables-panel-dark","export const limits = defineGroup({\n  id: 'limits',\n  title: 'Limits',\n  place: { label: 'Admin › Limits', href: '\u002Fadmin\u002Flimits' },\n  fields: {\n    maxProjects: number({ label: 'Projects', unit: 'count', default: 3, max: 10000 }),\n    attachmentMb: number({ label: 'Attachment size', unit: 'megabytes', default: 10, max: 2048 })\n  }\n})\n","ts","",[162,163,164,191,213,230,271,282,344,401,407],"code",{"__ignoreMap":160},[165,166,169,173,176,180,184,188],"span",{"class":167,"line":168},"line",1,[165,170,172],{"class":171},"sZ4mS","export",[165,174,175],{"class":171}," const",[165,177,179],{"class":178},"sVz71"," limits ",[165,181,183],{"class":182},"sp9ci","=",[165,185,187],{"class":186},"sOThN"," defineGroup",[165,189,190],{"class":182},"({\n",[165,192,194,197,200,203,207,210],{"class":167,"line":193},2,[165,195,196],{"class":178},"  id",[165,198,199],{"class":182},":",[165,201,202],{"class":182}," '",[165,204,206],{"class":205},"sjCzm","limits",[165,208,209],{"class":182},"'",[165,211,212],{"class":182},",\n",[165,214,216,219,221,223,226,228],{"class":167,"line":215},3,[165,217,218],{"class":178},"  title",[165,220,199],{"class":182},[165,222,202],{"class":182},[165,224,225],{"class":205},"Limits",[165,227,209],{"class":182},[165,229,212],{"class":182},[165,231,233,236,238,241,244,246,248,251,253,256,259,261,263,266,268],{"class":167,"line":232},4,[165,234,235],{"class":178},"  place",[165,237,199],{"class":182},[165,239,240],{"class":182}," {",[165,242,243],{"class":178}," label",[165,245,199],{"class":182},[165,247,202],{"class":182},[165,249,250],{"class":205},"Admin › Limits",[165,252,209],{"class":182},[165,254,255],{"class":182},",",[165,257,258],{"class":178}," href",[165,260,199],{"class":182},[165,262,202],{"class":182},[165,264,265],{"class":205},"\u002Fadmin\u002Flimits",[165,267,209],{"class":182},[165,269,270],{"class":182}," },\n",[165,272,274,277,279],{"class":167,"line":273},5,[165,275,276],{"class":178},"  fields",[165,278,199],{"class":182},[165,280,281],{"class":182}," {\n",[165,283,285,288,290,293,296,298,300,302,305,307,309,312,314,316,319,321,323,326,328,331,333,336,338,341],{"class":167,"line":284},6,[165,286,287],{"class":178},"    maxProjects",[165,289,199],{"class":182},[165,291,292],{"class":186}," number",[165,294,295],{"class":182},"({",[165,297,243],{"class":178},[165,299,199],{"class":182},[165,301,202],{"class":182},[165,303,304],{"class":205},"Projects",[165,306,209],{"class":182},[165,308,255],{"class":182},[165,310,311],{"class":178}," unit",[165,313,199],{"class":182},[165,315,202],{"class":182},[165,317,318],{"class":205},"count",[165,320,209],{"class":182},[165,322,255],{"class":182},[165,324,325],{"class":178}," default",[165,327,199],{"class":182},[165,329,330],{"class":205}," 3",[165,332,255],{"class":182},[165,334,335],{"class":178}," max",[165,337,199],{"class":182},[165,339,340],{"class":205}," 10000",[165,342,343],{"class":182}," }),\n",[165,345,347,350,352,354,356,358,360,362,365,367,369,371,373,375,378,380,382,384,386,389,391,393,395,398],{"class":167,"line":346},7,[165,348,349],{"class":178},"    attachmentMb",[165,351,199],{"class":182},[165,353,292],{"class":186},[165,355,295],{"class":182},[165,357,243],{"class":178},[165,359,199],{"class":182},[165,361,202],{"class":182},[165,363,364],{"class":205},"Attachment size",[165,366,209],{"class":182},[165,368,255],{"class":182},[165,370,311],{"class":178},[165,372,199],{"class":182},[165,374,202],{"class":182},[165,376,377],{"class":205},"megabytes",[165,379,209],{"class":182},[165,381,255],{"class":182},[165,383,325],{"class":178},[165,385,199],{"class":182},[165,387,388],{"class":205}," 10",[165,390,255],{"class":182},[165,392,335],{"class":178},[165,394,199],{"class":182},[165,396,397],{"class":205}," 2048",[165,399,400],{"class":182}," })\n",[165,402,404],{"class":167,"line":403},8,[165,405,406],{"class":182},"  }\n",[165,408,410],{"class":167,"line":409},9,[165,411,412],{"class":182},"})\n",[133,414,415,416,420,421,427,428,432],{},"A ",[137,417,419],{"href":418},".\u002Fsettings-and-groups","group",", its form placed beside what it affects\nwith ",[137,422,424],{"href":423},".\u002Fnuxt-layer#tunablesgroupform",[162,425,426],{},"TunablesGroupForm",", and a\n",[137,429,431],{"href":430},".\u002Fhistory-and-restore","revision"," for every change.",[143,434,436],{"id":435},"plans-and-tiers","Plans and tiers",[133,438,439,442],{},[150,440,441],{},"Free gets 3 projects, Pro 50, Team 500."," A plan is a named set of values a\nworkspace is on. Staff edit Pro once and every workspace on Pro has the new\nlimits.",[155,444,446],{"className":157,"code":445,"language":159,"meta":160,"style":160},"export const plans = definePresets({\n  id: 'plans',\n  title: 'Plans',\n  slots: ['plan'],\n  layer: { group: 'limits', scope: 'workspace' },\n  fields: { maxProjects: number({ label: 'Projects', unit: 'count', default: 3 }) },\n  seed: [\n    { key: 'free', slot: 'plan', label: 'Free', values: { maxProjects: 3 } },\n    { key: 'pro', slot: 'plan', label: 'Pro', values: { maxProjects: 50 } }\n  ]\n})\n\nawait tunables.presets('plans').assign('acme', 'plan', 'pro', { actor })\n",[162,447,448,464,479,494,514,550,602,612,675,735,741,746,753],{"__ignoreMap":160},[165,449,450,452,454,457,459,462],{"class":167,"line":168},[165,451,172],{"class":171},[165,453,175],{"class":171},[165,455,456],{"class":178}," plans ",[165,458,183],{"class":182},[165,460,461],{"class":186}," definePresets",[165,463,190],{"class":182},[165,465,466,468,470,472,475,477],{"class":167,"line":193},[165,467,196],{"class":178},[165,469,199],{"class":182},[165,471,202],{"class":182},[165,473,474],{"class":205},"plans",[165,476,209],{"class":182},[165,478,212],{"class":182},[165,480,481,483,485,487,490,492],{"class":167,"line":215},[165,482,218],{"class":178},[165,484,199],{"class":182},[165,486,202],{"class":182},[165,488,489],{"class":205},"Plans",[165,491,209],{"class":182},[165,493,212],{"class":182},[165,495,496,499,501,504,506,509,511],{"class":167,"line":232},[165,497,498],{"class":178},"  slots",[165,500,199],{"class":182},[165,502,503],{"class":182}," [",[165,505,209],{"class":182},[165,507,508],{"class":205},"plan",[165,510,209],{"class":182},[165,512,513],{"class":182},"],\n",[165,515,516,519,521,523,526,528,530,532,534,536,539,541,543,546,548],{"class":167,"line":273},[165,517,518],{"class":178},"  layer",[165,520,199],{"class":182},[165,522,240],{"class":182},[165,524,525],{"class":178}," group",[165,527,199],{"class":182},[165,529,202],{"class":182},[165,531,206],{"class":205},[165,533,209],{"class":182},[165,535,255],{"class":182},[165,537,538],{"class":178}," scope",[165,540,199],{"class":182},[165,542,202],{"class":182},[165,544,545],{"class":205},"workspace",[165,547,209],{"class":182},[165,549,270],{"class":182},[165,551,552,554,556,558,561,563,565,567,569,571,573,575,577,579,581,583,585,587,589,591,593,595,597,600],{"class":167,"line":284},[165,553,276],{"class":178},[165,555,199],{"class":182},[165,557,240],{"class":182},[165,559,560],{"class":178}," maxProjects",[165,562,199],{"class":182},[165,564,292],{"class":186},[165,566,295],{"class":182},[165,568,243],{"class":178},[165,570,199],{"class":182},[165,572,202],{"class":182},[165,574,304],{"class":205},[165,576,209],{"class":182},[165,578,255],{"class":182},[165,580,311],{"class":178},[165,582,199],{"class":182},[165,584,202],{"class":182},[165,586,318],{"class":205},[165,588,209],{"class":182},[165,590,255],{"class":182},[165,592,325],{"class":178},[165,594,199],{"class":182},[165,596,330],{"class":205},[165,598,599],{"class":182}," })",[165,601,270],{"class":182},[165,603,604,607,609],{"class":167,"line":346},[165,605,606],{"class":178},"  seed",[165,608,199],{"class":182},[165,610,611],{"class":182}," [\n",[165,613,614,617,620,622,624,627,629,631,634,636,638,640,642,644,646,648,650,653,655,657,660,662,664,666,668,670,673],{"class":167,"line":403},[165,615,616],{"class":182},"    {",[165,618,619],{"class":178}," key",[165,621,199],{"class":182},[165,623,202],{"class":182},[165,625,626],{"class":205},"free",[165,628,209],{"class":182},[165,630,255],{"class":182},[165,632,633],{"class":178}," slot",[165,635,199],{"class":182},[165,637,202],{"class":182},[165,639,508],{"class":205},[165,641,209],{"class":182},[165,643,255],{"class":182},[165,645,243],{"class":178},[165,647,199],{"class":182},[165,649,202],{"class":182},[165,651,652],{"class":205},"Free",[165,654,209],{"class":182},[165,656,255],{"class":182},[165,658,659],{"class":178}," values",[165,661,199],{"class":182},[165,663,240],{"class":182},[165,665,560],{"class":178},[165,667,199],{"class":182},[165,669,330],{"class":205},[165,671,672],{"class":182}," }",[165,674,270],{"class":182},[165,676,677,679,681,683,685,688,690,692,694,696,698,700,702,704,706,708,710,713,715,717,719,721,723,725,727,730,732],{"class":167,"line":409},[165,678,616],{"class":182},[165,680,619],{"class":178},[165,682,199],{"class":182},[165,684,202],{"class":182},[165,686,687],{"class":205},"pro",[165,689,209],{"class":182},[165,691,255],{"class":182},[165,693,633],{"class":178},[165,695,199],{"class":182},[165,697,202],{"class":182},[165,699,508],{"class":205},[165,701,209],{"class":182},[165,703,255],{"class":182},[165,705,243],{"class":178},[165,707,199],{"class":182},[165,709,202],{"class":182},[165,711,712],{"class":205},"Pro",[165,714,209],{"class":182},[165,716,255],{"class":182},[165,718,659],{"class":178},[165,720,199],{"class":182},[165,722,240],{"class":182},[165,724,560],{"class":178},[165,726,199],{"class":182},[165,728,729],{"class":205}," 50",[165,731,672],{"class":182},[165,733,734],{"class":182}," }\n",[165,736,738],{"class":167,"line":737},10,[165,739,740],{"class":182},"  ]\n",[165,742,744],{"class":167,"line":743},11,[165,745,412],{"class":182},[165,747,749],{"class":167,"line":748},12,[165,750,752],{"emptyLinePlaceholder":751},true,"\n",[165,754,756,759,763,767,770,773,775,777,779,782,784,787,789,791,794,796,798,800,802,804,806,808,810,812,814,816,819],{"class":167,"line":755},13,[165,757,758],{"class":171},"await",[165,760,762],{"class":761},"sq0K3"," tunables",[165,764,766],{"class":765},"sBd0f",".",[165,768,769],{"class":186},"presets",[165,771,772],{"class":182},"(",[165,774,209],{"class":182},[165,776,474],{"class":205},[165,778,209],{"class":182},[165,780,781],{"class":182},")",[165,783,766],{"class":765},[165,785,786],{"class":186},"assign",[165,788,772],{"class":182},[165,790,209],{"class":182},[165,792,793],{"class":205},"acme",[165,795,209],{"class":182},[165,797,255],{"class":182},[165,799,202],{"class":182},[165,801,508],{"class":205},[165,803,209],{"class":182},[165,805,255],{"class":182},[165,807,202],{"class":182},[165,809,687],{"class":205},[165,811,209],{"class":182},[165,813,255],{"class":182},[165,815,240],{"class":182},[165,817,818],{"class":178}," actor ",[165,820,412],{"class":182},[133,822,823,826,827,830,831,834,835,838,839,841,842,766],{},[137,824,36],{"href":825},".\u002Fpresets",", assigned by reference. With ",[162,828,829],{},"layer",", the plan is read\nas a ",[137,832,829],{"href":833},".\u002Fscopes#plans-as-a-layer"," of Limits, so\n",[162,836,837],{},"get('limits', { scope: { workspace: 'acme' } })"," returns Pro's values.\nWithout ",[162,840,829],{},", resolve the preset yourself with\n",[162,843,844],{},"presets('plans').resolve('acme', 'plan')",[143,846,848],{"id":847},"per-workspace-and-per-person-values","Per-workspace and per-person values",[133,850,851,854],{},[150,852,853],{},"Acme wants fewer projects than Pro allows. Dana's week starts on Sunday.","\nEach workspace and person sets its own, within what it inherits.",[155,856,858],{"className":157,"code":857,"language":159,"meta":160,"style":160},"maxProjects: number({ label: 'Projects', unit: 'count', default: 3, override: { workspace: 'at-most' } }),\nweekStartsOn: number({ label: 'Week starts on', unit: 'weekday', default: 1, override: ['workspace', 'user'] }),\n",[162,859,860,926],{"__ignoreMap":160},[165,861,862,865,867,869,871,873,875,877,879,881,883,885,887,889,891,893,895,897,899,901,903,906,908,910,913,915,917,920,922,924],{"class":167,"line":168},[165,863,864],{"class":178},"maxProjects",[165,866,199],{"class":182},[165,868,292],{"class":186},[165,870,295],{"class":182},[165,872,243],{"class":178},[165,874,199],{"class":182},[165,876,202],{"class":182},[165,878,304],{"class":205},[165,880,209],{"class":182},[165,882,255],{"class":182},[165,884,311],{"class":178},[165,886,199],{"class":182},[165,888,202],{"class":182},[165,890,318],{"class":205},[165,892,209],{"class":182},[165,894,255],{"class":182},[165,896,325],{"class":178},[165,898,199],{"class":182},[165,900,330],{"class":205},[165,902,255],{"class":182},[165,904,905],{"class":178}," override",[165,907,199],{"class":182},[165,909,240],{"class":182},[165,911,912],{"class":178}," workspace",[165,914,199],{"class":182},[165,916,202],{"class":182},[165,918,919],{"class":205},"at-most",[165,921,209],{"class":182},[165,923,672],{"class":182},[165,925,343],{"class":182},[165,927,928,931,933,935,937,939,941,943,946,948,950,952,954,956,959,961,963,965,967,970,972,974,976,978,980,982,984,986,988,991,993,996],{"class":167,"line":193},[165,929,930],{"class":178},"weekStartsOn",[165,932,199],{"class":182},[165,934,292],{"class":186},[165,936,295],{"class":182},[165,938,243],{"class":178},[165,940,199],{"class":182},[165,942,202],{"class":182},[165,944,945],{"class":205},"Week starts on",[165,947,209],{"class":182},[165,949,255],{"class":182},[165,951,311],{"class":178},[165,953,199],{"class":182},[165,955,202],{"class":182},[165,957,958],{"class":205},"weekday",[165,960,209],{"class":182},[165,962,255],{"class":182},[165,964,325],{"class":178},[165,966,199],{"class":182},[165,968,969],{"class":205}," 1",[165,971,255],{"class":182},[165,973,905],{"class":178},[165,975,199],{"class":182},[165,977,503],{"class":182},[165,979,209],{"class":182},[165,981,545],{"class":205},[165,983,209],{"class":182},[165,985,255],{"class":182},[165,987,202],{"class":182},[165,989,990],{"class":205},"user",[165,992,209],{"class":182},[165,994,995],{"class":182},"]",[165,997,343],{"class":182},[155,999,1001],{"className":157,"code":1000,"language":159,"meta":160,"style":160},"await tunables.save('limits', { maxProjects: 20 }, { scope: { workspace: 'acme' }, actor: 'Olivia' })\nconst { weekStartsOn } = await tunables.get('workspace', { scope: { workspace: 'acme', user: 'dana' } })\n",[162,1002,1003,1070],{"__ignoreMap":160},[165,1004,1005,1007,1009,1011,1014,1016,1018,1020,1022,1024,1026,1028,1030,1033,1036,1038,1040,1042,1044,1046,1048,1050,1052,1054,1056,1059,1061,1063,1066,1068],{"class":167,"line":168},[165,1006,758],{"class":171},[165,1008,762],{"class":761},[165,1010,766],{"class":765},[165,1012,1013],{"class":186},"save",[165,1015,772],{"class":182},[165,1017,209],{"class":182},[165,1019,206],{"class":205},[165,1021,209],{"class":182},[165,1023,255],{"class":182},[165,1025,240],{"class":182},[165,1027,560],{"class":178},[165,1029,199],{"class":182},[165,1031,1032],{"class":205}," 20",[165,1034,1035],{"class":182}," },",[165,1037,240],{"class":182},[165,1039,538],{"class":178},[165,1041,199],{"class":182},[165,1043,240],{"class":182},[165,1045,912],{"class":178},[165,1047,199],{"class":182},[165,1049,202],{"class":182},[165,1051,793],{"class":205},[165,1053,209],{"class":182},[165,1055,1035],{"class":182},[165,1057,1058],{"class":178}," actor",[165,1060,199],{"class":182},[165,1062,202],{"class":182},[165,1064,1065],{"class":205},"Olivia",[165,1067,209],{"class":182},[165,1069,400],{"class":182},[165,1071,1072,1075,1077,1080,1083,1086,1089,1091,1093,1096,1098,1100,1102,1104,1106,1108,1110,1112,1114,1116,1118,1120,1122,1124,1126,1129,1131,1133,1136,1138,1140],{"class":167,"line":193},[165,1073,1074],{"class":171},"const",[165,1076,240],{"class":182},[165,1078,1079],{"class":178}," weekStartsOn ",[165,1081,1082],{"class":182},"}",[165,1084,1085],{"class":182}," =",[165,1087,1088],{"class":171}," await",[165,1090,762],{"class":761},[165,1092,766],{"class":765},[165,1094,1095],{"class":186},"get",[165,1097,772],{"class":182},[165,1099,209],{"class":182},[165,1101,545],{"class":205},[165,1103,209],{"class":182},[165,1105,255],{"class":182},[165,1107,240],{"class":182},[165,1109,538],{"class":178},[165,1111,199],{"class":182},[165,1113,240],{"class":182},[165,1115,912],{"class":178},[165,1117,199],{"class":182},[165,1119,202],{"class":182},[165,1121,793],{"class":205},[165,1123,209],{"class":182},[165,1125,255],{"class":182},[165,1127,1128],{"class":178}," user",[165,1130,199],{"class":182},[165,1132,202],{"class":182},[165,1134,1135],{"class":205},"dana",[165,1137,209],{"class":182},[165,1139,672],{"class":182},[165,1141,400],{"class":182},[133,1143,1144,1147,1148,1151,1152,1155,1156,766],{},[137,1145,40],{"href":1146},".\u002Fscopes",": ",[162,1149,1150],{},"scopes"," declares the chain, ",[162,1153,1154],{},"override"," says who may\nset a field and how far, and a scoped form lets an owner or a person change\ntheir own, allowed by ",[137,1157,1159],{"href":1158},".\u002Faccess#self-service-per-scope",[162,1160,1161],{},"authorize",[143,1163,1165],{"id":1164},"cost-and-ai-models","Cost and AI models",[133,1167,1168,1171],{},[150,1169,1170],{},"Which model drafts task suggestions, and what one costs."," A list of allowed\nchoices that settings pick from, where an entry still in use cannot be\nremoved.",[155,1173,1175],{"className":157,"code":1174,"language":159,"meta":160,"style":160},"export const models = defineCatalog({\n  id: 'models',\n  title: 'Models',\n  fields: { label: text({ label: 'Label', default: '' }), centsPerSuggestion: number({ label: 'Cost a suggestion', unit: 'count', default: 1 }) },\n  seed: [{ id: 'small', values: { label: 'Small and quick', centsPerSuggestion: 1 } }]\n})\n\nsuggestionModel: pick({ label: 'Suggestion model', catalog: 'models', default: 'small', affects: ['cost'] })\n",[162,1176,1177,1193,1208,1223,1307,1360,1364,1368],{"__ignoreMap":160},[165,1178,1179,1181,1183,1186,1188,1191],{"class":167,"line":168},[165,1180,172],{"class":171},[165,1182,175],{"class":171},[165,1184,1185],{"class":178}," models ",[165,1187,183],{"class":182},[165,1189,1190],{"class":186}," defineCatalog",[165,1192,190],{"class":182},[165,1194,1195,1197,1199,1201,1204,1206],{"class":167,"line":193},[165,1196,196],{"class":178},[165,1198,199],{"class":182},[165,1200,202],{"class":182},[165,1202,1203],{"class":205},"models",[165,1205,209],{"class":182},[165,1207,212],{"class":182},[165,1209,1210,1212,1214,1216,1219,1221],{"class":167,"line":215},[165,1211,218],{"class":178},[165,1213,199],{"class":182},[165,1215,202],{"class":182},[165,1217,1218],{"class":205},"Models",[165,1220,209],{"class":182},[165,1222,212],{"class":182},[165,1224,1225,1227,1229,1231,1233,1235,1238,1240,1242,1244,1246,1249,1251,1253,1255,1257,1260,1263,1266,1268,1270,1272,1274,1276,1278,1281,1283,1285,1287,1289,1291,1293,1295,1297,1299,1301,1303,1305],{"class":167,"line":232},[165,1226,276],{"class":178},[165,1228,199],{"class":182},[165,1230,240],{"class":182},[165,1232,243],{"class":178},[165,1234,199],{"class":182},[165,1236,1237],{"class":186}," text",[165,1239,295],{"class":182},[165,1241,243],{"class":178},[165,1243,199],{"class":182},[165,1245,202],{"class":182},[165,1247,1248],{"class":205},"Label",[165,1250,209],{"class":182},[165,1252,255],{"class":182},[165,1254,325],{"class":178},[165,1256,199],{"class":182},[165,1258,1259],{"class":182}," ''",[165,1261,1262],{"class":182}," }),",[165,1264,1265],{"class":178}," centsPerSuggestion",[165,1267,199],{"class":182},[165,1269,292],{"class":186},[165,1271,295],{"class":182},[165,1273,243],{"class":178},[165,1275,199],{"class":182},[165,1277,202],{"class":182},[165,1279,1280],{"class":205},"Cost a suggestion",[165,1282,209],{"class":182},[165,1284,255],{"class":182},[165,1286,311],{"class":178},[165,1288,199],{"class":182},[165,1290,202],{"class":182},[165,1292,318],{"class":205},[165,1294,209],{"class":182},[165,1296,255],{"class":182},[165,1298,325],{"class":178},[165,1300,199],{"class":182},[165,1302,969],{"class":205},[165,1304,599],{"class":182},[165,1306,270],{"class":182},[165,1308,1309,1311,1313,1316,1319,1321,1323,1326,1328,1330,1332,1334,1336,1338,1340,1342,1345,1347,1349,1351,1353,1355,1357],{"class":167,"line":273},[165,1310,606],{"class":178},[165,1312,199],{"class":182},[165,1314,1315],{"class":182}," [{",[165,1317,1318],{"class":178}," id",[165,1320,199],{"class":182},[165,1322,202],{"class":182},[165,1324,1325],{"class":205},"small",[165,1327,209],{"class":182},[165,1329,255],{"class":182},[165,1331,659],{"class":178},[165,1333,199],{"class":182},[165,1335,240],{"class":182},[165,1337,243],{"class":178},[165,1339,199],{"class":182},[165,1341,202],{"class":182},[165,1343,1344],{"class":205},"Small and quick",[165,1346,209],{"class":182},[165,1348,255],{"class":182},[165,1350,1265],{"class":178},[165,1352,199],{"class":182},[165,1354,969],{"class":205},[165,1356,672],{"class":182},[165,1358,1359],{"class":182}," }]\n",[165,1361,1362],{"class":167,"line":284},[165,1363,412],{"class":182},[165,1365,1366],{"class":167,"line":346},[165,1367,752],{"emptyLinePlaceholder":751},[165,1369,1370,1373,1375,1378,1380,1382,1384,1386,1389,1391,1393,1396,1398,1400,1402,1404,1406,1408,1410,1412,1414,1416,1418,1421,1423,1425,1427,1430,1432,1434],{"class":167,"line":403},[165,1371,1372],{"class":178},"suggestionModel",[165,1374,199],{"class":182},[165,1376,1377],{"class":186}," pick",[165,1379,295],{"class":182},[165,1381,243],{"class":178},[165,1383,199],{"class":182},[165,1385,202],{"class":182},[165,1387,1388],{"class":205},"Suggestion model",[165,1390,209],{"class":182},[165,1392,255],{"class":182},[165,1394,1395],{"class":178}," catalog",[165,1397,199],{"class":182},[165,1399,202],{"class":182},[165,1401,1203],{"class":205},[165,1403,209],{"class":182},[165,1405,255],{"class":182},[165,1407,325],{"class":178},[165,1409,199],{"class":182},[165,1411,202],{"class":182},[165,1413,1325],{"class":205},[165,1415,209],{"class":182},[165,1417,255],{"class":182},[165,1419,1420],{"class":178}," affects",[165,1422,199],{"class":182},[165,1424,503],{"class":182},[165,1426,209],{"class":182},[165,1428,1429],{"class":205},"cost",[165,1431,209],{"class":182},[165,1433,995],{"class":182},[165,1435,400],{"class":182},[133,1437,415,1438,1442,1443,1445,1446,1449,1450,1452,1453,1456],{},[137,1439,1441],{"href":1440},".\u002Fcatalogs","catalog",". Retiring ",[150,1444,1344],{}," while the system or any\nworkspace picks it is refused: ",[150,1447,1448],{},"\"Small and quick is in use by Workspace ›\nSuggestion model; move them to another model first.\""," Mark the effect ",[162,1451,1429],{},"\nwith ",[162,1454,1455],{},"warn: true"," and the screens flag every field that spends money.",[143,1458,1460],{"id":1459},"settings-that-bound-each-other","Settings that bound each other",[133,1462,1463,1466],{},[150,1464,1465],{},"Due soon may not be longer than the time before done tasks are archived.","\nTwo settings that only make sense together.",[155,1468,1470],{"className":157,"code":1469,"language":159,"meta":160,"style":160},"rules: values => values.dueSoonDays > values.archiveAfterDays\n  ? [{ key: 'dueSoonDays', message: 'Due soon may not be longer than the time before done tasks are archived.' }]\n  : []\n",[162,1471,1472,1502,1535],{"__ignoreMap":160},[165,1473,1474,1477,1479,1482,1485,1487,1489,1492,1495,1497,1499],{"class":167,"line":168},[165,1475,1476],{"class":178},"rules",[165,1478,199],{"class":182},[165,1480,1481],{"class":178}," values ",[165,1483,1484],{"class":171},"=>",[165,1486,659],{"class":178},[165,1488,766],{"class":182},[165,1490,1491],{"class":178},"dueSoonDays ",[165,1493,1494],{"class":182},">",[165,1496,659],{"class":178},[165,1498,766],{"class":182},[165,1500,1501],{"class":178},"archiveAfterDays\n",[165,1503,1504,1507,1509,1511,1513,1515,1518,1520,1522,1525,1527,1529,1531,1533],{"class":167,"line":193},[165,1505,1506],{"class":182},"  ?",[165,1508,1315],{"class":182},[165,1510,619],{"class":178},[165,1512,199],{"class":182},[165,1514,202],{"class":182},[165,1516,1517],{"class":205},"dueSoonDays",[165,1519,209],{"class":182},[165,1521,255],{"class":182},[165,1523,1524],{"class":178}," message",[165,1526,199],{"class":182},[165,1528,202],{"class":182},[165,1530,1465],{"class":205},[165,1532,209],{"class":182},[165,1534,1359],{"class":182},[165,1536,1537,1540],{"class":167,"line":215},[165,1538,1539],{"class":182},"  :",[165,1541,1542],{"class":182}," []\n",[133,1544,1545,1546,1551],{},"A group's ",[137,1547,1549],{"href":1548},".\u002Fchecks-and-guards#rules-across-settings",[162,1550,1476],{},", run on the\nwhole group as it would be after the change, at whatever scope the change is\nmade.",[143,1553,1555],{"id":1554},"presets-bounded-by-settings","Presets bounded by settings",[133,1557,1558,1561],{},[150,1559,1560],{},"No plan may offer more projects than the system's hard ceiling."," A preset is\nchecked against settings, and a setting cannot be lowered below a preset.",[155,1563,1565],{"className":157,"code":1564,"language":159,"meta":160,"style":160},"within: (values, settings) => {\n  const { projectCeiling } = settings.get('ceilings') as { projectCeiling: number }\n  return values.maxProjects > projectCeiling ? [`its Projects, ${values.maxProjects}, is above the ceiling of ${projectCeiling}.`] : []\n}\n",[162,1566,1567,1592,1637,1696],{"__ignoreMap":160},[165,1568,1569,1572,1574,1577,1580,1582,1585,1587,1590],{"class":167,"line":168},[165,1570,1571],{"class":178},"within",[165,1573,199],{"class":182},[165,1575,1576],{"class":182}," (",[165,1578,1579],{"class":178},"values",[165,1581,255],{"class":182},[165,1583,1584],{"class":178}," settings",[165,1586,781],{"class":182},[165,1588,1589],{"class":171}," =>",[165,1591,281],{"class":182},[165,1593,1594,1597,1599,1602,1604,1606,1608,1610,1612,1614,1616,1619,1621,1623,1626,1628,1631,1633,1635],{"class":167,"line":193},[165,1595,1596],{"class":171},"  const",[165,1598,240],{"class":182},[165,1600,1601],{"class":178}," projectCeiling ",[165,1603,1082],{"class":182},[165,1605,1085],{"class":182},[165,1607,1584],{"class":761},[165,1609,766],{"class":765},[165,1611,1095],{"class":186},[165,1613,772],{"class":182},[165,1615,209],{"class":182},[165,1617,1618],{"class":205},"ceilings",[165,1620,209],{"class":182},[165,1622,781],{"class":182},[165,1624,1625],{"class":171}," as",[165,1627,240],{"class":182},[165,1629,1630],{"class":178}," projectCeiling",[165,1632,199],{"class":182},[165,1634,292],{"class":186},[165,1636,734],{"class":182},[165,1638,1639,1642,1644,1646,1649,1651,1653,1656,1658,1661,1664,1667,1669,1671,1673,1675,1678,1680,1683,1685,1687,1689,1691,1694],{"class":167,"line":215},[165,1640,1641],{"class":171},"  return",[165,1643,659],{"class":178},[165,1645,766],{"class":182},[165,1647,1648],{"class":178},"maxProjects ",[165,1650,1494],{"class":182},[165,1652,1601],{"class":178},[165,1654,1655],{"class":182},"?",[165,1657,503],{"class":182},[165,1659,1660],{"class":182},"`",[165,1662,1663],{"class":205},"its Projects, ",[165,1665,1666],{"class":182},"${",[165,1668,1579],{"class":178},[165,1670,766],{"class":182},[165,1672,864],{"class":178},[165,1674,1082],{"class":182},[165,1676,1677],{"class":205},", is above the ceiling of ",[165,1679,1666],{"class":182},[165,1681,1682],{"class":178},"projectCeiling",[165,1684,1082],{"class":182},[165,1686,766],{"class":205},[165,1688,1660],{"class":182},[165,1690,995],{"class":182},[165,1692,1693],{"class":182}," :",[165,1695,1542],{"class":182},[165,1697,1698],{"class":167,"line":232},[165,1699,1700],{"class":182},"}\n",[133,1702,1703,1704,1709,1710,1713],{},"A preset set's ",[137,1705,1707],{"href":1706},".\u002Fpresets#within",[162,1708,1571],{},". Lowering the ceiling below\nTeam's 500 is refused: ",[150,1711,1712],{},"\"Plan \"Team\": its Projects, 500, is above the ceiling\nof 400.\""," Plans that are a layer are also held by every workspace's own\nvalues: editing Pro below what Acme keeps is refused, naming Acme.",[143,1715,1717],{"id":1716},"changes-with-consequences","Changes with consequences",[133,1719,1720,1723],{},[150,1721,1722],{},"Turning on Guests shows every task in a project to people outside the\nworkspace."," Allowed, but asked first.",[155,1725,1727],{"className":157,"code":1726,"language":159,"meta":160,"style":160},"confirm: (next, previous) => next.guestAccess && !previous.guestAccess\n  ? 'Guests will see every task in the project they are invited to, including its comments and attachments.'\n  : undefined\n",[162,1728,1729,1772,1784],{"__ignoreMap":160},[165,1730,1731,1734,1736,1738,1741,1743,1746,1748,1750,1753,1755,1758,1761,1764,1767,1769],{"class":167,"line":168},[165,1732,1733],{"class":178},"confirm",[165,1735,199],{"class":182},[165,1737,1576],{"class":182},[165,1739,1740],{"class":178},"next",[165,1742,255],{"class":182},[165,1744,1745],{"class":178}," previous",[165,1747,781],{"class":182},[165,1749,1589],{"class":171},[165,1751,1752],{"class":178}," next",[165,1754,766],{"class":182},[165,1756,1757],{"class":178},"guestAccess ",[165,1759,1760],{"class":182},"&&",[165,1762,1763],{"class":182}," !",[165,1765,1766],{"class":178},"previous",[165,1768,766],{"class":182},[165,1770,1771],{"class":178},"guestAccess\n",[165,1773,1774,1776,1778,1781],{"class":167,"line":193},[165,1775,1506],{"class":182},[165,1777,202],{"class":182},[165,1779,1780],{"class":205},"Guests will see every task in the project they are invited to, including its comments and attachments.",[165,1782,1783],{"class":182},"'\n",[165,1785,1786,1788],{"class":167,"line":215},[165,1787,1539],{"class":182},[165,1789,1790],{"class":171}," undefined\n",[133,1792,1793,1798,1799,1802],{},[137,1794,1796],{"href":1795},".\u002Fchecks-and-guards#confirmations-for-costly-changes",[162,1797,1733],{},": the form\nshows the question in a dialog, ",[162,1800,1801],{},"tunables set"," asks at the terminal, and\nnothing is saved until the change is sent again confirmed.",[143,1804,1806],{"id":1805},"guards-that-check-your-apps-own-data","Guards that check your app's own data",[133,1808,1809,1812],{},[150,1810,1811],{},"Lowering Tasks per project below what a project already holds strands it.","\nThe check needs your app's tables, not tunables'.",[155,1814,1816],{"className":157,"code":1815,"language":159,"meta":160,"style":160},"const projectsWithin: Guard = async ({ next, scope, host }) => {\n  const db = (host as Sql | undefined) ?? sql\n  const over = await db`select name, open_tasks from projects\n    where open_tasks > ${Number(next.maxTasksPerProject)} and (${scope.workspace ?? null}::text is null or workspace = ${scope.workspace ?? null})`\n  return over.map(project => `Project \"${project.name}\" has ${project.open_tasks} open tasks, above ${String(next.maxTasksPerProject)}.`)\n}\n\ncreateTunables({ store, groups, guards: { limits: [projectsWithin] } })\n",[162,1817,1818,1856,1890,1909,1973,2051,2055,2059],{"__ignoreMap":160},[165,1819,1820,1822,1825,1827,1830,1832,1835,1838,1840,1842,1844,1846,1849,1852,1854],{"class":167,"line":168},[165,1821,1074],{"class":171},[165,1823,1824],{"class":186}," projectsWithin",[165,1826,199],{"class":182},[165,1828,1829],{"class":186}," Guard",[165,1831,1085],{"class":182},[165,1833,1834],{"class":171}," async",[165,1836,1837],{"class":182}," ({",[165,1839,1752],{"class":178},[165,1841,255],{"class":182},[165,1843,538],{"class":178},[165,1845,255],{"class":182},[165,1847,1848],{"class":178}," host ",[165,1850,1851],{"class":182},"})",[165,1853,1589],{"class":171},[165,1855,281],{"class":182},[165,1857,1858,1860,1863,1865,1867,1870,1873,1876,1879,1882,1884,1887],{"class":167,"line":193},[165,1859,1596],{"class":171},[165,1861,1862],{"class":178}," db ",[165,1864,183],{"class":182},[165,1866,1576],{"class":182},[165,1868,1869],{"class":178},"host ",[165,1871,1872],{"class":171},"as",[165,1874,1875],{"class":186}," Sql",[165,1877,1878],{"class":182}," |",[165,1880,1881],{"class":186}," undefined",[165,1883,781],{"class":182},[165,1885,1886],{"class":182}," ??",[165,1888,1889],{"class":178}," sql\n",[165,1891,1892,1894,1897,1899,1901,1904,1906],{"class":167,"line":215},[165,1893,1596],{"class":171},[165,1895,1896],{"class":178}," over ",[165,1898,183],{"class":182},[165,1900,1088],{"class":171},[165,1902,1903],{"class":186}," db",[165,1905,1660],{"class":182},[165,1907,1908],{"class":205},"select name, open_tasks from projects\n",[165,1910,1911,1914,1916,1919,1921,1923,1925,1928,1930,1932,1935,1937,1940,1942,1944,1946,1949,1951,1954,1956,1958,1960,1962,1964,1966,1968,1970],{"class":167,"line":232},[165,1912,1913],{"class":205},"    where open_tasks > ",[165,1915,1666],{"class":182},[165,1917,1918],{"class":186},"Number",[165,1920,772],{"class":182},[165,1922,1740],{"class":178},[165,1924,766],{"class":182},[165,1926,1927],{"class":178},"maxTasksPerProject",[165,1929,781],{"class":182},[165,1931,1082],{"class":182},[165,1933,1934],{"class":205}," and (",[165,1936,1666],{"class":182},[165,1938,1939],{"class":178},"scope",[165,1941,766],{"class":182},[165,1943,545],{"class":178},[165,1945,1886],{"class":182},[165,1947,1948],{"class":171}," null",[165,1950,1082],{"class":182},[165,1952,1953],{"class":205},"::text is null or workspace = ",[165,1955,1666],{"class":182},[165,1957,1939],{"class":178},[165,1959,766],{"class":182},[165,1961,545],{"class":178},[165,1963,1886],{"class":182},[165,1965,1948],{"class":171},[165,1967,1082],{"class":182},[165,1969,781],{"class":205},[165,1971,1972],{"class":182},"`\n",[165,1974,1975,1977,1980,1982,1985,1987,1990,1992,1995,1998,2000,2003,2005,2008,2010,2013,2015,2017,2019,2022,2024,2027,2029,2032,2034,2036,2038,2040,2042,2044,2046,2048],{"class":167,"line":273},[165,1976,1641],{"class":171},[165,1978,1979],{"class":761}," over",[165,1981,766],{"class":765},[165,1983,1984],{"class":186},"map",[165,1986,772],{"class":182},[165,1988,1989],{"class":178},"project ",[165,1991,1484],{"class":171},[165,1993,1994],{"class":182}," `",[165,1996,1997],{"class":205},"Project \"",[165,1999,1666],{"class":182},[165,2001,2002],{"class":178},"project",[165,2004,766],{"class":182},[165,2006,2007],{"class":178},"name",[165,2009,1082],{"class":182},[165,2011,2012],{"class":205},"\" has ",[165,2014,1666],{"class":182},[165,2016,2002],{"class":178},[165,2018,766],{"class":182},[165,2020,2021],{"class":178},"open_tasks",[165,2023,1082],{"class":182},[165,2025,2026],{"class":205}," open tasks, above ",[165,2028,1666],{"class":182},[165,2030,2031],{"class":186},"String",[165,2033,772],{"class":182},[165,2035,1740],{"class":178},[165,2037,766],{"class":182},[165,2039,1927],{"class":178},[165,2041,781],{"class":182},[165,2043,1082],{"class":182},[165,2045,766],{"class":205},[165,2047,1660],{"class":182},[165,2049,2050],{"class":182},")\n",[165,2052,2053],{"class":167,"line":284},[165,2054,1700],{"class":182},[165,2056,2057],{"class":167,"line":346},[165,2058,752],{"emptyLinePlaceholder":751},[165,2060,2061,2064,2066,2069,2071,2074,2076,2079,2081,2083,2086,2088,2090,2093,2095,2097],{"class":167,"line":403},[165,2062,2063],{"class":186},"createTunables",[165,2065,295],{"class":182},[165,2067,2068],{"class":178}," store",[165,2070,255],{"class":182},[165,2072,2073],{"class":178}," groups",[165,2075,255],{"class":182},[165,2077,2078],{"class":178}," guards",[165,2080,199],{"class":182},[165,2082,240],{"class":182},[165,2084,2085],{"class":178}," limits",[165,2087,199],{"class":182},[165,2089,503],{"class":182},[165,2091,2092],{"class":178},"projectsWithin",[165,2094,995],{"class":182},[165,2096,672],{"class":182},[165,2098,400],{"class":182},[133,2100,2101,2105,2106,2112],{},[137,2102,2104],{"href":2103},".\u002Fchecks-and-guards#guards-for-what-your-app-keeps","Guards",", registered\nper group, run on every save with the scope it is made at. Writes in your app\nthat depend on a limit take ",[137,2107,2109],{"href":2108},".\u002Fstorage#hold",[162,2110,2111],{},"hold",", so a lowering waits\nfor them and its guard sees them.",[143,2114,2116],{"id":2115},"reading-values-in-handlers-and-jobs","Reading values in handlers and jobs",[133,2118,2119,2122],{},[150,2120,2121],{},"A request handler checks the limit for the person asking. A worker sends\nreminders at each person's hour."," Read at the point of use, every time.",[155,2124,2126],{"className":157,"code":2125,"language":159,"meta":160,"style":160},"\u002F\u002F In a request handler: the value for this person.\nconst limits = await tunables.get('limits', { scope: { workspace: person.workspace, user: person.id } })\n\n\u002F\u002F In a long-running job: keep working on the last values read if the database blips.\nconst { reminderHour } = await tunables.get('workspace', { scope, lastGood: true })\n\n\u002F\u002F Inside your own transaction: hold the settings while you write what depends on them.\nawait sql.begin(async (tx) => {\n  const inTx = tunables.within(tx)\n  await inTx.hold()\n  const { maxTasksPerProject } = await inTx.get('limits', { scope: { workspace: project.workspace } })\n  if (project.openTasks >= maxTasksPerProject) throw new Error('This project is full.')\n  await tx`insert into tasks (project_id, title) values (${project.id}, ${title})`\n})\n",[162,2127,2128,2134,2196,2200,2205,2252,2256,2261,2289,2310,2325,2380,2422,2459],{"__ignoreMap":160},[165,2129,2130],{"class":167,"line":168},[165,2131,2133],{"class":2132},"sKllz","\u002F\u002F In a request handler: the value for this person.\n",[165,2135,2136,2138,2140,2142,2144,2146,2148,2150,2152,2154,2156,2158,2160,2162,2164,2166,2168,2170,2172,2175,2177,2179,2181,2183,2185,2187,2189,2192,2194],{"class":167,"line":193},[165,2137,1074],{"class":171},[165,2139,179],{"class":178},[165,2141,183],{"class":182},[165,2143,1088],{"class":171},[165,2145,762],{"class":761},[165,2147,766],{"class":765},[165,2149,1095],{"class":186},[165,2151,772],{"class":182},[165,2153,209],{"class":182},[165,2155,206],{"class":205},[165,2157,209],{"class":182},[165,2159,255],{"class":182},[165,2161,240],{"class":182},[165,2163,538],{"class":178},[165,2165,199],{"class":182},[165,2167,240],{"class":182},[165,2169,912],{"class":178},[165,2171,199],{"class":182},[165,2173,2174],{"class":178}," person",[165,2176,766],{"class":182},[165,2178,545],{"class":178},[165,2180,255],{"class":182},[165,2182,1128],{"class":178},[165,2184,199],{"class":182},[165,2186,2174],{"class":178},[165,2188,766],{"class":182},[165,2190,2191],{"class":178},"id ",[165,2193,1082],{"class":182},[165,2195,400],{"class":182},[165,2197,2198],{"class":167,"line":215},[165,2199,752],{"emptyLinePlaceholder":751},[165,2201,2202],{"class":167,"line":232},[165,2203,2204],{"class":2132},"\u002F\u002F In a long-running job: keep working on the last values read if the database blips.\n",[165,2206,2207,2209,2211,2214,2216,2218,2220,2222,2224,2226,2228,2230,2232,2234,2236,2238,2240,2242,2245,2247,2250],{"class":167,"line":273},[165,2208,1074],{"class":171},[165,2210,240],{"class":182},[165,2212,2213],{"class":178}," reminderHour ",[165,2215,1082],{"class":182},[165,2217,1085],{"class":182},[165,2219,1088],{"class":171},[165,2221,762],{"class":761},[165,2223,766],{"class":765},[165,2225,1095],{"class":186},[165,2227,772],{"class":182},[165,2229,209],{"class":182},[165,2231,545],{"class":205},[165,2233,209],{"class":182},[165,2235,255],{"class":182},[165,2237,240],{"class":182},[165,2239,538],{"class":178},[165,2241,255],{"class":182},[165,2243,2244],{"class":178}," lastGood",[165,2246,199],{"class":182},[165,2248,2249],{"class":171}," true",[165,2251,400],{"class":182},[165,2253,2254],{"class":167,"line":284},[165,2255,752],{"emptyLinePlaceholder":751},[165,2257,2258],{"class":167,"line":346},[165,2259,2260],{"class":2132},"\u002F\u002F Inside your own transaction: hold the settings while you write what depends on them.\n",[165,2262,2263,2265,2268,2270,2273,2275,2278,2280,2283,2285,2287],{"class":167,"line":403},[165,2264,758],{"class":171},[165,2266,2267],{"class":761}," sql",[165,2269,766],{"class":765},[165,2271,2272],{"class":186},"begin",[165,2274,772],{"class":182},[165,2276,2277],{"class":171},"async",[165,2279,1576],{"class":182},[165,2281,2282],{"class":178},"tx",[165,2284,781],{"class":182},[165,2286,1589],{"class":171},[165,2288,281],{"class":182},[165,2290,2291,2293,2296,2298,2300,2302,2304,2306,2308],{"class":167,"line":409},[165,2292,1596],{"class":171},[165,2294,2295],{"class":178}," inTx ",[165,2297,183],{"class":182},[165,2299,762],{"class":761},[165,2301,766],{"class":765},[165,2303,1571],{"class":186},[165,2305,772],{"class":182},[165,2307,2282],{"class":178},[165,2309,2050],{"class":182},[165,2311,2312,2315,2318,2320,2322],{"class":167,"line":737},[165,2313,2314],{"class":171},"  await",[165,2316,2317],{"class":761}," inTx",[165,2319,766],{"class":765},[165,2321,2111],{"class":186},[165,2323,2324],{"class":182},"()\n",[165,2326,2327,2329,2331,2334,2336,2338,2340,2342,2344,2346,2348,2350,2352,2354,2356,2358,2360,2362,2364,2366,2368,2371,2373,2376,2378],{"class":167,"line":743},[165,2328,1596],{"class":171},[165,2330,240],{"class":182},[165,2332,2333],{"class":178}," maxTasksPerProject ",[165,2335,1082],{"class":182},[165,2337,1085],{"class":182},[165,2339,1088],{"class":171},[165,2341,2317],{"class":761},[165,2343,766],{"class":765},[165,2345,1095],{"class":186},[165,2347,772],{"class":182},[165,2349,209],{"class":182},[165,2351,206],{"class":205},[165,2353,209],{"class":182},[165,2355,255],{"class":182},[165,2357,240],{"class":182},[165,2359,538],{"class":178},[165,2361,199],{"class":182},[165,2363,240],{"class":182},[165,2365,912],{"class":178},[165,2367,199],{"class":182},[165,2369,2370],{"class":178}," project",[165,2372,766],{"class":182},[165,2374,2375],{"class":178},"workspace ",[165,2377,1082],{"class":182},[165,2379,400],{"class":182},[165,2381,2382,2385,2387,2389,2391,2394,2397,2400,2402,2405,2408,2411,2413,2415,2418,2420],{"class":167,"line":748},[165,2383,2384],{"class":171},"  if",[165,2386,1576],{"class":182},[165,2388,2002],{"class":178},[165,2390,766],{"class":182},[165,2392,2393],{"class":178},"openTasks ",[165,2395,2396],{"class":182},">=",[165,2398,2399],{"class":178}," maxTasksPerProject",[165,2401,781],{"class":182},[165,2403,2404],{"class":171}," throw",[165,2406,2407],{"class":171}," new",[165,2409,2410],{"class":186}," Error",[165,2412,772],{"class":182},[165,2414,209],{"class":182},[165,2416,2417],{"class":205},"This project is full.",[165,2419,209],{"class":182},[165,2421,2050],{"class":182},[165,2423,2424,2426,2429,2431,2434,2436,2438,2440,2443,2445,2448,2450,2453,2455,2457],{"class":167,"line":755},[165,2425,2314],{"class":171},[165,2427,2428],{"class":186}," tx",[165,2430,1660],{"class":182},[165,2432,2433],{"class":205},"insert into tasks (project_id, title) values (",[165,2435,1666],{"class":182},[165,2437,2002],{"class":178},[165,2439,766],{"class":182},[165,2441,2442],{"class":178},"id",[165,2444,1082],{"class":182},[165,2446,2447],{"class":205},", ",[165,2449,1666],{"class":182},[165,2451,2452],{"class":178},"title",[165,2454,1082],{"class":182},[165,2456,781],{"class":205},[165,2458,1972],{"class":182},[165,2460,2462],{"class":167,"line":2461},14,[165,2463,412],{"class":182},[133,2465,2466,2471,2472,2475,2476,2484,2485,2488,2489,2492],{},[137,2467,2469],{"href":2468},".\u002Fsettings-and-groups#reading-values",[162,2470,1095],{}," returns every field or\nthrows; never write a fallback. ",[162,2473,2474],{},"lastGood"," returns the last values this\nprocess read when a read fails. ",[137,2477,2479,2481,2482],{"href":2478},".\u002Fstorage#transactions",[162,2480,1571],{}," and ",[162,2483,2111],{},"\njoin your transaction; ",[162,2486,2487],{},"committed()"," tells ",[162,2490,2491],{},"onChange"," listeners once it has\ncommitted.",[143,2494,2496],{"id":2495},"deployment-values-are-not-settings","Deployment values are not settings",[133,2498,2499,2502],{},[150,2500,2501],{},"The database URL, a lock id, the log level."," Set per deployment, never tuned\nwhile the app runs.",[155,2504,2506],{"className":157,"code":2505,"language":159,"meta":160,"style":160},"export const environment = defineEnvironment({\n  DATABASE_URL: env.string({ description: 'The Postgres the app and tunables share.' }),\n  TASKLANE_LOG_LEVEL: env.choice({ options: ['debug', 'info', 'warn', 'error'], default: 'info', description: 'How much the server logs.' })\n})\n",[162,2507,2508,2524,2555,2640],{"__ignoreMap":160},[165,2509,2510,2512,2514,2517,2519,2522],{"class":167,"line":168},[165,2511,172],{"class":171},[165,2513,175],{"class":171},[165,2515,2516],{"class":178}," environment ",[165,2518,183],{"class":182},[165,2520,2521],{"class":186}," defineEnvironment",[165,2523,190],{"class":182},[165,2525,2526,2529,2531,2534,2536,2539,2541,2544,2546,2548,2551,2553],{"class":167,"line":193},[165,2527,2528],{"class":178},"  DATABASE_URL",[165,2530,199],{"class":182},[165,2532,2533],{"class":761}," env",[165,2535,766],{"class":765},[165,2537,2538],{"class":186},"string",[165,2540,295],{"class":182},[165,2542,2543],{"class":178}," description",[165,2545,199],{"class":182},[165,2547,202],{"class":182},[165,2549,2550],{"class":205},"The Postgres the app and tunables share.",[165,2552,209],{"class":182},[165,2554,343],{"class":182},[165,2556,2557,2560,2562,2564,2566,2569,2571,2574,2576,2578,2580,2583,2585,2587,2589,2592,2594,2596,2598,2601,2603,2605,2607,2610,2612,2615,2617,2619,2621,2623,2625,2627,2629,2631,2633,2636,2638],{"class":167,"line":215},[165,2558,2559],{"class":178},"  TASKLANE_LOG_LEVEL",[165,2561,199],{"class":182},[165,2563,2533],{"class":761},[165,2565,766],{"class":765},[165,2567,2568],{"class":186},"choice",[165,2570,295],{"class":182},[165,2572,2573],{"class":178}," options",[165,2575,199],{"class":182},[165,2577,503],{"class":182},[165,2579,209],{"class":182},[165,2581,2582],{"class":205},"debug",[165,2584,209],{"class":182},[165,2586,255],{"class":182},[165,2588,202],{"class":182},[165,2590,2591],{"class":205},"info",[165,2593,209],{"class":182},[165,2595,255],{"class":182},[165,2597,202],{"class":182},[165,2599,2600],{"class":205},"warn",[165,2602,209],{"class":182},[165,2604,255],{"class":182},[165,2606,202],{"class":182},[165,2608,2609],{"class":205},"error",[165,2611,209],{"class":182},[165,2613,2614],{"class":182},"],",[165,2616,325],{"class":178},[165,2618,199],{"class":182},[165,2620,202],{"class":182},[165,2622,2591],{"class":205},[165,2624,209],{"class":182},[165,2626,255],{"class":182},[165,2628,2543],{"class":178},[165,2630,199],{"class":182},[165,2632,202],{"class":182},[165,2634,2635],{"class":205},"How much the server logs.",[165,2637,209],{"class":182},[165,2639,400],{"class":182},[165,2641,2642],{"class":167,"line":232},[165,2643,412],{"class":182},[133,2645,2646,2649,2650,2653],{},[137,2647,48],{"href":2648},".\u002Fdeployment-values",", declared once and written to\n",[162,2651,2652],{},".env.example",". A value is one or the other, never both, so there is no\nprecedence between the environment and the database.",[143,2655,65],{"id":2656},"testing",[133,2658,2659,2662],{},[150,2660,2661],{},"A test that needs a workspace at its limit."," Your own definitions, in memory.",[155,2664,2666],{"className":157,"code":2665,"language":159,"meta":160,"style":160},"import { setForTest, testTunables } from 'tunables\u002Ftesting'\n\nconst tunables = await testTunables({ groups, presets, catalogs, effects, scopes })\nawait setForTest(tunables, { limits: { maxProjects: 1 } })\n",[162,2667,2668,2695,2699,2739],{"__ignoreMap":160},[165,2669,2670,2673,2675,2678,2680,2683,2685,2688,2690,2693],{"class":167,"line":168},[165,2671,2672],{"class":171},"import",[165,2674,240],{"class":182},[165,2676,2677],{"class":178}," setForTest",[165,2679,255],{"class":182},[165,2681,2682],{"class":178}," testTunables ",[165,2684,1082],{"class":182},[165,2686,2687],{"class":171}," from",[165,2689,202],{"class":182},[165,2691,2692],{"class":205},"tunables\u002Ftesting",[165,2694,1783],{"class":182},[165,2696,2697],{"class":167,"line":193},[165,2698,752],{"emptyLinePlaceholder":751},[165,2700,2701,2703,2706,2708,2710,2713,2715,2717,2719,2722,2724,2727,2729,2732,2734,2737],{"class":167,"line":215},[165,2702,1074],{"class":171},[165,2704,2705],{"class":178}," tunables ",[165,2707,183],{"class":182},[165,2709,1088],{"class":171},[165,2711,2712],{"class":186}," testTunables",[165,2714,295],{"class":182},[165,2716,2073],{"class":178},[165,2718,255],{"class":182},[165,2720,2721],{"class":178}," presets",[165,2723,255],{"class":182},[165,2725,2726],{"class":178}," catalogs",[165,2728,255],{"class":182},[165,2730,2731],{"class":178}," effects",[165,2733,255],{"class":182},[165,2735,2736],{"class":178}," scopes ",[165,2738,412],{"class":182},[165,2740,2741,2743,2745,2747,2750,2752,2754,2756,2758,2760,2762,2764,2766,2768],{"class":167,"line":232},[165,2742,758],{"class":171},[165,2744,2677],{"class":186},[165,2746,772],{"class":182},[165,2748,2749],{"class":178},"tunables",[165,2751,255],{"class":182},[165,2753,240],{"class":182},[165,2755,2085],{"class":178},[165,2757,199],{"class":182},[165,2759,240],{"class":182},[165,2761,560],{"class":178},[165,2763,199],{"class":182},[165,2765,969],{"class":205},[165,2767,672],{"class":182},[165,2769,400],{"class":182},[133,2771,2772,2777,2778,2781],{},[137,2773,2775],{"href":2774},".\u002Ftesting",[162,2776,2692],{}," syncs your definitions over a memory store;\n",[162,2779,2780],{},"setForTest"," puts values in place past the checks a person would meet.",[2783,2784,2785],"style",{},"html pre.shiki code .sZ4mS, html code.shiki .sZ4mS{--shiki-light:#115E59;--shiki-default:#115E59;--shiki-dark:#5EEAD4}html pre.shiki code .sVz71, html code.shiki .sVz71{--shiki-light:#182025;--shiki-default:#182025;--shiki-dark:#E6EBEC}html pre.shiki code .sp9ci, html code.shiki .sp9ci{--shiki-light:#4F5D63;--shiki-default:#4F5D63;--shiki-dark:#A3AFB4}html pre.shiki code .sOThN, html code.shiki .sOThN{--shiki-light:#2A3439;--shiki-light-font-weight:bold;--shiki-default:#2A3439;--shiki-default-font-weight:bold;--shiki-dark:#F6F8F8;--shiki-dark-font-weight:bold}html pre.shiki code .sjCzm, html code.shiki .sjCzm{--shiki-light:#92400E;--shiki-default:#92400E;--shiki-dark:#FCD34D}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 .sq0K3, html code.shiki .sq0K3{--shiki-light:#182025;--shiki-light-font-weight:bold;--shiki-default:#182025;--shiki-default-font-weight:bold;--shiki-dark:#E6EBEC;--shiki-dark-font-weight:bold}html pre.shiki code .sBd0f, html code.shiki .sBd0f{--shiki-light:#4F5D63;--shiki-light-font-weight:bold;--shiki-default:#4F5D63;--shiki-default-font-weight:bold;--shiki-dark:#A3AFB4;--shiki-dark-font-weight:bold}html pre.shiki code .sKllz, html code.shiki .sKllz{--shiki-light:#4F5D63;--shiki-light-font-style:italic;--shiki-default:#4F5D63;--shiki-default-font-style:italic;--shiki-dark:#A3AFB4;--shiki-dark-font-style:italic}",{"title":160,"searchDepth":193,"depth":193,"links":2787},[2788,2789,2790,2791,2792,2793,2794,2795,2796,2797,2798],{"id":145,"depth":193,"text":146},{"id":435,"depth":193,"text":436},{"id":847,"depth":193,"text":848},{"id":1164,"depth":193,"text":1165},{"id":1459,"depth":193,"text":1460},{"id":1554,"depth":193,"text":1555},{"id":1716,"depth":193,"text":1717},{"id":1805,"depth":193,"text":1806},{"id":2115,"depth":193,"text":2116},{"id":2495,"depth":193,"text":2496},{"id":2656,"depth":193,"text":65},"md",{},{"title":15,"description":16},"docs\u002Fpatterns","L1OaJ2-_iS9qx1QzCyML5K0l4XlhuWKUs77wPfKuy-I",1792845119394]