[{"data":1,"prerenderedAt":775},["ShallowReactive",2],{"cycle-doc-design-c133-publish.md":3},{"cycle":4,"project":7,"path":10,"title":11,"toc":12,"ast":19},{"id":5,"title":6},"c133","Remediation Training",{"id":8,"label":9},"design","Design","publish.md","c133 · What the apps read: the training API (Phase 3, 2026-11-11)",[13,14,15,16,17,18],"The two routes","Slots: what the app fills","Versions and the hash (D26)","Languages (D11)","What is not in it","Delivery",{"type":20,"children":21},"root",[22,59,65,87,100,130,138,183,220,225,246,518,523,528,659,664,685,690,710,715,770],{"type":23,"tag":24,"props":25,"children":26},"element","p",{},[27,34,36,41,43,50,52,57],{"type":23,"tag":28,"props":29,"children":30},"strong",{},[31],{"type":32,"value":33},"text","For:",{"type":32,"value":35}," c134 (the web app's training page and the record), c131 (the native app's notice and, under D5, the content module), Tyler (event-shape.md §6 Q5). ",{"type":23,"tag":28,"props":37,"children":38},{},[39],{"type":32,"value":40},"Ruling:",{"type":32,"value":42}," D21, published trainings are served by the design site's API like the brand API (",{"type":23,"tag":44,"props":45,"children":47},"code",{"className":46},[],[48],{"type":32,"value":49},"server\u002Fapi\u002Fbrand\u002F*",{"type":32,"value":51},"), with the content collection as the source. ",{"type":23,"tag":28,"props":53,"children":54},{},[55],{"type":32,"value":56},"Status:",{"type":32,"value":58}," built in the lab and served by the dev server; the hash follows D26 (ruled 2026-11-11 while Phase 3 was built); the static deploy prerenders these routes through the preview page's links (not yet checked on a deploy).",{"type":23,"tag":60,"props":61,"children":63},"h2",{"id":62},"the-two-routes",[64],{"type":32,"value":13},{"type":23,"tag":24,"props":66,"children":67},{},[68,77,79,85],{"type":23,"tag":28,"props":69,"children":70},{},[71],{"type":23,"tag":44,"props":72,"children":74},{"className":73},[],[75],{"type":32,"value":76},"GET \u002Fapi\u002Ftrainings",{"type":32,"value":78}," every published training, one entry per training and language; ",{"type":23,"tag":44,"props":80,"children":82},{"className":81},[],[83],{"type":32,"value":84},"?lang=en",{"type":32,"value":86}," narrows to a language.",{"type":23,"tag":88,"props":89,"children":95},"pre",{"className":90,"code":91,"language":92,"meta":93,"style":94},"language-json","{\n  \"source\": \"design site · app\u002Fdata\u002Ftrainings (c133 D4, D21)\",\n  \"count\": 3,\n  \"trainings\": [\n    {\n      \"id\": \"T-MCP-1\", \"slug\": \"unapproved-mcp-servers\", \"title\": \"Unapproved MCP servers\",\n      \"policyId\": \"P-MCP\", \"minutes\": 3, \"version\": 1, \"lang\": \"en\",\n      \"contentHash\": \"sha256:7f6e1737e972ea6a38e72cfa6ef2f75fc58e7d51f0b2e8341644328fc51812b3\",\n      \"steps\": 4, \"questions\": 4,\n      \"slots\": [\"agent\", \"blocked\", \"date\", \"device\", \"evidence\", \"…\"],\n      \"href\": \"\u002Fapi\u002Ftrainings\u002FT-MCP-1\"\n    }\n  ],\n  \"withheld\": [{ \"path\": \"app\u002Fdata\u002Ftrainings\u002Fen\u002F\u003Cslug>.md\", \"problems\": [\"…\"] }]\n}\n","json","","undefined",[96],{"type":23,"tag":44,"props":97,"children":98},{"__ignoreMap":93},[99],{"type":32,"value":91},{"type":23,"tag":24,"props":101,"children":102},{},[103,112,114,120,122,128],{"type":23,"tag":28,"props":104,"children":105},{},[106],{"type":23,"tag":44,"props":107,"children":109},{"className":108},[],[110],{"type":32,"value":111},"GET \u002Fapi\u002Ftrainings\u002F\u003Cid>",{"type":32,"value":113}," one training, whole; ",{"type":23,"tag":44,"props":115,"children":117},{"className":116},[],[118],{"type":32,"value":119},"?lang=",{"type":32,"value":121}," picks the language (default ",{"type":23,"tag":44,"props":123,"children":125},{"className":124},[],[126],{"type":32,"value":127},"en",{"type":32,"value":129},"). 404 when there is no such training in that language; 422 when the file exists but is withheld, with its problems.",{"type":23,"tag":88,"props":131,"children":133},{"className":90,"code":132,"language":92,"meta":93,"style":94},"{\n  \"training\": {\n    \"id\": \"T-MCP-1\", \"slug\": \"…\", \"title\": \"…\", \"policyId\": \"P-MCP\", \"minutes\": 3, \"version\": 1, \"lang\": \"en\",\n    \"assignment\": { \"happened\": \"On {date}, {agent} on {device} …; {blocked}.\", \"matters\": \"A {minutesWord} training …\" },\n    \"lesson\": [\n      { \"id\": \"what-happened\", \"heading\": \"What happened on {device}\", \"body\": [\"…\", \"…\"],\n        \"illustration\": { \"scene\": \"connection\", \"alt\": \"…\", \"animated\": true } },\n      { \"id\": \"instead\", \"heading\": \"What to do instead\", \"instead\": true, \"body\": [\"…\"], \"illustration\": { \"scene\": \"ask\", \"alt\": \"…\" } }\n    ],\n    \"check\": [\n      { \"id\": \"q-reach\", \"stepId\": \"server-reach\", \"prompt\": \"…\", \"explain\": \"…\",\n        \"choices\": [{ \"id\": \"a\", \"text\": \"…\" }, { \"id\": \"b\", \"text\": \"…\", \"right\": true }, { \"id\": \"c\", \"text\": \"…\" }] }\n    ],\n    \"contact\": { \"owner\": \"{owner}\", \"how\": \"…\" },\n    \"library\": {\n      \"lesson\": [{ \"id\": \"what-happened\", \"heading\": \"What happens when an agent connects a server\", \"body\": [\"…\", \"…\"],\n        \"illustration\": { \"scene\": \"connection\", \"alt\": \"…\", \"animated\": true } }],\n      \"check\": [{ \"id\": \"q-observe\", \"stepId\": \"what-happened\", \"prompt\": \"If the policy observes rather than blocks, …\", \"explain\": \"…\", \"choices\": [\"…\"] }]\n    }\n  },\n  \"version\": 1,\n  \"contentHash\": \"sha256:7f6e1737e972e…\",\n  \"slots\": [\"…\"],\n  \"path\": \"app\u002Fdata\u002Ftrainings\u002Fen\u002Funapproved-mcp-servers.md\"\n}\n",[134],{"type":23,"tag":44,"props":135,"children":136},{"__ignoreMap":93},[137],{"type":32,"value":132},{"type":23,"tag":24,"props":139,"children":140},{},[141,143,149,151,157,159,165,167,173,175,181],{"type":32,"value":142},"A training on a topic (Phase 7, D49) carries ",{"type":23,"tag":44,"props":144,"children":146},{"className":145},[],[147],{"type":32,"value":148},"\"topic\": { \"id\": \"observe-and-enforce\", \"label\": \"Observe and Enforce\" }",{"type":32,"value":150}," where a policy's training carries ",{"type":23,"tag":44,"props":152,"children":154},{"className":153},[],[155],{"type":32,"value":156},"policyId",{"type":32,"value":158},", never both; its ",{"type":23,"tag":44,"props":160,"children":162},{"className":161},[],[163],{"type":32,"value":164},"assignment",{"type":32,"value":166}," has ",{"type":23,"tag":44,"props":168,"children":170},{"className":169},[],[171],{"type":32,"value":172},"matters",{"type":32,"value":174}," only, and the list at ",{"type":23,"tag":44,"props":176,"children":178},{"className":177},[],[179],{"type":32,"value":180},"\u002Fapi\u002Ftrainings",{"type":32,"value":182}," names the topic beside the policy.",{"type":23,"tag":24,"props":184,"children":185},{},[186,188,194,196,202,204,210,212,218],{"type":32,"value":187},"The ",{"type":23,"tag":44,"props":189,"children":191},{"className":190},[],[192],{"type":32,"value":193},"training",{"type":32,"value":195}," object is the ",{"type":23,"tag":44,"props":197,"children":199},{"className":198},[],[200],{"type":32,"value":201},"Training",{"type":32,"value":203}," type in ",{"type":23,"tag":44,"props":205,"children":207},{"className":206},[],[208],{"type":32,"value":209},"app\u002Fdata\u002Ftrainings\u002Ftypes.ts",{"type":32,"value":211},", field for field: the plane's types can be written from it, as qdash's are written from the backend handlers. ",{"type":23,"tag":44,"props":213,"children":215},{"className":214},[],[216],{"type":32,"value":217},"library",{"type":32,"value":219}," (Phase 6, D38) is optional: the training's own words for a reading with no finding, each step or question replacing the one with its id when a person takes it on their own (the library page); an app that has a finding ignores it.",{"type":23,"tag":60,"props":221,"children":223},{"id":222},"slots-what-the-app-fills",[224],{"type":32,"value":14},{"type":23,"tag":24,"props":226,"children":227},{},[228,230,236,238,244],{"type":32,"value":229},"The text arrives with ",{"type":23,"tag":44,"props":231,"children":233},{"className":232},[],[234],{"type":32,"value":235},"{slots}",{"type":32,"value":237}," in it. The app fills them from the finding it is answering, then shows the result; an unknown slot is shown as written and is a bug to report, not to hide. The lab's fills (",{"type":23,"tag":44,"props":239,"children":241},{"className":240},[],[242],{"type":32,"value":243},"app\u002Fdata\u002Ftrainings\u002Fslots.ts",{"type":32,"value":245},") are the reference.",{"type":23,"tag":247,"props":248,"children":249},"table",{},[250,269],{"type":23,"tag":251,"props":252,"children":253},"thead",{},[254],{"type":23,"tag":255,"props":256,"children":257},"tr",{},[258,264],{"type":23,"tag":259,"props":260,"children":261},"th",{},[262],{"type":32,"value":263},"Slot",{"type":23,"tag":259,"props":265,"children":266},{},[267],{"type":32,"value":268},"From the finding's data",{"type":23,"tag":270,"props":271,"children":272},"tbody",{},[273,299,337,354,371,388,412,429,454],{"type":23,"tag":255,"props":274,"children":275},{},[276,294],{"type":23,"tag":277,"props":278,"children":279},"td",{},[280,286,288],{"type":23,"tag":44,"props":281,"children":283},{"className":282},[],[284],{"type":32,"value":285},"date",{"type":32,"value":287},", ",{"type":23,"tag":44,"props":289,"children":291},{"className":290},[],[292],{"type":32,"value":293},"time",{"type":23,"tag":277,"props":295,"children":296},{},[297],{"type":32,"value":298},"the finding's observed time: \"8 October\", \"14:09\" (UTC)",{"type":23,"tag":255,"props":300,"children":301},{},[302,332],{"type":23,"tag":277,"props":303,"children":304},{},[305,311,312,318,319,325,326],{"type":23,"tag":44,"props":306,"children":308},{"className":307},[],[309],{"type":32,"value":310},"agent",{"type":32,"value":287},{"type":23,"tag":44,"props":313,"children":315},{"className":314},[],[316],{"type":32,"value":317},"device",{"type":32,"value":287},{"type":23,"tag":44,"props":320,"children":322},{"className":321},[],[323],{"type":32,"value":324},"team",{"type":32,"value":287},{"type":23,"tag":44,"props":327,"children":329},{"className":328},[],[330],{"type":32,"value":331},"person",{"type":23,"tag":277,"props":333,"children":334},{},[335],{"type":32,"value":336},"the finding's subject",{"type":23,"tag":255,"props":338,"children":339},{},[340,349],{"type":23,"tag":277,"props":341,"children":342},{},[343],{"type":23,"tag":44,"props":344,"children":346},{"className":345},[],[347],{"type":32,"value":348},"session",{"type":23,"tag":277,"props":350,"children":351},{},[352],{"type":32,"value":353},"the session's summary, without the agent's name",{"type":23,"tag":255,"props":355,"children":356},{},[357,366],{"type":23,"tag":277,"props":358,"children":359},{},[360],{"type":23,"tag":44,"props":361,"children":363},{"className":362},[],[364],{"type":32,"value":365},"evidence",{"type":23,"tag":277,"props":367,"children":368},{},[369],{"type":32,"value":370},"qcontrol's evidence line, without its final full stop",{"type":23,"tag":255,"props":372,"children":373},{},[374,383],{"type":23,"tag":277,"props":375,"children":376},{},[377],{"type":23,"tag":44,"props":378,"children":380},{"className":379},[],[381],{"type":32,"value":382},"policy",{"type":23,"tag":277,"props":384,"children":385},{},[386],{"type":32,"value":387},"the policy's name",{"type":23,"tag":255,"props":389,"children":390},{},[391,400],{"type":23,"tag":277,"props":392,"children":393},{},[394],{"type":23,"tag":44,"props":395,"children":397},{"className":396},[],[398],{"type":32,"value":399},"owner",{"type":23,"tag":277,"props":401,"children":402},{},[403,405,410],{"type":32,"value":404},"the policy's owner (the lab reads the last person who changed it; a real ",{"type":23,"tag":44,"props":406,"children":408},{"className":407},[],[409],{"type":32,"value":399},{"type":32,"value":411}," on the policy, c121's model, would be honest)",{"type":23,"tag":255,"props":413,"children":414},{},[415,424],{"type":23,"tag":277,"props":416,"children":417},{},[418],{"type":23,"tag":44,"props":419,"children":421},{"className":420},[],[422],{"type":32,"value":423},"resource",{"type":23,"tag":277,"props":425,"children":426},{},[427],{"type":32,"value":428},"the thing the finding touched",{"type":23,"tag":255,"props":430,"children":431},{},[432,441],{"type":23,"tag":277,"props":433,"children":434},{},[435],{"type":23,"tag":44,"props":436,"children":438},{"className":437},[],[439],{"type":32,"value":440},"minutesWord",{"type":23,"tag":277,"props":442,"children":443},{},[444,446,452],{"type":32,"value":445},"the training's ",{"type":23,"tag":44,"props":447,"children":449},{"className":448},[],[450],{"type":32,"value":451},"minutes",{"type":32,"value":453}," as a word: \"three-minute\"",{"type":23,"tag":255,"props":455,"children":456},{},[457,462],{"type":23,"tag":277,"props":458,"children":459},{},[460],{"type":32,"value":461},"per kind",{"type":23,"tag":277,"props":463,"children":464},{},[465,467,473,474,480,481,487,488,494,495,501,502,508,510,516],{"type":32,"value":466},"MCP server: ",{"type":23,"tag":44,"props":468,"children":470},{"className":469},[],[471],{"type":32,"value":472},"server",{"type":32,"value":287},{"type":23,"tag":44,"props":475,"children":477},{"className":476},[],[478],{"type":32,"value":479},"sanctioned",{"type":32,"value":287},{"type":23,"tag":44,"props":482,"children":484},{"className":483},[],[485],{"type":32,"value":486},"exceptionTeam",{"type":32,"value":287},{"type":23,"tag":44,"props":489,"children":491},{"className":490},[],[492],{"type":32,"value":493},"exceptionSentence",{"type":32,"value":287},{"type":23,"tag":44,"props":496,"children":498},{"className":497},[],[499],{"type":32,"value":500},"outcome",{"type":32,"value":287},{"type":23,"tag":44,"props":503,"children":505},{"className":504},[],[506],{"type":32,"value":507},"blocked",{"type":32,"value":509},"; the Phase 4 kinds are listed in ",{"type":23,"tag":44,"props":511,"children":513},{"className":512},[],[514],{"type":32,"value":515},"slots.ts",{"type":32,"value":517}," and the review notes",{"type":23,"tag":24,"props":519,"children":520},{},[521],{"type":32,"value":522},"Three of these are thin in the real product (review-1 call 3): the sanctioned list for the person's team, the policy's owner, and an exception's reason, which the lesson quotes as the operator wrote it (D12).",{"type":23,"tag":60,"props":524,"children":526},{"id":525},"versions-and-the-hash-d26",[527],{"type":32,"value":15},{"type":23,"tag":529,"props":530,"children":531},"ul",{},[532,555,600,633],{"type":23,"tag":533,"props":534,"children":535},"li",{},[536,545,547,553],{"type":23,"tag":28,"props":537,"children":538},{},[539],{"type":23,"tag":44,"props":540,"children":542},{"className":541},[],[543],{"type":32,"value":544},"version",{"type":32,"value":546}," is written by the author and bumped when the meaning of a step, a question or a right answer changes (",{"type":23,"tag":44,"props":548,"children":550},{"className":549},[],[551],{"type":32,"value":552},"authoring.md",{"type":32,"value":554},"). The API serves the current version only; the text of an older version is the file at the commit that carried it.",{"type":23,"tag":533,"props":556,"children":557},{},[558,567,569,575,577,582,584,590,592,598],{"type":23,"tag":28,"props":559,"children":560},{},[561],{"type":23,"tag":44,"props":562,"children":564},{"className":563},[],[565],{"type":32,"value":566},"contentHash",{"type":32,"value":568}," is ",{"type":23,"tag":44,"props":570,"children":572},{"className":571},[],[573],{"type":32,"value":574},"sha256:",{"type":32,"value":576}," and the SHA-256 of the ",{"type":23,"tag":44,"props":578,"children":580},{"className":579},[],[581],{"type":32,"value":193},{"type":32,"value":583}," object exactly as served, serialised with ",{"type":23,"tag":44,"props":585,"children":587},{"className":586},[],[588],{"type":32,"value":589},"JSON.stringify",{"type":32,"value":591}," (no whitespace, the keys in the order served). An app that reserialises what it received gets the same bytes and the same hash. Any change to the served text moves it, a typo fix included; a change to an author's note in the file does not. One implementation computes it for the API and for the lab's record (",{"type":23,"tag":44,"props":593,"children":595},{"className":594},[],[596],{"type":32,"value":597},"app\u002Fdata\u002Ftrainings\u002Fhash.ts",{"type":32,"value":599},").",{"type":23,"tag":533,"props":601,"children":602},{},[603,608,610,616,618,623,625,631],{"type":23,"tag":28,"props":604,"children":605},{},[606],{"type":32,"value":607},"The record names both",{"type":32,"value":609}," (D26, event-shape.md §8): the lab's record carries ",{"type":23,"tag":44,"props":611,"children":613},{"className":612},[],[614],{"type":32,"value":615},"trainingVersion",{"type":32,"value":617}," and ",{"type":23,"tag":44,"props":619,"children":621},{"className":620},[],[622],{"type":32,"value":566},{"type":32,"value":624}," (",{"type":23,"tag":44,"props":626,"children":628},{"className":627},[],[629],{"type":32,"value":630},"content_hash",{"type":32,"value":632}," on the wire) beside the training's id and title (D25), and its card shows the version and the first twelve hex characters.",{"type":23,"tag":533,"props":634,"children":635},{},[636,641,643,649,651,657],{"type":23,"tag":28,"props":637,"children":638},{},[639],{"type":32,"value":640},"The result's name",{"type":32,"value":642}," (D25): every completion is one grouped record (Mark's sketch: type training, completed, the training's id and name, proof of understanding true). The lab keeps ",{"type":23,"tag":44,"props":644,"children":646},{"className":645},[],[647],{"type":32,"value":648},"result: 'passed'",{"type":32,"value":650},", the only value a record can have under D3; ",{"type":23,"tag":44,"props":652,"children":654},{"className":653},[],[655],{"type":32,"value":656},"proof_of_understanding: true",{"type":32,"value":658}," is the alternative name for the same fact, R3's meaning, and the wire name is Tyler's to pick (event-shape.md §8.4).",{"type":23,"tag":60,"props":660,"children":662},{"id":661},"languages-d11",[663],{"type":32,"value":16},{"type":23,"tag":24,"props":665,"children":666},{},[667,669,675,677,683],{"type":32,"value":668},"English for the pilots. A translation is its own file, ",{"type":23,"tag":44,"props":670,"children":672},{"className":671},[],[673],{"type":32,"value":674},"app\u002Fdata\u002Ftrainings\u002F\u003Clang>\u002F\u003Cslug>.md",{"type":32,"value":676},", with the same id and version; the list returns every language, ",{"type":23,"tag":44,"props":678,"children":680},{"className":679},[],[681],{"type":32,"value":682},"\u002Fapi\u002Ftrainings\u002F\u003Cid>?lang=de",{"type":32,"value":684}," returns one. When the person's language has no file the app shows the English one and says so. Operator text filled into a slot is shown as written, never translated by us.",{"type":23,"tag":60,"props":686,"children":688},{"id":687},"what-is-not-in-it",[689],{"type":32,"value":17},{"type":23,"tag":24,"props":691,"children":692},{},[693,695,701,703,709],{"type":32,"value":694},"No record, no assignment, no person: the API is content, the plane keeps the rest (event-shape.md). Nothing about a person's progress (R4). The right choice is in the data because the web app grades the check (D3); if the plane should grade instead, the API drops ",{"type":23,"tag":44,"props":696,"children":698},{"className":697},[],[699],{"type":32,"value":700},"right",{"type":32,"value":702}," and adds a grading route (a call for Mark in ",{"type":23,"tag":44,"props":704,"children":706},{"className":705},[],[707],{"type":32,"value":708},"review-3.md",{"type":32,"value":599},{"type":23,"tag":60,"props":711,"children":713},{"id":712},"delivery",[714],{"type":32,"value":18},{"type":23,"tag":529,"props":716,"children":717},{},[718,736,746],{"type":23,"tag":533,"props":719,"children":720},{},[721,726,728,734],{"type":23,"tag":28,"props":722,"children":723},{},[724],{"type":32,"value":725},"The web app (D1, c134):",{"type":32,"value":727}," reads ",{"type":23,"tag":44,"props":729,"children":731},{"className":730},[],[732],{"type":32,"value":733},"\u002Fapi\u002Ftrainings\u002F\u003Cid>",{"type":32,"value":735}," when it opens the training for a finding, or caches the list at build.",{"type":23,"tag":533,"props":737,"children":738},{},[739,744],{"type":23,"tag":28,"props":740,"children":741},{},[742],{"type":32,"value":743},"The native app (D5, c131):",{"type":32,"value":745}," the same JSON is the payload of a content module fetched from the control plane, so a training updates without an app release.",{"type":23,"tag":533,"props":747,"children":748},{},[749,754,756,761,763,768],{"type":23,"tag":28,"props":750,"children":751},{},[752],{"type":32,"value":753},"The static site:",{"type":32,"value":755}," the preview page links ",{"type":23,"tag":44,"props":757,"children":759},{"className":758},[],[760],{"type":32,"value":180},{"type":32,"value":762}," and each ",{"type":23,"tag":44,"props":764,"children":766},{"className":765},[],[767],{"type":32,"value":733},{"type":32,"value":769}," without an extension, so the deploy crawler prerenders them (CLAUDE.md: extension-bearing paths are skipped).",{"type":23,"tag":771,"props":772,"children":773},"style",{},[774],{"type":32,"value":93},1794599293156]